From c1f9b1f7b1b77776192048005dcc66dcf3df2bfb Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Sat, 27 Dec 2014 15:41:37 +0100 Subject: Update to MediaWiki 1.24.1 --- includes/Action.php | 600 - includes/AjaxDispatcher.php | 25 +- includes/AjaxResponse.php | 74 +- includes/ArrayUtils.php | 69 - includes/Article.php | 2154 -- includes/AuthPlugin.php | 65 +- includes/AutoLoader.php | 757 +- includes/Autopromote.php | 15 +- includes/Block.php | 370 +- includes/CacheHelper.php | 392 - includes/CallableUpdate.php | 30 - includes/Category.php | 57 +- includes/CategoryFinder.php | 244 + includes/CategoryPage.php | 118 - includes/CategoryViewer.php | 203 +- includes/Categoryfinder.php | 233 - includes/Cdb.php | 184 - includes/Cdb_PHP.php | 493 - includes/ChangeTags.php | 147 +- includes/ChangesFeed.php | 221 - includes/Collation.php | 175 +- includes/ConfEditor.php | 1109 - includes/Cookie.php | 59 +- includes/DataUpdate.php | 126 - includes/DefaultSettings.php | 1382 +- includes/DeferredUpdates.php | 129 - includes/Defines.php | 65 +- includes/DeprecatedGlobal.php | 17 +- includes/EditPage.php | 1422 +- includes/Exception.php | 824 - includes/Export.php | 373 +- includes/FakeTitle.php | 141 - includes/Fallback.php | 50 +- includes/Feed.php | 111 +- includes/FeedUtils.php | 52 +- includes/FileDeleteForm.php | 68 +- includes/ForkController.php | 20 +- includes/FormOptions.php | 58 +- includes/GitInfo.php | 323 +- includes/GlobalFunctions.php | 1376 +- includes/HTMLForm.php | 2965 --- includes/HashRing.php | 142 - includes/HistoryBlob.php | 146 +- includes/Hooks.php | 24 +- includes/Html.php | 226 +- includes/HtmlFormatter.php | 97 +- includes/HttpFunctions.php | 127 +- includes/IP.php | 761 - includes/ImagePage.php | 1402 -- includes/ImageQueryPage.php | 74 - includes/Import.php | 518 +- includes/Init.php | 136 - includes/Licenses.php | 58 +- includes/LinkFilter.php | 115 +- includes/Linker.php | 991 +- includes/LinksUpdate.php | 893 - includes/MWCryptRand.php | 497 - includes/MWFunction.php | 61 - includes/MWNamespace.php | 496 + includes/MWTimestamp.php | 422 + includes/MagicWord.php | 175 +- includes/MappedIterator.php | 114 - includes/MediaWiki.php | 704 + includes/MediaWikiVersionFetcher.php | 31 + includes/Message.php | 464 +- includes/MessageBlobStore.php | 106 +- includes/Metadata.php | 204 - includes/MimeMagic.php | 360 +- includes/MovePage.php | 343 + includes/Namespace.php | 436 - includes/OutputHandler.php | 15 +- includes/OutputPage.php | 1761 +- includes/PHPVersionError.php | 29 +- includes/PageQueryPage.php | 50 - includes/Pager.php | 1313 -- includes/PathRouter.php | 28 +- includes/PoolCounter.php | 329 - includes/Preferences.php | 527 +- includes/PrefixSearch.php | 239 +- includes/ProtectionForm.php | 313 +- includes/ProxyTools.php | 86 - includes/QueryPage.php | 834 - includes/Revision.php | 517 +- includes/RevisionList.php | 60 +- includes/Sanitizer.php | 232 +- includes/ScopedCallback.php | 73 - includes/ScopedPHPTimeout.php | 84 - includes/Setup.php | 307 +- includes/SiteConfiguration.php | 74 +- includes/SiteStats.php | 340 +- includes/Skin.php | 1623 -- includes/SkinTemplate.php | 2008 -- includes/SpecialPage.php | 1446 -- includes/SpecialPageFactory.php | 591 - includes/SqlDataUpdate.php | 152 - includes/SquidPurgeClient.php | 65 +- includes/StatCounter.php | 34 +- includes/Status.php | 151 +- includes/StreamFile.php | 16 +- includes/StringUtils.php | 606 - includes/StubObject.php | 115 +- includes/Timestamp.php | 396 - includes/TimestampException.php | 7 + includes/Title.php | 1932 +- includes/TitleArray.php | 67 +- includes/TitleArrayFromResult.php | 86 + includes/UIDGenerator.php | 337 - includes/User.php | 1160 +- includes/UserArray.php | 83 +- includes/UserArrayFromResult.php | 90 + includes/UserMailer.php | 883 - includes/UserRightsProxy.php | 75 +- includes/ViewCountUpdate.php | 105 - includes/WatchedItem.php | 194 +- includes/WebRequest.php | 479 +- includes/WebResponse.php | 41 +- includes/WebStart.php | 62 +- includes/Wiki.php | 701 - includes/WikiCategoryPage.php | 50 - includes/WikiError.php | 154 - includes/WikiFilePage.php | 192 - includes/WikiMap.php | 58 +- includes/WikiPage.php | 3621 ---- includes/Xml.php | 368 +- includes/XmlTypeCheck.php | 264 - includes/ZhClient.php | 164 - includes/ZhConversion.php | 3 +- includes/ZipDirectoryReader.php | 712 - includes/actions/Action.php | 386 + includes/actions/CachedAction.php | 23 +- includes/actions/CreditsAction.php | 51 +- includes/actions/DeleteAction.php | 10 +- includes/actions/EditAction.php | 38 +- includes/actions/FormAction.php | 123 + includes/actions/FormlessAction.php | 45 + includes/actions/HistoryAction.php | 218 +- includes/actions/InfoAction.php | 97 +- includes/actions/MarkpatrolledAction.php | 1 + includes/actions/ProtectAction.php | 29 +- includes/actions/RawAction.php | 97 +- includes/actions/RenderAction.php | 3 - includes/actions/RevertAction.php | 61 +- includes/actions/RevisiondeleteAction.php | 1 + includes/actions/RollbackAction.php | 27 +- includes/actions/SubmitAction.php | 42 + includes/actions/UnprotectAction.php | 43 + includes/actions/UnwatchAction.php | 57 + includes/actions/ViewAction.php | 1 - includes/actions/WatchAction.php | 59 +- includes/api/ApiBase.php | 2337 +- includes/api/ApiBlock.php | 77 +- includes/api/ApiClearHasMsg.php | 58 + includes/api/ApiComparePages.php | 55 +- includes/api/ApiCreateAccount.php | 106 +- includes/api/ApiDelete.php | 91 +- includes/api/ApiDisabled.php | 2 +- includes/api/ApiEditPage.php | 217 +- includes/api/ApiEmailUser.php | 43 +- includes/api/ApiExpandTemplates.php | 120 +- includes/api/ApiFeedContributions.php | 55 +- includes/api/ApiFeedRecentChanges.php | 207 + includes/api/ApiFeedWatchlist.php | 64 +- includes/api/ApiFileRevert.php | 67 +- includes/api/ApiFormatBase.php | 146 +- includes/api/ApiFormatDbg.php | 4 +- includes/api/ApiFormatDump.php | 4 +- includes/api/ApiFormatFeedWrapper.php | 101 + includes/api/ApiFormatJson.php | 13 +- includes/api/ApiFormatPhp.php | 3 +- includes/api/ApiFormatRaw.php | 8 +- includes/api/ApiFormatTxt.php | 4 +- includes/api/ApiFormatWddx.php | 17 +- includes/api/ApiFormatXml.php | 36 +- includes/api/ApiFormatYaml.php | 8 +- includes/api/ApiHelp.php | 32 +- includes/api/ApiImageRotate.php | 42 +- includes/api/ApiImport.php | 50 +- includes/api/ApiLogin.php | 105 +- includes/api/ApiLogout.php | 6 +- includes/api/ApiMain.php | 476 +- includes/api/ApiModuleManager.php | 176 +- includes/api/ApiMove.php | 105 +- includes/api/ApiOpenSearch.php | 34 +- includes/api/ApiOptions.php | 51 +- includes/api/ApiPageSet.php | 207 +- includes/api/ApiParamInfo.php | 93 +- includes/api/ApiParse.php | 278 +- includes/api/ApiPatrol.php | 39 +- includes/api/ApiProtect.php | 99 +- includes/api/ApiPurge.php | 95 +- includes/api/ApiQuery.php | 302 +- includes/api/ApiQueryAllCategories.php | 40 +- includes/api/ApiQueryAllImages.php | 136 +- includes/api/ApiQueryAllLinks.php | 124 +- includes/api/ApiQueryAllMessages.php | 40 +- includes/api/ApiQueryAllPages.php | 63 +- includes/api/ApiQueryAllUsers.php | 175 +- includes/api/ApiQueryBacklinks.php | 83 +- includes/api/ApiQueryBacklinksprop.php | 472 + includes/api/ApiQueryBase.php | 490 +- includes/api/ApiQueryBlocks.php | 147 +- includes/api/ApiQueryCategories.php | 52 +- includes/api/ApiQueryCategoryInfo.php | 42 +- includes/api/ApiQueryCategoryMembers.php | 141 +- includes/api/ApiQueryContributors.php | 282 + includes/api/ApiQueryDeletedrevs.php | 275 +- includes/api/ApiQueryDuplicateFiles.php | 18 +- includes/api/ApiQueryExtLinksUsage.php | 44 +- includes/api/ApiQueryExternalLinks.php | 25 +- includes/api/ApiQueryFileRepoInfo.php | 18 +- includes/api/ApiQueryFilearchive.php | 178 +- includes/api/ApiQueryIWBacklinks.php | 51 +- includes/api/ApiQueryIWLinks.php | 77 +- includes/api/ApiQueryImageInfo.php | 425 +- includes/api/ApiQueryImages.php | 30 +- includes/api/ApiQueryInfo.php | 166 +- includes/api/ApiQueryLangBacklinks.php | 48 +- includes/api/ApiQueryLangLinks.php | 73 +- includes/api/ApiQueryLinks.php | 30 +- includes/api/ApiQueryLogEvents.php | 289 +- includes/api/ApiQueryORM.php | 3 +- includes/api/ApiQueryPagePropNames.php | 7 +- includes/api/ApiQueryPageProps.php | 14 +- includes/api/ApiQueryPagesWithProp.php | 15 +- includes/api/ApiQueryPrefixSearch.php | 124 + includes/api/ApiQueryProtectedTitles.php | 88 +- includes/api/ApiQueryQueryPage.php | 65 +- includes/api/ApiQueryRandom.php | 61 +- includes/api/ApiQueryRecentChanges.php | 446 +- includes/api/ApiQueryRevisions.php | 316 +- includes/api/ApiQuerySearch.php | 165 +- includes/api/ApiQuerySiteinfo.php | 340 +- includes/api/ApiQueryStashImageInfo.php | 20 +- includes/api/ApiQueryTags.php | 25 +- includes/api/ApiQueryTokens.php | 104 + includes/api/ApiQueryUserContributions.php | 262 +- includes/api/ApiQueryUserInfo.php | 114 +- includes/api/ApiQueryUsers.php | 117 +- includes/api/ApiQueryWatchlist.php | 410 +- includes/api/ApiQueryWatchlistRaw.php | 41 +- includes/api/ApiResult.php | 314 +- includes/api/ApiRevisionDelete.php | 236 + includes/api/ApiRollback.php | 126 +- includes/api/ApiRsd.php | 10 +- includes/api/ApiSetNotificationTimestamp.php | 185 +- includes/api/ApiTokens.php | 21 +- includes/api/ApiUnblock.php | 50 +- includes/api/ApiUndelete.php | 58 +- includes/api/ApiUpload.php | 186 +- includes/api/ApiUserrights.php | 51 +- includes/api/ApiWatch.php | 155 +- includes/cache/BacklinkCache.php | 125 +- includes/cache/CacheDependency.php | 230 +- includes/cache/CacheHelper.php | 386 + includes/cache/FileCacheBase.php | 15 +- includes/cache/GenderCache.php | 28 +- includes/cache/HTMLCacheUpdate.php | 73 - includes/cache/HTMLFileCache.php | 47 +- includes/cache/LinkBatch.php | 47 +- includes/cache/LinkCache.php | 75 +- includes/cache/LocalisationCache.php | 571 +- includes/cache/MapCacheLRU.php | 123 + includes/cache/MessageCache.php | 83 +- includes/cache/ObjectFileCache.php | 4 +- includes/cache/ProcessCacheLRU.php | 131 - includes/cache/ResourceFileCache.php | 7 +- includes/cache/SquidUpdate.php | 300 - includes/cache/UserCache.php | 16 +- includes/cache/bloom/BloomCache.php | 323 + includes/cache/bloom/BloomCacheRedis.php | 370 + includes/cache/bloom/BloomFilters.php | 79 + includes/changes/ChangesFeed.php | 240 + includes/changes/ChangesList.php | 250 +- includes/changes/EnhancedChangesList.php | 336 +- includes/changes/OldChangesList.php | 95 +- includes/changes/RCCacheEntry.php | 16 +- includes/changes/RCCacheEntryFactory.php | 275 + includes/changes/RecentChange.php | 523 +- includes/clientpool/RedisConnectionPool.php | 197 +- includes/composer/ComposerHookHandler.php | 37 + includes/composer/ComposerPackageModifier.php | 62 + includes/composer/ComposerVersionNormalizer.php | 66 + includes/config/Config.php | 47 + includes/config/ConfigException.php | 29 + includes/config/ConfigFactory.php | 112 + includes/config/GlobalVarConfig.php | 108 + includes/config/HashConfig.php | 75 + includes/config/MultiConfig.php | 72 + includes/config/MutableConfig.php | 38 + includes/content/AbstractContent.php | 277 +- includes/content/CodeContentHandler.php | 65 + includes/content/Content.php | 163 +- includes/content/ContentHandler.php | 298 +- includes/content/CssContent.php | 28 +- includes/content/CssContentHandler.php | 37 +- includes/content/JavaScriptContent.php | 20 +- includes/content/JavaScriptContentHandler.php | 37 +- includes/content/JsonContent.php | 120 + includes/content/JsonContentHandler.php | 26 + includes/content/MessageContent.php | 54 +- includes/content/TextContent.php | 137 +- includes/content/TextContentHandler.php | 48 +- includes/content/WikitextContent.php | 175 +- includes/content/WikitextContentHandler.php | 44 +- includes/context/ContextSource.php | 34 +- includes/context/DerivativeContext.php | 58 +- includes/context/IContextSource.php | 20 +- includes/context/RequestContext.php | 187 +- includes/dao/DBAccessBase.php | 8 +- includes/dao/IDBAccessObject.php | 13 +- includes/db/ChronologyProtector.php | 13 +- includes/db/CloneDatabase.php | 65 +- includes/db/Database.php | 1325 +- includes/db/DatabaseError.php | 181 +- includes/db/DatabaseMssql.php | 1502 +- includes/db/DatabaseMysql.php | 35 +- includes/db/DatabaseMysqlBase.php | 451 +- includes/db/DatabaseMysqli.php | 138 +- includes/db/DatabaseOracle.php | 376 +- includes/db/DatabasePostgres.php | 426 +- includes/db/DatabaseSqlite.php | 289 +- includes/db/DatabaseUtility.php | 71 +- includes/db/IORMRow.php | 32 +- includes/db/IORMTable.php | 65 +- includes/db/LBFactory.php | 152 +- includes/db/LBFactoryMulti.php | 399 + includes/db/LBFactorySingle.php | 112 + includes/db/LBFactory_Multi.php | 306 - includes/db/LBFactory_Single.php | 118 - includes/db/LoadBalancer.php | 458 +- includes/db/LoadMonitor.php | 129 +- includes/db/ORMIterator.php | 1 - includes/db/ORMResult.php | 14 +- includes/db/ORMRow.php | 55 +- includes/db/ORMTable.php | 123 +- includes/debug/Debug.php | 541 - includes/debug/MWDebug.php | 584 + includes/deferred/CallableUpdate.php | 29 + includes/deferred/DataUpdate.php | 125 + includes/deferred/DeferredUpdates.php | 136 + includes/deferred/HTMLCacheUpdate.php | 70 + includes/deferred/LinksUpdate.php | 1019 + includes/deferred/SearchUpdate.php | 209 + includes/deferred/SiteStatsUpdate.php | 254 + includes/deferred/SqlDataUpdate.php | 159 + includes/deferred/SquidUpdate.php | 311 + includes/deferred/ViewCountUpdate.php | 119 + includes/diff/ArrayDiffFormatter.php | 82 + includes/diff/DairikiDiff.php | 911 +- includes/diff/DiffFormatter.php | 247 + includes/diff/DifferenceEngine.php | 502 +- includes/diff/TableDiffFormatter.php | 214 + includes/diff/UnifiedDiffFormatter.php | 74 + includes/diff/WikiDiff3.php | 89 +- includes/exception/BadTitleError.php | 51 + includes/exception/ErrorPageError.php | 61 + includes/exception/FatalError.php | 43 + includes/exception/HttpError.php | 93 + includes/exception/MWException.php | 262 + includes/exception/MWExceptionHandler.php | 372 + includes/exception/PermissionsError.php | 58 + includes/exception/ReadOnlyError.php | 36 + includes/exception/ThrottledError.php | 40 + includes/exception/UserBlockedError.php | 33 + includes/exception/UserNotLoggedIn.php | 102 + includes/externalstore/ExternalStore.php | 10 +- includes/externalstore/ExternalStoreDB.php | 92 +- includes/externalstore/ExternalStoreMedium.php | 9 +- includes/externalstore/ExternalStoreMwstore.php | 11 +- includes/filebackend/FSFile.php | 38 +- includes/filebackend/FSFileBackend.php | 201 +- includes/filebackend/FileBackend.php | 300 +- includes/filebackend/FileBackendGroup.php | 46 +- includes/filebackend/FileBackendMultiWrite.php | 102 +- includes/filebackend/FileBackendStore.php | 383 +- includes/filebackend/FileOp.php | 183 +- includes/filebackend/FileOpBatch.php | 21 +- includes/filebackend/MemoryFileBackend.php | 274 + includes/filebackend/README | 2 +- includes/filebackend/SwiftFileBackend.php | 2108 +- includes/filebackend/TempFSFile.php | 50 +- includes/filebackend/filejournal/DBFileJournal.php | 22 +- includes/filebackend/filejournal/FileJournal.php | 68 +- includes/filebackend/lockmanager/DBLockManager.php | 44 +- includes/filebackend/lockmanager/FSLockManager.php | 35 +- includes/filebackend/lockmanager/LSLockManager.php | 218 - includes/filebackend/lockmanager/LockManager.php | 41 +- .../filebackend/lockmanager/LockManagerGroup.php | 22 +- .../filebackend/lockmanager/MemcLockManager.php | 50 +- .../filebackend/lockmanager/QuorumLockManager.php | 22 +- .../filebackend/lockmanager/RedisLockManager.php | 150 +- includes/filebackend/lockmanager/ScopedLock.php | 15 +- includes/filerepo/FSRepo.php | 6 +- includes/filerepo/FileRepo.php | 453 +- includes/filerepo/FileRepoStatus.php | 11 +- includes/filerepo/ForeignAPIRepo.php | 139 +- includes/filerepo/ForeignDBRepo.php | 46 +- includes/filerepo/ForeignDBViaLBRepo.php | 24 +- includes/filerepo/LocalRepo.php | 195 +- includes/filerepo/NullRepo.php | 6 +- includes/filerepo/RepoGroup.php | 186 +- includes/filerepo/file/ArchivedFile.php | 165 +- includes/filerepo/file/File.php | 827 +- includes/filerepo/file/ForeignAPIFile.php | 53 +- includes/filerepo/file/ForeignDBFile.php | 58 +- includes/filerepo/file/LocalFile.php | 994 +- includes/filerepo/file/OldLocalFile.php | 125 +- includes/filerepo/file/UnregisteredLocalFile.php | 63 +- includes/gallery/ImageGalleryBase.php | 165 +- includes/gallery/NolinesImageGallery.php | 1 - includes/gallery/PackedImageGallery.php | 11 +- includes/gallery/PackedOverlayImageGallery.php | 22 +- includes/gallery/TraditionalImageGallery.php | 114 +- includes/htmlform/HTMLApiField.php | 19 + includes/htmlform/HTMLAutoCompleteSelectField.php | 165 + includes/htmlform/HTMLButtonField.php | 42 + includes/htmlform/HTMLCheckField.php | 91 + includes/htmlform/HTMLCheckMatrix.php | 250 + includes/htmlform/HTMLEditTools.php | 51 + includes/htmlform/HTMLFloatField.php | 46 + includes/htmlform/HTMLForm.php | 1472 ++ includes/htmlform/HTMLFormField.php | 893 + includes/htmlform/HTMLFormFieldCloner.php | 382 + .../HTMLFormFieldRequiredOptionsException.php | 9 + includes/htmlform/HTMLHiddenField.php | 44 + includes/htmlform/HTMLInfoField.php | 54 + includes/htmlform/HTMLIntField.php | 27 + includes/htmlform/HTMLMultiSelectField.php | 122 + includes/htmlform/HTMLNestedFilterable.php | 11 + includes/htmlform/HTMLRadioField.php | 71 + includes/htmlform/HTMLSelectAndOtherField.php | 126 + includes/htmlform/HTMLSelectField.php | 44 + includes/htmlform/HTMLSelectLimitField.php | 35 + includes/htmlform/HTMLSelectOrOtherField.php | 83 + includes/htmlform/HTMLSubmitField.php | 9 + includes/htmlform/HTMLTextAreaField.php | 38 + includes/htmlform/HTMLTextField.php | 65 + includes/installer/CliInstaller.php | 20 +- includes/installer/DatabaseInstaller.php | 99 +- includes/installer/DatabaseUpdater.php | 161 +- includes/installer/InstallDocFormatter.php | 2 +- includes/installer/Installer.i18n.php | 21554 ------------------- includes/installer/Installer.php | 457 +- includes/installer/LocalSettingsGenerator.php | 87 +- includes/installer/MssqlInstaller.php | 737 + includes/installer/MssqlUpdater.php | 138 + includes/installer/MysqlInstaller.php | 33 +- includes/installer/MysqlUpdater.php | 81 +- includes/installer/OracleInstaller.php | 1 + includes/installer/OracleUpdater.php | 23 +- includes/installer/PhpBugTests.php | 26 - includes/installer/PostgresInstaller.php | 43 +- includes/installer/PostgresUpdater.php | 51 +- includes/installer/SqliteInstaller.php | 10 +- includes/installer/SqliteUpdater.php | 23 + includes/installer/WebInstaller.php | 264 +- includes/installer/WebInstallerOutput.php | 173 +- includes/installer/WebInstallerPage.php | 366 +- includes/installer/i18n/af.json | 154 + includes/installer/i18n/aln.json | 9 + includes/installer/i18n/am.json | 5 + includes/installer/i18n/an.json | 9 + includes/installer/i18n/ang.json | 9 + includes/installer/i18n/ar.json | 120 + includes/installer/i18n/arc.json | 22 + includes/installer/i18n/ary.json | 10 + includes/installer/i18n/arz.json | 18 + includes/installer/i18n/as.json | 10 + includes/installer/i18n/ast.json | 71 + includes/installer/i18n/av.json | 8 + includes/installer/i18n/avk.json | 4 + includes/installer/i18n/az.json | 33 + includes/installer/i18n/ba.json | 10 + includes/installer/i18n/bar.json | 13 + includes/installer/i18n/bcc.json | 5 + includes/installer/i18n/bcl.json | 37 + includes/installer/i18n/be-tarask.json | 333 + includes/installer/i18n/be.json | 20 + includes/installer/i18n/bg.json | 293 + includes/installer/i18n/bjn.json | 10 + includes/installer/i18n/bn.json | 125 + includes/installer/i18n/bpy.json | 5 + includes/installer/i18n/br.json | 283 + includes/installer/i18n/bs.json | 59 + includes/installer/i18n/bto.json | 65 + includes/installer/i18n/ca.json | 203 + includes/installer/i18n/ce.json | 92 + includes/installer/i18n/ceb.json | 5 + includes/installer/i18n/ckb.json | 42 + includes/installer/i18n/cps.json | 10 + includes/installer/i18n/crh-cyrl.json | 5 + includes/installer/i18n/crh-latn.json | 5 + includes/installer/i18n/cs.json | 334 + includes/installer/i18n/csb.json | 4 + includes/installer/i18n/cu.json | 10 + includes/installer/i18n/cv.json | 9 + includes/installer/i18n/cy.json | 12 + includes/installer/i18n/da.json | 38 + includes/installer/i18n/de-ch.json | 8 + includes/installer/i18n/de-formal.json | 12 + includes/installer/i18n/de.json | 340 + includes/installer/i18n/diq.json | 67 + includes/installer/i18n/dsb.json | 9 + includes/installer/i18n/dtp.json | 9 + includes/installer/i18n/el.json | 111 + includes/installer/i18n/en-gb.json | 22 + includes/installer/i18n/en.json | 326 + includes/installer/i18n/eo.json | 50 + includes/installer/i18n/es-formal.json | 9 + includes/installer/i18n/es.json | 352 + includes/installer/i18n/et.json | 68 + includes/installer/i18n/eu.json | 78 + includes/installer/i18n/ext.json | 5 + includes/installer/i18n/fa.json | 331 + includes/installer/i18n/fi.json | 220 + includes/installer/i18n/fo.json | 40 + includes/installer/i18n/fr.json | 348 + includes/installer/i18n/frc.json | 5 + includes/installer/i18n/frp.json | 153 + includes/installer/i18n/frr.json | 10 + includes/installer/i18n/fur.json | 15 + includes/installer/i18n/fy.json | 9 + includes/installer/i18n/ga.json | 13 + includes/installer/i18n/gag.json | 5 + includes/installer/i18n/gan-hans.json | 5 + includes/installer/i18n/gan-hant.json | 9 + includes/installer/i18n/gd.json | 9 + includes/installer/i18n/gl.json | 331 + includes/installer/i18n/gom-latn.json | 9 + includes/installer/i18n/grc.json | 11 + includes/installer/i18n/gsw.json | 68 + includes/installer/i18n/gu.json | 45 + includes/installer/i18n/gv.json | 4 + includes/installer/i18n/hak.json | 5 + includes/installer/i18n/haw.json | 64 + includes/installer/i18n/he.json | 330 + includes/installer/i18n/hi.json | 49 + includes/installer/i18n/hif-latn.json | 9 + includes/installer/i18n/hil.json | 9 + includes/installer/i18n/hr.json | 5 + includes/installer/i18n/hrx.json | 314 + includes/installer/i18n/hsb.json | 256 + includes/installer/i18n/ht.json | 10 + includes/installer/i18n/hu-formal.json | 33 + includes/installer/i18n/hu.json | 304 + includes/installer/i18n/hy.json | 5 + includes/installer/i18n/ia.json | 318 + includes/installer/i18n/id.json | 323 + includes/installer/i18n/ie.json | 4 + includes/installer/i18n/ig.json | 17 + includes/installer/i18n/ilo.json | 4 + includes/installer/i18n/io.json | 9 + includes/installer/i18n/is.json | 5 + includes/installer/i18n/it.json | 334 + includes/installer/i18n/ja.json | 338 + includes/installer/i18n/jam.json | 9 + includes/installer/i18n/jut.json | 9 + includes/installer/i18n/jv.json | 5 + includes/installer/i18n/ka.json | 97 + includes/installer/i18n/kaa.json | 5 + includes/installer/i18n/kbd-cyrl.json | 10 + includes/installer/i18n/khw.json | 8 + includes/installer/i18n/kiu.json | 9 + includes/installer/i18n/kk-arab.json | 5 + includes/installer/i18n/kk-cyrl.json | 5 + includes/installer/i18n/kk-latn.json | 5 + includes/installer/i18n/km.json | 32 + includes/installer/i18n/kn.json | 46 + includes/installer/i18n/ko.json | 323 + includes/installer/i18n/krc.json | 29 + includes/installer/i18n/ksh.json | 319 + includes/installer/i18n/ku-latn.json | 17 + includes/installer/i18n/lad.json | 10 + includes/installer/i18n/lb.json | 212 + includes/installer/i18n/lez.json | 27 + includes/installer/i18n/lfn.json | 5 + includes/installer/i18n/lg.json | 9 + includes/installer/i18n/li.json | 9 + includes/installer/i18n/lo.json | 4 + includes/installer/i18n/lrc.json | 24 + includes/installer/i18n/lt.json | 89 + includes/installer/i18n/lv.json | 45 + includes/installer/i18n/lzh.json | 10 + includes/installer/i18n/lzz.json | 9 + includes/installer/i18n/mai.json | 9 + includes/installer/i18n/mdf.json | 5 + includes/installer/i18n/mg.json | 65 + includes/installer/i18n/mhr.json | 4 + includes/installer/i18n/min.json | 11 + includes/installer/i18n/mk.json | 329 + includes/installer/i18n/ml.json | 120 + includes/installer/i18n/mn.json | 10 + includes/installer/i18n/mr.json | 62 + includes/installer/i18n/ms.json | 151 + includes/installer/i18n/mt.json | 90 + includes/installer/i18n/my.json | 8 + includes/installer/i18n/myv.json | 16 + includes/installer/i18n/mzn.json | 8 + includes/installer/i18n/nah.json | 4 + includes/installer/i18n/nan.json | 59 + includes/installer/i18n/nap.json | 16 + includes/installer/i18n/nb.json | 324 + includes/installer/i18n/nds-nl.json | 9 + includes/installer/i18n/nds.json | 14 + includes/installer/i18n/ne.json | 26 + includes/installer/i18n/nl-informal.json | 79 + includes/installer/i18n/nl.json | 330 + includes/installer/i18n/nn.json | 41 + includes/installer/i18n/oc.json | 181 + includes/installer/i18n/or.json | 33 + includes/installer/i18n/os.json | 9 + includes/installer/i18n/pa.json | 13 + includes/installer/i18n/pam.json | 5 + includes/installer/i18n/pcd.json | 4 + includes/installer/i18n/pdc.json | 13 + includes/installer/i18n/pl.json | 340 + includes/installer/i18n/pms.json | 301 + includes/installer/i18n/pnt.json | 8 + includes/installer/i18n/prg.json | 9 + includes/installer/i18n/ps.json | 65 + includes/installer/i18n/pt-br.json | 244 + includes/installer/i18n/pt.json | 335 + includes/installer/i18n/qqq.json | 344 + includes/installer/i18n/qu.json | 20 + includes/installer/i18n/rgn.json | 4 + includes/installer/i18n/rm.json | 9 + includes/installer/i18n/ro.json | 140 + includes/installer/i18n/roa-tara.json | 56 + includes/installer/i18n/ru.json | 344 + includes/installer/i18n/rue.json | 9 + includes/installer/i18n/sa.json | 8 + includes/installer/i18n/sah.json | 5 + includes/installer/i18n/sc.json | 14 + includes/installer/i18n/scn.json | 5 + includes/installer/i18n/sco.json | 314 + includes/installer/i18n/sdc.json | 19 + includes/installer/i18n/sei.json | 4 + includes/installer/i18n/sh.json | 10 + includes/installer/i18n/shi.json | 10 + includes/installer/i18n/si.json | 142 + includes/installer/i18n/sk.json | 78 + includes/installer/i18n/sl.json | 174 + includes/installer/i18n/sli.json | 9 + includes/installer/i18n/so.json | 9 + includes/installer/i18n/sq.json | 5 + includes/installer/i18n/sr-ec.json | 73 + includes/installer/i18n/sr-el.json | 39 + includes/installer/i18n/srn.json | 9 + includes/installer/i18n/ss.json | 4 + includes/installer/i18n/stq.json | 9 + includes/installer/i18n/su.json | 5 + includes/installer/i18n/sv.json | 330 + includes/installer/i18n/sw.json | 9 + includes/installer/i18n/szl.json | 9 + includes/installer/i18n/ta.json | 90 + includes/installer/i18n/tcy.json | 5 + includes/installer/i18n/te.json | 240 + includes/installer/i18n/tet.json | 9 + includes/installer/i18n/tg-cyrl.json | 9 + includes/installer/i18n/tg-latn.json | 9 + includes/installer/i18n/th.json | 10 + includes/installer/i18n/tk.json | 9 + includes/installer/i18n/tl.json | 305 + includes/installer/i18n/tly.json | 8 + includes/installer/i18n/tr.json | 201 + includes/installer/i18n/tt-cyrl.json | 10 + includes/installer/i18n/tt-latn.json | 10 + includes/installer/i18n/tyv.json | 8 + includes/installer/i18n/udm.json | 8 + includes/installer/i18n/ug-arab.json | 9 + includes/installer/i18n/uk.json | 332 + includes/installer/i18n/ur.json | 34 + includes/installer/i18n/uz.json | 10 + includes/installer/i18n/vec.json | 10 + includes/installer/i18n/vep.json | 10 + includes/installer/i18n/vi.json | 195 + includes/installer/i18n/vo.json | 5 + includes/installer/i18n/vro.json | 5 + includes/installer/i18n/wa.json | 8 + includes/installer/i18n/war.json | 72 + includes/installer/i18n/wo.json | 9 + includes/installer/i18n/wuu.json | 9 + includes/installer/i18n/xal.json | 9 + includes/installer/i18n/yi.json | 58 + includes/installer/i18n/yo.json | 19 + includes/installer/i18n/yue.json | 5 + includes/installer/i18n/zea.json | 9 + includes/installer/i18n/zh-hans.json | 345 + includes/installer/i18n/zh-hant.json | 335 + includes/installer/i18n/zh-hk.json | 8 + includes/installer/i18n/zh-tw.json | 4 + includes/interwiki/Interwiki.php | 195 +- includes/job/Job.php | 321 - includes/job/JobQueue.php | 706 - includes/job/JobQueueDB.php | 816 - includes/job/JobQueueFederated.php | 473 - includes/job/JobQueueGroup.php | 427 - includes/job/JobQueueRedis.php | 856 - includes/job/README | 81 - includes/job/aggregator/JobQueueAggregator.php | 156 - includes/job/aggregator/JobQueueAggregatorMemc.php | 124 - .../job/aggregator/JobQueueAggregatorRedis.php | 193 - includes/job/jobs/AssembleUploadChunksJob.php | 127 - includes/job/jobs/DoubleRedirectJob.php | 221 - includes/job/jobs/DuplicateJob.php | 59 - includes/job/jobs/EmaillingJob.php | 47 - includes/job/jobs/EnotifNotifyJob.php | 58 - includes/job/jobs/HTMLCacheUpdateJob.php | 263 - includes/job/jobs/NullJob.php | 76 - includes/job/jobs/PublishStashedFileJob.php | 140 - includes/job/jobs/RefreshLinksJob.php | 222 - includes/job/jobs/UploadFromUrlJob.php | 184 - includes/jobqueue/Job.php | 350 + includes/jobqueue/JobQueue.php | 740 + includes/jobqueue/JobQueueDB.php | 849 + includes/jobqueue/JobQueueFederated.php | 559 + includes/jobqueue/JobQueueGroup.php | 440 + includes/jobqueue/JobQueueRedis.php | 865 + includes/jobqueue/JobRunner.php | 350 + includes/jobqueue/JobSpecification.php | 189 + includes/jobqueue/README | 81 + .../jobqueue/aggregator/JobQueueAggregator.php | 162 + .../jobqueue/aggregator/JobQueueAggregatorMemc.php | 125 + .../aggregator/JobQueueAggregatorRedis.php | 218 + includes/jobqueue/jobs/AssembleUploadChunksJob.php | 136 + includes/jobqueue/jobs/DoubleRedirectJob.php | 250 + includes/jobqueue/jobs/DuplicateJob.php | 59 + includes/jobqueue/jobs/EmaillingJob.php | 46 + includes/jobqueue/jobs/EnotifNotifyJob.php | 57 + includes/jobqueue/jobs/HTMLCacheUpdateJob.php | 162 + includes/jobqueue/jobs/NullJob.php | 76 + includes/jobqueue/jobs/PublishStashedFileJob.php | 150 + includes/jobqueue/jobs/RefreshLinksJob.php | 199 + includes/jobqueue/jobs/RefreshLinksJob2.php | 141 + includes/jobqueue/jobs/UploadFromUrlJob.php | 187 + includes/jobqueue/utils/BacklinkJobUtils.php | 122 + includes/json/FormatJson.php | 150 +- includes/libs/CSSJanus.php | 246 +- includes/libs/CSSMin.php | 300 +- includes/libs/GenericArrayObject.php | 5 +- includes/libs/HashRing.php | 239 + includes/libs/HttpStatus.php | 8 +- includes/libs/IEContentAnalyzer.php | 7 +- includes/libs/IPSet.php | 277 + includes/libs/JavaScriptMinifier.php | 1 + includes/libs/MWMessagePack.php | 189 + includes/libs/MappedIterator.php | 117 + includes/libs/MultiHttpClient.php | 389 + includes/libs/ProcessCacheLRU.php | 148 + includes/libs/RunningStat.php | 176 + includes/libs/ScopedCallback.php | 73 + includes/libs/ScopedPHPTimeout.php | 84 + includes/libs/XmlTypeCheck.php | 264 + includes/libs/jsminplus.php | 1 + includes/libs/lessc.inc.php | 88 +- .../libs/virtualrest/SwiftVirtualRESTService.php | 175 + includes/libs/virtualrest/VirtualRESTService.php | 107 + .../libs/virtualrest/VirtualRESTServiceClient.php | 289 + includes/limit.sh | 83 +- includes/logging/DeleteLogFormatter.php | 208 +- includes/logging/LogEntry.php | 126 +- includes/logging/LogEventsList.php | 235 +- includes/logging/LogFormatter.php | 126 +- includes/logging/LogPage.php | 178 +- includes/logging/LogPager.php | 112 +- includes/logging/MoveLogFormatter.php | 8 +- includes/logging/NewUsersLogFormatter.php | 3 + includes/logging/PageLangLogFormatter.php | 61 + includes/logging/PatrolLog.php | 15 +- includes/logging/PatrolLogFormatter.php | 2 + includes/logging/RightsLogFormatter.php | 1 + includes/mail/EmailNotification.php | 512 + includes/mail/MailAddress.php | 91 + includes/mail/UserMailer.php | 425 + includes/media/BMP.php | 14 +- includes/media/Bitmap.php | 488 +- includes/media/BitmapMetadataHandler.php | 58 +- includes/media/Bitmap_ClientOnly.php | 15 +- includes/media/DjVu.php | 140 +- includes/media/DjVuImage.php | 82 +- includes/media/Exif.php | 511 +- includes/media/ExifBitmap.php | 74 +- includes/media/FormatMetadata.php | 2061 +- includes/media/GIF.php | 48 +- includes/media/GIFMetadataExtractor.php | 51 +- includes/media/IPTC.php | 47 +- includes/media/ImageHandler.php | 72 +- includes/media/Jpeg.php | 94 +- includes/media/JpegMetadataExtractor.php | 24 +- includes/media/MediaHandler.php | 415 +- includes/media/MediaTransformOutput.php | 98 +- includes/media/PNG.php | 50 +- includes/media/PNGMetadataExtractor.php | 92 +- includes/media/SVG.php | 202 +- includes/media/SVGMetadataExtractor.php | 92 +- includes/media/Tiff.php | 24 +- includes/media/TransformationalImageHandler.php | 593 + includes/media/XCF.php | 141 +- includes/media/XMP.php | 213 +- includes/media/XMPInfo.php | 992 +- includes/media/XMPValidate.php | 82 +- includes/mime.info | 14 +- includes/mime.types | 8 + includes/normal/Makefile | 5 +- includes/normal/RandomTest.php | 20 +- includes/normal/Utf8Case.php | 2109 -- includes/normal/Utf8CaseGenerate.php | 112 - includes/normal/Utf8Test.php | 47 +- includes/normal/UtfNormal.php | 291 +- includes/normal/UtfNormalBench.php | 39 +- includes/normal/UtfNormalData.inc | 3 +- includes/normal/UtfNormalDataK.inc | 1 + includes/normal/UtfNormalDefines.php | 1 - includes/normal/UtfNormalGenerate.php | 103 +- includes/normal/UtfNormalMemStress.php | 49 +- includes/normal/UtfNormalTest.php | 70 +- includes/normal/UtfNormalTest2.php | 304 +- includes/normal/UtfNormalUtil.php | 42 +- includes/objectcache/APCBagOStuff.php | 30 +- includes/objectcache/BagOStuff.php | 180 +- includes/objectcache/DBABagOStuff.php | 307 - includes/objectcache/EhcacheBagOStuff.php | 324 - includes/objectcache/EmptyBagOStuff.php | 37 +- includes/objectcache/HashBagOStuff.php | 31 +- includes/objectcache/MemcachedBagOStuff.php | 58 +- includes/objectcache/MemcachedClient.php | 274 +- includes/objectcache/MemcachedPeclBagOStuff.php | 87 +- includes/objectcache/MemcachedPhpBagOStuff.php | 28 +- includes/objectcache/MultiWriteBagOStuff.php | 83 +- includes/objectcache/ObjectCache.php | 28 +- includes/objectcache/ObjectCacheSessionHandler.php | 31 +- includes/objectcache/RedisBagOStuff.php | 215 +- includes/objectcache/SqlBagOStuff.php | 222 +- includes/objectcache/WinCacheBagOStuff.php | 22 +- includes/objectcache/XCacheBagOStuff.php | 30 +- includes/page/Article.php | 2150 ++ includes/page/CategoryPage.php | 118 + includes/page/ImagePage.php | 1615 ++ includes/page/WikiCategoryPage.php | 50 + includes/page/WikiFilePage.php | 230 + includes/page/WikiPage.php | 3554 +++ includes/pager/AlphabeticPager.php | 108 + includes/pager/IndexPager.php | 742 + includes/pager/Pager.php | 35 + includes/pager/ReverseChronologicalPager.php | 118 + includes/pager/TablePager.php | 469 + includes/parser/CacheTime.php | 98 +- includes/parser/CoreParserFunctions.php | 710 +- includes/parser/CoreTagHooks.php | 26 +- includes/parser/DateFormatter.php | 51 +- includes/parser/LinkHolderArray.php | 243 +- includes/parser/MWTidy.php | 291 + includes/parser/Parser.php | 1364 +- includes/parser/ParserCache.php | 108 +- includes/parser/ParserDiffTest.php | 143 + includes/parser/ParserOptions.php | 517 +- includes/parser/ParserOutput.php | 449 +- includes/parser/Parser_DiffTest.php | 143 - includes/parser/Preprocessor.php | 176 +- includes/parser/Preprocessor_DOM.php | 485 +- includes/parser/Preprocessor_Hash.php | 620 +- includes/parser/StripState.php | 58 +- includes/parser/Tidy.php | 286 - includes/password/BcryptPassword.php | 88 + includes/password/EncryptedPassword.php | 98 + includes/password/InvalidPassword.php | 47 + includes/password/LayeredParameterizedPassword.php | 140 + includes/password/MWOldPassword.php | 48 + includes/password/MWSaltedPassword.php | 46 + includes/password/ParameterizedPassword.php | 119 + includes/password/Password.php | 186 + includes/password/PasswordError.php | 28 + includes/password/PasswordFactory.php | 178 + includes/password/Pbkdf2Password.php | 85 + includes/poolcounter/PoolCounter.php | 173 + includes/poolcounter/PoolCounterRedis.php | 417 + includes/poolcounter/PoolCounterWork.php | 160 + .../poolcounter/PoolCounterWorkViaCallback.php | 92 + includes/poolcounter/PoolWorkArticleView.php | 208 + includes/profiler/Profiler.php | 637 +- includes/profiler/ProfilerMwprof.php | 256 + includes/profiler/ProfilerSimple.php | 133 - includes/profiler/ProfilerSimpleDB.php | 111 + includes/profiler/ProfilerSimpleText.php | 6 +- includes/profiler/ProfilerSimpleTrace.php | 59 +- includes/profiler/ProfilerSimpleUDP.php | 17 +- includes/profiler/ProfilerStandard.php | 559 + includes/profiler/ProfilerStub.php | 38 +- includes/rcfeed/IRCColourfulRCFeedFormatter.php | 49 +- includes/rcfeed/JSONRCFeedFormatter.php | 112 +- includes/rcfeed/MachineReadableRCFeedFormatter.php | 130 + includes/rcfeed/RCFeedEngine.php | 33 +- includes/rcfeed/RCFeedFormatter.php | 27 +- includes/rcfeed/RedisPubSubFeedEngine.php | 62 +- includes/rcfeed/UDPRCFeedEngine.php | 26 +- includes/rcfeed/XMLRCFeedFormatter.php | 29 + .../DerivativeResourceLoaderContext.php | 202 + includes/resourceloader/ResourceLoader.php | 670 +- includes/resourceloader/ResourceLoaderContext.php | 49 +- .../ResourceLoaderEditToolbarModule.php | 102 + .../resourceloader/ResourceLoaderFileModule.php | 473 +- .../ResourceLoaderFilePageModule.php | 2 +- includes/resourceloader/ResourceLoaderFilePath.php | 74 + .../resourceloader/ResourceLoaderLESSFunctions.php | 67 - .../ResourceLoaderLanguageDataModule.php | 84 +- .../ResourceLoaderLanguageNamesModule.php | 79 + includes/resourceloader/ResourceLoaderModule.php | 221 +- .../ResourceLoaderNoscriptModule.php | 6 +- .../resourceloader/ResourceLoaderSiteModule.php | 12 +- .../resourceloader/ResourceLoaderStartUpModule.php | 412 +- .../ResourceLoaderUserCSSPrefsModule.php | 31 +- .../ResourceLoaderUserGroupsModule.php | 21 +- .../resourceloader/ResourceLoaderUserModule.php | 18 +- .../ResourceLoaderUserOptionsModule.php | 16 +- .../ResourceLoaderUserTokensModule.php | 10 +- .../resourceloader/ResourceLoaderWikiModule.php | 110 +- includes/revisiondelete/RevDelArchiveItem.php | 105 + includes/revisiondelete/RevDelArchiveList.php | 66 + includes/revisiondelete/RevDelArchivedFileItem.php | 129 + includes/revisiondelete/RevDelArchivedFileList.php | 56 + .../revisiondelete/RevDelArchivedRevisionItem.php | 53 + includes/revisiondelete/RevDelFileItem.php | 237 + includes/revisiondelete/RevDelFileList.php | 128 + includes/revisiondelete/RevDelItem.php | 62 + includes/revisiondelete/RevDelList.php | 313 + includes/revisiondelete/RevDelLogItem.php | 151 + includes/revisiondelete/RevDelLogList.php | 103 + includes/revisiondelete/RevDelRevisionItem.php | 187 + includes/revisiondelete/RevDelRevisionList.php | 143 + includes/revisiondelete/RevisionDelete.php | 964 - .../revisiondelete/RevisionDeleteAbstracts.php | 328 - includes/revisiondelete/RevisionDeleteUser.php | 38 +- includes/revisiondelete/RevisionDeleter.php | 37 +- includes/search/SearchDatabase.php | 57 + includes/search/SearchEngine.php | 1116 +- includes/search/SearchHighlighter.php | 575 + includes/search/SearchMssql.php | 110 +- includes/search/SearchMySQL.php | 122 +- includes/search/SearchOracle.php | 77 +- includes/search/SearchPostgres.php | 89 +- includes/search/SearchResult.php | 237 + includes/search/SearchResultSet.php | 211 + includes/search/SearchSqlite.php | 116 +- includes/search/SearchUpdate.php | 185 - includes/site/MediaWikiSite.php | 44 +- includes/site/Site.php | 32 +- includes/site/SiteList.php | 88 +- includes/site/SiteSQLStore.php | 52 +- includes/site/SiteStore.php | 8 +- includes/skins/Skin.php | 1620 ++ includes/skins/SkinException.php | 29 + includes/skins/SkinFactory.php | 214 + includes/skins/SkinFallback.php | 36 + includes/skins/SkinFallbackTemplate.php | 109 + includes/skins/SkinTemplate.php | 2123 ++ includes/specialpage/ChangesListSpecialPage.php | 468 + includes/specialpage/FormSpecialPage.php | 193 + includes/specialpage/ImageQueryPage.php | 79 + includes/specialpage/IncludableSpecialPage.php | 39 + includes/specialpage/PageQueryPage.php | 72 + includes/specialpage/QueryPage.php | 754 + includes/specialpage/RedirectSpecialPage.php | 209 + includes/specialpage/SpecialPage.php | 663 + includes/specialpage/SpecialPageFactory.php | 702 + includes/specialpage/UnlistedSpecialPage.php | 37 + includes/specialpage/WantedQueryPage.php | 130 + includes/specials/SpecialActiveusers.php | 237 +- includes/specials/SpecialAllMessages.php | 479 + includes/specials/SpecialAllPages.php | 384 + includes/specials/SpecialAllmessages.php | 444 - includes/specials/SpecialAllpages.php | 573 - includes/specials/SpecialBlock.php | 141 +- includes/specials/SpecialBlockList.php | 65 +- includes/specials/SpecialBooksources.php | 30 +- includes/specials/SpecialBrokenRedirects.php | 4 +- includes/specials/SpecialCachedPage.php | 24 +- includes/specials/SpecialCategories.php | 72 +- includes/specials/SpecialChangeEmail.php | 255 +- includes/specials/SpecialChangePassword.php | 385 +- includes/specials/SpecialComparePages.php | 4 +- includes/specials/SpecialConfirmemail.php | 44 +- includes/specials/SpecialContributions.php | 254 +- includes/specials/SpecialCreateAccount.php | 56 + includes/specials/SpecialDeletedContributions.php | 42 +- includes/specials/SpecialDiff.php | 61 + includes/specials/SpecialEditWatchlist.php | 222 +- includes/specials/SpecialEmailuser.php | 60 +- includes/specials/SpecialExpandTemplates.php | 286 + includes/specials/SpecialExport.php | 87 +- includes/specials/SpecialFewestrevisions.php | 2 +- includes/specials/SpecialFileDuplicateSearch.php | 12 +- includes/specials/SpecialFilepath.php | 8 +- includes/specials/SpecialImport.php | 164 +- includes/specials/SpecialJavaScriptTest.php | 33 +- includes/specials/SpecialLinkSearch.php | 114 +- includes/specials/SpecialListDuplicatedFiles.php | 113 + includes/specials/SpecialListfiles.php | 223 +- includes/specials/SpecialListgrouprights.php | 200 +- includes/specials/SpecialListredirects.php | 11 +- includes/specials/SpecialListusers.php | 65 +- includes/specials/SpecialLockdb.php | 10 +- includes/specials/SpecialLog.php | 33 +- includes/specials/SpecialLonelypages.php | 52 +- includes/specials/SpecialMIMEsearch.php | 58 +- includes/specials/SpecialMediaStatistics.php | 325 + includes/specials/SpecialMergeHistory.php | 105 +- includes/specials/SpecialMostinterwikis.php | 4 +- includes/specials/SpecialMostlinked.php | 6 +- includes/specials/SpecialMostlinkedcategories.php | 1 + includes/specials/SpecialMostlinkedtemplates.php | 11 +- includes/specials/SpecialMostrevisions.php | 1 + includes/specials/SpecialMovepage.php | 98 +- includes/specials/SpecialMyLanguage.php | 93 + includes/specials/SpecialMyRedirectPages.php | 114 + includes/specials/SpecialNewimages.php | 37 +- includes/specials/SpecialNewpages.php | 68 +- includes/specials/SpecialPageLanguage.php | 195 + includes/specials/SpecialPagesWithProp.php | 45 +- includes/specials/SpecialPasswordReset.php | 43 +- includes/specials/SpecialPermanentLink.php | 45 + includes/specials/SpecialPreferences.php | 16 +- includes/specials/SpecialPrefixindex.php | 46 +- includes/specials/SpecialProtectedpages.php | 406 +- includes/specials/SpecialProtectedtitles.php | 17 +- includes/specials/SpecialRandomInCategory.php | 77 +- includes/specials/SpecialRandompage.php | 9 +- includes/specials/SpecialRecentchanges.php | 562 +- includes/specials/SpecialRecentchangeslinked.php | 61 +- includes/specials/SpecialRedirect.php | 72 +- includes/specials/SpecialResetTokens.php | 12 +- includes/specials/SpecialRevisiondelete.php | 286 +- includes/specials/SpecialRunJobs.php | 112 + includes/specials/SpecialSearch.php | 661 +- includes/specials/SpecialShortpages.php | 21 +- includes/specials/SpecialSpecialpages.php | 27 +- includes/specials/SpecialStatistics.php | 168 +- includes/specials/SpecialTags.php | 10 +- includes/specials/SpecialTrackingCategories.php | 148 + includes/specials/SpecialUnblock.php | 38 +- includes/specials/SpecialUncategorizedimages.php | 13 +- includes/specials/SpecialUncategorizedpages.php | 26 +- includes/specials/SpecialUndelete.php | 393 +- includes/specials/SpecialUnlockdb.php | 11 +- includes/specials/SpecialUnusedcategories.php | 27 +- includes/specials/SpecialUnusedimages.php | 25 +- includes/specials/SpecialUnusedtemplates.php | 18 +- includes/specials/SpecialUnwatchedpages.php | 28 +- includes/specials/SpecialUpload.php | 254 +- includes/specials/SpecialUploadStash.php | 121 +- includes/specials/SpecialUserlogin.php | 455 +- includes/specials/SpecialUserlogout.php | 1 - includes/specials/SpecialUserrights.php | 165 +- includes/specials/SpecialVersion.php | 738 +- includes/specials/SpecialWantedcategories.php | 68 +- includes/specials/SpecialWantedfiles.php | 77 +- includes/specials/SpecialWantedpages.php | 7 +- includes/specials/SpecialWantedtemplates.php | 22 +- includes/specials/SpecialWatchlist.php | 652 +- includes/specials/SpecialWhatlinkshere.php | 216 +- includes/specials/SpecialWithoutinterwiki.php | 34 +- includes/templates/NoLocalSettings.php | 32 +- includes/templates/Usercreate.php | 80 +- includes/templates/Userlogin.php | 45 +- includes/title/MalformedTitleException.php | 32 + includes/title/MediaWikiPageLinkRenderer.php | 131 + includes/title/MediaWikiTitleCodec.php | 400 + includes/title/PageLinkRenderer.php | 67 + includes/title/TitleFormatter.php | 90 + includes/title/TitleParser.php | 47 + includes/title/TitleValue.php | 161 + includes/upload/UploadBase.php | 462 +- includes/upload/UploadFromChunks.php | 110 +- includes/upload/UploadFromFile.php | 9 +- includes/upload/UploadFromStash.php | 46 +- includes/upload/UploadFromUrl.php | 72 +- includes/upload/UploadStash.php | 286 +- includes/utils/ArrayUtils.php | 187 + includes/utils/Cdb.php | 163 + includes/utils/CdbDBA.php | 75 + includes/utils/CdbPHP.php | 494 + includes/utils/IP.php | 738 + includes/utils/MWCryptHKDF.php | 332 + includes/utils/MWCryptRand.php | 516 + includes/utils/MWFunction.php | 63 + includes/utils/README | 9 + includes/utils/StringUtils.php | 612 + includes/utils/UIDGenerator.php | 507 + includes/utils/ZipDirectoryReader.php | 732 + 1088 files changed, 126469 insertions(+), 102967 deletions(-) delete mode 100644 includes/Action.php delete mode 100644 includes/ArrayUtils.php delete mode 100644 includes/Article.php delete mode 100644 includes/CacheHelper.php delete mode 100644 includes/CallableUpdate.php create mode 100644 includes/CategoryFinder.php delete mode 100644 includes/CategoryPage.php delete mode 100644 includes/Categoryfinder.php delete mode 100644 includes/Cdb.php delete mode 100644 includes/Cdb_PHP.php delete mode 100644 includes/ChangesFeed.php delete mode 100644 includes/ConfEditor.php delete mode 100644 includes/DataUpdate.php delete mode 100644 includes/DeferredUpdates.php delete mode 100644 includes/Exception.php delete mode 100644 includes/FakeTitle.php delete mode 100644 includes/HTMLForm.php delete mode 100644 includes/HashRing.php delete mode 100644 includes/IP.php delete mode 100644 includes/ImagePage.php delete mode 100644 includes/ImageQueryPage.php delete mode 100644 includes/Init.php delete mode 100644 includes/LinksUpdate.php delete mode 100644 includes/MWCryptRand.php delete mode 100644 includes/MWFunction.php create mode 100644 includes/MWNamespace.php create mode 100644 includes/MWTimestamp.php delete mode 100644 includes/MappedIterator.php create mode 100644 includes/MediaWiki.php create mode 100644 includes/MediaWikiVersionFetcher.php delete mode 100644 includes/Metadata.php create mode 100644 includes/MovePage.php delete mode 100644 includes/Namespace.php delete mode 100644 includes/PageQueryPage.php delete mode 100644 includes/Pager.php delete mode 100644 includes/PoolCounter.php delete mode 100644 includes/ProxyTools.php delete mode 100644 includes/QueryPage.php delete mode 100644 includes/ScopedCallback.php delete mode 100644 includes/ScopedPHPTimeout.php delete mode 100644 includes/Skin.php delete mode 100644 includes/SkinTemplate.php delete mode 100644 includes/SpecialPage.php delete mode 100644 includes/SpecialPageFactory.php delete mode 100644 includes/SqlDataUpdate.php delete mode 100644 includes/StringUtils.php delete mode 100644 includes/Timestamp.php create mode 100644 includes/TimestampException.php create mode 100644 includes/TitleArrayFromResult.php delete mode 100644 includes/UIDGenerator.php create mode 100644 includes/UserArrayFromResult.php delete mode 100644 includes/UserMailer.php delete mode 100644 includes/ViewCountUpdate.php delete mode 100644 includes/Wiki.php delete mode 100644 includes/WikiCategoryPage.php delete mode 100644 includes/WikiError.php delete mode 100644 includes/WikiFilePage.php delete mode 100644 includes/WikiPage.php delete mode 100644 includes/XmlTypeCheck.php delete mode 100644 includes/ZhClient.php delete mode 100644 includes/ZipDirectoryReader.php create mode 100644 includes/actions/Action.php create mode 100644 includes/actions/FormAction.php create mode 100644 includes/actions/FormlessAction.php create mode 100644 includes/actions/SubmitAction.php create mode 100644 includes/actions/UnprotectAction.php create mode 100644 includes/actions/UnwatchAction.php create mode 100644 includes/api/ApiClearHasMsg.php create mode 100644 includes/api/ApiFeedRecentChanges.php create mode 100644 includes/api/ApiFormatFeedWrapper.php create mode 100644 includes/api/ApiQueryBacklinksprop.php create mode 100644 includes/api/ApiQueryContributors.php create mode 100644 includes/api/ApiQueryPrefixSearch.php create mode 100644 includes/api/ApiQueryTokens.php create mode 100644 includes/api/ApiRevisionDelete.php create mode 100644 includes/cache/CacheHelper.php delete mode 100644 includes/cache/HTMLCacheUpdate.php create mode 100644 includes/cache/MapCacheLRU.php delete mode 100644 includes/cache/ProcessCacheLRU.php delete mode 100644 includes/cache/SquidUpdate.php create mode 100644 includes/cache/bloom/BloomCache.php create mode 100644 includes/cache/bloom/BloomCacheRedis.php create mode 100644 includes/cache/bloom/BloomFilters.php create mode 100644 includes/changes/ChangesFeed.php create mode 100644 includes/changes/RCCacheEntryFactory.php create mode 100644 includes/composer/ComposerHookHandler.php create mode 100644 includes/composer/ComposerPackageModifier.php create mode 100644 includes/composer/ComposerVersionNormalizer.php create mode 100644 includes/config/Config.php create mode 100644 includes/config/ConfigException.php create mode 100644 includes/config/ConfigFactory.php create mode 100644 includes/config/GlobalVarConfig.php create mode 100644 includes/config/HashConfig.php create mode 100644 includes/config/MultiConfig.php create mode 100644 includes/config/MutableConfig.php create mode 100644 includes/content/CodeContentHandler.php create mode 100644 includes/content/JsonContent.php create mode 100644 includes/content/JsonContentHandler.php create mode 100644 includes/db/LBFactoryMulti.php create mode 100644 includes/db/LBFactorySingle.php delete mode 100644 includes/db/LBFactory_Multi.php delete mode 100644 includes/db/LBFactory_Single.php delete mode 100644 includes/debug/Debug.php create mode 100644 includes/debug/MWDebug.php create mode 100644 includes/deferred/CallableUpdate.php create mode 100644 includes/deferred/DataUpdate.php create mode 100644 includes/deferred/DeferredUpdates.php create mode 100644 includes/deferred/HTMLCacheUpdate.php create mode 100644 includes/deferred/LinksUpdate.php create mode 100644 includes/deferred/SearchUpdate.php create mode 100644 includes/deferred/SiteStatsUpdate.php create mode 100644 includes/deferred/SqlDataUpdate.php create mode 100644 includes/deferred/SquidUpdate.php create mode 100644 includes/deferred/ViewCountUpdate.php create mode 100644 includes/diff/ArrayDiffFormatter.php create mode 100644 includes/diff/DiffFormatter.php create mode 100644 includes/diff/TableDiffFormatter.php create mode 100644 includes/diff/UnifiedDiffFormatter.php create mode 100644 includes/exception/BadTitleError.php create mode 100644 includes/exception/ErrorPageError.php create mode 100644 includes/exception/FatalError.php create mode 100644 includes/exception/HttpError.php create mode 100644 includes/exception/MWException.php create mode 100644 includes/exception/MWExceptionHandler.php create mode 100644 includes/exception/PermissionsError.php create mode 100644 includes/exception/ReadOnlyError.php create mode 100644 includes/exception/ThrottledError.php create mode 100644 includes/exception/UserBlockedError.php create mode 100644 includes/exception/UserNotLoggedIn.php create mode 100644 includes/filebackend/MemoryFileBackend.php delete mode 100644 includes/filebackend/lockmanager/LSLockManager.php create mode 100644 includes/htmlform/HTMLApiField.php create mode 100644 includes/htmlform/HTMLAutoCompleteSelectField.php create mode 100644 includes/htmlform/HTMLButtonField.php create mode 100644 includes/htmlform/HTMLCheckField.php create mode 100644 includes/htmlform/HTMLCheckMatrix.php create mode 100644 includes/htmlform/HTMLEditTools.php create mode 100644 includes/htmlform/HTMLFloatField.php create mode 100644 includes/htmlform/HTMLForm.php create mode 100644 includes/htmlform/HTMLFormField.php create mode 100644 includes/htmlform/HTMLFormFieldCloner.php create mode 100644 includes/htmlform/HTMLFormFieldRequiredOptionsException.php create mode 100644 includes/htmlform/HTMLHiddenField.php create mode 100644 includes/htmlform/HTMLInfoField.php create mode 100644 includes/htmlform/HTMLIntField.php create mode 100644 includes/htmlform/HTMLMultiSelectField.php create mode 100644 includes/htmlform/HTMLNestedFilterable.php create mode 100644 includes/htmlform/HTMLRadioField.php create mode 100644 includes/htmlform/HTMLSelectAndOtherField.php create mode 100644 includes/htmlform/HTMLSelectField.php create mode 100644 includes/htmlform/HTMLSelectLimitField.php create mode 100644 includes/htmlform/HTMLSelectOrOtherField.php create mode 100644 includes/htmlform/HTMLSubmitField.php create mode 100644 includes/htmlform/HTMLTextAreaField.php create mode 100644 includes/htmlform/HTMLTextField.php delete mode 100644 includes/installer/Installer.i18n.php create mode 100644 includes/installer/MssqlInstaller.php create mode 100644 includes/installer/MssqlUpdater.php create mode 100644 includes/installer/i18n/af.json create mode 100644 includes/installer/i18n/aln.json create mode 100644 includes/installer/i18n/am.json create mode 100644 includes/installer/i18n/an.json create mode 100644 includes/installer/i18n/ang.json create mode 100644 includes/installer/i18n/ar.json create mode 100644 includes/installer/i18n/arc.json create mode 100644 includes/installer/i18n/ary.json create mode 100644 includes/installer/i18n/arz.json create mode 100644 includes/installer/i18n/as.json create mode 100644 includes/installer/i18n/ast.json create mode 100644 includes/installer/i18n/av.json create mode 100644 includes/installer/i18n/avk.json create mode 100644 includes/installer/i18n/az.json create mode 100644 includes/installer/i18n/ba.json create mode 100644 includes/installer/i18n/bar.json create mode 100644 includes/installer/i18n/bcc.json create mode 100644 includes/installer/i18n/bcl.json create mode 100644 includes/installer/i18n/be-tarask.json create mode 100644 includes/installer/i18n/be.json create mode 100644 includes/installer/i18n/bg.json create mode 100644 includes/installer/i18n/bjn.json create mode 100644 includes/installer/i18n/bn.json create mode 100644 includes/installer/i18n/bpy.json create mode 100644 includes/installer/i18n/br.json create mode 100644 includes/installer/i18n/bs.json create mode 100644 includes/installer/i18n/bto.json create mode 100644 includes/installer/i18n/ca.json create mode 100644 includes/installer/i18n/ce.json create mode 100644 includes/installer/i18n/ceb.json create mode 100644 includes/installer/i18n/ckb.json create mode 100644 includes/installer/i18n/cps.json create mode 100644 includes/installer/i18n/crh-cyrl.json create mode 100644 includes/installer/i18n/crh-latn.json create mode 100644 includes/installer/i18n/cs.json create mode 100644 includes/installer/i18n/csb.json create mode 100644 includes/installer/i18n/cu.json create mode 100644 includes/installer/i18n/cv.json create mode 100644 includes/installer/i18n/cy.json create mode 100644 includes/installer/i18n/da.json create mode 100644 includes/installer/i18n/de-ch.json create mode 100644 includes/installer/i18n/de-formal.json create mode 100644 includes/installer/i18n/de.json create mode 100644 includes/installer/i18n/diq.json create mode 100644 includes/installer/i18n/dsb.json create mode 100644 includes/installer/i18n/dtp.json create mode 100644 includes/installer/i18n/el.json create mode 100644 includes/installer/i18n/en-gb.json create mode 100644 includes/installer/i18n/en.json create mode 100644 includes/installer/i18n/eo.json create mode 100644 includes/installer/i18n/es-formal.json create mode 100644 includes/installer/i18n/es.json create mode 100644 includes/installer/i18n/et.json create mode 100644 includes/installer/i18n/eu.json create mode 100644 includes/installer/i18n/ext.json create mode 100644 includes/installer/i18n/fa.json create mode 100644 includes/installer/i18n/fi.json create mode 100644 includes/installer/i18n/fo.json create mode 100644 includes/installer/i18n/fr.json create mode 100644 includes/installer/i18n/frc.json create mode 100644 includes/installer/i18n/frp.json create mode 100644 includes/installer/i18n/frr.json create mode 100644 includes/installer/i18n/fur.json create mode 100644 includes/installer/i18n/fy.json create mode 100644 includes/installer/i18n/ga.json create mode 100644 includes/installer/i18n/gag.json create mode 100644 includes/installer/i18n/gan-hans.json create mode 100644 includes/installer/i18n/gan-hant.json create mode 100644 includes/installer/i18n/gd.json create mode 100644 includes/installer/i18n/gl.json create mode 100644 includes/installer/i18n/gom-latn.json create mode 100644 includes/installer/i18n/grc.json create mode 100644 includes/installer/i18n/gsw.json create mode 100644 includes/installer/i18n/gu.json create mode 100644 includes/installer/i18n/gv.json create mode 100644 includes/installer/i18n/hak.json create mode 100644 includes/installer/i18n/haw.json create mode 100644 includes/installer/i18n/he.json create mode 100644 includes/installer/i18n/hi.json create mode 100644 includes/installer/i18n/hif-latn.json create mode 100644 includes/installer/i18n/hil.json create mode 100644 includes/installer/i18n/hr.json create mode 100644 includes/installer/i18n/hrx.json create mode 100644 includes/installer/i18n/hsb.json create mode 100644 includes/installer/i18n/ht.json create mode 100644 includes/installer/i18n/hu-formal.json create mode 100644 includes/installer/i18n/hu.json create mode 100644 includes/installer/i18n/hy.json create mode 100644 includes/installer/i18n/ia.json create mode 100644 includes/installer/i18n/id.json create mode 100644 includes/installer/i18n/ie.json create mode 100644 includes/installer/i18n/ig.json create mode 100644 includes/installer/i18n/ilo.json create mode 100644 includes/installer/i18n/io.json create mode 100644 includes/installer/i18n/is.json create mode 100644 includes/installer/i18n/it.json create mode 100644 includes/installer/i18n/ja.json create mode 100644 includes/installer/i18n/jam.json create mode 100644 includes/installer/i18n/jut.json create mode 100644 includes/installer/i18n/jv.json create mode 100644 includes/installer/i18n/ka.json create mode 100644 includes/installer/i18n/kaa.json create mode 100644 includes/installer/i18n/kbd-cyrl.json create mode 100644 includes/installer/i18n/khw.json create mode 100644 includes/installer/i18n/kiu.json create mode 100644 includes/installer/i18n/kk-arab.json create mode 100644 includes/installer/i18n/kk-cyrl.json create mode 100644 includes/installer/i18n/kk-latn.json create mode 100644 includes/installer/i18n/km.json create mode 100644 includes/installer/i18n/kn.json create mode 100644 includes/installer/i18n/ko.json create mode 100644 includes/installer/i18n/krc.json create mode 100644 includes/installer/i18n/ksh.json create mode 100644 includes/installer/i18n/ku-latn.json create mode 100644 includes/installer/i18n/lad.json create mode 100644 includes/installer/i18n/lb.json create mode 100644 includes/installer/i18n/lez.json create mode 100644 includes/installer/i18n/lfn.json create mode 100644 includes/installer/i18n/lg.json create mode 100644 includes/installer/i18n/li.json create mode 100644 includes/installer/i18n/lo.json create mode 100644 includes/installer/i18n/lrc.json create mode 100644 includes/installer/i18n/lt.json create mode 100644 includes/installer/i18n/lv.json create mode 100644 includes/installer/i18n/lzh.json create mode 100644 includes/installer/i18n/lzz.json create mode 100644 includes/installer/i18n/mai.json create mode 100644 includes/installer/i18n/mdf.json create mode 100644 includes/installer/i18n/mg.json create mode 100644 includes/installer/i18n/mhr.json create mode 100644 includes/installer/i18n/min.json create mode 100644 includes/installer/i18n/mk.json create mode 100644 includes/installer/i18n/ml.json create mode 100644 includes/installer/i18n/mn.json create mode 100644 includes/installer/i18n/mr.json create mode 100644 includes/installer/i18n/ms.json create mode 100644 includes/installer/i18n/mt.json create mode 100644 includes/installer/i18n/my.json create mode 100644 includes/installer/i18n/myv.json create mode 100644 includes/installer/i18n/mzn.json create mode 100644 includes/installer/i18n/nah.json create mode 100644 includes/installer/i18n/nan.json create mode 100644 includes/installer/i18n/nap.json create mode 100644 includes/installer/i18n/nb.json create mode 100644 includes/installer/i18n/nds-nl.json create mode 100644 includes/installer/i18n/nds.json create mode 100644 includes/installer/i18n/ne.json create mode 100644 includes/installer/i18n/nl-informal.json create mode 100644 includes/installer/i18n/nl.json create mode 100644 includes/installer/i18n/nn.json create mode 100644 includes/installer/i18n/oc.json create mode 100644 includes/installer/i18n/or.json create mode 100644 includes/installer/i18n/os.json create mode 100644 includes/installer/i18n/pa.json create mode 100644 includes/installer/i18n/pam.json create mode 100644 includes/installer/i18n/pcd.json create mode 100644 includes/installer/i18n/pdc.json create mode 100644 includes/installer/i18n/pl.json create mode 100644 includes/installer/i18n/pms.json create mode 100644 includes/installer/i18n/pnt.json create mode 100644 includes/installer/i18n/prg.json create mode 100644 includes/installer/i18n/ps.json create mode 100644 includes/installer/i18n/pt-br.json create mode 100644 includes/installer/i18n/pt.json create mode 100644 includes/installer/i18n/qqq.json create mode 100644 includes/installer/i18n/qu.json create mode 100644 includes/installer/i18n/rgn.json create mode 100644 includes/installer/i18n/rm.json create mode 100644 includes/installer/i18n/ro.json create mode 100644 includes/installer/i18n/roa-tara.json create mode 100644 includes/installer/i18n/ru.json create mode 100644 includes/installer/i18n/rue.json create mode 100644 includes/installer/i18n/sa.json create mode 100644 includes/installer/i18n/sah.json create mode 100644 includes/installer/i18n/sc.json create mode 100644 includes/installer/i18n/scn.json create mode 100644 includes/installer/i18n/sco.json create mode 100644 includes/installer/i18n/sdc.json create mode 100644 includes/installer/i18n/sei.json create mode 100644 includes/installer/i18n/sh.json create mode 100644 includes/installer/i18n/shi.json create mode 100644 includes/installer/i18n/si.json create mode 100644 includes/installer/i18n/sk.json create mode 100644 includes/installer/i18n/sl.json create mode 100644 includes/installer/i18n/sli.json create mode 100644 includes/installer/i18n/so.json create mode 100644 includes/installer/i18n/sq.json create mode 100644 includes/installer/i18n/sr-ec.json create mode 100644 includes/installer/i18n/sr-el.json create mode 100644 includes/installer/i18n/srn.json create mode 100644 includes/installer/i18n/ss.json create mode 100644 includes/installer/i18n/stq.json create mode 100644 includes/installer/i18n/su.json create mode 100644 includes/installer/i18n/sv.json create mode 100644 includes/installer/i18n/sw.json create mode 100644 includes/installer/i18n/szl.json create mode 100644 includes/installer/i18n/ta.json create mode 100644 includes/installer/i18n/tcy.json create mode 100644 includes/installer/i18n/te.json create mode 100644 includes/installer/i18n/tet.json create mode 100644 includes/installer/i18n/tg-cyrl.json create mode 100644 includes/installer/i18n/tg-latn.json create mode 100644 includes/installer/i18n/th.json create mode 100644 includes/installer/i18n/tk.json create mode 100644 includes/installer/i18n/tl.json create mode 100644 includes/installer/i18n/tly.json create mode 100644 includes/installer/i18n/tr.json create mode 100644 includes/installer/i18n/tt-cyrl.json create mode 100644 includes/installer/i18n/tt-latn.json create mode 100644 includes/installer/i18n/tyv.json create mode 100644 includes/installer/i18n/udm.json create mode 100644 includes/installer/i18n/ug-arab.json create mode 100644 includes/installer/i18n/uk.json create mode 100644 includes/installer/i18n/ur.json create mode 100644 includes/installer/i18n/uz.json create mode 100644 includes/installer/i18n/vec.json create mode 100644 includes/installer/i18n/vep.json create mode 100644 includes/installer/i18n/vi.json create mode 100644 includes/installer/i18n/vo.json create mode 100644 includes/installer/i18n/vro.json create mode 100644 includes/installer/i18n/wa.json create mode 100644 includes/installer/i18n/war.json create mode 100644 includes/installer/i18n/wo.json create mode 100644 includes/installer/i18n/wuu.json create mode 100644 includes/installer/i18n/xal.json create mode 100644 includes/installer/i18n/yi.json create mode 100644 includes/installer/i18n/yo.json create mode 100644 includes/installer/i18n/yue.json create mode 100644 includes/installer/i18n/zea.json create mode 100644 includes/installer/i18n/zh-hans.json create mode 100644 includes/installer/i18n/zh-hant.json create mode 100644 includes/installer/i18n/zh-hk.json create mode 100644 includes/installer/i18n/zh-tw.json delete mode 100644 includes/job/Job.php delete mode 100644 includes/job/JobQueue.php delete mode 100644 includes/job/JobQueueDB.php delete mode 100644 includes/job/JobQueueFederated.php delete mode 100644 includes/job/JobQueueGroup.php delete mode 100644 includes/job/JobQueueRedis.php delete mode 100644 includes/job/README delete mode 100644 includes/job/aggregator/JobQueueAggregator.php delete mode 100644 includes/job/aggregator/JobQueueAggregatorMemc.php delete mode 100644 includes/job/aggregator/JobQueueAggregatorRedis.php delete mode 100644 includes/job/jobs/AssembleUploadChunksJob.php delete mode 100644 includes/job/jobs/DoubleRedirectJob.php delete mode 100644 includes/job/jobs/DuplicateJob.php delete mode 100644 includes/job/jobs/EmaillingJob.php delete mode 100644 includes/job/jobs/EnotifNotifyJob.php delete mode 100644 includes/job/jobs/HTMLCacheUpdateJob.php delete mode 100644 includes/job/jobs/NullJob.php delete mode 100644 includes/job/jobs/PublishStashedFileJob.php delete mode 100644 includes/job/jobs/RefreshLinksJob.php delete mode 100644 includes/job/jobs/UploadFromUrlJob.php create mode 100644 includes/jobqueue/Job.php create mode 100644 includes/jobqueue/JobQueue.php create mode 100644 includes/jobqueue/JobQueueDB.php create mode 100644 includes/jobqueue/JobQueueFederated.php create mode 100644 includes/jobqueue/JobQueueGroup.php create mode 100644 includes/jobqueue/JobQueueRedis.php create mode 100644 includes/jobqueue/JobRunner.php create mode 100644 includes/jobqueue/JobSpecification.php create mode 100644 includes/jobqueue/README create mode 100644 includes/jobqueue/aggregator/JobQueueAggregator.php create mode 100644 includes/jobqueue/aggregator/JobQueueAggregatorMemc.php create mode 100644 includes/jobqueue/aggregator/JobQueueAggregatorRedis.php create mode 100644 includes/jobqueue/jobs/AssembleUploadChunksJob.php create mode 100644 includes/jobqueue/jobs/DoubleRedirectJob.php create mode 100644 includes/jobqueue/jobs/DuplicateJob.php create mode 100644 includes/jobqueue/jobs/EmaillingJob.php create mode 100644 includes/jobqueue/jobs/EnotifNotifyJob.php create mode 100644 includes/jobqueue/jobs/HTMLCacheUpdateJob.php create mode 100644 includes/jobqueue/jobs/NullJob.php create mode 100644 includes/jobqueue/jobs/PublishStashedFileJob.php create mode 100644 includes/jobqueue/jobs/RefreshLinksJob.php create mode 100644 includes/jobqueue/jobs/RefreshLinksJob2.php create mode 100644 includes/jobqueue/jobs/UploadFromUrlJob.php create mode 100644 includes/jobqueue/utils/BacklinkJobUtils.php create mode 100644 includes/libs/HashRing.php create mode 100644 includes/libs/IPSet.php create mode 100644 includes/libs/MWMessagePack.php create mode 100644 includes/libs/MappedIterator.php create mode 100644 includes/libs/MultiHttpClient.php create mode 100644 includes/libs/ProcessCacheLRU.php create mode 100644 includes/libs/RunningStat.php create mode 100644 includes/libs/ScopedCallback.php create mode 100644 includes/libs/ScopedPHPTimeout.php create mode 100644 includes/libs/XmlTypeCheck.php create mode 100644 includes/libs/virtualrest/SwiftVirtualRESTService.php create mode 100644 includes/libs/virtualrest/VirtualRESTService.php create mode 100644 includes/libs/virtualrest/VirtualRESTServiceClient.php create mode 100644 includes/logging/PageLangLogFormatter.php create mode 100644 includes/mail/EmailNotification.php create mode 100644 includes/mail/MailAddress.php create mode 100644 includes/mail/UserMailer.php create mode 100644 includes/media/TransformationalImageHandler.php delete mode 100644 includes/normal/Utf8Case.php delete mode 100644 includes/normal/Utf8CaseGenerate.php delete mode 100644 includes/objectcache/DBABagOStuff.php delete mode 100644 includes/objectcache/EhcacheBagOStuff.php create mode 100644 includes/page/Article.php create mode 100644 includes/page/CategoryPage.php create mode 100644 includes/page/ImagePage.php create mode 100644 includes/page/WikiCategoryPage.php create mode 100644 includes/page/WikiFilePage.php create mode 100644 includes/page/WikiPage.php create mode 100644 includes/pager/AlphabeticPager.php create mode 100644 includes/pager/IndexPager.php create mode 100644 includes/pager/Pager.php create mode 100644 includes/pager/ReverseChronologicalPager.php create mode 100644 includes/pager/TablePager.php create mode 100644 includes/parser/MWTidy.php create mode 100644 includes/parser/ParserDiffTest.php delete mode 100644 includes/parser/Parser_DiffTest.php delete mode 100644 includes/parser/Tidy.php create mode 100644 includes/password/BcryptPassword.php create mode 100644 includes/password/EncryptedPassword.php create mode 100644 includes/password/InvalidPassword.php create mode 100644 includes/password/LayeredParameterizedPassword.php create mode 100644 includes/password/MWOldPassword.php create mode 100644 includes/password/MWSaltedPassword.php create mode 100644 includes/password/ParameterizedPassword.php create mode 100644 includes/password/Password.php create mode 100644 includes/password/PasswordError.php create mode 100644 includes/password/PasswordFactory.php create mode 100644 includes/password/Pbkdf2Password.php create mode 100644 includes/poolcounter/PoolCounter.php create mode 100644 includes/poolcounter/PoolCounterRedis.php create mode 100644 includes/poolcounter/PoolCounterWork.php create mode 100644 includes/poolcounter/PoolCounterWorkViaCallback.php create mode 100644 includes/poolcounter/PoolWorkArticleView.php create mode 100644 includes/profiler/ProfilerMwprof.php delete mode 100644 includes/profiler/ProfilerSimple.php create mode 100644 includes/profiler/ProfilerSimpleDB.php create mode 100644 includes/profiler/ProfilerStandard.php create mode 100644 includes/rcfeed/MachineReadableRCFeedFormatter.php create mode 100644 includes/rcfeed/XMLRCFeedFormatter.php create mode 100644 includes/resourceloader/DerivativeResourceLoaderContext.php create mode 100644 includes/resourceloader/ResourceLoaderEditToolbarModule.php create mode 100644 includes/resourceloader/ResourceLoaderFilePath.php delete mode 100644 includes/resourceloader/ResourceLoaderLESSFunctions.php create mode 100644 includes/resourceloader/ResourceLoaderLanguageNamesModule.php create mode 100644 includes/revisiondelete/RevDelArchiveItem.php create mode 100644 includes/revisiondelete/RevDelArchiveList.php create mode 100644 includes/revisiondelete/RevDelArchivedFileItem.php create mode 100644 includes/revisiondelete/RevDelArchivedFileList.php create mode 100644 includes/revisiondelete/RevDelArchivedRevisionItem.php create mode 100644 includes/revisiondelete/RevDelFileItem.php create mode 100644 includes/revisiondelete/RevDelFileList.php create mode 100644 includes/revisiondelete/RevDelItem.php create mode 100644 includes/revisiondelete/RevDelList.php create mode 100644 includes/revisiondelete/RevDelLogItem.php create mode 100644 includes/revisiondelete/RevDelLogList.php create mode 100644 includes/revisiondelete/RevDelRevisionItem.php create mode 100644 includes/revisiondelete/RevDelRevisionList.php delete mode 100644 includes/revisiondelete/RevisionDelete.php delete mode 100644 includes/revisiondelete/RevisionDeleteAbstracts.php create mode 100644 includes/search/SearchDatabase.php create mode 100644 includes/search/SearchHighlighter.php create mode 100644 includes/search/SearchResult.php create mode 100644 includes/search/SearchResultSet.php delete mode 100644 includes/search/SearchUpdate.php create mode 100644 includes/skins/Skin.php create mode 100644 includes/skins/SkinException.php create mode 100644 includes/skins/SkinFactory.php create mode 100644 includes/skins/SkinFallback.php create mode 100644 includes/skins/SkinFallbackTemplate.php create mode 100644 includes/skins/SkinTemplate.php create mode 100644 includes/specialpage/ChangesListSpecialPage.php create mode 100644 includes/specialpage/FormSpecialPage.php create mode 100644 includes/specialpage/ImageQueryPage.php create mode 100644 includes/specialpage/IncludableSpecialPage.php create mode 100644 includes/specialpage/PageQueryPage.php create mode 100644 includes/specialpage/QueryPage.php create mode 100644 includes/specialpage/RedirectSpecialPage.php create mode 100644 includes/specialpage/SpecialPage.php create mode 100644 includes/specialpage/SpecialPageFactory.php create mode 100644 includes/specialpage/UnlistedSpecialPage.php create mode 100644 includes/specialpage/WantedQueryPage.php create mode 100644 includes/specials/SpecialAllMessages.php create mode 100644 includes/specials/SpecialAllPages.php delete mode 100644 includes/specials/SpecialAllmessages.php delete mode 100644 includes/specials/SpecialAllpages.php create mode 100644 includes/specials/SpecialCreateAccount.php create mode 100644 includes/specials/SpecialDiff.php create mode 100644 includes/specials/SpecialExpandTemplates.php create mode 100644 includes/specials/SpecialListDuplicatedFiles.php create mode 100644 includes/specials/SpecialMediaStatistics.php create mode 100644 includes/specials/SpecialMyLanguage.php create mode 100644 includes/specials/SpecialMyRedirectPages.php create mode 100644 includes/specials/SpecialPageLanguage.php create mode 100644 includes/specials/SpecialPermanentLink.php create mode 100644 includes/specials/SpecialRunJobs.php create mode 100644 includes/specials/SpecialTrackingCategories.php create mode 100644 includes/title/MalformedTitleException.php create mode 100644 includes/title/MediaWikiPageLinkRenderer.php create mode 100644 includes/title/MediaWikiTitleCodec.php create mode 100644 includes/title/PageLinkRenderer.php create mode 100644 includes/title/TitleFormatter.php create mode 100644 includes/title/TitleParser.php create mode 100644 includes/title/TitleValue.php create mode 100644 includes/utils/ArrayUtils.php create mode 100644 includes/utils/Cdb.php create mode 100644 includes/utils/CdbDBA.php create mode 100644 includes/utils/CdbPHP.php create mode 100644 includes/utils/IP.php create mode 100644 includes/utils/MWCryptHKDF.php create mode 100644 includes/utils/MWCryptRand.php create mode 100644 includes/utils/MWFunction.php create mode 100644 includes/utils/README create mode 100644 includes/utils/StringUtils.php create mode 100644 includes/utils/UIDGenerator.php create mode 100644 includes/utils/ZipDirectoryReader.php (limited to 'includes') diff --git a/includes/Action.php b/includes/Action.php deleted file mode 100644 index 4b6e4468..00000000 --- a/includes/Action.php +++ /dev/null @@ -1,600 +0,0 @@ -getActionOverrides() ); - - if ( is_string( $classOrCallable ) ) { - $obj = new $classOrCallable( $page, $context ); - return $obj; - } - - if ( is_callable( $classOrCallable ) ) { - return call_user_func_array( $classOrCallable, array( $page, $context ) ); - } - - return $classOrCallable; - } - - /** - * Get the action that will be executed, not necessarily the one passed - * passed through the "action" request parameter. Actions disabled in - * $wgActions will be replaced by "nosuchaction". - * - * @since 1.19 - * @param $context IContextSource - * @return string: action name - */ - final public static function getActionName( IContextSource $context ) { - global $wgActions; - - $request = $context->getRequest(); - $actionName = $request->getVal( 'action', 'view' ); - - // Check for disabled actions - if ( isset( $wgActions[$actionName] ) && $wgActions[$actionName] === false ) { - $actionName = 'nosuchaction'; - } - - // Workaround for bug #20966: inability of IE to provide an action dependent - // on which submit button is clicked. - if ( $actionName === 'historysubmit' ) { - if ( $request->getBool( 'revisiondelete' ) ) { - $actionName = 'revisiondelete'; - } else { - $actionName = 'view'; - } - } elseif ( $actionName == 'editredlink' ) { - $actionName = 'edit'; - } - - // Trying to get a WikiPage for NS_SPECIAL etc. will result - // in WikiPage::factory throwing "Invalid or virtual namespace -1 given." - // For SpecialPages et al, default to action=view. - if ( !$context->canUseWikiPage() ) { - return 'view'; - } - - $action = Action::factory( $actionName, $context->getWikiPage(), $context ); - if ( $action instanceof Action ) { - return $action->getName(); - } - - return 'nosuchaction'; - } - - /** - * Check if a given action is recognised, even if it's disabled - * - * @param string $name name of an action - * @return Bool - */ - final public static function exists( $name ) { - return self::getClass( $name, array() ) !== null; - } - - /** - * Get the IContextSource in use here - * @return IContextSource - */ - final public function getContext() { - if ( $this->context instanceof IContextSource ) { - return $this->context; - } else if ( $this->page instanceof Article ) { - // NOTE: $this->page can be a WikiPage, which does not have a context. - wfDebug( __METHOD__ . ': no context known, falling back to Article\'s context.' ); - return $this->page->getContext(); - } - - wfWarn( __METHOD__ . ': no context known, falling back to RequestContext::getMain().' ); - return RequestContext::getMain(); - } - - /** - * Get the WebRequest being used for this instance - * - * @return WebRequest - */ - final public function getRequest() { - return $this->getContext()->getRequest(); - } - - /** - * Get the OutputPage being used for this instance - * - * @return OutputPage - */ - final public function getOutput() { - return $this->getContext()->getOutput(); - } - - /** - * Shortcut to get the User being used for this instance - * - * @return User - */ - final public function getUser() { - return $this->getContext()->getUser(); - } - - /** - * Shortcut to get the Skin being used for this instance - * - * @return Skin - */ - final public function getSkin() { - return $this->getContext()->getSkin(); - } - - /** - * Shortcut to get the user Language being used for this instance - * - * @return Language - */ - final public function getLanguage() { - return $this->getContext()->getLanguage(); - } - - /** - * Shortcut to get the user Language being used for this instance - * - * @deprecated since 1.19 Use getLanguage instead - * @return Language - */ - final public function getLang() { - wfDeprecated( __METHOD__, '1.19' ); - return $this->getLanguage(); - } - - /** - * Shortcut to get the Title object from the page - * @return Title - */ - final public function getTitle() { - return $this->page->getTitle(); - } - - /** - * Get a Message object with context set - * Parameters are the same as wfMessage() - * - * @return Message object - */ - final public function msg() { - $params = func_get_args(); - return call_user_func_array( array( $this->getContext(), 'msg' ), $params ); - } - - /** - * Constructor. - * - * Only public since 1.21 - * - * @param $page Page - * @param $context IContextSource - */ - public function __construct( Page $page, IContextSource $context = null ) { - if ( $context === null ) { - wfWarn( __METHOD__ . ' called without providing a Context object.' ); - // NOTE: We could try to initialize $context using $page->getContext(), - // if $page is an Article. That however seems to not work seamlessly. - } - - $this->page = $page; - $this->context = $context; - } - - /** - * Return the name of the action this object responds to - * @return String lowercase - */ - abstract public function getName(); - - /** - * Get the permission required to perform this action. Often, but not always, - * the same as the action name - * @return String|null - */ - public function getRestriction() { - return null; - } - - /** - * 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 UserBlockedError|ReadOnlyError|PermissionsError - * @return bool True on success - */ - protected function checkCanExecute( User $user ) { - $right = $this->getRestriction(); - if ( $right !== null ) { - $errors = $this->getTitle()->getUserPermissionsErrors( $right, $user ); - if ( count( $errors ) ) { - throw new PermissionsError( $right, $errors ); - } - } - - if ( $this->requiresUnblock() && $user->isBlocked() ) { - $block = $user->getBlock(); - throw new UserBlockedError( $block ); - } - - // This should be checked at the end so that the user won't think the - // error is only temporary when he also don't have the rights to execute - // this action - if ( $this->requiresWrite() && wfReadOnly() ) { - throw new ReadOnlyError(); - } - return true; - } - - /** - * 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 $this->msg( strtolower( $this->getName() ) )->escaped(); - } - - /** - * 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 - */ - abstract public function show(); - - /** - * Execute the action in a silent fashion: do not display anything or release any errors. - * @return Bool whether execution was successful - */ - abstract public function execute(); -} - -/** - * An action which shows a form and does something based on the input from the form - */ -abstract class FormAction extends Action { - - /** - * Get an HTMLForm descriptor array - * @return Array - */ - abstract protected function getFormFields(); - - /** - * Add pre- or post-text to the form - * @return String HTML which will be sent to $form->addPreText() - */ - protected function preText() { - return ''; - } - - /** - * @return string - */ - 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 behavior - * @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(), $this->getName() ); - $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 - */ - abstract public 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). - */ - abstract public 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 - * behavior, 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() - * - * @param $data array|null - * @param $captureErrors bool - * @throws ErrorPageError|Exception - * @return bool - */ - public function execute( array $data = null, $captureErrors = true ) { - try { - // Set a new context so output doesn't leak. - $this->context = clone $this->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; - } - } - } -} - -/** - * An action which just does something, without showing a form first. - */ -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 - */ - abstract public function onView(); - - /** - * We don't want an HTMLForm - * @return bool - */ - protected function getFormFields() { - return false; - } - - /** - * @param $data Array - * @return bool - */ - public function onSubmit( $data ) { - return false; - } - - /** - * @return bool - */ - 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 array $data values that would normally be in the GET request - * @param bool $captureErrors whether to catch exceptions and just return false - * @throws ErrorPageError|Exception - * @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->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 c9ca1283..9bc92be9 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -47,15 +47,22 @@ class AjaxDispatcher { */ private $args; + /** + * @var Config + */ + private $config; + /** * Load up our object with user supplied data */ - function __construct() { + function __construct( Config $config ) { wfProfileIn( __METHOD__ ); + $this->config = $config; + $this->mode = ""; - if ( ! empty( $_GET["rs"] ) ) { + if ( !empty( $_GET["rs"] ) ) { $this->mode = "get"; } @@ -66,7 +73,7 @@ class AjaxDispatcher { switch ( $this->mode ) { case 'get': $this->func_name = isset( $_GET["rs"] ) ? $_GET["rs"] : ''; - if ( ! empty( $_GET["rsargs"] ) ) { + if ( !empty( $_GET["rsargs"] ) ) { $this->args = $_GET["rsargs"]; } else { $this->args = array(); @@ -74,7 +81,7 @@ class AjaxDispatcher { break; case 'post': $this->func_name = isset( $_POST["rs"] ) ? $_POST["rs"] : ''; - if ( ! empty( $_POST["rsargs"] ) ) { + if ( !empty( $_POST["rsargs"] ) ) { $this->args = $_POST["rsargs"]; } else { $this->args = array(); @@ -95,17 +102,17 @@ class AjaxDispatcher { * BEWARE! Data are passed as they have been supplied by the user, * they should be carefully handled in the function processing the * request. + * + * @param User $user */ - function performAction() { - global $wgAjaxExportList, $wgUser; - + function performAction( User $user ) { if ( empty( $this->mode ) ) { return; } wfProfileIn( __METHOD__ ); - if ( ! in_array( $this->func_name, $wgAjaxExportList ) ) { + if ( !in_array( $this->func_name, $this->config->get( 'AjaxExportList' ) ) ) { wfDebug( __METHOD__ . ' Bad Request for unknown function ' . $this->func_name . "\n" ); wfHttpError( @@ -113,7 +120,7 @@ class AjaxDispatcher { 'Bad Request', "unknown function " . $this->func_name ); - } elseif ( !User::isEveryoneAllowed( 'read' ) && !$wgUser->isAllowed( 'read' ) ) { + } elseif ( !User::isEveryoneAllowed( 'read' ) && !$user->isAllowed( 'read' ) ) { wfHttpError( 403, 'Forbidden', diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index d5536529..8e9f490f 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -28,7 +28,6 @@ * @ingroup Ajax */ class AjaxResponse { - /** * Number of seconds to get the response cached by a proxy * @var int $mCacheDuration @@ -49,7 +48,7 @@ class AjaxResponse { /** * Date for the HTTP header Last-modified - * @var string|false $mLastModified + * @var string|bool $mLastModified */ private $mLastModified; @@ -72,11 +71,18 @@ class AjaxResponse { private $mText; /** - * @param $text string|null + * @var Config */ - function __construct( $text = null ) { + private $mConfig; + + /** + * @param string|null $text + * @param Config|null $config + */ + function __construct( $text = null, Config $config = null ) { $this->mCacheDuration = null; $this->mVary = null; + $this->mConfig = $config ?: ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); $this->mDisabled = false; $this->mText = ''; @@ -91,7 +97,7 @@ class AjaxResponse { /** * Set the number of seconds to get the response cached by a proxy - * @param $duration int + * @param int $duration */ function setCacheDuration( $duration ) { $this->mCacheDuration = $duration; @@ -99,7 +105,7 @@ class AjaxResponse { /** * Set the HTTP Vary header - * @param $vary string + * @param string $vary */ function setVary( $vary ) { $this->mVary = $vary; @@ -107,7 +113,7 @@ class AjaxResponse { /** * Set the HTTP response code - * @param $code string + * @param string $code */ function setResponseCode( $code ) { $this->mResponseCode = $code; @@ -115,7 +121,7 @@ class AjaxResponse { /** * Set the HTTP header Content-Type - * @param $type string + * @param string $type */ function setContentType( $type ) { $this->mContentType = $type; @@ -130,10 +136,10 @@ class AjaxResponse { /** * Add content to the response - * @param $text string + * @param string $text */ function addText( $text ) { - if ( ! $this->mDisabled && $text ) { + if ( !$this->mDisabled && $text ) { $this->mText .= $text; } } @@ -142,7 +148,7 @@ class AjaxResponse { * Output text */ function printText() { - if ( ! $this->mDisabled ) { + if ( !$this->mDisabled ) { print $this->mText; } } @@ -151,8 +157,6 @@ class AjaxResponse { * Construct the header and output it */ function sendHeaders() { - global $wgUseSquid, $wgUseESI; - if ( $this->mResponseCode ) { $n = preg_replace( '/^ *(\d+)/', '\1', $this->mResponseCode ); header( "Status: " . $this->mResponseCode, true, (int)$n ); @@ -171,12 +175,12 @@ class AjaxResponse { # and tell the client to always check with the squid. Otherwise, # tell the client to use a cached copy, without a way to purge it. - if ( $wgUseSquid ) { + if ( $this->mConfig->get( 'UseSquid' ) ) { # Expect explicit purge of the proxy cache, but require end user agents # to revalidate against the proxy on each visit. # Surrogate-Control controls our Squid, Cache-Control downstream caches - if ( $wgUseESI ) { + if ( $this->mConfig->get( 'UseESI' ) ) { header( 'Surrogate-Control: max-age=' . $this->mCacheDuration . ', content="ESI/1.0"' ); header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); } else { @@ -184,10 +188,10 @@ class AjaxResponse { } } else { - # Let the client do the caching. Cache is not purged. header ( "Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT" ); - header ( "Cache-Control: s-maxage={$this->mCacheDuration},public,max-age={$this->mCacheDuration}" ); + header ( "Cache-Control: s-maxage={$this->mCacheDuration}," . + "public,max-age={$this->mCacheDuration}" ); } } else { @@ -207,7 +211,7 @@ class AjaxResponse { * possible. If successful, the AjaxResponse is disabled so that * any future call to AjaxResponse::printText() have no effect. * - * @param $timestamp string + * @param string $timestamp * @return bool Returns true if the response code was set to 304 Not Modified. */ function checkLastModified( $timestamp ) { @@ -215,17 +219,12 @@ class AjaxResponse { $fname = 'AjaxResponse::checkLastModified'; if ( !$timestamp || $timestamp == '19700101000000' ) { - wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP\n" ); + wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP\n", 'log' ); return false; } if ( !$wgCachePages ) { - wfDebug( "$fname: CACHE DISABLED\n", false ); - return false; - } - - if ( $wgUser->getOption( 'nocache' ) ) { - wfDebug( "$fname: USER DISABLED CACHE\n", false ); + wfDebug( "$fname: CACHE DISABLED\n", 'log' ); return false; } @@ -239,32 +238,37 @@ class AjaxResponse { $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] ); $modsinceTime = strtotime( $modsince ); $ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 ); - wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", false ); - wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", false ); + wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", 'log' ); + wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", 'log' ); - if ( ( $ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) { + if ( ( $ismodsince >= $timestamp ) + && $wgUser->validateCache( $ismodsince ) && + $ismodsince >= $wgCacheEpoch + ) { ini_set( 'zlib.output_compression', 0 ); $this->setResponseCode( "304 Not Modified" ); $this->disable(); $this->mLastModified = $lastmod; - wfDebug( "$fname: CACHED client: $ismodsince ; user: {$wgUser->getTouched()} ; page: $timestamp ; site $wgCacheEpoch\n", false ); + wfDebug( "$fname: CACHED client: $ismodsince ; user: {$wgUser->getTouched()} ; " . + "page: $timestamp ; site $wgCacheEpoch\n", 'log' ); return true; } else { - wfDebug( "$fname: READY client: $ismodsince ; user: {$wgUser->getTouched()} ; page: $timestamp ; site $wgCacheEpoch\n", false ); + wfDebug( "$fname: READY client: $ismodsince ; user: {$wgUser->getTouched()} ; " . + "page: $timestamp ; site $wgCacheEpoch\n", 'log' ); $this->mLastModified = $lastmod; } } else { - wfDebug( "$fname: client did not send If-Modified-Since header\n", false ); + wfDebug( "$fname: client did not send If-Modified-Since header\n", 'log' ); $this->mLastModified = $lastmod; } return false; } /** - * @param $mckey string - * @param $touched int + * @param string $mckey + * @param int $touched * @return bool */ function loadFromMemcached( $mckey, $touched ) { @@ -291,8 +295,8 @@ class AjaxResponse { } /** - * @param $mckey string - * @param $expiry int + * @param string $mckey + * @param int $expiry * @return bool */ function storeInMemcached( $mckey, $expiry = 86400 ) { diff --git a/includes/ArrayUtils.php b/includes/ArrayUtils.php deleted file mode 100644 index 985271f7..00000000 --- a/includes/ArrayUtils.php +++ /dev/null @@ -1,69 +0,0 @@ - $w ) { - $sum += $w; - # Do not return keys if they have 0 weight. - # Note that the "all 0 weight" case is handed above - if ( $w > 0 && $sum >= $rand ) { - break; - } - } - return $i; - } -} diff --git a/includes/Article.php b/includes/Article.php deleted file mode 100644 index 0b18221a..00000000 --- a/includes/Article.php +++ /dev/null @@ -1,2154 +0,0 @@ -mOldId = $oldId; - $this->mPage = $this->newPage( $title ); - } - - /** - * @param $title Title - * @return WikiPage - */ - protected function newPage( Title $title ) { - return new WikiPage( $title ); - } - - /** - * Constructor from a page id - * @param int $id article ID to load - * @return Article|null - */ - public static function newFromID( $id ) { - $t = Title::newFromID( $id ); - # @todo FIXME: Doesn't inherit right - return $t == null ? null : new self( $t ); - # return $t == null ? null : new static( $t ); // PHP 5.3 - } - - /** - * Create an Article object of the appropriate class for the given page. - * - * @param $title Title - * @param $context IContextSource - * @return Article object - */ - public static function newFromTitle( $title, IContextSource $context ) { - if ( NS_MEDIA == $title->getNamespace() ) { - // FIXME: where should this go? - $title = Title::makeTitle( NS_FILE, $title->getDBkey() ); - } - - $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 ); - - return $page; - } - - /** - * Create an Article object of the appropriate class for the given page. - * - * @param $page WikiPage - * @param $context IContextSource - * @return Article object - */ - 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; - } - - /** - * 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; - } - - /** - * Get the title object of the article - * - * @return Title object of this page - */ - public function getTitle() { - return $this->mPage->getTitle(); - } - - /** - * Get the WikiPage object of this instance - * - * @since 1.19 - * @return WikiPage - */ - public function getPage() { - return $this->mPage; - } - - /** - * Clear the object - */ - public function clear() { - $this->mContentLoaded = false; - - $this->mRedirectedFrom = null; # Title object if set - $this->mRevIdFetched = 0; - $this->mRedirectUrl = false; - - $this->mPage->clear(); - } - - /** - * Note that getContent/loadContent do not follow redirects anymore. - * If you need to fetch redirectable content easily, try - * the shortcut in WikiPage::getRedirectTarget() - * - * This function has side effects! Do not use this function if you - * only want the real revision text if any. - * - * @deprecated in 1.21; use WikiPage::getContent() instead - * - * @return string Return the text of this revision - */ - public function getContent() { - ContentHandler::deprecated( __METHOD__, '1.21' ); - $content = $this->getContentObject(); - return ContentHandler::getContentText( $content ); - } - - /** - * Returns a Content object representing the pages effective display content, - * not necessarily the revision's content! - * - * Note that getContent/loadContent do not follow redirects anymore. - * If you need to fetch redirectable content easily, try - * the shortcut in WikiPage::getRedirectTarget() - * - * This function has side effects! Do not use this function if you - * only want the real revision text if any. - * - * @return Content Return the content of this revision - * - * @since 1.21 - */ - protected function getContentObject() { - wfProfileIn( __METHOD__ ); - - 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->getTitle()->getNamespace() == NS_MEDIAWIKI ) { - $text = $this->getTitle()->getDefaultMessageText(); - if ( $text === false ) { - $text = ''; - } - - $content = ContentHandler::makeContent( $text, $this->getTitle() ); - } else { - $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon'; - $content = new MessageContent( $message, null, 'parsemag' ); - } - } else { - $this->fetchContentObject(); - $content = $this->mContentObject; - } - - wfProfileOut( __METHOD__ ); - return $content; - } - - /** - * @return int The oldid of the article that is to be shown, 0 for the - * current revision - */ - public function getOldID() { - if ( is_null( $this->mOldId ) ) { - $this->mOldId = $this->getOldIDFromRequest(); - } - - return $this->mOldId; - } - - /** - * Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect - * - * @return int The old id for the request - */ - public function getOldIDFromRequest() { - $this->mRedirectUrl = false; - - $request = $this->getContext()->getRequest(); - $oldid = $request->getIntOrNull( 'oldid' ); - - if ( $oldid === null ) { - return 0; - } - - if ( $oldid !== 0 ) { - # Load the given revision and check whether the page is another one. - # In that case, update this instance to reflect the change. - if ( $oldid === $this->mPage->getLatest() ) { - $this->mRevision = $this->mPage->getRevision(); - } else { - $this->mRevision = Revision::newFromId( $oldid ); - if ( $this->mRevision !== null ) { - // Revision title doesn't match the page title given? - if ( $this->mPage->getID() != $this->mRevision->getPage() ) { - $function = array( get_class( $this->mPage ), 'newFromID' ); - $this->mPage = call_user_func( $function, $this->mRevision->getPage() ); - } - } - } - } - - if ( $request->getVal( 'direction' ) == 'next' ) { - $nextid = $this->getTitle()->getNextRevisionID( $oldid ); - if ( $nextid ) { - $oldid = $nextid; - $this->mRevision = null; - } else { - $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' ); - } - } elseif ( $request->getVal( 'direction' ) == 'prev' ) { - $previd = $this->getTitle()->getPreviousRevisionID( $oldid ); - if ( $previd ) { - $oldid = $previd; - $this->mRevision = null; - } - } - - return $oldid; - } - - /** - * Load the revision (including text) into this object - * - * @deprecated in 1.19; use fetchContent() - */ - function loadContent() { - wfDeprecated( __METHOD__, '1.19' ); - $this->fetchContent(); - } - - /** - * Get text of an article from database - * Does *NOT* follow redirects. - * - * @protected - * @note this is really internal functionality that should really NOT be used by other functions. For accessing - * article content, use the WikiPage class, especially WikiBase::getContent(). However, a lot of legacy code - * uses this method to retrieve page text from the database, so the function has to remain public for now. - * - * @return mixed string containing article contents, or false if null - * @deprecated in 1.21, use WikiPage::getContent() instead - */ - function fetchContent() { #BC cruft! - ContentHandler::deprecated( __METHOD__, '1.21' ); - - if ( $this->mContentLoaded && $this->mContent ) { - return $this->mContent; - } - - wfProfileIn( __METHOD__ ); - - $content = $this->fetchContentObject(); - - // @todo Get rid of mContent everywhere! - $this->mContent = ContentHandler::getContentText( $content ); - ContentHandler::runLegacyHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); - - wfProfileOut( __METHOD__ ); - - return $this->mContent; - } - - /** - * Get text content object - * Does *NOT* follow redirects. - * TODO: when is this null? - * - * @note code that wants to retrieve page content from the database should use WikiPage::getContent(). - * - * @return Content|null|boolean false - * - * @since 1.21 - */ - protected function fetchContentObject() { - if ( $this->mContentLoaded ) { - return $this->mContentObject; - } - - wfProfileIn( __METHOD__ ); - - $this->mContentLoaded = true; - $this->mContent = null; - - $oldid = $this->getOldID(); - - # Pre-fill content with error message so that if something - # fails we'll have something telling us what we intended. - //XXX: this isn't page content but a UI message. horrible. - $this->mContentObject = new MessageContent( 'missing-revision', array( $oldid ), array() ); - - if ( $oldid ) { - # $this->mRevision might already be fetched by getOldIDFromRequest() - if ( !$this->mRevision ) { - $this->mRevision = Revision::newFromId( $oldid ); - if ( !$this->mRevision ) { - wfDebug( __METHOD__ . " failed to retrieve specified revision, id $oldid\n" ); - wfProfileOut( __METHOD__ ); - return false; - } - } - } else { - if ( !$this->mPage->getLatest() ) { - wfDebug( __METHOD__ . " failed to find page data for title " . $this->getTitle()->getPrefixedText() . "\n" ); - wfProfileOut( __METHOD__ ); - return false; - } - - $this->mRevision = $this->mPage->getRevision(); - - if ( !$this->mRevision ) { - wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" ); - wfProfileOut( __METHOD__ ); - return false; - } - } - - // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks. - // We should instead work with the Revision object when we need it... - $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER, $this->getContext()->getUser() ); // Loads if user is allowed - $this->mRevIdFetched = $this->mRevision->getId(); - - wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) ); - - wfProfileOut( __METHOD__ ); - - return $this->mContentObject; - } - - /** - * No-op - * @deprecated since 1.18 - */ - public function forUpdate() { - wfDeprecated( __METHOD__, '1.18' ); - } - - /** - * Returns true if the currently-referenced revision is the current edit - * to this page (and it exists). - * @return bool - */ - public function isCurrent() { - # If no oldid, this is the current version. - if ( $this->getOldID() == 0 ) { - return true; - } - - return $this->mPage->exists() && $this->mRevision && $this->mRevision->isCurrent(); - } - - /** - * Get the fetched Revision object depending on request parameters or null - * on failure. - * - * @since 1.19 - * @return Revision|null - */ - public function getRevisionFetched() { - $this->fetchContentObject(); - - return $this->mRevision; - } - - /** - * Use this to fetch the rev ID used on page views - * - * @return int revision ID of last article revision - */ - public function getRevIdFetched() { - if ( $this->mRevIdFetched ) { - return $this->mRevIdFetched; - } else { - return $this->mPage->getLatest(); - } - } - - /** - * This is the default action of the index.php entry point: just view the - * page of the given title. - */ - public function view() { - global $wgUseFileCache, $wgUseETag, $wgDebugToolbar; - - wfProfileIn( __METHOD__ ); - - # Get variables from query string - # As side effect this will load the revision and update the title - # in a revision ID is passed in the request, so this should remain - # the first call of this method even if $oldid is used way below. - $oldid = $this->getOldID(); - - $user = $this->getContext()->getUser(); - # Another whitelist check in case getOldID() is altering the title - $permErrors = $this->getTitle()->getUserPermissionsErrors( 'read', $user ); - if ( count( $permErrors ) ) { - wfDebug( __METHOD__ . ": denied on secondary read check\n" ); - wfProfileOut( __METHOD__ ); - throw new PermissionsError( 'read', $permErrors ); - } - - $outputPage = $this->getContext()->getOutput(); - # getOldID() may as well want us to redirect somewhere else - if ( $this->mRedirectUrl ) { - $outputPage->redirect( $this->mRedirectUrl ); - wfDebug( __METHOD__ . ": redirecting due to oldid\n" ); - wfProfileOut( __METHOD__ ); - - return; - } - - # If we got diff in the query, we want to see a diff page instead of the article. - if ( $this->getContext()->getRequest()->getCheck( 'diff' ) ) { - wfDebug( __METHOD__ . ": showing diff page\n" ); - $this->showDiffPage(); - wfProfileOut( __METHOD__ ); - - return; - } - - # Set page title (may be overridden by DISPLAYTITLE) - $outputPage->setPageTitle( $this->getTitle()->getPrefixedText() ); - - $outputPage->setArticleFlag( true ); - # Allow frames by default - $outputPage->allowClickjacking(); - - $parserCache = ParserCache::singleton(); - - $parserOptions = $this->getParserOptions(); - # Render printable version, use printable version cache - if ( $outputPage->isPrintable() ) { - $parserOptions->setIsPrintable( true ); - $parserOptions->setEditSection( false ); - } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit', $user ) ) { - $parserOptions->setEditSection( false ); - } - - # Try client and file cache - if ( !$wgDebugToolbar && $oldid === 0 && $this->mPage->checkTouched() ) { - if ( $wgUseETag ) { - $outputPage->setETag( $parserCache->getETag( $this, $parserOptions ) ); - } - - # Is it client cached? - if ( $outputPage->checkLastModified( $this->mPage->getTouched() ) ) { - wfDebug( __METHOD__ . ": done 304\n" ); - wfProfileOut( __METHOD__ ); - - return; - # Try file cache - } elseif ( $wgUseFileCache && $this->tryFileCache() ) { - wfDebug( __METHOD__ . ": done file cache\n" ); - # tell wgOut that output is taken care of - $outputPage->disable(); - $this->mPage->doViewUpdates( $user ); - wfProfileOut( __METHOD__ ); - - return; - } - } - - # Should the parser cache be used? - $useParserCache = $this->mPage->isParserCacheUsed( $parserOptions, $oldid ); - wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); - if ( $user->getStubThreshold() ) { - wfIncrStats( 'pcache_miss_stub' ); - } - - $this->showRedirectedFromHeader(); - $this->showNamespaceHeader(); - - # Iterate through the possible ways of constructing the output text. - # Keep going until $outputDone is set, or we run out of things to do. - $pass = 0; - $outputDone = false; - $this->mParserOutput = false; - - while ( !$outputDone && ++$pass ) { - switch ( $pass ) { - case 1: - wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$useParserCache ) ); - break; - case 2: - # Early abort if the page doesn't exist - if ( !$this->mPage->exists() ) { - wfDebug( __METHOD__ . ": showing missing article\n" ); - $this->showMissingArticle(); - wfProfileOut( __METHOD__ ); - return; - } - - # Try the parser cache - if ( $useParserCache ) { - $this->mParserOutput = $parserCache->get( $this, $parserOptions ); - - if ( $this->mParserOutput !== false ) { - if ( $oldid ) { - wfDebug( __METHOD__ . ": showing parser cache contents for current rev permalink\n" ); - $this->setOldSubtitle( $oldid ); - } else { - wfDebug( __METHOD__ . ": showing parser cache contents\n" ); - } - $outputPage->addParserOutput( $this->mParserOutput ); - # Ensure that UI elements requiring revision ID have - # the correct version information. - $outputPage->setRevisionId( $this->mPage->getLatest() ); - # Preload timestamp to avoid a DB hit - $cachedTimestamp = $this->mParserOutput->getTimestamp(); - if ( $cachedTimestamp !== null ) { - $outputPage->setRevisionTimestamp( $cachedTimestamp ); - $this->mPage->setTimestamp( $cachedTimestamp ); - } - $outputDone = true; - } - } - break; - case 3: - # This will set $this->mRevision if needed - $this->fetchContentObject(); - - # Are we looking at an old revision - if ( $oldid && $this->mRevision ) { - $this->setOldSubtitle( $oldid ); - - if ( !$this->showDeletedRevisionHeader() ) { - wfDebug( __METHOD__ . ": cannot view deleted revision\n" ); - wfProfileOut( __METHOD__ ); - return; - } - } - - # Ensure that UI elements requiring revision ID have - # the correct version information. - $outputPage->setRevisionId( $this->getRevIdFetched() ); - # Preload timestamp to avoid a DB hit - $outputPage->setRevisionTimestamp( $this->getTimestamp() ); - - # Pages containing custom CSS or JavaScript get special treatment - if ( $this->getTitle()->isCssOrJsPage() || $this->getTitle()->isCssJsSubpage() ) { - wfDebug( __METHOD__ . ": showing CSS/JS source\n" ); - $this->showCssOrJsPage(); - $outputDone = true; - } elseif ( !wfRunHooks( 'ArticleContentViewCustom', - array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) { - - # Allow extensions do their own custom view for certain pages - $outputDone = true; - } elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', - array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) { - - # Allow extensions do their own custom view for certain pages - $outputDone = true; - } else { - $content = $this->getContentObject(); - $rt = $content ? $content->getRedirectChain() : null; - if ( $rt ) { - wfDebug( __METHOD__ . ": showing redirect=no page\n" ); - # Viewing a redirect page (e.g. with parameter redirect=no) - $outputPage->addHTML( $this->viewRedirect( $rt ) ); - # Parse just to get categories, displaytitle, etc. - $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid, $parserOptions, false ); - $outputPage->addParserOutputNoText( $this->mParserOutput ); - $outputDone = true; - } - } - break; - case 4: - # Run the parse, protected by a pool counter - wfDebug( __METHOD__ . ": doing uncached parse\n" ); - - $poolArticleView = new PoolWorkArticleView( $this->getPage(), $parserOptions, - $this->getRevIdFetched(), $useParserCache, $this->getContentObject() ); - - if ( !$poolArticleView->execute() ) { - $error = $poolArticleView->getError(); - if ( $error ) { - $outputPage->clearHTML(); // for release() errors - $outputPage->enableClientCache( false ); - $outputPage->setRobotPolicy( 'noindex,nofollow' ); - - $errortext = $error->getWikiText( false, 'view-pool-error' ); - $outputPage->addWikiText( '
' . $errortext . '
' ); - } - # Connection or timeout error - wfProfileOut( __METHOD__ ); - return; - } - - $this->mParserOutput = $poolArticleView->getParserOutput(); - $outputPage->addParserOutput( $this->mParserOutput ); - - # Don't cache a dirty ParserOutput object - if ( $poolArticleView->getIsDirty() ) { - $outputPage->setSquidMaxage( 0 ); - $outputPage->addHTML( "\n" ); - } - - $outputDone = true; - break; - # Should be unreachable, but just in case... - default: - break 2; - } - } - - # Get the ParserOutput actually *displayed* here. - # Note that $this->mParserOutput is the *current* version output. - $pOutput = ( $outputDone instanceof ParserOutput ) - ? $outputDone // object fetched by hook - : $this->mParserOutput; - - # Adjust title for main page & pages with displaytitle - if ( $pOutput ) { - $this->adjustDisplayTitle( $pOutput ); - } - - # For the main page, overwrite the element with the con- - # 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->getTitle()->isMainPage() ) { - $msg = wfMessage( 'pagetitle-view-mainpage' )->inContentLanguage(); - if ( !$msg->isDisabled() ) { - $outputPage->setHTMLTitle( $msg->title( $this->getTitle() )->text() ); - } - } - - # Check for any __NOINDEX__ tags on the page using $pOutput - $policy = $this->getRobotPolicy( 'view', $pOutput ); - $outputPage->setIndexPolicy( $policy['index'] ); - $outputPage->setFollowPolicy( $policy['follow'] ); - - $this->showViewFooter(); - $this->mPage->doViewUpdates( $user ); - - $outputPage->addModules( 'mediawiki.action.view.postEdit' ); - - wfProfileOut( __METHOD__ ); - } - - /** - * Adjust title for pages with displaytitle, -{T|}- or language conversion - * @param $pOutput ParserOutput - */ - public function adjustDisplayTitle( ParserOutput $pOutput ) { - # Adjust the title if it was set by displaytitle, -{T|}- or language conversion - $titleText = $pOutput->getTitleText(); - if ( strval( $titleText ) !== '' ) { - $this->getContext()->getOutput()->setPageTitle( $titleText ); - } - } - - /** - * Show a diff page according to current request variables. For use within - * Article::view() only, other callers should use the DifferenceEngine class. - * - * @todo Make protected - */ - public function showDiffPage() { - $request = $this->getContext()->getRequest(); - $user = $this->getContext()->getUser(); - $diff = $request->getVal( 'diff' ); - $rcid = $request->getVal( 'rcid' ); - $diffOnly = $request->getBool( 'diffonly', $user->getOption( 'diffonly' ) ); - $purge = $request->getVal( 'action' ) == 'purge'; - $unhide = $request->getInt( 'unhide' ) == 1; - $oldid = $this->getOldID(); - - $rev = $this->getRevisionFetched(); - - if ( !$rev ) { - $this->getContext()->getOutput()->setPageTitle( wfMessage( 'errorpagetitle' ) ); - $this->getContext()->getOutput()->addWikiMsg( 'difference-missing-revision', $oldid, 1 ); - return; - } - - $contentHandler = $rev->getContentHandler(); - $de = $contentHandler->createDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide ); - - // DifferenceEngine directly fetched the revision: - $this->mRevIdFetched = $de->mNewid; - $de->showDiffPage( $diffOnly ); - - if ( $diff == 0 || $diff == $this->mPage->getLatest() ) { - # Run view updates for current revision only - $this->mPage->doViewUpdates( $user ); - } - } - - /** - * Show a page view for a page formatted as CSS or JavaScript. To be called by - * Article::view() only. - * - * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these - * page views. - * - * @param bool $showCacheHint whether to show a message telling the user to clear the browser cache (default: true). - */ - protected function showCssOrJsPage( $showCacheHint = true ) { - $outputPage = $this->getContext()->getOutput(); - - if ( $showCacheHint ) { - $dir = $this->getContext()->getLanguage()->getDir(); - $lang = $this->getContext()->getLanguage()->getCode(); - - $outputPage->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>", - 'clearyourcache' ); - } - - $this->fetchContentObject(); - - if ( $this->mContentObject ) { - // Give hooks a chance to customise the output - if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mContentObject, $this->getTitle(), $outputPage ) ) ) { - $po = $this->mContentObject->getParserOutput( $this->getTitle() ); - $outputPage->addHTML( $po->getText() ); - } - } - } - - /** - * Get the robot policy to be used for the current view - * @param string $action the action= GET parameter - * @param $pOutput ParserOutput|null - * @return Array the policy that should be set - * TODO: actions other than 'view' - */ - public function getRobotPolicy( $action, $pOutput = null ) { - global $wgArticleRobotPolicies, $wgNamespaceRobotPolicies, $wgDefaultRobotPolicy; - - $ns = $this->getTitle()->getNamespace(); - - # Don't index user and user talk pages for blocked users (bug 11443) - if ( ( $ns == NS_USER || $ns == NS_USER_TALK ) && !$this->getTitle()->isSubpage() ) { - $specificTarget = null; - $vagueTarget = null; - $titleText = $this->getTitle()->getText(); - if ( IP::isValid( $titleText ) ) { - $vagueTarget = $titleText; - } else { - $specificTarget = $titleText; - } - if ( Block::newFromTarget( $specificTarget, $vagueTarget ) instanceof Block ) { - return array( - 'index' => 'noindex', - 'follow' => 'nofollow' - ); - } - } - - if ( $this->mPage->getID() === 0 || $this->getOldID() ) { - # Non-articles (special pages etc), and old revisions - return array( - 'index' => 'noindex', - 'follow' => 'nofollow' - ); - } elseif ( $this->getContext()->getOutput()->isPrintable() ) { - # Discourage indexing of printable versions, but encourage following - return array( - 'index' => 'noindex', - 'follow' => 'follow' - ); - } elseif ( $this->getContext()->getRequest()->getInt( 'curid' ) ) { - # For ?curid=x urls, disallow indexing - return array( - 'index' => 'noindex', - 'follow' => 'follow' - ); - } - - # Otherwise, construct the policy based on the various config variables. - $policy = self::formatRobotPolicy( $wgDefaultRobotPolicy ); - - if ( isset( $wgNamespaceRobotPolicies[$ns] ) ) { - # Honour customised robot policies for this namespace - $policy = array_merge( - $policy, - self::formatRobotPolicy( $wgNamespaceRobotPolicies[$ns] ) - ); - } - if ( $this->getTitle()->canUseNoindex() && is_object( $pOutput ) && $pOutput->getIndexPolicy() ) { - # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates - # a final sanity check that we have really got the parser output. - $policy = array_merge( - $policy, - array( 'index' => $pOutput->getIndexPolicy() ) - ); - } - - 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->getTitle()->getPrefixedText()] ) - ); - } - - return $policy; - } - - /** - * Converts a String robot policy into an associative array, to allow - * merging of several policies using array_merge(). - * @param $policy Mixed, returns empty array on null/false/'', transparent - * to already-converted arrays, converts String. - * @return Array: 'index' => \<indexpolicy\>, 'follow' => \<followpolicy\> - */ - public static function formatRobotPolicy( $policy ) { - if ( is_array( $policy ) ) { - return $policy; - } elseif ( !$policy ) { - return array(); - } - - $policy = explode( ',', $policy ); - $policy = array_map( 'trim', $policy ); - - $arr = array(); - foreach ( $policy as $var ) { - if ( in_array( $var, array( 'index', 'noindex' ) ) ) { - $arr['index'] = $var; - } elseif ( in_array( $var, array( 'follow', 'nofollow' ) ) ) { - $arr['follow'] = $var; - } - } - - return $arr; - } - - /** - * If this request is a redirect view, send "redirected from" subtitle to - * the output. Returns true if the header was needed, false if this is not - * a redirect view. Handles both local and remote redirects. - * - * @return boolean - */ - public function showRedirectedFromHeader() { - global $wgRedirectSources; - $outputPage = $this->getContext()->getOutput(); - - $rdfrom = $this->getContext()->getRequest()->getVal( 'rdfrom' ); - - 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 = Linker::linkKnown( - $this->mRedirectedFrom, - null, - array(), - array( 'redirect' => 'no' ) - ); - - $outputPage->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) ); - - // Set the fragment if one was specified in the redirect - if ( strval( $this->getTitle()->getFragment() ) != '' ) { - $outputPage->addInlineScript( Xml::encodeJsCall( - 'redirectToFragment', array( $this->getTitle()->getFragmentForURL() ) - ) ); - } - - // Add a <link rel="canonical"> tag - $outputPage->setCanonicalUrl( $this->getTitle()->getLocalURL() ); - - // Tell the output object that the user arrived at this article through a redirect - $outputPage->setRedirectedFrom( $this->mRedirectedFrom ); - - return true; - } - } elseif ( $rdfrom ) { - // 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 = Linker::makeExternalLink( $rdfrom, $rdfrom ); - $outputPage->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) ); - - return true; - } - } - - return false; - } - - /** - * Show a header specific to the namespace currently being viewed, like - * [[MediaWiki:Talkpagetext]]. For Article::view(). - */ - public function showNamespaceHeader() { - if ( $this->getTitle()->isTalkPage() ) { - if ( !wfMessage( 'talkpageheader' )->isDisabled() ) { - $this->getContext()->getOutput()->wrapWikiMsg( "<div class=\"mw-talkpageheader\">\n$1\n</div>", array( 'talkpageheader' ) ); - } - } - } - - /** - * Show the footer section of an ordinary page view - */ - public function showViewFooter() { - # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page - if ( $this->getTitle()->getNamespace() == NS_USER_TALK && IP::isValid( $this->getTitle()->getText() ) ) { - $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' ); - } - - // Show a footer allowing the user to patrol the shown revision or page if possible - $patrolFooterShown = $this->showPatrolFooter(); - - wfRunHooks( 'ArticleViewFooter', array( $this, $patrolFooterShown ) ); - - } - - /** - * If patrol is possible, output a patrol UI box. This is called from the - * footer section of ordinary page views. If patrol is not possible or not - * desired, does nothing. - * Side effect: When the patrol link is build, this method will call - * OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax. - * - * @return bool - */ - public function showPatrolFooter() { - global $wgUseNPPatrol, $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI; - - $outputPage = $this->getContext()->getOutput(); - $user = $this->getContext()->getUser(); - $cache = wfGetMainCache(); - $rc = false; - - if ( !$this->getTitle()->quickUserCan( 'patrol', $user ) || !( $wgUseRCPatrol || $wgUseNPPatrol ) ) { - // Patrolling is disabled or the user isn't allowed to - return false; - } - - wfProfileIn( __METHOD__ ); - - // New page patrol: Get the timestamp of the oldest revison which - // the revision table holds for the given page. Then we look - // whether it's within the RC lifespan and if it is, we try - // to get the recentchanges row belonging to that entry - // (with rc_new = 1). - - // Check for cached results - if ( $cache->get( wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() ) ) ) { - wfProfileOut( __METHOD__ ); - return false; - } - - if ( $this->mRevision && !RecentChange::isInRCLifespan( $this->mRevision->getTimestamp(), 21600 ) ) { - // The current revision is already older than what could be in the RC table - // 6h tolerance because the RC might not be cleaned out regularly - wfProfileOut( __METHOD__ ); - return false; - } - - $dbr = wfGetDB( DB_SLAVE ); - $oldestRevisionTimestamp = $dbr->selectField( - 'revision', - 'MIN( rev_timestamp )', - array( 'rev_page' => $this->getTitle()->getArticleID() ), - __METHOD__ - ); - - if ( $oldestRevisionTimestamp && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 ) ) { - // 6h tolerance because the RC might not be cleaned out regularly - $rc = RecentChange::newFromConds( - array( - 'rc_new' => 1, - 'rc_timestamp' => $oldestRevisionTimestamp, - 'rc_namespace' => $this->getTitle()->getNamespace(), - 'rc_cur_id' => $this->getTitle()->getArticleID(), - 'rc_patrolled' => 0 - ), - __METHOD__, - array( 'USE INDEX' => 'new_name_timestamp' ) - ); - } - - if ( !$rc ) { - // No RC entry around - - // Cache the information we gathered above in case we can't patrol - // Don't cache in case we can patrol as this could change - $cache->set( wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() ), '1' ); - - wfProfileOut( __METHOD__ ); - return false; - } - - if ( $rc->getPerformer()->getName() == $user->getName() ) { - // Don't show a patrol link for own creations. If the user could - // patrol them, they already would be patrolled - wfProfileOut( __METHOD__ ); - return false; - } - - $rcid = $rc->getAttribute( 'rc_id' ); - - $token = $user->getEditToken( $rcid ); - - $outputPage->preventClickjacking(); - if ( $wgEnableAPI && $wgEnableWriteAPI && $user->isAllowed( 'writeapi' ) ) { - $outputPage->addModules( 'mediawiki.page.patrol.ajax' ); - } - - $link = Linker::linkKnown( - $this->getTitle(), - wfMessage( 'markaspatrolledtext' )->escaped(), - array(), - array( - 'action' => 'markpatrolled', - 'rcid' => $rcid, - 'token' => $token, - ) - ); - - $outputPage->addHTML( - "<div class='patrollink'>" . - wfMessage( 'markaspatrolledlink' )->rawParams( $link )->escaped() . - '</div>' - ); - - wfProfileOut( __METHOD__ ); - return true; - } - - /** - * Show the error text for a missing article. For articles in the MediaWiki - * namespace, show the default message text. To be called from Article::view(). - */ - public function showMissingArticle() { - global $wgSend404Code; - $outputPage = $this->getContext()->getOutput(); - // Whether the page is a root user page of an existing user (but not a subpage) - $validUserPage = false; - - # Show info in user (talk) namespace. Does the user exist? Is he blocked? - if ( $this->getTitle()->getNamespace() == NS_USER || $this->getTitle()->getNamespace() == NS_USER_TALK ) { - $parts = explode( '/', $this->getTitle()->getText() ); - $rootPart = $parts[0]; - $user = User::newFromName( $rootPart, false /* allow IP users*/ ); - $ip = User::isIP( $rootPart ); - - if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist - $outputPage->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>", - array( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) ) ); - } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked - LogEventsList::showLogExtract( - $outputPage, - 'block', - $user->getUserPage(), - '', - array( - 'lim' => 1, - 'showIfEmpty' => false, - 'msgKey' => array( - 'blocked-notice-logextract', - $user->getName() # Support GENDER in notice - ) - ) - ); - $validUserPage = !$this->getTitle()->isSubpage(); - } else { - $validUserPage = !$this->getTitle()->isSubpage(); - } - } - - wfRunHooks( 'ShowMissingArticle', array( $this ) ); - - # Show delete and move logs - LogEventsList::showLogExtract( $outputPage, array( 'delete', 'move' ), $this->getTitle(), '', - array( 'lim' => 10, - 'conds' => array( "log_action != 'revision'" ), - 'showIfEmpty' => false, - 'msgKey' => array( 'moveddeleted-notice' ) ) - ); - - if ( !$this->mPage->hasViewableContent() && $wgSend404Code && !$validUserPage ) { - // If there's no backing content, send a 404 Not Found - // for better machine handling of broken links. - $this->getContext()->getRequest()->response()->header( "HTTP/1.1 404 Not Found" ); - } - - if ( $validUserPage ) { - // Also apply the robot policy for nonexisting user pages (as those aren't served as 404) - $policy = $this->getRobotPolicy( 'view' ); - $outputPage->setIndexPolicy( $policy['index'] ); - $outputPage->setFollowPolicy( $policy['follow'] ); - } - - $hookResult = wfRunHooks( 'BeforeDisplayNoArticleText', array( $this ) ); - - if ( ! $hookResult ) { - return; - } - - # Show error message - $oldid = $this->getOldID(); - if ( $oldid ) { - $text = wfMessage( 'missing-revision', $oldid )->plain(); - } elseif ( $this->getTitle()->getNamespace() === NS_MEDIAWIKI ) { - // Use the default message text - $text = $this->getTitle()->getDefaultMessageText(); - } elseif ( $this->getTitle()->quickUserCan( 'create', $this->getContext()->getUser() ) - && $this->getTitle()->quickUserCan( 'edit', $this->getContext()->getUser() ) - ) { - $text = wfMessage( 'noarticletext' )->plain(); - } else { - $text = wfMessage( 'noarticletext-nopermission' )->plain(); - } - $text = "<div class='noarticletext'>\n$text\n</div>"; - - $outputPage->addWikiText( $text ); - } - - /** - * If the revision requested for view is deleted, check permissions. - * Send either an error message or a warning header to the output. - * - * @return boolean true if the view is allowed, false if not. - */ - public function showDeletedRevisionHeader() { - if ( !$this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { - // Not deleted - return true; - } - - $outputPage = $this->getContext()->getOutput(); - $user = $this->getContext()->getUser(); - // If the user is not allowed to see it... - if ( !$this->mRevision->userCan( Revision::DELETED_TEXT, $user ) ) { - $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", - 'rev-deleted-text-permission' ); - - return false; - // If the user needs to confirm that they want to see it... - } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) != 1 ) { - # Give explanation and add a link to view the revision... - $oldid = intval( $this->getOldID() ); - $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" ); - $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ? - 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide'; - $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", - array( $msg, $link ) ); - - return false; - // We are allowed to see... - } else { - $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ? - 'rev-suppressed-text-view' : 'rev-deleted-text-view'; - $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", $msg ); - - return true; - } - } - - /** - * Generate the navigation links when browsing through an article revisions - * It shows the information as: - * Revision as of \<date\>; view current revision - * \<- Previous version | Next Version -\> - * - * @param int $oldid revision ID of this article revision - */ - public function setOldSubtitle( $oldid = 0 ) { - if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) { - return; - } - - $unhide = $this->getContext()->getRequest()->getInt( 'unhide' ) == 1; - - # Cascade unhide param in links for easy deletion browsing - $extraParams = array(); - if ( $unhide ) { - $extraParams['unhide'] = 1; - } - - if ( $this->mRevision && $this->mRevision->getId() === $oldid ) { - $revision = $this->mRevision; - } else { - $revision = Revision::newFromId( $oldid ); - } - - $timestamp = $revision->getTimestamp(); - - $current = ( $oldid == $this->mPage->getLatest() ); - $language = $this->getContext()->getLanguage(); - $user = $this->getContext()->getUser(); - - $td = $language->userTimeAndDate( $timestamp, $user ); - $tddate = $language->userDate( $timestamp, $user ); - $tdtime = $language->userTime( $timestamp, $user ); - - # Show user links if allowed to see them. If hidden, then show them only if requested... - $userlinks = Linker::revUserTools( $revision, !$unhide ); - - $infomsg = $current && !wfMessage( 'revision-info-current' )->isDisabled() - ? 'revision-info-current' - : 'revision-info'; - - $outputPage = $this->getContext()->getOutput(); - $outputPage->addSubtitle( "<div id=\"mw-{$infomsg}\">" . wfMessage( $infomsg, - $td )->rawParams( $userlinks )->params( $revision->getID(), $tddate, - $tdtime, $revision->getUser() )->parse() . "</div>" ); - - $lnk = $current - ? wfMessage( 'currentrevisionlink' )->escaped() - : Linker::linkKnown( - $this->getTitle(), - wfMessage( 'currentrevisionlink' )->escaped(), - array(), - $extraParams - ); - $curdiff = $current - ? wfMessage( 'diff' )->escaped() - : Linker::linkKnown( - $this->getTitle(), - wfMessage( 'diff' )->escaped(), - array(), - array( - 'diff' => 'cur', - 'oldid' => $oldid - ) + $extraParams - ); - $prev = $this->getTitle()->getPreviousRevisionID( $oldid ); - $prevlink = $prev - ? Linker::linkKnown( - $this->getTitle(), - wfMessage( 'previousrevision' )->escaped(), - array(), - array( - 'direction' => 'prev', - 'oldid' => $oldid - ) + $extraParams - ) - : wfMessage( 'previousrevision' )->escaped(); - $prevdiff = $prev - ? Linker::linkKnown( - $this->getTitle(), - wfMessage( 'diff' )->escaped(), - array(), - array( - 'diff' => 'prev', - 'oldid' => $oldid - ) + $extraParams - ) - : wfMessage( 'diff' )->escaped(); - $nextlink = $current - ? wfMessage( 'nextrevision' )->escaped() - : Linker::linkKnown( - $this->getTitle(), - wfMessage( 'nextrevision' )->escaped(), - array(), - array( - 'direction' => 'next', - 'oldid' => $oldid - ) + $extraParams - ); - $nextdiff = $current - ? wfMessage( 'diff' )->escaped() - : Linker::linkKnown( - $this->getTitle(), - wfMessage( 'diff' )->escaped(), - array(), - array( - 'diff' => 'next', - 'oldid' => $oldid - ) + $extraParams - ); - - $cdel = Linker::getRevDeleteLink( $user, $revision, $this->getTitle() ); - if ( $cdel !== '' ) { - $cdel .= ' '; - } - - $outputPage->addSubtitle( "<div id=\"mw-revision-nav\">" . $cdel . - wfMessage( 'revision-nav' )->rawParams( - $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff - )->escaped() . "</div>" ); - } - - /** - * View redirect - * - * @param $target Title|Array of destination(s) to redirect - * @param $appendSubtitle Boolean [optional] - * @param $forceKnown Boolean: should the image be shown as a bluelink regardless of existence? - * @return string containing HMTL with redirect link - */ - public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) { - global $wgStylePath; - - if ( !is_array( $target ) ) { - $target = array( $target ); - } - - $lang = $this->getTitle()->getPageLanguage(); - $imageDir = $lang->getDir(); - - if ( $appendSubtitle ) { - $out = $this->getContext()->getOutput(); - $out->addSubtitle( wfMessage( 'redirectpagesub' )->escaped() ); - } - - // the loop prepends the arrow image before the link, so the first case needs to be outside - - /** - * @var $title Title - */ - $title = array_shift( $target ); - - if ( $forceKnown ) { - $link = Linker::linkKnown( $title, htmlspecialchars( $title->getFullText() ) ); - } else { - $link = Linker::link( $title, htmlspecialchars( $title->getFullText() ) ); - } - - $nextRedirect = $wgStylePath . '/common/images/nextredirect' . $imageDir . '.png'; - $alt = $lang->isRTL() ? '←' : '→'; - // Automatically append redirect=no to each link, since most of them are redirect pages themselves. - foreach ( $target as $rt ) { - $link .= Html::element( 'img', array( 'src' => $nextRedirect, 'alt' => $alt ) ); - if ( $forceKnown ) { - $link .= Linker::linkKnown( $rt, htmlspecialchars( $rt->getFullText(), array(), array( 'redirect' => 'no' ) ) ); - } else { - $link .= Linker::link( $rt, htmlspecialchars( $rt->getFullText() ), array(), array( 'redirect' => 'no' ) ); - } - } - - $imageUrl = $wgStylePath . '/common/images/redirect' . $imageDir . '.png'; - return '<div class="redirectMsg">' . - Html::element( 'img', array( 'src' => $imageUrl, 'alt' => '#REDIRECT' ) ) . - '<span class="redirectText">' . $link . '</span></div>'; - } - - /** - * Handle action=render - */ - public function render() { - $this->getContext()->getOutput()->setArticleBodyOnly( true ); - $this->view(); - } - - /** - * action=protect handler - */ - public function protect() { - $form = new ProtectionForm( $this ); - $form->execute(); - } - - /** - * action=unprotect handler (alias) - */ - public function unprotect() { - $this->protect(); - } - - /** - * UI entry point for page deletion - */ - public function delete() { - # This code desperately needs to be totally rewritten - - $title = $this->getTitle(); - $user = $this->getContext()->getUser(); - - # Check permissions - $permission_errors = $title->getUserPermissionsErrors( 'delete', $user ); - if ( count( $permission_errors ) ) { - throw new PermissionsError( 'delete', $permission_errors ); - } - - # Read-only check... - if ( wfReadOnly() ) { - throw new ReadOnlyError; - } - - # Better double-check that it hasn't been deleted yet! - $this->mPage->loadPageData( 'fromdbmaster' ); - if ( !$this->mPage->exists() ) { - $deleteLogPage = new LogPage( 'delete' ); - $outputPage = $this->getContext()->getOutput(); - $outputPage->setPageTitle( wfMessage( 'cannotdelete-title', $title->getPrefixedText() ) ); - $outputPage->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>", - array( 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) ) - ); - $outputPage->addHTML( - Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) - ); - LogEventsList::showLogExtract( - $outputPage, - 'delete', - $title - ); - - return; - } - - $request = $this->getContext()->getRequest(); - $deleteReasonList = $request->getText( 'wpDeleteReasonList', 'other' ); - $deleteReason = $request->getText( 'wpReason' ); - - if ( $deleteReasonList == 'other' ) { - $reason = $deleteReason; - } elseif ( $deleteReason != '' ) { - // Entry from drop down menu + additional comment - $colonseparator = wfMessage( 'colon-separator' )->inContentLanguage()->text(); - $reason = $deleteReasonList . $colonseparator . $deleteReason; - } else { - $reason = $deleteReasonList; - } - - if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ), - array( 'delete', $this->getTitle()->getPrefixedText() ) ) ) - { - # Flag to hide all contents of the archived revisions - $suppress = $request->getVal( 'wpSuppress' ) && $user->isAllowed( 'suppressrevision' ); - - $this->doDelete( $reason, $suppress ); - - WatchAction::doWatchOrUnwatch( $request->getCheck( 'wpWatch' ), $title, $user ); - - return; - } - - // Generate deletion reason - $hasHistory = false; - if ( !$reason ) { - try { - $reason = $this->generateReason( $hasHistory ); - } catch ( MWException $e ) { - # if a page is horribly broken, we still want to be able to delete it. so be lenient about errors here. - wfDebug( "Error while building auto delete summary: $e" ); - $reason = ''; - } - } - - // If the page has a history, insert a warning - if ( $hasHistory ) { - $revisions = $this->mTitle->estimateRevisionCount(); - // @todo FIXME: i18n issue/patchwork message - $this->getContext()->getOutput()->addHTML( '<strong class="mw-delete-warning-revisions">' . - wfMessage( 'historywarning' )->numParams( $revisions )->parse() . - wfMessage( 'word-separator' )->plain() . Linker::linkKnown( $title, - wfMessage( 'history' )->escaped(), - array( 'rel' => 'archives' ), - array( 'action' => 'history' ) ) . - '</strong>' - ); - - if ( $this->mTitle->isBigDeletion() ) { - global $wgDeleteRevisionsLimit; - $this->getContext()->getOutput()->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n", - array( 'delete-warning-toobig', $this->getContext()->getLanguage()->formatNum( $wgDeleteRevisionsLimit ) ) ); - } - } - - $this->confirmDelete( $reason ); - } - - /** - * Output deletion confirmation dialog - * @todo FIXME: Move to another file? - * @param string $reason prefilled reason - */ - public function confirmDelete( $reason ) { - wfDebug( "Article::confirmDelete\n" ); - - $outputPage = $this->getContext()->getOutput(); - $outputPage->setPageTitle( wfMessage( 'delete-confirm', $this->getTitle()->getPrefixedText() ) ); - $outputPage->addBacklinkSubtitle( $this->getTitle() ); - $outputPage->setRobotPolicy( 'noindex,nofollow' ); - $outputPage->addWikiMsg( 'confirmdeletetext' ); - - wfRunHooks( 'ArticleConfirmDelete', array( $this, $outputPage, &$reason ) ); - - $user = $this->getContext()->getUser(); - - if ( $user->isAllowed( 'suppressrevision' ) ) { - $suppress = "<tr id=\"wpDeleteSuppressRow\"> - <td></td> - <td class='mw-input'><strong>" . - Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(), - 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) . - "</strong></td> - </tr>"; - } else { - $suppress = ''; - } - $checkWatch = $user->getBoolOption( 'watchdeletion' ) || $user->isWatched( $this->getTitle() ); - - $form = Xml::openElement( 'form', array( 'method' => 'post', - 'action' => $this->getTitle()->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) . - Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) . - Xml::tags( 'legend', null, wfMessage( 'delete-legend' )->escaped() ) . - Xml::openElement( 'table', array( 'id' => 'mw-deleteconfirm-table' ) ) . - "<tr id=\"wpDeleteReasonListRow\"> - <td class='mw-label'>" . - Xml::label( wfMessage( 'deletecomment' )->text(), 'wpDeleteReasonList' ) . - "</td> - <td class='mw-input'>" . - Xml::listDropDown( 'wpDeleteReasonList', - wfMessage( 'deletereason-dropdown' )->inContentLanguage()->text(), - wfMessage( 'deletereasonotherlist' )->inContentLanguage()->text(), '', 'wpReasonDropDown', 1 ) . - "</td> - </tr> - <tr id=\"wpDeleteReasonRow\"> - <td class='mw-label'>" . - Xml::label( wfMessage( 'deleteotherreason' )->text(), 'wpReason' ) . - "</td> - <td class='mw-input'>" . - Html::input( 'wpReason', $reason, 'text', array( - 'size' => '60', - 'maxlength' => '255', - 'tabindex' => '2', - 'id' => 'wpReason', - 'autofocus' - ) ) . - "</td> - </tr>"; - - # Disallow watching if user is not logged in - if ( $user->isLoggedIn() ) { - $form .= " - <tr> - <td></td> - <td class='mw-input'>" . - Xml::checkLabel( wfMessage( 'watchthis' )->text(), - 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) . - "</td> - </tr>"; - } - - $form .= " - $suppress - <tr> - <td></td> - <td class='mw-submit'>" . - Xml::submitButton( wfMessage( 'deletepage' )->text(), - array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '5' ) ) . - "</td> - </tr>" . - Xml::closeElement( 'table' ) . - Xml::closeElement( 'fieldset' ) . - Html::hidden( 'wpEditToken', $user->getEditToken( array( 'delete', $this->getTitle()->getPrefixedText() ) ) ) . - Xml::closeElement( 'form' ); - - if ( $user->isAllowed( 'editinterface' ) ) { - $title = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' ); - $link = Linker::link( - $title, - wfMessage( 'delete-edit-reasonlist' )->escaped(), - array(), - array( 'action' => 'edit' ) - ); - $form .= '<p class="mw-delete-editreasons">' . $link . '</p>'; - } - - $outputPage->addHTML( $form ); - - $deleteLogPage = new LogPage( 'delete' ); - $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) ); - LogEventsList::showLogExtract( $outputPage, 'delete', - $this->getTitle() - ); - } - - /** - * Perform a deletion and output success or failure messages - * @param $reason - * @param $suppress bool - */ - public function doDelete( $reason, $suppress = false ) { - $error = ''; - $outputPage = $this->getContext()->getOutput(); - $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error ); - if ( $status->isGood() ) { - $deleted = $this->getTitle()->getPrefixedText(); - - $outputPage->setPageTitle( wfMessage( 'actioncomplete' ) ); - $outputPage->setRobotPolicy( 'noindex,nofollow' ); - - $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]'; - - $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink ); - $outputPage->returnToMain( false ); - } else { - $outputPage->setPageTitle( wfMessage( 'cannotdelete-title', $this->getTitle()->getPrefixedText() ) ); - if ( $error == '' ) { - $outputPage->addWikiText( - "<div class=\"error mw-error-cannotdelete\">\n" . $status->getWikiText() . "\n</div>" - ); - $deleteLogPage = new LogPage( 'delete' ); - $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) ); - - LogEventsList::showLogExtract( - $outputPage, - 'delete', - $this->getTitle() - ); - } else { - $outputPage->addHTML( $error ); - } - } - } - - /* Caching functions */ - - /** - * checkLastModified returns true if it has taken care of all - * output to the client that is necessary for this request. - * (that is, it has sent a cached version of the page) - * - * @return boolean true if cached version send, false otherwise - */ - protected function tryFileCache() { - static $called = false; - - if ( $called ) { - wfDebug( "Article::tryFileCache(): called twice!?\n" ); - return false; - } - - $called = true; - if ( $this->isFileCacheable() ) { - $cache = HTMLFileCache::newFromTitle( $this->getTitle(), 'view' ); - if ( $cache->isCacheGood( $this->mPage->getTouched() ) ) { - wfDebug( "Article::tryFileCache(): about to load file\n" ); - $cache->loadFromFileCache( $this->getContext() ); - return true; - } else { - wfDebug( "Article::tryFileCache(): starting buffer\n" ); - ob_start( array( &$cache, 'saveToFileCache' ) ); - } - } else { - wfDebug( "Article::tryFileCache(): not cacheable\n" ); - } - - return false; - } - - /** - * Check if the page can be cached - * @return bool - */ - public function isFileCacheable() { - $cacheable = false; - - if ( HTMLFileCache::useFileCache( $this->getContext() ) ) { - $cacheable = $this->mPage->getID() - && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect(); - // Extension may have reason to disable file caching on some pages. - if ( $cacheable ) { - $cacheable = wfRunHooks( 'IsFileCacheable', array( &$this ) ); - } - } - - return $cacheable; - } - - /**#@-*/ - - /** - * Lightweight method to get the parser output for a page, checking the parser cache - * and so on. Doesn't consider most of the stuff that WikiPage::view is forced to - * consider, so it's not appropriate to use there. - * - * @since 1.16 (r52326) for LiquidThreads - * - * @param $oldid mixed integer Revision ID or null - * @param $user User The relevant user - * @return ParserOutput or false if the given revision ID is not found - */ - public function getParserOutput( $oldid = null, User $user = null ) { - //XXX: bypasses mParserOptions and thus setParserOptions() - - if ( $user === null ) { - $parserOptions = $this->getParserOptions(); - } else { - $parserOptions = $this->mPage->makeParserOptions( $user ); - } - - return $this->mPage->getParserOutput( $parserOptions, $oldid ); - } - - /** - * Override the ParserOptions used to render the primary article wikitext. - * - * @param ParserOptions $options - * @throws MWException if the parser options where already initialized. - */ - public function setParserOptions( ParserOptions $options ) { - if ( $this->mParserOptions ) { - throw new MWException( "can't change parser options after they have already been set" ); - } - - // clone, so if $options is modified later, it doesn't confuse the parser cache. - $this->mParserOptions = clone $options; - } - - /** - * Get parser options suitable for rendering the primary article wikitext - * @return ParserOptions - */ - public function getParserOptions() { - if ( !$this->mParserOptions ) { - $this->mParserOptions = $this->mPage->makeParserOptions( $this->getContext() ); - } - // Clone to allow modifications of the return value without affecting cache - return clone $this->mParserOptions; - } - - /** - * Sets the context this Article is executed in - * - * @param $context IContextSource - * @since 1.18 - */ - public function setContext( $context ) { - $this->mContext = $context; - } - - /** - * Gets the context this Article is executed in - * - * @return IContextSource - * @since 1.18 - */ - public function getContext() { - if ( $this->mContext instanceof IContextSource ) { - return $this->mContext; - } else { - wfDebug( __METHOD__ . " called and \$mContext is null. Return RequestContext::getMain(); for sanity\n" ); - return RequestContext::getMain(); - } - } - - /** - * Info about this page - * @deprecated since 1.19 - */ - public function info() { - wfDeprecated( __METHOD__, '1.19' ); - Action::factory( 'info', $this )->show(); - } - - /** - * Mark this particular edit/page as patrolled - * @deprecated since 1.18 - */ - public function markpatrolled() { - wfDeprecated( __METHOD__, '1.18' ); - Action::factory( 'markpatrolled', $this )->show(); - } - - /** - * Handle action=purge - * @deprecated since 1.19 - * @return Action|bool|null false if the action is disabled, null if it is not recognised - */ - public function purge() { - return Action::factory( 'purge', $this )->show(); - } - - /** - * Handle action=revert - * @deprecated since 1.19 - */ - public function revert() { - wfDeprecated( __METHOD__, '1.19' ); - Action::factory( 'revert', $this )->show(); - } - - /** - * Handle action=rollback - * @deprecated since 1.19 - */ - public function rollback() { - wfDeprecated( __METHOD__, '1.19' ); - Action::factory( 'rollback', $this )->show(); - } - - /** - * User-interface handler for the "watch" action. - * Requires Request to pass a token as of 1.18. - * @deprecated since 1.18 - */ - public function watch() { - wfDeprecated( __METHOD__, '1.18' ); - Action::factory( 'watch', $this )->show(); - } - - /** - * Add this page to the current user's watchlist - * - * This is safe to be called multiple times - * - * @return bool true on successful watch operation - * @deprecated since 1.18 - */ - public function doWatch() { - wfDeprecated( __METHOD__, '1.18' ); - return WatchAction::doWatch( $this->getTitle(), $this->getContext()->getUser() ); - } - - /** - * User interface handler for the "unwatch" action. - * Requires Request to pass a token as of 1.18. - * @deprecated since 1.18 - */ - public function unwatch() { - wfDeprecated( __METHOD__, '1.18' ); - Action::factory( 'unwatch', $this )->show(); - } - - /** - * Stop watching a page - * @return bool true on successful unwatch - * @deprecated since 1.18 - */ - public function doUnwatch() { - wfDeprecated( __METHOD__, '1.18' ); - return WatchAction::doUnwatch( $this->getTitle(), $this->getContext()->getUser() ); - } - - /** - * Output a redirect back to the article. - * This is typically used after an edit. - * - * @deprecated in 1.18; call OutputPage::redirect() directly - * @param $noRedir Boolean: add redirect=no - * @param string $sectionAnchor section to redirect to, including "#" - * @param string $extraQuery extra query params - */ - public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) { - wfDeprecated( __METHOD__, '1.18' ); - if ( $noRedir ) { - $query = 'redirect=no'; - if ( $extraQuery ) { - $query .= "&$extraQuery"; - } - } else { - $query = $extraQuery; - } - - $this->getContext()->getOutput()->redirect( $this->getTitle()->getFullURL( $query ) . $sectionAnchor ); - } - - /** - * Use PHP's magic __get handler to handle accessing of - * raw WikiPage fields for backwards compatibility. - * - * @param string $fname Field name - */ - public function __get( $fname ) { - if ( property_exists( $this->mPage, $fname ) ) { - #wfWarn( "Access to raw $fname field " . __CLASS__ ); - return $this->mPage->$fname; - } - trigger_error( 'Inaccessible property via __get(): ' . $fname, E_USER_NOTICE ); - } - - /** - * Use PHP's magic __set handler to handle setting of - * raw WikiPage fields for backwards compatibility. - * - * @param string $fname Field name - * @param $fvalue mixed New value - */ - public function __set( $fname, $fvalue ) { - if ( property_exists( $this->mPage, $fname ) ) { - #wfWarn( "Access to raw $fname field of " . __CLASS__ ); - $this->mPage->$fname = $fvalue; - // Note: extensions may want to toss on new fields - } elseif ( !in_array( $fname, array( 'mContext', 'mPage' ) ) ) { - $this->mPage->$fname = $fvalue; - } else { - trigger_error( 'Inaccessible property via __set(): ' . $fname, E_USER_NOTICE ); - } - } - - /** - * Use PHP's magic __call handler to transform instance calls to - * WikiPage functions for backwards compatibility. - * - * @param string $fname Name of called method - * @param array $args Arguments to the method - * @return mixed - */ - public function __call( $fname, $args ) { - if ( is_callable( array( $this->mPage, $fname ) ) ) { - #wfWarn( "Call to " . __CLASS__ . "::$fname; please use WikiPage instead" ); - return call_user_func_array( array( $this->mPage, $fname ), $args ); - } - trigger_error( 'Inaccessible function via __call(): ' . $fname, E_USER_ERROR ); - } - - // ****** B/C functions to work-around PHP silliness with __call and references ****** // - - /** - * @param $limit array - * @param $expiry array - * @param $cascade bool - * @param $reason string - * @param $user User - * @return Status - */ - public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) { - return $this->mPage->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user ); - } - - /** - * @param $limit array - * @param $reason string - * @param $cascade int - * @param $expiry array - * @return bool - */ - public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) { - return $this->mPage->doUpdateRestrictions( - $limit, - $expiry, - $cascade, - $reason, - $this->getContext()->getUser() - ); - } - - /** - * @param $reason string - * @param $suppress bool - * @param $id int - * @param $commit bool - * @param $error string - * @return bool - */ - public function doDeleteArticle( $reason, $suppress = false, $id = 0, $commit = true, &$error = '' ) { - return $this->mPage->doDeleteArticle( $reason, $suppress, $id, $commit, $error ); - } - - /** - * @param $fromP - * @param $summary - * @param $token - * @param $bot - * @param $resultDetails - * @param $user User - * @return array - */ - public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails, User $user = null ) { - $user = is_null( $user ) ? $this->getContext()->getUser() : $user; - return $this->mPage->doRollback( $fromP, $summary, $token, $bot, $resultDetails, $user ); - } - - /** - * @param $fromP - * @param $summary - * @param $bot - * @param $resultDetails - * @param $guser User - * @return array - */ - public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser = null ) { - $guser = is_null( $guser ) ? $this->getContext()->getUser() : $guser; - return $this->mPage->commitRollback( $fromP, $summary, $bot, $resultDetails, $guser ); - } - - /** - * @param $hasHistory bool - * @return mixed - */ - public function generateReason( &$hasHistory ) { - $title = $this->mPage->getTitle(); - $handler = ContentHandler::getForTitle( $title ); - return $handler->getAutoDeleteReason( $title, $hasHistory ); - } - - // ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** // - - /** - * @return array - */ - public static function selectFields() { - return WikiPage::selectFields(); - } - - /** - * @param $title Title - */ - public static function onArticleCreate( $title ) { - WikiPage::onArticleCreate( $title ); - } - - /** - * @param $title Title - */ - public static function onArticleDelete( $title ) { - WikiPage::onArticleDelete( $title ); - } - - /** - * @param $title Title - */ - public static function onArticleEdit( $title ) { - WikiPage::onArticleEdit( $title ); - } - - /** - * @param $oldtext - * @param $newtext - * @param $flags - * @return string - * @deprecated since 1.21, use ContentHandler::getAutosummary() instead - */ - public static function getAutosummary( $oldtext, $newtext, $flags ) { - return WikiPage::getAutosummary( $oldtext, $newtext, $flags ); - } - // ****** -} diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php index 84cf3d5e..45ad4d1b 100644 --- a/includes/AuthPlugin.php +++ b/includes/AuthPlugin.php @@ -3,7 +3,7 @@ * Authentication plugin interface * * Copyright © 2004 Brion Vibber <brion@pobox.com> - * http://www.mediawiki.org/ + * https://www.mediawiki.org/ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,7 +34,6 @@ * someone logs in who can be authenticated externally. */ class AuthPlugin { - /** * @var string */ @@ -46,7 +45,7 @@ class AuthPlugin { * you might need to munge it (for instance, for lowercase initial * letters). * - * @param string $username username. + * @param string $username Username. * @return bool */ public function userExists( $username ) { @@ -60,8 +59,8 @@ class AuthPlugin { * you might need to munge it (for instance, for lowercase initial * letters). * - * @param string $username username. - * @param string $password user password. + * @param string $username Username. + * @param string $password User password. * @return bool */ public function authenticate( $username, $password ) { @@ -72,7 +71,7 @@ class AuthPlugin { /** * Modify options in the login template. * - * @param $template UserLoginTemplate object. + * @param UserLoginTemplate $template * @param string $type 'signup' or 'login'. Added in 1.16. */ public function modifyUITemplate( &$template, &$type ) { @@ -83,7 +82,7 @@ class AuthPlugin { /** * Set the domain this plugin is supposed to use when authenticating. * - * @param string $domain authentication domain. + * @param string $domain Authentication domain. */ public function setDomain( $domain ) { $this->domain = $domain; @@ -105,7 +104,7 @@ class AuthPlugin { /** * Check to see if the specific domain is a valid domain. * - * @param string $domain authentication domain. + * @param string $domain Authentication domain. * @return bool */ public function validDomain( $domain ) { @@ -121,7 +120,7 @@ class AuthPlugin { * The User object is passed by reference so it can be modified; don't * forget the & on your function declaration. * - * @param $user User object + * @param User $user * @return bool */ public function updateUser( &$user ) { @@ -140,7 +139,7 @@ class AuthPlugin { * * This is just a question, and shouldn't perform any actions. * - * @return Boolean + * @return bool */ public function autoCreate() { return false; @@ -151,9 +150,9 @@ class AuthPlugin { * and use the same keys. 'Realname' 'Emailaddress' and 'Nickname' * all reference this. * - * @param $prop string + * @param string $prop * - * @return Boolean + * @return bool */ public function allowPropChange( $prop = '' ) { if ( $prop == 'realname' && is_callable( array( $this, 'allowRealNameChange' ) ) ) { @@ -193,8 +192,8 @@ class AuthPlugin { * * Return true if successful. * - * @param $user User object. - * @param string $password password. + * @param User $user + * @param string $password Password. * @return bool */ public function setPassword( $user, $password ) { @@ -205,8 +204,8 @@ class AuthPlugin { * Update user information in the external authentication database. * Return true if successful. * - * @param $user User object. - * @return Boolean + * @param User $user + * @return bool */ public function updateExternalDB( $user ) { return true; @@ -216,10 +215,10 @@ class AuthPlugin { * Update user groups in the external authentication database. * Return true if successful. * - * @param $user User object. - * @param $addgroups Groups to add. - * @param $delgroups Groups to remove. - * @return Boolean + * @param User $user + * @param array $addgroups Groups to add. + * @param array $delgroups Groups to remove. + * @return bool */ public function updateExternalDBGroups( $user, $addgroups, $delgroups = array() ) { return true; @@ -228,7 +227,7 @@ class AuthPlugin { /** * Check to see if external accounts can be created. * Return true if external accounts can be created. - * @return Boolean + * @return bool */ public function canCreateAccounts() { return false; @@ -238,11 +237,11 @@ class AuthPlugin { * Add a user to the external authentication database. * Return true if successful. * - * @param $user User: only the name should be assumed valid at this point - * @param $password String - * @param $email String - * @param $realname String - * @return Boolean + * @param User $user Only the name should be assumed valid at this point + * @param string $password + * @param string $email + * @param string $realname + * @return bool */ public function addUser( $user, $password, $email = '', $realname = '' ) { return true; @@ -254,7 +253,7 @@ class AuthPlugin { * * This is just a question, and shouldn't perform any actions. * - * @return Boolean + * @return bool */ public function strict() { return false; @@ -264,8 +263,8 @@ class AuthPlugin { * Check if a user should authenticate locally if the global authentication fails. * If either this or strict() returns true, local authentication is not used. * - * @param string $username username. - * @return Boolean + * @param string $username Username. + * @return bool */ public function strictUserAuth( $username ) { return false; @@ -279,8 +278,8 @@ class AuthPlugin { * The User object is passed by reference so it can be modified; don't * forget the & on your function declaration. * - * @param $user User object. - * @param $autocreate Boolean: True if user is being autocreated on login + * @param User $user + * @param bool $autocreate True if user is being autocreated on login */ public function initUser( &$user, $autocreate = false ) { # Override this to do something. @@ -289,7 +288,7 @@ class AuthPlugin { /** * If you want to munge the case of an account name before the final * check, now is your chance. - * @param $username string + * @param string $username * @return string */ public function getCanonicalName( $username ) { @@ -299,7 +298,7 @@ class AuthPlugin { /** * Get an instance of a User object * - * @param $user User + * @param User $user * * @return AuthPluginUser */ diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 0706fe3f..6b0daa14 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -29,49 +29,32 @@ global $wgAutoloadLocalClasses; $wgAutoloadLocalClasses = array( # Includes - 'Action' => 'includes/Action.php', 'AjaxDispatcher' => 'includes/AjaxDispatcher.php', 'AjaxResponse' => 'includes/AjaxResponse.php', - 'AlphabeticPager' => 'includes/Pager.php', - 'ArrayUtils' => 'includes/ArrayUtils.php', - 'Article' => 'includes/Article.php', 'AtomFeed' => 'includes/Feed.php', 'AuthPlugin' => 'includes/AuthPlugin.php', 'AuthPluginUser' => 'includes/AuthPlugin.php', 'Autopromote' => 'includes/Autopromote.php', - 'BadTitleError' => 'includes/Exception.php', - 'BaseTemplate' => 'includes/SkinTemplate.php', 'Block' => 'includes/Block.php', + 'BloomCache' => 'includes/cache/bloom/BloomCache.php', + 'BloomCacheRedis' => 'includes/cache/bloom/BloomCacheRedis.php', + 'BloomFilterTitleHasLogs' => 'includes/cache/bloom/BloomFilters.php', 'CacheHelper' => 'includes/CacheHelper.php', 'Category' => 'includes/Category.php', - 'Categoryfinder' => 'includes/Categoryfinder.php', - 'CategoryPage' => 'includes/CategoryPage.php', + 'CategoryFinder' => 'includes/CategoryFinder.php', 'CategoryViewer' => 'includes/CategoryViewer.php', - 'CdbFunctions' => 'includes/Cdb_PHP.php', - 'CdbReader' => 'includes/Cdb.php', - 'CdbReader_DBA' => 'includes/Cdb.php', - 'CdbReader_PHP' => 'includes/Cdb_PHP.php', - 'CdbWriter' => 'includes/Cdb.php', - 'CdbWriter_DBA' => 'includes/Cdb.php', - 'CdbWriter_PHP' => 'includes/Cdb_PHP.php', - 'ChangesFeed' => 'includes/ChangesFeed.php', 'ChangeTags' => 'includes/ChangeTags.php', 'ChannelFeed' => 'includes/Feed.php', 'Collation' => 'includes/Collation.php', + 'CollationCkb' => 'includes/Collation.php', + 'CollationEt' => 'includes/Collation.php', 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php', - 'ConfEditor' => 'includes/ConfEditor.php', - 'ConfEditorParseError' => 'includes/ConfEditor.php', - 'ConfEditorToken' => 'includes/ConfEditor.php', 'Cookie' => 'includes/Cookie.php', 'CookieJar' => 'includes/Cookie.php', 'CurlHttpRequest' => 'includes/HttpFunctions.php', - 'DeferrableUpdate' => 'includes/DeferredUpdates.php', - 'DeferredUpdates' => 'includes/DeferredUpdates.php', - 'MWCallableUpdate' => 'includes/CallableUpdate.php', 'DeprecatedGlobal' => 'includes/DeprecatedGlobal.php', 'DerivativeRequest' => 'includes/WebRequest.php', 'DiffHistoryBlob' => 'includes/HistoryBlob.php', - 'DoubleReplacer' => 'includes/StringUtils.php', 'DummyLinker' => 'includes/Linker.php', 'Dump7ZipOutput' => 'includes/Export.php', 'DumpBZip2Output' => 'includes/Export.php', @@ -85,126 +68,83 @@ $wgAutoloadLocalClasses = array( 'DumpOutput' => 'includes/Export.php', 'DumpPipeOutput' => 'includes/Export.php', 'EditPage' => 'includes/EditPage.php', - 'EmailNotification' => 'includes/UserMailer.php', - 'ErrorPageError' => 'includes/Exception.php', - 'ExplodeIterator' => 'includes/StringUtils.php', - 'FakeTitle' => 'includes/FakeTitle.php', + 'EmptyBloomCache' => 'includes/cache/bloom/BloomCache.php', 'Fallback' => 'includes/Fallback.php', - 'FatalError' => 'includes/Exception.php', 'FauxRequest' => 'includes/WebRequest.php', 'FauxResponse' => 'includes/WebResponse.php', 'FeedItem' => 'includes/Feed.php', 'FeedUtils' => 'includes/FeedUtils.php', 'FileDeleteForm' => 'includes/FileDeleteForm.php', 'ForkController' => 'includes/ForkController.php', - 'FormlessAction' => 'includes/Action.php', - 'FormAction' => 'includes/Action.php', 'FormOptions' => 'includes/FormOptions.php', - 'FormSpecialPage' => 'includes/SpecialPage.php', 'GitInfo' => 'includes/GitInfo.php', - 'HashRing' => 'includes/HashRing.php', - 'HashtableReplacer' => 'includes/StringUtils.php', 'HistoryBlob' => 'includes/HistoryBlob.php', 'HistoryBlobCurStub' => 'includes/HistoryBlob.php', 'HistoryBlobStub' => 'includes/HistoryBlob.php', 'Hooks' => 'includes/Hooks.php', 'Html' => 'includes/Html.php', 'HtmlFormatter' => 'includes/HtmlFormatter.php', - 'HTMLApiField' => 'includes/HTMLForm.php', - 'HTMLButtonField' => 'includes/HTMLForm.php', - 'HTMLCheckField' => 'includes/HTMLForm.php', - 'HTMLCheckMatrix' => 'includes/HTMLForm.php', - 'HTMLEditTools' => 'includes/HTMLForm.php', - 'HTMLFloatField' => 'includes/HTMLForm.php', - 'HTMLForm' => 'includes/HTMLForm.php', - 'HTMLFormField' => 'includes/HTMLForm.php', - 'HTMLFormFieldRequiredOptionsException' => 'includes/HTMLForm.php', - 'HTMLHiddenField' => 'includes/HTMLForm.php', - 'HTMLInfoField' => 'includes/HTMLForm.php', - 'HTMLIntField' => 'includes/HTMLForm.php', - 'HTMLNestedFilterable' => 'includes/HTMLForm.php', - 'HTMLMultiSelectField' => 'includes/HTMLForm.php', - 'HTMLRadioField' => 'includes/HTMLForm.php', - 'HTMLSelectAndOtherField' => 'includes/HTMLForm.php', - 'HTMLSelectField' => 'includes/HTMLForm.php', - 'HTMLSelectOrOtherField' => 'includes/HTMLForm.php', - 'HTMLSubmitField' => 'includes/HTMLForm.php', - 'HTMLTextAreaField' => 'includes/HTMLForm.php', - 'HTMLTextField' => 'includes/HTMLForm.php', + 'HTMLApiField' => 'includes/htmlform/HTMLApiField.php', + 'HTMLAutoCompleteSelectField' => 'includes/htmlform/HTMLAutoCompleteSelectField.php', + 'HTMLButtonField' => 'includes/htmlform/HTMLButtonField.php', + 'HTMLCheckField' => 'includes/htmlform/HTMLCheckField.php', + 'HTMLCheckMatrix' => 'includes/htmlform/HTMLCheckMatrix.php', + 'HTMLFormFieldCloner' => 'includes/htmlform/HTMLFormFieldCloner.php', + 'HTMLEditTools' => 'includes/htmlform/HTMLEditTools.php', + 'HTMLFloatField' => 'includes/htmlform/HTMLFloatField.php', + 'HTMLForm' => 'includes/htmlform/HTMLForm.php', + 'HTMLFormField' => 'includes/htmlform/HTMLFormField.php', + 'HTMLFormFieldRequiredOptionsException' => + 'includes/htmlform/HTMLFormFieldRequiredOptionsException.php', + 'HTMLHiddenField' => 'includes/htmlform/HTMLHiddenField.php', + 'HTMLInfoField' => 'includes/htmlform/HTMLInfoField.php', + 'HTMLIntField' => 'includes/htmlform/HTMLIntField.php', + 'HTMLNestedFilterable' => 'includes/htmlform/HTMLNestedFilterable.php', + 'HTMLMultiSelectField' => 'includes/htmlform/HTMLMultiSelectField.php', + 'HTMLRadioField' => 'includes/htmlform/HTMLRadioField.php', + 'HTMLSelectAndOtherField' => 'includes/htmlform/HTMLSelectAndOtherField.php', + 'HTMLSelectField' => 'includes/htmlform/HTMLSelectField.php', + 'HTMLSelectLimitField' => 'includes/htmlform/HTMLSelectLimitField.php', + 'HTMLSelectOrOtherField' => 'includes/htmlform/HTMLSelectOrOtherField.php', + 'HTMLSubmitField' => 'includes/htmlform/HTMLSubmitField.php', + 'HTMLTextAreaField' => 'includes/htmlform/HTMLTextAreaField.php', + 'HTMLTextField' => 'includes/htmlform/HTMLTextField.php', 'Http' => 'includes/HttpFunctions.php', - 'HttpError' => 'includes/Exception.php', - 'ICacheHelper' => 'includes/CacheHelper.php', 'IcuCollation' => 'includes/Collation.php', 'IdentityCollation' => 'includes/Collation.php', - 'ImageHistoryList' => 'includes/ImagePage.php', - 'ImageHistoryPseudoPager' => 'includes/ImagePage.php', - 'ImagePage' => 'includes/ImagePage.php', - 'ImageQueryPage' => 'includes/ImageQueryPage.php', 'ImportStreamSource' => 'includes/Import.php', 'ImportStringSource' => 'includes/Import.php', - 'IncludableSpecialPage' => 'includes/SpecialPage.php', - 'IndexPager' => 'includes/Pager.php', 'Interwiki' => 'includes/interwiki/Interwiki.php', - 'IP' => 'includes/IP.php', - 'LCStore' => 'includes/cache/LocalisationCache.php', - 'LCStore_Accel' => 'includes/cache/LocalisationCache.php', - 'LCStore_CDB' => 'includes/cache/LocalisationCache.php', - 'LCStore_DB' => 'includes/cache/LocalisationCache.php', - 'LCStore_Null' => 'includes/cache/LocalisationCache.php', 'License' => 'includes/Licenses.php', 'Licenses' => 'includes/Licenses.php', 'Linker' => 'includes/Linker.php', 'LinkFilter' => 'includes/LinkFilter.php', - 'LinksUpdate' => 'includes/LinksUpdate.php', - 'LinksDeletionUpdate' => 'includes/LinksUpdate.php', - 'LocalisationCache' => 'includes/cache/LocalisationCache.php', - 'LocalisationCache_BulkLoad' => 'includes/cache/LocalisationCache.php', 'MagicWord' => 'includes/MagicWord.php', 'MagicWordArray' => 'includes/MagicWord.php', - 'MailAddress' => 'includes/UserMailer.php', - 'MappedIterator' => 'includes/MappedIterator.php', - 'MediaWiki' => 'includes/Wiki.php', - 'MediaWiki_I18N' => 'includes/SkinTemplate.php', + 'MediaWiki' => 'includes/MediaWiki.php', + 'MediaWikiVersionFetcher' => 'includes/MediaWikiVersionFetcher.php', 'Message' => 'includes/Message.php', 'MessageBlobStore' => 'includes/MessageBlobStore.php', 'MimeMagic' => 'includes/MimeMagic.php', - 'MWCryptRand' => 'includes/MWCryptRand.php', - 'MWException' => 'includes/Exception.php', - 'MWExceptionHandler' => 'includes/Exception.php', - 'MWFunction' => 'includes/MWFunction.php', + 'MovePage' => 'includes/MovePage.php', 'MWHookException' => 'includes/Hooks.php', 'MWHttpRequest' => 'includes/HttpFunctions.php', - 'MWInit' => 'includes/Init.php', - 'MWNamespace' => 'includes/Namespace.php', + 'MWNamespace' => 'includes/MWNamespace.php', 'OutputPage' => 'includes/OutputPage.php', - 'Page' => 'includes/WikiPage.php', - 'PageQueryPage' => 'includes/PageQueryPage.php', - 'Pager' => 'includes/Pager.php', - 'PasswordError' => 'includes/User.php', 'PathRouter' => 'includes/PathRouter.php', 'PathRouterPatternReplacer' => 'includes/PathRouter.php', - 'PermissionsError' => 'includes/Exception.php', 'PhpHttpRequest' => 'includes/HttpFunctions.php', - 'PoolCounter' => 'includes/PoolCounter.php', - 'PoolCounter_Stub' => 'includes/PoolCounter.php', - 'PoolCounterWork' => 'includes/PoolCounter.php', - 'PoolCounterWorkViaCallback' => 'includes/PoolCounter.php', - 'PoolWorkArticleView' => 'includes/WikiPage.php', + 'PoolCounter' => 'includes/poolcounter/PoolCounter.php', + 'PoolCounter_Stub' => 'includes/poolcounter/PoolCounter.php', + 'PoolCounterRedis' => 'includes/poolcounter/PoolCounterRedis.php', + 'PoolCounterWork' => 'includes/poolcounter/PoolCounterWork.php', + 'PoolCounterWorkViaCallback' => 'includes/poolcounter/PoolCounterWorkViaCallback.php', + 'PoolWorkArticleView' => 'includes/poolcounter/PoolWorkArticleView.php', 'Preferences' => 'includes/Preferences.php', 'PreferencesForm' => 'includes/Preferences.php', 'PrefixSearch' => 'includes/PrefixSearch.php', 'ProtectionForm' => 'includes/ProtectionForm.php', - 'QueryPage' => 'includes/QueryPage.php', - 'QuickTemplate' => 'includes/SkinTemplate.php', 'RawMessage' => 'includes/Message.php', - 'RdfMetaData' => 'includes/Metadata.php', - 'ReadOnlyError' => 'includes/Exception.php', - 'RedirectSpecialArticle' => 'includes/SpecialPage.php', - 'RedirectSpecialPage' => 'includes/SpecialPage.php', - 'RegexlikeReplacer' => 'includes/StringUtils.php', - 'ReplacementArray' => 'includes/StringUtils.php', - 'Replacer' => 'includes/StringUtils.php', - 'ReverseChronologicalPager' => 'includes/Pager.php', 'RevisionItem' => 'includes/RevisionList.php', 'RevisionItemBase' => 'includes/RevisionList.php', 'RevisionListBase' => 'includes/RevisionList.php', @@ -212,125 +152,72 @@ $wgAutoloadLocalClasses = array( 'RevisionList' => 'includes/RevisionList.php', 'RSSFeed' => 'includes/Feed.php', 'Sanitizer' => 'includes/Sanitizer.php', - 'DataUpdate' => 'includes/DataUpdate.php', - 'SqlDataUpdate' => 'includes/SqlDataUpdate.php', - 'ScopedCallback' => 'includes/ScopedCallback.php', - 'ScopedPHPTimeout' => 'includes/ScopedPHPTimeout.php', 'SiteConfiguration' => 'includes/SiteConfiguration.php', 'SiteStats' => 'includes/SiteStats.php', 'SiteStatsInit' => 'includes/SiteStats.php', - 'SiteStatsUpdate' => 'includes/SiteStats.php', - 'Skin' => 'includes/Skin.php', - 'SkinTemplate' => 'includes/SkinTemplate.php', - 'SpecialCreateAccount' => 'includes/SpecialPage.php', - 'SpecialListAdmins' => 'includes/SpecialPage.php', - 'SpecialListBots' => 'includes/SpecialPage.php', - 'SpecialMycontributions' => 'includes/SpecialPage.php', - 'SpecialMypage' => 'includes/SpecialPage.php', - 'SpecialMytalk' => 'includes/SpecialPage.php', - 'SpecialMyuploads' => 'includes/SpecialPage.php', - 'SpecialAllMyUploads' => 'includes/SpecialPage.php', - 'SpecialPage' => 'includes/SpecialPage.php', - 'SpecialPageFactory' => 'includes/SpecialPageFactory.php', - 'SpecialRedirectToSpecial' => 'includes/SpecialPage.php', 'SquidPurgeClient' => 'includes/SquidPurgeClient.php', 'SquidPurgeClientPool' => 'includes/SquidPurgeClient.php', 'StatCounter' => 'includes/StatCounter.php', 'Status' => 'includes/Status.php', 'StreamFile' => 'includes/StreamFile.php', - 'StringUtils' => 'includes/StringUtils.php', - 'StubContLang' => 'includes/StubObject.php', + 'StringPrefixSearch' => 'includes/PrefixSearch.php', 'StubObject' => 'includes/StubObject.php', 'StubUserLang' => 'includes/StubObject.php', - 'TablePager' => 'includes/Pager.php', - 'MWTimestamp' => 'includes/Timestamp.php', - 'TimestampException' => 'includes/Timestamp.php', + 'MWTimestamp' => 'includes/MWTimestamp.php', + 'TimestampException' => 'includes/TimestampException.php', 'Title' => 'includes/Title.php', 'TitleArray' => 'includes/TitleArray.php', - 'TitleArrayFromResult' => 'includes/TitleArray.php', - 'ThrottledError' => 'includes/Exception.php', - 'UIDGenerator' => 'includes/UIDGenerator.php', - 'UnlistedSpecialPage' => 'includes/SpecialPage.php', + 'TitleArrayFromResult' => 'includes/TitleArrayFromResult.php', + 'TitlePrefixSearch' => 'includes/PrefixSearch.php', 'UploadSourceAdapter' => 'includes/Import.php', 'UppercaseCollation' => 'includes/Collation.php', 'User' => 'includes/User.php', 'UserArray' => 'includes/UserArray.php', - 'UserArrayFromResult' => 'includes/UserArray.php', - 'UserBlockedError' => 'includes/Exception.php', - 'UserNotLoggedIn' => 'includes/Exception.php', - 'UserCache' => 'includes/cache/UserCache.php', - 'UserMailer' => 'includes/UserMailer.php', + 'UserArrayFromResult' => 'includes/UserArrayFromResult.php', 'UserRightsProxy' => 'includes/UserRightsProxy.php', - 'ViewCountUpdate' => 'includes/ViewCountUpdate.php', - 'WantedQueryPage' => 'includes/QueryPage.php', 'WatchedItem' => 'includes/WatchedItem.php', 'WebRequest' => 'includes/WebRequest.php', 'WebRequestUpload' => 'includes/WebRequest.php', 'WebResponse' => 'includes/WebResponse.php', - 'WikiCategoryPage' => 'includes/WikiCategoryPage.php', - 'WikiError' => 'includes/WikiError.php', - 'WikiErrorMsg' => 'includes/WikiError.php', 'WikiExporter' => 'includes/Export.php', - 'WikiFilePage' => 'includes/WikiFilePage.php', 'WikiImporter' => 'includes/Import.php', - 'WikiPage' => 'includes/WikiPage.php', 'WikiRevision' => 'includes/Import.php', 'WikiMap' => 'includes/WikiMap.php', 'WikiReference' => 'includes/WikiMap.php', - 'WikiXmlError' => 'includes/WikiError.php', 'Xml' => 'includes/Xml.php', 'XmlDumpWriter' => 'includes/Export.php', 'XmlJsCode' => 'includes/Xml.php', - 'XMLReader2' => 'includes/Import.php', 'XmlSelect' => 'includes/Xml.php', - 'XmlTypeCheck' => 'includes/XmlTypeCheck.php', - 'ZhClient' => 'includes/ZhClient.php', - 'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php', - 'ZipDirectoryReaderError' => 'includes/ZipDirectoryReader.php', - - # content handler - 'AbstractContent' => 'includes/content/AbstractContent.php', - 'ContentHandler' => 'includes/content/ContentHandler.php', - 'Content' => 'includes/content/Content.php', - 'CssContentHandler' => 'includes/content/CssContentHandler.php', - 'CssContent' => 'includes/content/CssContent.php', - 'JavaScriptContentHandler' => 'includes/content/JavaScriptContentHandler.php', - 'JavaScriptContent' => 'includes/content/JavaScriptContent.php', - 'MessageContent' => 'includes/content/MessageContent.php', - 'MWContentSerializationException' => 'includes/content/ContentHandler.php', - 'TextContentHandler' => 'includes/content/TextContentHandler.php', - 'TextContent' => 'includes/content/TextContent.php', - 'WikitextContentHandler' => 'includes/content/WikitextContentHandler.php', - 'WikitextContent' => 'includes/content/WikitextContent.php', # includes/actions + 'Action' => 'includes/actions/Action.php', 'CachedAction' => 'includes/actions/CachedAction.php', 'CreditsAction' => 'includes/actions/CreditsAction.php', 'DeleteAction' => 'includes/actions/DeleteAction.php', 'EditAction' => 'includes/actions/EditAction.php', + 'FormlessAction' => 'includes/actions/FormlessAction.php', + 'FormAction' => 'includes/actions/FormAction.php', 'HistoryAction' => 'includes/actions/HistoryAction.php', - 'HistoryPage' => 'includes/actions/HistoryAction.php', 'HistoryPager' => 'includes/actions/HistoryAction.php', 'InfoAction' => 'includes/actions/InfoAction.php', 'MarkpatrolledAction' => 'includes/actions/MarkpatrolledAction.php', 'ProtectAction' => 'includes/actions/ProtectAction.php', 'PurgeAction' => 'includes/actions/PurgeAction.php', 'RawAction' => 'includes/actions/RawAction.php', - 'RawPage' => 'includes/actions/RawAction.php', 'RenderAction' => 'includes/actions/RenderAction.php', 'RevertAction' => 'includes/actions/RevertAction.php', - 'RevertFileAction' => 'includes/actions/RevertAction.php', 'RevisiondeleteAction' => 'includes/actions/RevisiondeleteAction.php', 'RollbackAction' => 'includes/actions/RollbackAction.php', - 'SubmitAction' => 'includes/actions/EditAction.php', - 'UnprotectAction' => 'includes/actions/ProtectAction.php', - 'UnwatchAction' => 'includes/actions/WatchAction.php', + 'SubmitAction' => 'includes/actions/SubmitAction.php', + 'UnprotectAction' => 'includes/actions/UnprotectAction.php', + 'UnwatchAction' => 'includes/actions/UnwatchAction.php', 'ViewAction' => 'includes/actions/ViewAction.php', 'WatchAction' => 'includes/actions/WatchAction.php', # includes/api 'ApiBase' => 'includes/api/ApiBase.php', 'ApiBlock' => 'includes/api/ApiBlock.php', + 'ApiClearHasMsg' => 'includes/api/ApiClearHasMsg.php', 'ApiComparePages' => 'includes/api/ApiComparePages.php', 'ApiCreateAccount' => 'includes/api/ApiCreateAccount.php', 'ApiDelete' => 'includes/api/ApiDelete.php', @@ -339,12 +226,13 @@ $wgAutoloadLocalClasses = array( 'ApiEmailUser' => 'includes/api/ApiEmailUser.php', 'ApiExpandTemplates' => 'includes/api/ApiExpandTemplates.php', 'ApiFeedContributions' => 'includes/api/ApiFeedContributions.php', + 'ApiFeedRecentChanges' => 'includes/api/ApiFeedRecentChanges.php', 'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php', 'ApiFileRevert' => 'includes/api/ApiFileRevert.php', 'ApiFormatBase' => 'includes/api/ApiFormatBase.php', 'ApiFormatDbg' => 'includes/api/ApiFormatDbg.php', 'ApiFormatDump' => 'includes/api/ApiFormatDump.php', - 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php', + 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatFeedWrapper.php', 'ApiFormatJson' => 'includes/api/ApiFormatJson.php', 'ApiFormatNone' => 'includes/api/ApiFormatNone.php', 'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php', @@ -379,12 +267,14 @@ $wgAutoloadLocalClasses = array( 'ApiQueryAllPages' => 'includes/api/ApiQueryAllPages.php', 'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php', 'ApiQueryBacklinks' => 'includes/api/ApiQueryBacklinks.php', + 'ApiQueryBacklinksprop' => 'includes/api/ApiQueryBacklinksprop.php', 'ApiQueryBase' => 'includes/api/ApiQueryBase.php', 'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php', 'ApiQueryCategories' => 'includes/api/ApiQueryCategories.php', 'ApiQueryCategoryInfo' => 'includes/api/ApiQueryCategoryInfo.php', 'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php', 'ApiQueryContributions' => 'includes/api/ApiQueryUserContributions.php', + 'ApiQueryContributors' => 'includes/api/ApiQueryContributors.php', 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php', 'ApiQueryDisabled' => 'includes/api/ApiQueryDisabled.php', 'ApiQueryDuplicateFiles' => 'includes/api/ApiQueryDuplicateFiles.php', @@ -405,6 +295,7 @@ $wgAutoloadLocalClasses = array( 'ApiQueryPageProps' => 'includes/api/ApiQueryPageProps.php', 'ApiQueryPagesWithProp' => 'includes/api/ApiQueryPagesWithProp.php', 'ApiQueryPagePropNames' => 'includes/api/ApiQueryPagePropNames.php', + 'ApiQueryPrefixSearch' => 'includes/api/ApiQueryPrefixSearch.php', 'ApiQueryProtectedTitles' => 'includes/api/ApiQueryProtectedTitles.php', 'ApiQueryQueryPage' => 'includes/api/ApiQueryQueryPage.php', 'ApiQueryRandom' => 'includes/api/ApiQueryRandom.php', @@ -415,11 +306,13 @@ $wgAutoloadLocalClasses = array( 'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php', 'ApiQueryStashImageInfo' => 'includes/api/ApiQueryStashImageInfo.php', 'ApiQueryTags' => 'includes/api/ApiQueryTags.php', + 'ApiQueryTokens' => 'includes/api/ApiQueryTokens.php', 'ApiQueryUserInfo' => 'includes/api/ApiQueryUserInfo.php', 'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php', 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php', 'ApiQueryWatchlistRaw' => 'includes/api/ApiQueryWatchlistRaw.php', 'ApiResult' => 'includes/api/ApiResult.php', + 'ApiRevisionDelete' => 'includes/api/ApiRevisionDelete.php', 'ApiRollback' => 'includes/api/ApiRollback.php', 'ApiRsd' => 'includes/api/ApiRsd.php', 'ApiSetNotificationTimestamp' => 'includes/api/ApiSetNotificationTimestamp.php', @@ -434,35 +327,73 @@ $wgAutoloadLocalClasses = array( # includes/cache 'BacklinkCache' => 'includes/cache/BacklinkCache.php', 'CacheDependency' => 'includes/cache/CacheDependency.php', + 'CacheHelper' => 'includes/cache/CacheHelper.php', 'ConstantDependency' => 'includes/cache/CacheDependency.php', 'DependencyWrapper' => 'includes/cache/CacheDependency.php', 'FileCacheBase' => 'includes/cache/FileCacheBase.php', 'FileDependency' => 'includes/cache/CacheDependency.php', 'GenderCache' => 'includes/cache/GenderCache.php', 'GlobalDependency' => 'includes/cache/CacheDependency.php', - 'HTMLCacheUpdate' => 'includes/cache/HTMLCacheUpdate.php', 'HTMLFileCache' => 'includes/cache/HTMLFileCache.php', + 'ICacheHelper' => 'includes/cache/CacheHelper.php', + 'LCStore' => 'includes/cache/LocalisationCache.php', + 'LCStoreCDB' => 'includes/cache/LocalisationCache.php', + 'LCStoreDB' => 'includes/cache/LocalisationCache.php', + 'LCStoreNull' => 'includes/cache/LocalisationCache.php', 'LinkBatch' => 'includes/cache/LinkBatch.php', 'LinkCache' => 'includes/cache/LinkCache.php', + 'LocalisationCache' => 'includes/cache/LocalisationCache.php', + 'LocalisationCacheBulkLoad' => 'includes/cache/LocalisationCache.php', + 'MapCacheLRU' => 'includes/cache/MapCacheLRU.php', 'MessageCache' => 'includes/cache/MessageCache.php', 'ObjectFileCache' => 'includes/cache/ObjectFileCache.php', - 'ProcessCacheLRU' => 'includes/cache/ProcessCacheLRU.php', 'ResourceFileCache' => 'includes/cache/ResourceFileCache.php', - 'SquidUpdate' => 'includes/cache/SquidUpdate.php', - 'TitleDependency' => 'includes/cache/CacheDependency.php', - 'TitleListDependency' => 'includes/cache/CacheDependency.php', + 'UserCache' => 'includes/cache/UserCache.php', # includes/changes + 'ChangesFeed' => 'includes/changes/ChangesFeed.php', 'ChangesList' => 'includes/changes/ChangesList.php', 'EnhancedChangesList' => 'includes/changes/EnhancedChangesList.php', 'OldChangesList' => 'includes/changes/OldChangesList.php', 'RCCacheEntry' => 'includes/changes/RCCacheEntry.php', + 'RCCacheEntryFactory' => 'includes/changes/RCCacheEntryFactory.php', 'RecentChange' => 'includes/changes/RecentChange.php', # includes/clientpool 'RedisConnectionPool' => 'includes/clientpool/RedisConnectionPool.php', 'RedisConnRef' => 'includes/clientpool/RedisConnectionPool.php', + # includes/composer + 'ComposerPackageModifier' => 'includes/composer/ComposerPackageModifier.php', + 'ComposerVersionNormalizer' => 'includes/composer/ComposerVersionNormalizer.php', + + # includes/config + 'Config' => 'includes/config/Config.php', + 'ConfigException' => 'includes/config/ConfigException.php', + 'ConfigFactory' => 'includes/config/ConfigFactory.php', + 'GlobalVarConfig' => 'includes/config/GlobalVarConfig.php', + 'HashConfig' => 'includes/config/HashConfig.php', + 'MultiConfig' => 'includes/config/MultiConfig.php', + 'MutableConfig' => 'includes/config/MutableConfig.php', + + # includes/content + 'AbstractContent' => 'includes/content/AbstractContent.php', + 'CodeContentHandler' => 'includes/content/CodeContentHandler.php', + 'Content' => 'includes/content/Content.php', + 'ContentHandler' => 'includes/content/ContentHandler.php', + 'CssContent' => 'includes/content/CssContent.php', + 'CssContentHandler' => 'includes/content/CssContentHandler.php', + 'JavaScriptContent' => 'includes/content/JavaScriptContent.php', + 'JavaScriptContentHandler' => 'includes/content/JavaScriptContentHandler.php', + 'JsonContent' => 'includes/content/JsonContent.php', + 'JsonContentHandler' => 'includes/content/JsonContentHandler.php', + 'MessageContent' => 'includes/content/MessageContent.php', + 'MWContentSerializationException' => 'includes/content/ContentHandler.php', + 'TextContent' => 'includes/content/TextContent.php', + 'TextContentHandler' => 'includes/content/TextContentHandler.php', + 'WikitextContent' => 'includes/content/WikitextContent.php', + 'WikitextContentHandler' => 'includes/content/WikitextContentHandler.php', + # includes/context 'ContextSource' => 'includes/context/ContextSource.php', 'DerivativeContext' => 'includes/context/DerivativeContext.php', @@ -491,6 +422,7 @@ $wgAutoloadLocalClasses = array( 'DBConnectionError' => 'includes/db/DatabaseError.php', 'DBConnRef' => 'includes/db/LoadBalancer.php', 'DBError' => 'includes/db/DatabaseError.php', + 'DBExpectedError' => 'includes/db/DatabaseError.php', 'DBObject' => 'includes/db/DatabaseUtility.php', 'IDatabase' => 'includes/db/Database.php', 'IORMRow' => 'includes/db/IORMRow.php', @@ -501,18 +433,19 @@ $wgAutoloadLocalClasses = array( 'FakeResultWrapper' => 'includes/db/DatabaseUtility.php', 'Field' => 'includes/db/DatabaseUtility.php', 'LBFactory' => 'includes/db/LBFactory.php', - 'LBFactory_Fake' => 'includes/db/LBFactory.php', - 'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php', - 'LBFactory_Simple' => 'includes/db/LBFactory.php', - 'LBFactory_Single' => 'includes/db/LBFactory_Single.php', + 'LBFactoryFake' => 'includes/db/LBFactory.php', + 'LBFactoryMulti' => 'includes/db/LBFactoryMulti.php', + 'LBFactorySimple' => 'includes/db/LBFactory.php', + 'LBFactorySingle' => 'includes/db/LBFactorySingle.php', 'LikeMatch' => 'includes/db/DatabaseUtility.php', 'LoadBalancer' => 'includes/db/LoadBalancer.php', - 'LoadBalancer_Single' => 'includes/db/LBFactory_Single.php', + 'LoadBalancerSingle' => 'includes/db/LBFactorySingle.php', 'LoadMonitor' => 'includes/db/LoadMonitor.php', - 'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php', - 'LoadMonitor_Null' => 'includes/db/LoadMonitor.php', + 'LoadMonitorMySQL' => 'includes/db/LoadMonitor.php', + 'LoadMonitorNull' => 'includes/db/LoadMonitor.php', 'MssqlField' => 'includes/db/DatabaseMssql.php', - 'MssqlResult' => 'includes/db/DatabaseMssql.php', + 'MssqlBlob' => 'includes/db/DatabaseMssql.php', + 'MssqlResultWrapper' => 'includes/db/DatabaseMssql.php', 'MySQLField' => 'includes/db/DatabaseMysqlBase.php', 'MySQLMasterPos' => 'includes/db/DatabaseMysqlBase.php', 'ORAField' => 'includes/db/DatabaseOracle.php', @@ -528,27 +461,54 @@ $wgAutoloadLocalClasses = array( 'SQLiteField' => 'includes/db/DatabaseSqlite.php', # includes/debug - 'MWDebug' => 'includes/debug/Debug.php', + 'MWDebug' => 'includes/debug/MWDebug.php', + + # includes/deferred + 'DataUpdate' => 'includes/deferred/DataUpdate.php', + 'DeferrableUpdate' => 'includes/deferred/DeferredUpdates.php', + 'DeferredUpdates' => 'includes/deferred/DeferredUpdates.php', + 'HTMLCacheUpdate' => 'includes/deferred/HTMLCacheUpdate.php', + 'LinksDeletionUpdate' => 'includes/deferred/LinksUpdate.php', + 'LinksUpdate' => 'includes/deferred/LinksUpdate.php', + 'MWCallableUpdate' => 'includes/deferred/CallableUpdate.php', + 'SearchUpdate' => 'includes/deferred/SearchUpdate.php', + 'SiteStatsUpdate' => 'includes/deferred/SiteStatsUpdate.php', + 'SqlDataUpdate' => 'includes/deferred/SqlDataUpdate.php', + 'SquidUpdate' => 'includes/deferred/SquidUpdate.php', + 'ViewCountUpdate' => 'includes/deferred/ViewCountUpdate.php', # includes/diff - '_DiffEngine' => 'includes/diff/DairikiDiff.php', - '_DiffOp' => 'includes/diff/DairikiDiff.php', - '_DiffOp_Add' => 'includes/diff/DairikiDiff.php', - '_DiffOp_Change' => 'includes/diff/DairikiDiff.php', - '_DiffOp_Copy' => 'includes/diff/DairikiDiff.php', - '_DiffOp_Delete' => 'includes/diff/DairikiDiff.php', - '_HWLDF_WordAccumulator' => 'includes/diff/DairikiDiff.php', - 'ArrayDiffFormatter' => 'includes/diff/DairikiDiff.php', + 'DiffEngine' => 'includes/diff/DairikiDiff.php', + 'DiffOp' => 'includes/diff/DairikiDiff.php', + 'DiffOpAdd' => 'includes/diff/DairikiDiff.php', + 'DiffOpChange' => 'includes/diff/DairikiDiff.php', + 'DiffOpCopy' => 'includes/diff/DairikiDiff.php', + 'DiffOpDelete' => 'includes/diff/DairikiDiff.php', + 'HWLDFWordAccumulator' => 'includes/diff/DairikiDiff.php', + 'ArrayDiffFormatter' => 'includes/diff/ArrayDiffFormatter.php', 'Diff' => 'includes/diff/DairikiDiff.php', 'DifferenceEngine' => 'includes/diff/DifferenceEngine.php', - 'DiffFormatter' => 'includes/diff/DairikiDiff.php', + 'DiffFormatter' => 'includes/diff/DiffFormatter.php', 'MappedDiff' => 'includes/diff/DairikiDiff.php', 'RangeDifference' => 'includes/diff/WikiDiff3.php', - 'TableDiffFormatter' => 'includes/diff/DairikiDiff.php', - 'UnifiedDiffFormatter' => 'includes/diff/DairikiDiff.php', + 'TableDiffFormatter' => 'includes/diff/TableDiffFormatter.php', + 'UnifiedDiffFormatter' => 'includes/diff/UnifiedDiffFormatter.php', 'WikiDiff3' => 'includes/diff/WikiDiff3.php', 'WordLevelDiff' => 'includes/diff/DairikiDiff.php', + # includes/exception + 'UserBlockedError' => 'includes/exception/UserBlockedError.php', + 'UserNotLoggedIn' => 'includes/exception/UserNotLoggedIn.php', + 'ThrottledError' => 'includes/exception/ThrottledError.php', + 'ReadOnlyError' => 'includes/exception/ReadOnlyError.php', + 'PermissionsError' => 'includes/exception/PermissionsError.php', + 'MWException' => 'includes/exception/MWException.php', + 'MWExceptionHandler' => 'includes/exception/MWExceptionHandler.php', + 'HttpError' => 'includes/exception/HttpError.php', + 'BadTitleError' => 'includes/exception/BadTitleError.php', + 'ErrorPageError' => 'includes/exception/ErrorPageError.php', + 'FatalError' => 'includes/exception/FatalError.php', + # includes/externalstore 'ExternalStore' => 'includes/externalstore/ExternalStore.php', 'ExternalStoreDB' => 'includes/externalstore/ExternalStoreDB.php', @@ -560,6 +520,7 @@ $wgAutoloadLocalClasses = array( 'FileBackendGroup' => 'includes/filebackend/FileBackendGroup.php', 'FileBackend' => 'includes/filebackend/FileBackend.php', 'FileBackendError' => 'includes/filebackend/FileBackend.php', + 'FileBackendException' => 'includes/filebackend/FileBackend.php', 'FileBackendStore' => 'includes/filebackend/FileBackendStore.php', 'FileBackendStoreShardListIterator' => 'includes/filebackend/FileBackendStore.php', 'FileBackendStoreShardDirIterator' => 'includes/filebackend/FileBackendStore.php', @@ -572,6 +533,7 @@ $wgAutoloadLocalClasses = array( 'FSFileBackendDirList' => 'includes/filebackend/FSFileBackend.php', 'FSFileBackendFileList' => 'includes/filebackend/FSFileBackend.php', 'FSFileOpHandle' => 'includes/filebackend/FSFileBackend.php', + 'MemoryFileBackend' => 'includes/filebackend/MemoryFileBackend.php', 'SwiftFileBackend' => 'includes/filebackend/SwiftFileBackend.php', 'SwiftFileBackendList' => 'includes/filebackend/SwiftFileBackend.php', 'SwiftFileBackendDirList' => 'includes/filebackend/SwiftFileBackend.php', @@ -586,7 +548,6 @@ $wgAutoloadLocalClasses = array( 'ScopedLock' => 'includes/filebackend/lockmanager/ScopedLock.php', 'FSLockManager' => 'includes/filebackend/lockmanager/FSLockManager.php', 'DBLockManager' => 'includes/filebackend/lockmanager/DBLockManager.php', - 'LSLockManager' => 'includes/filebackend/lockmanager/LSLockManager.php', 'MemcLockManager' => 'includes/filebackend/lockmanager/MemcLockManager.php', 'QuorumLockManager' => 'includes/filebackend/lockmanager/QuorumLockManager.php', 'MySqlLockManager' => 'includes/filebackend/lockmanager/DBLockManager.php', @@ -634,73 +595,83 @@ $wgAutoloadLocalClasses = array( 'InstallDocFormatter' => 'includes/installer/InstallDocFormatter.php', 'Installer' => 'includes/installer/Installer.php', 'LocalSettingsGenerator' => 'includes/installer/LocalSettingsGenerator.php', + 'MssqlInstaller' => 'includes/installer/MssqlInstaller.php', + 'MssqlUpdater' => 'includes/installer/MssqlUpdater.php', 'MysqlInstaller' => 'includes/installer/MysqlInstaller.php', 'MysqlUpdater' => 'includes/installer/MysqlUpdater.php', 'OracleInstaller' => 'includes/installer/OracleInstaller.php', 'OracleUpdater' => 'includes/installer/OracleUpdater.php', - 'PhpRefCallBugTester' => 'includes/installer/PhpBugTests.php', 'PhpXmlBugTester' => 'includes/installer/PhpBugTests.php', 'PostgresInstaller' => 'includes/installer/PostgresInstaller.php', 'PostgresUpdater' => 'includes/installer/PostgresUpdater.php', 'SqliteInstaller' => 'includes/installer/SqliteInstaller.php', 'SqliteUpdater' => 'includes/installer/SqliteUpdater.php', 'WebInstaller' => 'includes/installer/WebInstaller.php', - 'WebInstaller_Complete' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_Copying' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_DBConnect' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_DBSettings' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_Document' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_ExistingWiki' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_Install' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_Language' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_Name' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_Options' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_Readme' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_ReleaseNotes' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_Restart' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_Upgrade' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_UpgradeDoc' => 'includes/installer/WebInstallerPage.php', - 'WebInstaller_Welcome' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerComplete' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerCopying' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerDBConnect' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerDBSettings' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerDocument' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerExistingWiki' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerInstall' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerLanguage' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerName' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerOptions' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerReadme' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerReleaseNotes' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerRestart' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerUpgrade' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerUpgradeDoc' => 'includes/installer/WebInstallerPage.php', + 'WebInstallerWelcome' => 'includes/installer/WebInstallerPage.php', 'WebInstallerOutput' => 'includes/installer/WebInstallerOutput.php', 'WebInstallerPage' => 'includes/installer/WebInstallerPage.php', # includes/job - 'Job' => 'includes/job/Job.php', - 'JobQueue' => 'includes/job/JobQueue.php', - 'JobQueueAggregator' => 'includes/job/aggregator/JobQueueAggregator.php', - 'JobQueueAggregatorMemc' => 'includes/job/aggregator/JobQueueAggregatorMemc.php', - 'JobQueueAggregatorRedis' => 'includes/job/aggregator/JobQueueAggregatorRedis.php', - 'JobQueueDB' => 'includes/job/JobQueueDB.php', - 'JobQueueConnectionError' => 'includes/job/JobQueue.php', - 'JobQueueError' => 'includes/job/JobQueue.php', - 'JobQueueGroup' => 'includes/job/JobQueueGroup.php', - 'JobQueueFederated' => 'includes/job/JobQueueFederated.php', - 'JobQueueRedis' => 'includes/job/JobQueueRedis.php', - - # includes/job/jobs - 'DoubleRedirectJob' => 'includes/job/jobs/DoubleRedirectJob.php', - 'DuplicateJob' => 'includes/job/jobs/DuplicateJob.php', - 'EmaillingJob' => 'includes/job/jobs/EmaillingJob.php', - 'EnotifNotifyJob' => 'includes/job/jobs/EnotifNotifyJob.php', - 'HTMLCacheUpdateJob' => 'includes/job/jobs/HTMLCacheUpdateJob.php', - 'NullJob' => 'includes/job/jobs/NullJob.php', - 'RefreshLinksJob' => 'includes/job/jobs/RefreshLinksJob.php', - 'RefreshLinksJob2' => 'includes/job/jobs/RefreshLinksJob.php', - 'UploadFromUrlJob' => 'includes/job/jobs/UploadFromUrlJob.php', - 'AssembleUploadChunksJob' => 'includes/job/jobs/AssembleUploadChunksJob.php', - 'PublishStashedFileJob' => 'includes/job/jobs/PublishStashedFileJob.php', + 'IJobSpecification' => 'includes/jobqueue/JobSpecification.php', + 'Job' => 'includes/jobqueue/Job.php', + 'JobQueue' => 'includes/jobqueue/JobQueue.php', + 'JobQueueAggregator' => 'includes/jobqueue/aggregator/JobQueueAggregator.php', + 'JobQueueAggregatorMemc' => 'includes/jobqueue/aggregator/JobQueueAggregatorMemc.php', + 'JobQueueAggregatorRedis' => 'includes/jobqueue/aggregator/JobQueueAggregatorRedis.php', + 'JobQueueDB' => 'includes/jobqueue/JobQueueDB.php', + 'JobQueueConnectionError' => 'includes/jobqueue/JobQueue.php', + 'JobQueueError' => 'includes/jobqueue/JobQueue.php', + 'JobQueueGroup' => 'includes/jobqueue/JobQueueGroup.php', + 'JobQueueFederated' => 'includes/jobqueue/JobQueueFederated.php', + 'JobQueueRedis' => 'includes/jobqueue/JobQueueRedis.php', + 'JobRunner' => 'includes/jobqueue/JobRunner.php', + 'JobSpecification' => 'includes/jobqueue/JobSpecification.php', + + # includes/jobqueue/jobs + 'DoubleRedirectJob' => 'includes/jobqueue/jobs/DoubleRedirectJob.php', + 'DuplicateJob' => 'includes/jobqueue/jobs/DuplicateJob.php', + 'EmaillingJob' => 'includes/jobqueue/jobs/EmaillingJob.php', + 'EnotifNotifyJob' => 'includes/jobqueue/jobs/EnotifNotifyJob.php', + 'HTMLCacheUpdateJob' => 'includes/jobqueue/jobs/HTMLCacheUpdateJob.php', + 'NullJob' => 'includes/jobqueue/jobs/NullJob.php', + 'RefreshLinksJob' => 'includes/jobqueue/jobs/RefreshLinksJob.php', + 'RefreshLinksJob2' => 'includes/jobqueue/jobs/RefreshLinksJob2.php', + 'UploadFromUrlJob' => 'includes/jobqueue/jobs/UploadFromUrlJob.php', + 'AssembleUploadChunksJob' => 'includes/jobqueue/jobs/AssembleUploadChunksJob.php', + 'PublishStashedFileJob' => 'includes/jobqueue/jobs/PublishStashedFileJob.php', + + # includes/jobqueue/utils + 'BacklinkJobUtils' => 'includes/jobqueue/utils/BacklinkJobUtils.php', # includes/json 'FormatJson' => 'includes/json/FormatJson.php', # includes/libs 'CSSJanus' => 'includes/libs/CSSJanus.php', - 'CSSJanus_Tokenizer' => 'includes/libs/CSSJanus.php', + 'CSSJanusTokenizer' => 'includes/libs/CSSJanus.php', 'CSSMin' => 'includes/libs/CSSMin.php', 'GenericArrayObject' => 'includes/libs/GenericArrayObject.php', + 'HashRing' => 'includes/libs/HashRing.php', 'HttpStatus' => 'includes/libs/HttpStatus.php', 'IEContentAnalyzer' => 'includes/libs/IEContentAnalyzer.php', 'IEUrlExtension' => 'includes/libs/IEUrlExtension.php', + 'MappedIterator' => 'includes/libs/MappedIterator.php', + 'IPSet' => 'includes/libs/IPSet.php', 'JavaScriptMinifier' => 'includes/libs/JavaScriptMinifier.php', 'JSCompilerContext' => 'includes/libs/jsminplus.php', 'JSMinPlus' => 'includes/libs/jsminplus.php', @@ -708,6 +679,16 @@ $wgAutoloadLocalClasses = array( 'JSParser' => 'includes/libs/jsminplus.php', 'JSToken' => 'includes/libs/jsminplus.php', 'JSTokenizer' => 'includes/libs/jsminplus.php', + 'MultiHttpClient' => 'includes/libs/MultiHttpClient.php', + 'MWMessagePack' => 'includes/libs/MWMessagePack.php', + 'ProcessCacheLRU' => 'includes/libs/ProcessCacheLRU.php', + 'RunningStat' => 'includes/libs/RunningStat.php', + 'ScopedCallback' => 'includes/libs/ScopedCallback.php', + 'ScopedPHPTimeout' => 'includes/libs/ScopedPHPTimeout.php', + 'SwiftVirtualRESTService' => 'includes/libs/virtualrest/SwiftVirtualRESTService.php', + 'VirtualRESTService' => 'includes/libs/virtualrest/VirtualRESTService.php', + 'VirtualRESTServiceClient' => 'includes/libs/virtualrest/VirtualRESTServiceClient.php', + 'XmlTypeCheck' => 'includes/libs/XmlTypeCheck.php', # includes/libs/lessphp 'lessc' => 'includes/libs/lessc.inc.php', @@ -729,6 +710,7 @@ $wgAutoloadLocalClasses = array( 'ManualLogEntry' => 'includes/logging/LogEntry.php', 'MoveLogFormatter' => 'includes/logging/MoveLogFormatter.php', 'NewUsersLogFormatter' => 'includes/logging/NewUsersLogFormatter.php', + 'PageLangLogFormatter' => 'includes/logging/PageLangLogFormatter.php', 'PatrolLog' => 'includes/logging/PatrolLog.php', 'PatrolLogFormatter' => 'includes/logging/PatrolLogFormatter.php', 'RCDatabaseLogEntry' => 'includes/logging/LogEntry.php', @@ -744,6 +726,11 @@ $wgAutoloadLocalClasses = array( 'PackedHoverImageGallery' => 'includes/gallery/PackedOverlayImageGallery.php', 'PackedOverlayImageGallery' => 'includes/gallery/PackedOverlayImageGallery.php', + # includes/mail + 'EmailNotification' => 'includes/mail/EmailNotification.php', + 'MailAddress' => 'includes/mail/MailAddress.php', + 'UserMailer' => 'includes/mail/UserMailer.php', + # includes/media 'BitmapHandler' => 'includes/media/Bitmap.php', 'BitmapHandler_ClientOnly' => 'includes/media/Bitmap_ClientOnly.php', @@ -753,7 +740,6 @@ $wgAutoloadLocalClasses = array( 'DjVuImage' => 'includes/media/DjVuImage.php', 'Exif' => 'includes/media/Exif.php', 'ExifBitmapHandler' => 'includes/media/ExifBitmap.php', - 'FormatExif' => 'includes/media/FormatMetadata.php', 'FormatMetadata' => 'includes/media/FormatMetadata.php', 'GIFHandler' => 'includes/media/GIF.php', 'GIFMetadataExtractor' => 'includes/media/GIFMetadataExtractor.php', @@ -771,6 +757,7 @@ $wgAutoloadLocalClasses = array( 'SVGReader' => 'includes/media/SVGMetadataExtractor.php', 'ThumbnailImage' => 'includes/media/MediaTransformOutput.php', 'TiffHandler' => 'includes/media/Tiff.php', + 'TransformationalImageHandler' => 'includes/media/TransformationalImageHandler.php', 'TransformParameterError' => 'includes/media/MediaTransformOutput.php', 'XCFHandler' => 'includes/media/XCF.php', 'XMPInfo' => 'includes/media/XMPInfo.php', @@ -783,10 +770,7 @@ $wgAutoloadLocalClasses = array( # includes/objectcache 'APCBagOStuff' => 'includes/objectcache/APCBagOStuff.php', 'BagOStuff' => 'includes/objectcache/BagOStuff.php', - 'DBABagOStuff' => 'includes/objectcache/DBABagOStuff.php', - 'EhcacheBagOStuff' => 'includes/objectcache/EhcacheBagOStuff.php', 'EmptyBagOStuff' => 'includes/objectcache/EmptyBagOStuff.php', - 'FakeMemCachedClient' => 'includes/objectcache/EmptyBagOStuff.php', 'HashBagOStuff' => 'includes/objectcache/HashBagOStuff.php', 'MediaWikiBagOStuff' => 'includes/objectcache/SqlBagOStuff.php', 'MemCachedClientforWiki' => 'includes/objectcache/MemcachedClient.php', @@ -802,14 +786,32 @@ $wgAutoloadLocalClasses = array( 'WinCacheBagOStuff' => 'includes/objectcache/WinCacheBagOStuff.php', 'XCacheBagOStuff' => 'includes/objectcache/XCacheBagOStuff.php', + # includes/page + 'Article' => 'includes/page/Article.php', + 'CategoryPage' => 'includes/page/CategoryPage.php', + 'ImageHistoryList' => 'includes/page/ImagePage.php', + 'ImageHistoryPseudoPager' => 'includes/page/ImagePage.php', + 'ImagePage' => 'includes/page/ImagePage.php', + 'Page' => 'includes/page/WikiPage.php', + 'WikiCategoryPage' => 'includes/page/WikiCategoryPage.php', + 'WikiFilePage' => 'includes/page/WikiFilePage.php', + 'WikiPage' => 'includes/page/WikiPage.php', + + # includes/pager + 'AlphabeticPager' => 'includes/pager/AlphabeticPager.php', + 'IndexPager' => 'includes/pager/IndexPager.php', + 'Pager' => 'includes/pager/Pager.php', + 'ReverseChronologicalPager' => 'includes/pager/ReverseChronologicalPager.php', + 'TablePager' => 'includes/pager/TablePager.php', + # includes/parser 'CacheTime' => 'includes/parser/CacheTime.php', 'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php', 'CoreTagHooks' => 'includes/parser/CoreTagHooks.php', 'DateFormatter' => 'includes/parser/DateFormatter.php', 'LinkHolderArray' => 'includes/parser/LinkHolderArray.php', - 'MWTidy' => 'includes/parser/Tidy.php', - 'MWTidyWrapper' => 'includes/parser/Tidy.php', + 'MWTidy' => 'includes/parser/MWTidy.php', + 'MWTidyWrapper' => 'includes/parser/MWTidy.php', 'PPCustomFrame_DOM' => 'includes/parser/Preprocessor_DOM.php', 'PPCustomFrame_Hash' => 'includes/parser/Preprocessor_Hash.php', 'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php', @@ -834,20 +836,36 @@ $wgAutoloadLocalClasses = array( 'ParserCache' => 'includes/parser/ParserCache.php', 'ParserOptions' => 'includes/parser/ParserOptions.php', 'ParserOutput' => 'includes/parser/ParserOutput.php', - 'Parser_DiffTest' => 'includes/parser/Parser_DiffTest.php', + 'ParserDiffTest' => 'includes/parser/ParserDiffTest.php', 'Preprocessor' => 'includes/parser/Preprocessor.php', 'Preprocessor_DOM' => 'includes/parser/Preprocessor_DOM.php', 'Preprocessor_Hash' => 'includes/parser/Preprocessor_Hash.php', 'StripState' => 'includes/parser/StripState.php', + # includes/password + 'BcryptPassword' => 'includes/password/BcryptPassword.php', + 'InvalidPassword' => 'includes/password/InvalidPassword.php', + 'LayeredParameterizedPassword' => 'includes/password/LayeredParameterizedPassword.php', + 'MWSaltedPassword' => 'includes/password/MWSaltedPassword.php', + 'MWOldPassword' => 'includes/password/MWOldPassword.php', + 'ParameterizedPassword' => 'includes/password/ParameterizedPassword.php', + 'Password' => 'includes/password/Password.php', + 'PasswordError' => 'includes/password/PasswordError.php', + 'PasswordFactory' => 'includes/password/PasswordFactory.php', + 'Pbkdf2Password' => 'includes/password/Pbkdf2Password.php', + 'EncryptedPassword' => 'includes/password/EncryptedPassword.php', + # includes/profiler 'Profiler' => 'includes/profiler/Profiler.php', - 'ProfilerSimple' => 'includes/profiler/ProfilerSimple.php', + 'ProfilerMwprof' => 'includes/profiler/ProfilerMwprof.php', + 'ProfilerSimpleDB' => 'includes/profiler/ProfilerSimpleDB.php', 'ProfilerSimpleText' => 'includes/profiler/ProfilerSimpleText.php', 'ProfilerSimpleTrace' => 'includes/profiler/ProfilerSimpleTrace.php', 'ProfilerSimpleUDP' => 'includes/profiler/ProfilerSimpleUDP.php', + 'ProfilerStandard' => 'includes/profiler/ProfilerStandard.php', 'ProfilerStub' => 'includes/profiler/ProfilerStub.php', 'ProfileSection' => 'includes/profiler/Profiler.php', + 'TransactionProfiler' => 'includes/profiler/Profiler.php', # includes/rcfeed 'RCFeedEngine' => 'includes/rcfeed/RCFeedEngine.php', @@ -856,62 +874,65 @@ $wgAutoloadLocalClasses = array( 'RCFeedFormatter' => 'includes/rcfeed/RCFeedFormatter.php', 'IRCColourfulRCFeedFormatter' => 'includes/rcfeed/IRCColourfulRCFeedFormatter.php', 'JSONRCFeedFormatter' => 'includes/rcfeed/JSONRCFeedFormatter.php', + 'XMLRCFeedFormatter' => 'includes/rcfeed/XMLRCFeedFormatter.php', + 'MachineReadableRCFeedFormatter' => 'includes/rcfeed/MachineReadableRCFeedFormatter.php', # includes/resourceloader + 'DerivativeResourceLoaderContext' => + 'includes/resourceloader/DerivativeResourceLoaderContext.php', 'ResourceLoader' => 'includes/resourceloader/ResourceLoader.php', 'ResourceLoaderContext' => 'includes/resourceloader/ResourceLoaderContext.php', + 'ResourceLoaderEditToolbarModule' => 'includes/resourceloader/ResourceLoaderEditToolbarModule.php', 'ResourceLoaderFileModule' => 'includes/resourceloader/ResourceLoaderFileModule.php', 'ResourceLoaderFilePageModule' => 'includes/resourceloader/ResourceLoaderFilePageModule.php', - 'ResourceLoaderLESSFunctions' => 'includes/resourceloader/ResourceLoaderLESSFunctions.php', + 'ResourceLoaderFilePath' => 'includes/resourceloader/ResourceLoaderFilePath.php', 'ResourceLoaderModule' => 'includes/resourceloader/ResourceLoaderModule.php', 'ResourceLoaderNoscriptModule' => 'includes/resourceloader/ResourceLoaderNoscriptModule.php', 'ResourceLoaderSiteModule' => 'includes/resourceloader/ResourceLoaderSiteModule.php', 'ResourceLoaderStartUpModule' => 'includes/resourceloader/ResourceLoaderStartUpModule.php', - 'ResourceLoaderUserCSSPrefsModule' => 'includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php', + 'ResourceLoaderUserCSSPrefsModule' => + 'includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php', 'ResourceLoaderUserGroupsModule' => 'includes/resourceloader/ResourceLoaderUserGroupsModule.php', 'ResourceLoaderUserModule' => 'includes/resourceloader/ResourceLoaderUserModule.php', 'ResourceLoaderUserOptionsModule' => 'includes/resourceloader/ResourceLoaderUserOptionsModule.php', 'ResourceLoaderUserTokensModule' => 'includes/resourceloader/ResourceLoaderUserTokensModule.php', - 'ResourceLoaderLanguageDataModule' => 'includes/resourceloader/ResourceLoaderLanguageDataModule.php', + 'ResourceLoaderLanguageDataModule' => + 'includes/resourceloader/ResourceLoaderLanguageDataModule.php', + 'ResourceLoaderLanguageNamesModule' => + 'includes/resourceloader/ResourceLoaderLanguageNamesModule.php', 'ResourceLoaderWikiModule' => 'includes/resourceloader/ResourceLoaderWikiModule.php', # includes/revisiondelete - 'RevDel_ArchivedFileItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_ArchivedFileList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_ArchivedRevisionItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_ArchiveItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_ArchiveList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_FileItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_FileList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_Item' => 'includes/revisiondelete/RevisionDeleteAbstracts.php', - 'RevDel_List' => 'includes/revisiondelete/RevisionDeleteAbstracts.php', - 'RevDel_LogItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_LogList' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_RevisionItem' => 'includes/revisiondelete/RevisionDelete.php', - 'RevDel_RevisionList' => 'includes/revisiondelete/RevisionDelete.php', + 'RevDelArchivedFileItem' => 'includes/revisiondelete/RevDelArchivedFileItem.php', + 'RevDelArchivedFileList' => 'includes/revisiondelete/RevDelArchivedFileList.php', + 'RevDelArchivedRevisionItem' => 'includes/revisiondelete/RevDelArchivedRevisionItem.php', + 'RevDelArchiveItem' => 'includes/revisiondelete/RevDelArchiveItem.php', + 'RevDelArchiveList' => 'includes/revisiondelete/RevDelArchiveList.php', + 'RevDelFileItem' => 'includes/revisiondelete/RevDelFileItem.php', + 'RevDelFileList' => 'includes/revisiondelete/RevDelFileList.php', + 'RevDelItem' => 'includes/revisiondelete/RevDelItem.php', + 'RevDelList' => 'includes/revisiondelete/RevDelList.php', + 'RevDelLogItem' => 'includes/revisiondelete/RevDelLogItem.php', + 'RevDelLogList' => 'includes/revisiondelete/RevDelLogList.php', + 'RevDelRevisionItem' => 'includes/revisiondelete/RevDelRevisionItem.php', + 'RevDelRevisionList' => 'includes/revisiondelete/RevDelRevisionList.php', 'RevisionDeleter' => 'includes/revisiondelete/RevisionDeleter.php', 'RevisionDeleteUser' => 'includes/revisiondelete/RevisionDeleteUser.php', # includes/search - 'MssqlSearchResultSet' => 'includes/search/SearchMssql.php', - 'MySQLSearchResultSet' => 'includes/search/SearchMySQL.php', - 'PostgresSearchResult' => 'includes/search/SearchPostgres.php', - 'PostgresSearchResultSet' => 'includes/search/SearchPostgres.php', + 'SearchDatabase' => 'includes/search/SearchDatabase.php', 'SearchEngine' => 'includes/search/SearchEngine.php', 'SearchEngineDummy' => 'includes/search/SearchEngine.php', - 'SearchHighlighter' => 'includes/search/SearchEngine.php', + 'SearchHighlighter' => 'includes/search/SearchHighlighter.php', 'SearchMssql' => 'includes/search/SearchMssql.php', 'SearchMySQL' => 'includes/search/SearchMySQL.php', - 'SearchNearMatchResultSet' => 'includes/search/SearchEngine.php', + 'SearchNearMatchResultSet' => 'includes/search/SearchResultSet.php', 'SearchOracle' => 'includes/search/SearchOracle.php', 'SearchPostgres' => 'includes/search/SearchPostgres.php', - 'SearchResult' => 'includes/search/SearchEngine.php', - 'SearchResultSet' => 'includes/search/SearchEngine.php', - 'SearchResultTooMany' => 'includes/search/SearchEngine.php', + 'SearchResult' => 'includes/search/SearchResult.php', + 'SearchResultSet' => 'includes/search/SearchResultSet.php', 'SearchSqlite' => 'includes/search/SearchSqlite.php', - 'SearchUpdate' => 'includes/search/SearchUpdate.php', - 'SqliteSearchResultSet' => 'includes/search/SearchSqlite.php', - 'SqlSearchResultSet' => 'includes/search/SearchEngine.php', + 'SqlSearchResultSet' => 'includes/search/SearchResultSet.php', # includes/site 'MediaWikiSite' => 'includes/site/MediaWikiSite.php', @@ -923,9 +944,35 @@ $wgAutoloadLocalClasses = array( 'Sites' => 'includes/site/SiteSQLStore.php', 'SiteStore' => 'includes/site/SiteStore.php', + # includes/skins + 'BaseTemplate' => 'includes/skins/SkinTemplate.php', + 'MediaWikiI18N' => 'includes/skins/SkinTemplate.php', + 'QuickTemplate' => 'includes/skins/SkinTemplate.php', + 'Skin' => 'includes/skins/Skin.php', + 'SkinException' => 'includes/skins/SkinException.php', + 'SkinFactory' => 'includes/skins/SkinFactory.php', + 'SkinFallback' => 'includes/skins/SkinFallback.php', + 'SkinFallbackTemplate' => 'includes/skins/SkinFallbackTemplate.php', + 'SkinTemplate' => 'includes/skins/SkinTemplate.php', + + # includes/specialpage + 'ChangesListSpecialPage' => 'includes/specialpage/ChangesListSpecialPage.php', + 'FormSpecialPage' => 'includes/specialpage/FormSpecialPage.php', + 'ImageQueryPage' => 'includes/specialpage/ImageQueryPage.php', + 'IncludableSpecialPage' => 'includes/specialpage/IncludableSpecialPage.php', + 'PageQueryPage' => 'includes/specialpage/PageQueryPage.php', + 'QueryPage' => 'includes/specialpage/QueryPage.php', + 'RedirectSpecialArticle' => 'includes/specialpage/RedirectSpecialPage.php', + 'RedirectSpecialPage' => 'includes/specialpage/RedirectSpecialPage.php', + 'SpecialPage' => 'includes/specialpage/SpecialPage.php', + 'SpecialPageFactory' => 'includes/specialpage/SpecialPageFactory.php', + 'SpecialRedirectToSpecial' => 'includes/specialpage/RedirectSpecialPage.php', + 'UnlistedSpecialPage' => 'includes/specialpage/UnlistedSpecialPage.php', + 'WantedQueryPage' => 'includes/specialpage/WantedQueryPage.php', + # includes/specials 'ActiveUsersPager' => 'includes/specials/SpecialActiveusers.php', - 'AllmessagesTablePager' => 'includes/specials/SpecialAllmessages.php', + 'AllMessagesTablePager' => 'includes/specials/SpecialAllMessages.php', 'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php', 'BlockListPager' => 'includes/specials/SpecialBlockList.php', 'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php', @@ -941,15 +988,15 @@ $wgAutoloadLocalClasses = array( 'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php', 'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php', 'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php', - 'HTMLBlockedUsersItemSelect' => 'includes/specials/SpecialBlockList.php', 'ImageListPager' => 'includes/specials/SpecialListfiles.php', 'ImportReporter' => 'includes/specials/SpecialImport.php', - 'IPBlockForm' => 'includes/specials/SpecialBlock.php', 'LinkSearchPage' => 'includes/specials/SpecialLinkSearch.php', 'ListredirectsPage' => 'includes/specials/SpecialListredirects.php', + 'ListDuplicatedFilesPage' => 'includes/specials/SpecialListDuplicatedFiles.php', 'LoginForm' => 'includes/specials/SpecialUserlogin.php', 'LonelyPagesPage' => 'includes/specials/SpecialLonelypages.php', 'LongPagesPage' => 'includes/specials/SpecialLongpages.php', + 'MediaStatisticsPage' => 'includes/specials/SpecialMediaStatistics.php', 'MergeHistoryPager' => 'includes/specials/SpecialMergeHistory.php', 'MIMEsearchPage' => 'includes/specials/SpecialMIMEsearch.php', 'MostcategoriesPage' => 'includes/specials/SpecialMostcategories.php', @@ -969,8 +1016,9 @@ $wgAutoloadLocalClasses = array( 'RandomPage' => 'includes/specials/SpecialRandompage.php', 'ShortPagesPage' => 'includes/specials/SpecialShortpages.php', 'SpecialActiveUsers' => 'includes/specials/SpecialActiveusers.php', - 'SpecialAllmessages' => 'includes/specials/SpecialAllmessages.php', - 'SpecialAllpages' => 'includes/specials/SpecialAllpages.php', + 'SpecialAllMessages' => 'includes/specials/SpecialAllMessages.php', + 'SpecialAllMyUploads' => 'includes/specials/SpecialMyRedirectPages.php', + 'SpecialAllPages' => 'includes/specials/SpecialAllPages.php', 'SpecialBlankpage' => 'includes/specials/SpecialBlankpage.php', 'SpecialBlock' => 'includes/specials/SpecialBlock.php', 'SpecialBlockList' => 'includes/specials/SpecialBlockList.php', @@ -981,23 +1029,34 @@ $wgAutoloadLocalClasses = array( 'SpecialChangePassword' => 'includes/specials/SpecialChangePassword.php', 'SpecialComparePages' => 'includes/specials/SpecialComparePages.php', 'SpecialContributions' => 'includes/specials/SpecialContributions.php', + 'SpecialCreateAccount' => 'includes/specials/SpecialCreateAccount.php', + 'SpecialDiff' => 'includes/specials/SpecialDiff.php', 'SpecialEditWatchlist' => 'includes/specials/SpecialEditWatchlist.php', 'SpecialEmailUser' => 'includes/specials/SpecialEmailuser.php', + 'SpecialExpandTemplates' => 'includes/specials/SpecialExpandTemplates.php', 'SpecialExport' => 'includes/specials/SpecialExport.php', 'SpecialFilepath' => 'includes/specials/SpecialFilepath.php', 'SpecialImport' => 'includes/specials/SpecialImport.php', 'SpecialJavaScriptTest' => 'includes/specials/SpecialJavaScriptTest.php', + 'SpecialListAdmins' => 'includes/specials/SpecialListusers.php', + 'SpecialListBots' => 'includes/specials/SpecialListusers.php', 'SpecialListFiles' => 'includes/specials/SpecialListfiles.php', 'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php', 'SpecialListUsers' => 'includes/specials/SpecialListusers.php', 'SpecialLockdb' => 'includes/specials/SpecialLockdb.php', 'SpecialLog' => 'includes/specials/SpecialLog.php', 'SpecialMergeHistory' => 'includes/specials/SpecialMergeHistory.php', + 'SpecialMycontributions' => 'includes/specials/SpecialMyRedirectPages.php', + 'SpecialMyLanguage' => 'includes/specials/SpecialMyLanguage.php', + 'SpecialMypage' => 'includes/specials/SpecialMyRedirectPages.php', + 'SpecialMytalk' => 'includes/specials/SpecialMyRedirectPages.php', + 'SpecialMyuploads' => 'includes/specials/SpecialMyRedirectPages.php', 'SpecialNewFiles' => 'includes/specials/SpecialNewimages.php', 'SpecialNewpages' => 'includes/specials/SpecialNewpages.php', + 'SpecialPageLanguage' => 'includes/specials/SpecialPageLanguage.php', 'SpecialPasswordReset' => 'includes/specials/SpecialPasswordReset.php', 'SpecialPagesWithProp' => 'includes/specials/SpecialPagesWithProp.php', - 'SpecialPermanentLink' => 'includes/SpecialPage.php', + 'SpecialPermanentLink' => 'includes/specials/SpecialPermanentLink.php', 'SpecialPreferences' => 'includes/specials/SpecialPreferences.php', 'SpecialPrefixindex' => 'includes/specials/SpecialPrefixindex.php', 'SpecialProtectedpages' => 'includes/specials/SpecialProtectedpages.php', @@ -1005,14 +1064,16 @@ $wgAutoloadLocalClasses = array( 'SpecialRandomInCategory' => 'includes/specials/SpecialRandomInCategory.php', 'SpecialRandomredirect' => 'includes/specials/SpecialRandomredirect.php', 'SpecialRecentChanges' => 'includes/specials/SpecialRecentchanges.php', - 'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php', + 'SpecialRecentChangesLinked' => 'includes/specials/SpecialRecentchangeslinked.php', 'SpecialRedirect' => 'includes/specials/SpecialRedirect.php', 'SpecialResetTokens' => 'includes/specials/SpecialResetTokens.php', 'SpecialRevisionDelete' => 'includes/specials/SpecialRevisiondelete.php', + 'SpecialRunJobs' => 'includes/specials/SpecialRunJobs.php', 'SpecialSearch' => 'includes/specials/SpecialSearch.php', 'SpecialSpecialpages' => 'includes/specials/SpecialSpecialpages.php', 'SpecialStatistics' => 'includes/specials/SpecialStatistics.php', 'SpecialTags' => 'includes/specials/SpecialTags.php', + 'SpecialTrackingCategories' => 'includes/specials/SpecialTrackingCategories.php', 'SpecialUnblock' => 'includes/specials/SpecialUnblock.php', 'SpecialUndelete' => 'includes/specials/SpecialUndelete.php', 'SpecialUnlockdb' => 'includes/specials/SpecialUnlockdb.php', @@ -1042,13 +1103,21 @@ $wgAutoloadLocalClasses = array( 'WantedFilesPage' => 'includes/specials/SpecialWantedfiles.php', 'WantedPagesPage' => 'includes/specials/SpecialWantedpages.php', 'WantedTemplatesPage' => 'includes/specials/SpecialWantedtemplates.php', - 'WatchlistEditor' => 'includes/specials/SpecialEditWatchlist.php', 'WithoutInterwikiPage' => 'includes/specials/SpecialWithoutinterwiki.php', # includes/templates 'UserloginTemplate' => 'includes/templates/Userlogin.php', 'UsercreateTemplate' => 'includes/templates/Usercreate.php', + # includes/title + 'PageLinkRenderer' => 'includes/title/PageLinkRenderer.php', + 'TitleFormatter' => 'includes/title/TitleFormatter.php', + 'TitleParser' => 'includes/title/TitleParser.php', + 'TitleValue' => 'includes/title/TitleValue.php', + 'MalformedTitleException' => 'includes/title/MalformedTitleException.php', + 'MediaWikiPageLinkRenderer' => 'includes/title/MediaWikiPageLinkRenderer.php', + 'MediaWikiTitleCodec' => 'includes/title/MediaWikiTitleCodec.php', + # includes/upload 'UploadBase' => 'includes/upload/UploadBase.php', 'UploadFromFile' => 'includes/upload/UploadFromFile.php', @@ -1067,18 +1136,43 @@ $wgAutoloadLocalClasses = array( 'UploadStashWrongOwnerException' => 'includes/upload/UploadStash.php', 'UploadStashNoSuchKeyException' => 'includes/upload/UploadStash.php', + # includes/utils + 'ArrayUtils' => 'includes/utils/ArrayUtils.php', + 'CdbException' => 'includes/utils/Cdb.php', + 'CdbFunctions' => 'includes/utils/CdbPHP.php', + 'CdbReader' => 'includes/utils/Cdb.php', + 'CdbReaderDBA' => 'includes/utils/CdbDBA.php', + 'CdbReaderPHP' => 'includes/utils/CdbPHP.php', + 'CdbWriter' => 'includes/utils/Cdb.php', + 'CdbWriterDBA' => 'includes/utils/CdbDBA.php', + 'CdbWriterPHP' => 'includes/utils/CdbPHP.php', + 'DoubleReplacer' => 'includes/utils/StringUtils.php', + 'ExplodeIterator' => 'includes/utils/StringUtils.php', + 'HashtableReplacer' => 'includes/utils/StringUtils.php', + 'IP' => 'includes/utils/IP.php', + 'MWCryptRand' => 'includes/utils/MWCryptRand.php', + 'MWCryptHKDF' => 'includes/utils/MWCryptHKDF.php', + 'MWFunction' => 'includes/utils/MWFunction.php', + 'RegexlikeReplacer' => 'includes/utils/StringUtils.php', + 'ReplacementArray' => 'includes/utils/StringUtils.php', + 'Replacer' => 'includes/utils/StringUtils.php', + 'StringUtils' => 'includes/utils/StringUtils.php', + 'UIDGenerator' => 'includes/utils/UIDGenerator.php', + 'ZipDirectoryReader' => 'includes/utils/ZipDirectoryReader.php', + 'ZipDirectoryReaderError' => 'includes/utils/ZipDirectoryReader.php', + # languages - 'ConverterRule' => 'languages/LanguageConverter.php', - 'FakeConverter' => 'languages/Language.php', + 'ConverterRule' => 'languages/ConverterRule.php', + 'FakeConverter' => 'languages/FakeConverter.php', 'Language' => 'languages/Language.php', 'LanguageConverter' => 'languages/LanguageConverter.php', - 'CLDRPluralRuleConverter' => 'languages/utils/CLDRPluralRuleEvaluator.php', - 'CLDRPluralRuleConverter_Expression' => 'languages/utils/CLDRPluralRuleEvaluator.php', - 'CLDRPluralRuleConverter_Fragment' => 'languages/utils/CLDRPluralRuleEvaluator.php', - 'CLDRPluralRuleConverter_Operator' => 'languages/utils/CLDRPluralRuleEvaluator.php', + 'CLDRPluralRuleConverter' => 'languages/utils/CLDRPluralRuleConverter.php', + 'CLDRPluralRuleConverterExpression' => 'languages/utils/CLDRPluralRuleConverterExpression.php', + 'CLDRPluralRuleConverterFragment' => 'languages/utils/CLDRPluralRuleConverterFragment.php', + 'CLDRPluralRuleConverterOperator' => 'languages/utils/CLDRPluralRuleConverterOperator.php', 'CLDRPluralRuleEvaluator' => 'languages/utils/CLDRPluralRuleEvaluator.php', - 'CLDRPluralRuleEvaluator_Range' => 'languages/utils/CLDRPluralRuleEvaluator.php', - 'CLDRPluralRuleError' => 'languages/utils/CLDRPluralRuleEvaluator.php', + 'CLDRPluralRuleEvaluatorRange' => 'languages/utils/CLDRPluralRuleEvaluatorRange.php', + 'CLDRPluralRuleError' => 'languages/utils/CLDRPluralRuleError.php', # maintenance 'BackupDumper' => 'maintenance/backup.inc', @@ -1092,6 +1186,7 @@ $wgAutoloadLocalClasses = array( 'FixExtLinksProtocolRelative' => 'maintenance/fixExtLinksProtocolRelative.php', 'LoggedUpdateMaintenance' => 'maintenance/Maintenance.php', 'Maintenance' => 'maintenance/Maintenance.php', + 'PopulateBacklinkNamespace' => 'maintenance/populateBacklinkNamespace.php', 'PopulateCategory' => 'maintenance/populateCategory.php', 'PopulateImageSha1' => 'maintenance/populateImageSha1.php', 'PopulateFilearchiveSha1' => 'maintenance/populateFilearchiveSha1.php', @@ -1108,13 +1203,12 @@ $wgAutoloadLocalClasses = array( 'UserDupes' => 'maintenance/userDupes.inc', # maintenance/language - 'csvStatsOutput' => 'maintenance/language/StatOutputs.php', - 'extensionLanguages' => 'maintenance/language/languages.inc', - 'languages' => 'maintenance/language/languages.inc', - 'MessageWriter' => 'maintenance/language/writeMessagesArray.inc', - 'statsOutput' => 'maintenance/language/StatOutputs.php', - 'textStatsOutput' => 'maintenance/language/StatOutputs.php', - 'wikiStatsOutput' => 'maintenance/language/StatOutputs.php', + 'CsvStatsOutput' => 'maintenance/language/StatOutputs.php', + 'ExtensionLanguages' => 'maintenance/language/languages.inc', + 'Languages' => 'maintenance/language/languages.inc', + 'StatsOutput' => 'maintenance/language/StatOutputs.php', + 'TextStatsOutput' => 'maintenance/language/StatOutputs.php', + 'WikiStatsOutput' => 'maintenance/language/StatOutputs.php', # maintenance/term 'AnsiTermColorer' => 'maintenance/term/MWTerm.php', @@ -1123,29 +1217,19 @@ $wgAutoloadLocalClasses = array( # mw-config 'InstallerOverrides' => 'mw-config/overrides.php', 'MyLocalSettingsGenerator' => 'mw-config/overrides.php', - - # skins - 'CologneBlueTemplate' => 'skins/CologneBlue.php', - 'ModernTemplate' => 'skins/Modern.php', - 'MonoBookTemplate' => 'skins/MonoBook.php', - 'SkinCologneBlue' => 'skins/CologneBlue.php', - 'SkinModern' => 'skins/Modern.php', - 'SkinMonoBook' => 'skins/MonoBook.php', - 'SkinVector' => 'skins/Vector.php', - 'VectorTemplate' => 'skins/Vector.php', ); class AutoLoader { + static protected $autoloadLocalClassesLower = null; + /** * autoload - take a class name and attempt to load it * - * @param string $className name of class we're looking for. - * @return bool Returning false is important on failure as - * it allows Zend to try and look in other registered autoloaders - * as well. + * @param string $className Name of class we're looking for. */ static function autoload( $className ) { - global $wgAutoloadClasses, $wgAutoloadLocalClasses; + global $wgAutoloadClasses, $wgAutoloadLocalClasses, + $wgAutoloadAttemptLowercase; // Workaround for PHP bug <https://bugs.php.net/bug.php?id=49143> (5.3.2. is broken, it's // fixed in 5.3.6). Strip leading backslashes from class names. When namespaces are used, @@ -1156,41 +1240,46 @@ class AutoLoader { // do not strip the leading backlash in this case, causing autoloading to fail. $className = ltrim( $className, '\\' ); + $filename = false; + if ( isset( $wgAutoloadLocalClasses[$className] ) ) { $filename = $wgAutoloadLocalClasses[$className]; } elseif ( isset( $wgAutoloadClasses[$className] ) ) { $filename = $wgAutoloadClasses[$className]; - } else { - # Try a different capitalisation - # The case can sometimes be wrong when unserializing PHP 4 objects - $filename = false; + } elseif ( $wgAutoloadAttemptLowercase ) { + /* + * Try a different capitalisation. + * + * PHP 4 objects are always serialized with the classname coerced to lowercase, + * and we are plagued with several legacy uses created by MediaWiki < 1.5, see + * https://wikitech.wikimedia.org/wiki/Text_storage_data + */ $lowerClass = strtolower( $className ); - foreach ( $wgAutoloadLocalClasses as $class2 => $file2 ) { - if ( strtolower( $class2 ) == $lowerClass ) { - $filename = $file2; - } + if ( self::$autoloadLocalClassesLower === null ) { + self::$autoloadLocalClassesLower = array_change_key_case( $wgAutoloadLocalClasses, CASE_LOWER ); } - if ( !$filename ) { - if ( function_exists( 'wfDebug' ) ) { - wfDebug( "Class {$className} not found; skipped loading\n" ); + if ( isset( self::$autoloadLocalClassesLower[$lowerClass] ) ) { + if ( function_exists( 'wfDebugLog' ) ) { + wfDebugLog( 'autoloader', "Class {$className} was loaded using incorrect case" ); } - - # Give up - return false; + $filename = self::$autoloadLocalClassesLower[$lowerClass]; } } - # Make an absolute path, this improves performance by avoiding some stat calls + if ( !$filename ) { + // Class not found; let the next autoloader try to find it + return; + } + + // Make an absolute path, this improves performance by avoiding some stat calls if ( substr( $filename, 0, 1 ) != '/' && substr( $filename, 1, 1 ) != ':' ) { global $IP; $filename = "$IP/$filename"; } require $filename; - - return true; } /** @@ -1198,12 +1287,20 @@ class AutoLoader { * Sanitizer that have define()s outside of their class definition. Of course * this wouldn't be necessary if everything in MediaWiki was class-based. Sigh. * - * @param $class string - * @return Boolean Return the results of class_exists() so we know if we were successful + * @param string $class + * @return bool Return the results of class_exists() so we know if we were successful */ static function loadClass( $class ) { return class_exists( $class ); } + + /** + * Method to clear the protected class property $autoloadLocalClassesLower. + * Used in tests. + */ + static function resetAutoloadLocalClassesLower() { + self::$autoloadLocalClassesLower = null; + } } spl_autoload_register( array( 'AutoLoader', 'autoload' ) ); diff --git a/includes/Autopromote.php b/includes/Autopromote.php index 170d7abf..81f3b7aa 100644 --- a/includes/Autopromote.php +++ b/includes/Autopromote.php @@ -29,7 +29,7 @@ class Autopromote { /** * Get the groups for the given user based on $wgAutopromote. * - * @param $user User The user to get the groups for + * @param User $user The user to get the groups for * @return array Array of groups to promote to. */ public static function getAutopromoteGroups( User $user ) { @@ -53,8 +53,8 @@ class Autopromote { * * Does not return groups the user already belongs to or has once belonged. * - * @param $user User The user to get the groups for - * @param string $event key in $wgAutopromoteOnce (each one has groups/criteria) + * @param User $user The user to get the groups for + * @param string $event Key in $wgAutopromoteOnce (each one has groups/criteria) * * @return array Groups the user should be promoted to. * @@ -99,8 +99,8 @@ class Autopromote { * This function evaluates the former type recursively, and passes off to * self::checkCondition for evaluation of the latter type. * - * @param $cond Mixed: a condition, possibly containing other conditions - * @param $user User The user to check the conditions against + * @param mixed $cond A condition, possibly containing other conditions + * @param User $user The user to check the conditions against * @return bool Whether the condition is true */ private static function recCheckCondition( $cond, User $user ) { @@ -156,7 +156,7 @@ class Autopromote { * APCOND_AGE. Other types will throw an exception if no extension evaluates them. * * @param array $cond A condition, which must not contain other conditions - * @param $user User The user to check the condition against + * @param User $user The user to check the condition against * @throws MWException * @return bool Whether the condition is true for the user */ @@ -197,7 +197,8 @@ class Autopromote { return in_array( 'bot', User::getGroupPermissions( $user->getGroups() ) ); default: $result = null; - wfRunHooks( 'AutopromoteCondition', array( $cond[0], array_slice( $cond, 1 ), $user, &$result ) ); + wfRunHooks( 'AutopromoteCondition', array( $cond[0], + array_slice( $cond, 1 ), $user, &$result ) ); if ( $result === null ) { throw new MWException( "Unrecognized condition {$cond[0]} for autopromotion!" ); } diff --git a/includes/Block.php b/includes/Block.php index 34b89e73..6a29a056 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -20,33 +20,54 @@ * @file */ class Block { - /* public*/ var $mReason, $mTimestamp, $mAuto, $mExpiry, $mHideName; + /** @var string */ + public $mReason; - protected - $mId, - $mFromMaster, + /** @var bool|string */ + public $mTimestamp; - $mBlockEmail, - $mDisableUsertalk, - $mCreateAccount, - $mParentBlockId; + /** @var int */ + public $mAuto; - /// @var User|String + /** @var bool|string */ + public $mExpiry; + + public $mHideName; + + /** @var int */ + public $mParentBlockId; + + /** @var int */ + protected $mId; + + /** @var bool */ + protected $mFromMaster; + + /** @var bool */ + protected $mBlockEmail; + + /** @var bool */ + protected $mDisableUsertalk; + + /** @var bool */ + protected $mCreateAccount; + + /** @var User|string */ protected $target; - // @var Integer Hack for foreign blocking (CentralAuth) + /** @var int Hack for foreign blocking (CentralAuth) */ protected $forcedTargetID; - /// @var Block::TYPE_ constant. Can only be USER, IP or RANGE internally + /** @var int Block::TYPE_ constant. Can only be USER, IP or RANGE internally */ protected $type; - /// @var User + /** @var User */ protected $blocker; - /// @var Bool + /** @var bool */ protected $isHardblock = true; - /// @var Bool + /** @var bool */ protected $isAutoblocking = true; # TYPE constants @@ -57,14 +78,27 @@ class Block { const TYPE_ID = 5; /** - * Constructor - * @todo FIXME: Don't know what the best format to have for this constructor is, but fourteen - * optional parameters certainly isn't it. + * @todo FIXME: Don't know what the best format to have for this constructor + * is, but fourteen optional parameters certainly isn't it. + * @param string $address + * @param int $user + * @param int $by + * @param string $reason + * @param mixed $timestamp + * @param int $auto + * @param string $expiry + * @param int $anonOnly + * @param int $createAccount + * @param int $enableAutoblock + * @param int $hideName + * @param int $blockEmail + * @param int $allowUsertalk + * @param string $byText */ function __construct( $address = '', $user = 0, $by = 0, $reason = '', $timestamp = 0, $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0, - $hideName = 0, $blockEmail = 0, $allowUsertalk = 0, $byText = '' ) - { + $hideName = 0, $blockEmail = 0, $allowUsertalk = 0, $byText = '' + ) { if ( $timestamp === 0 ) { $timestamp = wfTimestampNow(); } @@ -101,26 +135,11 @@ class Block { $this->mFromMaster = false; } - /** - * Load a block from the database, using either the IP address or - * user ID. Tries the user ID first, and if that doesn't work, tries - * the address. - * - * @param string $address IP address of user/anon - * @param $user Integer: user id of user - * @return Block Object - * @deprecated since 1.18 - */ - public static function newFromDB( $address, $user = 0 ) { - wfDeprecated( __METHOD__, '1.18' ); - return self::newFromTarget( User::whoIs( $user ), $address ); - } - /** * Load a blocked user from their block id. * - * @param $id Integer: Block id to search for - * @return Block object or null + * @param int $id Block id to search for + * @return Block|null */ public static function newFromID( $id ) { $dbr = wfGetDB( DB_SLAVE ); @@ -166,7 +185,7 @@ class Block { * Check if two blocks are effectively equal. Doesn't check irrelevant things like * the blocking user or the block timestamp, only things which affect the blocked user * - * @param $block Block + * @param Block $block * * @return bool */ @@ -186,53 +205,15 @@ class Block { ); } - /** - * Clear all member variables in the current object. Does not clear - * the block from the DB. - * @deprecated since 1.18 - */ - public function clear() { - wfDeprecated( __METHOD__, '1.18' ); - # Noop - } - - /** - * Get a block from the DB, with either the given address or the given username - * - * @param string $address The IP address of the user, or blank to skip IP blocks - * @param int $user The user ID, or zero for anonymous users - * @return Boolean: the user is blocked from editing - * @deprecated since 1.18 - */ - public function load( $address = '', $user = 0 ) { - wfDeprecated( __METHOD__, '1.18' ); - if ( $user ) { - $username = User::whoIs( $user ); - $block = self::newFromTarget( $username, $address ); - } else { - $block = self::newFromTarget( null, $address ); - } - - if ( $block instanceof Block ) { - # This is mildly evil, but hey, it's B/C :D - foreach ( $block as $variable => $value ) { - $this->$variable = $value; - } - return true; - } else { - return false; - } - } - /** * Load a block from the database which affects the already-set $this->target: * 1) A block directly on the given user or IP * 2) A rangeblock encompassing the given IP (smallest first) * 3) An autoblock on the given IP - * @param $vagueTarget User|String also search for blocks affecting this target. Doesn't + * @param User|string $vagueTarget Also search for blocks affecting this target. Doesn't * make any sense to use TYPE_AUTO / TYPE_ID here. Leave blank to skip IP lookups. * @throws MWException - * @return Bool whether a relevant block was found + * @return bool Whether a relevant block was found */ protected function newLoad( $vagueTarget = null ) { $db = wfGetDB( $this->mFromMaster ? DB_MASTER : DB_SLAVE ); @@ -333,7 +314,7 @@ class Block { * Get a set of SQL conditions which will select rangeblocks encompassing a given range * @param string $start Hexadecimal IP representation * @param string $end Hexadecimal IP representation, or null to use $start = $end - * @return String + * @return string */ public static function getRangeCond( $start, $end = null ) { if ( $end === null ) { @@ -365,8 +346,8 @@ class Block { /** * Get the component of an IP address which is certain to be the same between an IP * address and a rangeblock containing that IP address. - * @param $hex String Hexadecimal IP representation - * @return String + * @param string $hex Hexadecimal IP representation + * @return string */ protected static function getIpFragment( $hex ) { global $wgBlockCIDRLimit; @@ -380,7 +361,7 @@ class Block { /** * Given a database row from the ipblocks table, initialize * member variables - * @param $row ResultWrapper: a row from the ipblocks table + * @param stdClass $row A row from the ipblocks table */ protected function initFromRow( $row ) { $this->setTarget( $row->ipb_address ); @@ -415,7 +396,7 @@ class Block { /** * Create a new Block object from a database row - * @param $row ResultWrapper row from the ipblocks table + * @param stdClass $row Row from the ipblocks table * @return Block */ public static function newFromRow( $row ) { @@ -428,7 +409,7 @@ class Block { * Delete the row from the IP blocks table. * * @throws MWException - * @return Boolean + * @return bool */ public function delete() { if ( wfReadOnly() ) { @@ -450,8 +431,8 @@ class Block { * Insert a block into the block table. Will fail if there is a conflicting * block (same name and options) already in the database. * - * @param $dbw DatabaseBase if you have one available - * @return mixed: false on failure, assoc array on success: + * @param DatabaseBase $dbw If you have one available + * @return bool|array False on failure, assoc array on success: * ('id' => block ID, 'autoIds' => array of autoblock IDs) */ public function insert( $dbw = null ) { @@ -488,13 +469,15 @@ class Block { * Update a block in the DB with new parameters. * The ID field needs to be loaded first. * - * @return Int number of affected rows, which should probably be 1 or something has - * gone slightly awry + * @return bool|array False on failure, array on success: + * ('id' => block ID, 'autoIds' => array of autoblock IDs) */ public function update() { wfDebug( "Block::update; timestamp {$this->mTimestamp}\n" ); $dbw = wfGetDB( DB_MASTER ); + $dbw->startAtomic( __METHOD__ ); + $dbw->update( 'ipblocks', $this->getDatabaseArray( $dbw ), @@ -502,13 +485,39 @@ class Block { __METHOD__ ); - return $dbw->affectedRows(); + $affected = $dbw->affectedRows(); + + if ( $this->isAutoblocking() ) { + // update corresponding autoblock(s) (bug 48813) + $dbw->update( + 'ipblocks', + $this->getAutoblockUpdateArray(), + array( 'ipb_parent_block_id' => $this->getId() ), + __METHOD__ + ); + } else { + // autoblock no longer required, delete corresponding autoblock(s) + $dbw->delete( + 'ipblocks', + array( 'ipb_parent_block_id' => $this->getId() ), + __METHOD__ + ); + } + + $dbw->endAtomic( __METHOD__ ); + + if ( $affected ) { + $auto_ipd_ids = $this->doRetroactiveAutoblock(); + return array( 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ); + } + + return false; } /** * Get an array suitable for passing to $dbw->insert() or $dbw->update() - * @param $db DatabaseBase - * @return Array + * @param DatabaseBase $db + * @return array */ protected function getDatabaseArray( $db = null ) { if ( !$db ) { @@ -545,11 +554,25 @@ class Block { return $a; } + /** + * @return array + */ + protected function getAutoblockUpdateArray() { + return array( + 'ipb_by' => $this->getBy(), + 'ipb_by_text' => $this->getByName(), + 'ipb_reason' => $this->mReason, + 'ipb_create_account' => $this->prevents( 'createaccount' ), + 'ipb_deleted' => (int)$this->mHideName, // typecast required for SQLite + 'ipb_allow_usertalk' => !$this->prevents( 'editownusertalk' ), + ); + } + /** * Retroactively autoblocks the last IP used by the user (if it is a user) * blocked by this Block. * - * @return Array: block IDs of retroactive autoblocks made + * @return array Block IDs of retroactive autoblocks made */ protected function doRetroactiveAutoblock() { $blockIds = array(); @@ -573,7 +596,6 @@ class Block { * * @param Block $block * @param array &$blockIds - * @return Array: block IDs of retroactive autoblocks made */ protected static function defaultRetroactiveAutoblock( Block $block, array &$blockIds ) { global $wgPutIPinRC; @@ -614,7 +636,7 @@ class Block { * TODO: this probably belongs somewhere else, but not sure where... * * @param string $ip The IP to check - * @return Boolean + * @return bool */ public static function isWhitelistedFromAutoblocks( $ip ) { global $wgMemc; @@ -656,8 +678,8 @@ class Block { /** * Autoblocks the given IP, referring to this Block. * - * @param string $autoblockIP the IP to autoblock. - * @return mixed: block ID if an autoblock was inserted, false if not. + * @param string $autoblockIP The IP to autoblock. + * @return int|bool Block ID if an autoblock was inserted, false if not. */ public function doAutoblock( $autoblockIP ) { # If autoblocks are disabled, go away. @@ -698,7 +720,8 @@ class Block { wfDebug( "Autoblocking {$this->getTarget()}@" . $autoblockIP . "\n" ); $autoblock->setTarget( $autoblockIP ); $autoblock->setBlocker( $this->getBlocker() ); - $autoblock->mReason = wfMessage( 'autoblocker', $this->getTarget(), $this->mReason )->inContentLanguage()->plain(); + $autoblock->mReason = wfMessage( 'autoblocker', $this->getTarget(), $this->mReason ) + ->inContentLanguage()->plain(); $timestamp = wfTimestampNow(); $autoblock->mTimestamp = $timestamp; $autoblock->mAuto = 1; @@ -726,7 +749,7 @@ class Block { /** * Check if a block has expired. Delete it if it is. - * @return Boolean + * @return bool */ public function deleteIfExpired() { wfProfileIn( __METHOD__ ); @@ -746,7 +769,7 @@ class Block { /** * Has the block expired? - * @return Boolean + * @return bool */ public function isExpired() { $timestamp = wfTimestampNow(); @@ -761,7 +784,7 @@ class Block { /** * Is the block address valid (i.e. not a null string?) - * @return Boolean + * @return bool */ public function isValid() { return $this->getTarget() != null; @@ -792,7 +815,7 @@ class Block { /** * Get the IP address at the start of the range in Hex form * @throws MWException - * @return String IP in Hex form + * @return string IP in Hex form */ public function getRangeStart() { switch ( $this->type ) { @@ -811,7 +834,7 @@ class Block { /** * Get the IP address at the end of the range in Hex form * @throws MWException - * @return String IP in Hex form + * @return string IP in Hex form */ public function getRangeEnd() { switch ( $this->type ) { @@ -830,7 +853,7 @@ class Block { /** * Get the user id of the blocking sysop * - * @return Integer (0 for foreign users) + * @return int (0 for foreign users) */ public function getBy() { $blocker = $this->getBlocker(); @@ -842,7 +865,7 @@ class Block { /** * Get the username of the blocking sysop * - * @return String + * @return string */ public function getByName() { $blocker = $this->getBlocker(); @@ -859,22 +882,11 @@ class Block { return $this->mId; } - /** - * Get/set the SELECT ... FOR UPDATE flag - * @deprecated since 1.18 - * - * @param $x Bool - */ - public function forUpdate( $x = null ) { - wfDeprecated( __METHOD__, '1.18' ); - # noop - } - /** * Get/set a flag determining whether the master is used for reads * - * @param $x Bool - * @return Bool + * @param bool $x + * @return bool */ public function fromMaster( $x = null ) { return wfSetVar( $this->mFromMaster, $x ); @@ -882,8 +894,8 @@ class Block { /** * Get/set whether the Block is a hardblock (affects logged-in users on a given IP/range - * @param $x Bool - * @return Bool + * @param bool $x + * @return bool */ public function isHardblock( $x = null ) { wfSetVar( $this->isHardblock, $x ); @@ -906,9 +918,9 @@ class Block { /** * Get/set whether the Block prevents a given action - * @param $action String - * @param $x Bool - * @return Bool + * @param string $action + * @param bool $x + * @return bool */ public function prevents( $action, $x = null ) { switch ( $action ) { @@ -932,7 +944,7 @@ class Block { /** * Get the block name, but with autoblocked IPs hidden as per standard privacy policy - * @return String, text is escaped + * @return string Text is escaped */ public function getRedactedName() { if ( $this->mAuto ) { @@ -946,38 +958,11 @@ class Block { } } - /** - * Encode expiry for DB - * - * @param string $expiry timestamp for expiry, or - * @param $db DatabaseBase object - * @return String - * @deprecated since 1.18; use $dbw->encodeExpiry() instead - */ - public static function encodeExpiry( $expiry, $db ) { - wfDeprecated( __METHOD__, '1.18' ); - return $db->encodeExpiry( $expiry ); - } - - /** - * Decode expiry which has come from the DB - * - * @param string $expiry Database expiry format - * @param int $timestampType Requested timestamp format - * @return String - * @deprecated since 1.18; use $wgLang->formatExpiry() instead - */ - public static function decodeExpiry( $expiry, $timestampType = TS_MW ) { - wfDeprecated( __METHOD__, '1.18' ); - global $wgContLang; - return $wgContLang->formatExpiry( $expiry, $timestampType ); - } - /** * Get a timestamp of the expiry for autoblocks * - * @param $timestamp String|Int - * @return String + * @param string|int $timestamp + * @return string */ public static function getAutoblockExpiry( $timestamp ) { global $wgAutoblockExpiry; @@ -985,18 +970,6 @@ class Block { return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry ); } - /** - * Gets rid of unneeded numbers in quad-dotted/octet IP strings - * For example, 127.111.113.151/24 -> 127.111.113.0/24 - * @param string $range IP address to normalize - * @return string - * @deprecated since 1.18, call IP::sanitizeRange() directly - */ - public static function normaliseRange( $range ) { - wfDeprecated( __METHOD__, '1.18' ); - return IP::sanitizeRange( $range ); - } - /** * Purge expired blocks from the ipblocks table */ @@ -1007,38 +980,15 @@ class Block { $method = __METHOD__; $dbw = wfGetDB( DB_MASTER ); - $dbw->onTransactionIdle( function() use ( $dbw, $method ) { + $dbw->onTransactionIdle( function () use ( $dbw, $method ) { $dbw->delete( 'ipblocks', array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), $method ); } ); } - /** - * Get a value to insert into expiry field of the database when infinite expiry - * is desired - * @deprecated since 1.18, call $dbr->getInfinity() directly - * @return String - */ - public static function infinity() { - wfDeprecated( __METHOD__, '1.18' ); - return wfGetDB( DB_SLAVE )->getInfinity(); - } - - /** - * Convert a submitted expiry time, which may be relative ("2 weeks", etc) or absolute - * ("24 May 2034"), into an absolute timestamp we can put into the database. - * @param string $expiry whatever was typed into the form - * @return String: timestamp or "infinity" string for th DB implementation - * @deprecated since 1.18 moved to SpecialBlock::parseExpiryInput() - */ - public static function parseExpiryInput( $expiry ) { - wfDeprecated( __METHOD__, '1.18' ); - return SpecialBlock::parseExpiryInput( $expiry ); - } - /** * Given a target and the target's type, get an existing Block object if possible. - * @param $specificTarget String|User|Int a block target, which may be one of several types: + * @param string|User|int $specificTarget A block target, which may be one of several types: * * A user to block, in which case $target will be a User * * An IP to block, in which case $target will be a User generated by using * User::newFromName( $ip, false ) to turn off name validation @@ -1048,10 +998,10 @@ class Block { * Calling this with a user, IP address or range will not select autoblocks, and will * only select a block where the targets match exactly (so looking for blocks on * 1.2.3.4 will not select 1.2.0.0/16 or even 1.2.3.4/32) - * @param $vagueTarget String|User|Int as above, but we will search for *any* block which + * @param string|User|int $vagueTarget As above, but we will search for *any* block which * affects that target (so for an IP address, get ranges containing that IP; and also * get any relevant autoblocks). Leave empty or blank to skip IP-based lookups. - * @param bool $fromMaster whether to use the DB_MASTER database + * @param bool $fromMaster Whether to use the DB_MASTER database * @return Block|null (null if no relevant block could be found). The target and type * of the returned Block will refer to the actual block which was found, which might * not be the same as the target you gave if you used $vagueTarget! @@ -1068,7 +1018,10 @@ class Block { # passed by some callers (bug 29116) return null; - } elseif ( in_array( $type, array( Block::TYPE_USER, Block::TYPE_IP, Block::TYPE_RANGE, null ) ) ) { + } elseif ( in_array( + $type, + array( Block::TYPE_USER, Block::TYPE_IP, Block::TYPE_RANGE, null ) ) + ) { $block = new Block(); $block->fromMaster( $fromMaster ); @@ -1083,15 +1036,14 @@ class Block { return null; } - /** * Get all blocks that match any IP from an array of IP addresses * - * @param Array $ipChain list of IPs (strings), usually retrieved from the + * @param array $ipChain List of IPs (strings), usually retrieved from the * X-Forwarded-For header of the request - * @param Bool $isAnon Exclude anonymous-only blocks if false - * @param Bool $fromMaster Whether to query the master or slave database - * @return Array of Blocks + * @param bool $isAnon Exclude anonymous-only blocks if false + * @param bool $fromMaster Whether to query the master or slave database + * @return array Array of Blocks * @since 1.22 */ public static function getBlocksForIPList( array $ipChain, $isAnon, $fromMaster = false ) { @@ -1111,7 +1063,7 @@ class Block { continue; } # Don't check trusted IPs (includes local squids which will be in every request) - if ( wfIsTrustedProxy( $ipaddr ) ) { + if ( IP::isTrustedProxy( $ipaddr ) ) { continue; } # Check both the original IP (to check against single blocks), as well as build @@ -1165,13 +1117,13 @@ class Block { * - Other softblocks are chosen over autoblocks * - If there are multiple exact or range blocks at the same level, the one chosen * is random - - * @param Array $ipChain list of IPs (strings). This is used to determine how "close" + * + * @param array $blocks Array of blocks + * @param array $ipChain List of IPs (strings). This is used to determine how "close" * a block is to the server, and if a block matches exactly, or is in a range. * The order is furthest from the server to nearest e.g., (Browser, proxy1, proxy2, * local-squid, ...) - * @param Array $block Array of blocks - * @return Block|null the "best" block from the list + * @return Block|null The "best" block from the list */ public static function chooseBlock( array $blocks, array $ipChain ) { if ( !count( $blocks ) ) { @@ -1184,7 +1136,7 @@ class Block { // Sort hard blocks before soft ones and secondarily sort blocks // that disable account creation before those that don't. - usort( $blocks, function( Block $a, Block $b ) { + usort( $blocks, function ( Block $a, Block $b ) { $aWeight = (int)$a->isHardblock() . (int)$a->prevents( 'createaccount' ); $bWeight = (int)$b->isHardblock() . (int)$b->prevents( 'createaccount' ); return strcmp( $bWeight, $aWeight ); // highest weight first @@ -1275,7 +1227,7 @@ class Block { * as a string; for User objects this will return User::__toString() * which in turn gives User::getName(). * - * @param $target String|Int|User|null + * @param string|int|User|null $target * @return array( User|String|null, Block::TYPE_ constant|null ) */ public static function parseTarget( $target ) { @@ -1332,7 +1284,7 @@ class Block { /** * Get the type of target for this particular block - * @return Block::TYPE_ constant, will never be TYPE_ID + * @return int Block::TYPE_ constant, will never be TYPE_ID */ public function getType() { return $this->mAuto @@ -1355,7 +1307,7 @@ class Block { * Get the target for this particular Block. Note that for autoblocks, * this returns the unredacted name; frontend functions need to call $block->getRedactedName() * in this situation. - * @return User|String + * @return User|string */ public function getTarget() { return $this->target; @@ -1364,7 +1316,7 @@ class Block { /** * @since 1.19 * - * @return Mixed|string + * @return mixed|string */ public function getExpiry() { return $this->mExpiry; @@ -1372,7 +1324,7 @@ class Block { /** * Set the target for this block, and update $this->type accordingly - * @param $target Mixed + * @param mixed $target */ public function setTarget( $target ) { list( $this->target, $this->type ) = self::parseTarget( $target ); @@ -1388,7 +1340,7 @@ class Block { /** * Set the user who implemented (or will implement) this block - * @param $user User|string Local User object or username string for foreign users + * @param User|string $user Local User object or username string for foreign users */ public function setBlocker( $user ) { $this->blocker = $user; @@ -1429,7 +1381,7 @@ class Block { $this->getId(), $lang->formatExpiry( $this->mExpiry ), (string)$intended, - $lang->timeanddate( wfTimestamp( TS_MW, $this->mTimestamp ), true ), + $lang->userTimeAndDate( $this->mTimestamp, $context->getUser() ), ); } } diff --git a/includes/CacheHelper.php b/includes/CacheHelper.php deleted file mode 100644 index f0ae5a31..00000000 --- a/includes/CacheHelper.php +++ /dev/null @@ -1,392 +0,0 @@ -<?php -/** - * Cache of various elements in a single cache entry. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @license GNU GPL v2 or later - * @author Jeroen De Dauw < jeroendedauw@gmail.com > - */ - -/** - * Interface for all classes implementing CacheHelper functionality. - * - * @since 1.20 - */ -interface ICacheHelper { - - /** - * Sets if the cache should be enabled or not. - * - * @since 1.20 - * @param boolean $cacheEnabled - */ - function setCacheEnabled( $cacheEnabled ); - - /** - * Initializes the caching. - * Should be called before the first time anything is added via addCachedHTML. - * - * @since 1.20 - * - * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp. - * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not. - */ - function startCache( $cacheExpiry = null, $cacheEnabled = null ); - - /** - * Get a cached value if available or compute it if not and then cache it if possible. - * The provided $computeFunction is only called when the computation needs to happen - * and should return a result value. $args are arguments that will be passed to the - * compute function when called. - * - * @since 1.20 - * - * @param {function} $computeFunction - * @param array|mixed $args - * @param string|null $key - * - * @return mixed - */ - function getCachedValue( $computeFunction, $args = array(), $key = null ); - - /** - * Saves the HTML to the cache in case it got recomputed. - * Should be called after the last time anything is added via addCachedHTML. - * - * @since 1.20 - */ - function saveCache(); - - /** - * Sets the time to live for the cache, in seconds or a unix timestamp - * indicating the point of expiry... - * - * @since 1.20 - * - * @param integer $cacheExpiry - */ - function setExpiry( $cacheExpiry ); - -} - -/** - * Helper class for caching various elements in a single cache entry. - * - * To get a cached value or compute it, use getCachedValue like this: - * $this->getCachedValue( $callback ); - * - * To add HTML that should be cached, use addCachedHTML like this: - * $this->addCachedHTML( $callback ); - * - * The callback function is only called when needed, so do all your expensive - * computations here. This function should returns the HTML to be cached. - * It should not add anything to the PageOutput object! - * - * Before the first addCachedHTML call, you should call $this->startCache(); - * After adding the last HTML that should be cached, call $this->saveCache(); - * - * @since 1.20 - */ -class CacheHelper implements ICacheHelper { - - /** - * The time to live for the cache, in seconds or a unix timestamp indicating the point of expiry. - * - * @since 1.20 - * @var integer - */ - protected $cacheExpiry = 3600; - - /** - * List of HTML chunks to be cached (if !hasCached) or that where cached (of hasCached). - * If not cached already, then the newly computed chunks are added here, - * if it as cached already, chunks are removed from this list as they are needed. - * - * @since 1.20 - * @var array - */ - protected $cachedChunks; - - /** - * Indicates if the to be cached content was already cached. - * Null if this information is not available yet. - * - * @since 1.20 - * @var boolean|null - */ - protected $hasCached = null; - - /** - * If the cache is enabled or not. - * - * @since 1.20 - * @var boolean - */ - protected $cacheEnabled = true; - - /** - * Function that gets called when initialization is done. - * - * @since 1.20 - * @var callable - */ - protected $onInitHandler = false; - - /** - * Elements to build a cache key with. - * - * @since 1.20 - * @var array - */ - protected $cacheKey = array(); - - /** - * Sets if the cache should be enabled or not. - * - * @since 1.20 - * @param boolean $cacheEnabled - */ - public function setCacheEnabled( $cacheEnabled ) { - $this->cacheEnabled = $cacheEnabled; - } - - /** - * Initializes the caching. - * Should be called before the first time anything is added via addCachedHTML. - * - * @since 1.20 - * - * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp. - * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not. - */ - public function startCache( $cacheExpiry = null, $cacheEnabled = null ) { - if ( is_null( $this->hasCached ) ) { - if ( !is_null( $cacheExpiry ) ) { - $this->cacheExpiry = $cacheExpiry; - } - - if ( !is_null( $cacheEnabled ) ) { - $this->setCacheEnabled( $cacheEnabled ); - } - - $this->initCaching(); - } - } - - /** - * Returns a message that notifies the user he/she is looking at - * a cached version of the page, including a refresh link. - * - * @since 1.20 - * - * @param IContextSource $context - * @param boolean $includePurgeLink - * - * @return string - */ - public function getCachedNotice( IContextSource $context, $includePurgeLink = true ) { - if ( $this->cacheExpiry < 86400 * 3650 ) { - $message = $context->msg( - 'cachedspecial-viewing-cached-ttl', - $context->getLanguage()->formatDuration( $this->cacheExpiry ) - )->escaped(); - } - else { - $message = $context->msg( - 'cachedspecial-viewing-cached-ts' - )->escaped(); - } - - if ( $includePurgeLink ) { - $refreshArgs = $context->getRequest()->getQueryValues(); - unset( $refreshArgs['title'] ); - $refreshArgs['action'] = 'purge'; - - $subPage = $context->getTitle()->getFullText(); - $subPage = explode( '/', $subPage, 2 ); - $subPage = count( $subPage ) > 1 ? $subPage[1] : false; - - $message .= ' ' . Linker::link( - $context->getTitle( $subPage ), - $context->msg( 'cachedspecial-refresh-now' )->escaped(), - array(), - $refreshArgs - ); - } - - return $message; - } - - /** - * Initializes the caching if not already done so. - * Should be called before any of the caching functionality is used. - * - * @since 1.20 - */ - protected function initCaching() { - if ( $this->cacheEnabled && is_null( $this->hasCached ) ) { - $cachedChunks = wfGetCache( CACHE_ANYTHING )->get( $this->getCacheKeyString() ); - - $this->hasCached = is_array( $cachedChunks ); - $this->cachedChunks = $this->hasCached ? $cachedChunks : array(); - - if ( $this->onInitHandler !== false ) { - call_user_func( $this->onInitHandler, $this->hasCached ); - } - } - } - - /** - * Get a cached value if available or compute it if not and then cache it if possible. - * The provided $computeFunction is only called when the computation needs to happen - * and should return a result value. $args are arguments that will be passed to the - * compute function when called. - * - * @since 1.20 - * - * @param {function} $computeFunction - * @param array|mixed $args - * @param string|null $key - * - * @return mixed - */ - public function getCachedValue( $computeFunction, $args = array(), $key = null ) { - $this->initCaching(); - - if ( $this->cacheEnabled && $this->hasCached ) { - $value = null; - - if ( is_null( $key ) ) { - $itemKey = array_keys( array_slice( $this->cachedChunks, 0, 1 ) ); - $itemKey = array_shift( $itemKey ); - - if ( !is_integer( $itemKey ) ) { - wfWarn( "Attempted to get item with non-numeric key while the next item in the queue has a key ($itemKey) in " . __METHOD__ ); - } - elseif ( is_null( $itemKey ) ) { - wfWarn( "Attempted to get an item while the queue is empty in " . __METHOD__ ); - } - else { - $value = array_shift( $this->cachedChunks ); - } - } - else { - if ( array_key_exists( $key, $this->cachedChunks ) ) { - $value = $this->cachedChunks[$key]; - unset( $this->cachedChunks[$key] ); - } - else { - wfWarn( "There is no item with key '$key' in this->cachedChunks in " . __METHOD__ ); - } - } - } - else { - if ( !is_array( $args ) ) { - $args = array( $args ); - } - - $value = call_user_func_array( $computeFunction, $args ); - - if ( $this->cacheEnabled ) { - if ( is_null( $key ) ) { - $this->cachedChunks[] = $value; - } - else { - $this->cachedChunks[$key] = $value; - } - } - } - - return $value; - } - - /** - * Saves the HTML to the cache in case it got recomputed. - * Should be called after the last time anything is added via addCachedHTML. - * - * @since 1.20 - */ - public function saveCache() { - if ( $this->cacheEnabled && $this->hasCached === false && !empty( $this->cachedChunks ) ) { - wfGetCache( CACHE_ANYTHING )->set( $this->getCacheKeyString(), $this->cachedChunks, $this->cacheExpiry ); - } - } - - /** - * Sets the time to live for the cache, in seconds or a unix timestamp - * indicating the point of expiry... - * - * @since 1.20 - * - * @param integer $cacheExpiry - */ - public function setExpiry( $cacheExpiry ) { - $this->cacheExpiry = $cacheExpiry; - } - - /** - * Returns the cache key to use to cache this page's HTML output. - * Is constructed from the special page name and language code. - * - * @since 1.20 - * - * @return string - * @throws MWException - */ - protected function getCacheKeyString() { - if ( $this->cacheKey === array() ) { - throw new MWException( 'No cache key set, so cannot obtain or save the CacheHelper values.' ); - } - - return call_user_func_array( 'wfMemcKey', $this->cacheKey ); - } - - /** - * Sets the cache key that should be used. - * - * @since 1.20 - * - * @param array $cacheKey - */ - public function setCacheKey( array $cacheKey ) { - $this->cacheKey = $cacheKey; - } - - /** - * Rebuild the content, even if it's already cached. - * This effectively has the same effect as purging the cache, - * since it will be overridden with the new value on the next request. - * - * @since 1.20 - */ - public function rebuildOnDemand() { - $this->hasCached = false; - } - - /** - * Sets a function that gets called when initialization of the cache is done. - * - * @since 1.20 - * - * @param $handlerFunction - */ - public function setOnInitializedHandler( $handlerFunction ) { - $this->onInitHandler = $handlerFunction; - } - -} diff --git a/includes/CallableUpdate.php b/includes/CallableUpdate.php deleted file mode 100644 index 6eb55413..00000000 --- a/includes/CallableUpdate.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php - -/** - * Deferrable Update for closure/callback - */ -class MWCallableUpdate implements DeferrableUpdate { - - /** - * @var closure/callabck - */ - private $callback; - - /** - * @param callable $callback - */ - public function __construct( $callback ) { - if ( !is_callable( $callback ) ) { - throw new MWException( 'Not a valid callback/closure!' ); - } - $this->callback = $callback; - } - - /** - * Run the update - */ - public function doUpdate() { - call_user_func( $this->callback ); - } - -} diff --git a/includes/Category.php b/includes/Category.php index 126b8fee..322b0530 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -26,7 +26,7 @@ * like to refresh link counts, the objects will be appropriately reinitialized. * Member variables are lazy-initialized. * - * TODO: Move some stuff from CategoryPage.php to here, and use that. + * @todo Move some stuff from CategoryPage.php to here, and use that. */ class Category { /** Name of the category, normalized to DB-key form */ @@ -75,7 +75,8 @@ class Category { if ( !$row ) { # Okay, there were no contents. Nothing to initialize. if ( $this->mTitle ) { - # If there is a title object but no record in the category table, treat this as an empty category + # If there is a title object but no record in the category table, + # treat this as an empty category. $this->mID = false; $this->mName = $this->mTitle->getDBkey(); $this->mPages = 0; @@ -128,8 +129,8 @@ class Category { /** * Factory function. * - * @param $title Title for the category page - * @return Category|bool on a totally invalid name + * @param Title $title Title for the category page + * @return Category|bool On a totally invalid name */ public static function newFromTitle( $title ) { $cat = new self(); @@ -143,7 +144,7 @@ class Category { /** * Factory function. * - * @param $id Integer: a category id + * @param int $id A category id * @return Category */ public static function newFromID( $id ) { @@ -155,11 +156,13 @@ class Category { /** * Factory function, for constructing a Category object from a result set * - * @param $row result set row, must contain the cat_xxx fields. If the fields are null, - * the resulting Category object will represent an empty category if a title object - * was given. If the fields are null and no title was given, this method fails and returns false. - * @param Title $title optional title object for the category represented by the given row. - * May be provided if it is already known, to avoid having to re-create a title object later. + * @param object $row Result set row, must contain the cat_xxx fields. If the + * fields are null, the resulting Category object will represent an empty + * category if a title object was given. If the fields are null and no + * title was given, this method fails and returns false. + * @param Title $title Optional title object for the category represented by + * the given row. May be provided if it is already known, to avoid having + * to re-create a title object later. * @return Category */ public static function newFromRow( $row, $title = null ) { @@ -177,7 +180,8 @@ class Category { # but we can't know that here... return false; } else { - $cat->mName = $title->getDBkey(); # if we have a title object, fetch the category name from there + # if we have a title object, fetch the category name from there + $cat->mName = $title->getDBkey(); } $cat->mID = false; @@ -195,27 +199,37 @@ class Category { return $cat; } - /** @return mixed DB key name, or false on failure */ + /** + * @return mixed DB key name, or false on failure + */ public function getName() { return $this->getX( 'mName' ); } - /** @return mixed Category ID, or false on failure */ + /** + * @return mixed Category ID, or false on failure + */ public function getID() { return $this->getX( 'mID' ); } - /** @return mixed Total number of member pages, or false on failure */ + /** + * @return mixed Total number of member pages, or false on failure + */ public function getPageCount() { return $this->getX( 'mPages' ); } - /** @return mixed Number of subcategories, or false on failure */ + /** + * @return mixed Number of subcategories, or false on failure + */ public function getSubcatCount() { return $this->getX( 'mSubcats' ); } - /** @return mixed Number of member files, or false on failure */ + /** + * @return mixed Number of member files, or false on failure + */ public function getFileCount() { return $this->getX( 'mFiles' ); } @@ -239,9 +253,9 @@ class Category { /** * Fetch a TitleArray of up to $limit category members, beginning after the * category sort key $offset. - * @param $limit integer - * @param $offset string - * @return TitleArray object for category members. + * @param int $limit + * @param string $offset + * @return TitleArray TitleArray object for category members. */ public function getMembers( $limit = false, $offset = '' ) { wfProfileIn( __METHOD__ ); @@ -277,6 +291,7 @@ class Category { /** * Generic accessor + * @param string $key * @return bool */ private function getX( $key ) { @@ -306,7 +321,7 @@ class Category { wfProfileIn( __METHOD__ ); $dbw = wfGetDB( DB_MASTER ); - $dbw->begin( __METHOD__ ); + $dbw->startAtomic( __METHOD__ ); # Insert the row if it doesn't exist yet (e.g., this is being run via # update.php from a pre-1.16 schema). TODO: This will cause lots and @@ -346,7 +361,7 @@ class Category { array( 'cat_title' => $this->mName ), __METHOD__ ); - $dbw->commit( __METHOD__ ); + $dbw->endAtomic( __METHOD__ ); wfProfileOut( __METHOD__ ); diff --git a/includes/CategoryFinder.php b/includes/CategoryFinder.php new file mode 100644 index 00000000..cf537e15 --- /dev/null +++ b/includes/CategoryFinder.php @@ -0,0 +1,244 @@ +<?php +/** + * Recent changes filtering by category. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * The "CategoryFinder" class takes a list of articles, creates an internal + * representation of all their parent categories (as well as parents of + * parents etc.). From this representation, it determines which of these + * articles are in one or all of a given subset of categories. + * + * Example use : + * <code> + * # Determines whether the article with the page_id 12345 is in both + * # "Category 1" and "Category 2" or their subcategories, respectively + * + * $cf = new CategoryFinder; + * $cf->seed( + * array( 12345 ), + * array( 'Category 1', 'Category 2' ), + * 'AND' + * ); + * $a = $cf->run(); + * print implode( ',' , $a ); + * </code> + * + */ +class CategoryFinder { + /** @var int[] The original article IDs passed to the seed function */ + protected $articles = array(); + + /** @var array Array of DBKEY category names for categories that don't have a page */ + protected $deadend = array(); + + /** @var array Array of [ID => array()] */ + protected $parents = array(); + + /** @var array Array of article/category IDs */ + protected $next = array(); + + /** @var array Array of DBKEY category names */ + protected $targets = array(); + + /** @var array */ + protected $name2id = array(); + + /** @var string "AND" or "OR" */ + protected $mode; + + /** @var DatabaseBase Read-DB slave */ + protected $dbr; + + /** + * Initializes the instance. Do this prior to calling run(). + * @param array $articleIds Array of article IDs + * @param array $categories FIXME + * @param string $mode FIXME, default 'AND'. + * @todo FIXME: $categories/$mode + */ + public function seed( $articleIds, $categories, $mode = 'AND' ) { + $this->articles = $articleIds; + $this->next = $articleIds; + $this->mode = $mode; + + # Set the list of target categories; convert them to DBKEY form first + $this->targets = array(); + foreach ( $categories as $c ) { + $ct = Title::makeTitleSafe( NS_CATEGORY, $c ); + if ( $ct ) { + $c = $ct->getDBkey(); + $this->targets[$c] = $c; + } + } + } + + /** + * Iterates through the parent tree starting with the seed values, + * then checks the articles if they match the conditions + * @return array Array of page_ids (those given to seed() that match the conditions) + */ + public function run() { + $this->dbr = wfGetDB( DB_SLAVE ); + while ( count( $this->next ) > 0 ) { + $this->scanNextLayer(); + } + + # Now check if this applies to the individual articles + $ret = array(); + + foreach ( $this->articles as $article ) { + $conds = $this->targets; + if ( $this->check( $article, $conds ) ) { + # Matches the conditions + $ret[] = $article; + } + } + return $ret; + } + + /** + * Get the parents. Only really useful if run() has been called already + * @return array + */ + public function getParents() { + return $this->parents; + } + + /** + * This functions recurses through the parent representation, trying to match the conditions + * @param int $id The article/category to check + * @param array $conds The array of categories to match + * @param array $path Used to check for recursion loops + * @return bool Does this match the conditions? + */ + private function check( $id, &$conds, $path = array() ) { + // Check for loops and stop! + if ( in_array( $id, $path ) ) { + return false; + } + + $path[] = $id; + + # Shortcut (runtime paranoia): No conditions=all matched + if ( count( $conds ) == 0 ) { + return true; + } + + if ( !isset( $this->parents[$id] ) ) { + return false; + } + + # iterate through the parents + foreach ( $this->parents[$id] as $p ) { + $pname = $p->cl_to; + + # Is this a condition? + if ( isset( $conds[$pname] ) ) { + # This key is in the category list! + if ( $this->mode == 'OR' ) { + # One found, that's enough! + $conds = array(); + return true; + } else { + # Assuming "AND" as default + unset( $conds[$pname] ); + if ( count( $conds ) == 0 ) { + # All conditions met, done + return true; + } + } + } + + # Not done yet, try sub-parents + if ( !isset( $this->name2id[$pname] ) ) { + # No sub-parent + continue; + } + $done = $this->check( $this->name2id[$pname], $conds, $path ); + if ( $done || count( $conds ) == 0 ) { + # Subparents have done it! + return true; + } + } + return false; + } + + /** + * Scans a "parent layer" of the articles/categories in $this->next + */ + private function scanNextLayer() { + $profiler = new ProfileSection( __METHOD__ ); + + # Find all parents of the article currently in $this->next + $layer = array(); + $res = $this->dbr->select( + /* FROM */ 'categorylinks', + /* SELECT */ '*', + /* WHERE */ array( 'cl_from' => $this->next ), + __METHOD__ . '-1' + ); + foreach ( $res as $o ) { + $k = $o->cl_to; + + # Update parent tree + if ( !isset( $this->parents[$o->cl_from] ) ) { + $this->parents[$o->cl_from] = array(); + } + $this->parents[$o->cl_from][$k] = $o; + + # Ignore those we already have + if ( in_array( $k, $this->deadend ) ) { + continue; + } + + if ( isset( $this->name2id[$k] ) ) { + continue; + } + + # Hey, new category! + $layer[$k] = $k; + } + + $this->next = array(); + + # Find the IDs of all category pages in $layer, if they exist + if ( count( $layer ) > 0 ) { + $res = $this->dbr->select( + /* FROM */ 'page', + /* SELECT */ array( 'page_id', 'page_title' ), + /* WHERE */ array( 'page_namespace' => NS_CATEGORY, 'page_title' => $layer ), + __METHOD__ . '-2' + ); + foreach ( $res as $o ) { + $id = $o->page_id; + $name = $o->page_title; + $this->name2id[$name] = $id; + $this->next[] = $id; + unset( $layer[$name] ); + } + } + + # Mark dead ends + foreach ( $layer as $v ) { + $this->deadend[$v] = $v; + } + } +} diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php deleted file mode 100644 index ba71aa01..00000000 --- a/includes/CategoryPage.php +++ /dev/null @@ -1,118 +0,0 @@ -<?php -/** - * Special handling for category description pages. - * Modelled after ImagePage.php. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Special handling for category description pages, showing pages, - * subcategories and file that belong to the category - */ -class CategoryPage extends Article { - # Subclasses can change this to override the viewer class. - protected $mCategoryViewerClass = 'CategoryViewer'; - - /** - * @param $title Title - * @return WikiCategoryPage - */ - protected function newPage( Title $title ) { - // Overload mPage with a category-specific page - return new WikiCategoryPage( $title ); - } - - /** - * Constructor from a page id - * @param int $id article ID to load - * @return CategoryPage|null - */ - public static function newFromID( $id ) { - $t = Title::newFromID( $id ); - # @todo FIXME: Doesn't inherit right - return $t == null ? null : new self( $t ); - # return $t == null ? null : new static( $t ); // PHP 5.3 - } - - function view() { - $request = $this->getContext()->getRequest(); - $diff = $request->getVal( 'diff' ); - $diffOnly = $request->getBool( 'diffonly', - $this->getContext()->getUser()->getOption( 'diffonly' ) ); - - if ( $diff !== null && $diffOnly ) { - parent::view(); - return; - } - - if ( !wfRunHooks( 'CategoryPageView', array( &$this ) ) ) { - return; - } - - $title = $this->getTitle(); - if ( NS_CATEGORY == $title->getNamespace() ) { - $this->openShowCategory(); - } - - parent::view(); - - if ( NS_CATEGORY == $title->getNamespace() ) { - $this->closeShowCategory(); - } - } - - function openShowCategory() { - # For overloading - } - - function closeShowCategory() { - // Use these as defaults for back compat --catrope - $request = $this->getContext()->getRequest(); - $oldFrom = $request->getVal( 'from' ); - $oldUntil = $request->getVal( 'until' ); - - $reqArray = $request->getValues(); - - $from = $until = array(); - foreach ( array( 'page', 'subcat', 'file' ) as $type ) { - $from[$type] = $request->getVal( "{$type}from", $oldFrom ); - $until[$type] = $request->getVal( "{$type}until", $oldUntil ); - - // Do not want old-style from/until propagating in nav links. - if ( !isset( $reqArray["{$type}from"] ) && isset( $reqArray["from"] ) ) { - $reqArray["{$type}from"] = $reqArray["from"]; - } - if ( !isset( $reqArray["{$type}to"] ) && isset( $reqArray["to"] ) ) { - $reqArray["{$type}to"] = $reqArray["to"]; - } - } - - unset( $reqArray["from"] ); - unset( $reqArray["to"] ); - - $viewer = new $this->mCategoryViewerClass( - $this->getContext()->getTitle(), - $this->getContext(), - $from, - $until, - $reqArray - ); - $this->getContext()->getOutput()->addHTML( $viewer->getHTML() ); - } -} diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php index 55d9c1e5..7581ae40 100644 --- a/includes/CategoryViewer.php +++ b/includes/CategoryViewer.php @@ -21,68 +21,77 @@ */ class CategoryViewer extends ContextSource { - var $limit, $from, $until, - $articles, $articles_start_char, - $children, $children_start_char, - $showGallery, $imgsNoGalley, - $imgsNoGallery_start_char, - $imgsNoGallery; + /** @var int */ + public $limit; - /** - * @var Array - */ - var $nextPage; + /** @var array */ + public $from; - /** - * @var Array - */ - var $flip; + /** @var array */ + public $until; - /** - * @var Title - */ - var $title; + /** @var string[] */ + public $articles; - /** - * @var Collation - */ - var $collation; + /** @var array */ + public $articles_start_char; - /** - * @var ImageGallery - */ - var $gallery; + /** @var array */ + public $children; - /** - * Category object for this page - * @var Category - */ + /** @var array */ + public $children_start_char; + + /** @var bool */ + public $showGallery; + + /** @var array */ + public $imgsNoGallery_start_char; + + /** @var array */ + public $imgsNoGallery; + + /** @var array */ + public $nextPage; + + /** @var array */ + protected $prevPage; + + /** @var array */ + public $flip; + + /** @var Title */ + public $title; + + /** @var Collation */ + public $collation; + + /** @var ImageGallery */ + public $gallery; + + /** @var Category Category object for this page. */ private $cat; - /** - * The original query array, to be used in generating paging links. - * @var array - */ + /** @var array The original query array, to be used in generating paging links. */ private $query; /** - * Constructor - * * @since 1.19 $context is a second, required parameter - * @param $title Title - * @param $context IContextSource + * @param Title $title + * @param IContextSource $context * @param array $from An array with keys page, subcat, * and file for offset of results of each section (since 1.17) * @param array $until An array with 3 keys for until of each section (since 1.17) - * @param $query Array + * @param array $query */ - function __construct( $title, IContextSource $context, $from = array(), $until = array(), $query = array() ) { - global $wgCategoryPagingLimit; + function __construct( $title, IContextSource $context, $from = array(), + $until = array(), $query = array() + ) { $this->title = $title; $this->setContext( $context ); $this->from = $from; $this->until = $until; - $this->limit = $wgCategoryPagingLimit; + $this->limit = $context->getConfig()->get( 'CategoryPagingLimit' ); $this->cat = Category::newFromTitle( $title ); $this->query = $query; $this->collation = Collation::singleton(); @@ -95,10 +104,10 @@ class CategoryViewer extends ContextSource { * @return string HTML output */ public function getHTML() { - global $wgCategoryMagicGallery; wfProfileIn( __METHOD__ ); - $this->showGallery = $wgCategoryMagicGallery && !$this->getOutput()->mNoGallery; + $this->showGallery = $this->getConfig()->get( 'CategoryMagicGallery' ) + && !$this->getOutput()->mNoGallery; $this->clearCategoryState(); $this->doCategoryQuery(); @@ -144,14 +153,13 @@ class CategoryViewer extends ContextSource { // Note that null for mode is taken to mean use default. $mode = $this->getRequest()->getVal( 'gallerymode', null ); try { - $this->gallery = ImageGalleryBase::factory( $mode ); + $this->gallery = ImageGalleryBase::factory( $mode, $this->getContext() ); } catch ( MWException $e ) { // User specified something invalid, fallback to default. - $this->gallery = ImageGalleryBase::factory(); + $this->gallery = ImageGalleryBase::factory( false, $this->getContext() ); } $this->gallery->setHideBadImages(); - $this->gallery->setContext( $this->getContext() ); } else { $this->imgsNoGallery = array(); $this->imgsNoGallery_start_char = array(); @@ -160,9 +168,9 @@ class CategoryViewer extends ContextSource { /** * Add a subcategory to the internal lists, using a Category object - * @param $cat Category - * @param $sortkey - * @param $pageLength + * @param Category $cat + * @param string $sortkey + * @param int $pageLength */ function addSubcategoryObject( Category $cat, $sortkey, $pageLength ) { // Subcategory; strip the 'Category' namespace from the link text. @@ -181,15 +189,6 @@ class CategoryViewer extends ContextSource { $this->getSubcategorySortChar( $cat->getTitle(), $sortkey ); } - /** - * Add a subcategory to the internal lists, using a title object - * @deprecated since 1.17 kept for compatibility, use addSubcategoryObject instead - */ - function addSubcategory( Title $title, $sortkey, $pageLength ) { - wfDeprecated( __METHOD__, '1.17' ); - $this->addSubcategoryObject( Category::newFromTitle( $title ), $sortkey, $pageLength ); - } - /** * Get the character to be used for sorting subcategories. * If there's a link from Category:A to Category:B, the sortkey of the resulting @@ -217,10 +216,10 @@ class CategoryViewer extends ContextSource { /** * Add a page in the image namespace - * @param $title Title - * @param $sortkey - * @param $pageLength - * @param $isRedirect bool + * @param Title $title + * @param string $sortkey + * @param int $pageLength + * @param bool $isRedirect */ function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) { global $wgContLang; @@ -247,10 +246,10 @@ class CategoryViewer extends ContextSource { /** * Add a miscellaneous page - * @param $title - * @param $sortkey - * @param $pageLength - * @param $isRedirect bool + * @param Title $title + * @param string $sortkey + * @param int $pageLength + * @param bool $isRedirect */ function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) { global $wgContLang; @@ -290,6 +289,12 @@ class CategoryViewer extends ContextSource { 'subcat' => null, 'file' => null, ); + $this->prevPage = array( + 'page' => null, + 'subcat' => null, + 'file' => null, + ); + $this->flip = array( 'page' => false, 'subcat' => false, 'file' => false ); foreach ( array( 'page', 'subcat', 'file' ) as $type ) { @@ -346,6 +351,9 @@ class CategoryViewer extends ContextSource { $this->nextPage[$type] = $humanSortkey; break; } + if ( $count == $this->limit ) { + $this->prevPage[$type] = $humanSortkey; + } if ( $title->getNamespace() == NS_CATEGORY ) { $cat = Category::newFromRow( $row, $title ); @@ -432,7 +440,12 @@ class CategoryViewer extends ContextSource { $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' ); $r .= "<div id=\"mw-category-media\">\n"; - $r .= '<h2>' . $this->msg( 'category-media-header', wfEscapeWikiText( $this->title->getText() ) )->text() . "</h2>\n"; + $r .= '<h2>' . + $this->msg( + 'category-media-header', + wfEscapeWikiText( $this->title->getText() ) + )->text() . + "</h2>\n"; $r .= $countmsg; $r .= $this->getSectionPagingLinks( 'file' ); if ( $this->showGallery ) { @@ -451,12 +464,24 @@ class CategoryViewer extends ContextSource { * of the output. * * @param string $type 'page', 'subcat', or 'file' - * @return String: HTML output, possibly empty if there are no other pages + * @return string HTML output, possibly empty if there are no other pages */ private function getSectionPagingLinks( $type ) { if ( isset( $this->until[$type] ) && $this->until[$type] !== null ) { - return $this->pagingLinks( $this->nextPage[$type], $this->until[$type], $type ); - } elseif ( $this->nextPage[$type] !== null || ( isset( $this->from[$type] ) && $this->from[$type] !== null ) ) { + // The new value for the until parameter should be pointing to the first + // result displayed on the page which is the second last result retrieved + // from the database.The next link should have a from parameter pointing + // to the until parameter of the current page. + if ( $this->nextPage[$type] !== null ) { + return $this->pagingLinks( $this->prevPage[$type], $this->until[$type], $type ); + } else { + // If the nextPage variable is null, it means that we have reached the first page + // and therefore the previous link should be disabled. + return $this->pagingLinks( null, $this->until[$type], $type ); + } + } elseif ( $this->nextPage[$type] !== null + || ( isset( $this->from[$type] ) && $this->from[$type] !== null ) + ) { return $this->pagingLinks( $this->from[$type], $this->nextPage[$type], $type ); } else { return ''; @@ -474,10 +499,10 @@ class CategoryViewer extends ContextSource { * Format a list of articles chunked by letter, either as a * bullet list or a columnar format, depending on the length. * - * @param $articles Array - * @param $articles_start_char Array - * @param $cutoff Int - * @return String + * @param array $articles + * @param array $articles_start_char + * @param int $cutoff + * @return string * @private */ function formatList( $articles, $articles_start_char, $cutoff = 6 ) { @@ -507,9 +532,9 @@ class CategoryViewer extends ContextSource { * More distant TODO: Scrap this and use CSS columns, whenever IE finally * supports those. * - * @param $articles Array - * @param $articles_start_char Array - * @return String + * @param array $articles + * @param string[] $articles_start_char + * @return string * @private */ static function columnList( $articles, $articles_start_char ) { @@ -563,15 +588,16 @@ class CategoryViewer extends ContextSource { /** * Format a list of articles chunked by letter in a bullet list. - * @param $articles Array - * @param $articles_start_char Array - * @return String + * @param array $articles + * @param string[] $articles_start_char + * @return string * @private */ static function shortList( $articles, $articles_start_char ) { $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n"; $r .= '<ul><li>' . $articles[0] . '</li>'; - for ( $index = 1; $index < count( $articles ); $index++ ) { + $articleCount = count( $articles ); + for ( $index = 1; $index < $articleCount; $index++ ) { if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) { $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>"; } @@ -589,7 +615,7 @@ class CategoryViewer extends ContextSource { * @param string $last The 'from' parameter for the generated URL * @param string $type A prefix for parameters, 'page' or 'subcat' or * 'file' - * @return String HTML + * @return string HTML */ private function pagingLinks( $first, $last, $type = '' ) { $prevLink = $this->msg( 'prevn' )->numParams( $this->limit )->escaped(); @@ -627,8 +653,8 @@ class CategoryViewer extends ContextSource { * 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 + * @param Title $title The title (usually $this->title) + * @param string $section Which section * @throws MWException * @return Title */ @@ -660,7 +686,7 @@ class CategoryViewer extends ContextSource { * @param int $rescnt The number of items returned by our database query. * @param int $dbcnt The number of items according to the category table. * @param string $type 'subcat', 'article', or 'file' - * @return string: A message giving the number of items, to output to HTML. + * @return string A message giving the number of items, to output to HTML. */ private function getCountMessage( $rescnt, $dbcnt, $type ) { // There are three cases: @@ -701,7 +727,10 @@ class CategoryViewer extends ContextSource { // to refresh the incorrect category table entry -- which should be // quick due to the small number of entries. $totalcnt = $rescnt; - $this->cat->refreshCounts(); + $category = $this->cat; + wfGetDB( DB_MASTER )->onTransactionIdle( function () use ( $category ) { + $category->refreshCounts(); + } ); } else { // Case 3: hopeless. Don't give a total count at all. // Messages: category-subcat-count-limited, category-article-count-limited, diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php deleted file mode 100644 index 6ef224b6..00000000 --- a/includes/Categoryfinder.php +++ /dev/null @@ -1,233 +0,0 @@ -<?php -/** - * Recent changes filtering by category. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * The "Categoryfinder" class takes a list of articles, creates an internal - * representation of all their parent categories (as well as parents of - * parents etc.). From this representation, it determines which of these - * articles are in one or all of a given subset of categories. - * - * Example use : - * <code> - * # Determines whether the article with the page_id 12345 is in both - * # "Category 1" and "Category 2" or their subcategories, respectively - * - * $cf = new Categoryfinder; - * $cf->seed( - * array( 12345 ), - * array( 'Category 1', 'Category 2' ), - * 'AND' - * ); - * $a = $cf->run(); - * print implode( ',' , $a ); - * </code> - * - */ -class Categoryfinder { - var $articles = array(); # The original article IDs passed to the seed function - var $deadend = array(); # Array of DBKEY category names for categories that don't have a page - var $parents = array(); # Array of [ID => array()] - var $next = array(); # Array of article/category IDs - var $targets = array(); # Array of DBKEY category names - var $name2id = array(); - var $mode; # "AND" or "OR" - - /** - * @var DatabaseBase - */ - var $dbr; # Read-DB slave - - /** - * Constructor (currently empty). - */ - function __construct() { - } - - /** - * Initializes the instance. Do this prior to calling run(). - * @param $article_ids Array of article IDs - * @param $categories FIXME - * @param string $mode FIXME, default 'AND'. - * @todo FIXME: $categories/$mode - */ - function seed( $article_ids, $categories, $mode = 'AND' ) { - $this->articles = $article_ids; - $this->next = $article_ids; - $this->mode = $mode; - - # Set the list of target categories; convert them to DBKEY form first - $this->targets = array(); - foreach ( $categories as $c ) { - $ct = Title::makeTitleSafe( NS_CATEGORY, $c ); - if ( $ct ) { - $c = $ct->getDBkey(); - $this->targets[$c] = $c; - } - } - } - - /** - * Iterates through the parent tree starting with the seed values, - * then checks the articles if they match the conditions - * @return array of page_ids (those given to seed() that match the conditions) - */ - function run() { - $this->dbr = wfGetDB( DB_SLAVE ); - while ( count( $this->next ) > 0 ) { - $this->scan_next_layer(); - } - - # Now check if this applies to the individual articles - $ret = array(); - - foreach ( $this->articles as $article ) { - $conds = $this->targets; - if ( $this->check( $article, $conds ) ) { - # Matches the conditions - $ret[] = $article; - } - } - return $ret; - } - - /** - * This functions recurses through the parent representation, trying to match the conditions - * @param int $id The article/category to check - * @param array $conds The array of categories to match - * @param array $path used to check for recursion loops - * @return bool Does this match the conditions? - */ - function check( $id, &$conds, $path = array() ) { - // Check for loops and stop! - if ( in_array( $id, $path ) ) { - return false; - } - - $path[] = $id; - - # Shortcut (runtime paranoia): No conditions=all matched - if ( count( $conds ) == 0 ) { - return true; - } - - if ( !isset( $this->parents[$id] ) ) { - return false; - } - - # iterate through the parents - foreach ( $this->parents[$id] as $p ) { - $pname = $p->cl_to; - - # Is this a condition? - if ( isset( $conds[$pname] ) ) { - # This key is in the category list! - if ( $this->mode == 'OR' ) { - # One found, that's enough! - $conds = array(); - return true; - } else { - # Assuming "AND" as default - unset( $conds[$pname] ); - if ( count( $conds ) == 0 ) { - # All conditions met, done - return true; - } - } - } - - # Not done yet, try sub-parents - if ( !isset( $this->name2id[$pname] ) ) { - # No sub-parent - continue; - } - $done = $this->check( $this->name2id[$pname], $conds, $path ); - if ( $done || count( $conds ) == 0 ) { - # Subparents have done it! - return true; - } - } - return false; - } - - /** - * Scans a "parent layer" of the articles/categories in $this->next - */ - function scan_next_layer() { - wfProfileIn( __METHOD__ ); - - # Find all parents of the article currently in $this->next - $layer = array(); - $res = $this->dbr->select( - /* FROM */ 'categorylinks', - /* SELECT */ '*', - /* WHERE */ array( 'cl_from' => $this->next ), - __METHOD__ . '-1' - ); - foreach ( $res as $o ) { - $k = $o->cl_to; - - # Update parent tree - if ( !isset( $this->parents[$o->cl_from] ) ) { - $this->parents[$o->cl_from] = array(); - } - $this->parents[$o->cl_from][$k] = $o; - - # Ignore those we already have - if ( in_array( $k, $this->deadend ) ) { - continue; - } - - if ( isset( $this->name2id[$k] ) ) { - continue; - } - - # Hey, new category! - $layer[$k] = $k; - } - - $this->next = array(); - - # Find the IDs of all category pages in $layer, if they exist - if ( count( $layer ) > 0 ) { - $res = $this->dbr->select( - /* FROM */ 'page', - /* SELECT */ array( 'page_id', 'page_title' ), - /* WHERE */ array( 'page_namespace' => NS_CATEGORY, 'page_title' => $layer ), - __METHOD__ . '-2' - ); - foreach ( $res as $o ) { - $id = $o->page_id; - $name = $o->page_title; - $this->name2id[$name] = $id; - $this->next[] = $id; - unset( $layer[$name] ); - } - } - - # Mark dead ends - foreach ( $layer as $v ) { - $this->deadend[$v] = $v; - } - - wfProfileOut( __METHOD__ ); - } -} diff --git a/includes/Cdb.php b/includes/Cdb.php deleted file mode 100644 index 81c0afe1..00000000 --- a/includes/Cdb.php +++ /dev/null @@ -1,184 +0,0 @@ -<?php -/** - * Native CDB file reader and writer. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Read from a CDB file. - * Native and pure PHP implementations are provided. - * http://cr.yp.to/cdb.html - */ -abstract class CdbReader { - /** - * Open a file and return a subclass instance - * - * @param $fileName string - * - * @return CdbReader - */ - public static function open( $fileName ) { - if ( self::haveExtension() ) { - return new CdbReader_DBA( $fileName ); - } else { - wfDebug( "Warning: no dba extension found, using emulation.\n" ); - return new CdbReader_PHP( $fileName ); - } - } - - /** - * Returns true if the native extension is available - * - * @return bool - */ - public static function haveExtension() { - if ( !function_exists( 'dba_handlers' ) ) { - return false; - } - $handlers = dba_handlers(); - if ( !in_array( 'cdb', $handlers ) || !in_array( 'cdb_make', $handlers ) ) { - return false; - } - return true; - } - - /** - * Construct the object and open the file - */ - abstract function __construct( $fileName ); - - /** - * Close the file. Optional, you can just let the variable go out of scope. - */ - abstract function close(); - - /** - * Get a value with a given key. Only string values are supported. - * - * @param $key string - */ - abstract public function get( $key ); -} - -/** - * Write to a CDB file. - * Native and pure PHP implementations are provided. - */ -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 CdbWriter_DBA|CdbWriter_PHP - */ - public static function open( $fileName ) { - if ( CdbReader::haveExtension() ) { - return new CdbWriter_DBA( $fileName ); - } else { - wfDebug( "Warning: no dba extension found, using emulation.\n" ); - return new CdbWriter_PHP( $fileName ); - } - } - - /** - * Create the object and open the file - * - * @param $fileName string - */ - abstract function __construct( $fileName ); - - /** - * Set a key to a given value. The value will be converted to string. - * @param $key string - * @param $value string - */ - abstract public function set( $key, $value ); - - /** - * Close the writer object. You should call this function before the object - * goes out of scope, to write out the final hashtables. - */ - abstract public function close(); -} - -/** - * Reader class which uses the DBA extension - */ -class CdbReader_DBA { - var $handle; - - function __construct( $fileName ) { - $this->handle = dba_open( $fileName, 'r-', 'cdb' ); - if ( !$this->handle ) { - throw new MWException( 'Unable to open CDB file "' . $fileName . '"' ); - } - } - - function close() { - if ( isset( $this->handle ) ) { - dba_close( $this->handle ); - } - unset( $this->handle ); - } - - function get( $key ) { - return dba_fetch( $key, $this->handle ); - } -} - -/** - * Writer class which uses the DBA extension - */ -class CdbWriter_DBA { - var $handle, $realFileName, $tmpFileName; - - function __construct( $fileName ) { - $this->realFileName = $fileName; - $this->tmpFileName = $fileName . '.tmp.' . mt_rand( 0, 0x7fffffff ); - $this->handle = dba_open( $this->tmpFileName, 'n', 'cdb_make' ); - if ( !$this->handle ) { - throw new MWException( 'Unable to open CDB file for write "' . $fileName . '"' ); - } - } - - function set( $key, $value ) { - return dba_insert( $key, $value, $this->handle ); - } - - function close() { - if ( isset( $this->handle ) ) { - dba_close( $this->handle ); - } - if ( wfIsWindows() ) { - unlink( $this->realFileName ); - } - if ( !rename( $this->tmpFileName, $this->realFileName ) ) { - throw new MWException( 'Unable to move the new CDB file into place.' ); - } - unset( $this->handle ); - } - - function __destruct() { - if ( isset( $this->handle ) ) { - $this->close(); - } - } -} diff --git a/includes/Cdb_PHP.php b/includes/Cdb_PHP.php deleted file mode 100644 index a38b9a86..00000000 --- a/includes/Cdb_PHP.php +++ /dev/null @@ -1,493 +0,0 @@ -<?php -/** - * This is a port of D.J. Bernstein's CDB to PHP. It's based on the copy that - * appears in PHP 5.3. Changes are: - * * Error returns replaced with exceptions - * * Exception thrown if sizes or offsets are between 2GB and 4GB - * * Some variables renamed - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Common functions for readers and writers - */ -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 ) { - $m = ( $a & 0x7fffffff ) % $b + 2 * ( 0x40000000 % $b ); - return $m % $b; - } else { - 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 ) { - return $a; - } - if ( $a & 0x80000000 ) { - return ( ( $a & 0x7fffffff ) >> $b ) | ( 0x40000000 >> ( $b - 1 ) ); - } else { - return $a >> $b; - } - } - - /** - * The CDB hash function. - * - * @param $s string - * - * @return - */ - public static function hash( $s ) { - $h = 5381; - for ( $i = 0; $i < strlen( $s ); $i++ ) { - $h5 = ( $h << 5 ) & 0xffffffff; - // Do a 32-bit sum - // Inlined here for speed - $sum = ( $h & 0x3fffffff ) + ( $h5 & 0x3fffffff ); - $h = - ( - ( $sum & 0x40000000 ? 1 : 0 ) - + ( $h & 0x80000000 ? 2 : 0 ) - + ( $h & 0x40000000 ? 1 : 0 ) - + ( $h5 & 0x80000000 ? 2 : 0 ) - + ( $h5 & 0x40000000 ? 1 : 0 ) - ) << 30 - | ( $sum & 0x3fffffff ); - $h ^= ord( $s[$i] ); - $h &= 0xffffffff; - } - return $h; - } -} - -/** - * CDB reader class - */ -class CdbReader_PHP extends CdbReader { - /** The filename */ - var $fileName; - - /** The file handle */ - var $handle; - - /* number of hash slots searched under this key */ - var $loop; - - /* initialized if loop is nonzero */ - var $khash; - - /* initialized if loop is nonzero */ - var $kpos; - - /* initialized if loop is nonzero */ - var $hpos; - - /* initialized if loop is nonzero */ - var $hslots; - - /* initialized if findNext() returns true */ - var $dpos; - - /* initialized if cdb_findnext() returns 1 */ - var $dlen; - - /** - * @param $fileName string - * @throws MWException - */ - function __construct( $fileName ) { - $this->fileName = $fileName; - $this->handle = fopen( $fileName, 'rb' ); - if ( !$this->handle ) { - throw new MWException( 'Unable to open CDB file "' . $this->fileName . '".' ); - } - $this->findStart(); - } - - function close() { - 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 ) ) ) { - return $this->read( $this->dlen, $this->dpos ); - } else { - return false; - } - } - - /** - * @param $key - * @param $pos - * @return bool - */ - protected function match( $key, $pos ) { - $buf = $this->read( strlen( $key ), $pos ); - return $buf === $key; - } - - protected function findStart() { - $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 - throw new MWException( - 'Seek failed, file "' . $this->fileName . '" may be corrupted.' ); - } - - if ( $length == 0 ) { - return ''; - } - - $buf = fread( $this->handle, $length ); - if ( $buf === false || strlen( $buf ) !== $length ) { - throw new MWException( - 'Read from CDB file failed, file "' . $this->fileName . '" may be corrupted.' ); - } - return $buf; - } - - /** - * Unpack an unsigned integer and throw an exception if it needs more than 31 bits - * @param $s - * @throws MWException - * @return mixed - */ - protected function unpack31( $s ) { - $data = unpack( 'V', $s ); - if ( $data[1] > 0x7fffffff ) { - throw new MWException( - 'Error in CDB file "' . $this->fileName . '", integer too big.' ); - } - return $data[1]; - } - - /** - * 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 ); - $buf = $this->read( 8, ( $u << 3 ) & 2047 ); - $this->hslots = $this->unpack31( substr( $buf, 4 ) ); - if ( !$this->hslots ) { - return false; - } - $this->hpos = $this->unpack31( substr( $buf, 0, 4 ) ); - $this->khash = $u; - $u = CdbFunctions::unsignedShiftRight( $u, 8 ); - $u = CdbFunctions::unsignedMod( $u, $this->hslots ); - $u <<= 3; - $this->kpos = $this->hpos + $u; - } - - while ( $this->loop < $this->hslots ) { - $buf = $this->read( 8, $this->kpos ); - $pos = $this->unpack31( substr( $buf, 4 ) ); - if ( !$pos ) { - return false; - } - $this->loop += 1; - $this->kpos += 8; - if ( $this->kpos == $this->hpos + ( $this->hslots << 3 ) ) { - $this->kpos = $this->hpos; - } - $u = $this->unpackSigned( substr( $buf, 0, 4 ) ); - if ( $u === $this->khash ) { - $buf = $this->read( 8, $pos ); - $keyLen = $this->unpack31( substr( $buf, 0, 4 ) ); - if ( $keyLen == strlen( $key ) && $this->match( $key, $pos + 8 ) ) { - // Found - $this->dlen = $this->unpack31( substr( $buf, 4 ) ); - $this->dpos = $pos + 8 + $keyLen; - return true; - } - } - } - return false; - } - - /** - * @param $key - * @return bool - */ - protected function find( $key ) { - $this->findStart(); - return $this->findNext( $key ); - } -} - -/** - * CDB writer class - */ -class CdbWriter_PHP extends CdbWriter { - var $handle, $realFileName, $tmpFileName; - - var $hplist; - var $numentries, $pos; - - /** - * @param $fileName string - */ - function __construct( $fileName ) { - $this->realFileName = $fileName; - $this->tmpFileName = $fileName . '.tmp.' . mt_rand( 0, 0x7fffffff ); - $this->handle = fopen( $this->tmpFileName, 'wb' ); - if ( !$this->handle ) { - $this->throwException( - 'Unable to open CDB file "' . $this->tmpFileName . '" for write.' ); - } - $this->hplist = array(); - $this->numentries = 0; - $this->pos = 2048; // leaving space for the pointer array, 256 * 8 - if ( fseek( $this->handle, $this->pos ) == -1 ) { - $this->throwException( 'fseek failed in file "' . $this->tmpFileName . '".' ); - } - } - - function __destruct() { - if ( isset( $this->handle ) ) { - $this->close(); - } - } - - /** - * @param $key - * @param $value - * @return - */ - public function set( $key, $value ) { - if ( strval( $key ) === '' ) { - // DBA cross-check hack - return; - } - $this->addbegin( strlen( $key ), strlen( $value ) ); - $this->write( $key ); - $this->write( $value ); - $this->addend( strlen( $key ), strlen( $value ), CdbFunctions::hash( $key ) ); - } - - /** - * @throws MWException - */ - public function close() { - $this->finish(); - if ( isset( $this->handle ) ) { - fclose( $this->handle ); - } - if ( wfIsWindows() && file_exists( $this->realFileName ) ) { - unlink( $this->realFileName ); - } - if ( !rename( $this->tmpFileName, $this->realFileName ) ) { - $this->throwException( 'Unable to move the new CDB file into place.' ); - } - unset( $this->handle ); - } - - /** - * @throws MWException - * @param $buf - */ - protected function write( $buf ) { - $len = fwrite( $this->handle, $buf ); - if ( $len !== strlen( $buf ) ) { - $this->throwException( 'Error writing to CDB file "' . $this->tmpFileName . '".' ); - } - } - - /** - * @throws MWException - * @param $len - */ - protected function posplus( $len ) { - $newpos = $this->pos + $len; - if ( $newpos > 0x7fffffff ) { - $this->throwException( - 'A value in the CDB file "' . $this->tmpFileName . '" is too large.' ); - } - $this->pos = $newpos; - } - - /** - * @param $keylen - * @param $datalen - * @param $h - */ - protected function addend( $keylen, $datalen, $h ) { - $this->hplist[] = array( - 'h' => $h, - 'p' => $this->pos - ); - - $this->numentries++; - $this->posplus( 8 ); - $this->posplus( $keylen ); - $this->posplus( $datalen ); - } - - /** - * @throws MWException - * @param $keylen - * @param $datalen - */ - protected function addbegin( $keylen, $datalen ) { - if ( $keylen > 0x7fffffff ) { - $this->throwException( 'Key length too long in file "' . $this->tmpFileName . '".' ); - } - if ( $datalen > 0x7fffffff ) { - $this->throwException( 'Data length too long in file "' . $this->tmpFileName . '".' ); - } - $buf = pack( 'VV', $keylen, $datalen ); - $this->write( $buf ); - } - - /** - * @throws MWException - */ - protected function finish() { - // Hack for DBA cross-check - $this->hplist = array_reverse( $this->hplist ); - - // Calculate the number of items that will be in each hashtable - $counts = array_fill( 0, 256, 0 ); - foreach ( $this->hplist as $item ) { - ++ $counts[255 & $item['h']]; - } - - // Fill in $starts with the *end* indexes - $starts = array(); - $pos = 0; - for ( $i = 0; $i < 256; ++$i ) { - $pos += $counts[$i]; - $starts[$i] = $pos; - } - - // Excessively clever and indulgent code to simultaneously fill $packedTables - // with the packed hashtables, and adjust the elements of $starts - // to actually point to the starts instead of the ends. - $packedTables = array_fill( 0, $this->numentries, false ); - foreach ( $this->hplist as $item ) { - $packedTables[--$starts[255 & $item['h']]] = $item; - } - - $final = ''; - for ( $i = 0; $i < 256; ++$i ) { - $count = $counts[$i]; - - // The size of the hashtable will be double the item count. - // The rest of the slots will be empty. - $len = $count + $count; - $final .= pack( 'VV', $this->pos, $len ); - - $hashtable = array(); - for ( $u = 0; $u < $len; ++$u ) { - $hashtable[$u] = array( 'h' => 0, 'p' => 0 ); - } - - // Fill the hashtable, using the next empty slot if the hashed slot - // is taken. - for ( $u = 0; $u < $count; ++$u ) { - $hp = $packedTables[$starts[$i] + $u]; - $where = CdbFunctions::unsignedMod( - CdbFunctions::unsignedShiftRight( $hp['h'], 8 ), $len ); - while ( $hashtable[$where]['p'] ) { - if ( ++$where == $len ) { - $where = 0; - } - } - $hashtable[$where] = $hp; - } - - // Write the hashtable - for ( $u = 0; $u < $len; ++$u ) { - $buf = pack( 'vvV', - $hashtable[$u]['h'] & 0xffff, - CdbFunctions::unsignedShiftRight( $hashtable[$u]['h'], 16 ), - $hashtable[$u]['p'] ); - $this->write( $buf ); - $this->posplus( 8 ); - } - } - - // Write the pointer array at the start of the file - rewind( $this->handle ); - if ( ftell( $this->handle ) != 0 ) { - $this->throwException( 'Error rewinding to start of file "' . $this->tmpFileName . '".' ); - } - $this->write( $final ); - } - - /** - * Clean up the temp file and throw an exception - * - * @param $msg string - * @throws MWException - */ - protected function throwException( $msg ) { - if ( $this->handle ) { - fclose( $this->handle ); - unlink( $this->tmpFileName ); - } - throw new MWException( $msg ); - } -} diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php index 3fc27f9a..94b7b7a9 100644 --- a/includes/ChangeTags.php +++ b/includes/ChangeTags.php @@ -21,18 +21,15 @@ */ class ChangeTags { - /** * Creates HTML for the given tags * * @param string $tags Comma-separated list of tags * @param string $page A label for the type of action which is being displayed, - * for example: 'history', 'contributions' or 'newpages' - * - * @return Array with two items: (html, classes) - * - html: String: HTML for displaying the tags (empty string when param $tags is empty) - * - classes: Array of strings: CSS classes used in the generated html, one class for each tag - * + * for example: 'history', 'contributions' or 'newpages' + * @return array Array with two items: (html, classes) + * - html: String: HTML for displaying the tags (empty string when param $tags is empty) + * - classes: Array of strings: CSS classes used in the generated html, one class for each tag */ public static function formatSummaryRow( $tags, $page ) { global $wgLang; @@ -66,10 +63,10 @@ class ChangeTags { /** * Get a short description for a tag * - * @param string $tag tag + * @param string $tag Tag * - * @return String: Short description of the tag from "mediawiki:tag-$tag" if this message exists, - * html-escaped version of $tag otherwise + * @return string Short description of the tag from "mediawiki:tag-$tag" if this message exists, + * html-escaped version of $tag otherwise */ public static function tagDescription( $tag ) { $msg = wfMessage( "tag-$tag" ); @@ -80,17 +77,19 @@ class ChangeTags { * Add tags to a change given its rc_id, rev_id and/or log_id * * @param string|array $tags Tags to add to the change - * @param $rc_id int: rc_id of the change to add the tags to - * @param $rev_id int: rev_id of the change to add the tags to - * @param $log_id int: log_id of the change to add the tags to - * @param string $params params to put in the ct_params field of table 'change_tag' + * @param int|null $rc_id The rc_id of the change to add the tags to + * @param int|null $rev_id The rev_id of the change to add the tags to + * @param int|null $log_id The log_id of the change to add the tags to + * @param string $params Params to put in the ct_params field of table 'change_tag' * * @throws MWException - * @return bool: false if no changes are made, otherwise true + * @return bool False if no changes are made, otherwise true * - * @exception MWException when $rc_id, $rev_id and $log_id are all null + * @exception MWException When $rc_id, $rev_id and $log_id are all null */ - public static function addTags( $tags, $rc_id = null, $rev_id = null, $log_id = null, $params = null ) { + public static function addTags( $tags, $rc_id = null, $rev_id = null, + $log_id = null, $params = null + ) { if ( !is_array( $tags ) ) { $tags = array( $tags ); } @@ -102,26 +101,52 @@ class ChangeTags { 'specified when adding a tag to a change!' ); } - $dbr = wfGetDB( DB_SLAVE ); + $dbw = wfGetDB( DB_MASTER ); // Might as well look for rcids and so on. if ( !$rc_id ) { - $dbr = wfGetDB( DB_MASTER ); // Info might be out of date, somewhat fractionally, on slave. + // Info might be out of date, somewhat fractionally, on slave. if ( $log_id ) { - $rc_id = $dbr->selectField( 'recentchanges', 'rc_id', array( 'rc_logid' => $log_id ), __METHOD__ ); + $rc_id = $dbw->selectField( + 'recentchanges', + 'rc_id', + array( 'rc_logid' => $log_id ), + __METHOD__ + ); } elseif ( $rev_id ) { - $rc_id = $dbr->selectField( 'recentchanges', 'rc_id', array( 'rc_this_oldid' => $rev_id ), __METHOD__ ); + $rc_id = $dbw->selectField( + 'recentchanges', + 'rc_id', + array( 'rc_this_oldid' => $rev_id ), + __METHOD__ + ); } } elseif ( !$log_id && !$rev_id ) { - $dbr = wfGetDB( DB_MASTER ); // Info might be out of date, somewhat fractionally, on slave. - $log_id = $dbr->selectField( 'recentchanges', 'rc_logid', array( 'rc_id' => $rc_id ), __METHOD__ ); - $rev_id = $dbr->selectField( 'recentchanges', 'rc_this_oldid', array( 'rc_id' => $rc_id ), __METHOD__ ); + // Info might be out of date, somewhat fractionally, on slave. + $log_id = $dbw->selectField( + 'recentchanges', + 'rc_logid', + array( 'rc_id' => $rc_id ), + __METHOD__ + ); + $rev_id = $dbw->selectField( + 'recentchanges', + 'rc_this_oldid', + array( 'rc_id' => $rc_id ), + __METHOD__ + ); } - $tsConds = array_filter( array( 'ts_rc_id' => $rc_id, 'ts_rev_id' => $rev_id, 'ts_log_id' => $log_id ) ); + $tsConds = array_filter( array( + 'ts_rc_id' => $rc_id, + 'ts_rev_id' => $rev_id, + 'ts_log_id' => $log_id ) + ); - ## Update the summary row. - $prevTags = $dbr->selectField( 'tag_summary', 'ts_tags', $tsConds, __METHOD__ ); + // Update the summary row. + // $prevTags can be out of date on slaves, especially when addTags is called consecutively, + // causing loss of tags added recently in tag_summary table. + $prevTags = $dbw->selectField( 'tag_summary', 'ts_tags', $tsConds, __METHOD__ ); $prevTags = $prevTags ? $prevTags : ''; $prevTags = array_filter( explode( ',', $prevTags ) ); $newTags = array_unique( array_merge( $prevTags, $tags ) ); @@ -133,7 +158,6 @@ class ChangeTags { return false; } - $dbw = wfGetDB( DB_MASTER ); $dbw->replace( 'tag_summary', array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ), @@ -167,9 +191,9 @@ class ChangeTags { * * @param string|array $tables Table names, see DatabaseBase::select * @param string|array $fields Fields used in query, see DatabaseBase::select - * @param string|array $conds conditions used in query, see DatabaseBase::select - * @param $join_conds Array: join conditions, see DatabaseBase::select - * @param array $options options, see Database::select + * @param string|array $conds Conditions used in query, see DatabaseBase::select + * @param array $join_conds Join conditions, see DatabaseBase::select + * @param array $options Options, see Database::select * @param bool|string $filter_tag Tag to select on * * @throws MWException When unable to determine appropriate JOIN condition for tagging @@ -184,29 +208,27 @@ class ChangeTags { // Figure out which conditions can be done. if ( in_array( 'recentchanges', $tables ) ) { - $join_cond = 'rc_id'; + $join_cond = 'ct_rc_id=rc_id'; } elseif ( in_array( 'logging', $tables ) ) { - $join_cond = 'log_id'; + $join_cond = 'ct_log_id=log_id'; } elseif ( in_array( 'revision', $tables ) ) { - $join_cond = 'rev_id'; + $join_cond = 'ct_rev_id=rev_id'; + } elseif ( in_array( 'archive', $tables ) ) { + $join_cond = 'ct_rev_id=ar_rev_id'; } else { throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' ); } - // JOIN on tag_summary - $tables[] = 'tag_summary'; - $join_conds['tag_summary'] = array( 'LEFT JOIN', "ts_$join_cond=$join_cond" ); - $fields[] = 'ts_tags'; + $fields['ts_tags'] = wfGetDB( DB_SLAVE )->buildGroupConcatField( + ',', 'change_tag', 'ct_tag', $join_cond + ); if ( $wgUseTagFilter && $filter_tag ) { // Somebody wants to filter on a tag. // Add an INNER JOIN on change_tag - // FORCE INDEX -- change_tags will almost ALWAYS be the correct query plan. - $options['USE INDEX'] = array( 'change_tag' => 'change_tag_tag_id' ); - unset( $options['FORCE INDEX'] ); $tables[] = 'change_tag'; - $join_conds['change_tag'] = array( 'INNER JOIN', "ct_$join_cond=$join_cond" ); + $join_conds['change_tag'] = array( 'INNER JOIN', $join_cond ); $conds['ct_tag'] = $filter_tag; } } @@ -214,34 +236,55 @@ class ChangeTags { /** * Build a text box to select a change tag * - * @param string $selected tag to select by default - * @param $fullForm Boolean: + * @param string $selected Tag to select by default + * @param bool $fullForm * - 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. + * @param Title $title Title object to send the form to. * Used when, and only when $fullForm is true. - * @return String or array: + * @return string|array * - if $fullForm is false: Array with * - if $fullForm is true: String, html fragment */ - public static function buildTagFilterSelector( $selected = '', $fullForm = false, Title $title = null ) { + public static function buildTagFilterSelector( $selected = '', + $fullForm = false, Title $title = null + ) { global $wgUseTagFilter; if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) { return $fullForm ? '' : array(); } - $data = array( Html::rawElement( 'label', array( 'for' => 'tagfilter' ), wfMessage( 'tag-filter' )->parse() ), - Xml::input( 'tagfilter', 20, $selected, array( 'class' => 'mw-tagfilter-input' ) ) ); + $data = array( + Html::rawElement( + 'label', + array( 'for' => 'tagfilter' ), + wfMessage( 'tag-filter' )->parse() + ), + Xml::input( + 'tagfilter', + 20, + $selected, + array( 'class' => 'mw-tagfilter-input', 'id' => 'tagfilter' ) + ) + ); if ( !$fullForm ) { return $data; } $html = implode( ' ', $data ); - $html .= "\n" . Xml::element( 'input', array( 'type' => 'submit', 'value' => wfMessage( 'tag-filter-submit' )->text() ) ); + $html .= "\n" . + Xml::element( + 'input', + array( 'type' => 'submit', 'value' => wfMessage( 'tag-filter-submit' )->text() ) + ); $html .= "\n" . Html::hidden( 'title', $title->getPrefixedText() ); - $html = Xml::tags( 'form', array( 'action' => $title->getLocalURL(), 'class' => 'mw-tagfilter-form', 'method' => 'get' ), $html ); + $html = Xml::tags( + 'form', + array( 'action' => $title->getLocalURL(), 'class' => 'mw-tagfilter-form', 'method' => 'get' ), + $html + ); return $html; } @@ -253,7 +296,7 @@ class ChangeTags { * * Tries memcached first. * - * @return Array of strings: tags + * @return string[] Array of strings: tags */ public static function listDefinedTags() { // Caching... diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php deleted file mode 100644 index 0736c507..00000000 --- a/includes/ChangesFeed.php +++ /dev/null @@ -1,221 +0,0 @@ -<?php -/** - * Feed for list of changes. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Feed to Special:RecentChanges and Special:RecentChangesLiked - * - * @ingroup Feed - */ -class ChangesFeed { - public $format, $type, $titleMsg, $descMsg; - - /** - * Constructor - * - * @param string $format feed's format (either 'rss' or 'atom') - * @param string $type type of feed (for cache keys) - */ - public function __construct( $format, $type ) { - $this->format = $format; - $this->type = $type; - } - - /** - * Get a ChannelFeed subclass object to use - * - * @param string $title feed's title - * @param string $description feed's description - * @param string $url url of origin page - * @return ChannelFeed subclass or false on failure - */ - public function getFeedObject( $title, $description, $url ) { - global $wgSitename, $wgLanguageCode, $wgFeedClasses; - - if ( !isset( $wgFeedClasses[$this->format] ) ) { - return false; - } - - if ( !array_key_exists( $this->format, $wgFeedClasses ) ) { - // falling back to atom - $this->format = 'atom'; - } - - $feedTitle = "$wgSitename - {$title} [$wgLanguageCode]"; - return new $wgFeedClasses[$this->format]( - $feedTitle, htmlspecialchars( $description ), $url ); - } - - /** - * Generates feed's content - * - * @param $feed ChannelFeed subclass object (generally the one returned by getFeedObject()) - * @param $rows ResultWrapper object with rows in recentchanges table - * @param $lastmod Integer: timestamp of the last item in the recentchanges table (only used for the cache key) - * @param $opts FormOptions as in SpecialRecentChanges::getDefaultOptions() - * @return null|bool True or null - */ - public function execute( $feed, $rows, $lastmod, $opts ) { - global $wgLang, $wgRenderHashAppend; - - if ( !FeedUtils::checkFeedOutput( $this->format ) ) { - return null; - } - - $optionsHash = md5( serialize( $opts->getAllValues() ) ) . $wgRenderHashAppend; - $timekey = wfMemcKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash, 'timestamp' ); - $key = wfMemcKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash ); - - 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. - */ - $cachedFeed = $this->loadFromCache( $lastmod, $timekey, $key ); - if ( is_string( $cachedFeed ) ) { - wfDebug( "RC: Outputting cached feed\n" ); - $feed->httpHeaders(); - echo $cachedFeed; - } else { - wfDebug( "RC: rendering new feed and caching it\n" ); - ob_start(); - self::generateFeed( $rows, $feed ); - $cachedFeed = ob_get_contents(); - ob_end_flush(); - $this->saveToCache( $cachedFeed, $timekey, $key ); - } - return true; - } - - /** - * Save to feed result to $messageMemc - * - * @param string $feed feed's content - * @param string $timekey memcached key of the last modification - * @param string $key memcached key of the content - */ - public function saveToCache( $feed, $timekey, $key ) { - global $messageMemc; - $expire = 3600 * 24; # One day - $messageMemc->set( $key, $feed, $expire ); - $messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire ); - } - - /** - * Try to load the feed result from $messageMemc - * - * @param $lastmod Integer: timestamp of the last item in the recentchanges table - * @param string $timekey memcached key of the last modification - * @param string $key memcached key of the content - * @return string|bool feed's content on cache hit or false on cache miss - */ - public function loadFromCache( $lastmod, $timekey, $key ) { - global $wgFeedCacheTimeout, $wgOut, $messageMemc; - - $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. - */ - - $feedAge = time() - wfTimestamp( TS_UNIX, $feedLastmod ); - $feedLastmodUnix = wfTimestamp( TS_UNIX, $feedLastmod ); - $lastmodUnix = wfTimestamp( TS_UNIX, $lastmod ); - - if ( $feedAge < $wgFeedCacheTimeout || $feedLastmodUnix > $lastmodUnix ) { - wfDebug( "RC: loading feed from cache ($key; $feedLastmod; $lastmod)...\n" ); - if ( $feedLastmodUnix < $lastmodUnix ) { - $wgOut->setLastModified( $feedLastmod ); // bug 21916 - } - return $messageMemc->get( $key ); - } else { - wfDebug( "RC: cached feed timestamp check failed ($feedLastmod; $lastmod)\n" ); - } - } - return false; - } - - /** - * Generate the feed items given a row from the database. - * @param $rows DatabaseBase resource with recentchanges rows - * @param $feed Feed object - */ - public static function generateFeed( $rows, &$feed ) { - wfProfileIn( __METHOD__ ); - - $feed->outHeader(); - - # Merge adjacent edits by one user - $sorted = array(); - $n = 0; - foreach ( $rows as $obj ) { - if ( $n > 0 && - $obj->rc_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 ) { - $sorted[$n - 1]->rc_last_oldid = $obj->rc_last_oldid; - } else { - $sorted[$n] = $obj; - $n++; - } - } - - foreach ( $sorted as $obj ) { - $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title ); - $talkpage = 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( array( - '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 ), - $url, - $obj->rc_timestamp, - ( $obj->rc_deleted & Revision::DELETED_USER ) - ? wfMessage( 'rev-deleted-user' )->escaped() : $obj->rc_user_text, - $talkpage - ); - $feed->outItem( $item ); - } - $feed->outFooter(); - wfProfileOut( __METHOD__ ); - } - -} diff --git a/includes/Collation.php b/includes/Collation.php index b0252c70..1c2c2db3 100644 --- a/includes/Collation.php +++ b/includes/Collation.php @@ -21,7 +21,7 @@ */ abstract class Collation { - static $instance; + private static $instance; /** * @return Collation @@ -36,7 +36,7 @@ abstract class Collation { /** * @throws MWException - * @param $collationName string + * @param string $collationName * @return Collation */ static function factory( $collationName ) { @@ -47,6 +47,10 @@ abstract class Collation { return new IdentityCollation; case 'uca-default': return new IcuCollation( 'root' ); + case 'xx-uca-ckb': + return new CollationCkb; + case 'xx-uca-et': + return new CollationEt; default: $match = array(); if ( preg_match( '/^uca-([a-z@=-]+)$/', $collationName, $match ) ) { @@ -106,7 +110,8 @@ abstract class Collation { } class UppercaseCollation extends Collation { - var $lang; + private $lang; + function __construct() { // Get a language object so that we can use the generic UTF-8 uppercase // function there @@ -149,10 +154,22 @@ class IdentityCollation extends Collation { } class IcuCollation extends Collation { - const FIRST_LETTER_VERSION = 1; + const FIRST_LETTER_VERSION = 2; + + /** @var Collator */ + private $primaryCollator; + + /** @var Collator */ + private $mainCollator; + + /** @var string */ + private $locale; - var $primaryCollator, $mainCollator, $locale; - var $firstLetterData; + /** @var Language */ + protected $digitTransformLanguage; + + /** @var array */ + private $firstLetterData; /** * Unified CJK blocks. @@ -163,7 +180,7 @@ class IcuCollation extends Collation { * is pretty useless for sorting Chinese text anyway. Japanese and Korean * blocks are not included here, because they are smaller and more useful. */ - static $cjkBlocks = array( + private static $cjkBlocks = array( array( 0x2E80, 0x2EFF ), // CJK Radicals Supplement array( 0x2F00, 0x2FDF ), // Kangxi Radicals array( 0x2FF0, 0x2FFF ), // Ideographic Description Characters @@ -202,14 +219,19 @@ class IcuCollation extends Collation { * Empty arrays are intended; this signifies that the data for the language is * available and that there are, in fact, no additional letters to consider. */ - static $tailoringFirstLetters = array( + private static $tailoringFirstLetters = array( // Verified by native speakers 'be' => array( "Ё" ), 'be-tarask' => array( "Ё" ), + 'cy' => array( "Ch", "Dd", "Ff", "Ng", "Ll", "Ph", "Rh", "Th" ), 'en' => array(), + 'fa' => array( "آ", "ء", "ه" ), 'fi' => array( "Å", "Ä", "Ö" ), + 'fr' => array(), 'hu' => array( "Cs", "Dz", "Dzs", "Gy", "Ly", "Ny", "Ö", "Sz", "Ty", "Ü", "Zs" ), + 'is' => array( "Á", "Ð", "É", "Í", "Ó", "Ú", "Ý", "Þ", "Æ", "Ö", "Å" ), 'it' => array(), + 'lv' => array( "Č", "Ģ", "Ķ", "Ļ", "Ņ", "Š", "Ž" ), 'pl' => array( "Ą", "Ć", "Ę", "Ł", "Ń", "Ó", "Ś", "Ź", "Ż" ), 'pt' => array(), 'ru' => array(), @@ -227,18 +249,15 @@ class IcuCollation extends Collation { 'ca' => array(), 'co' => array(), 'cs' => array( "Č", "Ch", "Ř", "Š", "Ž" ), - 'cy' => array( "Ch", "Dd", "Ff", "Ng", "Ll", "Ph", "Rh", "Th" ), 'da' => array( "Æ", "Ø", "Å" ), 'de' => array(), 'dsb' => array( "Č", "Ć", "Dź", "Ě", "Ch", "Ł", "Ń", "Ŕ", "Š", "Ś", "Ž", "Ź" ), 'el' => array(), 'eo' => array( "Ĉ", "Ĝ", "Ĥ", "Ĵ", "Ŝ", "Ŭ" ), 'es' => array( "Ñ" ), - 'et' => array( "Š", "Ž", "Õ", "Ä", "Ö", "Ü" ), + 'et' => array( "Š", "Ž", "Õ", "Ä", "Ö", "Ü", "W" ), // added W for CollationEt (xx-uca-et) 'eu' => array( "Ñ" ), - 'fa' => array( "آ", "ء", "ه" ), 'fo' => array( "Á", "Ð", "Í", "Ó", "Ú", "Ý", "Æ", "Ø", "Å" ), - 'fr' => array(), 'fur' => array( "À", "Á", "Â", "È", "Ì", "Ò", "Ù" ), 'fy' => array(), 'ga' => array(), @@ -246,7 +265,6 @@ class IcuCollation extends Collation { 'gl' => array( "Ch", "Ll", "Ñ" ), 'hr' => array( "Č", "Ć", "Dž", "Đ", "Lj", "Nj", "Š", "Ž" ), 'hsb' => array( "Č", "Dź", "Ě", "Ch", "Ł", "Ń", "Ř", "Š", "Ć", "Ž" ), - 'is' => array( "Á", "Ð", "É", "Í", "Ó", "Ú", "Ý", "Þ", "Æ", "Ö", "Å" ), 'kk' => array( "Ү", "І" ), 'kl' => array( "Æ", "Ø", "Å" ), 'ku' => array( "Ç", "Ê", "Î", "Ş", "Û" ), @@ -254,7 +272,6 @@ class IcuCollation extends Collation { 'la' => array(), 'lb' => array(), 'lt' => array( "Č", "Š", "Ž" ), - 'lv' => array( "Č", "Ģ", "Ķ", "Ļ", "Ņ", "Š", "Ž" ), 'mk' => array(), 'mo' => array( "Ă", "Â", "Î", "Ş", "Ţ" ), 'mt' => array( "Ċ", "Ġ", "Għ", "Ħ", "Ż" ), @@ -284,7 +301,12 @@ class IcuCollation extends Collation { throw new MWException( 'An ICU collation was requested, ' . 'but the intl extension is not available.' ); } + $this->locale = $locale; + // Drop everything after the '@' in locale's name + $localeParts = explode( '@', $locale ); + $this->digitTransformLanguage = Language::factory( $locale === 'root' ? 'en' : $localeParts[0] ); + $this->mainCollator = Collator::create( $locale ); if ( !$this->mainCollator ) { throw new MWException( "Invalid ICU locale specified for collation: $locale" ); @@ -319,16 +341,14 @@ class IcuCollation extends Collation { // Check for CJK $firstChar = mb_substr( $string, 0, 1, 'UTF-8' ); - if ( ord( $firstChar ) > 0x7f - && self::isCjk( utf8ToCodepoint( $firstChar ) ) ) - { + if ( ord( $firstChar ) > 0x7f && self::isCjk( utf8ToCodepoint( $firstChar ) ) ) { return $firstChar; } $sortKey = $this->getPrimarySortKey( $string ); // Do a binary search to find the correct letter to sort under - $min = $this->findLowerBound( + $min = ArrayUtils::findLowerBound( array( $this, 'getSortKeyByLetterIndex' ), $this->getFirstLetterCount(), 'strcmp', @@ -347,7 +367,12 @@ class IcuCollation extends Collation { } $cache = wfGetCache( CACHE_ANYTHING ); - $cacheKey = wfMemcKey( 'first-letters', $this->locale ); + $cacheKey = wfMemcKey( + 'first-letters', + $this->locale, + $this->digitTransformLanguage->getCode(), + self::getICUVersion() + ); $cacheEntry = $cache->get( $cacheKey ); if ( $cacheEntry && isset( $cacheEntry['version'] ) @@ -367,6 +392,12 @@ class IcuCollation extends Collation { if ( isset( self::$tailoringFirstLetters['-' . $this->locale] ) ) { $letters = array_diff( $letters, self::$tailoringFirstLetters['-' . $this->locale] ); } + // Apply digit transforms + $digits = array( '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ); + $letters = array_diff( $letters, $digits ); + foreach ( $digits as $digit ) { + $letters[] = $this->digitTransformLanguage->formatNum( $digit, true ); + } } else { $letters = wfGetPrecompiledData( "first-letters-{$this->locale}.ser" ); if ( $letters === false ) { @@ -459,7 +490,7 @@ class IcuCollation extends Collation { $prev = $trimmedKey; } foreach ( $duplicatePrefixes as $badKey ) { - wfDebug( "Removing '{$letterMap[$badKey]}' from first letters." ); + wfDebug( "Removing '{$letterMap[$badKey]}' from first letters.\n" ); unset( $letterMap[$badKey] ); // This code assumes that unsetting does not change sort order. } @@ -499,53 +530,6 @@ class IcuCollation extends Collation { return count( $this->firstLetterData['chars'] ); } - /** - * Do a binary search, and return the index of the largest item that sorts - * less than or equal to the target value. - * - * @param array $valueCallback A function to call to get the value with - * a given array index. - * @param int $valueCount The number of items accessible via $valueCallback, - * indexed from 0 to $valueCount - 1 - * @param array $comparisonCallback A callback to compare two values, returning - * -1, 0 or 1 in the style of strcmp(). - * @param string $target The target value to find. - * - * @return int|bool The item index of the lower bound, or false if the target value - * sorts before all items. - */ - function findLowerBound( $valueCallback, $valueCount, $comparisonCallback, $target ) { - if ( $valueCount === 0 ) { - return false; - } - - $min = 0; - $max = $valueCount; - do { - $mid = $min + ( ( $max - $min ) >> 1 ); - $item = call_user_func( $valueCallback, $mid ); - $comparison = call_user_func( $comparisonCallback, $target, $item ); - if ( $comparison > 0 ) { - $min = $mid; - } elseif ( $comparison == 0 ) { - $min = $mid; - break; - } else { - $max = $mid; - } - } while ( $min < $max - 1 ); - - if ( $min == 0 ) { - $item = call_user_func( $valueCallback, $min ); - $comparison = call_user_func( $comparisonCallback, $target, $item ); - if ( $comparison < 0 ) { - // Before the first item - return false; - } - } - return $min; - } - static function isCjk( $codepoint ) { foreach ( self::$cjkBlocks as $block ) { if ( $codepoint >= $block[0] && $codepoint <= $block[1] ) { @@ -565,7 +549,7 @@ class IcuCollation extends Collation { * This function will return false on older PHPs. * * @since 1.21 - * @return string|false + * @return string|bool */ static function getICUVersion() { return defined( 'INTL_ICU_VERSION' ) ? INTL_ICU_VERSION : false; @@ -576,7 +560,7 @@ class IcuCollation extends Collation { * currently in use, or false when it can't be determined. * * @since 1.21 - * @return string|false + * @return string|bool */ static function getUnicodeVersionForICU() { $icuVersion = IcuCollation::getICUVersion(); @@ -606,3 +590,56 @@ class IcuCollation extends Collation { } } } + +/** + * Workaround for the lack of support of Sorani Kurdish / Central Kurdish language ('ckb') in ICU. + * + * Uses the same collation rules as Persian / Farsi ('fa'), but different characters for digits. + */ +class CollationCkb extends IcuCollation { + function __construct() { + // This will set $locale and collators, which affect the actual sorting order + parent::__construct( 'fa' ); + // Override the 'fa' language set by parent constructor, which affects #getFirstLetterData() + $this->digitTransformLanguage = Language::factory( 'ckb' ); + } +} + +/** + * Workaround for incorrect collation of Estonian language ('et') in ICU (bug 54168). + * + * 'W' and 'V' should not be considered the same letter for the purposes of collation in modern + * Estonian. We work around this by replacing 'W' and 'w' with 'ᴡ' U+1D21 'LATIN LETTER SMALL + * CAPITAL W' for sortkey generation, which is collated like 'W' and is not tailored to have the + * same primary weight as 'V' in Estonian. + */ +class CollationEt extends IcuCollation { + function __construct() { + parent::__construct( 'et' ); + } + + private static function mangle( $string ) { + return str_replace( + array( 'w', 'W' ), + 'ᴡ', // U+1D21 'LATIN LETTER SMALL CAPITAL W' + $string + ); + } + + private static function unmangle( $string ) { + // Casing data is lost… + return str_replace( + 'ᴡ', // U+1D21 'LATIN LETTER SMALL CAPITAL W' + 'W', + $string + ); + } + + function getSortKey( $string ) { + return parent::getSortKey( self::mangle( $string ) ); + } + + function getFirstLetter( $string ) { + return self::unmangle( parent::getFirstLetter( self::mangle( $string ) ) ); + } +} diff --git a/includes/ConfEditor.php b/includes/ConfEditor.php deleted file mode 100644 index 67cb87db..00000000 --- a/includes/ConfEditor.php +++ /dev/null @@ -1,1109 +0,0 @@ -<?php -/** - * Configuration file editor. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * This is a state machine style parser with two internal stacks: - * * A next state stack, which determines the state the machine will progress to next - * * A path stack, which keeps track of the logical location in the file. - * - * Reference grammar: - * - * file = T_OPEN_TAG *statement - * statement = T_VARIABLE "=" expression ";" - * expression = array / scalar / T_VARIABLE - * array = T_ARRAY "(" [ element *( "," element ) [ "," ] ] ")" - * element = assoc-element / expression - * assoc-element = scalar T_DOUBLE_ARROW expression - * scalar = T_LNUMBER / T_DNUMBER / T_STRING / T_CONSTANT_ENCAPSED_STRING - */ -class ConfEditor { - /** The text to parse */ - var $text; - - /** The token array from token_get_all() */ - var $tokens; - - /** The current position in the token array */ - var $pos; - - /** The current 1-based line number */ - var $lineNum; - - /** The current 1-based column number */ - var $colNum; - - /** The current 0-based byte number */ - var $byteNum; - - /** The current ConfEditorToken object */ - var $currentToken; - - /** The previous ConfEditorToken object */ - var $prevToken; - - /** - * The state machine stack. This is an array of strings where the topmost - * element will be popped off and become the next parser state. - */ - var $stateStack; - - /** - * The path stack is a stack of associative arrays with the following elements: - * name The name of top level of the path - * level The level (number of elements) of the path - * startByte The byte offset of the start of the path - * startToken The token offset of the start - * endByte The byte offset of thee - * endToken The token offset of the end, plus one - * valueStartToken The start token offset of the value part - * valueStartByte The start byte offset of the value part - * valueEndToken The end token offset of the value part, plus one - * valueEndByte The end byte offset of the value part, plus one - * nextArrayIndex The next numeric array index at this level - * hasComma True if the array element ends with a comma - * arrowByte The byte offset of the "=>", or false if there isn't one - */ - var $pathStack; - - /** - * The elements of the top of the pathStack for every path encountered, indexed - * by slash-separated path. - */ - var $pathInfo; - - /** - * Next serial number for whitespace placeholder paths (\@extra-N) - */ - var $serial; - - /** - * Editor state. This consists of the internal copy/insert operations which - * are applied to the source string to obtain the destination string. - */ - var $edits; - - /** - * Simple entry point for command-line testing - * - * @param $text string - * - * @return string - */ - static function test( $text ) { - try { - $ce = new self( $text ); - $ce->parse(); - } catch ( ConfEditorParseError $e ) { - return $e->getMessage() . "\n" . $e->highlight( $text ); - } - return "OK"; - } - - /** - * Construct a new parser - */ - public function __construct( $text ) { - $this->text = $text; - } - - /** - * Edit the text. Returns the edited text. - * @param array $ops of operations. - * - * Operations are given as an associative array, with members: - * type: One of delete, set, append or insert (required) - * path: The path to operate on (required) - * key: The array key to insert/append, with PHP quotes - * value: The value, with PHP quotes - * - * delete - * Deletes an array element or statement with the specified path. - * e.g. - * array('type' => '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 - * 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', - * 'key' => 'baz', 'value' => "'x'" ) - * is like the PHP code: - * $foo['bar']['baz'] = 'x'; - * - * insert - * Insert a new element at the start of the array. - * - * @throws MWException - * @return string - */ - public function edit( $ops ) { - $this->parse(); - - $this->edits = array( - array( 'copy', 0, strlen( $this->text ) ) - ); - foreach ( $ops as $op ) { - $type = $op['type']; - $path = $op['path']; - $value = isset( $op['value'] ) ? $op['value'] : null; - $key = isset( $op['key'] ) ? $op['key'] : null; - - switch ( $type ) { - case 'delete': - list( $start, $end ) = $this->findDeletionRegion( $path ); - $this->replaceSourceRegion( $start, $end, false ); - break; - case 'set': - if ( isset( $this->pathInfo[$path] ) ) { - list( $start, $end ) = $this->findValueRegion( $path ); - $encValue = $value; // var_export( $value, true ); - $this->replaceSourceRegion( $start, $end, $encValue ); - break; - } - // No existing path, fall through to append - $slashPos = strrpos( $path, '/' ); - $key = var_export( substr( $path, $slashPos + 1 ), true ); - $path = substr( $path, 0, $slashPos ); - // Fall through - case 'append': - // Find the last array element - $lastEltPath = $this->findLastArrayElement( $path ); - if ( $lastEltPath === false ) { - throw new MWException( "Can't find any element of array \"$path\"" ); - } - $lastEltInfo = $this->pathInfo[$lastEltPath]; - - // Has it got a comma already? - if ( strpos( $lastEltPath, '@extra' ) === false && !$lastEltInfo['hasComma'] ) { - // No comma, insert one after the value region - list( , $end ) = $this->findValueRegion( $lastEltPath ); - $this->replaceSourceRegion( $end - 1, $end - 1, ',' ); - } - - // Make the text to insert - list( $start, $end ) = $this->findDeletionRegion( $lastEltPath ); - - if ( $key === null ) { - list( $indent, ) = $this->getIndent( $start ); - $textToInsert = "$indent$value,"; - } else { - list( $indent, $arrowIndent ) = - $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] ); - $textToInsert = "$indent$key$arrowIndent=> $value,"; - } - $textToInsert .= ( $indent === false ? ' ' : "\n" ); - - // Insert the item - $this->replaceSourceRegion( $end, $end, $textToInsert ); - break; - case 'insert': - // Find first array element - $firstEltPath = $this->findFirstArrayElement( $path ); - if ( $firstEltPath === false ) { - throw new MWException( "Can't find array element of \"$path\"" ); - } - list( $start, ) = $this->findDeletionRegion( $firstEltPath ); - $info = $this->pathInfo[$firstEltPath]; - - // Make the text to insert - if ( $key === null ) { - list( $indent, ) = $this->getIndent( $start ); - $textToInsert = "$indent$value,"; - } else { - list( $indent, $arrowIndent ) = - $this->getIndent( $start, $key, $info['arrowByte'] ); - $textToInsert = "$indent$key$arrowIndent=> $value,"; - } - $textToInsert .= ( $indent === false ? ' ' : "\n" ); - - // Insert the item - $this->replaceSourceRegion( $start, $start, $textToInsert ); - break; - default: - throw new MWException( "Unrecognised operation: \"$type\"" ); - } - } - - // Do the edits - $out = ''; - foreach ( $this->edits as $edit ) { - if ( $edit[0] == 'copy' ) { - $out .= substr( $this->text, $edit[1], $edit[2] - $edit[1] ); - } else { // if ( $edit[0] == 'insert' ) - $out .= $edit[1]; - } - } - - // Do a second parse as a sanity check - $this->text = $out; - try { - $this->parse(); - } catch ( ConfEditorParseError $e ) { - 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 ) - */ - function getVars() { - $vars = array(); - $this->parse(); - foreach ( $this->pathInfo as $path => $data ) { - if ( $path[0] != '$' ) { - continue; - } - $trimmedPath = substr( $path, 1 ); - $name = $data['name']; - if ( $name[0] == '@' ) { - continue; - } - if ( $name[0] == '$' ) { - $name = substr( $name, 1 ); - } - $parentPath = substr( $trimmedPath, 0, - strlen( $trimmedPath ) - strlen( $name ) ); - if ( substr( $parentPath, -1 ) == '/' ) { - $parentPath = substr( $parentPath, 0, -1 ); - } - - $value = substr( $this->text, $data['valueStartByte'], - $data['valueEndByte'] - $data['valueStartByte'] - ); - $this->setVar( $vars, $parentPath, $name, - $this->parseScalar( $value ) ); - } - return $vars; - } - - /** - * Set a value in an array, unless it's set already. For instance, - * setVar( $arr, 'foo/bar', 'baz', 3 ); will set - * $arr['foo']['bar']['baz'] = 3; - * @param $array array - * @param string $path slash-delimited path - * @param $key mixed Key - * @param $value mixed Value - */ - function setVar( &$array, $path, $key, $value ) { - $pathArr = explode( '/', $path ); - $target =& $array; - if ( $path !== '' ) { - foreach ( $pathArr as $p ) { - if ( !isset( $target[$p] ) ) { - $target[$p] = array(); - } - $target =& $target[$p]; - } - } - if ( !isset( $target[$key] ) ) { - $target[$key] = $value; - } - } - - /** - * Parse a scalar value in PHP - * @return mixed Parsed value - */ - function parseScalar( $str ) { - if ( $str !== '' && $str[0] == '\'' ) { - // Single-quoted string - // @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] == '"' ) { - // Double-quoted string - // @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 ) ); - } - if ( substr( $str, 0, 4 ) == 'true' ) { - return true; - } - if ( substr( $str, 0, 5 ) == 'false' ) { - return false; - } - if ( substr( $str, 0, 4 ) == 'null' ) { - return null; - } - // Must be some kind of numeric value, so let PHP's weak typing - // be useful for a change - return $str; - } - - /** - * Replace the byte offset region of the source with $newText. - * Works by adding elements to the $this->edits array. - */ - function replaceSourceRegion( $start, $end, $newText = false ) { - // Split all copy operations with a source corresponding to the region - // in question. - $newEdits = array(); - foreach ( $this->edits as $edit ) { - if ( $edit[0] !== 'copy' ) { - $newEdits[] = $edit; - continue; - } - $copyStart = $edit[1]; - $copyEnd = $edit[2]; - if ( $start >= $copyEnd || $end <= $copyStart ) { - // Outside this region - $newEdits[] = $edit; - continue; - } - if ( ( $start < $copyStart && $end > $copyStart ) - || ( $start < $copyEnd && $end > $copyEnd ) - ) { - throw new MWException( "Overlapping regions found, can't do the edit" ); - } - // Split the copy - $newEdits[] = array( 'copy', $copyStart, $start ); - if ( $newText !== false ) { - $newEdits[] = array( 'insert', $newText ); - } - $newEdits[] = array( 'copy', $end, $copyEnd ); - } - $this->edits = $newEdits; - } - - /** - * 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. - * @param $pathName - * @throws MWException - * @return array - */ - function findDeletionRegion( $pathName ) { - if ( !isset( $this->pathInfo[$pathName] ) ) { - throw new MWException( "Can't find path \"$pathName\"" ); - } - $path = $this->pathInfo[$pathName]; - // Find the start - $this->firstToken(); - while ( $this->pos != $path['startToken'] ) { - $this->nextToken(); - } - $regionStart = $path['startByte']; - for ( $offset = -1; $offset >= -$this->pos; $offset-- ) { - $token = $this->getTokenAhead( $offset ); - if ( !$token->isSkip() ) { - // If there is other content on the same line, don't move the start point - // back, because that will cause the regions to overlap. - $regionStart = $path['startByte']; - break; - } - $lfPos = strrpos( $token->text, "\n" ); - if ( $lfPos === false ) { - $regionStart -= strlen( $token->text ); - } else { - // The line start does not include the LF - $regionStart -= strlen( $token->text ) - $lfPos - 1; - break; - } - } - // Find the end - while ( $this->pos != $path['endToken'] ) { - $this->nextToken(); - } - $regionEnd = $path['endByte']; // past the end - for ( $offset = 0; $offset < count( $this->tokens ) - $this->pos; $offset++ ) { - $token = $this->getTokenAhead( $offset ); - if ( !$token->isSkip() ) { - break; - } - $lfPos = strpos( $token->text, "\n" ); - if ( $lfPos === false ) { - $regionEnd += strlen( $token->text ); - } else { - // This should point past the LF - $regionEnd += $lfPos + 1; - break; - } - } - return array( $regionStart, $regionEnd ); - } - - /** - * 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. - * @param $pathName - * @throws MWException - * @return array - */ - function findValueRegion( $pathName ) { - if ( !isset( $this->pathInfo[$pathName] ) ) { - throw new MWException( "Can't find path \"$pathName\"" ); - } - $path = $this->pathInfo[$pathName]; - if ( $path['valueStartByte'] === false || $path['valueEndByte'] === false ) { - throw new MWException( "Can't find value region for path \"$pathName\"" ); - } - return array( $path['valueStartByte'], $path['valueEndByte'] ); - } - - /** - * Find the path name of the last 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. - * @return bool|int|string - */ - function findLastArrayElement( $path ) { - // Try for a real element - $lastEltPath = false; - foreach ( $this->pathInfo as $candidatePath => $info ) { - $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); - $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); - if ( $part2 == '@' ) { - // Do nothing - } elseif ( $part1 == "$path/" ) { - $lastEltPath = $candidatePath; - } elseif ( $lastEltPath !== false ) { - break; - } - } - if ( $lastEltPath !== false ) { - return $lastEltPath; - } - - // Try for an interstitial element - $extraPath = false; - foreach ( $this->pathInfo as $candidatePath => $info ) { - $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); - if ( $part1 == "$path/" ) { - $extraPath = $candidatePath; - } elseif ( $extraPath !== false ) { - break; - } - } - 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. - * @return bool|int|string - */ - function findFirstArrayElement( $path ) { - // Try for an ordinary element - foreach ( $this->pathInfo as $candidatePath => $info ) { - $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); - $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); - if ( $part1 == "$path/" && $part2 != '@' ) { - return $candidatePath; - } - } - - // Try for an interstitial element - foreach ( $this->pathInfo as $candidatePath => $info ) { - $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); - if ( $part1 == "$path/" ) { - return $candidatePath; - } - } - return false; - } - - /** - * Get the indent string which sits after a given start position. - * Returns false if the position is not at the start of the line. - * @return array - */ - function getIndent( $pos, $key = false, $arrowPos = false ) { - $arrowIndent = ' '; - if ( $pos == 0 || $this->text[$pos - 1] == "\n" ) { - $indentLength = strspn( $this->text, " \t", $pos ); - $indent = substr( $this->text, $pos, $indentLength ); - } else { - $indent = false; - } - if ( $indent !== false && $arrowPos !== false ) { - $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key ); - if ( $arrowIndentLength > 0 ) { - $arrowIndent = str_repeat( ' ', $arrowIndentLength ); - } - } - return array( $indent, $arrowIndent ); - } - - /** - * Run the parser on the text. Throws an exception if the string does not - * match our defined subset of PHP syntax. - */ - public function parse() { - $this->initParse(); - $this->pushState( 'file' ); - $this->pushPath( '@extra-' . ( $this->serial++ ) ); - $token = $this->firstToken(); - - while ( !$token->isEnd() ) { - $state = $this->popState(); - if ( !$state ) { - $this->error( 'internal error: empty state stack' ); - } - - switch ( $state ) { - case 'file': - $this->expect( T_OPEN_TAG ); - $token = $this->skipSpace(); - if ( $token->isEnd() ) { - break 2; - } - $this->pushState( 'statement', 'file 2' ); - break; - case 'file 2': - $token = $this->skipSpace(); - if ( $token->isEnd() ) { - break 2; - } - $this->pushState( 'statement', 'file 2' ); - break; - case 'statement': - $token = $this->skipSpace(); - if ( !$this->validatePath( $token->text ) ) { - $this->error( "Invalid variable name \"{$token->text}\"" ); - } - $this->nextPath( $token->text ); - $this->expect( T_VARIABLE ); - $this->skipSpace(); - $arrayAssign = false; - if ( $this->currentToken()->type == '[' ) { - $this->nextToken(); - $token = $this->skipSpace(); - if ( !$token->isScalar() ) { - $this->error( "expected a string or number for the array key" ); - } - if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { - $text = $this->parseScalar( $token->text ); - } else { - $text = $token->text; - } - if ( !$this->validatePath( $text ) ) { - $this->error( "Invalid associative array name \"$text\"" ); - } - $this->pushPath( $text ); - $this->nextToken(); - $this->skipSpace(); - $this->expect( ']' ); - $this->skipSpace(); - $arrayAssign = true; - } - $this->expect( '=' ); - $this->skipSpace(); - $this->startPathValue(); - if ( $arrayAssign ) { - $this->pushState( 'expression', 'array assign end' ); - } else { - $this->pushState( 'expression', 'statement end' ); - } - break; - case 'array assign end': - case 'statement end': - $this->endPathValue(); - if ( $state == 'array assign end' ) { - $this->popPath(); - } - $this->skipSpace(); - $this->expect( ';' ); - $this->nextPath( '@extra-' . ( $this->serial++ ) ); - break; - case 'expression': - $token = $this->skipSpace(); - if ( $token->type == T_ARRAY ) { - $this->pushState( 'array' ); - } elseif ( $token->isScalar() ) { - $this->nextToken(); - } elseif ( $token->type == T_VARIABLE ) { - $this->nextToken(); - } else { - $this->error( "expected simple expression" ); - } - break; - case 'array': - $this->skipSpace(); - $this->expect( T_ARRAY ); - $this->skipSpace(); - $this->expect( '(' ); - $this->skipSpace(); - $this->pushPath( '@extra-' . ( $this->serial++ ) ); - if ( $this->isAhead( ')' ) ) { - // Empty array - $this->pushState( 'array end' ); - } else { - $this->pushState( 'element', 'array end' ); - } - break; - case 'array end': - $this->skipSpace(); - $this->popPath(); - $this->expect( ')' ); - break; - case 'element': - $token = $this->skipSpace(); - // Look ahead to find the double arrow - if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW, 1 ) ) { - // Found associative element - $this->pushState( 'assoc-element', 'element end' ); - } else { - // Not associative - $this->nextPath( '@next' ); - $this->startPathValue(); - $this->pushState( 'expression', 'element end' ); - } - break; - case 'element end': - $token = $this->skipSpace(); - if ( $token->type == ',' ) { - $this->endPathValue(); - $this->markComma(); - $this->nextToken(); - $this->nextPath( '@extra-' . ( $this->serial++ ) ); - // Look ahead to find ending bracket - if ( $this->isAhead( ")" ) ) { - // Found ending bracket, no continuation - $this->skipSpace(); - } else { - // No ending bracket, continue to next element - $this->pushState( 'element' ); - } - } elseif ( $token->type == ')' ) { - // End array - $this->endPathValue(); - } else { - $this->error( "expected the next array element or the end of the array" ); - } - break; - case 'assoc-element': - $token = $this->skipSpace(); - if ( !$token->isScalar() ) { - $this->error( "expected a string or number for the array key" ); - } - if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { - $text = $this->parseScalar( $token->text ); - } else { - $text = $token->text; - } - if ( !$this->validatePath( $text ) ) { - $this->error( "Invalid associative array name \"$text\"" ); - } - $this->nextPath( $text ); - $this->nextToken(); - $this->skipSpace(); - $this->markArrow(); - $this->expect( T_DOUBLE_ARROW ); - $this->skipSpace(); - $this->startPathValue(); - $this->pushState( 'expression' ); - break; - } - } - if ( count( $this->stateStack ) ) { - $this->error( 'unexpected end of file' ); - } - $this->popPath(); - } - - /** - * Initialise a parse. - */ - protected function initParse() { - $this->tokens = token_get_all( $this->text ); - $this->stateStack = array(); - $this->pathStack = array(); - $this->firstToken(); - $this->pathInfo = array(); - $this->serial = 1; - } - - /** - * 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 ) { - $this->pos = $pos; - if ( $this->pos >= count( $this->tokens ) ) { - $this->currentToken = ConfEditorToken::newEnd(); - } else { - $this->currentToken = $this->newTokenObj( $this->tokens[$this->pos] ); - } - return $this->currentToken; - } - - /** - * Create a ConfEditorToken from an element of token_get_all() - * @return ConfEditorToken - */ - function newTokenObj( $internalToken ) { - if ( is_array( $internalToken ) ) { - return new ConfEditorToken( $internalToken[0], $internalToken[1] ); - } else { - return new ConfEditorToken( $internalToken, $internalToken ); - } - } - - /** - * Reset the parse position - */ - function firstToken() { - $this->setPos( 0 ); - $this->prevToken = ConfEditorToken::newEnd(); - $this->lineNum = 1; - $this->colNum = 1; - $this->byteNum = 0; - return $this->currentToken; - } - - /** - * Get the current token - */ - function currentToken() { - return $this->currentToken; - } - - /** - * Advance the current position and return the resulting next token - */ - function nextToken() { - if ( $this->currentToken ) { - $text = $this->currentToken->text; - $lfCount = substr_count( $text, "\n" ); - if ( $lfCount ) { - $this->lineNum += $lfCount; - $this->colNum = strlen( $text ) - strrpos( $text, "\n" ); - } else { - $this->colNum += strlen( $text ); - } - $this->byteNum += strlen( $text ); - } - $this->prevToken = $this->currentToken; - $this->setPos( $this->pos + 1 ); - return $this->currentToken; - } - - /** - * Get the token $offset steps ahead of the current position. - * $offset may be negative, to get tokens behind the current position. - * @return ConfEditorToken - */ - function getTokenAhead( $offset ) { - $pos = $this->pos + $offset; - if ( $pos >= count( $this->tokens ) || $pos < 0 ) { - return ConfEditorToken::newEnd(); - } else { - return $this->newTokenObj( $this->tokens[$pos] ); - } - } - - /** - * Advances the current position past any whitespace or comments - */ - function skipSpace() { - while ( $this->currentToken && $this->currentToken->isSkip() ) { - $this->nextToken(); - } - return $this->currentToken; - } - - /** - * Throws an error if the current token is not of the given type, and - * then advances to the next position. - */ - function expect( $type ) { - if ( $this->currentToken && $this->currentToken->type == $type ) { - return $this->nextToken(); - } else { - $this->error( "expected " . $this->getTypeName( $type ) . - ", got " . $this->getTypeName( $this->currentToken->type ) ); - } - } - - /** - * Push a state or two on to the state stack. - */ - function pushState( $nextState, $stateAfterThat = null ) { - if ( $stateAfterThat !== null ) { - $this->stateStack[] = $stateAfterThat; - } - $this->stateStack[] = $nextState; - } - - /** - * Pop a state from the state stack. - * @return mixed - */ - function popState() { - return array_pop( $this->stateStack ); - } - - /** - * Returns true if the user input path is valid. - * This exists to allow "/" and "@" to be reserved for string path keys - * @return bool - */ - function validatePath( $path ) { - return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@'; - } - - /** - * Internal function to update some things at the end of a path region. Do - * not call except from popPath() or nextPath(). - */ - function endPath() { - $key = ''; - foreach ( $this->pathStack as $pathInfo ) { - if ( $key !== '' ) { - $key .= '/'; - } - $key .= $pathInfo['name']; - } - $pathInfo['endByte'] = $this->byteNum; - $pathInfo['endToken'] = $this->pos; - $this->pathInfo[$key] = $pathInfo; - } - - /** - * Go up to a new path level, for example at the start of an array. - */ - function pushPath( $path ) { - $this->pathStack[] = array( - 'name' => $path, - 'level' => count( $this->pathStack ) + 1, - 'startByte' => $this->byteNum, - 'startToken' => $this->pos, - 'valueStartToken' => false, - 'valueStartByte' => false, - 'valueEndToken' => false, - 'valueEndByte' => false, - 'nextArrayIndex' => 0, - 'hasComma' => false, - 'arrowByte' => false - ); - } - - /** - * Go down a path level, for example at the end of an array. - */ - function popPath() { - $this->endPath(); - array_pop( $this->pathStack ); - } - - /** - * 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. - */ - function nextPath( $path ) { - $this->endPath(); - $i = count( $this->pathStack ) - 1; - if ( $path == '@next' ) { - $nextArrayIndex =& $this->pathStack[$i]['nextArrayIndex']; - $this->pathStack[$i]['name'] = $nextArrayIndex; - $nextArrayIndex++; - } else { - $this->pathStack[$i]['name'] = $path; - } - $this->pathStack[$i] = - array( - 'startByte' => $this->byteNum, - 'startToken' => $this->pos, - 'valueStartToken' => false, - 'valueStartByte' => false, - 'valueEndToken' => false, - 'valueEndByte' => false, - 'hasComma' => false, - 'arrowByte' => false, - ) + $this->pathStack[$i]; - } - - /** - * Mark the start of the value part of a path. - */ - function startPathValue() { - $path =& $this->pathStack[count( $this->pathStack ) - 1]; - $path['valueStartToken'] = $this->pos; - $path['valueStartByte'] = $this->byteNum; - } - - /** - * Mark the end of the value part of a path. - */ - function endPathValue() { - $path =& $this->pathStack[count( $this->pathStack ) - 1]; - $path['valueEndToken'] = $this->pos; - $path['valueEndByte'] = $this->byteNum; - } - - /** - * Mark the comma separator in an array element - */ - function markComma() { - $path =& $this->pathStack[count( $this->pathStack ) - 1]; - $path['hasComma'] = true; - } - - /** - * Mark the arrow separator in an associative array element - */ - function markArrow() { - $path =& $this->pathStack[count( $this->pathStack ) - 1]; - $path['arrowByte'] = $this->byteNum; - } - - /** - * Generate a parse error - */ - function error( $msg ) { - throw new ConfEditorParseError( $this, $msg ); - } - - /** - * Get a readable name for the given token type. - * @return string - */ - function getTypeName( $type ) { - if ( is_int( $type ) ) { - return token_name( $type ); - } else { - return "\"$type\""; - } - } - - /** - * 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. - * @return bool - */ - function isAhead( $type, $offset = 0 ) { - $ahead = $offset; - $token = $this->getTokenAhead( $offset ); - while ( !$token->isEnd() ) { - if ( $token->isSkip() ) { - $ahead++; - $token = $this->getTokenAhead( $ahead ); - continue; - } elseif ( $token->type == $type ) { - // Found the type - return true; - } else { - // Not found - return false; - } - } - return false; - } - - /** - * Get the previous token object - */ - function prevToken() { - return $this->prevToken; - } - - /** - * Echo a reasonably readable representation of the tokenizer array. - */ - function dumpTokens() { - $out = ''; - foreach ( $this->tokens as $token ) { - $obj = $this->newTokenObj( $token ); - $out .= sprintf( "%-28s %s\n", - $this->getTypeName( $obj->type ), - addcslashes( $obj->text, "\0..\37" ) ); - } - echo "<pre>" . htmlspecialchars( $out ) . "</pre>"; - } -} - -/** - * Exception class for parse errors - */ -class ConfEditorParseError extends MWException { - var $lineNum, $colNum; - function __construct( $editor, $msg ) { - $this->lineNum = $editor->lineNum; - $this->colNum = $editor->colNum; - parent::__construct( "Parse error on line {$editor->lineNum} " . - "col {$editor->colNum}: $msg" ); - } - - function highlight( $text ) { - $lines = StringUtils::explode( "\n", $text ); - foreach ( $lines as $lineNum => $line ) { - if ( $lineNum == $this->lineNum - 1 ) { - return "$line\n" . str_repeat( ' ', $this->colNum - 1 ) . "^\n"; - } - } - return ''; - } - -} - -/** - * Class to wrap a token from the tokenizer. - */ -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 ); - - static function newEnd() { - return new self( 'END', '' ); - } - - function __construct( $type, $text ) { - $this->type = $type; - $this->text = $text; - } - - function isSkip() { - return in_array( $this->type, self::$skipTypes ); - } - - function isScalar() { - return in_array( $this->type, self::$scalarTypes ); - } - - function isEnd() { - return $this->type == 'END'; - } -} diff --git a/includes/Cookie.php b/includes/Cookie.php index ecf4667d..cb041904 100644 --- a/includes/Cookie.php +++ b/includes/Cookie.php @@ -43,8 +43,8 @@ class Cookie { * cookies. Used internally after a request to parse the * Set-Cookie headers. * - * @param string $value the value of the cookie - * @param array $attr possible key/values: + * @param string $value The value of the cookie + * @param array $attr Possible key/values: * expires A date string * path The path this cookie is used on * domain Domain this cookie is used on @@ -85,18 +85,21 @@ class Cookie { * @todo 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 string $domain the domain to validate + * @param string $domain The domain to validate * @param string $originDomain (optional) the domain the cookie originates from - * @return Boolean + * @return bool */ public static function validateCookieDomain( $domain, $originDomain = null ) { - // Don't allow a trailing dot - if ( substr( $domain, -1 ) == '.' ) { + $dc = explode( ".", $domain ); + + // Don't allow a trailing dot or addresses without a or just a leading dot + if ( substr( $domain, -1 ) == '.' || + count( $dc ) <= 1 || + count( $dc ) == 2 && $dc[0] === '' + ) { return false; } - $dc = explode( ".", $domain ); - // Only allow full, valid IP addresses if ( preg_match( '/^[0-9.]+$/', $domain ) ) { if ( count( $dc ) != 4 ) { @@ -131,8 +134,14 @@ class Cookie { } if ( substr( $domain, 0, 1 ) == '.' - && substr_compare( $originDomain, $domain, -strlen( $domain ), - strlen( $domain ), true ) != 0 ) { + && substr_compare( + $originDomain, + $domain, + -strlen( $domain ), + strlen( $domain ), + true + ) != 0 + ) { return false; } } @@ -143,9 +152,9 @@ class Cookie { /** * Serialize the cookie jar into a format useful for HTTP Request headers. * - * @param string $path the path that will be used. Required. - * @param string $domain the domain that will be used. Required. - * @return String + * @param string $path The path that will be used. Required. + * @param string $domain The domain that will be used. Required. + * @return string */ public function serializeToHttpRequest( $path, $domain ) { $ret = ''; @@ -160,15 +169,22 @@ class Cookie { } /** - * @param $domain + * @param string $domain * @return bool */ 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 ) ) { + && substr_compare( + $domain, + $this->domain, + -strlen( $this->domain ), + strlen( $this->domain ), + true + ) == 0 + ) + ) { return true; } @@ -176,7 +192,7 @@ class Cookie { } /** - * @param $path + * @param string $path * @return bool */ protected function canServePath( $path ) { @@ -197,6 +213,9 @@ class CookieJar { /** * Set a cookie in the cookie jar. Make sure only one cookie per-name exists. * @see Cookie::set() + * @param string $name + * @param string $value + * @param array $attr */ public function setCookie( $name, $value, $attr ) { /* cookies: case insensitive, so this should work. @@ -213,6 +232,8 @@ class CookieJar { /** * @see Cookie::serializeToHttpRequest + * @param string $path + * @param string $domain * @return string */ public function serializeToHttpRequest( $path, $domain ) { @@ -232,8 +253,8 @@ class CookieJar { /** * Parse the content of an Set-Cookie HTTP Response header. * - * @param $cookie String - * @param string $domain cookie's domain + * @param string $cookie + * @param string $domain Cookie's domain * @return null */ public function parseCookieResponseHeader( $cookie, $domain ) { diff --git a/includes/DataUpdate.php b/includes/DataUpdate.php deleted file mode 100644 index 7b9ac281..00000000 --- a/includes/DataUpdate.php +++ /dev/null @@ -1,126 +0,0 @@ -<?php -/** - * Base code for update jobs that do something with some secondary - * data extracted from article. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Abstract base class for update jobs that do something with some secondary - * data extracted from article. - * - * @note: subclasses should NOT start or commit transactions in their doUpdate() method, - * a transaction will automatically be wrapped around the update. If need be, - * subclasses can override the beginTransaction() and commitTransaction() methods. - */ -abstract class DataUpdate implements DeferrableUpdate { - - /** - * Constructor - */ - public function __construct() { - # noop - } - - /** - * Begin an appropriate transaction, if any. - * This default implementation does nothing. - */ - public function beginTransaction() { - //noop - } - - /** - * Commit the transaction started via beginTransaction, if any. - * This default implementation does nothing. - */ - public function commitTransaction() { - //noop - } - - /** - * Abort / roll back the transaction started via beginTransaction, if any. - * This default implementation does nothing. - */ - public function rollbackTransaction() { - //noop - } - - /** - * Convenience method, calls doUpdate() on every DataUpdate in the array. - * - * This methods supports transactions logic by first calling beginTransaction() - * on all updates in the array, then calling doUpdate() on each, and, if all goes well, - * then calling commitTransaction() on each update. If an error occurs, - * rollbackTransaction() will be called on any update object that had beginTransaction() - * called but not yet commitTransaction(). - * - * This allows for limited transactional logic across multiple backends for storing - * secondary data. - * - * @param array $updates a list of DataUpdate instances - * @throws Exception|null - */ - public static function runUpdates( $updates ) { - if ( empty( $updates ) ) { - return; # nothing to do - } - - $open_transactions = array(); - $exception = null; - - /** - * @var $update DataUpdate - * @var $trans DataUpdate - */ - - try { - // begin transactions - foreach ( $updates as $update ) { - $update->beginTransaction(); - $open_transactions[] = $update; - } - - // do work - foreach ( $updates as $update ) { - $update->doUpdate(); - } - - // commit transactions - while ( count( $open_transactions ) > 0 ) { - $trans = array_pop( $open_transactions ); - $trans->commitTransaction(); - } - } catch ( Exception $ex ) { - $exception = $ex; - wfDebug( "Caught exception, will rethrow after rollback: " . $ex->getMessage() ); - } - - // rollback remaining transactions - while ( count( $open_transactions ) > 0 ) { - $trans = array_pop( $open_transactions ); - $trans->rollbackTransaction(); - } - - if ( $exception ) { - throw $exception; // rethrow after cleanup - } - } - -} diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 78568107..71268932 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -15,7 +15,7 @@ * performed in LocalSettings.php. * * Documentation is in the source and on: - * http://www.mediawiki.org/wiki/Manual:Configuration_settings + * https://www.mediawiki.org/wiki/Manual:Configuration_settings * * @warning Note: this (and other things) will break if the autoloader is not * enabled. Please include includes/AutoLoader.php before including this file. @@ -59,11 +59,23 @@ if ( !defined( 'MEDIAWIKI' ) ) { */ $wgConf = new SiteConfiguration; +/** + * Registry of factory functions to create config objects: + * The 'main' key must be set, and the value should be a valid + * callable. + * @since 1.23 + */ +$wgConfigRegistry = array( + 'main' => 'GlobalVarConfig::newInstance' +); + /** * MediaWiki version number + * Note that MediaWikiVersionFetcher::fetchVersion() uses a regex to check this. + * Using single quotes is, therefore, important here. * @since 1.2 */ -$wgVersion = '1.22.15'; +$wgVersion = '1.24.1'; /** * Name of the site. It must be changed in LocalSettings.php @@ -97,6 +109,13 @@ $wgServer = WebRequest::detectServer(); */ $wgCanonicalServer = false; +/** + * Server name. This is automatically computed by parsing the bare + * hostname out of $wgCanonicalServer. It should not be customized. + * @since 1.24 + */ +$wgServerName = false; + /************************************************************************//** * @name Script path settings * @{ @@ -236,7 +255,7 @@ $wgFileCacheDirectory = false; /** * The URL path of the wiki logo. The logo size should be 135x135 pixels. - * Defaults to "{$wgStylePath}/common/images/wiki.png". + * Defaults to "$wgResourceBasePath/resources/assets/wiki.png". */ $wgLogo = false; @@ -338,11 +357,6 @@ $wgEnableAsyncUploads = false; */ $wgIllegalFileChars = ":"; -/** - * @deprecated since 1.17 use $wgDeletedDirectory - */ -$wgFileStore = array(); - /** * What directory to place deleted uploads in. * Defaults to "{$wgUploadDirectory}/deleted". @@ -355,11 +369,20 @@ $wgDeletedDirectory = false; $wgImgAuthDetails = false; /** - * If this is enabled, img_auth.php will not allow image access unless the wiki - * is private. This improves security when image uploads are hosted on a - * separate domain. + * Map of relative URL directories to match to internal mwstore:// base storage paths. + * For img_auth.php requests, everything after "img_auth.php/" is checked to see + * if starts with any of the prefixes defined here. The prefixes should not overlap. + * The prefix that matches has a corresponding storage path, which the rest of the URL + * is assumed to be relative to. The file at that path (or a 404) is send to the client. + * + * Example: + * $wgImgAuthUrlPathMap['/timeline/'] = 'mwstore://local-fs/timeline-render/'; + * The above maps ".../img_auth.php/timeline/X" to "mwstore://local-fs/timeline-render/". + * The name "local-fs" should correspond by name to an entry in $wgFileBackends. + * + * @see $wgFileBackends */ -$wgImgAuthPublicTest = true; +$wgImgAuthUrlPathMap = array(); /** * File repository structures @@ -384,8 +407,6 @@ $wgImgAuthPublicTest = true; * url : base URL to the root of the zone * urlsByExt : map of file extension types to base URLs * (useful for using a different cache for videos) - * handlerUrl : base script-handled URL to the root of the zone - * (see FileRepo::getZoneHandlerUrl() function) * Zones default to using "<repo name>-<zone name>" as the container name * and default to using the container root as the zone's root directory. * Nesting of zone locations within other zones should be avoided. @@ -574,7 +595,7 @@ $wgCacheSharedUploads = true; /** * Allow for upload to be copied from an URL. - * The timeout for copy uploads is set by $wgHTTPTimeout. + * The timeout for copy uploads is set by $wgCopyUploadTimeout. * You have to assign the user right 'upload_by_url' to a user group, to use this. */ $wgAllowCopyUploads = false; @@ -739,7 +760,7 @@ $wgFileBlacklist = array( 'exe', 'scr', 'dll', 'msi', 'vbs', 'bat', 'com', 'pif', 'cmd', 'vxd', 'cpl' ); /** - * Files with these mime types will never be allowed as uploads + * Files with these MIME types will never be allowed as uploads * if $wgVerifyMimeType is enabled. */ $wgMimeTypeBlacklist = array( @@ -791,7 +812,7 @@ $wgDisableUploadScriptChecks = false; $wgUploadSizeWarning = false; /** - * list of trusted media-types and mime types. + * list of trusted media-types and MIME types. * Use the MEDIATYPE_xxx constants to represent media types. * This list is used by File::isSafeFile * @@ -839,12 +860,21 @@ $wgContentHandlers = array( CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // dumb version, no syntax highlighting CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', + // simple implementation, for use by extensions, etc. + CONTENT_MODEL_JSON => 'JsonContentHandler', // dumb version, no syntax highlighting CONTENT_MODEL_CSS => 'CssContentHandler', - // plain text, for use by extensions etc + // plain text, for use by extensions, etc. CONTENT_MODEL_TEXT => 'TextContentHandler', ); +/** + * Whether to enable server-side image thumbnailing. If false, images will + * always be sent to the client in full resolution, with appropriate width= and + * height= attributes on the <img> tag for the client to do its own scaling. + */ +$wgUseImageResize = true; + /** * Resizing can be done using PHP's internal image libraries or using * ImageMagick or another third-party converter, e.g. GraphicMagick. @@ -860,11 +890,6 @@ $wgUseImageMagick = false; */ $wgImageMagickConvertCommand = '/usr/bin/convert'; -/** - * The identify command shipped with ImageMagick - */ -$wgImageMagickIdentifyCommand = '/usr/bin/identify'; - /** * Sharpening parameter to ImageMagick */ @@ -921,7 +946,8 @@ $wgSVGConverters = array( 'ImageMagick' => '$path/convert -background white -thumbnail $widthx$height\! $input PNG:$output', 'sodipodi' => '$path/sodipodi -z -w $width -f $input -e $output', 'inkscape' => '$path/inkscape -z -w $width -f $input -e $output', - 'batik' => 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input', + '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' ), @@ -1007,6 +1033,14 @@ $wgTiffThumbnailType = false; */ $wgThumbnailEpoch = '20030516000000'; +/** + * Certain operations are avoided if there were too many recent failures, + * for example, thumbnail generation. Bump this value to invalidate all + * memory of failed operations and thus allow further attempts to resume. + * This is useful when a cause for the failures has been found and fixed. + */ +$wgAttemptFailureEpoch = 1; + /** * If set, inline scaled images will still produce "<img>" tags ready for * output instead of showing an error message. @@ -1034,11 +1068,6 @@ $wgGenerateThumbnailOnParse = true; */ $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 @@ -1108,45 +1137,45 @@ $wgAntivirusSetup = array( $wgAntivirusRequired = true; /** - * Determines if the mime type of uploaded files should be checked + * Determines if the MIME type of uploaded files should be checked */ $wgVerifyMimeType = true; /** - * Sets the mime type definition file to use by MimeMagic.php. + * Sets the MIME type definition file to use by MimeMagic.php. * Set to null, to use built-in defaults only. * example: $wgMimeTypeFile = '/etc/mime.types'; */ $wgMimeTypeFile = 'includes/mime.types'; /** - * Sets the mime type info file to use by MimeMagic.php. + * Sets the MIME type info file to use by MimeMagic.php. * Set to null, to use built-in defaults only. */ $wgMimeInfoFile = 'includes/mime.info'; /** - * Sets an external mime detector program. The command must print only - * the mime type to standard output. + * Sets an external MIME detector program. The command must print only + * the MIME type to standard output. * The name of the file to process will be appended to the command given here. - * If not set or NULL, mime_content_type will be used if available. + * If not set or NULL, PHP's fileinfo extension will be used if available. * * @par Example: * @code - * #$wgMimeDetectorCommand = "file -bi"; # use external mime detector (Linux) + * #$wgMimeDetectorCommand = "file -bi"; # use external MIME detector (Linux) * @endcode */ $wgMimeDetectorCommand = null; /** - * Switch for trivial mime detection. Used by thumb.php to disable all fancy + * Switch for trivial MIME detection. Used by thumb.php to disable all fancy * things, because only a few types of images are needed and file extensions * can be trusted. */ $wgTrivialMimeDetection = false; /** - * Additional XML types we can allow via mime-detection. + * Additional XML types we can allow via MIME-detection. * array = ( 'rootElement' => 'associatedMimeType' ) */ $wgXMLMimeTypes = array( @@ -1187,6 +1216,34 @@ $wgThumbLimits = array( 300 ); +/** + * When defined, is an array of image widths used as buckets for thumbnail generation. + * The goal is to save resources by generating thumbnails based on reference buckets instead of + * always using the original. This will incur a speed gain but cause a quality loss. + * + * The buckets generation is chained, with each bucket generated based on the above bucket + * when possible. File handlers have to opt into using that feature. For now only BitmapHandler + * supports it. + */ +$wgThumbnailBuckets = null; + +/** + * When using thumbnail buckets as defined above, this sets the minimum distance to the bucket + * above the requested size. The distance represents how many extra pixels of width the bucket + * needs in order to be used as the reference for a given thumbnail. For example, with the + * following buckets: + * + * $wgThumbnailBuckets = array ( 128, 256, 512 ); + * + * and a distance of 50: + * + * $wgThumbnailMinimumBucketDistance = 50; + * + * If we want to render a thumbnail of width 220px, the 512px bucket will be used, + * because 220 + 50 = 270 and the closest bucket bigger than 270px is 512. + */ +$wgThumbnailMinimumBucketDistance = 50; + /** * Default parameters for the "<gallery>" tag */ @@ -1251,7 +1308,7 @@ $wgDjvuTxt = null; * Path of the djvutoxml executable * This works like djvudump except much, much slower as of version 3.5. * - * For now we recommend you use djvudump instead. The djvuxml output is + * For now we recommend you use djvudump instead. The djvuxml output is * probably more stable, so we'll switch back to it as soon as they fix * the efficiency problem. * http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583 @@ -1265,7 +1322,7 @@ $wgDjvuToXML = null; /** * Shell command for the DJVU post processor - * Default: pnmtopng, since ddjvu generates ppm output + * Default: pnmtojpeg, since ddjvu generates ppm output * Set this to false to output the ppm file directly. */ $wgDjvuPostProcessor = 'pnmtojpeg'; @@ -1284,24 +1341,27 @@ $wgDjvuOutputExtension = 'jpg'; * @{ */ -$serverName = substr( $wgServer, strrpos( $wgServer, '/' ) + 1 ); /** * Site admin email address. + * + * Defaults to "wikiadmin@{$wgServerName}". */ -$wgEmergencyContact = 'wikiadmin@' . $serverName; +$wgEmergencyContact = false; /** * Password reminder email address. * * The address we should use as sender when a user is requesting his password. + * + * Defaults to "apache@{$wgServerName}". */ -$wgPasswordSender = 'apache@' . $serverName; - -unset( $serverName ); # Don't leak local variables to global scope +$wgPasswordSender = false; /** * Password reminder name + * + * @deprecated since 1.23; use the system message 'emailsender' instead. */ $wgPasswordSenderName = 'MediaWiki Mail'; @@ -1351,6 +1411,18 @@ $wgNewPasswordExpiry = 3600 * 24 * 7; */ $wgUserEmailConfirmationTokenExpiry = 7 * 24 * 60 * 60; +/** + * The number of days that a user's password is good for. After this number of days, the + * user will be asked to reset their password. Set to false to disable password expiration. + */ +$wgPasswordExpirationDays = false; + +/** + * If a user's password is expired, the number of seconds when they can still login, + * and cancel their password change, but are sent to the password change form on each login. + */ +$wgPasswordExpireGrace = 3600 * 24 * 7; // 7 days + /** * SMTP Mode. * @@ -1469,7 +1541,7 @@ $wgUsersNotifiedOnAllChanges = array(); $wgDBserver = 'localhost'; /** - * Database port number (for PostgreSQL) + * Database port number (for PostgreSQL and Microsoft SQL Server). */ $wgDBport = 5432; @@ -1495,11 +1567,21 @@ $wgDBtype = 'mysql'; /** * Whether to use SSL in DB connection. + * + * This setting is only used $wgLBFactoryConf['class'] is set to + * 'LBFactorySimple' and $wgDBservers is an empty array; otherwise + * the DBO_SSL flag must be set in the 'flags' option of the database + * connection to achieve the same functionality. */ $wgDBssl = false; /** * Whether to use compression in DB connection. + * + * This setting is only used $wgLBFactoryConf['class'] is set to + * 'LBFactorySimple' and $wgDBservers is an empty array; otherwise + * the DBO_COMPRESS flag must be set in the 'flags' option of the database + * connection to achieve the same functionality. */ $wgDBcompress = false; @@ -1551,7 +1633,7 @@ $wgSQLMode = ''; /** * Mediawiki schema */ -$wgDBmwschema = 'mediawiki'; +$wgDBmwschema = null; /** * To override default SQLite data directory ($docroot/../data) @@ -1582,10 +1664,10 @@ $wgAllDBsAreLocalhost = false; * $wgSharedPrefix is the table prefix for the shared database. It defaults to * $wgDBprefix. * - * @deprecated In new code, use the $wiki parameter to wfGetLB() to access - * remote databases. Using wfGetLB() allows the shared database to reside on - * separate servers to the wiki's own database, with suitable configuration - * of $wgLBFactoryConf. + * @deprecated since 1.21 In new code, use the $wiki parameter to wfGetLB() to + * access remote databases. Using wfGetLB() allows the shared database to + * reside on separate servers to the wiki's own database, with suitable + * configuration of $wgLBFactoryConf. */ $wgSharedDB = null; @@ -1607,8 +1689,13 @@ $wgSharedTables = array( 'user', 'user_properties' ); * - dbname: Default database name * - user: DB user * - password: DB password - * - type: "mysql" or "postgres" - * - load: ratio of DB_SLAVE load, must be >=0, the sum of all loads must be >0 + * - type: DB type + * + * - load: Ratio of DB_SLAVE load, must be >=0, the sum of all loads must be >0. + * If this is zero for any given server, no normal query traffic will be + * sent to it. It will be excluded from lag checks in maintenance scripts. + * The only way it can receive traffic is if groupLoads is used. + * * - groupLoads: array of load ratios, the key is the query group name. A query may belong * to several groups, the most specific group defined here is used. * @@ -1623,7 +1710,6 @@ $wgSharedTables = array( 'user', 'user_properties' ); * if available * * - max lag: (optional) Maximum replication lag before a slave will taken out of rotation - * - max threads: (optional) Maximum number of running threads * * These and any other user-defined properties will be assigned to the mLBInfo member * variable of the Database object. @@ -1654,13 +1740,14 @@ $wgDBservers = false; * The class identified here is responsible for reading $wgDBservers, * $wgDBserver, etc., so overriding it may cause those globals to be ignored. * - * The LBFactory_Multi class is provided for this purpose, please see - * includes/db/LBFactory_Multi.php for configuration information. + * The LBFactoryMulti class is provided for this purpose, please see + * includes/db/LBFactoryMulti.php for configuration information. */ -$wgLBFactoryConf = array( 'class' => 'LBFactory_Simple' ); +$wgLBFactoryConf = array( 'class' => 'LBFactorySimple' ); /** * How long to wait for a slave to catch up to the master + * @deprecated since 1.24 */ $wgMasterWaitTimeout = 10; @@ -1689,11 +1776,6 @@ $wgDBerrorLog = false; */ $wgDBerrorLogTZ = false; -/** - * When to give an error message - */ -$wgDBClusterTimeout = 10; - /** * Scale load balancer polling time so that under overload conditions, the * database server receives a SHOW STATUS query at an average interval of this @@ -1767,6 +1849,11 @@ $wgSlaveLagWarning = 10; */ $wgSlaveLagCritical = 30; +/** + * Use Windows Authentication instead of $wgDBuser / $wgDBpassword for MS SQL Server + */ +$wgDBWindowsAuthentication = false; + /**@}*/ # End of DB settings } /************************************************************************//** @@ -1793,7 +1880,7 @@ $wgCompressRevisions = false; * * CAUTION: Access to database might lead to code execution */ -$wgExternalStores = false; +$wgExternalStores = array(); /** * An array of external MySQL servers. @@ -1806,7 +1893,7 @@ $wgExternalStores = false; * ); * @endcode * - * Used by LBFactory_Simple, may be ignored if $wgLBFactoryConf is set to + * Used by LBFactorySimple, may be ignored if $wgLBFactoryConf is set to * another class. */ $wgExternalServers = array(); @@ -1921,9 +2008,6 @@ $wgCacheDirectory = false; * - CACHE_DB: Store cache objects in the DB * - CACHE_MEMCACHED: MemCached, must specify servers in $wgMemCachedServers * - CACHE_ACCEL: APC, XCache or WinCache - * - 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. * @@ -1976,15 +2060,10 @@ $wgLanguageConverterCacheType = CACHE_ANYTHING; * 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. - * - CACHE_DBA uses $wgTmpDirectory by default. The 'dir' parameter let you - * overrides that. */ $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' ), @@ -1999,16 +2078,32 @@ $wgObjectCaches = array( ); /** - * The expiry time for the parser cache, in seconds. - * The default is 86400 (one day). + * Map of bloom filter store names to configuration arrays. + * + * Example: + * $wgBloomFilterStores['main'] = array( + * 'cacheId' => 'main-v1', + * 'class' => 'BloomCacheRedis', + * 'redisServers' => array( '127.0.0.1:6379' ), + * 'redisConfig' => array( 'connectTimeout' => 2 ) + * ); + * + * A primary bloom filter must be created manually. + * Example in eval.php: + * <code> + * BloomCache::get( 'main' )->init( 'shared', 1000000000, .001 ); + * </code> + * The size should be as large as practical given wiki size and resources. + * + * @since 1.24 */ -$wgParserCacheExpireTime = 86400; +$wgBloomFilterStores = array(); /** - * Select which DBA handler <http://www.php.net/manual/en/dba.requirements.php> - * to use as CACHE_DBA backend. + * The expiry time for the parser cache, in seconds. + * The default is 86400 (one day). */ -$wgDBAhandler = 'db3'; +$wgParserCacheExpireTime = 86400; /** * Deprecated alias for $wgSessionsInObjectCache. @@ -2117,6 +2212,12 @@ $wgCachePages = true; */ $wgCacheEpoch = '20030516000000'; +/** + * Directory where GitInfo will look for pre-computed cache files. If false, + * $wgCacheDirectory/gitinfo will be used. + */ +$wgGitInfoCacheDirectory = false; + /** * Bump this number when changing the global style sheets and JavaScript. * @@ -2129,7 +2230,7 @@ $wgStyleVersion = '303'; /** * This will cache static pages for non-logged-in users to reduce * database traffic on public sites. - * Must set $wgShowIPinHeader = false + * Automatically sets $wgShowIPinHeader = false * ResourceLoader requests to default language and skins are cached * as well as single module requests. */ @@ -2218,7 +2319,7 @@ $wgInvalidateCacheOnLocalSettingsChange = true; * although they are referred to as Squid settings for historical reasons. * * Achieving a high hit ratio with an HTTP proxy requires special - * configuration. See http://www.mediawiki.org/wiki/Manual:Squid_caching for + * configuration. See https://www.mediawiki.org/wiki/Manual:Squid_caching for * more details. * * @{ @@ -2226,7 +2327,7 @@ $wgInvalidateCacheOnLocalSettingsChange = true; /** * Enable/disable Squid. - * See http://www.mediawiki.org/wiki/Manual:Squid_caching + * See https://www.mediawiki.org/wiki/Manual:Squid_caching */ $wgUseSquid = false; @@ -2285,7 +2386,9 @@ $wgSquidServers = array(); /** * As above, except these servers aren't purged on page changes; use to set a - * list of trusted proxies, etc. + * list of trusted proxies, etc. Supports both individual IP addresses and + * CIDR blocks. + * @since 1.23 Supports CIDR ranges */ $wgSquidServersNoPurge = array(); @@ -2368,42 +2471,6 @@ $wgSquidPurgeUseHostHeader = true; */ $wgHTCPRouting = array(); -/** - * @deprecated since 1.22, please use $wgHTCPRouting instead. - * - * Whenever this is set and $wgHTCPRouting evaluates to false, $wgHTCPRouting - * will be set to this value. - * This is merely for back compatibility. - * - * @since 1.20 - */ -$wgHTCPMulticastRouting = null; - -/** - * HTCP multicast address. Set this to a multicast IP address to enable HTCP. - * - * Note that MediaWiki uses the old non-RFC compliant HTCP format, which was - * present in the earliest Squid implementations of the protocol. - * - * This setting is DEPRECATED in favor of $wgHTCPRouting , and kept for - * backwards compatibility only. If $wgHTCPRouting is set, this setting is - * ignored. If $wgHTCPRouting is not set and this setting is, it is used to - * populate $wgHTCPRouting. - * - * @deprecated since 1.20 in favor of $wgHTCPMulticastRouting and since 1.22 in - * favor of $wgHTCPRouting. - */ -$wgHTCPMulticastAddress = false; - -/** - * HTCP multicast port. - * @deprecated since 1.20 in favor of $wgHTCPMulticastRouting and since 1.22 in - * favor of $wgHTCPRouting. - * - * @see $wgHTCPMulticastAddress - */ -$wgHTCPPort = 4827; - /** * HTCP multicast TTL. * @see $wgHTCPRouting @@ -2466,6 +2533,21 @@ $wgInterwikiMagic = true; */ $wgHideInterlanguageLinks = false; +/** + * List of additional interwiki prefixes that should be treated as + * interlanguage links (i.e. placed in the sidebar). + * Notes: + * - This will not do anything unless the prefixes are defined in the interwiki + * map. + * - The display text for these custom interlanguage links will be fetched from + * the system message "interlanguage-link-xyz" where xyz is the prefix in + * this array. + * - A friendly name for each site, used for tooltip text, may optionally be + * placed in the system message "interlanguage-link-sitename-xyz" where xyz is + * the prefix in this array. + */ +$wgExtraInterlanguageLinkPrefixes = array(); + /** * List of language names or overrides for default names in Names.php */ @@ -2641,11 +2723,6 @@ $wgDisableLangConversion = false; */ $wgDisableTitleConversion = false; -/** - * Whether to enable canonical language links in meta data. - */ -$wgCanonicalLanguageLinks = true; - /** * Default variant code, if false, the default will be the language code */ @@ -2793,6 +2870,23 @@ $wgHtml5 = true; */ $wgHtml5Version = null; +/** + * Temporary variable that allows HTMLForms to be rendered as tables. + * Table based layouts cause various issues when designing for mobile. + * This global allows skins or extensions a means to force non-table based rendering. + * Setting to false forces form components to always render as div elements. + * @since 1.24 + */ +$wgHTMLFormAllowTableFormat = true; + +/** + * Temporary variable that applies MediaWiki UI wherever it can be supported. + * Temporary variable that should be removed when mediawiki ui is more + * stable and change has been communicated. + * @since 1.24 + */ +$wgUseMediaWikiUIEverywhere = false; + /** * Enabled RDFa attributes for use in wikitext. * NOTE: Interaction with HTML5 is somewhat underspecified. @@ -2834,7 +2928,7 @@ $wgWellFormedXml = true; * Normally we wouldn't have to define this in the root "<html>" * element, but IE needs it there in some circumstances. * - * This is ignored if $wgMimeType is set to a non-XML mimetype. + * This is ignored if $wgMimeType is set to a non-XML MIME type. */ $wgXhtmlNamespaces = array(); @@ -2854,11 +2948,6 @@ $wgShowIPinHeader = true; */ $wgSiteNotice = ''; -/** - * A subtitle to add to the tagline, for skins that have it/ - */ -$wgExtraSubtitle = ''; - /** * If this is set, a "donate" link will appear in the sidebar. Set it to a URL. */ @@ -2873,23 +2962,28 @@ $wgValidateAllHtml = false; /** * Default skin, for new users and anonymous visitors. Registered users may * change this to any one of the other available skins in their preferences. - * This has to be completely lowercase; see the "skins" directory for the list - * of available skins. */ $wgDefaultSkin = 'vector'; /** - * 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 - * remove from the .../skins/ directory + * Fallback skin used when the skin defined by $wgDefaultSkin can't be found. + * + * @since 1.24 */ -$wgSkipSkin = ''; +$wgFallbackSkin = 'fallback'; /** - * Array for more like $wgSkipSkin. + * Specify the names of skins that should not be presented in the list of + * available skins in user preferences. If you want to remove a skin entirely, + * remove it from the skins/ directory and its entry from LocalSettings.php. */ $wgSkipSkins = array(); +/** + * @deprecated since 1.23; use $wgSkipSkins instead + */ +$wgSkipSkin = ''; + /** * Allow user Javascript page? * This enables a lot of neat customizations, but may @@ -3010,7 +3104,8 @@ $wgFooterIcons = array( ), "poweredby" => array( "mediawiki" => array( - "src" => null, // Defaults to "$wgStylePath/common/images/poweredby_mediawiki_88x31.png" + // src defaults to "$wgResourceBasePath/resources/assets/poweredby_mediawiki_88x31.png" + "src" => null, "url" => "//www.mediawiki.org/", "alt" => "Powered by MediaWiki", ) @@ -3025,33 +3120,11 @@ $wgFooterIcons = array( */ $wgUseCombinedLoginLink = false; -/** - * Search form look for Vector skin only. - * - true = use an icon search button - * - false = use Go & Search buttons - */ -$wgVectorUseSimpleSearch = true; - -/** - * Watch and unwatch as an icon rather than a link for Vector skin only. - * - true = use an icon watch/unwatch button - * - false = use watch/unwatch text link - */ -$wgVectorUseIconWatch = true; - /** * Display user edit counts in various prominent places. */ $wgEdititis = false; -/** - * Better directionality support (bug 6100 and related). - * Removed in 1.18, still kept here for LiquidThreads backwards compatibility. - * - * @deprecated since 1.18 - */ -$wgBetterDirectionality = true; - /** * Some web hosts attempt to rewrite all responses with a 404 (not found) * status code, mangling or hiding MediaWiki's output. If you are using such a @@ -3083,6 +3156,14 @@ $wgShowRollbackEditCount = 10; */ $wgEnableCanonicalServerLink = false; +/** + * When OutputHandler is used, mangle any output that contains + * <cross-domain-policy>. Without this, an attacker can send their own + * cross-domain policy unless it is prevented by the crossdomain.xml file at + * the domain root. + */ +$wgMangleFlashPolicy = true; + /** @} */ # End of output format settings } /*************************************************************************//** @@ -3109,6 +3190,111 @@ $wgEnableCanonicalServerLink = false; */ $wgResourceModules = array(); +/** + * Skin-specific styles for resource modules. + * + * These are later added to the 'skinStyles' list of the existing module. The 'styles' list can + * not be modified or disabled. + * + * For example, here is a module "bar" and how skin Foo would provide additional styles for it. + * + * @par Example: + * @code + * $wgResourceModules['bar'] = array( + * 'scripts' => 'resources/bar/bar.js', + * 'styles' => 'resources/bar/main.css', + * ); + * + * $wgResourceModuleSkinStyles['foo'] = array( + * 'bar' => 'skins/Foo/bar.css', + * ); + * @endcode + * + * This is mostly equivalent to: + * + * @par Equivalent: + * @code + * $wgResourceModules['bar'] = array( + * 'scripts' => 'resources/bar/bar.js', + * 'styles' => 'resources/bar/main.css', + * 'skinStyles' => array( + * 'foo' => skins/Foo/bar.css', + * ), + * ); + * @endcode + * + * If the module already defines its own entry in `skinStyles` for a given skin, then + * $wgResourceModuleSkinStyles is ignored. + * + * If a module defines a `skinStyles['default']` the skin may want to extend that instead + * of replacing them. This can be done using the `+` prefix. + * + * @par Example: + * @code + * $wgResourceModules['bar'] = array( + * 'scripts' => 'resources/bar/bar.js', + * 'styles' => 'resources/bar/basic.css', + * 'skinStyles' => array( + * 'default' => 'resources/bar/additional.css', + * ), + * ); + * // Note the '+' character: + * $wgResourceModuleSkinStyles['+foo'] = array( + * 'bar' => 'skins/Foo/bar.css', + * ); + * @endcode + * + * This is mostly equivalent to: + * + * @par Equivalent: + * @code + * $wgResourceModules['bar'] = array( + * 'scripts' => 'resources/bar/bar.js', + * 'styles' => 'resources/bar/basic.css', + * 'skinStyles' => array( + * 'default' => 'resources/bar/additional.css', + * 'foo' => array( + * 'resources/bar/additional.css', + * 'skins/Foo/bar.css', + * ), + * ), + * ); + * @endcode + * + * In other words, as a module author, use the `styles` list for stylesheets that may not be + * disabled by a skin. To provide default styles that may be extended or replaced, + * use `skinStyles['default']`. + * + * As with $wgResourceModules, paths default to being relative to the MediaWiki root. + * You should always provide a localBasePath and remoteBasePath (or remoteExtPath/remoteSkinPath). + * Either for all skin styles at once (first example below) or for each module separately (second + * example). + * + * @par Example: + * @code + * $wgResourceModuleSkinStyles['foo'] = array( + * 'bar' => 'bar.css', + * 'quux' => 'quux.css', + * 'remoteSkinPath' => 'Foo', + * 'localBasePath' => __DIR__, + * ); + * + * $wgResourceModuleSkinStyles['foo'] = array( + * 'bar' => array( + * 'bar.css', + * 'remoteSkinPath' => 'Foo', + * 'localBasePath' => __DIR__, + * ), + * 'quux' => array( + * 'quux.css', + * 'remoteSkinPath' => 'Foo', + * 'localBasePath' => __DIR__, + * ), + * ); + * @endcode + */ +$wgResourceModuleSkinStyles = array(); + /** * Extensions should register foreign module sources here. 'local' is a * built-in source that is not in this array, but defined by @@ -3116,10 +3302,7 @@ $wgResourceModules = array(); * * @par Example: * @code - * $wgResourceLoaderSources['foo'] = array( - * 'loadScript' => 'http://example.org/w/load.php', - * 'apiScript' => 'http://example.org/w/api.php' - * ); + * $wgResourceLoaderSources['foo'] = 'http://example.org/w/load.php'; * @endcode */ $wgResourceLoaderSources = array(); @@ -3132,14 +3315,23 @@ $wgResourceBasePath = null; /** * Maximum time in seconds to cache resources served by the resource loader. + * Used to set last modified headers (max-age/s-maxage). + * + * Following options to distinguish: + * - versioned: Used for modules with a version, because changing version + * numbers causes cache misses. This normally has a long expiry time. + * - unversioned: Used for modules without a version to propagate changes + * quickly to clients. Also used for modules with errors to recover quickly. + * This normally has a short expiry time. * - * @todo Document array structure + * Expiry time for the options to distinguish: + * - server: Squid/Varnish but also any other public proxy cache between the + * client and MediaWiki. + * - client: On the client side (e.g. in the browser cache). */ $wgResourceLoaderMaxage = array( 'versioned' => array( - // Squid/Varnish but also any other public proxy cache between the client and MediaWiki 'server' => 30 * 24 * 60 * 60, // 30 days - // On the client side (e.g. in the browser cache). 'client' => 30 * 24 * 60 * 60, // 30 days ), 'unversioned' => array( @@ -3181,6 +3373,15 @@ $wgResourceLoaderMinifierMaxLineLength = 1000; */ $wgIncludeLegacyJavaScript = true; +/** + * Whether to include the jQuery Migrate library, which lets legacy JS that + * requires jQuery 1.8.x to work and breaks with 1.9.x+. + * + * @since 1.24 + * @deprecated since 1.24, to be removed in 1.25 + */ +$wgIncludejQueryMigrate = false; + /** * Whether to preload the mediawiki.util module as blocking module in the top * queue. @@ -3270,12 +3471,14 @@ $wgResourceLoaderValidateStaticJS = false; $wgResourceLoaderExperimentalAsyncLoading = false; /** - * Global LESS variables. An associative array binding variable names to CSS - * string values. + * Global LESS variables. An associative array binding variable names to + * LESS code snippets representing their values. * - * Because the hashed contents of this array are used to construct the cache key - * that ResourceLoader uses to look up LESS compilation results, updating this - * array can be used to deliberately invalidate the set of cached results. + * Adding an item here is equivalent to writing `@variable: value;` + * at the beginning of all your .less files, with all the consequences. + * In particular, string values must be escaped and quoted. + * + * Changes to LESS variables do not trigger cache invalidation. * * @par Example: * @code @@ -3293,17 +3496,13 @@ $wgResourceLoaderLESSVars = array(); * Custom LESS functions. An associative array mapping function name to PHP * callable. * - * Changes to LESS functions do not trigger cache invalidation. If you update - * the behavior of a LESS function and need to invalidate stale compilation - * results, you can touch one of values in $wgResourceLoaderLESSVars, as - * documented above. + * Changes to LESS functions do not trigger cache invalidation. * * @since 1.22 + * @deprecated since 1.24 Questionable usefulness and problematic to support, + * will be removed in the future. */ -$wgResourceLoaderLESSFunctions = array( - 'embeddable' => 'ResourceLoaderLESSFunctions::embeddable', - 'embed' => 'ResourceLoaderLESSFunctions::embed', -); +$wgResourceLoaderLESSFunctions = array(); /** * Default import paths for LESS modules. LESS files referenced in @import @@ -3319,9 +3518,25 @@ $wgResourceLoaderLESSFunctions = array( * @since 1.22 */ $wgResourceLoaderLESSImportPaths = array( - "$IP/resources/mediawiki.less/", + "$IP/resources/src/mediawiki.less/", ); +/** + * Whether ResourceLoader should attempt to persist modules in localStorage on + * browsers that support the Web Storage API. + * + * @since 1.23 - Client-side module persistence is experimental. Exercise care. + */ +$wgResourceLoaderStorageEnabled = false; + +/** + * Cache version for client-side ResourceLoader module storage. You can trigger + * invalidation of the contents of the module store by incrementing this value. + * + * @since 1.23 + */ +$wgResourceLoaderStorageVersion = 1; + /** * Whether to allow site-wide CSS (MediaWiki:Common.css and friends) on * restricted pages like Special:UserLogin or Special:Preferences where @@ -3335,14 +3550,6 @@ $wgResourceLoaderLESSImportPaths = array( */ $wgAllowSiteCSSOnRestrictedPages = false; -/** - * When OutputHandler is used, mangle any output that contains - * <cross-domain-policy>. Without this, an attacker can send their own - * cross-domain policy unless it is prevented by the crossdomain.xml file at - * the domain root. - */ -$wgMangleFlashPolicy = true; - /** @} */ # End of resource loader settings } /*************************************************************************//** @@ -3452,9 +3659,21 @@ $wgLegalTitleChars = " %!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+"; /** * The interwiki prefix of the current wiki, or false if it doesn't have one. + * + * @deprecated since 1.23; use $wgLocalInterwikis instead */ $wgLocalInterwiki = false; +/** + * Array for multiple $wgLocalInterwiki values, in case there are several + * interwiki prefixes that point to the current wiki. If $wgLocalInterwiki is + * set, its value is prepended to this array, for backwards compatibility. + * + * Note, recent changes feeds use only the first entry in this array (or + * $wgLocalInterwiki, if it is set). See $wgRCFeeds + */ +$wgLocalInterwikis = array(); + /** * Expiry time for cache of interwiki table */ @@ -3556,6 +3775,30 @@ $wgNamespacesWithSubpages = array( NS_CATEGORY_TALK => true ); +/** + * Array holding default tracking category names. + * + * Array contains the system messages for each tracking category. + * Tracking categories allow pages with certain characteristics to be tracked. + * It works by adding any such page to a category automatically. + * + * A message with the suffix '-desc' should be added as a description message + * to have extra information on Special:TrackingCategories. + * + * @since 1.23 + */ +$wgTrackingCategories = array( + 'index-category', + 'noindex-category', + 'expensive-parserfunction-category', + 'post-expand-template-argument-category', + 'post-expand-template-inclusion-category', + 'hidden-category-category', + 'broken-file-category', + 'node-count-exceeded-category', + 'expansion-depth-exceeded-category', +); + /** * Array of namespaces which can be deemed to contain valid "content", as far * as the site statistics are concerned. Useful if additional namespaces also @@ -3653,36 +3896,20 @@ $wgMaxTemplateDepth = 40; $wgMaxPPExpandDepth = 40; /** - * The external URL protocols + * URL schemes that should be recognized as valid by wfParseUrl(). + * + * WARNING: Do not add 'file:' to this or internal file links will be broken. + * Instead, if you want to support file links, add 'file://'. The same applies + * to any other protocols with the same name as a namespace. See bug #44011 for + * more information. + * + * @see wfParseUrl */ $wgUrlProtocols = array( - 'http://', - 'https://', - 'ftp://', - 'ftps://', // If we allow ftp:// we should allow the secure version. - 'ssh://', - 'sftp://', // SFTP > FTP - 'irc://', - 'ircs://', // @bug 28503 - 'xmpp:', // Another open communication protocol - 'sip:', - 'sips:', - 'gopher://', - 'telnet://', // Well if we're going to support the above.. -ævar - 'nntp://', // @bug 3808 RFC 1738 - 'worldwind://', - 'mailto:', - 'tel:', // If we can make emails linkable, why not phone numbers? - 'sms:', // Likewise this is standardized too - 'news:', - 'svn://', - 'git://', - 'mms://', - 'bitcoin:', // Even registerProtocolHandler whitelists this along with mailto: - 'magnet:', // No reason to reject torrents over magnet: when they're allowed over http:// - 'urn:', // Allow URNs to be used in Microdata/RDFa <link ... href="urn:...">s - 'geo:', // urls define geo locations, they're useful in Microdata/RDFa and for coordinates - '//', // for protocol-relative URLs + 'bitcoin:', 'ftp://', 'ftps://', 'geo:', 'git://', 'gopher://', 'http://', + 'https://', 'irc://', 'ircs://', 'magnet:', 'mailto:', 'mms://', 'news:', + 'nntp://', 'redis://', 'sftp://', 'sip:', 'sips:', 'sms:', 'ssh://', + 'svn://', 'tel:', 'telnet://', 'urn:', 'worldwind://', 'xmpp:', '//' ); /** @@ -3809,13 +4036,16 @@ $wgNoFollowNsExceptions = array(); * (or any subdomains) will not be set to rel="nofollow" regardless of the * value of $wgNoFollowLinks. For instance: * - * $wgNoFollowDomainExceptions = array( 'en.wikipedia.org', 'wiktionary.org' ); + * $wgNoFollowDomainExceptions = array( 'en.wikipedia.org', 'wiktionary.org', + * 'mediawiki.org' ); * * This would add rel="nofollow" to links to de.wikipedia.org, but not * en.wikipedia.org, wiktionary.org, en.wiktionary.org, us.en.wikipedia.org, * etc. + * + * Defaults to mediawiki.org for the links included in the software by default. */ -$wgNoFollowDomainExceptions = array(); +$wgNoFollowDomainExceptions = array( 'mediawiki.org' ); /** * Allow DISPLAYTITLE to change title display @@ -3869,23 +4099,14 @@ $wgTranscludeCacheExpiry = 3600; * - '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 + * See also See https://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; +$wgArticleCountMethod = 'link'; /** * wgHitcounterUpdateFreq sets how often page counters should be updated, higher @@ -3914,6 +4135,7 @@ $wgActiveUserDays = 30; /** * For compatibility with old installations set to false + * @deprecated since 1.24 will be removed in future */ $wgPasswordSalt = true; @@ -3923,6 +4145,72 @@ $wgPasswordSalt = true; */ $wgMinimalPasswordLength = 1; +/** + * Specifies if users should be sent to a password-reset form on login, if their + * password doesn't meet the requirements of User::isValidPassword(). + * @since 1.23 + */ +$wgInvalidPasswordReset = true; + +/** + * Default password type to use when hashing user passwords + * + * @since 1.24 + */ +$wgPasswordDefault = 'pbkdf2'; + +/** + * Configuration for built-in password types. Maps the password type + * to an array of options. The 'class' option is the Password class to + * use. All other options are class-dependent. + * + * An advanced example: + * @code + * $wgPasswordConfig['bcrypt-peppered'] = array( + * 'class' => 'EncryptedPassword', + * 'underlying' => 'bcrypt', + * 'secrets' => array(), + * 'cipher' => MCRYPT_RIJNDAEL_256, + * 'mode' => MCRYPT_MODE_CBC, + * 'cost' => 5, + * ); + * @endcode + * + * @since 1.24 + */ +$wgPasswordConfig = array( + 'A' => array( + 'class' => 'MWOldPassword', + ), + 'B' => array( + 'class' => 'MWSaltedPassword', + ), + 'pbkdf2-legacyA' => array( + 'class' => 'LayeredParameterizedPassword', + 'types' => array( + 'A', + 'pbkdf2', + ), + ), + 'pbkdf2-legacyB' => array( + 'class' => 'LayeredParameterizedPassword', + 'types' => array( + 'B', + 'pbkdf2', + ), + ), + 'bcrypt' => array( + 'class' => 'BcryptPassword', + 'cost' => 9, + ), + 'pbkdf2' => array( + 'class' => 'Pbkdf2Password', + 'algo' => 'sha256', + 'cost' => '10000', + 'length' => '128', + ), +); + /** * 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 @@ -3963,8 +4251,8 @@ $wgReservedUsernames = array( /** * Settings added to this array will override the default globals for the user * preferences used by anonymous visitors and newly created accounts. - * For instance, to disable section editing links: - * $wgDefaultUserOptions ['editsection'] = 0; + * For instance, to disable editing on double clicks: + * $wgDefaultUserOptions ['editondblclick'] = 0; */ $wgDefaultUserOptions = array( 'ccmeonemails' => 0, @@ -3972,15 +4260,13 @@ $wgDefaultUserOptions = array( 'date' => 'default', 'diffonly' => 0, 'disablemail' => 0, - 'disablesuggest' => 0, 'editfont' => 'default', 'editondblclick' => 0, - 'editsection' => 1, 'editsectiononrightclick' => 0, 'enotifminoredits' => 0, 'enotifrevealaddr' => 0, 'enotifusertalkpages' => 1, - 'enotifwatchlistpages' => 0, + 'enotifwatchlistpages' => 1, 'extendwatchlist' => 0, 'fancysig' => 0, 'forceeditsummary' => 0, @@ -3988,34 +4274,28 @@ $wgDefaultUserOptions = array( 'hideminor' => 0, 'hidepatrolled' => 0, 'imagesize' => 2, - 'justify' => 0, 'math' => 1, 'minordefault' => 0, 'newpageshidepatrolled' => 0, - 'nocache' => 0, - 'noconvertlink' => 0, + 'nickname' => '', 'norollbackdiff' => 0, 'numberheadings' => 0, 'previewonfirst' => 0, 'previewontop' => 1, 'rcdays' => 7, 'rclimit' => 50, - 'rememberpassword' => 0, 'rows' => 25, - 'searchlimit' => 20, 'showhiddencats' => 0, 'shownumberswatching' => 1, - 'showtoc' => 1, 'showtoolbar' => 1, 'skin' => false, 'stubthreshold' => 0, - 'thumbsize' => 2, + 'thumbsize' => 5, 'underline' => 2, 'uselivepreview' => 0, 'usenewrc' => 0, - 'vector-simplesearch' => 1, - 'watchcreations' => 0, - 'watchdefault' => 0, + 'watchcreations' => 1, + 'watchdefault' => 1, 'watchdeletion' => 0, 'watchlistdays' => 3.0, 'watchlisthideanons' => 0, @@ -4025,6 +4305,7 @@ $wgDefaultUserOptions = array( 'watchlisthideown' => 0, 'watchlisthidepatrolled' => 0, 'watchmoves' => 0, + 'watchrollback' => 0, 'wllimit' => 250, 'useeditwarning' => 1, 'prefershttps' => 1, @@ -4126,7 +4407,7 @@ $wgBlockDisablesLogin = false; * * @note Also that this will only protect _pages in the wiki_. Uploaded files * will remain readable. You can use img_auth.php to protect uploaded files, - * see http://www.mediawiki.org/wiki/Manual:Image_Authorization + * see https://www.mediawiki.org/wiki/Manual:Image_Authorization */ $wgWhitelistRead = false; @@ -4211,6 +4492,7 @@ $wgGroupPermissions['*']['editmyoptions'] = true; $wgGroupPermissions['user']['move'] = true; $wgGroupPermissions['user']['move-subpages'] = true; $wgGroupPermissions['user']['move-rootuserpages'] = true; // can move root userpages +$wgGroupPermissions['user']['move-categorypages'] = true; $wgGroupPermissions['user']['movefile'] = true; $wgGroupPermissions['user']['read'] = true; $wgGroupPermissions['user']['edit'] = true; @@ -4258,6 +4540,7 @@ $wgGroupPermissions['sysop']['importupload'] = true; $wgGroupPermissions['sysop']['move'] = true; $wgGroupPermissions['sysop']['move-subpages'] = true; $wgGroupPermissions['sysop']['move-rootuserpages'] = true; +$wgGroupPermissions['sysop']['move-categorypages'] = true; $wgGroupPermissions['sysop']['patrol'] = true; $wgGroupPermissions['sysop']['autopatrol'] = true; $wgGroupPermissions['sysop']['protect'] = true; @@ -4279,8 +4562,9 @@ $wgGroupPermissions['sysop']['noratelimit'] = true; $wgGroupPermissions['sysop']['movefile'] = true; $wgGroupPermissions['sysop']['unblockself'] = true; $wgGroupPermissions['sysop']['suppressredirect'] = true; +#$wgGroupPermissions['sysop']['pagelang'] = true; #$wgGroupPermissions['sysop']['upload_by_url'] = true; -#$wgGroupPermissions['sysop']['mergehistory'] = true; +$wgGroupPermissions['sysop']['mergehistory'] = true; // Permission to change users' group assignments $wgGroupPermissions['bureaucrat']['userrights'] = true; @@ -4296,6 +4580,8 @@ $wgGroupPermissions['bureaucrat']['noratelimit'] = true; #$wgGroupPermissions['suppress']['hideuser'] = true; // To hide revisions/log items from users and Sysops #$wgGroupPermissions['suppress']['suppressrevision'] = true; +// To view revisions/log items hidden from users and Sysops +#$wgGroupPermissions['suppress']['viewsuppressed'] = true; // For private suppression log access #$wgGroupPermissions['suppress']['suppressionlog'] = true; @@ -4554,6 +4840,15 @@ $wgAvailableRights = array(); */ $wgDeleteRevisionsLimit = 0; +/** + * The maximum number of edits a user can have and + * can still be hidden by users with the hideuser permission. + * This is limited for performance reason. + * Set to false to disable the limit. + * @since 1.23 + */ +$wgHideUserContribLimit = 1000; + /** * Number of accounts each IP address may create, 0 to disable. * @@ -4586,12 +4881,6 @@ $wgSummarySpamRegex = array(); */ $wgEnableDnsBlacklist = false; -/** - * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for - * backward compatibility. - */ -$wgEnableSorbs = false; - /** * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true. * @@ -4617,12 +4906,6 @@ $wgEnableSorbs = false; */ $wgDnsBlacklistUrls = array( 'http.dnsbl.sorbs.net.' ); -/** - * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for - * backward compatibility. - */ -$wgSorbsUrl = array(); - /** * Proxy whitelist, list of addresses that are assumed to be non-proxy despite * what the other methods might say. @@ -4690,10 +4973,19 @@ $wgRateLimits = array( 'ip' => null, 'subnet' => null, ), + 'renderfile-nonstandard' => array( // same as above but for non-standard thumbnails + 'anon' => null, + 'user' => null, + 'newbie' => null, + 'ip' => null, + 'subnet' => null, + ), ); /** * Set to a filename to log rate limiter hits. + * + * @deprecated since 1.23, use $wgDebugLogGroups['ratelimit'] instead */ $wgRateLimitLog = null; @@ -4746,11 +5038,6 @@ $wgSecretKey = false; */ $wgProxyList = array(); -/** - * @deprecated since 1.14 - */ -$wgProxyKey = false; - /** @} */ # end of proxy scanner settings /************************************************************************//** @@ -4759,7 +5046,7 @@ $wgProxyKey = false; */ /** - * Default cookie expiration time. Setting to 0 makes all cookies session-only. + * Default cookie lifetime, in seconds. Setting to 0 makes all cookies session-only. */ $wgCookieExpiration = 180 * 86400; @@ -4805,17 +5092,6 @@ $wgCookiePrefix = false; */ $wgCookieHttpOnly = true; -/** - * If the requesting browser matches a regex in this blacklist, we won't - * send it cookies with HttpOnly mode, even if $wgCookieHttpOnly is on. - */ -$wgHttpOnlyBlacklist = array( - // Internet Explorer for Mac; sometimes the cookies work, sometimes - // they don't. It's difficult to predict, as combinations of path - // and expiration options affect its parsing. - '/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/', -); - /** * A list of cookies that vary the cache (for use by extensions) */ @@ -4852,7 +5128,7 @@ $wgUseTeX = false; */ /** - * Filename for debug logging. See http://www.mediawiki.org/wiki/How_to_debug + * Filename for debug logging. See https://www.mediawiki.org/wiki/How_to_debug * The debug log file should be not be publicly accessible if it is used, as it * may contain private data. */ @@ -4895,15 +5171,48 @@ $wgDebugComments = false; $wgDebugDBTransactions = false; /** - * Write SQL queries to the debug log + * Write SQL queries to the debug log. + * + * This setting is only used $wgLBFactoryConf['class'] is set to + * 'LBFactorySimple' and $wgDBservers is an empty array; otherwise + * the DBO_DEBUG flag must be set in the 'flags' option of the database + * connection to achieve the same functionality. */ $wgDebugDumpSql = false; /** - * Set to an array of log group keys to filenames. + * Trim logged SQL queries to this many bytes. Set 0/false/null to do no + * trimming. + * @since 1.24 + */ +$wgDebugDumpSqlLength = 500; + +/** + * Map of string log group names to log destinations. + * * If set, wfDebugLog() output for that group will go to that file instead * of the regular $wgDebugLogFile. Useful for enabling selective logging * in production. + * + * Log destinations may be one of the following: + * - false to completely remove from the output, including from $wgDebugLogFile. + * - string values specifying a filename or URI. + * - associative array mapping 'destination' key to the desired filename or URI. + * The associative array may also contain a 'sample' key with an integer value, + * specifying a sampling factor. + * + * @par Example: + * @code + * $wgDebugLogGroups['redis'] = '/var/log/mediawiki/redis.log'; + * @endcode + * + * @par Advanced example: + * @code + * $wgDebugLogGroups['memcached'] = ( + * 'destination' => '/var/log/mediawiki/memcached.log', + * 'sample' => 1000, // log 1 message out of every 1,000. + * ); + * @endcode */ $wgDebugLogGroups = array(); @@ -4947,6 +5256,11 @@ $wgShowExceptionDetails = false; /** * If true, show a backtrace for database errors + * + * @note This setting only applies when connection errors and query errors are + * reported in the normal manner. $wgShowExceptionDetails applies in other cases, + * including those in which an uncaught exception is thrown from within the + * exception handler. */ $wgShowDBErrorBacktrace = false; @@ -4987,20 +5301,11 @@ $wgProfileLimit = 0.0; /** * Don't put non-profiling info into log file - */ -$wgProfileOnly = false; - -/** - * Log sums from profiling into "profiling" table in db. * - * You have to create a 'profiling' table in your database before using - * this feature. Run set $wgProfileToDatabase to true in - * LocalSettings.php and run maintenance/update.php or otherwise - * manually add patch-profiling.sql to your database. - * - * To enable profiling, edit StartProfiler.php + * @deprecated since 1.23, set the log file in + * $wgDebugLogGroups['profileoutput'] instead. */ -$wgProfileToDatabase = false; +$wgProfileOnly = false; /** * If true, print a raw call tree instead of per-function report @@ -5016,7 +5321,8 @@ $wgProfilePerHost = false; * Host for UDP profiler. * * The host should be running a daemon which can be obtained from MediaWiki - * Subversion at: http://svn.wikimedia.org/svnroot/mediawiki/trunk/udpprofile + * Git at: + * http://git.wikimedia.org/tree/operations%2Fsoftware.git/master/udpprofile */ $wgUDPProfilerHost = '127.0.0.1'; @@ -5028,20 +5334,15 @@ $wgUDPProfilerPort = '3811'; /** * Format string for the UDP profiler. The UDP profiler invokes sprintf() with - * (profile id, count, cpu, cpu_sq, real, real_sq, entry name) as arguments. - * You can use sprintf's argument numbering/swapping capability to repeat, - * re-order or omit fields. + * (profile id, count, cpu, cpu_sq, real, real_sq, entry name, memory) as + * arguments. You can use sprintf's argument numbering/swapping capability to + * repeat, re-order or omit fields. * * @see $wgStatsFormatString * @since 1.22 */ $wgUDPProfilerFormatString = "%s - %d %f %f %f %f %s\n"; -/** - * Detects non-matching wfProfileIn/wfProfileOut calls - */ -$wgDebugProfiling = false; - /** * Output debug message on every wfProfileIn/wfProfileOut */ @@ -5111,21 +5412,6 @@ $wgParserTestFiles = array( "$IP/tests/parser/extraParserTests.txt" ); -/** - * If configured, specifies target CodeReview installation to send test - * result data from 'parserTests.php --upload' - * - * Something like this: - * $wgParserTestRemote = array( - * 'api-url' => 'http://www.mediawiki.org/w/api.php', - * 'repo' => 'MediaWiki', - * 'suite' => 'ParserTests', - * 'path' => '/trunk/phase3', // not used client-side; for reference - * 'secret' => 'qmoicj3mc4mcklmqw', // Shared secret used in HMAC validation - * ); - */ -$wgParserTestRemote = false; - /** * Allow running of javascript test suites via [[Special:JavaScriptTest]] (such as QUnit). */ @@ -5189,18 +5475,6 @@ $wgAdvancedSearchHighlighting = false; */ $wgSearchHighlightBoundaries = '[\p{Z}\p{P}\p{C}]'; -/** - * Set to true to have the search engine count total - * search matches to present in the Special:Search UI. - * Not supported by every search engine shipped with MW. - * - * This could however be slow on larger wikis, and is pretty flaky - * with the current title vs content split. Recommend avoiding until - * that's been worked out cleanly; but this may aid in testing the - * search UI and API to confirm that the result count works. - */ -$wgCountTotalSearchHits = false; - /** * Template for OpenSearch suggestions, defaults to API action=opensearch * @@ -5219,6 +5493,12 @@ $wgOpenSearchTemplate = false; */ $wgEnableOpenSearchSuggest = true; +/** + * Integer defining default number of entries to show on + * OpenSearch call. + */ +$wgOpenSearchDefaultLimit = 10; + /** * Expiry time for search suggestion responses */ @@ -5243,25 +5523,6 @@ $wgNamespacesToBeSearchedDefault = array( NS_MAIN => true, ); -/** - * Namespaces to be searched when user clicks the "Help" tab - * on Special:Search. - * - * Same format as $wgNamespacesToBeSearchedDefault. - */ -$wgNamespacesToBeSearchedHelp = array( - NS_PROJECT => true, - NS_HELP => true, -); - -/** - * If set to true the 'searcheverything' preference will be effective only for - * logged-in users. - * Useful for big wikis to maintain different search profiles for anonymous and - * logged-in users. - */ -$wgSearchEverythingOnlyLoggedIn = false; - /** * Disable the internal MySQL-based search, to allow it to be * implemented by an extension instead. @@ -5349,11 +5610,6 @@ $wgPreviewOnOpenNamespaces = array( NS_CATEGORY => true ); -/** - * Go button goes straight to the edit screen if the article doesn't exist. - */ -$wgGoToEdit = false; - /** * Enable the UniversalEditButton for browsers that support it * (currently only Firefox with an extension) @@ -5390,13 +5646,6 @@ if ( !isset( $wgCommandLineMode ) ) { */ $wgCommandLineDarkBg = false; -/** - * Array for extensions to register their maintenance scripts with the - * system. The key is the name of the class and the value is the full - * path to the file - */ -$wgMaintenanceScripts = array(); - /** * Set this to a string to put the wiki into read-only mode. The text will be * used as an explanation to users. @@ -5445,9 +5694,10 @@ $wgGitBin = '/usr/bin/git'; * @since 1.20 */ $wgGitRepositoryViewers = array( - 'https://gerrit.wikimedia.org/r/p/(.*)' => 'https://git.wikimedia.org/commit/%r/%H', - 'ssh://(?:[a-z0-9_]+@)?gerrit.wikimedia.org:29418/(.*)' - => 'https://git.wikimedia.org/commit/%r/%H', + 'https://(?:[a-z0-9_]+@)?gerrit.wikimedia.org/r/(?:p/)?(.*)' => + 'https://git.wikimedia.org/tree/%r/%H', + 'ssh://(?:[a-z0-9_]+@)?gerrit.wikimedia.org:29418/(.*)' => + 'https://git.wikimedia.org/tree/%r/%H', ); /** @} */ # End of maintenance } @@ -5474,74 +5724,44 @@ $wgRCMaxAge = 13 * 7 * 24 * 3600; $wgRCFilterByAge = false; /** - * List of Days and Limits options to list in the Special:Recentchanges and + * List of Limits options to list in the Special:Recentchanges and * Special:Recentchangeslinked pages. */ $wgRCLinkLimits = array( 50, 100, 250, 500 ); -$wgRCLinkDays = array( 1, 3, 7, 14, 30 ); - -/** - * Send recent changes updates via UDP. The updates will be formatted for IRC. - * Set this to the IP address of the receiver. - * - * @deprecated since 1.22, use $wgRCFeeds - */ -$wgRC2UDPAddress = false; - -/** - * Port number for RC updates - * - * @deprecated since 1.22, use $wgRCFeeds - */ -$wgRC2UDPPort = false; - -/** - * Prefix to prepend to each UDP packet. - * This can be used to identify the wiki. A script is available called - * mxircecho.py which listens on a UDP port, and uses a prefix ending in a - * tab to identify the IRC channel to send the log line to. - * - * @deprecated since 1.22, use $wgRCFeeds - */ -$wgRC2UDPPrefix = ''; - -/** - * If this is set to true, $wgLocalInterwiki will be prepended to links in the - * IRC feed. If this is set to a string, that string will be used as the prefix. - * - * @deprecated since 1.22, use $wgRCFeeds - */ -$wgRC2UDPInterwikiPrefix = false; /** - * Set to true to omit "bot" edits (by users with the bot permission) from the - * UDP feed. - * - * @deprecated since 1.22, use $wgRCFeeds + * List of Days options to list in the Special:Recentchanges and + * Special:Recentchangeslinked pages. */ -$wgRC2UDPOmitBots = false; +$wgRCLinkDays = array( 1, 3, 7, 14, 30 ); /** * Destinations to which notifications about recent changes * should be sent. * - * As of MediaWiki 1.22, the only supported 'engine' parameter option in core - * is 'UDPRCFeedEngine', which is used to send recent changes over UDP to the - * specified server. + * As of MediaWiki 1.22, there are 2 supported 'engine' parameter option in core: + * * 'UDPRCFeedEngine', which is used to send recent changes over UDP to the + * specified server. + * * 'RedisPubSubFeedEngine', which is used to send recent changes to Redis. + * * The common options are: * * 'uri' -- the address to which the notices are to be sent. * * 'formatter' -- the class name (implementing RCFeedFormatter) which will - * produce the text to send. + * produce the text to send. This can also be an object of the class. * * 'omit_bots' -- whether the bot edits should be in the feed + * * 'omit_anon' -- whether anonymous edits should be in the feed + * * 'omit_user' -- whether edits by registered users should be in the feed + * * 'omit_minor' -- whether minor edits should be in the feed + * * 'omit_patrolled' -- whether patrolled edits should be in the feed + * * The IRC-specific options are: * * 'add_interwiki_prefix' -- whether the titles should be prefixed with - * $wgLocalInterwiki. + * the first entry in the $wgLocalInterwikis array (or the value of + * $wgLocalInterwiki, if set) + * * The JSON-specific options are: * * 'channel' -- if set, the 'channel' parameter is also set in JSON values. * - * To ensure backwards-compatability, whenever $wgRC2UDPAddress is set, a - * 'default' feed will be created reusing the deprecated $wgRC2UDP* variables. - * * @example $wgRCFeeds['example'] = array( * 'formatter' => 'JSONRCFeedFormatter', * 'uri' => "udp://localhost:1336", @@ -5567,13 +5787,6 @@ $wgRCEngines = array( 'udp' => 'UDPRCFeedEngine', ); -/** - * Enable user search in Special:Newpages - * This is really a temporary hack around an index install bug on some Wikipedias. - * Kill it once fixed. - */ -$wgEnableNewpagesUserFilter = true; - /** * Use RC Patrolling to check for vandalism */ @@ -5707,24 +5920,42 @@ $wgUnwatchedPageThreshold = false; * To register a new one: * @code * $wgRecentChangesFlags['flag'] => array( + * // message for the letter displayed next to rows on changes lists * 'letter' => 'letter-msg', - * 'title' => 'tooltip-msg' + * // message for the tooltip of the letter + * 'title' => 'tooltip-msg', + * // optional (defaults to 'tooltip-msg'), message to use in the legend box + * 'legend' => 'legend-msg', + * // optional (defaults to 'flag'), CSS class to put on changes lists rows + * 'class' => 'css-class', * ); * @endcode * - * Optional 'class' allows to set a css class different than the flag name. - * * @since 1.22 */ $wgRecentChangesFlags = array( - 'newpage' => array( 'letter' => 'newpageletter', - 'title' => 'recentchanges-label-newpage' ), - 'minor' => array( 'letter' => 'minoreditletter', - 'title' => 'recentchanges-label-minor', 'class' => 'minoredit' ), - 'bot' => array( 'letter' => 'boteditletter', - 'title' => 'recentchanges-label-bot', 'class' => 'botedit' ), - 'unpatrolled' => array( 'letter' => 'unpatrolledletter', - 'title' => 'recentchanges-label-unpatrolled' ), + 'newpage' => array( + 'letter' => 'newpageletter', + 'title' => 'recentchanges-label-newpage', + 'legend' => 'recentchanges-legend-newpage', + ), + 'minor' => array( + 'letter' => 'minoreditletter', + 'title' => 'recentchanges-label-minor', + 'legend' => 'recentchanges-legend-minor', + 'class' => 'minoredit', + ), + 'bot' => array( + 'letter' => 'boteditletter', + 'title' => 'recentchanges-label-bot', + 'legend' => 'recentchanges-legend-bot', + 'class' => 'botedit', + ), + 'unpatrolled' => array( + 'letter' => 'unpatrolledletter', + 'title' => 'recentchanges-label-unpatrolled', + 'legend' => 'recentchanges-legend-unpatrolled', + ), ); /** @} */ # end RC/watchlist } @@ -5763,11 +5994,6 @@ $wgRightsText = null; */ $wgRightsIcon = null; -/** - * Set to an array of metadata terms. Else they will be loaded based on $wgRightsUrl - */ -$wgLicenseTerms = false; - /** * Set this to some HTML to override the rights icon with an arbitrary logo * @deprecated since 1.18 Use $wgFooterIcons['copyright']['copyright'] @@ -5806,6 +6032,17 @@ $wgShowCreditsIfMax = true; * Special:Import (for sysops). Since complete page history can be imported, * these should be 'trusted'. * + * This can either be a regular array, or an associative map specifying + * subprojects on the interwiki map of the target wiki, or a mix of the two, + * e.g. + * @code + * $wgImportSources = array( + * 'wikipedia' => array( 'cs', 'en', 'fr', 'zh' ), + * 'wikispecies', + * 'wikia' => array( 'animanga', 'brickipedia', 'desserts' ), + * ); + * @endcode + * * If a user has the 'import' permission but not the 'importupload' permission, * they will only be able to run imports through this transwiki interface. */ @@ -5886,6 +6123,16 @@ $wgExtensionFunctions = array(); * Variables defined in extensions will override conflicting variables defined * in the core. * + * Since MediaWiki 1.23, use of this variable to define messages is discouraged; instead, store + * messages in JSON format and use $wgMessagesDirs. For setting other variables than + * $messages, $wgExtensionMessagesFiles should still be used. Use a DIFFERENT key because + * any entry having a key that also exists in $wgMessagesDirs will be ignored. + * + * Extensions using the JSON message format can preserve backward compatibility with + * earlier versions of MediaWiki by using a compatibility shim, such as one generated + * by the generateJsonI18n.php maintenance script, listing it under the SAME key + * as for the $wgMessagesDirs entry. + * * @par Example: * @code * $wgExtensionMessagesFiles['ConfirmEdit'] = __DIR__.'/ConfirmEdit.i18n.php'; @@ -5893,6 +6140,34 @@ $wgExtensionFunctions = array(); */ $wgExtensionMessagesFiles = array(); +/** + * Extension messages directories. + * + * Associative array mapping extension name to the path of the directory where message files can + * be found. The message files are expected to be JSON files named for their language code, e.g. + * en.json, de.json, etc. Extensions with messages in multiple places may specify an array of + * message directories. + * + * @par Simple example: + * @code + * $wgMessagesDirs['Example'] = __DIR__ . '/i18n'; + * @endcode + * + * @par Complex example: + * @code + * $wgMessagesDirs['Example'] = array( + * __DIR__ . '/lib/ve/i18n', + * __DIR__ . '/lib/oojs-ui/i18n', + * __DIR__ . '/i18n', + * ) + * @endcode + * @since 1.23 + */ +$wgMessagesDirs = array( + 'core' => "$IP/languages/i18n", + 'oojs-ui' => "$IP/resources/lib/oojs-ui/i18n", +); + /** * Array of files with list(s) of extension entry points to be used in * maintenance/mergeMessageFileList.php @@ -5922,19 +6197,20 @@ $wgParserOutputHooks = array(); $wgEnableParserLimitReporting = true; /** - * List of valid skin names. + * List of valid skin names + * * 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. + * cased name for the skin. This value will be prefixed with "Skin" to create + * the class name of the skin to load. Use Skin::getSkinNames() as an accessor + * if you wish to have access to the full list. */ $wgValidSkinNames = array(); /** - * Special page list. - * See the top of SpecialPage.php for documentation. + * Special page list. This is an associative array mapping the (canonical) names of + * special pages to either a class name to be instantiated, or a callback to use for + * creating the special page object. In both cases, the result must be an instance of + * SpecialPage. */ $wgSpecialPages = array(); @@ -5944,30 +6220,63 @@ $wgSpecialPages = array(); $wgAutoloadClasses = array(); /** - * An array of extension types and inside that their names, versions, authors, - * urls, descriptions and pointers to localized description msgs. Note that - * the version, url, description and descriptionmsg key can be omitted. + * Switch controlling legacy case-insensitive classloading. + * Do not disable if your wiki must support data created by PHP4, or by + * MediaWiki 1.4 or earlier. + */ +$wgAutoloadAttemptLowercase = true; + +/** + * An array of information about installed extensions keyed by their type. + * + * All but 'name', 'path' and 'author' can be omitted. * * @code * $wgExtensionCredits[$type][] = array( - * 'name' => 'Example extension', - * 'version' => 1.9, * 'path' => __FILE__, - * 'author' => 'Foo Barstein', - * 'url' => 'http://www.example.com/Example%20Extension/', - * 'description' => 'An example extension', + * 'name' => 'Example extension', + * 'namemsg' => 'exampleextension-name', + * 'author' => array( + * 'Foo Barstein', + * ), + * 'version' => '1.9.0', + * 'url' => 'http://example.org/example-extension/', * 'descriptionmsg' => 'exampleextension-desc', + * 'license-name' => 'GPL-2.0', * ); * @endcode * - * Where $type is 'specialpage', 'parserhook', 'variable', 'media' or 'other'. - * Where 'descriptionmsg' can be an array with message key and parameters: - * 'descriptionmsg' => array( 'exampleextension-desc', param1, param2, ... ), + * The extensions are listed on Special:Version. This page also looks for a file + * named COPYING or LICENSE (optional .txt extension) and provides a link to + * view said file. When the 'license-name' key is specified, this file is + * interpreted as wikitext. + * + * - $type: One of 'specialpage', 'parserhook', 'variable', 'media', 'antispam', + * 'skin', 'api', or 'other', or any additional types as specified through the + * ExtensionTypes hook as used in SpecialVersion::getExtensionTypes(). + * + * - name: Name of extension as an inline string instead of localizable message. + * Do not omit this even if 'namemsg' is provided, as it is used to override + * the path Special:Version uses to find extension's license info, and is + * required for backwards-compatibility with MediaWiki 1.23 and older. + * + * - namemsg (since MW 1.24): A message key for a message containing the + * extension's name, if the name is localizable. (For example, skin names + * usually are.) + * + * - author: A string or an array of strings. Authors can be linked using + * the regular wikitext link syntax. To have an internationalized version of + * "and others" show, add an element "...". This element can also be linked, + * for instance "[http://example ...]". + * + * - descriptionmsg: A message key or an an array with message key and parameters: + * `'descriptionmsg' => 'exampleextension-desc',` + * + * - description: Description of extension as an inline string instead of + * localizable message (omit in favour of 'descriptionmsg'). * - * author can be a string or an array of strings. Authors can be linked using - * the regular wikitext link syntax. To have an internationalized version of - * "and others" show, add an element "...". This element can also be linked, - * for instance "[http://example ...]". + * - license-name: Short name of the license (used as label for the link), such + * as "GPL-2.0" or "MIT" (https://spdx.org/licenses/ for a list of identifiers). */ $wgExtensionCredits = array(); @@ -6019,7 +6328,7 @@ $wgHooks = array(); */ $wgJobClasses = array( 'refreshLinks' => 'RefreshLinksJob', - 'refreshLinks2' => 'RefreshLinksJob2', + 'refreshLinks2' => 'RefreshLinksJob2', // b/c 'htmlCacheUpdate' => 'HTMLCacheUpdateJob', 'sendMail' => 'EmaillingJob', 'enotifNotify' => 'EnotifNotifyJob', @@ -6039,9 +6348,21 @@ $wgJobClasses = array( * - 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 ) + * These settings should be global to all wikis. */ $wgJobTypesExcludedFromDefaultQueue = array( 'AssembleUploadChunks', 'PublishStashedFile' ); +/** + * Map of job types to how many job "work items" should be run per second + * on each job runner process. The meaning of "work items" varies per job, + * but typically would be something like "pages to update". A single job + * may have a variable number of work items, as is the case with batch jobs. + * This is used by runJobs.php and not jobs run via $wgJobRunRate. + * These settings should be global to all wikis. + * @var float[] + */ +$wgJobBackoffThrottling = array(); + /** * Map of job types to configuration arrays. * This determines which queue class and storage system is used for each job type. @@ -6065,7 +6386,8 @@ $wgJobQueueAggregator = array( * Expensive Querypages are already updated. */ $wgSpecialPageCacheUpdates = array( - 'Statistics' => array( 'SiteStatsUpdate', 'cacheUpdate' ) + 'Statistics' => array( 'SiteStatsUpdate', 'cacheUpdate' ), + 'Activeusers' => array( 'SpecialActiveUsers', 'cacheUpdate' ), ); /** @@ -6262,9 +6584,6 @@ $wgLogActions = array( 'protect/modify' => 'modifiedarticleprotection', 'protect/unprotect' => 'unprotectedarticle', 'protect/move_prot' => 'movedarticleprotection', - 'upload/upload' => 'uploadedimage', - 'upload/overwrite' => 'overwroteimage', - 'upload/revert' => 'uploadedimage', 'import/upload' => 'import-logentry-upload', 'import/interwiki' => 'import-logentry-interwiki', 'merge/merge' => 'pagemerge-logentry', @@ -6291,6 +6610,9 @@ $wgLogActionsHandlers = array( 'patrol/patrol' => 'PatrolLogFormatter', 'rights/rights' => 'RightsLogFormatter', 'rights/autopromote' => 'RightsLogFormatter', + 'upload/upload' => 'LogFormatter', + 'upload/overwrite' => 'LogFormatter', + 'upload/revert' => 'LogFormatter', ); /** @@ -6324,11 +6646,6 @@ $wgDisableQueryPageUpdate = false; */ $wgSpecialPageGroups = array(); -/** - * Whether or not to sort special pages in Special:Specialpages - */ -$wgSortSpecialPages = true; - /** * On Special:Unusedimages, consider images "used", if they are put * into a category. Default (false) is not to count those as used. @@ -6379,12 +6696,6 @@ $wgActions = array( '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(); - /** @} */ # end actions } /*************************************************************************//** @@ -6470,7 +6781,7 @@ $wgExemptFromUserRobotsControl = null; * Enable the MediaWiki API for convenient access to * machine-readable data via api.php * - * See http://www.mediawiki.org/wiki/API + * See https://www.mediawiki.org/wiki/API */ $wgEnableAPI = true; @@ -6499,13 +6810,76 @@ $wgDebugAPI = false; /** * API module extensions. - * Associative array mapping module name to class name. + * + * Associative array mapping module name to modules specs; + * Each module spec is an associative array containing at least + * the 'class' key for the module's class, and optionally a + * 'factory' key for the factory function to use for the module. + * + * That factory function will be called with two parameters, + * the parent module (an instance of ApiBase, usually ApiMain) + * and the name the module was registered under. The return + * value must be an instance of the class given in the 'class' + * field. + * + * For backward compatibility, the module spec may also be a + * simple string containing the module's class name. In that + * case, the class' constructor will be called with the parent + * module and module name as parameters, as described above. + * + * Examples for registering API modules: + * + * @code + * $wgAPIModules['foo'] = 'ApiFoo'; + * $wgAPIModules['bar'] = array( + * 'class' => 'ApiBar', + * 'factory' => function( $main, $name ) { ... } + * ); + * $wgAPIModules['xyzzy'] = array( + * 'class' => 'ApiXyzzy', + * 'factory' => array( 'XyzzyFactory', 'newApiModule' ) + * ); + * @endcode + * * Extension modules may override the core modules. - * @todo Describe each of the variables, group them and add examples + * See ApiMain::$Modules for a list of the core modules. */ $wgAPIModules = array(); + +/** + * API format module extensions. + * Associative array mapping format module name to module specs (see $wgAPIModules). + * Extension modules may override the core modules. + * + * See ApiMain::$Formats for a list of the core format modules. + */ +$wgAPIFormatModules = array(); + +/** + * API Query meta module extensions. + * Associative array mapping meta module name to module specs (see $wgAPIModules). + * Extension modules may override the core modules. + * + * See ApiQuery::$QueryMetaModules for a list of the core meta modules. + */ $wgAPIMetaModules = array(); + +/** + * API Query prop module extensions. + * Associative array mapping prop module name to module specs (see $wgAPIModules). + * Extension modules may override the core modules. + * + * See ApiQuery::$QueryPropModules for a list of the core prop modules. + */ $wgAPIPropModules = array(); + +/** + * API Query list module extensions. + * Associative array mapping list module name to module specs (see $wgAPIModules). + * Extension modules may override the core modules. + * + * See ApiQuery::$QueryListModules for a list of the core list modules. + */ $wgAPIListModules = array(); /** @@ -6683,12 +7057,12 @@ $wgShellLocale = 'en_US.utf8'; */ /** - * Timeout for HTTP requests done internally + * Timeout for HTTP requests done internally, in seconds. */ $wgHTTPTimeout = 25; /** - * Timeout for Asynchronous (background) HTTP requests + * Timeout for Asynchronous (background) HTTP requests, in seconds. */ $wgAsyncHTTPTimeout = 25; @@ -6719,6 +7093,14 @@ $wgHTTPConnectTimeout = 5e0; */ $wgJobRunRate = 1; +/** + * When $wgJobRunRate > 0, try to run jobs asynchronously, spawning a new process + * to handle the job execution, instead of blocking the request until the job + * execution finishes. + * @since 1.23 + */ +$wgRunJobsAsync = true; + /** * Number of rows to update per job */ @@ -6729,15 +7111,6 @@ $wgUpdateRowsPerJob = 500; */ $wgUpdateRowsPerQuery = 100; -/** - * Do not purge all the pages that use a page when it is edited - * if there are more than this many such pages. This is used to - * avoid invalidating a large portion of the squid/parser cache. - * - * This setting should factor in any squid/parser cache expiry settings. - */ -$wgMaxBacklinksInvalidate = false; - /** @} */ # End job queue } /************************************************************************//** @@ -6886,10 +7259,45 @@ $wgSiteTypes = array( ); /** - * Formerly a list of files for HipHop compilation - * @deprecated since 1.22 + * Whether the page_props table has a pp_sortkey column. Set to false in case + * the respective database schema change was not applied. + * @since 1.23 + */ +$wgPagePropsHaveSortkey = true; + +/** + * Port where you have HTTPS running + * Supports HTTPS on non-standard ports + * @see bug 65184 + * @since 1.24 + */ +$wgHttpsPort = 443; + +/** + * Secret and algorithm for hmac-based key derivation function (fast, + * cryptographically secure random numbers). + * This should be set in LocalSettings.php, otherwise wgSecretKey will + * be used. + * @since 1.24 + */ +$wgHKDFSecret = false; +$wgHKDFAlgorithm = 'sha256'; + +/** + * Enable page language feature + * Allows setting page language in database + * @var bool + * @since 1.24 + */ +$wgPageLanguageUseDB = false; + +/** + * Enable use of the *_namespace fields of the pagelinks, redirect, and templatelinks tables. + * Set this only if the fields are fully populated. This may be removed in 1.25. + * @var bool + * @since 1.24 */ -$wgCompiledFiles = array(); +$wgUseLinkNamespaceDBFields = true; /** * For really cool vim folding this needs to be at the end: diff --git a/includes/DeferredUpdates.php b/includes/DeferredUpdates.php deleted file mode 100644 index c385f138..00000000 --- a/includes/DeferredUpdates.php +++ /dev/null @@ -1,129 +0,0 @@ -<?php -/** - * Interface and manager for deferred updates. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Interface that deferrable updates should implement. Basically required so we - * can validate input on DeferredUpdates::addUpdate() - * - * @since 1.19 - */ -interface DeferrableUpdate { - /** - * Perform the actual work - */ - function doUpdate(); -} - -/** - * Class for managing the deferred updates. - * - * @since 1.19 - */ -class DeferredUpdates { - /** - * Store of updates to be deferred until the end of the request. - */ - private static $updates = array(); - - /** - * Add an update to the deferred list - * @param $update DeferrableUpdate Some object that implements doUpdate() - */ - public static function addUpdate( DeferrableUpdate $update ) { - array_push( self::$updates, $update ); - } - - /** - * HTMLCacheUpdates are the most common deferred update people use. This - * is a shortcut method for that. - * @see HTMLCacheUpdate::__construct() - * @param $title - * @param $table - */ - public static function addHTMLCacheUpdate( $title, $table ) { - self::addUpdate( new HTMLCacheUpdate( $title, $table ) ); - } - - /** - * Add a callable update. In a lot of cases, we just need a callback/closure, - * defining a new DeferrableUpdate object is not necessary - * @see MWCallableUpdate::__construct() - * @param callable $callable - */ - public static function addCallableUpdate( $callable ) { - self::addUpdate( new MWCallableUpdate( $callable ) ); - } - - /** - * Do any deferred updates and clear the list - * - * @param string $commit set to 'commit' to commit after every update to - * prevent lock contention - */ - public static function doUpdates( $commit = '' ) { - global $wgDeferredUpdateList; - - wfProfileIn( __METHOD__ ); - - $updates = array_merge( $wgDeferredUpdateList, self::$updates ); - - // No need to get master connections in case of empty updates array - if ( !count( $updates ) ) { - wfProfileOut( __METHOD__ ); - return; - } - - $doCommit = $commit == 'commit'; - if ( $doCommit ) { - $dbw = wfGetDB( DB_MASTER ); - } - - foreach ( $updates as $update ) { - try { - $update->doUpdate(); - - if ( $doCommit && $dbw->trxLevel() ) { - $dbw->commit( __METHOD__, 'flush' ); - } - } catch ( MWException $e ) { - // We don't want exceptions thrown during deferred updates to - // be reported to the user since the output is already sent. - // Instead we just log them. - if ( !$e instanceof ErrorPageError ) { - MWExceptionHandler::logException( $e ); - } - } - } - - self::clearPendingUpdates(); - wfProfileOut( __METHOD__ ); - } - - /** - * Clear all pending updates without performing them. Generally, you don't - * want or need to call this. Unit tests need it though. - */ - public static function clearPendingUpdates() { - global $wgDeferredUpdateList; - $wgDeferredUpdateList = self::$updates = array(); - } -} diff --git a/includes/Defines.php b/includes/Defines.php index 86c5520b..017e9ea4 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -59,7 +59,6 @@ define( 'DB_MASTER', -2 ); # Write to master (or only server) # Obsolete aliases define( 'DB_READ', -1 ); define( 'DB_WRITE', -2 ); -define( 'DB_LAST', -3 ); # deprecated since 2008, usage throws exception /**@{ * Virtual namespaces; don't appear in the page database @@ -113,23 +112,33 @@ define( 'CACHE_NONE', 0 ); // Do not cache define( 'CACHE_DB', 1 ); // Store cache objects in the DB define( 'CACHE_MEMCACHED', 2 ); // MemCached, must specify servers in $wgMemCacheServers define( 'CACHE_ACCEL', 3 ); // APC, XCache or WinCache -define( 'CACHE_DBA', 4 ); // Use PHP's DBA extension to store in a DBM-style database /**@}*/ /**@{ * Media types. * 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. -define( 'MEDIATYPE_DRAWING', 'DRAWING' ); // some vector drawing (SVG, WMF, PS, ...) or image source (oo-draw, etc). Can scale up. -define( 'MEDIATYPE_AUDIO', 'AUDIO' ); // simple audio file (ogg, mp3, wav, midi, whatever) -define( 'MEDIATYPE_VIDEO', 'VIDEO' ); // simple video file (ogg, mpg, etc; no not include formats here that may contain executable sections or scripts!) -define( 'MEDIATYPE_MULTIMEDIA', 'MULTIMEDIA' ); // Scriptable Multimedia (flash, advanced video container formats, etc) -define( 'MEDIATYPE_OFFICE', 'OFFICE' ); // Office Documents, Spreadsheets (office formats possibly containing apples, scripts, etc) -define( 'MEDIATYPE_TEXT', 'TEXT' ); // Plain text (possibly containing program code or scripts) -define( 'MEDIATYPE_EXECUTABLE', 'EXECUTABLE' ); // binary executable -define( 'MEDIATYPE_ARCHIVE', 'ARCHIVE' ); // archive file (zip, tar, etc) +// unknown format +define( 'MEDIATYPE_UNKNOWN', 'UNKNOWN' ); +// some bitmap image or image source (like psd, etc). Can't scale up. +define( 'MEDIATYPE_BITMAP', 'BITMAP' ); +// some vector drawing (SVG, WMF, PS, ...) or image source (oo-draw, etc). Can scale up. +define( 'MEDIATYPE_DRAWING', 'DRAWING' ); +// simple audio file (ogg, mp3, wav, midi, whatever) +define( 'MEDIATYPE_AUDIO', 'AUDIO' ); +// simple video file (ogg, mpg, etc; +// no not include formats here that may contain executable sections or scripts!) +define( 'MEDIATYPE_VIDEO', 'VIDEO' ); +// Scriptable Multimedia (flash, advanced video container formats, etc) +define( 'MEDIATYPE_MULTIMEDIA', 'MULTIMEDIA' ); +// Office Documents, Spreadsheets (office formats possibly containing apples, scripts, etc) +define( 'MEDIATYPE_OFFICE', 'OFFICE' ); +// Plain text (possibly containing program code or scripts) +define( 'MEDIATYPE_TEXT', 'TEXT' ); +// binary executable +define( 'MEDIATYPE_EXECUTABLE', 'EXECUTABLE' ); +// archive file (zip, tar, etc) +define( 'MEDIATYPE_ARCHIVE', 'ARCHIVE' ); /**@}*/ /**@{ @@ -155,11 +164,6 @@ define( 'ALF_NO_BLOCK_LOCK', 8 ); * Date format selectors; used in user preference storage and by * Language::date() and co. */ -/*define( 'MW_DATE_DEFAULT', '0' ); -define( 'MW_DATE_MDY', '1' ); -define( 'MW_DATE_DMY', '2' ); -define( 'MW_DATE_YMD', '3' ); -define( 'MW_DATE_ISO', 'ISO 8601' );*/ define( 'MW_DATE_DEFAULT', 'default' ); define( 'MW_DATE_MDY', 'mdy' ); define( 'MW_DATE_DMY', 'dmy' ); @@ -172,9 +176,7 @@ define( 'MW_DATE_ISO', 'ISO 8601' ); */ define( 'RC_EDIT', 0 ); define( 'RC_NEW', 1 ); -define( 'RC_MOVE', 2 ); // obsolete define( 'RC_LOG', 3 ); -define( 'RC_MOVE_OVER_REDIRECT', 4 ); // obsolete define( 'RC_EXTERNAL', 5 ); /**@}*/ @@ -279,6 +281,7 @@ define( 'CONTENT_MODEL_WIKITEXT', 'wikitext' ); define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' ); define( 'CONTENT_MODEL_CSS', 'css' ); define( 'CONTENT_MODEL_TEXT', 'text' ); +define( 'CONTENT_MODEL_JSON', 'json' ); /**@}*/ /**@{ @@ -288,12 +291,20 @@ define( 'CONTENT_MODEL_TEXT', 'text' ); * Extensions are free to use the below formats, or define their own. * It is recommended to stick with the conventions for MIME types. */ -define( 'CONTENT_FORMAT_WIKITEXT', 'text/x-wiki' ); // wikitext -define( 'CONTENT_FORMAT_JAVASCRIPT', 'text/javascript' ); // for js pages -define( 'CONTENT_FORMAT_CSS', 'text/css' ); // for css pages -define( 'CONTENT_FORMAT_TEXT', 'text/plain' ); // for future use, e.g. with some plain-html messages. -define( 'CONTENT_FORMAT_HTML', 'text/html' ); // for future use, e.g. with some plain-html messages. -define( 'CONTENT_FORMAT_SERIALIZED', 'application/vnd.php.serialized' ); // for future use with the api and for extensions -define( 'CONTENT_FORMAT_JSON', 'application/json' ); // for future use with the api, and for use by extensions -define( 'CONTENT_FORMAT_XML', 'application/xml' ); // for future use with the api, and for use by extensions +// wikitext +define( 'CONTENT_FORMAT_WIKITEXT', 'text/x-wiki' ); +// for js pages +define( 'CONTENT_FORMAT_JAVASCRIPT', 'text/javascript' ); +// for css pages +define( 'CONTENT_FORMAT_CSS', 'text/css' ); +// for future use, e.g. with some plain-html messages. +define( 'CONTENT_FORMAT_TEXT', 'text/plain' ); +// for future use, e.g. with some plain-html messages. +define( 'CONTENT_FORMAT_HTML', 'text/html' ); +// for future use with the api and for extensions +define( 'CONTENT_FORMAT_SERIALIZED', 'application/vnd.php.serialized' ); +// for future use with the api, and for use by extensions +define( 'CONTENT_FORMAT_JSON', 'application/json' ); +// for future use with the api, and for use by extensions +define( 'CONTENT_FORMAT_XML', 'application/xml' ); /**@}*/ diff --git a/includes/DeprecatedGlobal.php b/includes/DeprecatedGlobal.php index d48bd0b0..14329d32 100644 --- a/includes/DeprecatedGlobal.php +++ b/includes/DeprecatedGlobal.php @@ -23,20 +23,22 @@ /** * Class to allow throwing wfDeprecated warnings * when people use globals that we do not want them to. - * (For example like $wgArticle) */ class DeprecatedGlobal extends StubObject { - // The m's are to stay consistent with parent class. - protected $mRealValue, $mVersion; + protected $realValue, $version; function __construct( $name, $realValue, $version = false ) { parent::__construct( $name ); - $this->mRealValue = $realValue; - $this->mVersion = $version; + $this->realValue = $realValue; + $this->version = $version; } + // @codingStandardsIgnoreStart + // PSR2.Methods.MethodDeclaration.Underscore + // PSR2.Classes.PropertyDeclaration.ScopeMissing function _newObject() { + /* Put the caller offset for wfDeprecated as 6, as * that gives the function that uses this object, since: * 1 = this function ( _newObject ) @@ -49,7 +51,8 @@ class DeprecatedGlobal extends StubObject { * sequences for this method, but that seems to be * rather unlikely. */ - wfDeprecated( '$' . $this->mGlobal, $this->mVersion, false, 6 ); - return $this->mRealValue; + wfDeprecated( '$' . $this->global, $this->version, false, 6 ); + return $this->realValue; } + // @codingStandardsIgnoreEnd } diff --git a/includes/EditPage.php b/includes/EditPage.php index 4dd83845..128244a8 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -1,6 +1,6 @@ <?php /** - * Page edition user interface. + * User interface for page editing. * * 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 @@ -36,7 +36,6 @@ * headaches, which may be fatal. */ class EditPage { - /** * Status: Article successfully updated */ @@ -67,11 +66,6 @@ class EditPage { */ const AS_CONTENT_TOO_BIG = 216; - /** - * Status: User cannot edit? (not used) - */ - const AS_USER_CANNOT_EDIT = 217; - /** * Status: this anonymous user is not allowed to edit this page */ @@ -105,7 +99,7 @@ class EditPage { const AS_NO_CREATE_PERMISSION = 223; /** - * Status: user tried to create a blank page + * Status: user tried to create a blank page and wpIgnoreBlankArticle == false */ const AS_BLANK_ARTICLE = 224; @@ -130,11 +124,6 @@ class EditPage { */ const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229; - /** - * not used - */ - const AS_OK = 230; - /** * Status: WikiPage::doEdit() was unsuccessful */ @@ -182,8 +171,9 @@ class EditPage { * The cookie will be removed instantly if the JavaScript runs. * * Otherwise, though, we don't want the cookies to accumulate. - * RFC 2109 ( https://www.ietf.org/rfc/rfc2109.txt ) specifies a possible limit of only 20 cookies per domain. - * This still applies at least to some versions of IE without full updates: + * RFC 2109 ( https://www.ietf.org/rfc/rfc2109.txt ) specifies a possible + * limit of only 20 cookies per domain. This still applies at least to some + * versions of IE without full updates: * https://blogs.msdn.com/b/ieinternals/archive/2009/08/20/wininet-ie-cookie-internals-faq.aspx * * A value of 20 minutes should be enough to take into account slow loads and minor @@ -191,65 +181,166 @@ class EditPage { */ const POST_EDIT_COOKIE_DURATION = 1200; - /** - * @var Article - */ - var $mArticle; + /** @var Article */ + public $mArticle; - /** - * @var Title - */ - var $mTitle; + /** @var Title */ + public $mTitle; + + /** @var null|Title */ private $mContextTitle = null; - var $action = 'submit'; - var $isConflict = false; - var $isCssJsSubpage = false; - var $isCssSubpage = false; - var $isJsSubpage = 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; - var $missingSummary = false; - var $allowBlankSummary = false; - var $autoSumm = ''; - var $hookError = ''; - #var $mPreviewTemplates; - - /** - * @var ParserOutput - */ - var $mParserOutput; - - /** - * Has a summary been preset using GET parameter &summary= ? - * @var Bool - */ - var $hasPresetSummary = false; - - var $mBaseRevision = false; - var $mShowSummaryField = true; + + /** @var string */ + public $action = 'submit'; + + /** @var bool */ + public $isConflict = false; + + /** @var bool */ + public $isCssJsSubpage = false; + + /** @var bool */ + public $isCssSubpage = false; + + /** @var bool */ + public $isJsSubpage = false; + + /** @var bool */ + public $isWrongCaseCssJsPage = false; + + /** @var bool New page or new section */ + public $isNew = false; + + /** @var bool */ + public $deletedSinceEdit; + + /** @var string */ + public $formtype; + + /** @var bool */ + public $firsttime; + + /** @var bool|stdClass */ + public $lastDelete; + + /** @var bool */ + public $mTokenOk = false; + + /** @var bool */ + public $mTokenOkExceptSuffix = false; + + /** @var bool */ + public $mTriedSave = false; + + /** @var bool */ + public $incompleteForm = false; + + /** @var bool */ + public $tooBig = false; + + /** @var bool */ + public $kblength = false; + + /** @var bool */ + public $missingComment = false; + + /** @var bool */ + public $missingSummary = false; + + /** @var bool */ + public $allowBlankSummary = false; + + /** @var bool */ + protected $blankArticle = false; + + /** @var bool */ + protected $allowBlankArticle = false; + + /** @var string */ + public $autoSumm = ''; + + /** @var string */ + public $hookError = ''; + + /** @var ParserOutput */ + public $mParserOutput; + + /** @var bool Has a summary been preset using GET parameter &summary= ? */ + public $hasPresetSummary = false; + + /** @var bool */ + public $mBaseRevision = false; + + /** @var bool */ + public $mShowSummaryField = true; # Form values - var $save = false, $preview = false, $diff = false; - var $minoredit = false, $watchthis = false, $recreate = false; - var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false; - var $edittime = '', $section = '', $sectiontitle = '', $starttime = ''; - var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true; - var $contentModel = null, $contentFormat = null; + + /** @var bool */ + public $save = false; + + /** @var bool */ + public $preview = false; + + /** @var bool */ + public $diff = false; + + /** @var bool */ + public $minoredit = false; + + /** @var bool */ + public $watchthis = false; + + /** @var bool */ + public $recreate = false; + + /** @var string */ + public $textbox1 = ''; + + /** @var string */ + public $textbox2 = ''; + + /** @var string */ + public $summary = ''; + + /** @var bool */ + public $nosummary = false; + + /** @var string */ + public $edittime = ''; + + /** @var string */ + public $section = ''; + + /** @var string */ + public $sectiontitle = ''; + + /** @var string */ + public $starttime = ''; + + /** @var int */ + public $oldid = 0; + + /** @var string */ + public $editintro = ''; + + /** @var null */ + public $scrolltop = null; + + /** @var bool */ + public $bot = true; + + /** @var null|string */ + public $contentModel = null; + + /** @var null|string */ + public $contentFormat = null; # Placeholders for text injection by hooks (must be HTML) # extensions should take care to _append_ to the present value - public $editFormPageTop = ''; // Before even the preview + + /** @var string Before even the preview */ + public $editFormPageTop = ''; public $editFormTextTop = ''; public $editFormTextBeforeContent = ''; public $editFormTextAfterWarn = ''; @@ -265,15 +356,17 @@ class EditPage { public $suppressIntro = false; - /** - * Set to true to allow editing of non-text content types. - * - * @var bool - */ + /** @var bool Set to true to allow editing of non-text content types. */ public $allowNonTextContent = false; + /** @var bool */ + protected $edit; + + /** @var bool */ + public $live; + /** - * @param $article Article + * @param Article $article */ public function __construct( Article $article ) { $this->mArticle = $article; @@ -303,7 +396,7 @@ class EditPage { /** * Set the context Title object * - * @param $title Title object or null + * @param Title|null $title Title object or null */ public function setContextTitle( $title ) { $this->mContextTitle = $title; @@ -314,7 +407,7 @@ class EditPage { * If not set, $wgTitle will be returned. This behavior might change in * the future to return $this->mTitle instead. * - * @return Title object + * @return Title */ public function getContextTitle() { if ( is_null( $this->mContextTitle ) ) { @@ -325,6 +418,18 @@ class EditPage { } } + /** + * Returns if the given content model is editable. + * + * @param string $modelId The ID of the content model to test. Use CONTENT_MODEL_XXX constants. + * @return bool + * @throws MWException If $modelId has no known handler + */ + public function isSupportedContentModel( $modelId ) { + return $this->allowNonTextContent || + ContentHandler::getForModelID( $modelId ) instanceof TextContentHandler; + } + function submit() { $this->edit(); } @@ -406,6 +511,7 @@ class EditPage { $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage(); $this->isCssSubpage = $this->mTitle->isCssSubpage(); $this->isJsSubpage = $this->mTitle->isJsSubpage(); + // @todo FIXME: Silly assignment. $this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage(); # Show applicable editing introductions @@ -463,9 +569,9 @@ class EditPage { # Ignore some permissions errors when a user is just previewing/viewing diffs $remove = array(); foreach ( $permErrors as $error ) { - if ( ( $this->preview || $this->diff ) && - ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' ) ) - { + if ( ( $this->preview || $this->diff ) + && ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' ) + ) { $remove[] = $error; } } @@ -482,8 +588,8 @@ class EditPage { * "View source for ..." page displaying the source code after the error message. * * @since 1.19 - * @param array $permErrors of permissions errors, as returned by - * Title::getUserPermissionsErrors(). + * @param array $permErrors Array of permissions errors, as returned by + * Title::getUserPermissionsErrors(). * @throws PermissionsError */ protected function displayPermissionsError( array $permErrors ) { @@ -506,8 +612,13 @@ class EditPage { throw new PermissionsError( $action, $permErrors ); } + wfRunHooks( 'EditPage::showReadOnlyForm:initial', array( $this, &$wgOut ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->setPageTitle( wfMessage( 'viewsource-title', $this->getContextTitle()->getPrefixedText() ) ); + $wgOut->setPageTitle( wfMessage( + 'viewsource-title', + $this->getContextTitle()->getPrefixedText() + ) ); $wgOut->addBacklinkSubtitle( $this->getContextTitle() ); $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' ) ); $wgOut->addHTML( "<hr />\n" ); @@ -534,26 +645,6 @@ class EditPage { } } - /** - * Show a read-only error - * Parameters are the same as OutputPage:readOnlyPage() - * Redirect to the article page if redlink=1 - * @deprecated in 1.19; use displayPermissionsError() instead - */ - function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) { - wfDeprecated( __METHOD__, '1.19' ); - - global $wgRequest, $wgOut; - if ( $wgRequest->getBool( 'redlink' ) ) { - // The edit page was reached via a red link. - // Redirect to the article page and let them click the edit tab if - // they really want a permission error. - $wgOut->redirect( $this->mTitle->getFullURL() ); - } else { - $wgOut->readOnlyPage( $source, $protected, $reasons, $action ); - } - } - /** * Should we show a preview when the edit form is first shown? * @@ -570,13 +661,15 @@ class EditPage { } elseif ( $this->section == 'new' ) { // Nothing *to* preview for new sections return false; - } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) { + } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() ) + && $wgUser->getOption( 'previewonfirst' ) + ) { // Standard preference behavior return true; - } elseif ( !$this->mTitle->exists() && - isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] ) && - $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] ) - { + } elseif ( !$this->mTitle->exists() + && isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] ) + && $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] + ) { // Categories are special return true; } else { @@ -609,7 +702,7 @@ class EditPage { * Subclasses may override this to replace the default behavior, which is * to check ContentHandler::supportsSections. * - * @return bool true if this edit page supports sections, false otherwise. + * @return bool True if this edit page supports sections, false otherwise. */ protected function isSectionEditSupported() { $contentHandler = ContentHandler::getForTitle( $this->mTitle ); @@ -618,7 +711,8 @@ class EditPage { /** * This function collects the form data and uses it to populate various member variables. - * @param $request WebRequest + * @param WebRequest $request + * @throws ErrorPageError */ function importFormData( &$request ) { global $wgContLang, $wgUser; @@ -685,9 +779,13 @@ class EditPage { // 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 we receive the last parameter of the request, we can fairly + // claim the POST request has not been truncated. + + // TODO: softened the check for cutover. Once we determine + // that it is safe, we should complete the transition by + // removing the "edittime" clause. + $this->incompleteForm = ( !$request->getVal( 'wpUltimateParam' ) && is_null( $this->edittime ) ); } if ( $this->incompleteForm ) { # If the form is incomplete, force to preview. @@ -734,15 +832,18 @@ class EditPage { $this->watchthis = $request->getCheck( 'wpWatchthis' ); # Don't force edit summaries when a user is editing their own user or talk page - if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) && - $this->mTitle->getText() == $wgUser->getName() ) - { + if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) + && $this->mTitle->getText() == $wgUser->getName() + ) { $this->allowBlankSummary = true; } else { - $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ) || !$wgUser->getOption( 'forceeditsummary' ); + $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ) + || !$wgUser->getOption( 'forceeditsummary' ); } $this->autoSumm = $request->getText( 'wpAutoSummary' ); + + $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' ); } else { # Not a posted form? Start with nothing. wfDebug( __METHOD__ . ": Not a posted form.\n" ); @@ -756,7 +857,8 @@ class EditPage { $this->save = false; $this->diff = false; $this->minoredit = false; - $this->watchthis = $request->getBool( 'watchthis', false ); // Watch may be overridden by request parameters + // Watch may be overridden by request parameters + $this->watchthis = $request->getBool( 'watchthis', false ); $this->recreate = false; // When creating a new section, we can preload a section title by passing it as the @@ -765,8 +867,7 @@ class EditPage { $this->sectiontitle = $request->getVal( 'preloadtitle' ); // Once wpSummary isn't being use for setting section titles, we should delete this. $this->summary = $request->getVal( 'preloadtitle' ); - } - elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) { + } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) { $this->summary = $request->getText( 'summary' ); if ( $this->summary !== '' ) { $this->hasPresetSummary = true; @@ -783,12 +884,26 @@ class EditPage { $this->bot = $request->getBool( 'bot', true ); $this->nosummary = $request->getBool( 'nosummary' ); - $content_handler = ContentHandler::getForTitle( $this->mTitle ); - $this->contentModel = $request->getText( 'model', $content_handler->getModelID() ); #may be overridden by revision - $this->contentFormat = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision + // May be overridden by revision. + $this->contentModel = $request->getText( 'model', $this->contentModel ); + // May be overridden by revision. + $this->contentFormat = $request->getText( 'format', $this->contentFormat ); - #TODO: check if the desired model is allowed in this namespace, and if a transition from the page's current model to the new model is allowed - #TODO: check if the desired content model supports the given content format! + if ( !ContentHandler::getForModelID( $this->contentModel ) + ->isSupportedFormat( $this->contentFormat ) + ) { + throw new ErrorPageError( + 'editpage-notsupportedcontentformat-title', + 'editpage-notsupportedcontentformat-text', + array( $this->contentFormat, ContentHandler::getLocalizedName( $this->contentModel ) ) + ); + } + + /** + * @todo Check if the desired model is allowed in this namespace, and if + * a transition from the page's current model to the new model is + * allowed. + */ $this->live = $request->getCheck( 'live' ); $this->editintro = $request->getText( 'editintro', @@ -807,7 +922,7 @@ class EditPage { * this method should be overridden and return the page text that will be used * for saving, preview parsing and so on... * - * @param $request WebRequest + * @param WebRequest $request */ protected function importContentFormData( &$request ) { return; // Don't do anything, EditPage already extracted wpTextbox1 @@ -816,7 +931,7 @@ class EditPage { /** * Initialise form fields in the object * Called on the first invocation, e.g. when a user clicks an edit link - * @return bool -- if the requested section is valid + * @return bool If the requested section is valid */ function initialiseForm() { global $wgUser; @@ -849,38 +964,15 @@ class EditPage { return true; } - /** - * Fetch initial editing page content. - * - * @param $def_text string|bool - * @return mixed string on success, $def_text for invalid sections - * @private - * @deprecated since 1.21, get WikiPage::getContent() instead. - */ - function getContent( $def_text = false ) { - ContentHandler::deprecated( __METHOD__, '1.21' ); - - if ( $def_text !== null && $def_text !== false && $def_text !== '' ) { - $def_content = $this->toEditContent( $def_text ); - } else { - $def_content = false; - } - - $content = $this->getContentObject( $def_content ); - - // Note: EditPage should only be used with text based content anyway. - return $this->toEditText( $content ); - } - /** * @param Content|null $def_content The default value to return * - * @return mixed Content on success, $def_content for invalid sections + * @return Content|null Content on success, $def_content for invalid sections * * @since 1.21 */ protected function getContentObject( $def_content = null ) { - global $wgOut, $wgRequest; + global $wgOut, $wgRequest, $wgUser, $wgContLang; wfProfileIn( __METHOD__ ); @@ -900,14 +992,15 @@ class EditPage { $preload = $wgRequest->getVal( 'preload', // Custom preload text for new sections $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' ); + $params = $wgRequest->getArray( 'preloadparams', array() ); - $content = $this->getPreloadedContent( $preload ); + $content = $this->getPreloadedContent( $preload, $params ); } // For existing pages, get text based on "undo" or section parameters. } else { if ( $this->section != '' ) { // Get section edit text (returns $def_text for invalid sections) - $orig = $this->getOriginalContent(); + $orig = $this->getOriginalContent( $wgUser ); $content = $orig ? $orig->getSection( $this->section ) : null; if ( !$content ) { @@ -918,10 +1011,6 @@ class EditPage { $undo = $wgRequest->getInt( 'undo' ); if ( $undo > 0 && $undoafter > 0 ) { - if ( $undo < $undoafter ) { - # If they got undoafter and undo round the wrong way, switch them - list( $undo, $undoafter ) = array( $undoafter, $undo ); - } $undorev = Revision::newFromId( $undo ); $oldrev = Revision::newFromId( $undoafter ); @@ -930,8 +1019,6 @@ class EditPage { # the revisions exist and they were not deleted. # Otherwise, $content will be left as-is. if ( !is_null( $undorev ) && !is_null( $oldrev ) && - $undorev->getPage() == $oldrev->getPage() && - $undorev->getPage() == $this->mTitle->getArticleID() && !$undorev->isDeleted( Revision::DELETED_TEXT ) && !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { @@ -941,34 +1028,45 @@ class EditPage { # Warn the user that something went wrong $undoMsg = 'failure'; } else { - # Inform the user of our success and set an automatic edit summary - $undoMsg = 'success'; - - # If we just undid one rev, use an autosummary - $firstrev = $oldrev->getNext(); - if ( $firstrev && $firstrev->getId() == $undo ) { - $userText = $undorev->getUserText(); - if ( $userText === '' ) { - $undoSummary = wfMessage( - 'undo-summary-username-hidden', - $undo - )->inContentLanguage()->text(); - } else { - $undoSummary = wfMessage( - 'undo-summary', - $undo, - $userText - )->inContentLanguage()->text(); - } - if ( $this->summary === '' ) { - $this->summary = $undoSummary; - } else { - $this->summary = $undoSummary . wfMessage( 'colon-separator' ) - ->inContentLanguage()->text() . $this->summary; + $oldContent = $this->mArticle->getPage()->getContent( Revision::RAW ); + $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang ); + $newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts ); + + if ( $newContent->equals( $oldContent ) ) { + # Tell the user that the undo results in no change, + # i.e. the revisions were already undone. + $undoMsg = 'nochange'; + $content = false; + } else { + # Inform the user of our success and set an automatic edit summary + $undoMsg = 'success'; + + # If we just undid one rev, use an autosummary + $firstrev = $oldrev->getNext(); + if ( $firstrev && $firstrev->getId() == $undo ) { + $userText = $undorev->getUserText(); + if ( $userText === '' ) { + $undoSummary = wfMessage( + 'undo-summary-username-hidden', + $undo + )->inContentLanguage()->text(); + } else { + $undoSummary = wfMessage( + 'undo-summary', + $undo, + $userText + )->inContentLanguage()->text(); + } + if ( $this->summary === '' ) { + $this->summary = $undoSummary; + } else { + $this->summary = $undoSummary . wfMessage( 'colon-separator' ) + ->inContentLanguage()->text() . $this->summary; + } + $this->undidRev = $undo; } - $this->undidRev = $undo; + $this->formtype = 'diff'; } - $this->formtype = 'diff'; } } else { // Failed basic sanity checks. @@ -977,14 +1075,14 @@ class EditPage { $undoMsg = 'norev'; } - // Messages: undo-success, undo-failure, undo-norev + // Messages: undo-success, undo-failure, undo-norev, undo-nochange $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}"; $this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" . wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true ); } if ( $content === false ) { - $content = $this->getOriginalContent(); + $content = $this->getOriginalContent( $wgUser ); } } } @@ -1005,9 +1103,10 @@ class EditPage { * 'missing-revision' message. * * @since 1.19 + * @param User $user The user to get the revision for * @return Content|null */ - private function getOriginalContent() { + private function getOriginalContent( User $user ) { if ( $this->section == 'new' ) { return $this->getCurrentContent(); } @@ -1020,7 +1119,7 @@ class EditPage { return $handler->makeEmptyContent(); } - $content = $revision->getContent(); + $content = $revision->getContent( Revision::FOR_THIS_USER, $user ); return $content; } @@ -1052,24 +1151,10 @@ class EditPage { } } - /** - * Use this method before edit() to preload some text into the edit box - * - * @param $text string - * @deprecated since 1.21, use setPreloadedContent() instead. - */ - public function setPreloadedText( $text ) { - ContentHandler::deprecated( __METHOD__, "1.21" ); - - $content = $this->toEditContent( $text ); - - $this->setPreloadedContent( $content ); - } - /** * Use this method before edit() to preload some content into the edit box * - * @param $content Content + * @param Content $content * * @since 1.21 */ @@ -1081,32 +1166,14 @@ class EditPage { * Get the contents to be preloaded into the box, either set by * an earlier setPreloadText() or by loading the given page. * - * @param string $preload representing the title to preload from. - * - * @return String - * - * @deprecated since 1.21, use getPreloadedContent() instead - */ - protected function getPreloadedText( $preload ) { - ContentHandler::deprecated( __METHOD__, "1.21" ); - - $content = $this->getPreloadedContent( $preload ); - $text = $this->toEditText( $content ); - - return $text; - } - - /** - * Get the contents to be preloaded into the box, either set by - * an earlier setPreloadText() or by loading the given page. - * - * @param string $preload representing the title to preload from. + * @param string $preload Representing the title to preload from. + * @param array $params Parameters to use (interface-message style) in the preloaded text * * @return Content * * @since 1.21 */ - protected function getPreloadedContent( $preload ) { + protected function getPreloadedContent( $preload, $params = array() ) { global $wgUser; if ( !empty( $this->mPreloadContent ) ) { @@ -1160,13 +1227,13 @@ class EditPage { $content = $converted; } - return $content->preloadTransform( $title, $parserOptions ); + return $content->preloadTransform( $title, $parserOptions, $params ); } /** * Make sure the form isn't faking a user's credentials. * - * @param $request WebRequest + * @param WebRequest $request * @return bool * @private */ @@ -1189,18 +1256,24 @@ class EditPage { * marked HttpOnly. The JavaScript code converts the cookie to a wgPostEdit config * variable. * - * We use a path of '/' since wgCookiePath is not exposed to JS - * * If the variable were set on the server, it would be cached, which is unwanted * since the post-edit state should only apply to the load right after the save. + * + * @param int $statusValue The status value (to check for new article status) */ - protected function setPostEditCookie() { + protected function setPostEditCookie( $statusValue ) { $revisionId = $this->mArticle->getLatest(); $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId; + $val = 'saved'; + if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) { + $val = 'created'; + } elseif ( $this->oldid ) { + $val = 'restored'; + } + $response = RequestContext::getMain()->getRequest()->response(); - $response->setcookie( $postEditKey, '1', time() + self::POST_EDIT_COOKIE_DURATION, array( - 'path' => '/', + $response->setcookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, array( 'httpOnly' => false, ) ); } @@ -1208,20 +1281,41 @@ class EditPage { /** * Attempt submission * @throws UserBlockedError|ReadOnlyError|ThrottledError|PermissionsError - * @return bool false if output is done, true if the rest of the form should be displayed + * @return bool False if output is done, true if the rest of the form should be displayed */ - function attemptSave() { - global $wgUser, $wgOut; + public function attemptSave() { + global $wgUser; $resultDetails = false; # Allow bots to exempt some edits from bot flagging $bot = $wgUser->isAllowed( 'bot' ) && $this->bot; $status = $this->internalAttemptSave( $resultDetails, $bot ); - // FIXME: once the interface for internalAttemptSave() is made nicer, this should use the message in $status - if ( $status->value == self::AS_SUCCESS_UPDATE || $status->value == self::AS_SUCCESS_NEW_ARTICLE ) { + + return $this->handleStatus( $status, $resultDetails ); + } + + /** + * Handle status, such as after attempt save + * + * @param Status $status + * @param array|bool $resultDetails + * + * @throws ErrorPageError + * @return bool False, if output is done, true if rest of the form should be displayed + */ + private function handleStatus( Status $status, $resultDetails ) { + global $wgUser, $wgOut; + + /** + * @todo FIXME: once the interface for internalAttemptSave() is made + * nicer, this should use the message in $status + */ + if ( $status->value == self::AS_SUCCESS_UPDATE + || $status->value == self::AS_SUCCESS_NEW_ARTICLE + ) { $this->didSave = true; if ( !$resultDetails['nullEdit'] ) { - $this->setPostEditCookie(); + $this->setPostEditCookie( $status->value ); } } @@ -1234,6 +1328,7 @@ class EditPage { case self::AS_TEXTBOX_EMPTY: case self::AS_MAX_ARTICLE_SIZE_EXCEEDED: case self::AS_END: + case self::AS_BLANK_ARTICLE: return true; case self::AS_HOOK_ERROR: @@ -1254,7 +1349,10 @@ class EditPage { $sectionanchor = $resultDetails['sectionanchor']; // Give extensions a chance to modify URL query on update - wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this->mArticle, &$sectionanchor, &$extraQuery ) ); + wfRunHooks( + 'ArticleUpdateBeforeRedirect', + array( $this->mArticle, &$sectionanchor, &$extraQuery ) + ); if ( $resultDetails['redirect'] ) { if ( $extraQuery == '' ) { @@ -1266,10 +1364,6 @@ class EditPage { $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor ); return false; - case self::AS_BLANK_ARTICLE: - $wgOut->redirect( $this->getContextTitle()->getFullURL() ); - return false; - case self::AS_SPAM_ERROR: $this->spamPageWithContent( $resultDetails['spam'] ); return false; @@ -1312,9 +1406,9 @@ class EditPage { /** * Run hooks that can filter edits just before they get saved. * - * @param Content $content the Content to filter. - * @param Status $status for reporting the outcome to the caller - * @param User $user the user performing the edit + * @param Content $content The Content to filter. + * @param Status $status For reporting the outcome to the caller + * @param User $user The user performing the edit * * @return bool */ @@ -1357,21 +1451,59 @@ class EditPage { return true; } + /** + * Return the summary to be used for a new section. + * + * @param string $sectionanchor Set to the section anchor text + * @return string + */ + private function newSectionSummary( &$sectionanchor = null ) { + global $wgParser; + + if ( $this->sectiontitle !== '' ) { + $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); + // If no edit summary was specified, create one automatically from the section + // title and have it link to the new section. Otherwise, respect the summary as + // passed. + if ( $this->summary === '' ) { + $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); + return wfMessage( 'newsectionsummary' ) + ->rawParams( $cleanSectionTitle )->inContentLanguage()->text(); + } + } elseif ( $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 ); + return wfMessage( 'newsectionsummary' ) + ->rawParams( $cleanSummary )->inContentLanguage()->text(); + } + return $this->summary; + } + /** * Attempt submission (no UI) * - * @param array $result array to add statuses to, currently with the possible keys: - * spam - string - Spam string from content if any spam is detected by matchSpamRegex - * sectionanchor - string - Section anchor for a section save - * nullEdit - boolean - Set if doEditContent is OK. True if null edit, false otherwise. - * redirect - boolean - Set if doEditContent is OK. True if resulting revision is a redirect + * @param array $result Array to add statuses to, currently with the + * possible keys: + * - spam (string): Spam string from content if any spam is detected by + * matchSpamRegex. + * - sectionanchor (string): Section anchor for a section save. + * - nullEdit (boolean): Set if doEditContent is OK. True if null edit, + * false otherwise. + * - redirect (bool): Set if doEditContent is OK. True if resulting + * revision is a redirect. * @param bool $bot True if edit is being made under the bot right. * - * @return Status object, possibly with a message, but always with one of the AS_* constants in $status->value, + * @return Status 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. + * @todo 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 $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize; @@ -1412,7 +1544,12 @@ class EditPage { # Construct Content object $textbox_content = $this->toEditContent( $this->textbox1 ); } catch ( MWContentSerializationException $ex ) { - $status->fatal( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() ); + $status->fatal( + 'content-failed-to-parse', + $this->contentModel, + $this->contentFormat, + $ex->getMessage() + ); $status->value = self::AS_PARSE_ERROR; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); @@ -1422,7 +1559,8 @@ class EditPage { # Check image redirect if ( $this->mTitle->getNamespace() == NS_FILE && $textbox_content->isRedirect() && - !$wgUser->isAllowed( 'upload' ) ) { + !$wgUser->isAllowed( 'upload' ) + ) { $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; $status->setResult( false, $code ); @@ -1461,7 +1599,10 @@ class EditPage { wfProfileOut( __METHOD__ ); return $status; } - if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ) ) ) { + 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; @@ -1572,7 +1713,9 @@ class EditPage { $defaultText = ''; } - if ( $this->textbox1 === $defaultText ) { + if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) { + $this->blankArticle = true; + $status->fatal( 'blankarticle' ); $status->setResult( false, self::AS_BLANK_ARTICLE ); wfProfileOut( __METHOD__ ); return $status; @@ -1590,30 +1733,11 @@ class EditPage { if ( $this->sectiontitle !== '' ) { // Insert the section title above the content. $content = $content->addSectionHeader( $this->sectiontitle ); - - // Jump to the new section - $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); - - // If no edit summary was specified, create one automatically from the section - // title and have it link to the new section. Otherwise, respect the summary as - // passed. - if ( $this->summary === '' ) { - $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); - $this->summary = wfMessage( 'newsectionsummary' ) - ->rawParams( $cleanSectionTitle )->inContentLanguage()->text(); - } } elseif ( $this->summary !== '' ) { // Insert the section title above the content. $content = $content->addSectionHeader( $this->summary ); - - // Jump to the new section - $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); - - // Create a link to the new section from the edit summary. - $cleanSummary = $wgParser->stripSectionName( $this->summary ); - $this->summary = wfMessage( 'newsectionsummary' ) - ->rawParams( $cleanSummary )->inContentLanguage()->text(); } + $this->summary = $this->newSectionSummary( $result['sectionanchor'] ); } $status->value = self::AS_SUCCESS_NEW_ARTICLE; @@ -1631,18 +1755,24 @@ class EditPage { $this->isConflict = true; if ( $this->section == 'new' ) { if ( $this->mArticle->getUserText() == $wgUser->getName() && - $this->mArticle->getComment() == $this->summary ) { + $this->mArticle->getComment() == $this->newSectionSummary() + ) { // 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" ); + 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" ); } - } elseif ( $this->section == '' && Revision::userWasLastToEdit( DB_MASTER, $this->mTitle->getArticleID(), - $wgUser->getId(), $this->edittime ) ) { + } elseif ( $this->section == '' + && Revision::userWasLastToEdit( + DB_MASTER, $this->mTitle->getArticleID(), + $wgUser->getId(), $this->edittime + ) + ) { # Suppress edit conflict with self, except for section edits where merging is required. wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); $this->isConflict = false; @@ -1659,13 +1789,23 @@ class EditPage { $content = null; if ( $this->isConflict ) { - wfDebug( __METHOD__ . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'" - . " (article time '{$timestamp}')\n" ); - - $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime ); + wfDebug( __METHOD__ + . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'" + . " (article time '{$timestamp}')\n" ); + + $content = $this->mArticle->replaceSectionContent( + $this->section, + $textbox_content, + $sectionTitle, + $this->edittime + ); } else { wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" ); - $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle ); + $content = $this->mArticle->replaceSectionContent( + $this->section, + $textbox_content, + $sectionTitle + ); } if ( is_null( $content ) ) { @@ -1715,7 +1855,7 @@ class EditPage { return $status; } } elseif ( !$this->allowBlankSummary - && !$content->equals( $this->getOriginalContent() ) + && !$content->equals( $this->getOriginalContent( $wgUser ) ) && !$content->isRedirect() && md5( $this->summary ) == $this->autoSumm ) { @@ -1730,31 +1870,15 @@ class EditPage { wfProfileIn( __METHOD__ . '-sectionanchor' ); $sectionanchor = ''; if ( $this->section == 'new' ) { - if ( $this->sectiontitle !== '' ) { - $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); - // If no edit summary was specified, create one automatically from the section - // title and have it link to the new section. Otherwise, respect the summary as - // passed. - if ( $this->summary === '' ) { - $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); - $this->summary = wfMessage( 'newsectionsummary' ) - ->rawParams( $cleanSectionTitle )->inContentLanguage()->text(); - } - } elseif ( $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 = wfMessage( 'newsectionsummary' ) - ->rawParams( $cleanSummary )->inContentLanguage()->text(); - } + $this->summary = $this->newSectionSummary( $sectionanchor ); } 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 + # 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 + # 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] ); } @@ -1773,7 +1897,7 @@ class EditPage { } // Check for length errors again now that the section is merged in - $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 ); + $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 ); if ( $this->kblength > $wgMaxArticleSize ) { $this->tooBig = true; $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED ); @@ -1786,17 +1910,23 @@ class EditPage { ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) | ( $bot ? EDIT_FORCE_BOT : 0 ); - $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags, - false, null, $this->contentFormat ); + $doEditStatus = $this->mArticle->doEditContent( + $content, + $this->summary, + $flags, + false, + null, + $content->getDefaultFormat() + ); if ( !$doEditStatus->isOK() ) { // Failure from doEdit() // Show the edit conflict page for certain recognized errors from doEdit(), // but don't show it for errors from extension hooks $errors = $doEditStatus->getErrorsArray(); - if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict', - 'edit-already-exists' ) ) ) - { + if ( in_array( $errors[0][0], + array( 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ) ) + ) { $this->isConflict = true; // Destroys data doEdit() put in $status->value but who cares $doEditStatus->value = self::AS_END; @@ -1831,36 +1961,12 @@ class EditPage { // Do this in its own transaction to reduce contention... $dbw = wfGetDB( DB_MASTER ); - $dbw->onTransactionIdle( function() use ( $dbw, $title, $watch, $wgUser, $fname ) { - $dbw->begin( $fname ); + $dbw->onTransactionIdle( function () use ( $dbw, $title, $watch, $wgUser, $fname ) { WatchAction::doWatchOrUnwatch( $watch, $title, $wgUser ); - $dbw->commit( $fname ); } ); } } - /** - * Attempts to merge text content with base and current revisions - * - * @param $editText string - * - * @return bool - * @deprecated since 1.21, use mergeChangesIntoContent() instead - */ - function mergeChangesInto( &$editText ) { - ContentHandler::deprecated( __METHOD__, "1.21" ); - - $editContent = $this->toEditContent( $editText ); - - $ok = $this->mergeChangesIntoContent( $editContent ); - - if ( $ok ) { - $editText = $this->toEditText( $editContent ); - return true; - } - return false; - } - /** * Attempts to do 3-way merge of edit content with a base revision * and current content, in case of edit conflict, in whichever way appropriate @@ -1868,7 +1974,7 @@ class EditPage { * * @since 1.21 * - * @param $editContent + * @param Content $editContent * * @return bool */ @@ -1915,20 +2021,18 @@ class EditPage { function getBaseRevision() { if ( !$this->mBaseRevision ) { $db = wfGetDB( DB_MASTER ); - $baseRevision = Revision::loadFromTimestamp( + $this->mBaseRevision = Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime ); - return $this->mBaseRevision = $baseRevision; - } else { - return $this->mBaseRevision; } + return $this->mBaseRevision; } /** * Check given input text against $wgSpamRegex, and return the text of the first match. * - * @param $text string + * @param string $text * - * @return string|bool matching string or false + * @return string|bool Matching string or false */ public static function matchSpamRegex( $text ) { global $wgSpamRegex; @@ -1940,9 +2044,9 @@ class EditPage { /** * Check given input text against $wgSummarySpamRegex, and return the text of the first match. * - * @param $text string + * @param string $text * - * @return string|bool matching string or false + * @return string|bool Matching string or false */ public static function matchSummarySpamRegex( $text ) { global $wgSummarySpamRegex; @@ -1951,8 +2055,8 @@ class EditPage { } /** - * @param $text string - * @param $regexes array + * @param string $text + * @param array $regexes * @return bool|string */ protected static function matchSpamRegexInternal( $text, $regexes ) { @@ -1979,9 +2083,6 @@ class EditPage { $wgOut->addModules( 'mediawiki.action.edit.editWarning' ); } - // Bug #19334: textarea jumps when editing articles in IE8 - $wgOut->addStyle( 'common/IE80Fixes.css', 'screen', 'IE 8' ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); # Enabled article-related sidebar, toplinks, etc. @@ -1993,9 +2094,14 @@ class EditPage { } elseif ( $contextTitle->exists() && $this->section != '' ) { $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection'; } else { - $msg = $contextTitle->exists() || ( $contextTitle->getNamespace() == NS_MEDIAWIKI && $contextTitle->getDefaultMessageText() !== false ) ? - 'editing' : 'creating'; + $msg = $contextTitle->exists() + || ( $contextTitle->getNamespace() == NS_MEDIAWIKI + && $contextTitle->getDefaultMessageText() !== false + ) + ? 'editing' + : 'creating'; } + # Use the title defined by DISPLAYTITLE magic word when present $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false; if ( $displayTitle === false ) { @@ -2045,14 +2151,15 @@ class EditPage { $username = $parts[0]; $user = User::newFromName( $username, false /* allow IP users*/ ); $ip = User::isIP( $username ); + $block = Block::newFromTarget( $user, $user ); if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>", array( 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ) ); - } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked + } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) { # Show log extract if the user is currently blocked LogEventsList::showLogExtract( $wgOut, 'block', - $user->getUserPage(), + MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(), '', array( 'lim' => 1, @@ -2115,7 +2222,7 @@ class EditPage { if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) { global $wgOut; // Added using template syntax, to take <noinclude>'s into account. - $wgOut->addWikiTextTitleTidy( '{{:' . $title->getFullText() . '}}', $this->mTitle ); + $wgOut->addWikiTextTitleTidy( '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>', $this->mTitle ); return true; } } @@ -2129,14 +2236,16 @@ class EditPage { * * If $content is null or false or a string, $content is returned unchanged. * - * If the given Content object is not of a type that can be edited using the text base EditPage, - * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual + * If the given Content object is not of a type that can be edited using + * the text base EditPage, an exception will be raised. Set + * $this->allowNonTextContent to true to allow editing of non-textual * content. * * @param Content|null|bool|string $content - * @return String the editable text form of the content. + * @return string The editable text form of the content. * - * @throws MWException if $content is not an instance of TextContent and $this->allowNonTextContent is not true. + * @throws MWException If $content is not an instance of TextContent and + * $this->allowNonTextContent is not true. */ protected function toEditText( $content ) { if ( $content === null || $content === false ) { @@ -2147,9 +2256,9 @@ class EditPage { return $content; } - if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) { - throw new MWException( "This content model can not be edited as text: " - . ContentHandler::getLocalizedName( $content->getModel() ) ); + if ( !$this->isSupportedContentModel( $content->getModel() ) ) { + throw new MWException( 'This content model is not supported: ' + . ContentHandler::getLocalizedName( $content->getModel() ) ); } return $content->serialize( $this->contentFormat ); @@ -2158,16 +2267,18 @@ class EditPage { /** * Turns the given text into a Content object by unserializing it. * - * If the resulting Content object is not of a type that can be edited using the text base EditPage, - * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual + * If the resulting Content object is not of a type that can be edited using + * the text base EditPage, an exception will be raised. Set + * $this->allowNonTextContent to true to allow editing of non-textual * content. * * @param string|null|bool $text Text to unserialize - * @return Content The content object created from $text. If $text was false or null, false resp. null will be - * returned instead. + * @return Content The content object created from $text. If $text was false + * or null, false resp. null will be returned instead. * - * @throws MWException if unserializing the text results in a Content object that is not an instance of TextContent - * and $this->allowNonTextContent is not true. + * @throws MWException If unserializing the text results in a Content + * object that is not an instance of TextContent and + * $this->allowNonTextContent is not true. */ protected function toEditContent( $text ) { if ( $text === false || $text === null ) { @@ -2177,8 +2288,8 @@ class EditPage { $content = ContentHandler::makeContent( $text, $this->getTitle(), $this->contentModel, $this->contentFormat ); - if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) { - throw new MWException( "This content model can not be edited as text: " + if ( !$this->isSupportedContentModel( $content->getModel() ) ) { + throw new MWException( 'This content model is not supported: ' . ContentHandler::getLocalizedName( $content->getModel() ) ); } @@ -2187,7 +2298,7 @@ class EditPage { /** * Send the edit form and related headers to $wgOut - * @param $formCallback Callback|null that takes an OutputPage parameter; will be called + * @param callable|null $formCallback That takes an OutputPage parameter; will be called * during form output near the top, for captchas and the like. */ function showEditForm( $formCallback = null ) { @@ -2235,9 +2346,16 @@ class EditPage { // @todo add EditForm plugin interface and use it here! // search for textarea1 and textares2, and allow EditForm to override all uses. - $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID, - 'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ), - 'enctype' => 'multipart/form-data' ) ) ); + $wgOut->addHTML( Html::openElement( + 'form', + array( + 'id' => self::EDITFORM_ID, + 'name' => self::EDITFORM_ID, + 'method' => 'post', + 'action' => $this->getActionURL( $this->getContextTitle() ), + 'enctype' => 'multipart/form-data' + ) + ) ); if ( is_callable( $formCallback ) ) { call_user_func_array( $formCallback, array( &$wgOut ) ); @@ -2246,8 +2364,20 @@ class EditPage { // Add an empty field to trip up spambots $wgOut->addHTML( Xml::openElement( 'div', array( 'id' => 'antispam-container', 'style' => 'display: none;' ) ) - . Html::rawElement( 'label', array( 'for' => 'wpAntiSpam' ), wfMessage( 'simpleantispam-label' )->parse() ) - . Xml::element( 'input', array( 'type' => 'text', 'name' => 'wpAntispam', 'id' => 'wpAntispam', 'value' => '' ) ) + . Html::rawElement( + 'label', + array( 'for' => 'wpAntiSpam' ), + wfMessage( 'simpleantispam-label' )->parse() + ) + . Xml::element( + 'input', + array( + 'type' => 'text', + 'name' => 'wpAntispam', + 'id' => 'wpAntispam', + 'value' => '' + ) + ) . Xml::closeElement( 'div' ) ); @@ -2321,6 +2451,10 @@ class EditPage { $wgOut->addHTML( EditPage::getEditToolbar() ); } + if ( $this->blankArticle ) { + $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) ); + } + if ( $this->isConflict ) { // In an edit conflict bypass the overridable content form method // and fallback to the raw wpTextbox1 since editconflicts can't be @@ -2364,11 +2498,19 @@ class EditPage { $this->showConflict(); } catch ( MWContentSerializationException $ex ) { // this can't really happen, but be nice if it does. - $msg = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() ); + $msg = wfMessage( + 'content-failed-to-parse', + $this->contentModel, + $this->contentFormat, + $ex->getMessage() + ); $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' ); } } + // Marker for detecting truncated form data. This must be the last + // parameter sent in order to be of use, so do not move me. + $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) ); $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" ); if ( !$wgUser->getOption( 'previewontop' ) ) { @@ -2382,7 +2524,7 @@ class EditPage { * Extract the section title from current section text, if any. * * @param string $text - * @return Mixed|string or false + * @return string|bool String or false */ public static function extractSectionTitle( $text ) { preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches ); @@ -2394,8 +2536,12 @@ class EditPage { } } + /** + * @return bool + */ protected function showHeader() { global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang; + global $wgAllowUserCss, $wgAllowUserJs; if ( $this->mTitle->isTalkPage() ) { $wgOut->addWikiMsg( 'talkpagetext' ); @@ -2437,6 +2583,10 @@ class EditPage { $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' ); } + if ( $this->blankArticle ) { + $wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' ); + } + if ( $this->hookError !== '' ) { $wgOut->addWikiText( $this->hookError ); } @@ -2451,9 +2601,15 @@ class EditPage { // Let sysop know that this will make private content public if saved if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) { - $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-permission' ); + $wgOut->wrapWikiMsg( + "<div class='mw-warning plainlinks'>\n$1\n</div>\n", + 'rev-deleted-text-permission' + ); } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) { - $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-view' ); + $wgOut->wrapWikiMsg( + "<div class='mw-warning plainlinks'>\n$1\n</div>\n", + 'rev-deleted-text-view' + ); } if ( !$revision->isCurrent() ) { @@ -2470,32 +2626,55 @@ class EditPage { } if ( wfReadOnly() ) { - $wgOut->wrapWikiMsg( "<div id=\"mw-read-only-warning\">\n$1\n</div>", array( 'readonlywarning', wfReadOnlyReason() ) ); + $wgOut->wrapWikiMsg( + "<div id=\"mw-read-only-warning\">\n$1\n</div>", + array( 'readonlywarning', wfReadOnlyReason() ) + ); } elseif ( $wgUser->isAnon() ) { if ( $this->formtype != 'preview' ) { - $wgOut->wrapWikiMsg( "<div id=\"mw-anon-edit-warning\">\n$1</div>", 'anoneditwarning' ); + $wgOut->wrapWikiMsg( + "<div id='mw-anon-edit-warning'>\n$1\n</div>", + array( 'anoneditwarning', + // Log-in link + '{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}', + // Sign-up link + '{{fullurl:Special:UserLogin/signup|returnto={{FULLPAGENAMEE}}}}' ) + ); } else { - $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\">\n$1</div>", 'anonpreviewwarning' ); + $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\">\n$1</div>", + 'anonpreviewwarning' + ); } } else { if ( $this->isCssJsSubpage ) { # Check the skin exists if ( $this->isWrongCaseCssJsPage ) { - $wgOut->wrapWikiMsg( "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>", array( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) ); + $wgOut->wrapWikiMsg( + "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>", + array( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) + ); } if ( $this->formtype !== 'preview' ) { - if ( $this->isCssSubpage ) { - $wgOut->wrapWikiMsg( "<div id='mw-usercssyoucanpreview'>\n$1\n</div>", array( 'usercssyoucanpreview' ) ); + if ( $this->isCssSubpage && $wgAllowUserCss ) { + $wgOut->wrapWikiMsg( + "<div id='mw-usercssyoucanpreview'>\n$1\n</div>", + array( 'usercssyoucanpreview' ) + ); } - if ( $this->isJsSubpage ) { - $wgOut->wrapWikiMsg( "<div id='mw-userjsyoucanpreview'>\n$1\n</div>", array( 'userjsyoucanpreview' ) ); + if ( $this->isJsSubpage && $wgAllowUserJs ) { + $wgOut->wrapWikiMsg( + "<div id='mw-userjsyoucanpreview'>\n$1\n</div>", + array( 'userjsyoucanpreview' ) + ); } } } } - if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) { + if ( $this->mTitle->isProtected( 'edit' ) && + MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== array( '' ) + ) { # Is the title semi-protected? if ( $this->mTitle->isSemiProtected() ) { $noticeMsg = 'semiprotectedpagewarning'; @@ -2534,16 +2713,27 @@ class EditPage { if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) { $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>", - array( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgLang->formatNum( $wgMaxArticleSize ) ) ); + array( + 'longpageerror', + $wgLang->formatNum( $this->kblength ), + $wgLang->formatNum( $wgMaxArticleSize ) + ) + ); } else { if ( !wfMessage( 'longpage-hint' )->isDisabled() ) { $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>", - array( 'longpage-hint', $wgLang->formatSize( strlen( $this->textbox1 ) ), strlen( $this->textbox1 ) ) + array( + 'longpage-hint', + $wgLang->formatSize( strlen( $this->textbox1 ) ), + strlen( $this->textbox1 ) + ) ); } } # Add header copyright warning $this->showHeaderCopyrightWarning(); + + return true; } /** @@ -2555,12 +2745,14 @@ class EditPage { * * @param string $summary The value of the summary input * @param string $labelText The html to place inside the label - * @param array $inputAttrs of attrs to use on the input - * @param array $spanLabelAttrs of attrs to use on the span inside the label + * @param array $inputAttrs Array of attrs to use on the input + * @param array $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 ) { + function getSummaryInput( $summary = "", $labelText = null, + $inputAttrs = null, $spanLabelAttrs = null + ) { // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters. $inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : array() ) + array( 'id' => 'wpSummary', @@ -2577,7 +2769,11 @@ class EditPage { $label = null; if ( $labelText ) { - $label = Xml::tags( 'label', $inputAttrs['id'] ? array( 'for' => $inputAttrs['id'] ) : null, $labelText ); + $label = Xml::tags( + 'label', + $inputAttrs['id'] ? array( 'for' => $inputAttrs['id'] ) : null, + $labelText + ); $label = Xml::tags( 'span', $spanLabelAttrs, $label ); } @@ -2587,11 +2783,10 @@ class EditPage { } /** - * @param $isSubjectPreview Boolean: true if this is the section subject/title - * up top, or false if this is the comment summary - * down below the textarea + * @param bool $isSubjectPreview True if this is the section subject/title + * up top, or false if this is the comment summary + * down below the textarea * @param string $summary The text of the summary to display - * @return String */ protected function showSummaryInput( $isSubjectPreview, $summary = "" ) { global $wgOut, $wgContLang; @@ -2608,16 +2803,21 @@ class EditPage { } $summary = $wgContLang->recodeForEdit( $summary ); $labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse(); - list( $label, $input ) = $this->getSummaryInput( $summary, $labelText, array( 'class' => $summaryClass ), array() ); + list( $label, $input ) = $this->getSummaryInput( + $summary, + $labelText, + array( 'class' => $summaryClass ), + array() + ); $wgOut->addHTML( "{$label} {$input}" ); } /** - * @param $isSubjectPreview Boolean: true if this is the section subject/title - * up top, or false if this is the comment summary - * down below the textarea - * @param string $summary the text of the summary to display - * @return String + * @param bool $isSubjectPreview True if this is the section subject/title + * up top, or false if this is the comment summary + * down below the textarea + * @param string $summary The text of the summary to display + * @return string */ protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) { // avoid spaces in preview, gets always trimmed on save @@ -2635,7 +2835,8 @@ class EditPage { $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview'; - $summary = wfMessage( $message )->parse() . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview ); + $summary = wfMessage( $message )->parse() + . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview ); return Xml::tags( 'div', array( 'class' => 'mw-summary-preview' ), $summary ); } @@ -2689,15 +2890,17 @@ HTML * The $textoverride method can be used by subclasses overriding showContentForm * to pass back to this method. * - * @param array $customAttribs of html attributes to use in the textarea - * @param string $textoverride optional text to override $this->textarea1 with + * @param array $customAttribs Array of html attributes to use in the textarea + * @param string $textoverride Optional text to override $this->textarea1 with */ protected function showTextbox1( $customAttribs = null, $textoverride = null ) { if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) { $attribs = array( 'style' => 'display:none;' ); } else { $classes = array(); // Textarea CSS - if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) { + if ( $this->mTitle->isProtected( 'edit' ) && + MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== array( '' ) + ) { # Is the title semi-protected? if ( $this->mTitle->isSemiProtected() ) { $classes[] = 'mw-textarea-sprotected'; @@ -2725,7 +2928,11 @@ HTML } } - $this->showTextbox( $textoverride !== null ? $textoverride : $this->textbox1, 'wpTextbox1', $attribs ); + $this->showTextbox( + $textoverride !== null ? $textoverride : $this->textbox1, + 'wpTextbox1', + $attribs + ); } protected function showTextbox2() { @@ -2749,7 +2956,9 @@ HTML 'id' => $name, 'cols' => $wgUser->getIntOption( 'cols' ), 'rows' => $wgUser->getIntOption( 'rows' ), - 'style' => '' // avoid php notices when appending preferences (appending allows customAttribs['style'] to still work + // Avoid PHP notices when appending preferences + // (appending allows customAttribs['style'] to still work). + 'style' => '' ); $pageLang = $this->mTitle->getPageLanguage(); @@ -2784,7 +2993,12 @@ HTML try { $this->showDiff(); } catch ( MWContentSerializationException $ex ) { - $msg = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() ); + $msg = wfMessage( + 'content-failed-to-parse', + $this->contentModel, + $this->contentFormat, + $ex->getMessage() + ); $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' ); } } @@ -2794,7 +3008,7 @@ HTML * Append preview output to $wgOut. * Includes category rendering if this is a category page. * - * @param string $text the HTML to be output for the preview. + * @param string $text The HTML to be output for the preview. */ protected function showPreview( $text ) { global $wgOut; @@ -2914,6 +3128,7 @@ HTML * Get the copyright warning * * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility + * @return string */ protected function getCopywarn() { return self::getCopyrightWarning( $this->mTitle ); @@ -2923,8 +3138,7 @@ HTML * Get the copyright warning, by default returns wikitext * * @param Title $title - * @param string $format output format, valid values are any function of - * a Message object + * @param string $format Output format, valid values are any function of a Message object * @return string */ public static function getCopyrightWarning( $title, $format = 'plain' ) { @@ -2972,7 +3186,7 @@ HTML foreach ( $output->getLimitReportData() as $key => $value ) { if ( wfRunHooks( 'ParserLimitReportFormat', - array( $key, $value, &$limitReport, true, true ) + array( $key, &$value, &$limitReport, true, true ) ) ) { $keyMsg = wfMessage( $key ); $valueMsg = wfMessage( array( "$key-value-html", "$key-value" ) ); @@ -2998,7 +3212,7 @@ HTML } protected function showStandardInputs( &$tabindex = 2 ) { - global $wgOut; + global $wgOut, $wgUseMediaWikiUIEverywhere; $wgOut->addHTML( "<div class='editOptions'>\n" ); if ( $this->section != 'new' ) { @@ -3023,14 +3237,26 @@ HTML array( 'class' => 'mw-editButtons-pipe-separator' ), wfMessage( 'pipe-separator' )->text() ); } - $edithelpurl = Skin::makeInternalOrExternalUrl( wfMessage( 'edithelppage' )->inContentLanguage()->text() ); - $edithelp = '<a target="helpwindow" href="' . $edithelpurl . '">' . - wfMessage( 'edithelp' )->escaped() . '</a> ' . + + $message = wfMessage( 'edithelppage' )->inContentLanguage()->text(); + $edithelpurl = Skin::makeInternalOrExternalUrl( $message ); + $attrs = array( + 'target' => 'helpwindow', + 'href' => $edithelpurl, + ); + if ( $wgUseMediaWikiUIEverywhere ) { + $attrs['class'] = 'mw-ui-button mw-ui-quiet'; + } + $edithelp = Html::element( 'a', $attrs, wfMessage( 'edithelp' )->text() ) . + wfMessage( 'word-separator' )->escaped() . wfMessage( 'newwindow' )->parse(); + $wgOut->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" ); $wgOut->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" ); $wgOut->addHTML( "</div><!-- editButtons -->\n" ); + wfRunHooks( 'EditPage::showStandardInputs:options', array( $this, $wgOut, &$tabindex ) ); + $wgOut->addHTML( "</div><!-- editOptions -->\n" ); } @@ -3064,15 +3290,20 @@ HTML * @return string */ public function getCancelLink() { + global $wgUseMediaWikiUIEverywhere; $cancelParams = array(); if ( !$this->isConflict && $this->oldid > 0 ) { $cancelParams['oldid'] = $this->oldid; } + $attrs = array( 'id' => 'mw-editform-cancel' ); + if ( $wgUseMediaWikiUIEverywhere ) { + $attrs['class'] = 'mw-ui-button mw-ui-quiet'; + } return Linker::linkKnown( $this->getContextTitle(), wfMessage( 'cancel' )->parse(), - array( 'id' => 'mw-editform-cancel' ), + $attrs, $cancelParams ); } @@ -3083,7 +3314,7 @@ HTML * variable in the constructor is not enough. This can be used when the * EditPage lives inside of a Special page rather than a custom page action. * - * @param $title Title object for which is being edited (where we go to for &action= links) + * @param Title $title Title object for which is being edited (where we go to for &action= links) * @return string */ protected function getActionURL( Title $title ) { @@ -3095,6 +3326,7 @@ HTML * 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. + * @return bool */ protected function wasDeletedSinceLastEdit() { if ( $this->deletedSinceEdit !== null ) { @@ -3116,6 +3348,9 @@ HTML return $this->deletedSinceEdit; } + /** + * @return bool|stdClass + */ protected function getLastDelete() { $dbr = wfGetDB( DB_SLAVE ); $data = $dbr->selectRow( @@ -3151,6 +3386,7 @@ HTML $data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped(); } } + return $data; } @@ -3161,6 +3397,7 @@ HTML */ function getPreviewText() { global $wgOut, $wgUser, $wgRawHtml, $wgLang; + global $wgAllowUserCss, $wgAllowUserJs; wfProfileIn( __METHOD__ ); @@ -3185,23 +3422,28 @@ HTML $content = $this->toEditContent( $this->textbox1 ); $previewHTML = ''; - if ( !wfRunHooks( 'AlternateEditPreview', array( $this, &$content, &$previewHTML, &$this->mParserOutput ) ) ) { + if ( !wfRunHooks( + 'AlternateEditPreview', + array( $this, &$content, &$previewHTML, &$this->mParserOutput ) ) + ) { wfProfileOut( __METHOD__ ); return $previewHTML; } + # provide a anchor link to the editform + $continueEditing = '<span class="mw-continue-editing">' . + '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . + wfMessage( 'continue-editing' )->text() . ']]</span>'; if ( $this->mTriedSave && !$this->mTokenOk ) { if ( $this->mTokenOkExceptSuffix ) { $note = wfMessage( 'token_suffix_mismatch' )->plain(); - } else { $note = wfMessage( 'session_fail_preview' )->plain(); } } elseif ( $this->incompleteForm ) { $note = wfMessage( 'edit_form_incomplete' )->plain(); } else { - $note = wfMessage( 'previewnote' )->plain() . - ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]'; + $note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing; } $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() ); @@ -3221,8 +3463,14 @@ HTML if ( $content->getModel() == CONTENT_MODEL_CSS ) { $format = 'css'; + if ( $level === 'user' && !$wgAllowUserCss ) { + $format = false; + } } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) { $format = 'js'; + if ( $level === 'user' && !$wgAllowUserJs ) { + $format = false; + } } else { $format = false; } @@ -3230,49 +3478,55 @@ HTML # Used messages to make sure grep find them: # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview if ( $level && $format ) { - $note = "<div id='mw-{$level}{$format}preview'>" . wfMessage( "{$level}{$format}preview" )->text() . "</div>"; + $note = "<div id='mw-{$level}{$format}preview'>" . + wfMessage( "{$level}{$format}preview" )->text() . + ' ' . $continueEditing . "</div>"; } } - $rt = $content->getRedirectChain(); - if ( $rt ) { - $previewHTML = $this->mArticle->viewRedirect( $rt, false ); - } else { - - # If we're adding a comment, we need to show the - # summary as the headline - if ( $this->section === "new" && $this->summary !== "" ) { - $content = $content->addSectionHeader( $this->summary ); - } + # If we're adding a comment, we need to show the + # summary as the headline + if ( $this->section === "new" && $this->summary !== "" ) { + $content = $content->addSectionHeader( $this->summary ); + } - $hook_args = array( $this, &$content ); - ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args ); - wfRunHooks( 'EditPageGetPreviewContent', $hook_args ); + $hook_args = array( $this, &$content ); + ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args ); + wfRunHooks( 'EditPageGetPreviewContent', $hook_args ); - $parserOptions->enableLimitReport(); + $parserOptions->enableLimitReport(); - # For CSS/JS pages, we should have called the ShowRawCssJs hook here. - # But it's now deprecated, so never mind + # For CSS/JS pages, we should have called the ShowRawCssJs hook here. + # But it's now deprecated, so never mind - $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions ); - $parserOutput = $content->getParserOutput( $this->getArticle()->getTitle(), null, $parserOptions ); + $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions ); + $parserOutput = $content->getParserOutput( + $this->getArticle()->getTitle(), + null, + $parserOptions + ); - $previewHTML = $parserOutput->getText(); - $this->mParserOutput = $parserOutput; - $wgOut->addParserOutputNoText( $parserOutput ); + $previewHTML = $parserOutput->getText(); + $this->mParserOutput = $parserOutput; + $wgOut->addParserOutputMetadata( $parserOutput ); - if ( count( $parserOutput->getWarnings() ) ) { - $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); - } + if ( count( $parserOutput->getWarnings() ) ) { + $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); } } catch ( MWContentSerializationException $ex ) { - $m = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() ); + $m = wfMessage( + 'content-failed-to-parse', + $this->contentModel, + $this->contentFormat, + $ex->getMessage() + ); $note .= "\n\n" . $m->parse(); $previewHTML = ''; } if ( $this->isConflict ) { - $conflict = '<h2 id="mw-previewconflict">' . wfMessage( 'previewconflict' )->escaped() . "</h2>\n"; + $conflict = '<h2 id="mw-previewconflict">' + . wfMessage( 'previewconflict' )->escaped() . "</h2>\n"; } else { $conflict = '<hr />'; } @@ -3291,7 +3545,7 @@ HTML } /** - * @return Array + * @return array */ function getTemplates() { if ( $this->preview || $this->section != '' ) { @@ -3313,128 +3567,94 @@ HTML /** * Shows a bulletin board style toolbar for common editing functions. * It can be disabled in the user preferences. - * The necessary JavaScript code can be found in skins/common/edit.js. * * @return string */ static function getEditToolbar() { - global $wgStylePath, $wgContLang, $wgLang, $wgOut; - global $wgUseTeX, $wgEnableUploads, $wgForeignFileRepos; + global $wgContLang, $wgOut; + global $wgEnableUploads, $wgForeignFileRepos; $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos ); /** * $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 + * opening tag, the closing tag, optionally a sample text that is * inserted between the two when no selection is highlighted * and. The tip text is shown when the user moves the mouse * over the button. * - * 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. + * Images are defined in ResourceLoaderEditToolbarModule. */ $toolarray = array( array( - 'image' => $wgLang->getImageFile( 'button-bold' ), 'id' => 'mw-editbutton-bold', 'open' => '\'\'\'', 'close' => '\'\'\'', 'sample' => wfMessage( 'bold_sample' )->text(), 'tip' => wfMessage( 'bold_tip' )->text(), - 'key' => 'B' ), array( - 'image' => $wgLang->getImageFile( 'button-italic' ), 'id' => 'mw-editbutton-italic', 'open' => '\'\'', 'close' => '\'\'', 'sample' => wfMessage( 'italic_sample' )->text(), 'tip' => wfMessage( 'italic_tip' )->text(), - 'key' => 'I' ), array( - 'image' => $wgLang->getImageFile( 'button-link' ), 'id' => 'mw-editbutton-link', 'open' => '[[', 'close' => ']]', 'sample' => wfMessage( 'link_sample' )->text(), 'tip' => wfMessage( 'link_tip' )->text(), - 'key' => 'L' ), array( - 'image' => $wgLang->getImageFile( 'button-extlink' ), 'id' => 'mw-editbutton-extlink', 'open' => '[', 'close' => ']', 'sample' => wfMessage( 'extlink_sample' )->text(), 'tip' => wfMessage( 'extlink_tip' )->text(), - 'key' => 'X' ), array( - 'image' => $wgLang->getImageFile( 'button-headline' ), 'id' => 'mw-editbutton-headline', 'open' => "\n== ", 'close' => " ==\n", 'sample' => wfMessage( 'headline_sample' )->text(), 'tip' => wfMessage( 'headline_tip' )->text(), - 'key' => 'H' ), $imagesAvailable ? array( - 'image' => $wgLang->getImageFile( 'button-image' ), 'id' => 'mw-editbutton-image', 'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':', 'close' => ']]', 'sample' => wfMessage( 'image_sample' )->text(), 'tip' => wfMessage( 'image_tip' )->text(), - 'key' => 'D', ) : false, $imagesAvailable ? array( - 'image' => $wgLang->getImageFile( 'button-media' ), 'id' => 'mw-editbutton-media', 'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':', 'close' => ']]', 'sample' => wfMessage( 'media_sample' )->text(), 'tip' => wfMessage( 'media_tip' )->text(), - 'key' => 'M' - ) : false, - $wgUseTeX ? array( - 'image' => $wgLang->getImageFile( 'button-math' ), - 'id' => 'mw-editbutton-math', - 'open' => "<math>", - 'close' => "</math>", - 'sample' => wfMessage( 'math_sample' )->text(), - 'tip' => wfMessage( 'math_tip' )->text(), - 'key' => 'C' ) : false, array( - 'image' => $wgLang->getImageFile( 'button-nowiki' ), 'id' => 'mw-editbutton-nowiki', 'open' => "<nowiki>", 'close' => "</nowiki>", 'sample' => wfMessage( 'nowiki_sample' )->text(), 'tip' => wfMessage( 'nowiki_tip' )->text(), - 'key' => 'N' ), array( - 'image' => $wgLang->getImageFile( 'button-sig' ), 'id' => 'mw-editbutton-signature', 'open' => '--~~~~', 'close' => '', 'sample' => '', 'tip' => wfMessage( 'sig_tip' )->text(), - 'key' => 'Y' ), array( - 'image' => $wgLang->getImageFile( 'button-hr' ), 'id' => 'mw-editbutton-hr', 'open' => "\n----\n", 'close' => '', 'sample' => '', 'tip' => wfMessage( 'hr_tip' )->text(), - 'key' => 'R' ) ); @@ -3445,16 +3665,17 @@ HTML } $params = array( - $image = $wgStylePath . '/common/images/' . $tool['image'], + // Images are defined in ResourceLoaderEditToolbarModule + false, // Note that we use the tip both for the ALT tag and the TITLE tag of the image. // Older browsers show a "speedtip" type message only for ALT. // Ideally these should be different, realistically they // probably don't need to be. - $tip = $tool['tip'], - $open = $tool['open'], - $close = $tool['close'], - $sample = $tool['sample'], - $cssId = $tool['id'], + $tool['tip'], + $tool['open'], + $tool['close'], + $tool['sample'], + $tool['id'], ); $script .= Xml::encodeJsCall( 'mw.toolbar.addButton', $params ); @@ -3481,13 +3702,13 @@ HTML * minor and watch * * @param int $tabindex Current tabindex - * @param array $checked of checkbox => bool, where bool indicates the checked + * @param array $checked Array of checkbox => bool, where bool indicates the checked * status of the checkbox * * @return array */ public function getCheckboxes( &$tabindex, $checked ) { - global $wgUser; + global $wgUser, $wgUseMediaWikiUIEverywhere; $checkboxes = array(); @@ -3501,11 +3722,19 @@ HTML 'accesskey' => wfMessage( 'accesskey-minoredit' )->text(), 'id' => 'wpMinoredit', ); - $checkboxes['minor'] = + $minorEditHtml = Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) . " <label for='wpMinoredit' id='mw-editpage-minoredit'" . Xml::expandAttributes( array( 'title' => Linker::titleAttrib( 'minoredit', 'withaccess' ) ) ) . ">{$minorLabel}</label>"; + + if ( $wgUseMediaWikiUIEverywhere ) { + $checkboxes['minor'] = Html::openElement( 'div', array( 'class' => 'mw-ui-checkbox' ) ) . + $minorEditHtml . + Html::closeElement( 'div' ); + } else { + $checkboxes['minor'] = $minorEditHtml; + } } } @@ -3517,11 +3746,18 @@ HTML 'accesskey' => wfMessage( 'accesskey-watch' )->text(), 'id' => 'wpWatchthis', ); - $checkboxes['watch'] = + $watchThisHtml = Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) . " <label for='wpWatchthis' id='mw-editpage-watch'" . Xml::expandAttributes( array( 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ) ) . ">{$watchLabel}</label>"; + if ( $wgUseMediaWikiUIEverywhere ) { + $checkboxes['watch'] = Html::openElement( 'div', array( 'class' => 'mw-ui-checkbox' ) ) . + $watchThisHtml . + Html::closeElement( 'div' ); + } else { + $checkboxes['watch'] = $watchThisHtml; + } } wfRunHooks( 'EditPageBeforeEditChecks', array( &$this, &$checkboxes, &$tabindex ) ); return $checkboxes; @@ -3536,42 +3772,47 @@ HTML * @return array */ public function getEditButtons( &$tabindex ) { + global $wgUseMediaWikiUIEverywhere; + $buttons = array(); - $temp = array( + $attribs = array( 'id' => 'wpSave', 'name' => 'wpSave', 'type' => 'submit', 'tabindex' => ++$tabindex, 'value' => wfMessage( 'savearticle' )->text(), - 'accesskey' => wfMessage( 'accesskey-save' )->text(), - 'title' => wfMessage( 'tooltip-save' )->text() . ' [' . wfMessage( 'accesskey-save' )->text() . ']', - ); - $buttons['save'] = Xml::element( 'input', $temp, '' ); + ) + Linker::tooltipAndAccesskeyAttribs( 'save' ); + if ( $wgUseMediaWikiUIEverywhere ) { + $attribs['class'] = 'mw-ui-button mw-ui-constructive'; + } + $buttons['save'] = Xml::element( 'input', $attribs, '' ); ++$tabindex; // use the same for preview and live preview - $temp = array( + $attribs = array( 'id' => 'wpPreview', 'name' => 'wpPreview', 'type' => 'submit', 'tabindex' => $tabindex, 'value' => wfMessage( 'showpreview' )->text(), - 'accesskey' => wfMessage( 'accesskey-preview' )->text(), - 'title' => wfMessage( 'tooltip-preview' )->text() . ' [' . wfMessage( 'accesskey-preview' )->text() . ']', - ); - $buttons['preview'] = Xml::element( 'input', $temp, '' ); + ) + Linker::tooltipAndAccesskeyAttribs( 'preview' ); + if ( $wgUseMediaWikiUIEverywhere ) { + $attribs['class'] = 'mw-ui-button mw-ui-progressive'; + } + $buttons['preview'] = Xml::element( 'input', $attribs, '' ); $buttons['live'] = ''; - $temp = array( + $attribs = array( 'id' => 'wpDiff', 'name' => 'wpDiff', 'type' => 'submit', 'tabindex' => ++$tabindex, 'value' => wfMessage( 'showdiff' )->text(), - 'accesskey' => wfMessage( 'accesskey-diff' )->text(), - 'title' => wfMessage( 'tooltip-diff' )->text() . ' [' . wfMessage( 'accesskey-diff' )->text() . ']', - ); - $buttons['diff'] = Xml::element( 'input', $temp, '' ); + ) + Linker::tooltipAndAccesskeyAttribs( 'diff' ); + if ( $wgUseMediaWikiUIEverywhere ) { + $attribs['class'] = 'mw-ui-button mw-ui-progressive'; + } + $buttons['diff'] = Xml::element( 'input', $attribs, '' ); wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons, &$tabindex ) ); return $buttons; @@ -3599,48 +3840,14 @@ HTML #$categories = $skin->getCategoryLinks(); $s = - '<?xml version="1.0" encoding="UTF-8" ?>' . "\n" . - Xml::tags( 'livepreview', null, - Xml::element( 'preview', null, $previewText ) - #. Xml::element( 'category', null, $categories ) - ); + '<?xml version="1.0" encoding="UTF-8" ?>' . "\n" . + Xml::tags( 'livepreview', null, + Xml::element( 'preview', null, $previewText ) + #. Xml::element( 'category', null, $categories ) + ); echo $s; } - /** - * Call the stock "user is blocked" page - * - * @deprecated in 1.19; throw an exception directly instead - */ - function blockedPage() { - wfDeprecated( __METHOD__, '1.19' ); - global $wgUser; - - throw new UserBlockedError( $wgUser->getBlock() ); - } - - /** - * Produce the stock "please login to edit pages" page - * - * @deprecated in 1.19; throw an exception directly instead - */ - function userNotLoggedInPage() { - wfDeprecated( __METHOD__, '1.19' ); - throw new PermissionsError( 'edit' ); - } - - /** - * Show an error page saying to the user that he has insufficient permissions - * to create a new page - * - * @deprecated in 1.19; throw an exception directly instead - */ - function noCreatePermission() { - wfDeprecated( __METHOD__, '1.19' ); - $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage'; - throw new PermissionsError( $permission ); - } - /** * Creates a basic error page which informs the user that * they have attempted to edit a nonexistent section. @@ -3657,33 +3864,10 @@ HTML $wgOut->returnToMain( false, $this->mTitle ); } - /** - * Produce the stock "your edit contains spam" page - * - * @param string|bool $match Text which triggered one or more filters - * @deprecated since 1.17 Use method spamPageWithContent() instead - */ - static function spamPage( $match = false ) { - wfDeprecated( __METHOD__, '1.17' ); - - global $wgOut, $wgTitle; - - $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) ); - - $wgOut->addHTML( '<div id="spamprotected">' ); - $wgOut->addWikiMsg( 'spamprotectiontext' ); - if ( $match ) { - $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) ); - } - $wgOut->addHTML( '</div>' ); - - $wgOut->returnToMain( false, $wgTitle ); - } - /** * Show "your edit contains spam" page with your diff and text * - * @param $match string|Array|bool Text (or array of texts) which triggered one or more filters + * @param string|array|bool $match Text (or array of texts) which triggered one or more filters */ public function spamPageWithContent( $match = false ) { global $wgOut, $wgLang; @@ -3710,25 +3894,13 @@ HTML $wgOut->addReturnTo( $this->getContextTitle(), array( 'action' => 'edit' ) ); } - /** - * Format an anchor fragment as it would appear for a given section name - * @param $text String - * @return String - * @private - */ - function sectionAnchor( $text ) { - global $wgParser; - return $wgParser->guessSectionNameFromWikiText( $text ); - } - /** * Check if the browser is on a blacklist of user-agents known to * mangle UTF-8 data on form submission. Returns true if Unicode * should make it through, false if it's known to be a problem. * @return bool - * @private */ - function checkUnicodeCompliantBrowser() { + private function checkUnicodeCompliantBrowser() { global $wgBrowserBlackList, $wgRequest; $currentbrowser = $wgRequest->getHeader( 'User-Agent' ); @@ -3749,27 +3921,14 @@ HTML * Filter an input field through a Unicode de-armoring process if it * came from an old browser with known broken Unicode editing issues. * - * @param $request WebRequest - * @param $field String - * @return String - * @private - */ - function safeUnicodeInput( $request, $field ) { - $text = rtrim( $request->getText( $field ) ); - return $request->getBool( 'safemode' ) - ? $this->unmakesafe( $text ) - : $text; - } - - /** - * @param $request WebRequest - * @param $text string + * @param WebRequest $request + * @param string $field * @return string */ - function safeUnicodeText( $request, $text ) { - $text = rtrim( $text ); + protected function safeUnicodeInput( $request, $field ) { + $text = rtrim( $request->getText( $field ) ); return $request->getBool( 'safemode' ) - ? $this->unmakesafe( $text ) + ? $this->unmakeSafe( $text ) : $text; } @@ -3777,16 +3936,15 @@ HTML * Filter an output field through a Unicode armoring process if it is * going to an old browser with known broken Unicode editing issues. * - * @param $text String - * @return String - * @private + * @param string $text + * @return string */ - function safeUnicodeOutput( $text ) { + protected function safeUnicodeOutput( $text ) { global $wgContLang; $codedText = $wgContLang->recodeForEdit( $text ); return $this->checkUnicodeCompliantBrowser() ? $codedText - : $this->makesafe( $codedText ); + : $this->makeSafe( $codedText ); } /** @@ -3798,18 +3956,18 @@ HTML * Preexisting such character references will have a 0 added to them * to ensure that round-trips do not alter the original data. * - * @param $invalue String - * @return String - * @private + * @param string $invalue + * @return string */ - function makesafe( $invalue ) { + private function makeSafe( $invalue ) { // Armor existing references for reversibility. $invalue = strtr( $invalue, array( "&#x" => "�" ) ); $bytesleft = 0; $result = ""; $working = 0; - for ( $i = 0; $i < strlen( $invalue ); $i++ ) { + $valueLength = strlen( $invalue ); + for ( $i = 0; $i < $valueLength; $i++ ) { $bytevalue = ord( $invalue[$i] ); if ( $bytevalue <= 0x7F ) { // 0xxx xxxx $result .= chr( $bytevalue ); @@ -3840,13 +3998,13 @@ HTML * back to UTF-8. Used to protect data from corruption by broken web browsers * as listed in $wgBrowserBlackList. * - * @param $invalue String - * @return String - * @private + * @param string $invalue + * @return string */ - function unmakesafe( $invalue ) { + private function unmakeSafe( $invalue ) { $result = ""; - for ( $i = 0; $i < strlen( $invalue ); $i++ ) { + $valueLength = strlen( $invalue ); + for ( $i = 0; $i < $valueLength; $i++ ) { if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) { $i += 3; $hexstring = ""; diff --git a/includes/Exception.php b/includes/Exception.php deleted file mode 100644 index 5bad88c2..00000000 --- a/includes/Exception.php +++ /dev/null @@ -1,824 +0,0 @@ -<?php -/** - * Exception class and handler. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * @defgroup Exception Exception - */ - -/** - * MediaWiki exception - * - * @ingroup Exception - */ -class MWException extends Exception { - - /** - * Should the exception use $wgOut to output the error? - * - * @return bool - */ - function useOutputPage() { - return $this->useMessageCache() && - !empty( $GLOBALS['wgFullyInitialised'] ) && - !empty( $GLOBALS['wgOut'] ) && - !empty( $GLOBALS['wgTitle'] ); - } - - /** - * Whether to log this exception in the exception debug log. - * - * @since 1.23 - * @return boolean - */ - function isLoggable() { - return true; - } - - /** - * Can the extension use the Message class/wfMessage to get i18n-ed messages? - * - * @return bool - */ - function useMessageCache() { - global $wgLang; - - foreach ( $this->getTrace() as $frame ) { - if ( isset( $frame['class'] ) && $frame['class'] === 'LocalisationCache' ) { - return false; - } - } - - return $wgLang instanceof Language; - } - - /** - * Run hook to allow extensions to modify the text of the exception - * - * @param string $name class name of the exception - * @param array $args arguments to pass to the callback functions - * @return string|null string to output or null if any hook has been called - */ - function runHooks( $name, $args = array() ) { - global $wgExceptionHooks; - - if ( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) { - return null; // Just silently ignore - } - - if ( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[$name] ) ) { - return null; - } - - $hooks = $wgExceptionHooks[$name]; - $callargs = array_merge( array( $this ), $args ); - - foreach ( $hooks as $hook ) { - if ( is_string( $hook ) || ( is_array( $hook ) && count( $hook ) >= 2 && is_string( $hook[0] ) ) ) { // 'function' or array( 'class', hook' ) - $result = call_user_func_array( $hook, $callargs ); - } else { - $result = null; - } - - if ( is_string( $result ) ) { - return $result; - } - } - return null; - } - - /** - * Get a message from i18n - * - * @param string $key message name - * @param string $fallback default message if the message cache can't be - * called by the exception - * The function also has other parameters that are arguments for the message - * @return string message with arguments replaced - */ - function msg( $key, $fallback /*[, params...] */ ) { - $args = array_slice( func_get_args(), 2 ); - - if ( $this->useMessageCache() ) { - return wfMessage( $key, $args )->plain(); - } else { - return wfMsgReplaceArgs( $fallback, $args ); - } - } - - /** - * If $wgShowExceptionDetails is true, return a HTML message with a - * backtrace to the error, otherwise show a message to ask to set it to true - * to show that information. - * - * @return string html to output - */ - function getHTML() { - global $wgShowExceptionDetails; - - if ( $wgShowExceptionDetails ) { - return '<p>' . nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $this ) ) ) . - '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $this ) ) ) . - "</p>\n"; - } else { - return "<div class=\"errorbox\">" . - '[' . MWExceptionHandler::getLogId( $this ) . '] ' . - gmdate( 'Y-m-d H:i:s' ) . - ": Fatal exception of type " . get_class( $this ) . "</div>\n" . - "<!-- Set \$wgShowExceptionDetails = true; " . - "at the bottom of LocalSettings.php to show detailed " . - "debugging information. -->"; - } - } - - /** - * Get the text to display when reporting the error on the command line. - * If $wgShowExceptionDetails is true, return a text message with a - * backtrace to the error. - * - * @return string - */ - function getText() { - global $wgShowExceptionDetails; - - if ( $wgShowExceptionDetails ) { - return MWExceptionHandler::getLogMessage( $this ) . - "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $this ) . "\n"; - } else { - return "Set \$wgShowExceptionDetails = true; " . - "in LocalSettings.php to show detailed debugging information.\n"; - } - } - - /** - * Return the title of the page when reporting this error in a HTTP response. - * - * @return string - */ - function getPageTitle() { - return $this->msg( 'internalerror', "Internal error" ); - } - - /** - * Get a the ID for this error. - * - * @since 1.20 - * @deprecated since 1.22 Use MWExceptionHandler::getLogId instead. - * @return string - */ - function getLogId() { - wfDeprecated( __METHOD__, '1.22' ); - return MWExceptionHandler::getLogId( $this ); - } - - /** - * Return the requested URL and point to file and line number from which the - * exception occurred - * - * @since 1.8 - * @deprecated since 1.22 Use MWExceptionHandler::getLogMessage instead. - * @return string - */ - function getLogMessage() { - wfDeprecated( __METHOD__, '1.22' ); - return MWExceptionHandler::getLogMessage( $this ); - } - - /** - * Output the exception report using HTML. - */ - function reportHTML() { - global $wgOut; - if ( $this->useOutputPage() ) { - $wgOut->prepareErrorPage( $this->getPageTitle() ); - - $hookResult = $this->runHooks( get_class( $this ) ); - if ( $hookResult ) { - $wgOut->addHTML( $hookResult ); - } else { - $wgOut->addHTML( $this->getHTML() ); - } - - $wgOut->output(); - } else { - header( "Content-Type: text/html; charset=utf-8" ); - echo "<!doctype html>\n" . - '<html><head>' . - '<title>' . htmlspecialchars( $this->getPageTitle() ) . '' . - "\n"; - - $hookResult = $this->runHooks( get_class( $this ) . "Raw" ); - if ( $hookResult ) { - echo $hookResult; - } else { - echo $this->getHTML(); - } - - echo "\n"; - } - } - - /** - * Output a report about the exception and takes care of formatting. - * It will be either HTML or plain text based on isCommandLine(). - */ - function report() { - global $wgMimeType; - - MWExceptionHandler::logException( $this ); - - if ( defined( 'MW_API' ) ) { - // Unhandled API exception, we can't be sure that format printer is alive - header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $this ) ); - wfHttpError( 500, 'Internal Server Error', $this->getText() ); - } elseif ( self::isCommandLine() ) { - MWExceptionHandler::printError( $this->getText() ); - } else { - header( "HTTP/1.1 500 MediaWiki exception" ); - header( "Status: 500 MediaWiki exception", true ); - header( "Content-Type: $wgMimeType; charset=utf-8", true ); - - $this->reportHTML(); - } - } - - /** - * Check whether we are in command line mode or not to report the exception - * in the correct format. - * - * @return bool - */ - static function isCommandLine() { - return !empty( $GLOBALS['wgCommandLineMode'] ); - } -} - -/** - * Exception class which takes an HTML error message, and does not - * produce a backtrace. Replacement for OutputPage::fatalError(). - * - * @since 1.7 - * @ingroup Exception - */ -class FatalError extends MWException { - - /** - * @return string - */ - function getHTML() { - return $this->getMessage(); - } - - /** - * @return string - */ - function getText() { - return $this->getMessage(); - } -} - -/** - * An error page which can definitely be safely rendered using the OutputPage. - * - * @since 1.7 - * @ingroup Exception - */ -class ErrorPageError extends MWException { - public $title, $msg, $params; - - /** - * Note: these arguments are keys into wfMessage(), not text! - * - * @param string|Message $title Message key (string) for page title, or a Message object - * @param string|Message $msg Message key (string) for error text, or a Message object - * @param array $params with parameters to wfMessage() - */ - function __construct( $title, $msg, $params = null ) { - $this->title = $title; - $this->msg = $msg; - $this->params = $params; - - // Bug 44111: Messages in the log files should be in English and not - // customized by the local wiki. So get the default English version for - // passing to the parent constructor. Our overridden report() below - // makes sure that the page shown to the user is not forced to English. - if ( $msg instanceof Message ) { - $enMsg = clone( $msg ); - } else { - $enMsg = wfMessage( $msg, $params ); - } - $enMsg->inLanguage( 'en' )->useDatabase( false ); - parent::__construct( $enMsg->text() ); - } - - function report() { - global $wgOut; - - $wgOut->showErrorPage( $this->title, $this->msg, $this->params ); - $wgOut->output(); - } -} - -/** - * Show an error page on a badtitle. - * Similar to ErrorPage, but emit a 400 HTTP error code to let mobile - * browser it is not really a valid content. - * - * @since 1.19 - * @ingroup Exception - */ -class BadTitleError extends ErrorPageError { - /** - * @param string|Message $msg A message key (default: 'badtitletext') - * @param array $params parameter to wfMessage() - */ - function __construct( $msg = 'badtitletext', $params = null ) { - parent::__construct( 'badtitle', $msg, $params ); - } - - /** - * Just like ErrorPageError::report() but additionally set - * a 400 HTTP status code (bug 33646). - */ - function report() { - global $wgOut; - - // bug 33646: a badtitle error page need to return an error code - // to let mobile browser now that it is not a normal page. - $wgOut->setStatusCode( 400 ); - parent::report(); - } - -} - -/** - * Show an error when a user tries to do something they do not have the necessary - * permissions for. - * - * @since 1.18 - * @ingroup Exception - */ -class PermissionsError extends ErrorPageError { - public $permission, $errors; - - function __construct( $permission, $errors = array() ) { - global $wgLang; - - $this->permission = $permission; - - if ( !count( $errors ) ) { - $groups = array_map( - array( 'User', 'makeGroupLinkWiki' ), - User::getGroupsWithPermission( $this->permission ) - ); - - if ( $groups ) { - $errors[] = array( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) ); - } else { - $errors[] = array( 'badaccess-group0' ); - } - } - - $this->errors = $errors; - } - - function report() { - global $wgOut; - - $wgOut->showPermissionsErrorPage( $this->errors, $this->permission ); - $wgOut->output(); - } -} - -/** - * Show an error when the wiki is locked/read-only and the user tries to do - * something that requires write access. - * - * @since 1.18 - * @ingroup Exception - */ -class ReadOnlyError extends ErrorPageError { - public function __construct() { - parent::__construct( - 'readonly', - 'readonlytext', - wfReadOnlyReason() - ); - } -} - -/** - * Show an error when the user hits a rate limit. - * - * @since 1.18 - * @ingroup Exception - */ -class ThrottledError extends ErrorPageError { - public function __construct() { - parent::__construct( - 'actionthrottled', - 'actionthrottledtext' - ); - } - - public function report() { - global $wgOut; - $wgOut->setStatusCode( 503 ); - parent::report(); - } -} - -/** - * Show an error when the user tries to do something whilst blocked. - * - * @since 1.18 - * @ingroup Exception - */ -class UserBlockedError extends ErrorPageError { - public function __construct( Block $block ) { - // @todo FIXME: Implement a more proper way to get context here. - $params = $block->getPermissionsError( RequestContext::getMain() ); - parent::__construct( 'blockedtitle', array_shift( $params ), $params ); - } -} - -/** - * Shows a generic "user is not logged in" error page. - * - * This is essentially an ErrorPageError exception which by default uses the - * 'exception-nologin' as a title and 'exception-nologin-text' for the message. - * @see bug 37627 - * @since 1.20 - * - * @par Example: - * @code - * if( $user->isAnon() ) { - * throw new UserNotLoggedIn(); - * } - * @endcode - * - * Note the parameter order differs from ErrorPageError, this allows you to - * simply specify a reason without overriding the default title. - * - * @par Example: - * @code - * if( $user->isAnon() ) { - * throw new UserNotLoggedIn( 'action-require-loggedin' ); - * } - * @endcode - * - * @ingroup Exception - */ -class UserNotLoggedIn extends ErrorPageError { - - /** - * @param $reasonMsg A message key containing the reason for the error. - * Optional, default: 'exception-nologin-text' - * @param $titleMsg A message key to set the page title. - * Optional, default: 'exception-nologin' - * @param $params Parameters to wfMessage(). - * Optional, default: null - */ - public function __construct( - $reasonMsg = 'exception-nologin-text', - $titleMsg = 'exception-nologin', - $params = null - ) { - parent::__construct( $titleMsg, $reasonMsg, $params ); - } -} - -/** - * Show an error that looks like an HTTP server error. - * Replacement for wfHttpError(). - * - * @since 1.19 - * @ingroup Exception - */ -class HttpError extends MWException { - private $httpCode, $header, $content; - - /** - * Constructor - * - * @param $httpCode Integer: HTTP status code to send to the client - * @param string|Message $content content of the message - * @param string|Message $header content of the header (\ and \) - */ - public function __construct( $httpCode, $content, $header = null ) { - parent::__construct( $content ); - $this->httpCode = (int)$httpCode; - $this->header = $header; - $this->content = $content; - } - - /** - * Returns the HTTP status code supplied to the constructor. - * - * @return int - */ - public function getStatusCode() { - return $this->httpCode; - } - - /** - * Report the HTTP error. - * Sends the appropriate HTTP status code and outputs an - * HTML page with an error message. - */ - public function report() { - $httpMessage = HttpStatus::getMessage( $this->httpCode ); - - header( "Status: {$this->httpCode} {$httpMessage}", true, $this->httpCode ); - header( 'Content-type: text/html; charset=utf-8' ); - - print $this->getHTML(); - } - - /** - * Returns HTML for reporting the HTTP error. - * This will be a minimal but complete HTML document. - * - * @return string HTML - */ - public function getHTML() { - if ( $this->header === null ) { - $header = HttpStatus::getMessage( $this->httpCode ); - } elseif ( $this->header instanceof Message ) { - $header = $this->header->escaped(); - } else { - $header = htmlspecialchars( $this->header ); - } - - if ( $this->content instanceof Message ) { - $content = $this->content->escaped(); - } else { - $content = htmlspecialchars( $this->content ); - } - - return "\n" . - "$header\n" . - "

$header

$content

\n"; - } -} - -/** - * 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: ' . self::getLogMessage( $e ) . - "\nBacktrace:\n" . self::getRedactedTraceAsString( $e ) . - "\n\nException caught inside exception handler: " . self::getLogMessage( $e2 ) . - "\nBacktrace:\n" . self::getRedactedTraceAsString( $e2 ); - } 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 { - echo nl2br( htmlspecialchars( $message ) ) . "\n"; - } - } - } else { - $message = "Unexpected non-MediaWiki exception encountered, of type \"" . get_class( $e ) . "\""; - - if ( $wgShowExceptionDetails ) { - $message .= "\n" . MWExceptionHandler::getLogMessage( $e ) . "\nBacktrace:\n" . - self::getRedactedTraceAsString( $e ) . "\n"; - } - - if ( $cmdLine ) { - self::printError( $message ); - } else { - echo nl2br( htmlspecialchars( $message ) ) . "\n"; - } - } - } - - /** - * Print a message, if possible to STDERR. - * Use this in command line mode only (see isCommandLine) - * - * @param string $message 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 $message; - } - } - - /** - * 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; - - self::report( $e ); - - // Final cleanup - if ( $wgFullyInitialised ) { - try { - // uses $wgRequest, hence the $wgFullyInitialised condition - wfLogProfilingData(); - } catch ( Exception $e ) { - } - } - - // Exit value should be nonzero for the benefit of shell jobs - exit( 1 ); - } - - /** - * Generate a string representation of an exception's stack trace - * - * Like Exception::getTraceAsString, but replaces argument values with - * argument type or class name. - * - * @param Exception $e - * @return string - */ - public static function getRedactedTraceAsString( Exception $e ) { - $text = ''; - - foreach ( self::getRedactedTrace( $e ) as $level => $frame ) { - if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) { - $text .= "#{$level} {$frame['file']}({$frame['line']}): "; - } else { - // 'file' and 'line' are unset for calls via call_user_func (bug 55634) - // This matches behaviour of Exception::getTraceAsString to instead - // display "[internal function]". - $text .= "#{$level} [internal function]: "; - } - - if ( isset( $frame['class'] ) ) { - $text .= $frame['class'] . $frame['type'] . $frame['function']; - } else { - $text .= $frame['function']; - } - - if ( isset( $frame['args'] ) ) { - $text .= '(' . implode( ', ', $frame['args'] ) . ")\n"; - } else { - $text .= "()\n"; - } - } - - $level = $level + 1; - $text .= "#{$level} {main}"; - - return $text; - } - - /** - * Return a copy of an exception's backtrace as an array. - * - * Like Exception::getTrace, but replaces each element in each frame's - * argument array with the name of its class (if the element is an object) - * or its type (if the element is a PHP primitive). - * - * @since 1.22 - * @param Exception $e - * @return array - */ - public static function getRedactedTrace( Exception $e ) { - return array_map( function ( $frame ) { - if ( isset( $frame['args'] ) ) { - $frame['args'] = array_map( function ( $arg ) { - return is_object( $arg ) ? get_class( $arg ) : gettype( $arg ); - }, $frame['args'] ); - } - return $frame; - }, $e->getTrace() ); - } - - - /** - * Get the ID for this error. - * - * The ID is saved so that one can match the one output to the user (when - * $wgShowExceptionDetails is set to false), to the entry in the debug log. - * - * @since 1.22 - * @param Exception $e - * @return string - */ - public static function getLogId( Exception $e ) { - if ( !isset( $e->_mwLogId ) ) { - $e->_mwLogId = wfRandomString( 8 ); - } - return $e->_mwLogId; - } - - /** - * Return the requested URL and point to file and line number from which the - * exception occurred. - * - * @since 1.22 - * @param Exception $e - * @return string - */ - public static function getLogMessage( Exception $e ) { - global $wgRequest; - - $id = self::getLogId( $e ); - $file = $e->getFile(); - $line = $e->getLine(); - $message = $e->getMessage(); - - if ( isset( $wgRequest ) && !$wgRequest instanceof FauxRequest ) { - $url = $wgRequest->getRequestURL(); - if ( !$url ) { - $url = '[no URL]'; - } - } else { - $url = '[no req]'; - } - - return "[$id] $url Exception from line $line of $file: $message"; - } - - /** - * Log an exception to the exception log (if enabled). - * - * This method must not assume the exception is an MWException, - * it is also used to handle PHP errors or errors from other libraries. - * - * @since 1.22 - * @param Exception $e - */ - public static function logException( Exception $e ) { - global $wgLogExceptionBacktrace; - - if ( !( $e instanceof MWException ) || $e->isLoggable() ) { - $log = self::getLogMessage( $e ); - if ( $wgLogExceptionBacktrace ) { - wfDebugLog( 'exception', $log . "\n" . $e->getTraceAsString() . "\n" ); - } else { - wfDebugLog( 'exception', $log ); - } - } - } - -} diff --git a/includes/Export.php b/includes/Export.php index 98de4c00..84f5c60c 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -3,7 +3,7 @@ * Base classes for dumps and export * * Copyright © 2003, 2005, 2006 Brion Vibber - * http://www.mediawiki.org/ + * https://www.mediawiki.org/ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -31,11 +31,17 @@ * @ingroup SpecialPage Dump */ class WikiExporter { - var $list_authors = false; # Return distinct author list (when not returning full history) - var $author_list = ""; + /** @var bool Return distinct author list (when not returning full history) */ + public $list_authors = false; - var $dumpUploads = false; - var $dumpUploadFileContents = false; + /** @var bool */ + public $dumpUploads = false; + + /** @var bool */ + public $dumpUploadFileContents = false; + + /** @var string */ + public $author_list = ""; const FULL = 1; const CURRENT = 2; @@ -49,21 +55,21 @@ class WikiExporter { const TEXT = 0; const STUB = 1; - var $buffer; + /** @var int */ + public $buffer; - var $text; + /** @var int */ + public $text; - /** - * @var DumpOutput - */ - var $sink; + /** @var DumpOutput */ + public $sink; /** * Returns the export schema version. * @return string */ public static function schemaVersion() { - return "0.8"; + return "0.9"; } /** @@ -73,15 +79,14 @@ class WikiExporter { * make additional queries to pull source data while the * main query is still running. * - * @param $db DatabaseBase - * @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 - * dir: "asc" or "desc" timestamp order - * @param int $buffer one of WikiExporter::BUFFER or WikiExporter::STREAM - * @param int $text one of WikiExporter::TEXT or WikiExporter::STUB + * @param DatabaseBase $db + * @param int|array $history 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 + * - dir: "asc" or "desc" timestamp order + * @param int $buffer One of WikiExporter::BUFFER or WikiExporter::STREAM + * @param int $text One of WikiExporter::TEXT or WikiExporter::STUB */ function __construct( $db, $history = WikiExporter::CURRENT, $buffer = WikiExporter::BUFFER, $text = WikiExporter::TEXT ) { @@ -98,7 +103,7 @@ class WikiExporter { * various row objects and XML output for filtering. Filters * can be chained or used as callbacks. * - * @param $sink mixed + * @param DumpOutput $sink */ public function setOutputSink( &$sink ) { $this->sink =& $sink; @@ -126,9 +131,9 @@ class WikiExporter { /** * Dumps a series of page and revision records for those pages * in the database falling within the page_id range given. - * @param int $start inclusive lower limit (this id is included) - * @param $end Int: Exclusive upper limit (this id is not included) - * If 0, no upper limit. + * @param int $start Inclusive lower limit (this id is included) + * @param int $end Exclusive upper limit (this id is not included) + * If 0, no upper limit. */ public function pagesByRange( $start, $end ) { $condition = 'page_id >= ' . intval( $start ); @@ -141,9 +146,9 @@ class WikiExporter { /** * Dumps a series of page and revision records for those pages * in the database with revisions falling within the rev_id range given. - * @param int $start inclusive lower limit (this id is included) - * @param $end Int: Exclusive upper limit (this id is not included) - * If 0, no upper limit. + * @param int $start Inclusive lower limit (this id is included) + * @param int $end Exclusive upper limit (this id is not included) + * If 0, no upper limit. */ public function revsByRange( $start, $end ) { $condition = 'rev_id >= ' . intval( $start ); @@ -154,7 +159,7 @@ class WikiExporter { } /** - * @param $title Title + * @param Title $title */ public function pageByTitle( $title ) { $this->dumpFrom( @@ -163,7 +168,7 @@ class WikiExporter { } /** - * @param $name string + * @param string $name * @throws MWException */ public function pageByName( $name ) { @@ -176,7 +181,7 @@ class WikiExporter { } /** - * @param $names array + * @param array $names */ public function pagesByName( $names ) { foreach ( $names as $name ) { @@ -189,8 +194,8 @@ class WikiExporter { } /** - * @param $start int - * @param $end int + * @param int $start + * @param int $end */ public function logsByRange( $start, $end ) { $condition = 'log_id >= ' . intval( $start ); @@ -205,7 +210,7 @@ class WikiExporter { * Not called by default (depends on $this->list_authors) * Can be set by Special:Export when not exporting whole history * - * @param $cond + * @param array $cond */ protected function do_list_authors( $cond ) { wfProfileIn( __METHOD__ ); @@ -238,7 +243,7 @@ class WikiExporter { } /** - * @param $cond string + * @param string $cond * @throws MWException * @throws Exception */ @@ -262,7 +267,7 @@ class WikiExporter { if ( $this->buffer == WikiExporter::STREAM ) { $prev = $this->db->bufferResults( false ); } - $wrapper = null; // Assuring $wrapper is not undefined, if exception occurs early + $result = null; // Assuring $result is not undefined, if exception occurs early try { $result = $this->db->select( array( 'logging', 'user' ), array( "{$logging}.*", 'user_name' ), // grab the user name @@ -270,8 +275,7 @@ class WikiExporter { __METHOD__, array( 'ORDER BY' => 'log_id', 'USE INDEX' => array( 'logging' => 'PRIMARY' ) ) ); - $wrapper = $this->db->resultObject( $result ); - $this->outputLogStream( $wrapper ); + $this->outputLogStream( $result ); if ( $this->buffer == WikiExporter::STREAM ) { $this->db->bufferResults( $prev ); } @@ -281,8 +285,8 @@ class WikiExporter { // Freeing result try { - if ( $wrapper ) { - $wrapper->free(); + if ( $result ) { + $result->free(); } } catch ( Exception $e2 ) { // Already in panic mode -> ignoring $e2 as $e has @@ -372,16 +376,15 @@ class WikiExporter { $prev = $this->db->bufferResults( false ); } - $wrapper = null; // Assuring $wrapper is not undefined, if exception occurs early + $result = null; // Assuring $result is not undefined, if exception occurs early try { wfRunHooks( 'ModifyExportQuery', array( $this->db, &$tables, &$cond, &$opts, &$join ) ); # Do the query! $result = $this->db->select( $tables, '*', $cond, __METHOD__, $opts, $join ); - $wrapper = $this->db->resultObject( $result ); # Output dump results - $this->outputPageStream( $wrapper ); + $this->outputPageStream( $result ); if ( $this->buffer == WikiExporter::STREAM ) { $this->db->bufferResults( $prev ); @@ -392,8 +395,8 @@ class WikiExporter { // Freeing result try { - if ( $wrapper ) { - $wrapper->free(); + if ( $result ) { + $result->free(); } } catch ( Exception $e2 ) { // Already in panic mode -> ignoring $e2 as $e has @@ -427,7 +430,7 @@ class WikiExporter { * separate database connection not managed by LoadBalancer; some * blob storage types will make queries to pull source data. * - * @param $resultset ResultWrapper + * @param ResultWrapper $resultset */ protected function outputPageStream( $resultset ) { $last = null; @@ -462,7 +465,7 @@ class WikiExporter { } /** - * @param $resultset array + * @param ResultWrapper $resultset */ protected function outputLogStream( $resultset ) { foreach ( $resultset as $row ) { @@ -478,7 +481,7 @@ class WikiExporter { class XmlDumpWriter { /** * Returns the export schema version. - * @deprecated in 1.20; use WikiExporter::schemaVersion() instead + * @deprecated since 1.20; use WikiExporter::schemaVersion() instead * @return string */ function schemaVersion() { @@ -502,8 +505,18 @@ class XmlDumpWriter { return Xml::element( 'mediawiki', array( 'xmlns' => "http://www.mediawiki.org/xml/export-$ver/", 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance", + /* + * When a new version of the schema is created, it needs staging on mediawiki.org. + * This requires a change in the operations/mediawiki-config git repo. + * + * Create a changeset like https://gerrit.wikimedia.org/r/#/c/149643/ in which + * you copy in the new xsd file. + * + * After it is reviewed, merged and deployed (sync-docroot), the index.html needs purging. + * echo "http://www.mediawiki.org/xml/index.html" | mwscript purgeList.php --wiki=aawiki + */ 'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " . - "http://www.mediawiki.org/xml/export-$ver.xsd", #TODO: how do we get a new version up there? + "http://www.mediawiki.org/xml/export-$ver.xsd", 'version' => $ver, 'xml:lang' => $wgLanguageCode ), null ) . @@ -517,6 +530,7 @@ class XmlDumpWriter { function siteInfo() { $info = array( $this->sitename(), + $this->dbname(), $this->homelink(), $this->generator(), $this->caseSetting(), @@ -534,6 +548,14 @@ class XmlDumpWriter { return Xml::element( 'sitename', array(), $wgSitename ); } + /** + * @return string + */ + function dbname() { + global $wgDBname; + return Xml::element( 'dbname', array(), $wgDBname ); + } + /** * @return string */ @@ -591,11 +613,10 @@ class XmlDumpWriter { * Opens a "" section on the output stream, with data * from the given database row. * - * @param $row object + * @param object $row * @return string - * @access private */ - function openPage( $row ) { + public function openPage( $row ) { $out = " \n"; $title = Title::makeTitle( $row->page_namespace, $row->page_title ); $out .= ' ' . Xml::elementClean( 'title', array(), self::canonicalTitle( $title ) ) . "\n"; @@ -604,8 +625,10 @@ class XmlDumpWriter { if ( $row->page_is_redirect ) { $page = WikiPage::factory( $title ); $redirect = $page->getRedirectTarget(); - if ( $redirect instanceOf Title && $redirect->isValidRedirectTarget() ) { - $out .= ' ' . Xml::element( 'redirect', array( 'title' => self::canonicalTitle( $redirect ) ) ) . "\n"; + if ( $redirect instanceof Title && $redirect->isValidRedirectTarget() ) { + $out .= ' '; + $out .= Xml::element( 'redirect', array( 'title' => self::canonicalTitle( $redirect ) ) ); + $out .= "\n"; } } @@ -633,7 +656,7 @@ class XmlDumpWriter { * Dumps a "" section on the output stream, with * data filled in from the given database row. * - * @param $row object + * @param object $row * @return string * @access private */ @@ -663,12 +686,30 @@ class XmlDumpWriter { $out .= " " . Xml::elementClean( 'comment', array(), strval( $row->rev_comment ) ) . "\n"; } + if ( isset( $row->rev_content_model ) && !is_null( $row->rev_content_model ) ) { + $content_model = strval( $row->rev_content_model ); + } else { + // probably using $wgContentHandlerUseDB = false; + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $content_model = ContentHandler::getDefaultModelFor( $title ); + } + + $content_handler = ContentHandler::getForModelID( $content_model ); + + if ( isset( $row->rev_content_format ) && !is_null( $row->rev_content_format ) ) { + $content_format = strval( $row->rev_content_format ); + } else { + // probably using $wgContentHandlerUseDB = false; + $content_format = $content_handler->getDefaultFormat(); + } + $text = ''; if ( isset( $row->rev_deleted ) && ( $row->rev_deleted & Revision::DELETED_TEXT ) ) { $out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n"; } elseif ( isset( $row->old_text ) ) { // Raw text from the database may have invalid chars $text = strval( Revision::getRevisionText( $row ) ); + $text = $content_handler->exportTransform( $text, $content_format ); $out .= " " . Xml::elementClean( 'text', array( 'xml:space' => 'preserve', 'bytes' => intval( $row->rev_len ) ), strval( $text ) ) . "\n"; @@ -679,32 +720,16 @@ class XmlDumpWriter { "" ) . "\n"; } - if ( isset( $row->rev_sha1 ) && $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) { + if ( isset( $row->rev_sha1 ) + && $row->rev_sha1 + && !( $row->rev_deleted & Revision::DELETED_TEXT ) + ) { $out .= " " . Xml::element( 'sha1', null, strval( $row->rev_sha1 ) ) . "\n"; } else { $out .= " \n"; } - if ( isset( $row->rev_content_model ) && !is_null( $row->rev_content_model ) ) { - $content_model = strval( $row->rev_content_model ); - } else { - // probably using $wgContentHandlerUseDB = false; - // @todo test! - $title = Title::makeTitle( $row->page_namespace, $row->page_title ); - $content_model = ContentHandler::getDefaultModelFor( $title ); - } - $out .= " " . Xml::element( 'model', null, strval( $content_model ) ) . "\n"; - - if ( isset( $row->rev_content_format ) && !is_null( $row->rev_content_format ) ) { - $content_format = strval( $row->rev_content_format ); - } else { - // probably using $wgContentHandlerUseDB = false; - // @todo test! - $content_handler = ContentHandler::getForModelID( $content_model ); - $content_format = $content_handler->getDefaultFormat(); - } - $out .= " " . Xml::element( 'format', null, strval( $content_format ) ) . "\n"; wfRunHooks( 'XmlDumpWriterWriteRevision', array( &$this, &$out, $row, $text ) ); @@ -719,7 +744,7 @@ class XmlDumpWriter { * Dumps a "" section on the output stream, with * data filled in from the given database row. * - * @param $row object + * @param object $row * @return string * @access private */ @@ -763,7 +788,7 @@ class XmlDumpWriter { } /** - * @param $timestamp string + * @param string $timestamp * @param string $indent Default to six spaces * @return string */ @@ -773,8 +798,8 @@ class XmlDumpWriter { } /** - * @param $id - * @param $text string + * @param int $id + * @param string $text * @param string $indent Default to six spaces * @return string */ @@ -792,8 +817,8 @@ class XmlDumpWriter { /** * Warning! This data is potentially inconsistent. :( - * @param $row - * @param $dumpContents bool + * @param object $row + * @param bool $dumpContents * @return string */ function writeUploads( $row, $dumpContents = false ) { @@ -812,8 +837,8 @@ class XmlDumpWriter { } /** - * @param $file File - * @param $dumpContents bool + * @param File $file + * @param bool $dumpContents * @return string */ function writeUpload( $file, $dumpContents = false ) { @@ -827,7 +852,7 @@ class XmlDumpWriter { $be = $file->getRepo()->getBackend(); # Dump file as base64 # Uses only XML-safe characters, so does not need escaping - # @TODO: too bad this loads the contents into memory (script might swap) + # @todo Too bad this loads the contents into memory (script might swap) $contents = ' ' . chunk_split( base64_encode( $be->getFileContents( array( 'src' => $file->getPath() ) ) ) ) . @@ -865,7 +890,7 @@ class XmlDumpWriter { * @since 1.18 */ public static function canonicalTitle( Title $title ) { - if ( $title->getInterwiki() ) { + if ( $title->isExternal() ) { return $title->getPrefixedText(); } @@ -887,45 +912,45 @@ class XmlDumpWriter { class DumpOutput { /** - * @param $string string + * @param string $string */ function writeOpenStream( $string ) { $this->write( $string ); } /** - * @param $string string + * @param string $string */ function writeCloseStream( $string ) { $this->write( $string ); } /** - * @param $page - * @param $string string + * @param object $page + * @param string $string */ function writeOpenPage( $page, $string ) { $this->write( $string ); } /** - * @param $string string + * @param string $string */ function writeClosePage( $string ) { $this->write( $string ); } /** - * @param $rev - * @param $string string + * @param object $rev + * @param string $string */ function writeRevision( $rev, $string ) { $this->write( $string ); } /** - * @param $rev - * @param $string string + * @param object $rev + * @param string $string */ function writeLogItem( $rev, $string ) { $this->write( $string ); @@ -933,7 +958,7 @@ class DumpOutput { /** * Override to write to a different stream type. - * @param $string string + * @param string $string * @return bool */ function write( $string ) { @@ -945,7 +970,7 @@ class DumpOutput { * 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 + * @param string|array $newname File name. May be a string or an array with one element */ function closeRenameAndReopen( $newname ) { } @@ -954,8 +979,9 @@ class DumpOutput { * 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 bool $open If true, a new file with the old filename will be opened again for writing (default: false) + * @param string|array $newname File name. May be a string or an array with one element + * @param bool $open If true, a new file with the old filename will be opened + * again for writing (default: false) */ function closeAndRename( $newname, $open = false ) { } @@ -978,7 +1004,7 @@ class DumpFileOutput extends DumpOutput { protected $handle = false, $filename; /** - * @param $file + * @param string $file */ function __construct( $file ) { $this->handle = fopen( $file, "wt" ); @@ -986,7 +1012,7 @@ class DumpFileOutput extends DumpOutput { } /** - * @param $string string + * @param string $string */ function writeCloseStream( $string ) { parent::writeCloseStream( $string ); @@ -997,21 +1023,21 @@ class DumpFileOutput extends DumpOutput { } /** - * @param $string string + * @param string $string */ function write( $string ) { fputs( $this->handle, $string ); } /** - * @param $newname + * @param string $newname */ function closeRenameAndReopen( $newname ) { $this->closeAndRename( $newname, true ); } /** - * @param $newname + * @param string $newname * @throws MWException */ function renameOrException( $newname ) { @@ -1021,8 +1047,8 @@ class DumpFileOutput extends DumpOutput { } /** - * @param $newname array - * @return mixed + * @param array $newname + * @return string * @throws MWException */ function checkRenameArgCount( $newname ) { @@ -1037,8 +1063,8 @@ class DumpFileOutput extends DumpOutput { } /** - * @param $newname mixed - * @param $open bool + * @param string $newname + * @param bool $open */ function closeAndRename( $newname, $open = false ) { $newname = $this->checkRenameArgCount( $newname ); @@ -1073,8 +1099,8 @@ class DumpPipeOutput extends DumpFileOutput { protected $procOpenResource = false; /** - * @param $command - * @param $file null + * @param string $command + * @param string $file */ function __construct( $command, $file = null ) { if ( !is_null( $file ) ) { @@ -1087,7 +1113,7 @@ class DumpPipeOutput extends DumpFileOutput { } /** - * @param $string string + * @param string $string */ function writeCloseStream( $string ) { parent::writeCloseStream( $string ); @@ -1098,7 +1124,7 @@ class DumpPipeOutput extends DumpFileOutput { } /** - * @param $command + * @param string $command */ function startCommand( $command ) { $spec = array( @@ -1110,15 +1136,15 @@ class DumpPipeOutput extends DumpFileOutput { } /** - * @param mixed $newname + * @param string $newname */ function closeRenameAndReopen( $newname ) { $this->closeAndRename( $newname, true ); } /** - * @param $newname mixed - * @param $open bool + * @param string $newname + * @param bool $open */ function closeAndRename( $newname, $open = false ) { $newname = $this->checkRenameArgCount( $newname ); @@ -1139,7 +1165,6 @@ class DumpPipeOutput extends DumpFileOutput { } } } - } /** @@ -1147,9 +1172,8 @@ class DumpPipeOutput extends DumpFileOutput { * @ingroup Dump */ class DumpGZipOutput extends DumpPipeOutput { - /** - * @param $file string + * @param string $file */ function __construct( $file ) { parent::__construct( "gzip", $file ); @@ -1161,9 +1185,8 @@ class DumpGZipOutput extends DumpPipeOutput { * @ingroup Dump */ class DumpBZip2Output extends DumpPipeOutput { - /** - * @param $file string + * @param string $file */ function __construct( $file ) { parent::__construct( "bzip2", $file ); @@ -1175,9 +1198,8 @@ class DumpBZip2Output extends DumpPipeOutput { * @ingroup Dump */ class Dump7ZipOutput extends DumpPipeOutput { - /** - * @param $file string + * @param string $file */ function __construct( $file ) { $command = $this->setup7zCommand( $file ); @@ -1186,7 +1208,7 @@ class Dump7ZipOutput extends DumpPipeOutput { } /** - * @param $file string + * @param string $file * @return string */ function setup7zCommand( $file ) { @@ -1198,8 +1220,8 @@ class Dump7ZipOutput extends DumpPipeOutput { } /** - * @param $newname string - * @param $open bool + * @param string $newname + * @param bool $open */ function closeAndRename( $newname, $open = false ) { $newname = $this->checkRenameArgCount( $newname ); @@ -1222,7 +1244,6 @@ class Dump7ZipOutput extends DumpPipeOutput { * @ingroup Dump */ class DumpFilter { - /** * @var DumpOutput * FIXME will need to be made protected whenever legacy code @@ -1236,29 +1257,29 @@ class DumpFilter { protected $sendingThisPage; /** - * @param $sink DumpOutput + * @param DumpOutput $sink */ function __construct( &$sink ) { $this->sink =& $sink; } /** - * @param $string string + * @param string $string */ function writeOpenStream( $string ) { $this->sink->writeOpenStream( $string ); } /** - * @param $string string + * @param string $string */ function writeCloseStream( $string ) { $this->sink->writeCloseStream( $string ); } /** - * @param $page - * @param $string string + * @param object $page + * @param string $string */ function writeOpenPage( $page, $string ) { $this->sendingThisPage = $this->pass( $page, $string ); @@ -1268,7 +1289,7 @@ class DumpFilter { } /** - * @param $string string + * @param string $string */ function writeClosePage( $string ) { if ( $this->sendingThisPage ) { @@ -1278,8 +1299,8 @@ class DumpFilter { } /** - * @param $rev - * @param $string string + * @param object $rev + * @param string $string */ function writeRevision( $rev, $string ) { if ( $this->sendingThisPage ) { @@ -1288,23 +1309,23 @@ class DumpFilter { } /** - * @param $rev - * @param $string string + * @param object $rev + * @param string $string */ function writeLogItem( $rev, $string ) { $this->sink->writeRevision( $rev, $string ); } /** - * @param $newname string + * @param string $newname */ function closeRenameAndReopen( $newname ) { $this->sink->closeRenameAndReopen( $newname ); } /** - * @param $newname string - * @param $open bool + * @param string $newname + * @param bool $open */ function closeAndRename( $newname, $open = false ) { $this->sink->closeAndRename( $newname, $open ); @@ -1319,7 +1340,7 @@ class DumpFilter { /** * Override for page-based filter types. - * @param $page + * @param object $page * @return bool */ function pass( $page ) { @@ -1332,9 +1353,8 @@ class DumpFilter { * @ingroup Dump */ class DumpNotalkFilter extends DumpFilter { - /** - * @param $page + * @param object $page * @return bool */ function pass( $page ) { @@ -1347,12 +1367,15 @@ class DumpNotalkFilter extends DumpFilter { * @ingroup Dump */ class DumpNamespaceFilter extends DumpFilter { - var $invert = false; - var $namespaces = array(); + /** @var bool */ + public $invert = false; + + /** @var array */ + public $namespaces = array(); /** - * @param $sink DumpOutput - * @param $param + * @param DumpOutput $sink + * @param array $param * @throws MWException */ function __construct( &$sink, $param ) { @@ -1398,7 +1421,7 @@ class DumpNamespaceFilter extends DumpFilter { } /** - * @param $page + * @param object $page * @return bool */ function pass( $page ) { @@ -1412,11 +1435,17 @@ class DumpNamespaceFilter extends DumpFilter { * @ingroup Dump */ class DumpLatestFilter extends DumpFilter { - var $page, $pageString, $rev, $revString; + public $page; + + public $pageString; + + public $rev; + + public $revString; /** - * @param $page - * @param $string string + * @param object $page + * @param string $string */ function writeOpenPage( $page, $string ) { $this->page = $page; @@ -1424,7 +1453,7 @@ class DumpLatestFilter extends DumpFilter { } /** - * @param $string string + * @param string $string */ function writeClosePage( $string ) { if ( $this->rev ) { @@ -1439,8 +1468,8 @@ class DumpLatestFilter extends DumpFilter { } /** - * @param $rev - * @param $string string + * @param object $rev + * @param string $string */ function writeRevision( $rev, $string ) { if ( $rev->rev_id == $this->page->page_latest ) { @@ -1457,7 +1486,7 @@ class DumpLatestFilter extends DumpFilter { class DumpMultiWriter { /** - * @param $sinks + * @param array $sinks */ function __construct( $sinks ) { $this->sinks = $sinks; @@ -1465,7 +1494,7 @@ class DumpMultiWriter { } /** - * @param $string string + * @param string $string */ function writeOpenStream( $string ) { for ( $i = 0; $i < $this->count; $i++ ) { @@ -1474,7 +1503,7 @@ class DumpMultiWriter { } /** - * @param $string string + * @param string $string */ function writeCloseStream( $string ) { for ( $i = 0; $i < $this->count; $i++ ) { @@ -1483,8 +1512,8 @@ class DumpMultiWriter { } /** - * @param $page - * @param $string string + * @param object $page + * @param string $string */ function writeOpenPage( $page, $string ) { for ( $i = 0; $i < $this->count; $i++ ) { @@ -1493,7 +1522,7 @@ class DumpMultiWriter { } /** - * @param $string + * @param string $string */ function writeClosePage( $string ) { for ( $i = 0; $i < $this->count; $i++ ) { @@ -1502,8 +1531,8 @@ class DumpMultiWriter { } /** - * @param $rev - * @param $string + * @param object $rev + * @param string $string */ function writeRevision( $rev, $string ) { for ( $i = 0; $i < $this->count; $i++ ) { @@ -1512,14 +1541,14 @@ class DumpMultiWriter { } /** - * @param $newnames + * @param array $newnames */ function closeRenameAndReopen( $newnames ) { $this->closeAndRename( $newnames, true ); } /** - * @param $newnames array + * @param array $newnames * @param bool $open */ function closeAndRename( $newnames, $open = false ) { @@ -1538,24 +1567,4 @@ class DumpMultiWriter { } return $filenames; } - -} - -/** - * @param $string string - * @return string - */ -function xmlsafe( $string ) { - wfProfileIn( __FUNCTION__ ); - - /** - * The page may contain old data which has not been properly normalized. - * Invalid UTF-8 sequences or forbidden control characters will make our - * XML output invalid, so be sure to strip them out. - */ - $string = UtfNormal::cleanUp( $string ); - - $string = htmlspecialchars( $string ); - wfProfileOut( __FUNCTION__ ); - return $string; } diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php deleted file mode 100644 index efa213fb..00000000 --- a/includes/FakeTitle.php +++ /dev/null @@ -1,141 +0,0 @@ -error(); } - function isTrans() { $this->error(); } - function getText() { $this->error(); } - function getPartialURL() { $this->error(); } - function getDBkey() { $this->error(); } - function getNamespace() { $this->error(); } - function getNsText() { $this->error(); } - function getUserCaseDBKey() { $this->error(); } - function getSubjectNsText() { $this->error(); } - function getTalkNsText() { $this->error(); } - function canTalk() { $this->error(); } - function getInterwiki() { $this->error(); } - function getFragment() { $this->error(); } - function getFragmentForURL() { $this->error(); } - function getDefaultNamespace() { $this->error(); } - function getIndexTitle() { $this->error(); } - function getPrefixedDBkey() { $this->error(); } - function getPrefixedText() { $this->error(); } - function getFullText() { $this->error(); } - function getBaseText() { $this->error(); } - function getSubpageText() { $this->error(); } - function getSubpageUrlForm() { $this->error(); } - function getPrefixedURL() { $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 = '', $query2 = false ) { $this->error(); } - function escapeFullURL( $query = '', $query2 = false ) { $this->error(); } - function getInternalURL( $query = '', $variant = false ) { $this->error(); } - function getEditURL() { $this->error(); } - function getEscapedText() { $this->error(); } - function isExternal() { $this->error(); } - function isSemiProtected( $action = 'edit' ) { $this->error(); } - function isProtected( $action = '' ) { $this->error(); } - function isConversionTable() { $this->error(); } - function userIsWatching() { $this->error(); } - function quickUserCan( $action, $user = null ) { $this->error(); } - function isNamespaceProtected( User $user ) { $this->error(); } - function userCan( $action, $user = null, $doExpensiveQueries = true ) { $this->error(); } - function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) { $this->error(); } - function updateTitleProtection( $create_perm, $reason, $expiry ) { $this->error(); } - function deleteTitleProtection() { $this->error(); } - function isMovable() { $this->error(); } - function userCanRead() { $this->error(); } - function isTalkPage() { $this->error(); } - function isSubpage() { $this->error(); } - function hasSubpages() { $this->error(); } - function getSubpages( $limit = -1 ) { $this->error(); } - function isCssJsSubpage() { $this->error(); } - function isCssOrJsPage() { $this->error(); } - function getSkinFromCssJsSubpage() { $this->error(); } - function isCssSubpage() { $this->error(); } - function isJsSubpage() { $this->error(); } - function userCanEditCssSubpage() { $this->error(); } - function userCanEditJsSubpage() { $this->error(); } - function isCascadeProtected() { $this->error(); } - function getCascadeProtectionSources( $get_pages = true ) { $this->error(); } - function areRestrictionsCascading() { $this->error(); } - function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { $this->error(); } - function loadRestrictions( $res = null ) { $this->error(); } - function getRestrictions( $action ) { $this->error(); } - function getRestrictionExpiry( $action ) { $this->error(); } - function isDeleted() { $this->error(); } - function isDeletedQuick() { $this->error(); } - function getArticleID( $flags = 0 ) { $this->error(); } - function isRedirect( $flags = 0 ) { $this->error(); } - function getLength( $flags = 0 ) { $this->error(); } - function getLatestRevID( $flags = 0 ) { $this->error(); } - function resetArticleID( $newid ) { $this->error(); } - function invalidateCache() { $this->error(); } - function getTalkPage() { $this->error(); } - function setFragment( $fragment ) { $this->error(); } - function getSubjectPage() { $this->error(); } - function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { $this->error(); } - function getTemplateLinksTo( $options = array() ) { $this->error(); } - function getBrokenLinksFrom() { $this->error(); } - function getSquidURLs() { $this->error(); } - function purgeSquid() { $this->error(); } - function moveNoAuth( &$nt ) { $this->error(); } - function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) { $this->error(); } - function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { $this->error(); } - function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) { $this->error(); } - function isSingleRevRedirect() { $this->error(); } - function isValidMoveTarget( $nt ) { $this->error(); } - function isWatchable() { $this->error(); } - function getParentCategories() { $this->error(); } - function getParentCategoryTree( $children = array() ) { $this->error(); } - function pageCond() { $this->error(); } - function getPreviousRevisionID( $revId, $flags = 0 ) { $this->error(); } - function getNextRevisionID( $revId, $flags = 0 ) { $this->error(); } - function getFirstRevision( $flags = 0 ) { $this->error(); } - function isNewPage() { $this->error(); } - function getEarliestRevTime( $flags = 0 ) { $this->error(); } - function countRevisionsBetween( $old, $new ) { $this->error(); } - function equals( Title $title ) { $this->error(); } - function exists() { $this->error(); } - function isAlwaysKnown() { $this->error(); } - function isKnown() { $this->error(); } - function canExist() { $this->error(); } - function touchLinks() { $this->error(); } - function getTouched( $db = null ) { $this->error(); } - function getNotificationTimestamp( $user = null ) { $this->error(); } - function getNamespaceKey( $prepend = 'nstab-' ) { $this->error(); } - function isSpecialPage() { $this->error(); } - function isSpecial( $name ) { $this->error(); } - function fixSpecialName() { $this->error(); } - function isContentPage() { $this->error(); } - function getRedirectsHere( $ns = null ) { $this->error(); } - function isValidRedirectTarget() { $this->error(); } - function getBacklinkCache() { $this->error(); } - function canUseNoindex() { $this->error(); } - function getRestrictionTypes() { $this->error(); } -} diff --git a/includes/Fallback.php b/includes/Fallback.php index cdf6c88e..8e7f4b7e 100644 --- a/includes/Fallback.php +++ b/includes/Fallback.php @@ -25,28 +25,6 @@ */ class Fallback { - /** - * @param $from - * @param $to - * @param $string - * @return string - */ - public static 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; - } - /** * Fallback implementation for mb_substr, hardcoded to UTF-8. * Attempts to be at least _moderately_ efficient; best optimized @@ -57,9 +35,9 @@ class Fallback { * can be up to 100x slower than native if the text is heavily * multibyte and we have to slog through a few hundred kb. * - * @param $str - * @param $start - * @param $count string + * @param string $str + * @param int $start + * @param string $count * * @return string */ @@ -78,8 +56,8 @@ class Fallback { } /** - * @param $str - * @param $splitPos + * @param string $str + * @param int $splitPos * @return int */ public static function mb_substr_split_unicode( $str, $splitPos ) { @@ -130,7 +108,7 @@ class Fallback { /** * Fallback implementation of mb_strlen, hardcoded to UTF-8. * @param string $str - * @param string $enc optional encoding; ignored + * @param string $enc Optional encoding; ignored * @return int */ public static function mb_strlen( $str, $enc = '' ) { @@ -151,10 +129,10 @@ class Fallback { /** * Fallback implementation of mb_strpos, hardcoded to UTF-8. - * @param $haystack String - * @param $needle String - * @param string $offset optional start position - * @param string $encoding optional encoding; ignored + * @param string $haystack + * @param string $needle + * @param string $offset Optional start position + * @param string $encoding Optional encoding; ignored * @return int */ public static function mb_strpos( $haystack, $needle, $offset = 0, $encoding = '' ) { @@ -172,10 +150,10 @@ class Fallback { /** * Fallback implementation of mb_strrpos, hardcoded to UTF-8. - * @param $haystack String - * @param $needle String - * @param string $offset optional start position - * @param string $encoding optional encoding; ignored + * @param string $haystack + * @param string $needle + * @param string $offset Optional start position + * @param string $encoding Optional encoding; ignored * @return int */ public static function mb_strrpos( $haystack, $needle, $offset = 0, $encoding = '' ) { diff --git a/includes/Feed.php b/includes/Feed.php index 635b04e4..2fdfa424 100644 --- a/includes/Feed.php +++ b/includes/Feed.php @@ -6,7 +6,7 @@ * Available feeds are defined in Defines.php * * Copyright © 2004 Brion Vibber - * http://www.mediawiki.org/ + * https://www.mediawiki.org/ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -36,28 +36,32 @@ * @ingroup Feed */ class FeedItem { - /** - * @var Title - */ - var $title; + /** @var Title */ + public $title; + + public $description; + + public $url; + + public $date; + + public $author; - var $description; - var $url; - var $date; - var $author; - var $uniqueId; - var $comments; - var $rssIsPermalink = false; + public $uniqueId; + + public $comments; + + public $rssIsPermalink = false; /** * Constructor * * @param string|Title $title Item's title - * @param $description String + * @param string $description * @param string $url URL uniquely designating the item. * @param string $date Item's date * @param string $author Author's user name - * @param $comments String + * @param string $comments */ function __construct( $title, $description, $url, $date = '', $author = '', $comments = '' ) { $this->title = $title; @@ -72,8 +76,8 @@ class FeedItem { /** * Encode $string so that it can be safely embedded in a XML document * - * @param string $string string to encode - * @return String + * @param string $string String to encode + * @return string */ public function xmlEncode( $string ) { $string = str_replace( "\r\n", "\n", $string ); @@ -84,7 +88,7 @@ class FeedItem { /** * Get the unique id of this item * - * @return String + * @return string */ public function getUniqueId() { if ( $this->uniqueId ) { @@ -93,10 +97,10 @@ class FeedItem { } /** - * set the unique id of an item + * Set the unique id of an item * - * @param string $uniqueId unique id for the item - * @param $rssIsPermalink Boolean: set to true if the guid (unique id) is a permalink (RSS feeds only) + * @param string $uniqueId Unique id for the item + * @param bool $rssIsPermalink Set to true if the guid (unique id) is a permalink (RSS feeds only) */ public function setUniqueId( $uniqueId, $rssIsPermalink = false ) { $this->uniqueId = $uniqueId; @@ -106,7 +110,7 @@ class FeedItem { /** * Get the title of this item; already xml-encoded * - * @return String + * @return string */ public function getTitle() { return $this->xmlEncode( $this->title ); @@ -115,7 +119,7 @@ class FeedItem { /** * Get the URL of this item; already xml-encoded * - * @return String + * @return string */ public function getUrl() { return $this->xmlEncode( $this->url ); @@ -124,7 +128,7 @@ class FeedItem { /** * Get the description of this item; already xml-encoded * - * @return String + * @return string */ public function getDescription() { return $this->xmlEncode( $this->description ); @@ -133,7 +137,7 @@ class FeedItem { /** * Get the language of this item * - * @return String + * @return string */ public function getLanguage() { global $wgLanguageCode; @@ -141,9 +145,9 @@ class FeedItem { } /** - * Get the title of this item + * Get the date of this item * - * @return String + * @return string */ public function getDate() { return $this->date; @@ -152,7 +156,7 @@ class FeedItem { /** * Get the author of this item; already xml-encoded * - * @return String + * @return string */ public function getAuthor() { return $this->xmlEncode( $this->author ); @@ -161,7 +165,7 @@ class FeedItem { /** * Get the comment of this item; already xml-encoded * - * @return String + * @return string */ public function getComments() { return $this->xmlEncode( $this->comments ); @@ -170,8 +174,8 @@ class FeedItem { /** * Quickie hack... strip out wikilinks to more legible form from the comment. * - * @param string $text wikitext - * @return String + * @param string $text Wikitext + * @return string */ public static function stripComment( $text ) { return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text ); @@ -190,7 +194,6 @@ abstract class ChannelFeed extends FeedItem { * @code * print ""; * @endcode - * @param $item */ abstract public function outHeader(); @@ -200,7 +203,7 @@ abstract class ChannelFeed extends FeedItem { * @code * print "..."; * @endcode - * @param $item + * @param FeedItem $item */ abstract public function outItem( $item ); @@ -239,28 +242,27 @@ abstract class ChannelFeed extends FeedItem { * Return an internet media type to be sent in the headers. * * @return string - * @private */ - function contentType() { + private function contentType() { global $wgRequest; + $ctype = $wgRequest->getVal( 'ctype', 'application/xml' ); - $allowedctypes = array( 'application/xml', 'text/xml', 'application/rss+xml', 'application/atom+xml' ); + $allowedctypes = array( + 'application/xml', + 'text/xml', + 'application/rss+xml', + 'application/atom+xml' + ); + return ( in_array( $ctype, $allowedctypes ) ? $ctype : 'application/xml' ); } /** - * Output the initial XML headers with a stylesheet for legibility - * if someone finds it in a browser. - * @private + * Output the initial XML headers. */ - function outXmlHeader() { - global $wgStylePath, $wgStyleVersion; - + protected function outXmlHeader() { $this->httpHeaders(); echo '' . "\n"; - echo '\n"; } } @@ -274,8 +276,8 @@ class RSSFeed extends ChannelFeed { /** * Format a date given a timestamp * - * @param $ts Integer: timestamp - * @return String: date string + * @param int $ts Timestamp + * @return string Date string */ function formatTime( $ts ) { return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) ); @@ -301,9 +303,10 @@ class RSSFeed extends ChannelFeed { /** * Output an RSS 2.0 item - * @param $item FeedItem: item to be output + * @param FeedItem $item Item to be output */ function outItem( $item ) { + // @codingStandardsIgnoreStart Ignore long lines and formatting issues. ?> <?php print $item->getTitle(); ?> @@ -315,6 +318,7 @@ class RSSFeed extends ChannelFeed { getComments() ) { ?>getComments(), PROTO_CURRENT ); ?> outXmlHeader(); + // @codingStandardsIgnoreStart Ignore long lines and formatting issues. ?> getFeedId() ?> <?php print $this->getTitle() ?> @@ -359,6 +365,7 @@ class AtomFeed extends ChannelFeed { MediaWiki getSelfUrl(); } /** * Atom 1.0 requests a self-reference to the feed. * @return string - * @private */ - function getSelfUrl() { + private function getSelfUrl() { global $wgRequest; return htmlspecialchars( $wgRequest->getFullRequestURL() ); } /** * Output a given item. - * @param $item + * @param FeedItem $item */ function outItem( $item ) { global $wgMimeType; + // @codingStandardsIgnoreStart Ignore long lines and formatting issues. ?> getUniqueId(); ?> @@ -413,5 +419,6 @@ class AtomFeed extends ChannelFeed { */ function outFooter() {?> rc_namespace, $row->rc_title ); @@ -94,15 +94,17 @@ class FeedUtils { /** * Really format a diff for the newsfeed * - * @param $title Title object - * @param $oldid Integer: old revision's id - * @param $newid Integer: new revision's id - * @param $timestamp Integer: new revision's timestamp - * @param string $comment new revision's comment - * @param string $actiontext text of the action; in case of log event - * @return String + * @param Title $title Title object + * @param int $oldid Old revision's id + * @param int $newid New revision's id + * @param int $timestamp New revision's timestamp + * @param string $comment New revision's comment + * @param string $actiontext Text of the action; in case of log event + * @return string */ - public static function formatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext = '' ) { + public static function formatDiffRow( $title, $oldid, $newid, $timestamp, + $comment, $actiontext = '' + ) { global $wgFeedDiffCutoff, $wgLang; wfProfileIn( __METHOD__ ); @@ -214,9 +216,9 @@ class FeedUtils { * Generates a diff link. Used when the full diff is not wanted for example * when $wgFeedDiffCutoff is 0. * - * @param $title Title object: used to generate the diff URL - * @param $newid Integer newid for this diff - * @param $oldid Integer|null oldid for the diff. Null means it is a new article + * @param Title $title Title object: used to generate the diff URL + * @param int $newid Newid for this diff + * @param int|null $oldid Oldid for the diff. Null means it is a new article * @return string */ protected static function getDiffLink( Title $title, $newid, $oldid = null ) { @@ -237,17 +239,23 @@ class FeedUtils { * Might be 'cleaner' to use DOM or XSLT or something, * but *gack* it's a pain in the ass. * - * @param string $text diff's HTML output - * @return String: modified HTML + * @param string $text Diff's HTML output + * @return string Modified HTML */ public static function applyDiffStyle( $text ) { $styles = array( 'diff' => 'background-color: white; color:black;', 'diff-otitle' => 'background-color: white; color:black; text-align: center;', 'diff-ntitle' => 'background-color: white; color:black; text-align: center;', - 'diff-addedline' => 'color:black; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;', - 'diff-deletedline' => 'color:black; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;', - 'diff-context' => 'background-color: #f9f9f9; color: #333333; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #e6e6e6; vertical-align: top; white-space: pre-wrap;', + 'diff-addedline' => 'color:black; font-size: 88%; border-style: solid; ' + . 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; ' + . 'vertical-align: top; white-space: pre-wrap;', + 'diff-deletedline' => 'color:black; font-size: 88%; border-style: solid; ' + . 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; ' + . 'vertical-align: top; white-space: pre-wrap;', + 'diff-context' => 'background-color: #f9f9f9; color: #333333; font-size: 88%; ' + . 'border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; ' + . 'border-color: #e6e6e6; vertical-align: top; white-space: pre-wrap;', 'diffchange' => 'font-weight: bold; text-decoration: none;', ); diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index 65d82b87..b4e24581 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -48,7 +48,7 @@ class FileDeleteForm { /** * Constructor * - * @param $file File object we're deleting + * @param File $file File object we're deleting */ public function __construct( $file ) { $this->title = $file->getTitle(); @@ -83,7 +83,10 @@ class FileDeleteForm { $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed( 'suppressrevision' ); if ( $this->oldimage ) { - $this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage ); + $this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( + $this->title, + $this->oldimage + ); } if ( !self::haveDeletableFile( $this->file, $this->oldfile, $this->oldimage ) ) { @@ -107,11 +110,20 @@ class FileDeleteForm { $reason = $deleteReasonList; } - $status = self::doDelete( $this->title, $this->file, $this->oldimage, $reason, $suppress, $wgUser ); + $status = self::doDelete( + $this->title, + $this->file, + $this->oldimage, + $reason, + $suppress, + $wgUser + ); if ( !$status->isGood() ) { $wgOut->addHTML( '

' . $this->prepareMessage( 'filedeleteerror-short' ) . "

\n" ); - $wgOut->addWikiText( '
' . $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) . '
' ); + $wgOut->addWikiText( '
' . + $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) + . '
' ); } if ( $status->ok ) { $wgOut->setPageTitle( wfMessage( 'actioncomplete' ) ); @@ -132,16 +144,18 @@ class FileDeleteForm { /** * Really delete the file * - * @param $title Title object - * @param File $file: file object - * @param string $oldimage archive name - * @param string $reason reason of the deletion - * @param $suppress Boolean: whether to mark all deleted versions as restricted - * @param $user User object performing the request + * @param Title $title + * @param File $file + * @param string $oldimage Archive name + * @param string $reason Reason of the deletion + * @param bool $suppress Whether to mark all deleted versions as restricted + * @param User $user User object performing the request * @throws MWException * @return bool|Status */ - public static function doDelete( &$title, &$file, &$oldimage, $reason, $suppress, User $user = null ) { + public static function doDelete( &$title, &$file, &$oldimage, $reason, + $suppress, User $user = null + ) { if ( $user === null ) { global $wgUser; $user = $wgUser; @@ -149,7 +163,7 @@ class FileDeleteForm { if ( $oldimage ) { $page = null; - $status = $file->deleteOld( $oldimage, $reason, $suppress ); + $status = $file->deleteOld( $oldimage, $reason, $suppress, $user ); if ( $status->ok ) { // Need to do a log item $logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text(); @@ -180,7 +194,7 @@ class FileDeleteForm { // doDeleteArticleReal() returns a non-fatal error status if the page // or revision is missing, so check for isOK() rather than isGood() if ( $deleteStatus->isOK() ) { - $status = $file->delete( $reason, $suppress ); + $status = $file->delete( $reason, $suppress, $user ); if ( $status->isOK() ) { $dbw->commit( __METHOD__ ); } else { @@ -188,7 +202,8 @@ class FileDeleteForm { } } } catch ( MWException $e ) { - // rollback before returning to prevent UI from displaying incorrect "View or restore N deleted edits?" + // Rollback before returning to prevent UI from displaying + // incorrect "View or restore N deleted edits?" $dbw->rollback( __METHOD__ ); throw $e; } @@ -266,8 +281,14 @@ class FileDeleteForm { " . - Xml::submitButton( wfMessage( 'filedelete-submit' )->text(), - array( 'name' => 'mw-filedelete-submit', 'id' => 'mw-filedelete-submit', 'tabindex' => '4' ) ) . + Xml::submitButton( + wfMessage( 'filedelete-submit' )->text(), + array( + 'name' => 'mw-filedelete-submit', + 'id' => 'mw-filedelete-submit', + 'tabindex' => '4' + ) + ) . " " . Xml::closeElement( 'table' ) . @@ -303,14 +324,16 @@ class FileDeleteForm { * showing an appropriate message depending upon whether * it's a current file or an old version * - * @param string $message message base - * @return String + * @param string $message Message base + * @return string */ private function prepareMessage( $message ) { global $wgLang; if ( $this->oldimage ) { + # Message keys used: + # 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old' return wfMessage( - "{$message}-old", # To ensure grep will find them: 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old' + "{$message}-old", wfEscapeWikiText( $this->title->getText() ), $wgLang->date( $this->getTimestamp(), true ), $wgLang->time( $this->getTimestamp(), true ), @@ -336,6 +359,7 @@ class FileDeleteForm { /** * Is the provided `oldimage` value valid? * + * @param string $oldimage * @return bool */ public static function isValidOldSpec( $oldimage ) { @@ -349,9 +373,9 @@ class FileDeleteForm { * value was provided, does it correspond to an * existing, local, old version of this file? * - * @param $file File - * @param $oldfile File - * @param $oldimage File + * @param File $file + * @param File $oldfile + * @param File $oldimage * @return bool */ public static function haveDeletableFile( &$file, &$oldfile, $oldimage ) { diff --git a/includes/ForkController.php b/includes/ForkController.php index ced45af6..c1765e24 100644 --- a/includes/ForkController.php +++ b/includes/ForkController.php @@ -30,11 +30,11 @@ * @ingroup Maintenance */ class ForkController { - var $children = array(); - var $termReceived = false; - var $flags = 0, $procsToStart = 0; + protected $children = array(), $childNumber = 0; + protected $termReceived = false; + protected $flags = 0, $procsToStart = 0; - static $restartableSignals = array( + protected static $restartableSignals = array( SIGFPE, SIGILL, SIGSEGV, @@ -137,6 +137,16 @@ class ForkController { return 'done'; } + /** + * Get the number of the child currently running. Note, this + * is not the pid, but rather which of the total number of children + * we are + * @return int + */ + public function getChildNumber() { + return $this->childNumber; + } + protected function prepareEnvironment() { global $wgMemc; // Don't share DB, storage, or memcached connections @@ -150,6 +160,7 @@ class ForkController { /** * Fork a number of worker processes. * + * @param int $numProcs * @return string */ protected function forkWorkers( $numProcs ) { @@ -166,6 +177,7 @@ class ForkController { if ( !$pid ) { $this->initChild(); + $this->childNumber = $i; return 'child'; } else { // This is the parent process diff --git a/includes/FormOptions.php b/includes/FormOptions.php index 54822e32..c91c3367 100644 --- a/includes/FormOptions.php +++ b/includes/FormOptions.php @@ -4,6 +4,7 @@ * * Copyright © 2008, Niklas Laxström * Copyright © 2011, Antoine Musso + * Copyright © 2013, Bartosz Dziewoński * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -42,6 +43,9 @@ class FormOptions implements ArrayAccess { const STRING = 0; /** Integer type, maps guessType() to WebRequest::getInt() */ const INT = 1; + /** Float type, maps guessType() to WebRequest::getFloat() + * @since 1.23 */ + const FLOAT = 4; /** Boolean type, maps guessType() to WebRequest::getBool() */ const BOOL = 2; /** Integer type or null, maps to WebRequest::getIntOrNull() @@ -112,6 +116,8 @@ class FormOptions implements ArrayAccess { return self::BOOL; } elseif ( is_int( $data ) ) { return self::INT; + } elseif ( is_float( $data ) ) { + return self::FLOAT; } elseif ( is_string( $data ) ) { return self::STRING; } else { @@ -234,19 +240,29 @@ class FormOptions implements ArrayAccess { } /** - * Validate and set an option integer value - * The value will be altered to fit in the range. + * @see validateBounds() + */ + public function validateIntBounds( $name, $min, $max ) { + $this->validateBounds( $name, $min, $max ); + } + + /** + * Constrain a numeric value for a given option to a given range. The value will be altered to fit + * in the range. * - * @param string $name option name - * @param int $min minimum value - * @param int $max maximum value + * @since 1.23 + * + * @param string $name Option name + * @param int|float $min Minimum value + * @param int|float $max Maximum value * @throws MWException If option is not of type INT */ - public function validateIntBounds( $name, $min, $max ) { + public function validateBounds( $name, $min, $max ) { $this->validateName( $name, true ); + $type = $this->options[$name]['type']; - if ( $this->options[$name]['type'] !== self::INT ) { - throw new MWException( "Option $name is not of type int" ); + if ( $type !== self::INT && $type !== self::FLOAT ) { + throw new MWException( "Option $name is not of type INT or FLOAT" ); } $value = $this->getValueReal( $this->options[$name] ); @@ -333,6 +349,9 @@ class FormOptions implements ArrayAccess { case self::INT: $value = $r->getInt( $name, $default ); break; + case self::FLOAT: + $value = $r->getFloat( $name, $default ); + break; case self::STRING: $value = $r->getText( $name, $default ); break; @@ -354,22 +373,37 @@ class FormOptions implements ArrayAccess { * @see http://php.net/manual/en/class.arrayaccess.php */ /* @{ */ - /** Whether the option exists. */ + /** + * Whether the option exists. + * @param string $name + * @return bool + */ public function offsetExists( $name ) { return isset( $this->options[$name] ); } - /** Retrieve an option value. */ + /** + * Retrieve an option value. + * @param string $name + * @return mixed + */ public function offsetGet( $name ) { return $this->getValue( $name ); } - /** Set an option to given value. */ + /** + * Set an option to given value. + * @param string $name + * @param mixed $value + */ public function offsetSet( $name, $value ) { $this->setValue( $name, $value ); } - /** Delete the option. */ + /** + * Delete the option. + * @param string $name + */ public function offsetUnset( $name ) { $this->delete( $name ); } diff --git a/includes/GitInfo.php b/includes/GitInfo.php index f49f9be1..7052820e 100644 --- a/includes/GitInfo.php +++ b/includes/GitInfo.php @@ -35,33 +35,103 @@ class GitInfo { */ protected $basedir; + /** + * Path to JSON cache file for pre-computed git information. + */ + protected $cacheFile; + + /** + * Cached git information. + */ + protected $cache = array(); + /** * Map of repo URLs to viewer URLs. Access via static method getViewers(). */ private static $viewers = false; /** - * @param string $dir The root directory of the repo where the .git dir can be found + * @param string $repoDir The root directory of the repo where .git can be found + * @param bool $usePrecomputed Use precomputed information if available + * @see precomputeValues + */ + public function __construct( $repoDir, $usePrecomputed = true ) { + $this->cacheFile = self::getCacheFilePath( $repoDir ); + wfDebugLog( 'gitinfo', + "Computed cacheFile={$this->cacheFile} for {$repoDir}" + ); + if ( $usePrecomputed && + $this->cacheFile !== null && + is_readable( $this->cacheFile ) + ) { + $this->cache = FormatJson::decode( + file_get_contents( $this->cacheFile ), + true + ); + wfDebugLog( 'gitinfo', "Loaded git data from cache for {$repoDir}" ); + } + + if ( !$this->cacheIsComplete() ) { + wfDebugLog( 'gitinfo', "Cache incomplete for {$repoDir}" ); + $this->basedir = $repoDir . DIRECTORY_SEPARATOR . '.git'; + if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) { + $GITfile = file_get_contents( $this->basedir ); + if ( strlen( $GITfile ) > 8 && + substr( $GITfile, 0, 8 ) === 'gitdir: ' + ) { + $path = rtrim( substr( $GITfile, 8 ), "\r\n" ); + if ( $path[0] === '/' || substr( $path, 1, 1 ) === ':' ) { + // Path from GITfile is absolute + $this->basedir = $path; + } else { + $this->basedir = $repoDir . DIRECTORY_SEPARATOR . $path; + } + } + } + } + } + + /** + * Compute the path to the cache file for a given directory. + * + * @param string $repoDir The root directory of the repo where .git can be found + * @return string Path to GitInfo cache file in $wgGitInfoCacheDirectory or + * null if $wgGitInfoCacheDirectory is false (cache disabled). + * @since 1.24 */ - public function __construct( $dir ) { - $this->basedir = "{$dir}/.git"; - if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) { - $GITfile = file_get_contents( $this->basedir ); - if ( strlen( $GITfile ) > 8 && substr( $GITfile, 0, 8 ) === 'gitdir: ' ) { - $path = rtrim( substr( $GITfile, 8 ), "\r\n" ); - $isAbsolute = $path[0] === '/' || substr( $path, 1, 1 ) === ':'; - $this->basedir = $isAbsolute ? $path : "{$dir}/{$path}"; + protected static function getCacheFilePath( $repoDir ) { + global $IP, $wgGitInfoCacheDirectory; + + if ( $wgGitInfoCacheDirectory ) { + // Convert both $IP and $repoDir to canonical paths to protect against + // $IP having changed between the settings files and runtime. + $realIP = realpath( $IP ); + $repoName = realpath( $repoDir ); + if ( $repoName === false ) { + // Unit tests use fake path names + $repoName = $repoDir; + } + if ( strpos( $repoName, $realIP ) === 0 ) { + // Strip $IP from path + $repoName = substr( $repoName, strlen( $realIP ) ); } + // Transform path to git repo to something we can safely embed in + // a filename + $repoName = strtr( $repoName, DIRECTORY_SEPARATOR, '-' ); + $fileName = 'info' . $repoName . '.json'; + return "{$wgGitInfoCacheDirectory}/{$fileName}"; } + return null; } /** - * Return a singleton for the repo at $IP + * Get the singleton for the repo at $IP + * * @return GitInfo */ public static function repo() { - global $IP; if ( is_null( self::$repo ) ) { + global $IP; self::$repo = new self( $IP ); } return self::$repo; @@ -78,50 +148,56 @@ class GitInfo { } /** - * Return the HEAD of the repo (without any opening "ref: ") - * @return string The HEAD + * Get the HEAD of the repo (without any opening "ref: ") + * + * @return string|bool The HEAD (git reference or SHA1) or false */ public function getHead() { - $HEADfile = "{$this->basedir}/HEAD"; - - if ( !is_readable( $HEADfile ) ) { - return false; - } + if ( !isset( $this->cache['head'] ) ) { + $headFile = "{$this->basedir}/HEAD"; + $head = false; - $HEAD = file_get_contents( $HEADfile ); + if ( is_readable( $headFile ) ) { + $head = file_get_contents( $headFile ); - if ( preg_match( "/ref: (.*)/", $HEAD, $m ) ) { - return rtrim( $m[1] ); - } else { - return rtrim( $HEAD ); + if ( preg_match( "/ref: (.*)/", $head, $m ) ) { + $head = rtrim( $m[1] ); + } else { + $head = rtrim( $head ); + } + } + $this->cache['head'] = $head; } + return $this->cache['head']; } /** - * Return the SHA1 for the current HEAD of the repo - * @return string A SHA1 or false + * Get the SHA1 for the current HEAD of the repo + * + * @return string|bool A SHA1 or false */ public function getHeadSHA1() { - $HEAD = $this->getHead(); - - // If detached HEAD may be a SHA1 - if ( self::isSHA1( $HEAD ) ) { - return $HEAD; - } - - // If not a SHA1 it may be a ref: - $REFfile = "{$this->basedir}/{$HEAD}"; - if ( !is_readable( $REFfile ) ) { - return false; + if ( !isset( $this->cache['headSHA1'] ) ) { + $head = $this->getHead(); + $sha1 = false; + + // If detached HEAD may be a SHA1 + if ( self::isSHA1( $head ) ) { + $sha1 = $head; + } else { + // If not a SHA1 it may be a ref: + $refFile = "{$this->basedir}/{$head}"; + if ( is_readable( $refFile ) ) { + $sha1 = rtrim( file_get_contents( $refFile ) ); + } + } + $this->cache['headSHA1'] = $sha1; } - - $sha1 = rtrim( file_get_contents( $REFfile ) ); - - return $sha1; + return $this->cache['headSHA1']; } /** - * Return the commit date of HEAD entry of the git code repository + * Get the commit date of HEAD entry of the git code repository * * @since 1.22 * @return int|bool Commit date (UNIX timestamp) or false @@ -129,66 +205,54 @@ class GitInfo { public function getHeadCommitDate() { global $wgGitBin; - if ( !is_file( $wgGitBin ) || !is_executable( $wgGitBin ) ) { - return false; - } - - $environment = array( "GIT_DIR" => $this->basedir ); - $cmd = wfEscapeShellArg( $wgGitBin ) . " show -s --format=format:%ct HEAD"; - $retc = false; - $commitDate = wfShellExec( $cmd, $retc, $environment ); - - if ( $retc !== 0 ) { - return false; - } else { - return (int)$commitDate; + if ( !isset( $this->cache['headCommitDate'] ) ) { + $date = false; + if ( is_file( $wgGitBin ) && + is_executable( $wgGitBin ) && + $this->getHead() !== false + ) { + $environment = array( "GIT_DIR" => $this->basedir ); + $cmd = wfEscapeShellArg( $wgGitBin ) . + " show -s --format=format:%ct HEAD"; + $retc = false; + $commitDate = wfShellExec( $cmd, $retc, $environment ); + if ( $retc === 0 ) { + $date = (int)$commitDate; + } + } + $this->cache['headCommitDate'] = $date; } - - } + return $this->cache['headCommitDate']; + } /** - * Return the name of the current branch, or HEAD if not found - * @return string The branch name, HEAD, or false + * Get the name of the current branch, or HEAD if not found + * + * @return string|bool The branch name, HEAD, or false */ public function getCurrentBranch() { - $HEAD = $this->getHead(); - if ( $HEAD && preg_match( "#^refs/heads/(.*)$#", $HEAD, $m ) ) { - return $m[1]; - } else { - return $HEAD; + if ( !isset( $this->cache['branch'] ) ) { + $branch = $this->getHead(); + if ( $branch && + preg_match( "#^refs/heads/(.*)$#", $branch, $m ) + ) { + $branch = $m[1]; + } + $this->cache['branch'] = $branch; } + return $this->cache['branch']; } /** * Get an URL to a web viewer link to the HEAD revision. * - * @return string|bool string if a URL is available or false otherwise. + * @return string|bool String if a URL is available or false otherwise */ public function getHeadViewUrl() { - $config = "{$this->basedir}/config"; - if ( !is_readable( $config ) ) { - return false; - } - - $configArray = parse_ini_file( $config, true ); - $remote = false; - - // Use the "origin" remote repo if available or any other repo if not. - if ( isset( $configArray['remote origin'] ) ) { - $remote = $configArray['remote origin']; - } else { - foreach ( $configArray as $sectionName => $sectionConf ) { - if ( substr( $sectionName, 0, 6 ) == 'remote' ) { - $remote = $sectionConf; - } - } - } - - if ( $remote === false || !isset( $remote['url'] ) ) { + $url = $this->getRemoteUrl(); + if ( $url === false ) { return false; } - - $url = $remote['url']; if ( substr( $url, -4 ) !== '.git' ) { $url .= '.git'; } @@ -208,6 +272,93 @@ class GitInfo { return false; } + /** + * Get the URL of the remote origin. + * @return string|bool String if a URL is available or false otherwise. + */ + protected function getRemoteUrl() { + if ( !isset( $this->cache['remoteURL'] ) ) { + $config = "{$this->basedir}/config"; + $url = false; + if ( is_readable( $config ) ) { + wfSuppressWarnings(); + $configArray = parse_ini_file( $config, true ); + wfRestoreWarnings(); + $remote = false; + + // Use the "origin" remote repo if available or any other repo if not. + if ( isset( $configArray['remote origin'] ) ) { + $remote = $configArray['remote origin']; + } elseif ( is_array( $configArray ) ) { + foreach ( $configArray as $sectionName => $sectionConf ) { + if ( substr( $sectionName, 0, 6 ) == 'remote' ) { + $remote = $sectionConf; + } + } + } + + if ( $remote !== false && isset( $remote['url'] ) ) { + $url = $remote['url']; + } + } + $this->cache['remoteURL'] = $url; + } + return $this->cache['remoteURL']; + } + + /** + * Check to see if the current cache is fully populated. + * + * Note: This method is public only to make unit testing easier. There's + * really no strong reason that anything other than a test should want to + * call this method. + * + * @return bool True if all expected cache keys exist, false otherwise + */ + public function cacheIsComplete() { + return isset( $this->cache['head'] ) && + isset( $this->cache['headSHA1'] ) && + isset( $this->cache['headCommitDate'] ) && + isset( $this->cache['branch'] ) && + isset( $this->cache['remoteURL'] ); + } + + /** + * Precompute and cache git information. + * + * Creates a JSON file in the cache directory associated with this + * GitInfo instance. This cache file will be used by subsequent GitInfo objects referencing + * the same directory to avoid needing to examine the .git directory again. + * + * @since 1.24 + */ + public function precomputeValues() { + if ( $this->cacheFile !== null ) { + // Try to completely populate the cache + $this->getHead(); + $this->getHeadSHA1(); + $this->getHeadCommitDate(); + $this->getCurrentBranch(); + $this->getRemoteUrl(); + + if ( !$this->cacheIsComplete() ) { + wfDebugLog( 'gitinfo', + "Failed to compute GitInfo for \"{$this->basedir}\"" + ); + return; + } + + $cacheDir = dirname( $this->cacheFile ); + if ( !file_exists( $cacheDir ) && + !wfMkdirParents( $cacheDir, null, __METHOD__ ) + ) { + throw new MWException( "Unable to create GitInfo cache \"{$cacheDir}\"" ); + } + + file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) ); + } + } + /** * @see self::getHeadSHA1 * @return string diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 77c09e53..27f7cacb 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -35,19 +35,10 @@ if ( !defined( 'MEDIAWIKI' ) ) { * PHP extensions may be included here. */ -if ( !function_exists( 'iconv' ) ) { - /** - * @codeCoverageIgnore - * @return string - */ - function iconv( $from, $to, $string ) { - return Fallback::iconv( $from, $to, $string ); - } -} - if ( !function_exists( 'mb_substr' ) ) { /** * @codeCoverageIgnore + * @see Fallback::mb_substr * @return string */ function mb_substr( $str, $start, $count = 'end' ) { @@ -56,6 +47,7 @@ if ( !function_exists( 'mb_substr' ) ) { /** * @codeCoverageIgnore + * @see Fallback::mb_substr_split_unicode * @return int */ function mb_substr_split_unicode( $str, $splitPos ) { @@ -66,6 +58,7 @@ if ( !function_exists( 'mb_substr' ) ) { if ( !function_exists( 'mb_strlen' ) ) { /** * @codeCoverageIgnore + * @see Fallback::mb_strlen * @return int */ function mb_strlen( $str, $enc = '' ) { @@ -76,17 +69,18 @@ if ( !function_exists( 'mb_strlen' ) ) { if ( !function_exists( 'mb_strpos' ) ) { /** * @codeCoverageIgnore + * @see Fallback::mb_strpos * @return int */ function mb_strpos( $haystack, $needle, $offset = 0, $encoding = '' ) { return Fallback::mb_strpos( $haystack, $needle, $offset, $encoding ); } - } if ( !function_exists( 'mb_strrpos' ) ) { /** * @codeCoverageIgnore + * @see Fallback::mb_strrpos * @return int */ function mb_strrpos( $haystack, $needle, $offset = 0, $encoding = '' ) { @@ -99,18 +93,77 @@ if ( !function_exists( 'mb_strrpos' ) ) { if ( !function_exists( 'gzdecode' ) ) { /** * @codeCoverageIgnore + * @param string $data * @return string */ function gzdecode( $data ) { return gzinflate( substr( $data, 10, -8 ) ); } } + +// hash_equals function only exists in PHP >= 5.6.0 +// http://php.net/hash_equals +if ( !function_exists( 'hash_equals' ) ) { + /** + * Check whether a user-provided string is equal to a fixed-length secret string + * without revealing bytes of the secret string through timing differences. + * + * The usual way to compare strings (PHP's === operator or the underlying memcmp() + * function in C) is to compare corresponding bytes and stop at the first difference, + * which would take longer for a partial match than for a complete mismatch. This + * is not secure when one of the strings (e.g. an HMAC or token) must remain secret + * and the other may come from an attacker. Statistical analysis of timing measurements + * over many requests may allow the attacker to guess the string's bytes one at a time + * (and check his guesses) even if the timing differences are extremely small. + * + * When making such a security-sensitive comparison, it is essential that the sequence + * in which instructions are executed and memory locations are accessed not depend on + * the secret string's value. HOWEVER, for simplicity, we do not attempt to minimize + * the inevitable leakage of the string's length. That is generally known anyway as + * a chararacteristic of the hash function used to compute the secret value. + * + * Longer explanation: http://www.emerose.com/timing-attacks-explained + * + * @codeCoverageIgnore + * @param string $known_string Fixed-length secret string to compare against + * @param string $user_string User-provided string + * @return bool True if the strings are the same, false otherwise + */ + function hash_equals( $known_string, $user_string ) { + // Strict type checking as in PHP's native implementation + if ( !is_string( $known_string ) ) { + trigger_error( 'hash_equals(): Expected known_string to be a string, ' . + gettype( $known_string ) . ' given', E_USER_WARNING ); + + return false; + } + + if ( !is_string( $user_string ) ) { + trigger_error( 'hash_equals(): Expected user_string to be a string, ' . + gettype( $user_string ) . ' given', E_USER_WARNING ); + + return false; + } + + $known_string_len = strlen( $known_string ); + if ( $known_string_len !== strlen( $user_string ) ) { + return false; + } + + $result = 0; + for ( $i = 0; $i < $known_string_len; $i++ ) { + $result |= ord( $known_string[$i] ) ^ ord( $user_string[$i] ); + } + + return ( $result === 0 ); + } +} /// @endcond /** * Like array_diff( $a, $b ) except that it works with two-dimensional arrays. - * @param $a array - * @param $b array + * @param array $a + * @param array $b * @return array */ function wfArrayDiff2( $a, $b ) { @@ -118,8 +171,8 @@ function wfArrayDiff2( $a, $b ) { } /** - * @param $a array|string - * @param $b array|string + * @param array|string $a + * @param array|string $b * @return int */ function wfArrayDiff2_cmp( $a, $b ) { @@ -140,28 +193,13 @@ function wfArrayDiff2_cmp( $a, $b ) { } } -/** - * Array lookup - * Returns an array where the values in array $b are replaced by the - * values in array $a with the corresponding keys - * - * @deprecated since 1.22; use array_intersect_key() - * @param $a Array - * @param $b Array - * @return array - */ -function wfArrayLookup( $a, $b ) { - wfDeprecated( __FUNCTION__, '1.22' ); - 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 array $changed to alter + * @param string|int $key + * @param mixed $value + * @param mixed $default + * @param array $changed Array to alter * @throws MWException */ function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) { @@ -173,26 +211,6 @@ function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) { } } -/** - * Backwards array plus for people who haven't bothered to read the PHP manual - * XXX: will not darn your socks for you. - * - * @deprecated since 1.22; use array_replace() - * @param $array1 Array - * @param [$array2, [...]] Arrays - * @return Array - */ -function wfArrayMerge( $array1/* ... */ ) { - wfDeprecated( __FUNCTION__, '1.22' ); - $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. @@ -208,8 +226,9 @@ function wfArrayMerge( $array1/* ... */ ) { * array( 'x' ), * array( 'y' ) * ) - * @param varargs - * @return Array + * + * @param array $array1,... + * @return array */ function wfMergeErrorArrays( /*...*/ ) { $args = func_get_args(); @@ -230,8 +249,8 @@ function wfMergeErrorArrays( /*...*/ ) { * * @param array $array The array. * @param array $insert The array to insert. - * @param $after Mixed: The key to insert after - * @return Array + * @param mixed $after The key to insert after + * @return array */ function wfArrayInsertAfter( array $array, array $insert, $after ) { // Find the offset of the element to insert after. @@ -252,9 +271,9 @@ function wfArrayInsertAfter( array $array, array $insert, $after ) { /** * Recursively converts the parameter (an object) to an array with the same data * - * @param $objOrArray Object|Array - * @param $recursive Bool - * @return Array + * @param object|array $objOrArray + * @param bool $recursive + * @return array */ function wfObjectToArray( $objOrArray, $recursive = true ) { $array = array(); @@ -283,8 +302,8 @@ function wfRandom() { # The maximum random value is "only" 2^31-1, so get two random # values to reduce the chance of dupes $max = mt_getrandmax() + 1; - $rand = number_format( ( mt_rand() * $max + mt_rand() ) - / $max / $max, 12, '.', '' ); + $rand = number_format( ( mt_rand() * $max + mt_rand() ) / $max / $max, 12, '.', '' ); + return $rand; } @@ -295,7 +314,7 @@ function wfRandom() { * of token please use MWCryptRand instead. * * @param int $length The length of the string to generate - * @return String + * @return string * @since 1.20 */ function wfRandomString( $length = 32 ) { @@ -325,11 +344,12 @@ function wfRandomString( $length = 32 ) { * * %2F in the page titles seems to fatally break for some reason. * - * @param $s String: + * @param string $s * @return string */ function wfUrlencode( $s ) { static $needle; + if ( is_null( $s ) ) { $needle = null; return ''; @@ -337,7 +357,9 @@ function wfUrlencode( $s ) { 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 ) ) { + if ( !isset( $_SERVER['SERVER_SOFTWARE'] ) || + ( strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/7' ) === false ) + ) { $needle[] = '%3A'; } } @@ -359,8 +381,8 @@ function wfUrlencode( $s ) { * * @param array $array1 ( String|Array ) * @param array $array2 ( String|Array ) - * @param $prefix String - * @return String + * @param string $prefix + * @return string */ function wfArrayToCgi( $array1, $array2 = null, $prefix = '' ) { if ( !is_null( $array2 ) ) { @@ -404,8 +426,8 @@ function wfArrayToCgi( $array1, $array2 = null, $prefix = '' ) { * with legacy functions that accept raw query strings instead of nice * arrays. Of course, keys and values are urldecode()d. * - * @param string $query query string - * @return array Array version of input + * @param string $query Query string + * @return string[] Array version of input */ function wfCgiToArray( $query ) { if ( isset( $query[0] ) && $query[0] == '?' ) { @@ -450,8 +472,8 @@ function wfCgiToArray( $query ) { * 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 + * @param string $url + * @param string|string[] $query String or associative array * @return string */ function wfAppendQuery( $url, $query ) { @@ -470,39 +492,41 @@ function wfAppendQuery( $url, $query ) { } /** - * Expand a potentially local URL to a fully-qualified URL. Assumes $wgServer + * 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_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 string $url 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 + * @param string $url Either fully-qualified or a local path + query + * @param string $defaultProto One of the PROTO_* constants. Determines the + * protocol to use if $url or $wgServer is protocol-relative * @return string Fully-qualified URL, current-path-relative URL or false if - * no valid URL can be constructed + * no valid URL can be constructed */ function wfExpandUrl( $url, $defaultProto = PROTO_CURRENT ) { - global $wgServer, $wgCanonicalServer, $wgInternalServer; - $serverUrl = $wgServer; + global $wgServer, $wgCanonicalServer, $wgInternalServer, $wgRequest, + $wgHttpsPort; if ( $defaultProto === PROTO_CANONICAL ) { $serverUrl = $wgCanonicalServer; - } - // Make $wgInternalServer fall back to $wgServer if not set - if ( $defaultProto === PROTO_INTERNAL && $wgInternalServer !== false ) { + } elseif ( $defaultProto === PROTO_INTERNAL && $wgInternalServer !== false ) { + // Make $wgInternalServer fall back to $wgServer if not set $serverUrl = $wgInternalServer; - } - if ( $defaultProto === PROTO_CURRENT ) { - $defaultProto = WebRequest::detectProtocol() . '://'; + } else { + $serverUrl = $wgServer; + if ( $defaultProto === PROTO_CURRENT ) { + $defaultProto = $wgRequest->getProtocol() . '://'; + } } // Analyze $serverUrl to obtain its protocol @@ -513,8 +537,9 @@ function wfExpandUrl( $url, $defaultProto = PROTO_CURRENT ) { 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 + // $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; } } @@ -524,11 +549,19 @@ function wfExpandUrl( $url, $defaultProto = PROTO_CURRENT ) { if ( substr( $url, 0, 2 ) == '//' ) { $url = $defaultProtoWithoutSlashes . $url; } elseif ( substr( $url, 0, 1 ) == '/' ) { - // If $serverUrl is protocol-relative, prepend $defaultProtoWithoutSlashes, otherwise leave it alone + // If $serverUrl is protocol-relative, prepend $defaultProtoWithoutSlashes, + // otherwise leave it alone. $url = ( $serverHasProto ? '' : $defaultProtoWithoutSlashes ) . $serverUrl . $url; } $bits = wfParseUrl( $url ); + + // ensure proper port for HTTPS arrives in URL + // https://bugzilla.wikimedia.org/show_bug.cgi?id=65184 + if ( $defaultProto === PROTO_HTTPS && $wgHttpsPort != 443 ) { + $bits['port'] = $wgHttpsPort; + } + if ( $bits && isset( $bits['path'] ) ) { $bits['path'] = wfRemoveDotSegments( $bits['path'] ); return wfAssembleUrl( $bits ); @@ -685,7 +718,7 @@ function wfRemoveDotSegments( $urlPath ) { * * @param bool $includeProtocolRelative If false, remove '//' from the returned protocol list. * DO NOT USE this directly, use wfUrlProtocolsWithoutProtRel() instead - * @return String + * @return string */ function wfUrlProtocols( $includeProtocolRelative = true ) { global $wgUrlProtocols; @@ -730,7 +763,7 @@ function wfUrlProtocols( $includeProtocolRelative = true ) { * 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 - * @return String + * @return string */ function wfUrlProtocolsWithoutProtRel() { return wfUrlProtocols( false ); @@ -739,18 +772,20 @@ function wfUrlProtocolsWithoutProtRel() { /** * 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)) + * 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 string $url a URL to parse - * @return Array: bits of the URL in an associative array, per PHP docs + * @param string $url A URL to parse + * @return string[] 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 + // 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"; @@ -812,11 +847,15 @@ function wfParseUrl( $url ) { * * @todo handle punycode domains too * - * @param $url string + * @param string $url * @return string */ function wfExpandIRI( $url ) { - return preg_replace_callback( '/((?:%[89A-F][0-9A-F])+)/i', 'wfExpandIRI_callback', wfExpandUrl( $url ) ); + return preg_replace_callback( + '/((?:%[89A-F][0-9A-F])+)/i', + 'wfExpandIRI_callback', + wfExpandUrl( $url ) + ); } /** @@ -831,7 +870,7 @@ function wfExpandIRI_callback( $matches ) { /** * Make URL indexes, appropriate for the el_index field of externallinks. * - * @param $url String + * @param string $url * @return array */ function wfMakeUrlIndexes( $url ) { @@ -908,30 +947,41 @@ function wfMatchesDomainList( $url, $domains ) { * * Controlling globals: * $wgDebugLogFile - points to the log file - * $wgProfileOnly - if set, normal debug messages will not be recorded. * $wgDebugRawPage - if false, 'action=raw' hits will not result in debug output. * $wgDebugComments - if on, some debug items may appear in comments in the HTML output. * - * @param $text String - * @param bool $logonly set true to avoid appearing in HTML when $wgDebugComments is set + * @param string $text + * @param string|bool $dest Destination of the message: + * - 'all': both to the log and HTML (debug toolbar or HTML comments) + * - 'log': only to the log and not in HTML + * For backward compatibility, it can also take a boolean: + * - true: same as 'all' + * - false: same as 'log' */ -function wfDebug( $text, $logonly = false ) { - global $wgDebugLogFile, $wgProfileOnly, $wgDebugRawPage, $wgDebugLogPrefix; +function wfDebug( $text, $dest = 'all' ) { + global $wgDebugLogFile, $wgDebugRawPage, $wgDebugLogPrefix; if ( !$wgDebugRawPage && wfIsDebugRawPage() ) { return; } + // Turn $dest into a string if it's a boolean (for b/c) + if ( $dest === true ) { + $dest = 'all'; + } elseif ( $dest === false ) { + $dest = 'log'; + } + $timer = wfDebugTimer(); if ( $timer !== '' ) { $text = preg_replace( '/[^\n]/', $timer . '\0', $text, 1 ); } - if ( !$logonly ) { + if ( $dest === 'all' ) { MWDebug::debugMsg( $text ); } - if ( $wgDebugLogFile != '' && !$wgProfileOnly ) { + if ( $wgDebugLogFile != '' ) { # 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 ); @@ -954,8 +1004,8 @@ function wfIsDebugRawPage() { || ( isset( $_SERVER['SCRIPT_NAME'] ) && substr( $_SERVER['SCRIPT_NAME'], -8 ) == 'load.php' - ) ) - { + ) + ) { $cache = true; } else { $cache = false; @@ -983,44 +1033,84 @@ function wfDebugTimer() { /** * Send a line giving PHP memory usage. * - * @param bool $exact print exact values instead of kilobytes (default: false) + * @param bool $exact Print exact byte values instead of kibibytes (default: false) */ function wfDebugMem( $exact = false ) { $mem = memory_get_usage(); if ( !$exact ) { - $mem = floor( $mem / 1024 ) . ' kilobytes'; + $mem = floor( $mem / 1024 ) . ' KiB'; } else { - $mem .= ' bytes'; + $mem .= ' B'; } wfDebug( "Memory usage: $mem\n" ); } /** * Send a line to a supplementary debug log file, if configured, or main debug log if not. - * $wgDebugLogGroups[$logGroup] should be set to a filename to send to a separate log. - * - * @param $logGroup String - * @param $text String - * @param bool $public whether to log the event in the public log if no private - * log file is specified, (default true) - */ -function wfDebugLog( $logGroup, $text, $public = true ) { + * To configure a supplementary log file, set $wgDebugLogGroups[$logGroup] to a string + * filename or an associative array mapping 'destination' to the desired filename. The + * associative array may also contain a 'sample' key with an integer value, specifying + * a sampling factor. + * + * @since 1.23 support for sampling log messages via $wgDebugLogGroups. + * + * @param string $logGroup + * @param string $text + * @param string|bool $dest Destination of the message: + * - 'all': both to the log and HTML (debug toolbar or HTML comments) + * - 'log': only to the log and not in HTML + * - 'private': only to the specifc log if set in $wgDebugLogGroups and + * discarded otherwise + * For backward compatibility, it can also take a boolean: + * - true: same as 'all' + * - false: same as 'private' + */ +function wfDebugLog( $logGroup, $text, $dest = 'all' ) { global $wgDebugLogGroups; + $text = trim( $text ) . "\n"; - if ( isset( $wgDebugLogGroups[$logGroup] ) ) { - $time = wfTimestamp( TS_DB ); - $wiki = wfWikiID(); - $host = wfHostname(); - wfErrorLog( "$time $host $wiki: $text", $wgDebugLogGroups[$logGroup] ); - } elseif ( $public === true ) { - wfDebug( "[$logGroup] $text", false ); + + // Turn $dest into a string if it's a boolean (for b/c) + if ( $dest === true ) { + $dest = 'all'; + } elseif ( $dest === false ) { + $dest = 'private'; + } + + if ( !isset( $wgDebugLogGroups[$logGroup] ) ) { + if ( $dest !== 'private' ) { + wfDebug( "[$logGroup] $text", $dest ); + } + return; + } + + if ( $dest === 'all' ) { + MWDebug::debugMsg( "[$logGroup] $text" ); + } + + $logConfig = $wgDebugLogGroups[$logGroup]; + if ( $logConfig === false ) { + return; + } + if ( is_array( $logConfig ) ) { + if ( isset( $logConfig['sample'] ) && mt_rand( 1, $logConfig['sample'] ) !== 1 ) { + return; + } + $destination = $logConfig['destination']; + } else { + $destination = strval( $logConfig ); } + + $time = wfTimestamp( TS_DB ); + $wiki = wfWikiID(); + $host = wfHostname(); + wfErrorLog( "$time $host $wiki: $text", $destination ); } /** * Log for database errors * - * @param string $text database error message. + * @param string $text Database error message. */ function wfLogDBError( $text ) { global $wgDBerrorLog, $wgDBerrorLogTZ; @@ -1044,7 +1134,7 @@ function wfLogDBError( $text ) { $date = $d->format( 'D M j G:i:s T Y' ); - $text = "$date\t$host\t$wiki\t$text"; + $text = "$date\t$host\t$wiki\t" . trim( $text ) . "\n"; wfErrorLog( $text, $wgDBerrorLog ); } } @@ -1052,10 +1142,11 @@ function wfLogDBError( $text ) { /** * Throws a warning that $function is deprecated * - * @param $function String - * @param string|bool $version Version of MediaWiki that the function was deprecated in (Added in 1.19). + * @param string $function + * @param string|bool $version Version of MediaWiki that the function + * was deprecated in (Added in 1.19). * @param string|bool $component Added in 1.19. - * @param $callerOffset integer: How far up the call stack is the original + * @param int $callerOffset How far up the call stack is the original * caller. 2 = function that called the function that called * wfDeprecated (Added in 1.20) * @@ -1069,10 +1160,10 @@ function wfDeprecated( $function, $version = false, $component = false, $callerO * Send a warning either to the debug log or in a PHP error depending on * $wgDevelopmentWarnings. To log warnings in production, use wfLogWarning() instead. * - * @param string $msg message to send - * @param $callerOffset Integer: number of items to go back in the backtrace to + * @param string $msg Message to send + * @param int $callerOffset Number of items to go back in the backtrace to * find the correct caller (1 = function calling wfWarn, ...) - * @param $level Integer: PHP error level; defaults to E_USER_NOTICE; + * @param int $level PHP error level; defaults to E_USER_NOTICE; * only used when $wgDevelopmentWarnings is true */ function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) { @@ -1083,10 +1174,10 @@ function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) { * Send a warning as a PHP error and the debug log. This is intended for logging * warnings in production. For logging development warnings, use WfWarn instead. * - * @param $msg String: message to send - * @param $callerOffset Integer: number of items to go back in the backtrace to + * @param string $msg Message to send + * @param int $callerOffset Number of items to go back in the backtrace to * find the correct caller (1 = function calling wfLogWarning, ...) - * @param $level Integer: PHP error level; defaults to E_USER_WARNING + * @param int $level PHP error level; defaults to E_USER_WARNING */ function wfLogWarning( $msg, $callerOffset = 1, $level = E_USER_WARNING ) { MWDebug::warning( $msg, $callerOffset + 1, $level, 'production' ); @@ -1098,8 +1189,8 @@ function wfLogWarning( $msg, $callerOffset = 1, $level = E_USER_WARNING ) { * 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 string $file filename + * @param string $text + * @param string $file Filename * @throws MWException */ function wfErrorLog( $text, $file ) { @@ -1161,8 +1252,8 @@ function wfErrorLog( $text, $file ) { * @todo document */ function wfLogProfilingData() { - global $wgRequestTime, $wgDebugLogFile, $wgDebugRawPage, $wgRequest; - global $wgProfileLimit, $wgUser; + global $wgRequestTime, $wgDebugLogFile, $wgDebugLogGroups, $wgDebugRawPage; + global $wgProfileLimit, $wgUser, $wgRequest; StatCounter::singleton()->flush(); @@ -1183,7 +1274,17 @@ function wfLogProfilingData() { $profiler->logData(); // Check whether this should be logged in the debug file. - if ( $wgDebugLogFile == '' || ( !$wgDebugRawPage && wfIsDebugRawPage() ) ) { + if ( isset( $wgDebugLogGroups['profileoutput'] ) + && $wgDebugLogGroups['profileoutput'] === false + ) { + // Explicitely disabled + return; + } + if ( !isset( $wgDebugLogGroups['profileoutput'] ) && $wgDebugLogFile == '' ) { + // Logging not enabled; no point going further + return; + } + if ( !$wgDebugRawPage && wfIsDebugRawPage() ) { return; } @@ -1218,14 +1319,14 @@ function wfLogProfilingData() { gmdate( 'YmdHis' ), $elapsed, urldecode( $requestUrl . $forward ) ); - wfErrorLog( $log . $profiler->getOutput(), $wgDebugLogFile ); + wfDebugLog( 'profileoutput', $log . $profiler->getOutput() ); } /** * Increment a statistics counter * - * @param $key String - * @param $count Int + * @param string $key + * @param int $count * @return void */ function wfIncrStats( $key, $count = 1 ) { @@ -1244,7 +1345,7 @@ function wfReadOnly() { /** * Get the value of $wgReadOnly or the contents of $wgReadOnlyFile. * - * @return string|bool: String when in read-only mode; false otherwise + * @return string|bool String when in read-only mode; false otherwise */ function wfReadOnlyReason() { global $wgReadOnly, $wgReadOnlyFile; @@ -1264,7 +1365,7 @@ function wfReadOnlyReason() { /** * Return a Language object from $langcode * - * @param $langcode Mixed: either: + * @param Language|string|bool $langcode Either: * - a Language object * - code of the language to get the message for, if it is * a valid code create a language for that language, if @@ -1274,7 +1375,7 @@ function wfReadOnlyReason() { * the current user's language (as a fallback for the old parameter * functionality), or if it is true then use global object * for the wiki's content language. - * @return Language object + * @return Language */ function wfGetLangObj( $langcode = false ) { # Identify which language to get or create a language object for. @@ -1309,19 +1410,6 @@ function wfGetLangObj( $langcode = false ) { return $wgContLang; } -/** - * Old function when $wgBetterDirectionality existed - * All usage removed, wfUILang can be removed in near future - * - * @deprecated since 1.18 - * @return Language - */ -function wfUILang() { - wfDeprecated( __METHOD__, '1.18' ); - global $wgLang; - return $wgLang; -} - /** * This is the function for getting translated interface messages. * @@ -1330,12 +1418,15 @@ function wfUILang() { * * This function replaces all old wfMsg* functions. * - * @param $key \string Message key. - * Varargs: normal message parameters. + * @param string|string[] $key Message key, or array of keys + * @param mixed $params,... Normal message parameters * @return Message + * * @since 1.17 + * + * @see Message::__construct */ -function wfMessage( $key /*...*/) { +function wfMessage( $key /*...*/ ) { $params = func_get_args(); array_shift( $params ); if ( isset( $params[0] ) && is_array( $params[0] ) ) { @@ -1348,9 +1439,13 @@ function wfMessage( $key /*...*/) { * 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 + * + * @param string|string[] $keys,... Message keys * @return Message + * * @since 1.18 + * + * @see Message::newFallbackSequence */ function wfMessageFallback( /*...*/ ) { $args = func_get_args(); @@ -1365,7 +1460,7 @@ function wfMessageFallback( /*...*/ ) { * * @deprecated since 1.18 * - * @param string $key lookup key for the message, usually + * @param string $key Lookup key for the message, usually * defined in languages/Language.php * * Parameters to the message, which can be used to insert variable text into @@ -1374,7 +1469,7 @@ function wfMessageFallback( /*...*/ ) { * - As an array in the second parameter * These are not shown in the function definition. * - * @return String + * @return string */ function wfMsg( $key ) { wfDeprecated( __METHOD__, '1.21' ); @@ -1389,8 +1484,8 @@ function wfMsg( $key ) { * * @deprecated since 1.18 * - * @param $key String - * @return String + * @param string $key + * @return string */ function wfMsgNoTrans( $key ) { wfDeprecated( __METHOD__, '1.21' ); @@ -1421,9 +1516,9 @@ function wfMsgNoTrans( $key ) { * * @deprecated since 1.18 * - * @param string $key lookup key for the message, usually + * @param string $key Lookup key for the message, usually * defined in languages/Language.php - * @return String + * @return string */ function wfMsgForContent( $key ) { wfDeprecated( __METHOD__, '1.21' ); @@ -1432,9 +1527,9 @@ function wfMsgForContent( $key ) { $args = func_get_args(); array_shift( $args ); $forcontent = true; - if ( is_array( $wgForceUIMsgAsContentMsg ) && - in_array( $key, $wgForceUIMsgAsContentMsg ) ) - { + if ( is_array( $wgForceUIMsgAsContentMsg ) + && in_array( $key, $wgForceUIMsgAsContentMsg ) + ) { $forcontent = false; } return wfMsgReal( $key, $args, true, $forcontent ); @@ -1445,8 +1540,8 @@ function wfMsgForContent( $key ) { * * @deprecated since 1.18 * - * @param $key String - * @return String + * @param string $key + * @return string */ function wfMsgForContentNoTrans( $key ) { wfDeprecated( __METHOD__, '1.21' ); @@ -1455,9 +1550,9 @@ function wfMsgForContentNoTrans( $key ) { $args = func_get_args(); array_shift( $args ); $forcontent = true; - if ( is_array( $wgForceUIMsgAsContentMsg ) && - in_array( $key, $wgForceUIMsgAsContentMsg ) ) - { + if ( is_array( $wgForceUIMsgAsContentMsg ) + && in_array( $key, $wgForceUIMsgAsContentMsg ) + ) { $forcontent = false; } return wfMsgReal( $key, $args, true, $forcontent, false ); @@ -1468,12 +1563,12 @@ function wfMsgForContentNoTrans( $key ) { * * @deprecated since 1.18 * - * @param string $key key to get. - * @param $args - * @param $useDB Boolean - * @param $forContent Mixed: Language code, or false for user lang, true for content lang. - * @param $transform Boolean: Whether or not to transform the message. - * @return String: the requested message. + * @param string $key Key to get. + * @param array $args + * @param bool $useDB + * @param string|bool $forContent Language code, or false for user lang, true for content lang. + * @param bool $transform Whether or not to transform the message. + * @return string The requested message. */ function wfMsgReal( $key, $args, $useDB = true, $forContent = false, $transform = true ) { wfDeprecated( __METHOD__, '1.21' ); @@ -1490,11 +1585,11 @@ function wfMsgReal( $key, $args, $useDB = true, $forContent = false, $transform * * @deprecated since 1.18 * - * @param $key String - * @param $useDB Bool - * @param string $langCode Code of the language to get the message for, or - * behaves as a content language switch if it is a boolean. - * @param $transform Boolean: whether to parse magic words, etc. + * @param string $key + * @param bool $useDB + * @param string|bool $langCode Code of the language to get the message for, or + * behaves as a content language switch if it is a boolean. + * @param bool $transform Whether to parse magic words, etc. * @return string */ function wfMsgGetKey( $key, $useDB = true, $langCode = false, $transform = true ) { @@ -1515,8 +1610,8 @@ function wfMsgGetKey( $key, $useDB = true, $langCode = false, $transform = true /** * Replace message parameter keys on the given formatted output. * - * @param $message String - * @param $args Array + * @param string $message + * @param array $args * @return string * @private */ @@ -1549,8 +1644,8 @@ function wfMsgReplaceArgs( $message, $args ) { * * @deprecated since 1.18 * - * @param $key String - * @param string ... parameters + * @param string $key + * @param string $args,... Parameters * @return string */ function wfMsgHtml( $key ) { @@ -1570,8 +1665,8 @@ function wfMsgHtml( $key ) { * * @deprecated since 1.18 * - * @param $key String - * @param string ... parameters + * @param string $key + * @param string $args,... Parameters * @return string */ function wfMsgWikiHtml( $key ) { @@ -1590,22 +1685,23 @@ function wfMsgWikiHtml( $key ) { * * @deprecated since 1.18 * - * @param string $key key of the message - * @param array $options processing rules. Can take the following options: - * parse: parses wikitext to HTML - * parseinline: parses wikitext to HTML and removes the surrounding + * @param string $key Key of the message + * @param array $options Processing rules. + * Can take the following options: + * parse: parses wikitext to HTML + * parseinline: parses wikitext to HTML and removes the surrounding * p's added by parser or tidy - * escape: filters message through htmlspecialchars - * escapenoentities: same, but allows entity references like   through - * replaceafter: parameters are substituted after parsing or escaping - * parsemag: transform the message using magic phrases - * 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 - * (overridden by content). + * escape: filters message through htmlspecialchars + * escapenoentities: same, but allows entity references like   through + * replaceafter: parameters are substituted after parsing or escaping + * parsemag: transform the message using magic phrases + * 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 + * (overridden by content). * Behavior for conflicting options (e.g., parse+parseinline) is undefined. * - * @return String + * @return string */ function wfMsgExt( $key, $options ) { wfDeprecated( __METHOD__, '1.21' ); @@ -1656,10 +1752,7 @@ function wfMsgExt( $key, $options ) { } if ( $parseInline ) { - $m = array(); - if ( preg_match( '/^

(.*)\n?<\/p>\n?$/sU', $string, $m ) ) { - $string = $m[1]; - } + $string = Parser::stripOuterParagraph( $string ); } } elseif ( in_array( 'parsemag', $options, true ) ) { $string = $messageCache->transform( $string, @@ -1686,8 +1779,8 @@ function wfMsgExt( $key, $options ) { * * @deprecated since 1.18. Use Message::isDisabled(). * - * @param $key String: the message key looked up - * @return Boolean True if the message *doesn't* exist. + * @param string $key The message key looked up + * @return bool True if the message *doesn't* exist. */ function wfEmptyMsg( $key ) { wfDeprecated( __METHOD__, '1.21' ); @@ -1695,19 +1788,6 @@ function wfEmptyMsg( $key ) { return MessageCache::singleton()->get( $key, /*useDB*/true, /*content*/false ) === false; } -/** - * Throw a debugging exception. This function previously once exited the process, - * but now throws an exception instead, with similar results. - * - * @deprecated since 1.22; just throw an MWException yourself - * @param string $msg message shown when dying. - * @throws MWException - */ -function wfDebugDieBacktrace( $msg = '' ) { - wfDeprecated( __FUNCTION__, '1.22' ); - throw new MWException( $msg ); -} - /** * Fetch server name for use in error reporting etc. * Use real server name if available, so we know which machine @@ -1747,52 +1827,42 @@ function wfHostname() { } /** - * Returns a HTML comment with the elapsed time since request. - * This method has no side effects. + * Returns a script tag that stores the amount of time it took MediaWiki to + * handle the request in milliseconds as 'wgBackendResponseTime'. + * + * If $wgShowHostnames is true, the script will also set 'wgHostname' to the + * hostname of the server handling the request. * * @return string */ function wfReportTime() { global $wgRequestTime, $wgShowHostnames; - $elapsed = microtime( true ) - $wgRequestTime; - - return $wgShowHostnames - ? sprintf( '', wfHostname(), $elapsed ) - : sprintf( '', $elapsed ); + $responseTime = round( ( microtime( true ) - $wgRequestTime ) * 1000 ); + $reportVars = array( 'wgBackendResponseTime' => $responseTime ); + if ( $wgShowHostnames ) { + $reportVars['wgHostname'] = wfHostname(); + } + return Skin::makeVariablesScript( $reportVars ); } /** * Safety wrapper for debug_backtrace(). * - * With Zend Optimizer 3.2.0 loaded, this causes segfaults under somewhat - * murky circumstances, which may be triggered in part by stub objects - * or other fancy talking'. - * - * Will return an empty array if Zend Optimizer is detected or if - * debug_backtrace is disabled, otherwise the output from - * debug_backtrace() (trimmed). + * Will return an empty array if debug_backtrace is disabled, otherwise + * the output from debug_backtrace() (trimmed). * * @param int $limit This parameter can be used to limit the number of stack frames returned * - * @return array of backtrace information + * @return array Array of backtrace information */ function wfDebugBacktrace( $limit = 0 ) { static $disabled = null; - if ( extension_loaded( 'Zend Optimizer' ) ) { - wfDebug( "Zend Optimizer detected; skipping debug_backtrace for safety.\n" ); - return array(); - } - if ( is_null( $disabled ) ) { - $disabled = false; - $functions = explode( ',', ini_get( 'disable_functions' ) ); - $functions = array_map( 'trim', $functions ); - $functions = array_map( 'strtolower', $functions ); - if ( in_array( 'debug_backtrace', $functions ) ) { - wfDebug( "debug_backtrace is in disabled_functions\n" ); - $disabled = true; + $disabled = !function_exists( 'debug_backtrace' ); + if ( $disabled ) { + wfDebug( "debug_backtrace() is disabled\n" ); } } if ( $disabled ) { @@ -1863,7 +1933,7 @@ function wfBacktrace() { * wfGetCaller( 2 ) [default] is the caller of the function running wfGetCaller() * wfGetCaller( 3 ) is the parent of that. * - * @param $level Int + * @param int $level * @return string */ function wfGetCaller( $level = 2 ) { @@ -1879,9 +1949,8 @@ function wfGetCaller( $level = 2 ) { * Return a string consisting of callers in the stack. Useful sometimes * for profiling specific points. * - * @param int $limit The maximum depth of the stack frame to return, or false for - * the entire stack. - * @return String + * @param int $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() ); @@ -1895,7 +1964,7 @@ function wfGetAllCallers( $limit = 3 ) { /** * Return a string representation of frame * - * @param $frame Array + * @param array $frame * @return string */ function wfFormatStackFrame( $frame ) { @@ -1909,49 +1978,19 @@ function wfFormatStackFrame( $frame ) { /** * @todo document * - * @param $offset Int - * @param $limit Int - * @return String + * @param int $offset + * @param int $limit + * @return string */ function wfShowingResults( $offset, $limit ) { return wfMessage( 'showingresults' )->numParams( $limit, $offset + 1 )->parse(); } -/** - * Generate (prev x| next x) (20|50|100...) type links for paging - * - * @param $offset String - * @param $limit Integer - * @param $link String - * @param string $query optional URL query parameter string - * @param bool $atend optional param for specified if this is the last page - * @return String - * @deprecated in 1.19; use Language::viewPrevNext() instead - */ -function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { - wfDeprecated( __METHOD__, '1.19' ); - - global $wgLang; - - $query = wfCgiToArray( $query ); - - if ( is_object( $link ) ) { - $title = $link; - } else { - $title = Title::newFromText( $link ); - if ( is_null( $title ) ) { - return false; - } - } - - return $wgLang->viewPrevNext( $title, $offset, $limit, $query, $atend ); -} - /** * @todo document * @todo FIXME: We may want to blacklist some broken browsers * - * @param $force Bool + * @param bool $force * @return bool Whereas client accept gzip compression */ function wfClientAcceptsGzip( $force = false ) { @@ -1962,11 +2001,11 @@ function wfClientAcceptsGzip( $force = false ) { # @todo FIXME: We may want to blacklist some broken browsers $m = array(); if ( preg_match( - '/\bgzip(?:;(q)=([0-9]+(?:\.[0-9]+)))?\b/', - $_SERVER['HTTP_ACCEPT_ENCODING'], - $m ) - ) - { + '/\bgzip(?:;(q)=([0-9]+(?:\.[0-9]+)))?\b/', + $_SERVER['HTTP_ACCEPT_ENCODING'], + $m + ) + ) { if ( isset( $m[2] ) && ( $m[1] == 'q' ) && ( $m[2] == 0 ) ) { $result = false; return $result; @@ -1983,13 +2022,14 @@ function wfClientAcceptsGzip( $force = false ) { * Obtain the offset and limit values from the request string; * used in special pages * - * @param int $deflimit default limit if none supplied + * @param int $deflimit Default limit if none supplied * @param string $optionname Name of a user preference to check against * @return array - * + * @deprecated since 1.24, just call WebRequest::getLimitOffset() directly */ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) { global $wgRequest; + wfDeprecated( __METHOD__, '1.24' ); return $wgRequest->getLimitOffset( $deflimit, $optionname ); } @@ -1999,8 +2039,8 @@ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) { * is achieved by substituting certain characters with HTML entities. * As required by the callers, "" is not used. * - * @param string $text text to be escaped - * @return String + * @param string $text Text to be escaped + * @return string */ function wfEscapeWikiText( $text ) { static $repl = null, $repl2 = null; @@ -2044,25 +2084,15 @@ function wfEscapeWikiText( $text ) { return $text; } -/** - * Get the current unix timestamp with microseconds. Useful for profiling - * @deprecated since 1.22; call microtime() directly - * @return Float - */ -function wfTime() { - wfDeprecated( __FUNCTION__, '1.22' ); - return microtime( true ); -} - /** * 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 + * @param mixed $dest + * @param mixed $source + * @param bool $force + * @return mixed */ function wfSetVar( &$dest, $source, $force = false ) { $temp = $dest; @@ -2075,9 +2105,9 @@ function wfSetVar( &$dest, $source, $force = false ) { /** * As for wfSetVar except setting a bit * - * @param $dest Int - * @param $bit Int - * @param $state Bool + * @param int $dest + * @param int $bit + * @param bool $state * * @return bool */ @@ -2097,7 +2127,7 @@ function wfSetBit( &$dest, $bit, $state = true ) { * A wrapper around the PHP function var_export(). * Either print it or add it to the regular output ($wgOut). * - * @param $var mixed A PHP variable to dump. + * @param mixed $var A PHP variable to dump. */ function wfVarDump( $var ) { global $wgOut; @@ -2112,9 +2142,9 @@ function wfVarDump( $var ) { /** * Provide a simple HTTP error. * - * @param $code Int|String - * @param $label String - * @param $desc String + * @param int|string $code + * @param string $label + * @param string $desc */ function wfHttpError( $code, $label, $desc ) { global $wgOut; @@ -2149,7 +2179,7 @@ function wfHttpError( $code, $label, $desc ) { * Note that some PHP configuration options may add output buffer * layers which cannot be removed; these are left in place. * - * @param $resetGzipEncoding Bool + * @param bool $resetGzipEncoding */ function wfResetOutputBuffers( $resetGzipEncoding = true ) { if ( $resetGzipEncoding ) { @@ -2203,9 +2233,9 @@ function wfClearOutputBuffers() { * Converts an Accept-* header into an array mapping string values to quality * factors * - * @param $accept String - * @param string $def default - * @return Array + * @param string $accept + * @param string $def Default + * @return float[] Associative array of string => float pairs */ function wfAcceptToPrefs( $accept, $def = '*/*' ) { # No arg means accept anything (per HTTP spec) @@ -2238,8 +2268,8 @@ function wfAcceptToPrefs( $accept, $def = '*/*' ) { * Returns the matching MIME type (or wildcard) if a match, otherwise * NULL if no match. * - * @param $type String - * @param $avail Array + * @param string $type + * @param array $avail * @return string * @private */ @@ -2264,8 +2294,8 @@ function mimeTypeMatch( $type, $avail ) { * array of type to preference (preference is a float between 0.0 and 1.0). * Wildcards in the types are acceptable. * - * @param array $cprefs client's acceptable type list - * @param array $sprefs server's offered types + * @param array $cprefs Client's acceptable type list + * @param array $sprefs Server's offered types * @return string * * @todo FIXME: Doesn't handle params like 'text/plain; charset=UTF-8' @@ -2310,7 +2340,7 @@ function wfNegotiateType( $cprefs, $sprefs ) { /** * Reference-counted warning suppression * - * @param $end Bool + * @param bool $end */ function wfSuppressWarnings( $end = false ) { static $suppressCount = 0; @@ -2325,7 +2355,15 @@ function wfSuppressWarnings( $end = false ) { } } else { if ( !$suppressCount ) { - $originalLevel = error_reporting( E_ALL & ~( E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED | E_USER_DEPRECATED | E_STRICT ) ); + $originalLevel = error_reporting( E_ALL & ~( + E_WARNING | + E_NOTICE | + E_USER_WARNING | + E_USER_NOTICE | + E_DEPRECATED | + E_USER_DEPRECATED | + E_STRICT + ) ); } ++$suppressCount; } @@ -2394,11 +2432,10 @@ 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. - * @param $ts Mixed: optional timestamp to convert, default 0 for the current time - * @return Mixed: String / false The same date in the format specified in $outputtype or false + * @param mixed $outputtype A timestamp in one of the supported formats, the + * function will autodetect which format is supplied and act accordingly. + * @param mixed $ts Optional timestamp to convert, default 0 for the current time + * @return string|bool String / false The same date in the format specified in $outputtype or false */ function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) { try { @@ -2414,9 +2451,9 @@ 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 + * @param int $outputtype + * @param string $ts + * @return string */ function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) { if ( is_null( $ts ) ) { @@ -2439,7 +2476,7 @@ function wfTimestampNow() { /** * Check if the operating system is Windows * - * @return Bool: true if it's Windows, False otherwise. + * @return bool True if it's Windows, false otherwise. */ function wfIsWindows() { static $isWindows = null; @@ -2450,21 +2487,23 @@ function wfIsWindows() { } /** - * Check if we are running under HipHop + * Check if we are running under HHVM * - * @return Bool + * @return bool */ -function wfIsHipHop() { - return defined( 'HPHP_VERSION' ); +function wfIsHHVM() { + return defined( 'HHVM_VERSION' ); } /** * Swap two variables * - * @param $x Mixed - * @param $y Mixed + * @deprecated since 1.24 + * @param mixed $x + * @param mixed $y */ function swap( &$x, &$y ) { + wfDeprecated( __FUNCTION__, '1.24' ); $z = $x; $x = $y; $y = $z; @@ -2479,7 +2518,7 @@ function swap( &$x, &$y ) { * NOTE: When possible, use instead the tmpfile() function to create * temporary files to avoid race conditions on file creation, etc. * - * @return String + * @return string */ function wfTempDir() { global $wgTmpDirectory; @@ -2501,9 +2540,9 @@ function wfTempDir() { /** * Make directory, and make all parent directories if they don't exist * - * @param string $dir full path to directory to create - * @param $mode Integer: chmod value to use, default is $wgDirectoryMode - * @param string $caller optional caller param for debugging. + * @param string $dir Full path to directory to create + * @param int $mode Chmod value to use, default is $wgDirectoryMode + * @param string $caller Optional caller param for debugging. * @throws MWException * @return bool */ @@ -2548,6 +2587,7 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) { /** * Remove a directory and all its content. * Does not hide error. + * @param string $dir */ function wfRecursiveRemoveDir( $dir ) { wfDebug( __FUNCTION__ . "( $dir )\n" ); @@ -2569,35 +2609,16 @@ function wfRecursiveRemoveDir( $dir ) { } /** - * @param $nr Mixed: the number to format - * @param $acc Integer: the number of digits after the decimal point, default 2 - * @param $round Boolean: whether or not to round the value, default true - * @return float + * @param int $nr The number to format + * @param int $acc The number of digits after the decimal point, default 2 + * @param bool $round Whether or not to round the value, default true + * @return string */ function wfPercent( $nr, $acc = 2, $round = true ) { $ret = sprintf( "%.${acc}f", $nr ); return $round ? round( $ret, $acc ) . '%' : "$ret%"; } -/** - * Find out whether or not a mixed variable exists in a string - * - * @deprecated Just use str(i)pos - * @param $needle String - * @param $str String - * @param $insensitive Boolean - * @return Boolean - */ -function in_string( $needle, $str, $insensitive = false ) { - wfDeprecated( __METHOD__, '1.21' ); - $func = 'strpos'; - if ( $insensitive ) { - $func = 'stripos'; - } - - return $func( $str, $needle ) !== false; -} - /** * Safety wrapper around ini_get() for boolean settings. * The values returned from ini_get() are pre-normalized for settings @@ -2618,8 +2639,8 @@ function in_string( $needle, $str, $insensitive = false ) { * * I frickin' hate PHP... :P * - * @param $setting String - * @return Bool + * @param string $setting + * @return bool */ function wfIniGetBool( $setting ) { $val = strtolower( ini_get( $setting ) ); @@ -2638,10 +2659,10 @@ function wfIniGetBool( $setting ) { * Also fixes the locale problems on Linux in PHP 5.2.6+ (bug backported to * earlier distro releases of PHP) * - * @param varargs - * @return String + * @param string $args,... + * @return string */ -function wfEscapeShellArg() { +function wfEscapeShellArg( /*...*/ ) { wfInitShellLocale(); $args = func_get_args(); @@ -2656,12 +2677,14 @@ function wfEscapeShellArg() { if ( wfIsWindows() ) { // Escaping for an MSVC-style command line parser and CMD.EXE + // @codingStandardsIgnoreStart For long URLs // 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. + // @codingStandardsIgnoreEnd $tokens = preg_split( '/(\\\\*")/', $arg, -1, PREG_SPLIT_DELIM_CAPTURE ); $arg = ''; $iteration = 0; @@ -2696,24 +2719,21 @@ function wfEscapeShellArg() { /** * Check if wfShellExec() is effectively disabled via php.ini config + * * @return bool|string False or one of (safemode,disabled) * @since 1.22 */ function wfShellExecDisabled() { static $disabled = null; if ( is_null( $disabled ) ) { - $disabled = false; if ( wfIniGetBool( 'safe_mode' ) ) { wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" ); $disabled = 'safemode'; + } elseif ( !function_exists( 'proc_open' ) ) { + wfDebug( "proc_open() is disabled\n" ); + $disabled = 'disabled'; } else { - $functions = explode( ',', ini_get( 'disable_functions' ) ); - $functions = array_map( 'trim', $functions ); - $functions = array_map( 'strtolower', $functions ); - if ( in_array( 'passthru', $functions ) ) { - wfDebug( "passthru is in disabled_functions\n" ); - $disabled = 'passthru'; - } + $disabled = false; } } return $disabled; @@ -2722,18 +2742,26 @@ function wfShellExecDisabled() { /** * Execute a shell command, with time and memory limits mirrored from the PHP * configuration if supported. - * @param string $cmd Command line, properly escaped for shell. - * @param &$retval null|Mixed optional, will receive the program's exit code. - * (non-zero is usually failure) - * @param array $environ optional environment variables which should be - * added to the executed command environment. - * @param array $limits optional array with limits(filesize, memory, time, walltime) - * this overwrites the global wgShellMax* limits. - * @param array $options Array of options. Only one is "duplicateStderr" => true, which - * Which duplicates stderr to stdout, including errors from limit.sh - * @return string collected stdout as a string - */ -function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array(), $options = array() ) { + * + * @param string|string[] $cmd If string, a properly shell-escaped command line, + * or an array of unescaped arguments, in which case each value will be escaped + * Example: [ 'convert', '-font', 'font name' ] would produce "'convert' '-font' 'font name'" + * @param null|mixed &$retval Optional, will receive the program's exit code. + * (non-zero is usually failure). If there is an error from + * read, select, or proc_open(), this will be set to -1. + * @param array $environ Optional environment variables which should be + * added to the executed command environment. + * @param array $limits Optional array with limits(filesize, memory, time, walltime) + * this overwrites the global wgMaxShell* limits. + * @param array $options Array of options: + * - duplicateStderr: Set this to true to duplicate stderr to stdout, + * including errors from limit.sh + * + * @return string Collected stdout as a string + */ +function wfShellExec( $cmd, &$retval = null, $environ = array(), + $limits = array(), $options = array() +) { global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime, $wgMaxShellWallClockTime, $wgShellCgroup; @@ -2742,7 +2770,7 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array $retval = 1; return $disabled == 'safemode' ? 'Unable to run external programs in safe mode.' : - 'Unable to run external programs, passthru() is disabled.'; + 'Unable to run external programs, proc_open() is disabled.'; } $includeStderr = isset( $options['duplicateStderr'] ) && $options['duplicateStderr']; @@ -2766,9 +2794,19 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array $envcmd .= "$k=" . escapeshellarg( $v ) . ' '; } } + if ( is_array( $cmd ) ) { + // Command line may be given as an array, escape each value and glue them together with a space + $cmdVals = array(); + foreach ( $cmd as $val ) { + $cmdVals[] = wfEscapeShellArg( $val ); + } + $cmd = implode( ' ', $cmdVals ); + } + $cmd = $envcmd . $cmd; - if ( php_uname( 's' ) == 'Linux' ) { + $useLogPipe = false; + if ( is_executable( '/bin/bash' ) ) { $time = intval ( isset( $limits['time'] ) ? $limits['time'] : $wgMaxShellTime ); if ( isset( $limits['walltime'] ) ) { $wallTime = intval( $limits['walltime'] ); @@ -2789,8 +2827,10 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array 'MW_CGROUP=' . escapeshellarg( $wgShellCgroup ) . '; ' . "MW_MEM_LIMIT=$mem; " . "MW_FILE_SIZE_LIMIT=$filesize; " . - "MW_WALL_CLOCK_LIMIT=$wallTime" + "MW_WALL_CLOCK_LIMIT=$wallTime; " . + "MW_USE_LOG_PIPE=yes" ); + $useLogPipe = true; } elseif ( $includeStderr ) { $cmd .= ' 2>&1'; } @@ -2799,19 +2839,146 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array } wfDebug( "wfShellExec: $cmd\n" ); - // Default to an unusual value that shouldn't happen naturally, - // so in the unlikely event of a weird php bug, it would be - // more obvious what happened. - $retval = 200; - ob_start(); - passthru( $cmd, $retval ); - $output = ob_get_contents(); - ob_end_clean(); + $desc = array( + 0 => array( 'file', 'php://stdin', 'r' ), + 1 => array( 'pipe', 'w' ), + 2 => array( 'file', 'php://stderr', 'w' ) ); + if ( $useLogPipe ) { + $desc[3] = array( 'pipe', 'w' ); + } + $pipes = null; + $proc = proc_open( $cmd, $desc, $pipes ); + if ( !$proc ) { + wfDebugLog( 'exec', "proc_open() failed: $cmd" ); + $retval = -1; + return ''; + } + $outBuffer = $logBuffer = ''; + $emptyArray = array(); + $status = false; + $logMsg = false; + + // According to the documentation, it is possible for stream_select() + // to fail due to EINTR. I haven't managed to induce this in testing + // despite sending various signals. If it did happen, the error + // message would take the form: + // + // stream_select(): unable to select [4]: Interrupted system call (max_fd=5) + // + // where [4] is the value of the macro EINTR and "Interrupted system + // call" is string which according to the Linux manual is "possibly" + // localised according to LC_MESSAGES. + $eintr = defined( 'SOCKET_EINTR' ) ? SOCKET_EINTR : 4; + $eintrMessage = "stream_select(): unable to select [$eintr]"; + + // Build a table mapping resource IDs to pipe FDs to work around a + // PHP 5.3 issue in which stream_select() does not preserve array keys + // . + $fds = array(); + foreach ( $pipes as $fd => $pipe ) { + $fds[(int)$pipe] = $fd; + } + + $running = true; + $timeout = null; + $numReadyPipes = 0; + + while ( $running === true || $numReadyPipes !== 0 ) { + if ( $running ) { + $status = proc_get_status( $proc ); + // If the process has terminated, switch to nonblocking selects + // for getting any data still waiting to be read. + if ( !$status['running'] ) { + $running = false; + $timeout = 0; + } + } + + $readyPipes = $pipes; - if ( $retval == 127 ) { - wfDebugLog( 'exec', "Possibly missing executable file: $cmd\n" ); + // Clear last error + // @codingStandardsIgnoreStart Generic.PHP.NoSilencedErrors.Discouraged + @trigger_error( '' ); + $numReadyPipes = @stream_select( $readyPipes, $emptyArray, $emptyArray, $timeout ); + if ( $numReadyPipes === false ) { + // @codingStandardsIgnoreEnd + $error = error_get_last(); + if ( strncmp( $error['message'], $eintrMessage, strlen( $eintrMessage ) ) == 0 ) { + continue; + } else { + trigger_error( $error['message'], E_USER_WARNING ); + $logMsg = $error['message']; + break; + } + } + foreach ( $readyPipes as $pipe ) { + $block = fread( $pipe, 65536 ); + $fd = $fds[(int)$pipe]; + if ( $block === '' ) { + // End of file + fclose( $pipes[$fd] ); + unset( $pipes[$fd] ); + if ( !$pipes ) { + break 2; + } + } elseif ( $block === false ) { + // Read error + $logMsg = "Error reading from pipe"; + break 2; + } elseif ( $fd == 1 ) { + // From stdout + $outBuffer .= $block; + } elseif ( $fd == 3 ) { + // From log FD + $logBuffer .= $block; + if ( strpos( $block, "\n" ) !== false ) { + $lines = explode( "\n", $logBuffer ); + $logBuffer = array_pop( $lines ); + foreach ( $lines as $line ) { + wfDebugLog( 'exec', $line ); + } + } + } + } } - return $output; + + foreach ( $pipes as $pipe ) { + fclose( $pipe ); + } + + // Use the status previously collected if possible, since proc_get_status() + // just calls waitpid() which will not return anything useful the second time. + if ( $running ) { + $status = proc_get_status( $proc ); + } + + if ( $logMsg !== false ) { + // Read/select error + $retval = -1; + proc_close( $proc ); + } elseif ( $status['signaled'] ) { + $logMsg = "Exited with signal {$status['termsig']}"; + $retval = 128 + $status['termsig']; + proc_close( $proc ); + } else { + if ( $status['running'] ) { + $retval = proc_close( $proc ); + } else { + $retval = $status['exitcode']; + proc_close( $proc ); + } + if ( $retval == 127 ) { + $logMsg = "Possibly missing executable file"; + } elseif ( $retval >= 129 && $retval <= 192 ) { + $logMsg = "Probably exited with signal " . ( $retval - 128 ); + } + } + + if ( $logMsg !== false ) { + wfDebugLog( 'exec', "$logMsg: $cmd" ); + } + + return $outBuffer; } /** @@ -2820,13 +2987,13 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array * * @note This also includes errors from limit.sh, e.g. if $wgMaxShellFileSize is exceeded. * @param string $cmd Command line, properly escaped for shell. - * @param &$retval null|Mixed optional, will receive the program's exit code. - * (non-zero is usually failure) - * @param array $environ optional environment variables which should be - * added to the executed command environment. - * @param array $limits optional array with limits(filesize, memory, time, walltime) - * this overwrites the global wgShellMax* limits. - * @return string collected stdout and stderr as a string + * @param null|mixed &$retval Optional, will receive the program's exit code. + * (non-zero is usually failure) + * @param array $environ Optional environment variables which should be + * added to the executed command environment. + * @param array $limits Optional array with limits(filesize, memory, time, walltime) + * this overwrites the global wgMaxShell* limits. + * @return string Collected stdout and stderr as a string */ function wfShellExecWithStderr( $cmd, &$retval = null, $environ = array(), $limits = array() ) { return wfShellExec( $cmd, $retval, $environ, $limits, array( 'duplicateStderr' => true ) ); @@ -2851,6 +3018,7 @@ function wfInitShellLocale() { /** * Alias to wfShellWikiCmd() + * * @see wfShellWikiCmd() */ function wfShellMaintenanceCmd( $script, array $parameters = array(), array $options = array() ) { @@ -2861,12 +3029,13 @@ function wfShellMaintenanceCmd( $script, array $parameters = array(), array $opt * Generate a shell-escaped command line string to run a MediaWiki cli script. * Note that $parameters should be a flat array and an option with an argument * should consist of two consecutive items in the array (do not use "--option value"). + * * @param string $script MediaWiki cli script path * @param array $parameters Arguments and options to the script * @param array $options Associative array of options: * 'php': The path to the php executable * 'wrapper': Path to a PHP wrapper to handle the maintenance script - * @return Array + * @return string */ function wfShellWikiCmd( $script, array $parameters = array(), array $options = array() ) { global $wgPhpCli; @@ -2886,11 +3055,11 @@ function wfShellWikiCmd( $script, array $parameters = array(), array $options = * 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 + * @param string $old + * @param string $mine + * @param string $yours + * @param string $result + * @return bool */ function wfMerge( $old, $mine, $yours, &$result ) { global $wgDiff3; @@ -2965,10 +3134,10 @@ function wfMerge( $old, $mine, $yours, &$result ) { * Returns unified plain-text diff of two texts. * Useful for machine processing of diffs. * - * @param string $before the text before the changes. - * @param string $after the text after the changes. - * @param string $params command-line options for the diff command. - * @return String: unified diff of $before and $after + * @param string $before The text before the changes. + * @param string $after The text after the changes. + * @param string $params Command-line options for the diff command. + * @return string Unified diff of $before and $after */ function wfDiff( $before, $after, $params = '-u' ) { if ( $before == $after ) { @@ -3021,10 +3190,10 @@ function wfDiff( $before, $after, $params = '-u' ) { // Kill the --- and +++ lines. They're not useful. $diff_lines = explode( "\n", $diff ); - if ( strpos( $diff_lines[0], '---' ) === 0 ) { + if ( isset( $diff_lines[0] ) && strpos( $diff_lines[0], '---' ) === 0 ) { unset( $diff_lines[0] ); } - if ( strpos( $diff_lines[1], '+++' ) === 0 ) { + if ( isset( $diff_lines[1] ) && strpos( $diff_lines[1], '+++' ) === 0 ) { unset( $diff_lines[1] ); } @@ -3045,8 +3214,7 @@ function wfDiff( $before, $after, $params = '-u' ) { * * @see perldoc -f use * - * @param $req_ver Mixed: the version to check, can be a string, an integer, or - * a float + * @param string|int|float $req_ver The version to check, can be a string, an integer, or a float * @throws MWException */ function wfUsePHP( $req_ver ) { @@ -3065,10 +3233,17 @@ function wfUsePHP( $req_ver ) { * This is useful for extensions which due to their nature are not kept in sync * with releases * + * Note: Due to the behavior of PHP's version_compare() which is used in this + * function, if you want to allow the 'wmf' development versions add a 'c' (or + * any single letter other than 'a', 'b' or 'p') as a post-fix to your + * targeted version number. For example if you wanted to allow any variation + * of 1.22 use `wfUseMW( '1.22c' )`. Using an 'a' or 'b' instead of 'c' will + * not result in the same comparison due to the internal logic of + * version_compare(). + * * @see perldoc -f use * - * @param $req_ver Mixed: the version to check, can be a string, an integer, or - * a float + * @param string|int|float $req_ver The version to check, can be a string, an integer, or a float * @throws MWException */ function wfUseMW( $req_ver ) { @@ -3087,14 +3262,17 @@ function wfUseMW( $req_ver ) { * 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 string $suffix to remove if present - * @return String + * @param string $path + * @param string $suffix String to remove if present + * @return string */ function wfBaseName( $path, $suffix = '' ) { - $encSuffix = ( $suffix == '' ) - ? '' - : ( '(?:' . preg_quote( $suffix, '#' ) . ')?' ); + if ( $suffix == '' ) { + $encSuffix = ''; + } else { + $encSuffix = '(?:' . preg_quote( $suffix, '#' ) . ')?'; + } + $matches = array(); if ( preg_match( "#([^/\\\\]*?){$encSuffix}[/\\\\]*$#", $path, $matches ) ) { return $matches[1]; @@ -3108,9 +3286,9 @@ function wfBaseName( $path, $suffix = '' ) { * May explode on non-matching case-insensitive paths, * funky symlinks, etc. * - * @param string $path absolute destination path including target filename + * @param string $path Absolute destination path including target filename * @param string $from Absolute source path, directory only - * @return String + * @return string */ function wfRelativePath( $path, $from ) { // Normalize mixed input on Windows... @@ -3148,18 +3326,6 @@ function wfRelativePath( $path, $from ) { return implode( DIRECTORY_SEPARATOR, $pieces ); } -/** - * Do any deferred updates and clear the list - * - * @deprecated since 1.19 - * @see DeferredUpdates::doUpdate() - * @param $commit string - */ -function wfDoUpdates( $commit = '' ) { - wfDeprecated( __METHOD__, '1.19' ); - DeferredUpdates::doUpdates( $commit ); -} - /** * Convert an arbitrarily-long digit string from one numeric base * to another, optionally zero-padding to a minimum column width. @@ -3175,7 +3341,9 @@ function wfDoUpdates( $commit = '' ) { * @param string $engine Either "gmp", "bcmath", or "php" * @return string|bool The output number as a string, or false on error */ -function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = true, $engine = 'auto' ) { +function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, + $lowercase = true, $engine = 'auto' +) { $input = (string)$input; if ( $sourceBase < 2 || @@ -3185,7 +3353,10 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = t $sourceBase != (int)$sourceBase || $destBase != (int)$destBase || $pad != (int)$pad || - !preg_match( "/^[" . substr( '0123456789abcdefghijklmnopqrstuvwxyz', 0, $sourceBase ) . "]+$/i", $input ) + !preg_match( + "/^[" . substr( '0123456789abcdefghijklmnopqrstuvwxyz', 0, $sourceBase ) . "]+$/i", + $input + ) ) { return false; } @@ -3197,8 +3368,8 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = t 28 => 's', 29 => 't', 30 => 'u', 31 => 'v', 32 => 'w', 33 => 'x', 34 => 'y', 35 => 'z', - '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, - '6' => 6, '7' => 7, '8' => 8, '9' => 9, 'a' => 10, 'b' => 11, + '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, + '6' => 6, '7' => 7, '8' => 8, '9' => 9, 'a' => 10, 'b' => 11, 'c' => 12, 'd' => 13, 'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17, 'i' => 18, 'j' => 19, 'k' => 20, 'l' => 21, 'm' => 22, 'n' => 23, 'o' => 24, 'p' => 25, 'q' => 26, 'r' => 27, 's' => 28, 't' => 29, @@ -3206,7 +3377,10 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = t ); if ( extension_loaded( 'gmp' ) && ( $engine == 'auto' || $engine == 'gmp' ) ) { - $result = gmp_strval( gmp_init( $input, $sourceBase ), $destBase ); + // Removing leading zeros works around broken base detection code in + // some PHP versions (see and + // ). + $result = gmp_strval( gmp_init( ltrim( $input, '0' ), $sourceBase ), $destBase ); } elseif ( extension_loaded( 'bcmath' ) && ( $engine == 'auto' || $engine == 'bcmath' ) ) { $decimal = '0'; foreach ( str_split( strtolower( $input ) ) as $char ) { @@ -3214,9 +3388,11 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = t $decimal = bcadd( $decimal, $baseChars[$char] ); } + // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed for ( $result = ''; bccomp( $decimal, 0 ); $decimal = bcdiv( $decimal, $destBase, 0 ) ) { $result .= $baseChars[bcmod( $decimal, $destBase )]; } + // @codingStandardsIgnoreEnd $result = strrev( $result ); } else { @@ -3261,39 +3437,10 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = t return str_pad( $result, $pad, '0', STR_PAD_LEFT ); } -/** - * Create an object with a given name and an array of construct parameters - * - * @param $name String - * @param array $p parameters - * @return object - * @deprecated since 1.18, warnings in 1.18, removal in 1.20 - */ -function wfCreateObject( $name, $p ) { - wfDeprecated( __FUNCTION__, '1.18' ); - return MWFunction::newObj( $name, $p ); -} - -/** - * @return bool - */ -function wfHttpOnlySafe() { - global $wgHttpOnlyBlacklist; - - if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) { - foreach ( $wgHttpOnlyBlacklist as $regex ) { - if ( preg_match( $regex, $_SERVER['HTTP_USER_AGENT'] ) ) { - return false; - } - } - } - - return true; -} - /** * Check if there is sufficient entropy in php's built-in session generation - * @return bool true = there is sufficient entropy + * + * @return bool True = there is sufficient entropy */ function wfCheckEntropy() { return ( @@ -3319,15 +3466,18 @@ function wfFixSessionID() { // We treat it as disabled if it doesn't have an entropy length of at least 32 $entropyEnabled = wfCheckEntropy(); - // If built-in entropy is not enabled or not sufficient override php's built in session id generation code + // If built-in entropy is not enabled or not sufficient override PHP's + // built in session id generation code if ( !$entropyEnabled ) { - wfDebug( __METHOD__ . ": PHP's built in entropy is disabled or not sufficient, overriding session id generation using our cryptrand source.\n" ); + wfDebug( __METHOD__ . ": PHP's built in entropy is disabled or not sufficient, " . + "overriding session id generation using our cryptrand source.\n" ); session_id( MWCryptRand::generateHex( 32 ) ); } } /** * Reset the session_id + * * @since 1.22 */ function wfResetSessionID() { @@ -3346,11 +3496,10 @@ function wfResetSessionID() { wfRunHooks( 'ResetSessionID', array( $oldSessionId, $newSessionId ) ); } - /** * Initialise php session * - * @param $sessionId Bool + * @param bool $sessionId */ function wfSetupSession( $sessionId = false ) { global $wgSessionsInMemcached, $wgSessionsInObjectCache, $wgCookiePath, $wgCookieDomain, @@ -3362,16 +3511,8 @@ function wfSetupSession( $sessionId = false ) { # hasn't already been set to the desired value (that causes errors) ini_set( 'session.save_handler', $wgSessionHandler ); } - $httpOnlySafe = wfHttpOnlySafe() && $wgCookieHttpOnly; - wfDebugLog( 'cookie', - 'session_set_cookie_params: "' . implode( '", "', - array( - 0, - $wgCookiePath, - $wgCookieDomain, - $wgCookieSecure, - $httpOnlySafe ) ) . '"' ); - session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $httpOnlySafe ); + session_set_cookie_params( + 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly ); session_cache_limiter( 'private, must-revalidate' ); if ( $sessionId ) { session_id( $sessionId ); @@ -3386,8 +3527,8 @@ function wfSetupSession( $sessionId = false ) { /** * Get an object from the precompiled serialized directory * - * @param $name String - * @return Mixed: the variable on success, false on failure + * @param string $name + * @return mixed The variable on success, false on failure */ function wfGetPrecompiledData( $name ) { global $IP; @@ -3405,10 +3546,10 @@ function wfGetPrecompiledData( $name ) { /** * Get a cache key * - * @param varargs - * @return String + * @param string $args,... + * @return string */ -function wfMemcKey( /*... */ ) { +function wfMemcKey( /*...*/ ) { global $wgCachePrefix; $prefix = $wgCachePrefix === false ? wfWikiID() : $wgCachePrefix; $args = func_get_args(); @@ -3420,12 +3561,12 @@ function wfMemcKey( /*... */ ) { /** * Get a cache key for a foreign DB * - * @param $db String - * @param $prefix String - * @param varargs String - * @return String + * @param string $db + * @param string $prefix + * @param string $args,... + * @return string */ -function wfForeignMemcKey( $db, $prefix /*, ... */ ) { +function wfForeignMemcKey( $db, $prefix /*...*/ ) { $args = array_slice( func_get_args(), 2 ); if ( $prefix ) { $key = "$db-$prefix:" . implode( ':', $args ); @@ -3439,7 +3580,7 @@ function wfForeignMemcKey( $db, $prefix /*, ... */ ) { * Get an ASCII string identifying this wiki * This is used as a prefix in memcached keys * - * @return String + * @return string */ function wfWikiID() { global $wgDBprefix, $wgDBname; @@ -3453,7 +3594,7 @@ function wfWikiID() { /** * Split a wiki ID into DB name and table prefix * - * @param $wiki String + * @param string $wiki * * @return array */ @@ -3468,15 +3609,15 @@ function wfSplitWikiID( $wiki ) { /** * Get a Database object. * - * @param $db Integer: index of the connection to get. May be DB_MASTER for the + * @param int $db 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. * - * @param $groups Mixed: query groups. An array of group names that this query + * @param string|string[] $groups Query groups. An array of group names that this query * belongs to. May contain a single string if the query is only * in one group. * - * @param string $wiki the wiki ID, or false for the current wiki + * @param string|bool $wiki The wiki ID, or false for the current wiki * * Note: multiple calls to wfGetDB(DB_SLAVE) during the course of one request * will always return the same object, unless the underlying connection or load @@ -3494,7 +3635,7 @@ function &wfGetDB( $db, $groups = array(), $wiki = false ) { /** * Get a load balancer object. * - * @param string $wiki wiki ID, or false for the current wiki + * @param string|bool $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ function wfGetLB( $wiki = false ) { @@ -3514,7 +3655,7 @@ function &wfGetLBFactory() { * Find a file. * Shortcut for RepoGroup::singleton()->findFile() * - * @param string $title or Title object + * @param string $title String or Title object * @param array $options Associative array of options: * time: requested time for an archived image, or false for the * current version. An image object will be returned which was @@ -3528,7 +3669,7 @@ function &wfGetLBFactory() { * * bypassCache: If true, do not use the process-local cache of File objects * - * @return File, or false if the file does not exist + * @return File|bool File, or false if the file does not exist */ function wfFindFile( $title, $options = array() ) { return RepoGroup::singleton()->findFile( $title, $options ); @@ -3538,26 +3679,17 @@ 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|String + * @param Title|string $title * @return LocalFile|null A File, or null if passed an invalid Title */ function wfLocalFile( $title ) { return RepoGroup::singleton()->getLocalRepo()->newFile( $title ); } -/** - * Stream a file to the browser. Back-compat alias for StreamFile::stream() - * @deprecated since 1.19 - */ -function wfStreamFile( $fname, $headers = array() ) { - wfDeprecated( __FUNCTION__, '1.19' ); - StreamFile::stream( $fname, $headers ); -} - /** * Should low-performance queries be disabled? * - * @return Boolean + * @return bool * @codeCoverageIgnore */ function wfQueriesMustScale() { @@ -3573,8 +3705,8 @@ function wfQueriesMustScale() { * extensions; this is a wrapper around $wgScriptExtension etc. * except for 'index' and 'load' which use $wgScript/$wgLoadScript * - * @param string $script script filename, sans extension - * @return String + * @param string $script Script filename, sans extension + * @return string */ function wfScript( $script = 'index' ) { global $wgScriptPath, $wgScriptExtension, $wgScript, $wgLoadScript; @@ -3590,7 +3722,7 @@ function wfScript( $script = 'index' ) { /** * Get the script URL. * - * @return string script URL + * @return string Script URL */ function wfGetScriptUrl() { if ( isset( $_SERVER['SCRIPT_NAME'] ) ) { @@ -3614,8 +3746,8 @@ function wfGetScriptUrl() { * Convenience function converts boolean values into "true" * or "false" (string) values * - * @param $value Boolean - * @return String + * @param bool $value + * @return string */ function wfBoolToStr( $value ) { return $value ? 'true' : 'false'; @@ -3627,9 +3759,7 @@ function wfBoolToStr( $value ) { * @return string */ function wfGetNull() { - return wfIsWindows() - ? 'NUL' - : '/dev/null'; + return wfIsWindows() ? 'NUL' : '/dev/null'; } /** @@ -3639,51 +3769,52 @@ function wfGetNull() { * in maintenance scripts, to avoid causing too much lag. Of course, this is * a no-op if there are no slaves. * - * @param $maxLag Integer (deprecated) - * @param $wiki mixed Wiki identifier accepted by wfGetLB - * @param $cluster string cluster name accepted by LBFactory + * @param float|null $ifWritesSince Only wait if writes were done since this UNIX timestamp + * @param string|bool $wiki Wiki identifier accepted by wfGetLB + * @param string|bool $cluster Cluster name accepted by LBFactory. Default: false. + * @return bool Success (able to connect and no timeouts reached) */ -function wfWaitForSlaves( $maxLag = false, $wiki = false, $cluster = false ) { - $lb = ( $cluster !== false ) - ? wfGetLBFactory()->getExternalLB( $cluster ) - : wfGetLB( $wiki ); +function wfWaitForSlaves( $ifWritesSince = false, $wiki = false, $cluster = false ) { + // B/C: first argument used to be "max seconds of lag"; ignore such values + $ifWritesSince = ( $ifWritesSince > 1e9 ) ? $ifWritesSince : false; + + if ( $cluster !== false ) { + $lb = wfGetLBFactory()->getExternalLB( $cluster ); + } else { + $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 ) { + if ( $ifWritesSince && !$lb->hasMasterConnection() ) { + return true; // assume no writes done + } $dbw = $lb->getConnection( DB_MASTER, array(), $wiki ); + if ( $ifWritesSince && $dbw->lastDoneWrites() < $ifWritesSince ) { + return true; // no writes since the last wait + } $pos = $dbw->getMasterPos(); // The DBMS may not support getMasterPos() or the whole // load balancer might be fake (e.g. $wgAllDBsAreLocalhost). if ( $pos !== false ) { - $lb->waitForAll( $pos ); + return $lb->waitForAll( $pos, PHP_SAPI === 'cli' ? 86400 : null ); } } -} -/** - * Used to be used for outputting text in the installer/updater - * @deprecated since 1.18, warnings in 1.18, remove in 1.20 - */ -function wfOut( $s ) { - wfDeprecated( __FUNCTION__, '1.18' ); - global $wgCommandLineMode; - if ( $wgCommandLineMode ) { - echo $s; - } else { - echo htmlspecialchars( $s ); - } - flush(); + return true; } /** - * Count down from $n to zero on the terminal, with a one-second pause + * Count down from $seconds to zero on the terminal, with a one-second pause * between showing each number. For use in command-line scripts. + * * @codeCoverageIgnore - * @param $n int + * @param int $seconds */ -function wfCountDown( $n ) { - for ( $i = $n; $i >= 0; $i-- ) { - if ( $i != $n ) { +function wfCountDown( $seconds ) { + for ( $i = $seconds; $i >= 0; $i-- ) { + if ( $i != $seconds ) { echo str_repeat( "\x08", strlen( $i + 1 ) ); } echo $i; @@ -3695,28 +3826,13 @@ function wfCountDown( $n ) { echo "\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 - * @deprecated since 1.20; Please use MWCryptRand for security purposes and wfRandomString for pseudo-random strings - * @warning This method is NOT secure. Additionally it has many callers that use it for pseudo-random purposes. - */ -function wfGenerateToken( $salt = '' ) { - wfDeprecated( __METHOD__, '1.20' ); - $salt = serialize( $salt ); - return md5( mt_rand( 0, 0x7fffffff ) . $salt ); -} - /** * Replace all invalid characters with - * Additional characters can be defined in $wgIllegalFileChars (see bug 20489) * By default, $wgIllegalFileChars = ':' * - * @param $name Mixed: filename to process - * @return String + * @param string $name Filename to process + * @return string */ function wfStripIllegalFilenameChars( $name ) { global $wgIllegalFileChars; @@ -3733,7 +3849,7 @@ function wfStripIllegalFilenameChars( $name ) { /** * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit; * - * @return Integer value memory was set to. + * @return int Value the memory limit was set to. */ function wfMemoryLimit() { global $wgMemoryLimit; @@ -3760,8 +3876,8 @@ function wfMemoryLimit() { /** * Converts shorthand byte notation to integer form * - * @param $string String - * @return Integer + * @param string $string + * @return int */ function wfShorthandToInteger( $string = '' ) { $string = trim( $string ); @@ -3792,7 +3908,7 @@ function wfShorthandToInteger( $string = '' ) { * See unit test for examples. * * @param string $code The language code. - * @return String: The language code which complying with BCP 47 standards. + * @return string The language code which complying with BCP 47 standards. */ function wfBCP47( $code ) { $codeSegment = explode( '-', $code ); @@ -3819,7 +3935,7 @@ function wfBCP47( $code ) { /** * Get a cache object. * - * @param $inputType integer Cache type, one the the CACHE_* constants. + * @param int $inputType Cache type, one the the CACHE_* constants. * @return BagOStuff */ function wfGetCache( $inputType ) { @@ -3869,26 +3985,28 @@ function wfGetLangConverterCacheStorage() { /** * Call hook functions defined in $wgHooks * - * @param string $event event name - * @param array $args parameters passed to hook functions - * @return Boolean True if no handler aborted the hook + * @param string $event Event name + * @param array $args Parameters passed to hook functions + * @param string|null $deprecatedVersion Optionally mark hook as deprecated with version number + * + * @return bool True if no handler aborted the hook */ -function wfRunHooks( $event, array $args = array() ) { - return Hooks::run( $event, $args ); +function wfRunHooks( $event, array $args = array(), $deprecatedVersion = null ) { + return Hooks::run( $event, $args, $deprecatedVersion ); } /** * Wrapper around php's unpack. * * @param string $format The format string (See php's docs) - * @param $data: A binary string of binary data - * @param $length integer or false: The minimum length of $data. This is to + * @param string $data A binary string of binary data + * @param int|bool $length The minimum length of $data or false. 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 + * @throws MWException If $data not long enough, or if unpack fails * @return array Associative array of the extracted data */ function wfUnpack( $format, $data, $length = false ) { @@ -3922,9 +4040,9 @@ function wfUnpack( $format, $data, $length = false ) { * * Any subsequent links on the same line are considered to be exceptions, * i.e. articles where the image may occur inline. * - * @param string $name the image name to check - * @param $contextTitle Title|bool the page on which the image occurs, if known - * @param string $blacklist wikitext of a file blacklist + * @param string $name The image name to check + * @param Title|bool $contextTitle The page on which the image occurs, if known + * @param string $blacklist Wikitext of a file blacklist * @return bool */ function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) { @@ -3999,10 +4117,48 @@ function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) { * access the wiki via HTTPS. * * @param string $ip The IPv4/6 address in the normal human-readable form - * @return boolean + * @return bool */ function wfCanIPUseHTTPS( $ip ) { $canDo = true; wfRunHooks( 'CanIPUseHTTPS', array( $ip, &$canDo ) ); return !!$canDo; } + +/** + * Work out the IP address based on various globals + * For trusted proxies, use the XFF client IP (first of the chain) + * + * @deprecated since 1.19; call $wgRequest->getIP() directly. + * @return string + */ +function wfGetIP() { + wfDeprecated( __METHOD__, '1.19' ); + global $wgRequest; + return $wgRequest->getIP(); +} + +/** + * Checks if an IP is a trusted proxy provider. + * Useful to tell if X-Forwarded-For data is possibly bogus. + * Squid cache servers for the site are whitelisted. + * @deprecated Since 1.24, use IP::isTrustedProxy() + * + * @param string $ip + * @return bool + */ +function wfIsTrustedProxy( $ip ) { + return IP::isTrustedProxy( $ip ); +} + +/** + * Checks if an IP matches a proxy we've configured. + * @deprecated Since 1.24, use IP::isConfiguredProxy() + * + * @param string $ip + * @return bool + * @since 1.23 Supports CIDR ranges in $wgSquidServersNoPurge + */ +function wfIsConfiguredProxy( $ip ) { + return IP::isConfiguredProxy( $ip ); +} diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php deleted file mode 100644 index d260862c..00000000 --- a/includes/HTMLForm.php +++ /dev/null @@ -1,2965 +0,0 @@ - $info, - * where $info is an Associative Array with any of the following: - * - * 'class' -- the subclass of HTMLFormField that will be used - * to create the object. *NOT* the CSS class! - * 'type' -- roughly translates into the : ", or "" if nothing has been selected in the - * select dropdown. - * @todo FIXME: If made 'required', only the text field should be compulsory. - */ -class HTMLSelectAndOtherField extends HTMLSelectField { - - function __construct( $params ) { - if ( array_key_exists( 'other', $params ) ) { - } elseif ( array_key_exists( 'other-message', $params ) ) { - $params['other'] = wfMessage( $params['other-message'] )->plain(); - } else { - $params['other'] = null; - } - - if ( array_key_exists( 'options', $params ) ) { - # Options array already specified - } elseif ( array_key_exists( 'options-message', $params ) ) { - # Generate options array from a system message - $params['options'] = self::parseMessage( - wfMessage( $params['options-message'] )->inContentLanguage()->plain(), - $params['other'] - ); - } else { - # Sulk - throw new MWException( 'HTMLSelectAndOtherField called without any options' ); - } - $this->mFlatOptions = self::flattenOptions( $params['options'] ); - - parent::__construct( $params ); - } - - /** - * Build a drop-down box from a textual list. - * @param string $string message text - * @param string $otherName name of "other reason" option - * @return Array - * TODO: this is copied from Xml::listDropDown(), deprecate/avoid duplication? - */ - public static function parseMessage( $string, $otherName = null ) { - if ( $otherName === null ) { - $otherName = wfMessage( 'htmlform-selectorother-other' )->plain(); - } - - $optgroup = false; - $options = array( $otherName => 'other' ); - - foreach ( explode( "\n", $string ) as $option ) { - $value = trim( $option ); - if ( $value == '' ) { - continue; - } elseif ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) { - # A new group is starting... - $value = trim( substr( $value, 1 ) ); - $optgroup = $value; - } elseif ( substr( $value, 0, 2 ) == '**' ) { - # groupmember - $opt = trim( substr( $value, 2 ) ); - if ( $optgroup === false ) { - $options[$opt] = $opt; - } else { - $options[$optgroup][$opt] = $opt; - } - } else { - # groupless reason list - $optgroup = false; - $options[$option] = $option; - } - } - - return $options; - } - - function getInputHTML( $value ) { - $select = parent::getInputHTML( $value[1] ); - - $textAttribs = array( - 'id' => $this->mID . '-other', - 'size' => $this->getSize(), - ); - - if ( $this->mClass !== '' ) { - $textAttribs['class'] = $this->mClass; - } - - foreach ( array( 'required', 'autofocus', 'multiple', 'disabled' ) as $param ) { - if ( isset( $this->mParams[$param] ) ) { - $textAttribs[$param] = ''; - } - } - - $textbox = Html::input( - $this->mName . '-other', - $value[2], - 'text', - $textAttribs - ); - - return "$select
\n$textbox"; - } - - /** - * @param $request WebRequest - * @return Array("","" element. This supports the * new HTML5 input types and attributes. * - * @param $name string name attribute - * @param $value mixed value attribute - * @param $type string type attribute - * @param array $attribs Associative array of miscellaneous extra + * @param string $name Name attribute + * @param array $value Value attribute + * @param string $type Type attribute + * @param array $attribs Associative array of miscellaneous extra * attributes, passed to Html::element() * @return string Raw HTML */ @@ -646,16 +661,79 @@ class Html { $attribs['type'] = $type; $attribs['value'] = $value; $attribs['name'] = $name; - + if ( in_array( $type, array( 'text', 'search', 'email', 'password', 'number' ) ) ) { + $attribs = Html::getTextInputAttributes( $attribs ); + } return self::element( 'input', $attribs ); } + /** + * Convenience function to produce a checkbox (input element with type=checkbox) + * + * @param string $name Name attribute + * @param bool $checked Whether the checkbox is checked or not + * @param array $attribs Array of additional attributes + * @return string + */ + public static function check( $name, $checked = false, array $attribs = array() ) { + if ( isset( $attribs['value'] ) ) { + $value = $attribs['value']; + unset( $attribs['value'] ); + } else { + $value = 1; + } + + if ( $checked ) { + $attribs[] = 'checked'; + } + + return self::input( $name, $value, 'checkbox', $attribs ); + } + + /** + * Convenience function to produce a checkbox (input element with type=checkbox) + * + * @param string $name Name attribute + * @param bool $checked Whether the checkbox is checked or not + * @param array $attribs Array of additional attributes + * @return string + */ + public static function radio( $name, $checked = false, array $attribs = array() ) { + if ( isset( $attribs['value'] ) ) { + $value = $attribs['value']; + unset( $attribs['value'] ); + } else { + $value = 1; + } + + if ( $checked ) { + $attribs[] = 'checked'; + } + + return self::input( $name, $value, 'radio', $attribs ); + } + + /** + * Convenience function for generating a label for inputs. + * + * @param string $label Contents of the label + * @param string $id ID of the element being labeled + * @param array $attribs Additional attributes + * @return string + */ + public static function label( $label, $id, array $attribs = array() ) { + $attribs += array( + 'for' => $id + ); + return self::element( 'label', $attribs, $label ); + } + /** * Convenience function to produce an input element with type=hidden * - * @param $name string name attribute - * @param $value string value attribute - * @param array $attribs Associative array of miscellaneous extra + * @param string $name Name attribute + * @param string $value Value attribute + * @param array $attribs Associative array of miscellaneous extra * attributes, passed to Html::element() * @return string Raw HTML */ @@ -664,14 +742,14 @@ class Html { } /** - * Convenience function to produce an "" element. + * Convenience function to produce a ' ); $this->parent->output->flush(); } + } -class WebInstaller_Language extends WebInstallerPage { +class WebInstallerLanguage extends WebInstallerPage { + /** + * @return string|null + */ public function execute() { global $wgLang; $r = $this->parent->request; @@ -245,15 +270,18 @@ class WebInstaller_Language extends WebInstallerPage { $this->parent->getHelpBox( 'config-wiki-language-help' ) ); $this->addHTML( $s ); $this->endForm( 'continue', false ); + + return null; } /** * Get a "" element which has options for each of the allowed limits + * + * @param string $attribs Extra attributes to set + * @return string HTML fragment + */ + public function getLimitSelect( $attribs = array() ) { + $select = new XmlSelect( 'limit', false, $this->mLimit ); + $select->addOptions( $this->getLimitSelectList() ); + foreach ( $attribs as $name => $value ) { + $select->setAttribute( $name, $value ); + } + return $select->getHTML(); + } + + /** + * Get a list of items to show in a " elements for use in a method="get" form. + * Resubmits all defined elements of the query string, except for a + * blacklist, passed in the $blacklist parameter. + * + * @param array $blacklist Parameters from the request query which should not be resubmitted + * @return string HTML fragment + */ + function getHiddenFields( $blacklist = array() ) { + $blacklist = (array)$blacklist; + $query = $this->getRequest()->getQueryValues(); + foreach ( $blacklist as $name ) { + unset( $query[$name] ); + } + $s = ''; + foreach ( $query as $name => $value ) { + $s .= Html::hidden( $name, $value ) . "\n"; + } + return $s; + } + + /** + * Get a form containing a limit selection dropdown + * + * @return string HTML fragment + */ + function getLimitForm() { + return Html::rawElement( + 'form', + array( + 'method' => 'get', + 'action' => wfScript(), + ), + "\n" . $this->getLimitDropdown() + ) . "\n"; + } + + /** + * Gets a limit selection dropdown + * + * @return string + */ + function getLimitDropdown() { + # Make the select with some explanatory text + $msgSubmit = $this->msg( 'table_pager_limit_submit' )->escaped(); + + return $this->msg( 'table_pager_limit' ) + ->rawParams( $this->getLimitSelect() )->escaped() . + "\n\n" . + $this->getHiddenFields( array( 'limit' ) ); + } + + /** + * Return true if the named field should be sortable by the UI, false + * otherwise + * + * @param string $field + */ + abstract function isFieldSortable( $field ); + + /** + * Format a table cell. The return value should be HTML, but use an empty + * string not   for empty cells. Do not include the and . + * + * The current result row is available as $this->mCurrentRow, in case you + * need more context. + * + * @protected + * + * @param string $name The database field name + * @param string $value The value retrieved from the database + */ + abstract function formatValue( $name, $value ); + + /** + * The database field name used as a default sort order. + * + * @protected + * + * @return string + */ + abstract function getDefaultSort(); + + /** + * An array mapping database field names to a textual description of the + * field name, for use in the table header. The description should be plain + * text, it will be HTML-escaped later. + * + * @return array + */ + abstract function getFieldNames(); +} diff --git a/includes/parser/CacheTime.php b/includes/parser/CacheTime.php index 8190a8a0..94abc266 100644 --- a/includes/parser/CacheTime.php +++ b/includes/parser/CacheTime.php @@ -27,24 +27,64 @@ * @ingroup Parser */ class CacheTime { + /** @var array|bool ParserOptions which have been taken into account to + * produce output or false if not available. + */ + public $mUsedOptions; - var $mVersion = Parser::VERSION, # Compatibility check + public $mVersion = Parser::VERSION, # Compatibility check $mCacheTime = '', # Time when this object was generated, or -1 for uncacheable. Used in ParserCache. $mCacheExpiry = null, # Seconds after which the object should expire, use 0 for uncachable. Used in ParserCache. - $mContainsOldMagic; # Boolean variable indicating if the input contained variables like {{CURRENTDAY}} + $mContainsOldMagic, # Boolean variable indicating if the input contained variables like {{CURRENTDAY}} + $mCacheRevisionId = null; # Revision ID that was parsed - function getCacheTime() { return $this->mCacheTime; } + /** + * @return string TS_MW timestamp + */ + public function getCacheTime() { + return wfTimestamp( TS_MW, $this->mCacheTime ); + } - function containsOldMagic() { return $this->mContainsOldMagic; } - function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); } + /** + * @return bool + */ + public function containsOldMagic() { + return $this->mContainsOldMagic; + } + + /** + * @param bool $com + * @return bool + */ + public function setContainsOldMagic( $com ) { + return wfSetVar( $this->mContainsOldMagic, $com ); + } /** * setCacheTime() sets the timestamp expressing when the page has been rendered. - * This doesn not control expiry, see updateCacheExpiry() for that! - * @param $t string + * This does not control expiry, see updateCacheExpiry() for that! + * @param string $t * @return string */ - function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); } + public function setCacheTime( $t ) { + return wfSetVar( $this->mCacheTime, $t ); + } + + /** + * @since 1.23 + * @return int|null Revision id, if any was set + */ + public function getCacheRevisionId() { + return $this->mCacheRevisionId; + } + + /** + * @since 1.23 + * @param int $id Revision id + */ + public function setCacheRevisionId( $id ) { + $this->mCacheRevisionId = $id; + } /** * Sets the number of seconds after which this object should expire. @@ -54,9 +94,9 @@ class CacheTime { * or equal to the smallest number that was provided as an argument to * updateCacheExpiry(). * - * @param $seconds number + * @param int $seconds */ - function updateCacheExpiry( $seconds ) { + public function updateCacheExpiry( $seconds ) { $seconds = (int)$seconds; if ( $this->mCacheExpiry === null || $this->mCacheExpiry > $seconds ) { @@ -78,7 +118,7 @@ class CacheTime { * value of $wgParserCacheExpireTime. * @return int|mixed|null */ - function getCacheExpiry() { + public function getCacheExpiry() { global $wgParserCacheExpireTime; if ( $this->mCacheTime < 0 ) { @@ -107,7 +147,7 @@ class CacheTime { /** * @return bool */ - function isCacheable() { + public function isCacheable() { return $this->getCacheExpiry() > 0; } @@ -116,17 +156,35 @@ class CacheTime { * per-article cache invalidation timestamps, or if it comes from * an incompatible older version. * - * @param string $touched the affected article's last touched timestamp - * @return Boolean + * @param string $touched The affected article's last touched timestamp + * @return bool */ public function expired( $touched ) { global $wgCacheEpoch; - return !$this->isCacheable() || // parser says it's uncacheable - $this->getCacheTime() < $touched || - $this->getCacheTime() <= $wgCacheEpoch || - $this->getCacheTime() < wfTimestamp( TS_MW, time() - $this->getCacheExpiry() ) || // expiry period has passed - !isset( $this->mVersion ) || - version_compare( $this->mVersion, Parser::VERSION, "lt" ); + + return !$this->isCacheable() // parser says it's uncacheable + || $this->getCacheTime() < $touched + || $this->getCacheTime() <= $wgCacheEpoch + || $this->getCacheTime() < + wfTimestamp( TS_MW, time() - $this->getCacheExpiry() ) // expiry period has passed + || !isset( $this->mVersion ) + || version_compare( $this->mVersion, Parser::VERSION, "lt" ); } + /** + * Return true if this cached output object is for a different revision of + * the page. + * + * @todo We always return false if $this->getCacheRevisionId() is null; + * this prevents invalidating the whole parser cache when this change is + * deployed. Someday that should probably be changed. + * + * @since 1.23 + * @param int $id The affected article's current revision id + * @return bool + */ + public function isDifferentRevision( $id ) { + $cached = $this->getCacheRevisionId(); + return $cached !== null && $id !== $cached; + } } diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php index 4b6eeca2..eacbecd4 100644 --- a/includes/parser/CoreParserFunctions.php +++ b/includes/parser/CoreParserFunctions.php @@ -27,97 +27,69 @@ */ class CoreParserFunctions { /** - * @param $parser Parser + * @param Parser $parser * @return void */ - static function register( $parser ) { + public static function register( $parser ) { global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions; - # Syntax for arguments (see self::setFunctionHook): + # Syntax for arguments (see Parser::setFunctionHook): # "name for lookup in localized magic words array", # function callback, # optional SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}} # instead of {{#int:...}}) + $noHashFunctions = array( + 'ns', 'nse', 'urlencode', 'lcfirst', 'ucfirst', 'lc', 'uc', + 'localurl', 'localurle', 'fullurl', 'fullurle', 'canonicalurl', + 'canonicalurle', 'formatnum', 'grammar', 'gender', 'plural', + 'numberofpages', 'numberofusers', 'numberofactiveusers', + 'numberofarticles', 'numberoffiles', 'numberofadmins', + 'numberingroup', 'numberofedits', 'numberofviews', 'language', + 'padleft', 'padright', 'anchorencode', 'defaultsort', 'filepath', + 'pagesincategory', 'pagesize', 'protectionlevel', + 'namespacee', 'namespacenumber', 'talkspace', 'talkspacee', + 'subjectspace', 'subjectspacee', 'pagename', 'pagenamee', + 'fullpagename', 'fullpagenamee', 'rootpagename', 'rootpagenamee', + 'basepagename', 'basepagenamee', 'subpagename', 'subpagenamee', + 'talkpagename', 'talkpagenamee', 'subjectpagename', + 'subjectpagenamee', 'pageid', 'revisionid', 'revisionday', + 'revisionday2', 'revisionmonth', 'revisionmonth1', 'revisionyear', + 'revisiontimestamp', 'revisionuser', 'cascadingsources', + ); + foreach ( $noHashFunctions as $func ) { + $parser->setFunctionHook( $func, array( __CLASS__, $func ), SFH_NO_HASH ); + } - $parser->setFunctionHook( 'int', array( __CLASS__, 'intFunction' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'ns', array( __CLASS__, 'ns' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'nse', array( __CLASS__, 'nse' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'urlencode', array( __CLASS__, 'urlencode' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'lcfirst', array( __CLASS__, 'lcfirst' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'ucfirst', array( __CLASS__, 'ucfirst' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'lc', array( __CLASS__, 'lc' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'uc', array( __CLASS__, 'uc' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'localurl', array( __CLASS__, 'localurl' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'localurle', array( __CLASS__, 'localurle' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'fullurl', array( __CLASS__, 'fullurl' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'fullurle', array( __CLASS__, 'fullurle' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'canonicalurl', array( __CLASS__, 'canonicalurl' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'canonicalurle', array( __CLASS__, 'canonicalurle' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'formatnum', array( __CLASS__, 'formatnum' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'grammar', array( __CLASS__, 'grammar' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'gender', array( __CLASS__, 'gender' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'plural', array( __CLASS__, 'plural' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'numberofpages', array( __CLASS__, 'numberofpages' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'numberofusers', array( __CLASS__, 'numberofusers' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'numberofactiveusers', array( __CLASS__, 'numberofactiveusers' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'numberofarticles', array( __CLASS__, 'numberofarticles' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'numberoffiles', array( __CLASS__, 'numberoffiles' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'numberofadmins', array( __CLASS__, 'numberofadmins' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'numberingroup', array( __CLASS__, 'numberingroup' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'numberofedits', array( __CLASS__, 'numberofedits' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'numberofviews', array( __CLASS__, 'numberofviews' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'language', array( __CLASS__, 'language' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'padleft', array( __CLASS__, 'padleft' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'padright', array( __CLASS__, 'padright' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'anchorencode', array( __CLASS__, 'anchorencode' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'special', array( __CLASS__, 'special' ) ); - $parser->setFunctionHook( 'speciale', array( __CLASS__, 'speciale' ) ); - $parser->setFunctionHook( 'defaultsort', array( __CLASS__, 'defaultsort' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'filepath', array( __CLASS__, 'filepath' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'pagesincategory', array( __CLASS__, 'pagesincategory' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'pagesize', array( __CLASS__, 'pagesize' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'protectionlevel', array( __CLASS__, 'protectionlevel' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'namespace', array( __CLASS__, 'mwnamespace' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'namespacee', array( __CLASS__, 'namespacee' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'namespacenumber', array( __CLASS__, 'namespacenumber' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'talkspace', array( __CLASS__, 'talkspace' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'talkspacee', array( __CLASS__, 'talkspacee' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'subjectspace', array( __CLASS__, 'subjectspace' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'subjectspacee', array( __CLASS__, 'subjectspacee' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'pagename', array( __CLASS__, 'pagename' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'pagenamee', array( __CLASS__, 'pagenamee' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'fullpagename', array( __CLASS__, 'fullpagename' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'fullpagenamee', array( __CLASS__, 'fullpagenamee' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'rootpagename', array( __CLASS__, 'rootpagename' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'rootpagenamee', array( __CLASS__, 'rootpagenamee' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'basepagename', array( __CLASS__, 'basepagename' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'basepagenamee', array( __CLASS__, 'basepagenamee' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'subpagename', array( __CLASS__, 'subpagename' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'subpagenamee', array( __CLASS__, 'subpagenamee' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'talkpagename', array( __CLASS__, 'talkpagename' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'talkpagenamee', array( __CLASS__, 'talkpagenamee' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'subjectpagename', array( __CLASS__, 'subjectpagename' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'subjectpagenamee', array( __CLASS__, 'subjectpagenamee' ), SFH_NO_HASH ); - $parser->setFunctionHook( 'tag', array( __CLASS__, 'tagObj' ), SFH_OBJECT_ARGS ); - $parser->setFunctionHook( 'formatdate', array( __CLASS__, 'formatDate' ) ); + $parser->setFunctionHook( 'namespace', array( __CLASS__, 'mwnamespace' ), SFH_NO_HASH ); + $parser->setFunctionHook( 'int', array( __CLASS__, 'intFunction' ), SFH_NO_HASH ); + $parser->setFunctionHook( 'special', array( __CLASS__, 'special' ) ); + $parser->setFunctionHook( 'speciale', array( __CLASS__, 'speciale' ) ); + $parser->setFunctionHook( 'tag', array( __CLASS__, 'tagObj' ), SFH_OBJECT_ARGS ); + $parser->setFunctionHook( 'formatdate', array( __CLASS__, 'formatDate' ) ); if ( $wgAllowDisplayTitle ) { $parser->setFunctionHook( 'displaytitle', array( __CLASS__, 'displaytitle' ), SFH_NO_HASH ); } if ( $wgAllowSlowParserFunctions ) { - $parser->setFunctionHook( 'pagesinnamespace', array( __CLASS__, 'pagesinnamespace' ), SFH_NO_HASH ); + $parser->setFunctionHook( + 'pagesinnamespace', + array( __CLASS__, 'pagesinnamespace' ), + SFH_NO_HASH + ); } } /** - * @param $parser Parser + * @param Parser $parser * @param string $part1 * @return array */ - static function intFunction( $parser, $part1 = '' /*, ... */ ) { + public static function intFunction( $parser, $part1 = '' /*, ... */ ) { if ( strval( $part1 ) !== '' ) { $args = array_slice( func_get_args(), 2 ); - $message = wfMessage( $part1, $args )->inLanguage( $parser->getOptions()->getUserLangObj() )->plain(); + $message = wfMessage( $part1, $args ) + ->inLanguage( $parser->getOptions()->getUserLangObj() )->plain(); + return array( $message, 'noparse' => false ); } else { return array( 'found' => false ); @@ -125,12 +97,13 @@ class CoreParserFunctions { } /** - * @param $parser Parser - * @param $date - * @param null $defaultPref - * @return mixed|string + * @param Parser $parser + * @param string $date + * @param string $defaultPref + * + * @return string */ - static function formatDate( $parser, $date, $defaultPref = null ) { + public static function formatDate( $parser, $date, $defaultPref = null ) { $lang = $parser->getFunctionLang(); $df = DateFormatter::getInstance( $lang ); @@ -148,7 +121,7 @@ class CoreParserFunctions { return $date; } - static function ns( $parser, $part1 = '' ) { + public static function ns( $parser, $part1 = '' ) { global $wgContLang; if ( intval( $part1 ) || $part1 == "0" ) { $index = intval( $part1 ); @@ -162,7 +135,7 @@ class CoreParserFunctions { } } - static function nse( $parser, $part1 = '' ) { + public static function nse( $parser, $part1 = '' ) { $ret = self::ns( $parser, $part1 ); if ( is_string( $ret ) ) { $ret = wfUrlencode( str_replace( ' ', '_', $ret ) ); @@ -177,12 +150,12 @@ class CoreParserFunctions { * Or to encode a value for the HTTP "path", spaces are encoded as '%20'. * For links to "wiki"s, or similar software, spaces are encoded as '_', * - * @param $parser Parser object + * @param Parser $parser * @param string $s The text to encode. * @param string $arg (optional): The type of encoding. * @return string */ - static function urlencode( $parser, $s = '', $arg = null ) { + public static function urlencode( $parser, $s = '', $arg = null ) { static $magicWords = null; if ( is_null( $magicWords ) ) { $magicWords = new MagicWordArray( array( 'url_path', 'url_query', 'url_wiki' ) ); @@ -208,44 +181,76 @@ class CoreParserFunctions { return $parser->markerSkipCallback( $s, $func ); } - static function lcfirst( $parser, $s = '' ) { + public static function lcfirst( $parser, $s = '' ) { global $wgContLang; return $wgContLang->lcfirst( $s ); } - static function ucfirst( $parser, $s = '' ) { + public static function ucfirst( $parser, $s = '' ) { global $wgContLang; return $wgContLang->ucfirst( $s ); } /** - * @param $parser Parser + * @param Parser $parser * @param string $s - * @return + * @return string */ - static function lc( $parser, $s = '' ) { + public static function lc( $parser, $s = '' ) { global $wgContLang; return $parser->markerSkipCallback( $s, array( $wgContLang, 'lc' ) ); } /** - * @param $parser Parser + * @param Parser $parser * @param string $s - * @return + * @return string */ - static function uc( $parser, $s = '' ) { + public static function uc( $parser, $s = '' ) { global $wgContLang; return $parser->markerSkipCallback( $s, array( $wgContLang, 'uc' ) ); } - static function localurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getLocalURL', $s, $arg ); } - static function localurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeLocalURL', $s, $arg ); } - static function fullurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getFullURL', $s, $arg ); } - static function fullurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeFullURL', $s, $arg ); } - static function canonicalurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getCanonicalURL', $s, $arg ); } - static function canonicalurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeCanonicalURL', $s, $arg ); } + public static function localurl( $parser, $s = '', $arg = null ) { + return self::urlFunction( 'getLocalURL', $s, $arg ); + } + + public static function localurle( $parser, $s = '', $arg = null ) { + $temp = self::urlFunction( 'getLocalURL', $s, $arg ); + if ( !is_string( $temp ) ) { + return $temp; + } else { + return htmlspecialchars( $temp ); + } + } + + public static function fullurl( $parser, $s = '', $arg = null ) { + return self::urlFunction( 'getFullURL', $s, $arg ); + } + + public static function fullurle( $parser, $s = '', $arg = null ) { + $temp = self::urlFunction( 'getFullURL', $s, $arg ); + if ( !is_string( $temp ) ) { + return $temp; + } else { + return htmlspecialchars( $temp ); + } + } + + public static function canonicalurl( $parser, $s = '', $arg = null ) { + return self::urlFunction( 'getCanonicalURL', $s, $arg ); + } - static function urlFunction( $func, $s = '', $arg = null ) { + public static function canonicalurle( $parser, $s = '', $arg = null ) { + $temp = self::urlFunction( 'getCanonicalURL', $s, $arg ); + if ( !is_string( $temp ) ) { + return $temp; + } else { + return htmlspecialchars( $temp ); + } + } + + public static function urlFunction( $func, $s = '', $arg = null ) { $title = Title::newFromText( $s ); # Due to order of execution of a lot of bits, the values might be encoded # before arriving here; if that's true, then the title can't be created @@ -271,12 +276,12 @@ class CoreParserFunctions { } /** - * @param $parser Parser + * @param Parser $parser * @param string $num * @param string $arg * @return string */ - static function formatnum( $parser, $num = '', $arg = null ) { + public static function formatnum( $parser, $num = '', $arg = null ) { if ( self::matchAgainstMagicword( 'rawsuffix', $arg ) ) { $func = array( $parser->getFunctionLang(), 'parseFormattedNumber' ); } elseif ( self::matchAgainstMagicword( 'nocommafysuffix', $arg ) ) { @@ -288,22 +293,22 @@ class CoreParserFunctions { } /** - * @param $parser Parser + * @param Parser $parser * @param string $case * @param string $word - * @return + * @return string */ - static function grammar( $parser, $case = '', $word = '' ) { + public static function grammar( $parser, $case = '', $word = '' ) { $word = $parser->killMarkers( $word ); return $parser->getFunctionLang()->convertGrammar( $word, $case ); } /** - * @param $parser Parser - * @param $username string - * @return + * @param Parser $parser + * @param string $username + * @return string */ - static function gender( $parser, $username ) { + public static function gender( $parser, $username ) { wfProfileIn( __METHOD__ ); $forms = array_slice( func_get_args(), 2 ); @@ -341,11 +346,11 @@ class CoreParserFunctions { } /** - * @param $parser Parser + * @param Parser $parser * @param string $text - * @return + * @return string */ - static function plural( $parser, $text = '' ) { + public static function plural( $parser, $text = '' ) { $forms = array_slice( func_get_args(), 2 ); $text = $parser->getFunctionLang()->parseFormattedNumber( $text ); settype( $text, ctype_digit( $text ) ? 'int' : 'float' ); @@ -356,13 +361,20 @@ class CoreParserFunctions { * Override the title of the page when viewed, provided we've been given a * title which will normalise to the canonical title * - * @param $parser Parser: parent parser - * @param string $text desired title text - * @return String + * @param Parser $parser Parent parser + * @param string $text Desired title text + * @param string $uarg + * @return string */ - static function displaytitle( $parser, $text = '' ) { + public static function displaytitle( $parser, $text = '', $uarg = '' ) { global $wgRestrictDisplayTitle; + static $magicWords = null; + if ( is_null( $magicWords ) ) { + $magicWords = new MagicWordArray( array( 'displaytitle_noerror', 'displaytitle_noreplace' ) ); + } + $arg = $magicWords->matchStartToEnd( $uarg ); + // parse a limited subset of wiki markup (just the single quote items) $text = $parser->doQuotes( $text ); @@ -373,7 +385,7 @@ class CoreParserFunctions { // list of disallowed tags for DISPLAYTITLE // these will be escaped even though they are allowed in normal wiki text $bad = array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'blockquote', 'ol', 'ul', 'li', 'hr', - 'table', 'tr', 'th', 'td', 'dl', 'dd', 'caption', 'p', 'ruby', 'rb', 'rt', 'rp', 'br' ); + 'table', 'tr', 'th', 'td', 'dl', 'dd', 'caption', 'p', 'ruby', 'rb', 'rt', 'rtc', 'rp', 'br' ); // disallow some styles that could be used to bypass $wgRestrictDisplayTitle if ( $wgRestrictDisplayTitle ) { @@ -399,13 +411,34 @@ class CoreParserFunctions { // only requested titles that normalize to the actual title are allowed through // if $wgRestrictDisplayTitle is true (it is by default) // mimic the escaping process that occurs in OutputPage::setPageTitle - $text = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $text, $htmlTagsCallback, array(), array(), $bad ) ); + $text = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( + $text, + $htmlTagsCallback, + array(), + array(), + $bad + ) ); $title = Title::newFromText( Sanitizer::stripAllTags( $text ) ); - if ( !$wgRestrictDisplayTitle ) { - $parser->mOutput->setDisplayTitle( $text ); - } elseif ( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) ) { - $parser->mOutput->setDisplayTitle( $text ); + if ( !$wgRestrictDisplayTitle || + ( $title instanceof Title + && !$title->hasFragment() + && $title->equals( $parser->mTitle ) ) + ) { + $old = $parser->mOutput->getProperty( 'displaytitle' ); + if ( $old === false || $arg !== 'displaytitle_noreplace' ) { + $parser->mOutput->setDisplayTitle( $text ); + } + if ( $old !== false && $old !== $text && !$arg ) { + $converter = $parser->getConverterLanguage()->getConverter(); + return '' . + wfMessage( 'duplicate-displaytitle', + // Message should be parsed, but these params should only be escaped. + $converter->markNoConversion( wfEscapeWikiText( $old ) ), + $converter->markNoConversion( wfEscapeWikiText( $text ) ) + )->inContentLanguage()->text() . + ''; + } } return ''; @@ -414,19 +447,20 @@ class CoreParserFunctions { /** * Matches the given value against the value of given magic word * - * @param string $magicword magic word key - * @param mixed $value value to match - * @return boolean true on successful match + * @param string $magicword Magic word key + * @param string $value Value to match + * @return bool True on successful match */ - static private function matchAgainstMagicword( $magicword, $value ) { - if ( strval( $value ) === '' ) { + private static function matchAgainstMagicword( $magicword, $value ) { + $value = trim( strval( $value ) ); + if ( $value === '' ) { return false; } $mwObject = MagicWord::get( $magicword ); - return $mwObject->match( $value ); + return $mwObject->matchStartToEnd( $value ); } - static function formatRaw( $num, $raw ) { + public static function formatRaw( $num, $raw ) { if ( self::matchAgainstMagicword( 'rawsuffix', $raw ) ) { return $num; } else { @@ -434,35 +468,35 @@ class CoreParserFunctions { return $wgContLang->formatNum( $num ); } } - static function numberofpages( $parser, $raw = null ) { + public static function numberofpages( $parser, $raw = null ) { return self::formatRaw( SiteStats::pages(), $raw ); } - static function numberofusers( $parser, $raw = null ) { + public static function numberofusers( $parser, $raw = null ) { return self::formatRaw( SiteStats::users(), $raw ); } - static function numberofactiveusers( $parser, $raw = null ) { + public static function numberofactiveusers( $parser, $raw = null ) { return self::formatRaw( SiteStats::activeUsers(), $raw ); } - static function numberofarticles( $parser, $raw = null ) { + public static function numberofarticles( $parser, $raw = null ) { return self::formatRaw( SiteStats::articles(), $raw ); } - static function numberoffiles( $parser, $raw = null ) { + public static function numberoffiles( $parser, $raw = null ) { return self::formatRaw( SiteStats::images(), $raw ); } - static function numberofadmins( $parser, $raw = null ) { + public static function numberofadmins( $parser, $raw = null ) { return self::formatRaw( SiteStats::numberingroup( 'sysop' ), $raw ); } - static function numberofedits( $parser, $raw = null ) { + public static function numberofedits( $parser, $raw = null ) { return self::formatRaw( SiteStats::edits(), $raw ); } - static function numberofviews( $parser, $raw = null ) { + public static function numberofviews( $parser, $raw = null ) { global $wgDisableCounters; return !$wgDisableCounters ? self::formatRaw( SiteStats::views(), $raw ) : ''; } - static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) { + public static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) { return self::formatRaw( SiteStats::pagesInNs( intval( $namespace ) ), $raw ); } - static function numberingroup( $parser, $name = '', $raw = null ) { + public static function numberingroup( $parser, $name = '', $raw = null ) { return self::formatRaw( SiteStats::numberingroup( strtolower( $name ) ), $raw ); } @@ -471,51 +505,53 @@ class CoreParserFunctions { * corresponding magic word * Note: function name changed to "mwnamespace" rather than "namespace" * to not break PHP 5.3 + * @param Parser $parser + * @param string $title * @return mixed|string */ - static function mwnamespace( $parser, $title = null ) { + public static function mwnamespace( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return str_replace( '_', ' ', $t->getNsText() ); } - static function namespacee( $parser, $title = null ) { + public static function namespacee( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return wfUrlencode( $t->getNsText() ); } - static function namespacenumber( $parser, $title = null ) { + public static function namespacenumber( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return $t->getNamespace(); } - static function talkspace( $parser, $title = null ) { + public static function talkspace( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) || !$t->canTalk() ) { return ''; } return str_replace( '_', ' ', $t->getTalkNsText() ); } - static function talkspacee( $parser, $title = null ) { + public static function talkspacee( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) || !$t->canTalk() ) { return ''; } return wfUrlencode( $t->getTalkNsText() ); } - static function subjectspace( $parser, $title = null ) { + public static function subjectspace( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return str_replace( '_', ' ', $t->getSubjectNsText() ); } - static function subjectspacee( $parser, $title = null ) { + public static function subjectspacee( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; @@ -526,100 +562,102 @@ class CoreParserFunctions { /** * Functions to get and normalize pagenames, corresponding to the magic words * of the same names - * @return String + * @param Parser $parser + * @param string $title + * @return string */ - static function pagename( $parser, $title = null ) { + public static function pagename( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return wfEscapeWikiText( $t->getText() ); } - static function pagenamee( $parser, $title = null ) { + public static function pagenamee( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return wfEscapeWikiText( $t->getPartialURL() ); } - static function fullpagename( $parser, $title = null ) { + public static function fullpagename( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) || !$t->canTalk() ) { return ''; } return wfEscapeWikiText( $t->getPrefixedText() ); } - static function fullpagenamee( $parser, $title = null ) { + public static function fullpagenamee( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) || !$t->canTalk() ) { return ''; } return wfEscapeWikiText( $t->getPrefixedURL() ); } - static function subpagename( $parser, $title = null ) { + public static function subpagename( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return wfEscapeWikiText( $t->getSubpageText() ); } - static function subpagenamee( $parser, $title = null ) { + public static function subpagenamee( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return wfEscapeWikiText( $t->getSubpageUrlForm() ); } - static function rootpagename( $parser, $title = null ) { + public static function rootpagename( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return wfEscapeWikiText( $t->getRootText() ); } - static function rootpagenamee( $parser, $title = null ) { + public static function rootpagenamee( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return wfEscapeWikiText( wfUrlEncode( str_replace( ' ', '_', $t->getRootText() ) ) ); } - static function basepagename( $parser, $title = null ) { + public static function basepagename( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return wfEscapeWikiText( $t->getBaseText() ); } - static function basepagenamee( $parser, $title = null ) { + public static function basepagenamee( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return wfEscapeWikiText( wfUrlEncode( str_replace( ' ', '_', $t->getBaseText() ) ) ); } - static function talkpagename( $parser, $title = null ) { + public static function talkpagename( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) || !$t->canTalk() ) { return ''; } return wfEscapeWikiText( $t->getTalkPage()->getPrefixedText() ); } - static function talkpagenamee( $parser, $title = null ) { + public static function talkpagenamee( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) || !$t->canTalk() ) { return ''; } return wfEscapeWikiText( $t->getTalkPage()->getPrefixedURL() ); } - static function subjectpagename( $parser, $title = null ) { + public static function subjectpagename( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; } return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedText() ); } - static function subjectpagenamee( $parser, $title = null ) { + public static function subjectpagenamee( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) ) { return ''; @@ -631,9 +669,13 @@ class CoreParserFunctions { * Return the number of pages, files or subcats in the given category, * or 0 if it's nonexistent. This is an expensive parser function and * can't be called too many times per page. + * @param Parser $parser + * @param string $name + * @param string $arg1 + * @param string $arg2 * @return string */ - static function pagesincategory( $parser, $name = '', $arg1 = null, $arg2 = null ) { + public static function pagesincategory( $parser, $name = '', $arg1 = null, $arg2 = null ) { global $wgContLang; static $magicWords = null; if ( is_null( $magicWords ) ) { @@ -695,46 +737,29 @@ class CoreParserFunctions { * Return the size of the given page, or 0 if it's nonexistent. This is an * expensive parser function and can't be called too many times per page. * - * @todo FIXME: Title::getLength() documentation claims that it adds things - * to the link cache, so the local cache here should be unnecessary, but - * in fact calling getLength() repeatedly for the same $page does seem to - * run one query for each call? - * @todo Document parameters - * - * @param $parser Parser - * @param $page String Name of page to check (Default: empty string) - * @param $raw String Should number be human readable with commas or just number + * @param Parser $parser + * @param string $page Name of page to check (Default: empty string) + * @param string $raw Should number be human readable with commas or just number * @return string */ - static function pagesize( $parser, $page = '', $raw = null ) { - static $cache = array(); + public static function pagesize( $parser, $page = '', $raw = null ) { $title = Title::newFromText( $page ); if ( !is_object( $title ) ) { - $cache[$page] = 0; return self::formatRaw( 0, $raw ); } - # Normalize name for cache - $page = $title->getPrefixedText(); - - $length = 0; - if ( isset( $cache[$page] ) ) { - $length = $cache[$page]; - } elseif ( $parser->incrementExpensiveFunctionCount() ) { - $rev = Revision::newFromTitle( $title, false, Revision::READ_NORMAL ); - $pageID = $rev ? $rev->getPage() : 0; - $revID = $rev ? $rev->getId() : 0; - $length = $cache[$page] = $rev ? $rev->getSize() : 0; - - // Register dependency in templatelinks - $parser->mOutput->addTemplate( $title, $pageID, $revID ); - } + // fetch revision from cache/database and return the value + $rev = self::getCachedRevisionObject( $parser, $title ); + $length = $rev ? $rev->getSize() : 0; return self::formatRaw( $length, $raw ); } /** - * Returns the requested protection level for the current page + * Returns the requested protection level for the current page. This + * is an expensive parser function and can't be called too many times + * per page, unless the protection levels for the given title have + * already been retrieved * * @param Parser $parser * @param string $type @@ -742,25 +767,28 @@ class CoreParserFunctions { * * @return string */ - static function protectionlevel( $parser, $type = '', $title = '' ) { + public static function protectionlevel( $parser, $type = '', $title = '' ) { $titleObject = Title::newFromText( $title ); if ( !( $titleObject instanceof Title ) ) { $titleObject = $parser->mTitle; } - $restrictions = $titleObject->getRestrictions( strtolower( $type ) ); - # Title::getRestrictions returns an array, its possible it may have - # multiple values in the future - return implode( $restrictions, ',' ); + if ( $titleObject->areRestrictionsLoaded() || $parser->incrementExpensiveFunctionCount() ) { + $restrictions = $titleObject->getRestrictions( strtolower( $type ) ); + # Title::getRestrictions returns an array, its possible it may have + # multiple values in the future + return implode( $restrictions, ',' ); + } + return ''; } /** * Gives language names. - * @param $parser Parser - * @param string $code Language code (of which to get name) - * @param string $inLanguage Language code (in which to get name) - * @return String + * @param Parser $parser + * @param string $code Language code (of which to get name) + * @param string $inLanguage Language code (in which to get name) + * @return string */ - static function language( $parser, $code = '', $inLanguage = '' ) { + public static function language( $parser, $code = '', $inLanguage = '' ) { $code = strtolower( $code ); $inLanguage = strtolower( $inLanguage ); $lang = Language::fetchLanguageName( $code, $inLanguage ); @@ -769,9 +797,14 @@ class CoreParserFunctions { /** * Unicode-safe str_pad with the restriction that $length is forced to be <= 500 + * @param Parser $parser + * @param string $string + * @param int $length + * @param string $padding + * @param int $direction * @return string */ - static function pad( $parser, $string, $length, $padding = '0', $direction = STR_PAD_RIGHT ) { + public static function pad( $parser, $string, $length, $padding = '0', $direction = STR_PAD_RIGHT ) { $padding = $parser->killMarkers( $padding ); $lengthOfPadding = mb_strlen( $padding ); if ( $lengthOfPadding == 0 ) { @@ -797,25 +830,25 @@ class CoreParserFunctions { } } - static function padleft( $parser, $string = '', $length = 0, $padding = '0' ) { + public static function padleft( $parser, $string = '', $length = 0, $padding = '0' ) { return self::pad( $parser, $string, $length, $padding, STR_PAD_LEFT ); } - static function padright( $parser, $string = '', $length = 0, $padding = '0' ) { + public static function padright( $parser, $string = '', $length = 0, $padding = '0' ) { return self::pad( $parser, $string, $length, $padding ); } /** - * @param $parser Parser - * @param $text + * @param Parser $parser + * @param string $text * @return string */ - static function anchorencode( $parser, $text ) { + public static function anchorencode( $parser, $text ) { $text = $parser->killMarkers( $text ); return (string)substr( $parser->guessSectionNameFromWikiText( $text ), 1 ); } - static function special( $parser, $text ) { + public static function special( $parser, $text ) { list( $page, $subpage ) = SpecialPageFactory::resolveAlias( $text ); if ( $page ) { $title = SpecialPage::getTitleFor( $page, $subpage ); @@ -827,12 +860,12 @@ class CoreParserFunctions { } } - static function speciale( $parser, $text ) { + public static function speciale( $parser, $text ) { return wfUrlencode( str_replace( ' ', '_', self::special( $parser, $text ) ) ); } /** - * @param $parser Parser + * @param Parser $parser * @param string $text The sortkey to use * @param string $uarg Either "noreplace" or "noerror" (in en) * both suppress errors, and noreplace does nothing if @@ -869,8 +902,9 @@ class CoreParserFunctions { } } - // Usage {{filepath|300}}, {{filepath|nowiki}}, {{filepath|nowiki|300}} or {{filepath|300|nowiki}} - // or {{filepath|300px}}, {{filepath|200x300px}}, {{filepath|nowiki|200x300px}}, {{filepath|200x300px|nowiki}} + // Usage {{filepath|300}}, {{filepath|nowiki}}, {{filepath|nowiki|300}} + // or {{filepath|300|nowiki}} or {{filepath|300px}}, {{filepath|200x300px}}, + // {{filepath|nowiki|200x300px}}, {{filepath|200x300px|nowiki}}. public static function filepath( $parser, $name = '', $argA = '', $argB = '' ) { $file = wfFindFile( $name ); @@ -907,6 +941,9 @@ class CoreParserFunctions { /** * Parser function to extension tag adaptor + * @param Parser $parser + * @param PPFrame $frame + * @param array $args * @return string */ public static function tagObj( $parser, $frame, $args ) { @@ -949,4 +986,271 @@ class CoreParserFunctions { ); return $parser->extensionSubstitution( $params, $frame ); } + + /** + * Fetched the current revision of the given title and return this. + * Will increment the expensive function count and + * add a template link to get the value refreshed on changes. + * For a given title, which is equal to the current parser title, + * the revision object from the parser is used, when that is the current one + * + * @param Parser $parser + * @param Title $title + * @return Revision + * @since 1.23 + */ + private static function getCachedRevisionObject( $parser, $title = null ) { + static $cache = null; + if ( $cache == null ) { + $cache = new MapCacheLRU( 50 ); + } + + if ( is_null( $title ) ) { + return null; + } + + // Use the revision from the parser itself, when param is the current page + // and the revision is the current one + if ( $title->equals( $parser->getTitle() ) ) { + $parserRev = $parser->getRevisionObject(); + if ( $parserRev && $parserRev->isCurrent() ) { + // force reparse after edit with vary-revision flag + $parser->getOutput()->setFlag( 'vary-revision' ); + wfDebug( __METHOD__ . ": use current revision from parser, setting vary-revision...\n" ); + return $parserRev; + } + } + + // Normalize name for cache + $page = $title->getPrefixedDBkey(); + + if ( $cache->has( $page ) ) { // cache contains null values + return $cache->get( $page ); + } + if ( $parser->incrementExpensiveFunctionCount() ) { + $rev = Revision::newFromTitle( $title, false, Revision::READ_NORMAL ); + $pageID = $rev ? $rev->getPage() : 0; + $revID = $rev ? $rev->getId() : 0; + $cache->set( $page, $rev ); // maybe null + + // Register dependency in templatelinks + $parser->getOutput()->addTemplate( $title, $pageID, $revID ); + + return $rev; + } + $cache->set( $page, null ); + return null; + } + + /** + * Get the pageid of a specified page + * @param Parser $parser + * @param string $title Title to get the pageid from + * @return int|null|string + * @since 1.23 + */ + public static function pageid( $parser, $title = null ) { + $t = Title::newFromText( $title ); + if ( is_null( $t ) ) { + return ''; + } + // Use title from parser to have correct pageid after edit + if ( $t->equals( $parser->getTitle() ) ) { + $t = $parser->getTitle(); + return $t->getArticleID(); + } + + // These can't have ids + if ( !$t->canExist() || $t->isExternal() ) { + return 0; + } + + // Check the link cache, maybe something already looked it up. + $linkCache = LinkCache::singleton(); + $pdbk = $t->getPrefixedDBkey(); + $id = $linkCache->getGoodLinkID( $pdbk ); + if ( $id != 0 ) { + $parser->mOutput->addLink( $t, $id ); + return $id; + } + if ( $linkCache->isBadLink( $pdbk ) ) { + $parser->mOutput->addLink( $t, 0 ); + return $id; + } + + // We need to load it from the DB, so mark expensive + if ( $parser->incrementExpensiveFunctionCount() ) { + $id = $t->getArticleID(); + $parser->mOutput->addLink( $t, $id ); + return $id; + } + return null; + } + + /** + * Get the id from the last revision of a specified page. + * @param Parser $parser + * @param string $title Title to get the id from + * @return int|null|string + * @since 1.23 + */ + public static function revisionid( $parser, $title = null ) { + $t = Title::newFromText( $title ); + if ( is_null( $t ) ) { + return ''; + } + // fetch revision from cache/database and return the value + $rev = self::getCachedRevisionObject( $parser, $t ); + return $rev ? $rev->getId() : ''; + } + + /** + * Get the day from the last revision of a specified page. + * @param Parser $parser + * @param string $title Title to get the day from + * @return string + * @since 1.23 + */ + public static function revisionday( $parser, $title = null ) { + $t = Title::newFromText( $title ); + if ( is_null( $t ) ) { + return ''; + } + // fetch revision from cache/database and return the value + $rev = self::getCachedRevisionObject( $parser, $t ); + return $rev ? MWTimestamp::getLocalInstance( $rev->getTimestamp() )->format( 'j' ) : ''; + } + + /** + * Get the day with leading zeros from the last revision of a specified page. + * @param Parser $parser + * @param string $title Title to get the day from + * @return string + * @since 1.23 + */ + public static function revisionday2( $parser, $title = null ) { + $t = Title::newFromText( $title ); + if ( is_null( $t ) ) { + return ''; + } + // fetch revision from cache/database and return the value + $rev = self::getCachedRevisionObject( $parser, $t ); + return $rev ? MWTimestamp::getLocalInstance( $rev->getTimestamp() )->format( 'd' ) : ''; + } + + /** + * Get the month with leading zeros from the last revision of a specified page. + * @param Parser $parser + * @param string $title Title to get the month from + * @return string + * @since 1.23 + */ + public static function revisionmonth( $parser, $title = null ) { + $t = Title::newFromText( $title ); + if ( is_null( $t ) ) { + return ''; + } + // fetch revision from cache/database and return the value + $rev = self::getCachedRevisionObject( $parser, $t ); + return $rev ? MWTimestamp::getLocalInstance( $rev->getTimestamp() )->format( 'm' ) : ''; + } + + /** + * Get the month from the last revision of a specified page. + * @param Parser $parser + * @param string $title Title to get the month from + * @return string + * @since 1.23 + */ + public static function revisionmonth1( $parser, $title = null ) { + $t = Title::newFromText( $title ); + if ( is_null( $t ) ) { + return ''; + } + // fetch revision from cache/database and return the value + $rev = self::getCachedRevisionObject( $parser, $t ); + return $rev ? MWTimestamp::getLocalInstance( $rev->getTimestamp() )->format( 'n' ) : ''; + } + + /** + * Get the year from the last revision of a specified page. + * @param Parser $parser + * @param string $title Title to get the year from + * @return string + * @since 1.23 + */ + public static function revisionyear( $parser, $title = null ) { + $t = Title::newFromText( $title ); + if ( is_null( $t ) ) { + return ''; + } + // fetch revision from cache/database and return the value + $rev = self::getCachedRevisionObject( $parser, $t ); + return $rev ? MWTimestamp::getLocalInstance( $rev->getTimestamp() )->format( 'Y' ) : ''; + } + + /** + * Get the timestamp from the last revision of a specified page. + * @param Parser $parser + * @param string $title Title to get the timestamp from + * @return string + * @since 1.23 + */ + public static function revisiontimestamp( $parser, $title = null ) { + $t = Title::newFromText( $title ); + if ( is_null( $t ) ) { + return ''; + } + // fetch revision from cache/database and return the value + $rev = self::getCachedRevisionObject( $parser, $t ); + return $rev ? MWTimestamp::getLocalInstance( $rev->getTimestamp() )->format( 'YmdHis' ) : ''; + } + + /** + * Get the user from the last revision of a specified page. + * @param Parser $parser + * @param string $title Title to get the user from + * @return string + * @since 1.23 + */ + public static function revisionuser( $parser, $title = null ) { + $t = Title::newFromText( $title ); + if ( is_null( $t ) ) { + return ''; + } + // fetch revision from cache/database and return the value + $rev = self::getCachedRevisionObject( $parser, $t ); + return $rev ? $rev->getUserText() : ''; + } + + /** + * Returns the sources of any cascading protection acting on a specified page. + * Pages will not return their own title unless they transclude themselves. + * This is an expensive parser function and can't be called too many times per page, + * unless cascading protection sources for the page have already been loaded. + * + * @param Parser $parser + * @param string $title + * + * @return string + * @since 1.23 + */ + public static function cascadingsources( $parser, $title = '' ) { + $titleObject = Title::newFromText( $title ); + if ( !( $titleObject instanceof Title ) ) { + $titleObject = $parser->mTitle; + } + if ( $titleObject->areCascadeProtectionSourcesLoaded() + || $parser->incrementExpensiveFunctionCount() + ) { + $names = array(); + $sources = $titleObject->getCascadeProtectionSources(); + foreach ( $sources[0] as $sourceTitle ) { + $names[] = $sourceTitle->getPrefixedText(); + } + return implode( $names, '|' ); + } + return ''; + } + } diff --git a/includes/parser/CoreTagHooks.php b/includes/parser/CoreTagHooks.php index a2eb6987..85920cc1 100644 --- a/includes/parser/CoreTagHooks.php +++ b/includes/parser/CoreTagHooks.php @@ -27,10 +27,10 @@ */ class CoreTagHooks { /** - * @param $parser Parser + * @param Parser $parser * @return void */ - static function register( $parser ) { + public static function register( $parser ) { global $wgRawHtml; $parser->setHook( 'pre', array( __CLASS__, 'pre' ) ); $parser->setHook( 'nowiki', array( __CLASS__, 'nowiki' ) ); @@ -50,7 +50,7 @@ class CoreTagHooks { * @param Parser $parser * @return string HTML */ - static function pre( $text, $attribs, $parser ) { + public static function pre( $text, $attribs, $parser ) { // Backwards-compatibility hack $content = StringUtils::delimiterReplace( '', '', '$1', $text, 'i' ); @@ -69,13 +69,13 @@ class CoreTagHooks { * * Uses undocumented extended tag hook return values, introduced in r61913. * - * @param $content string - * @param $attributes array - * @param $parser Parser + * @param string $content + * @param array $attributes + * @param Parser $parser * @throws MWException * @return array */ - static function html( $content, $attributes, $parser ) { + public static function html( $content, $attributes, $parser ) { global $wgRawHtml; if ( $wgRawHtml ) { return array( $content, 'markerType' => 'nowiki' ); @@ -91,12 +91,12 @@ class CoreTagHooks { * * Uses undocumented extended tag hook return values, introduced in r61913. * - * @param $content string - * @param $attributes array - * @param $parser Parser + * @param string $content + * @param array $attributes + * @param Parser $parser * @return array */ - static function nowiki( $content, $attributes, $parser ) { + public static function nowiki( $content, $attributes, $parser ) { $content = strtr( $content, array( '-{' => '-{', '}-' => '}-' ) ); return array( Xml::escapeTagsOnly( $content ), 'markerType' => 'nowiki' ); } @@ -107,7 +107,7 @@ class CoreTagHooks { * Renders a thumbnail list of the given images, with optional captions. * Full syntax documented on the wiki: * - * http://www.mediawiki.org/wiki/Help:Images#Gallery_syntax + * https://www.mediawiki.org/wiki/Help:Images#Gallery_syntax * * @todo break Parser::renderImageGallery out here too. * @@ -116,7 +116,7 @@ class CoreTagHooks { * @param Parser $parser * @return string HTML */ - static function gallery( $content, $attributes, $parser ) { + public static function gallery( $content, $attributes, $parser ) { return $parser->renderImageGallery( $content, $attributes ); } } diff --git a/includes/parser/DateFormatter.php b/includes/parser/DateFormatter.php index 0a69b045..82f0e9d4 100644 --- a/includes/parser/DateFormatter.php +++ b/includes/parser/DateFormatter.php @@ -27,11 +27,11 @@ * @ingroup Parser */ class DateFormatter { - var $mSource, $mTarget; - var $monthNames = '', $rxDM, $rxMD, $rxDMY, $rxYDM, $rxMDY, $rxYMD; + public $mSource, $mTarget; + public $monthNames = '', $rxDM, $rxMD, $rxDMY, $rxYDM, $rxMDY, $rxYMD; - var $regexes, $pDays, $pMonths, $pYears; - var $rules, $xMonths, $preferences; + public $regexes, $pDays, $pMonths, $pYears; + public $rules, $xMonths, $preferences; protected $lang; @@ -49,9 +49,9 @@ class DateFormatter { const LAST = 8; /** - * @param $lang Language In which language to format the date + * @param Language $lang In which language to format the date */ - function __construct( Language $lang ) { + public function __construct( Language $lang ) { $this->lang = $lang; $this->monthNames = $this->getMonthRegex(); @@ -120,9 +120,9 @@ class DateFormatter { /** * Get a DateFormatter object * - * @param $lang Language|string|null In which language to format the date + * @param Language|string|null $lang In which language to format the date * Defaults to the site content language - * @return DateFormatter object + * @return DateFormatter */ public static function &getInstance( $lang = null ) { global $wgMemc, $wgContLang; @@ -142,10 +142,11 @@ class DateFormatter { /** * @param string $preference User preference * @param string $text Text to reformat - * @param array $options can contain 'linked' and/or 'match-whole' - * @return mixed|String + * @param array $options Array can contain 'linked' and/or 'match-whole' + * + * @return string */ - function reformat( $preference, $text, $options = array( 'linked' ) ) { + public function reformat( $preference, $text, $options = array( 'linked' ) ) { $linked = in_array( 'linked', $options ); $match_whole = in_array( 'match-whole', $options ); @@ -192,10 +193,10 @@ class DateFormatter { } /** - * @param $matches + * @param array $matches * @return string */ - function replace( $matches ) { + public function replace( $matches ) { # Extract information from $matches $linked = true; if ( isset( $this->mLinked ) ) { @@ -204,7 +205,8 @@ class DateFormatter { $bits = array(); $key = $this->keys[$this->mSource]; - for ( $p = 0; $p < strlen( $key ); $p++ ) { + $keyLength = strlen( $key ); + for ( $p = 0; $p < $keyLength; $p++ ) { if ( $key[$p] != ' ' ) { $bits[$key[$p]] = $matches[$p + 1]; } @@ -214,11 +216,11 @@ class DateFormatter { } /** - * @param $bits array - * @param $link bool + * @param array $bits + * @param bool $link * @return string */ - function formatDate( $bits, $link = true ) { + public function formatDate( $bits, $link = true ) { $format = $this->targets[$this->mTarget]; if ( !$link ) { @@ -253,7 +255,8 @@ class DateFormatter { $bits['d'] = sprintf( '%02d', $bits['j'] ); } - for ( $p = 0; $p < strlen( $format ); $p++ ) { + $formatLength = strlen( $format ); + for ( $p = 0; $p < $formatLength; $p++ ) { $char = $format[$p]; switch ( $char ) { case 'd': # ISO day of month @@ -292,6 +295,7 @@ class DateFormatter { } } if ( $fail ) { + /** @todo FIXME: $matches doesn't exist here, what's expected? */ $text = $matches[0]; } @@ -314,7 +318,7 @@ class DateFormatter { * @todo document * @return string */ - function getMonthRegex() { + public function getMonthRegex() { $names = array(); for ( $i = 1; $i <= 12; $i++ ) { $names[] = $this->lang->getMonthName( $i ); @@ -325,10 +329,10 @@ class DateFormatter { /** * Makes an ISO month, e.g. 02, from a month name - * @param string $monthName month name + * @param string $monthName Month name * @return string ISO month name */ - function makeIsoMonth( $monthName ) { + public function makeIsoMonth( $monthName ) { $n = $this->xMonths[$this->lang->lc( $monthName )]; return sprintf( '%02d', $n ); } @@ -338,7 +342,7 @@ class DateFormatter { * @param string $year Year name * @return string ISO year name */ - function makeIsoYear( $year ) { + public function makeIsoYear( $year ) { # Assumes the year is in a nice format, as enforced by the regex if ( substr( $year, -2 ) == 'BC' ) { $num = intval( substr( $year, 0, -3 ) ) - 1; @@ -353,9 +357,10 @@ class DateFormatter { /** * @todo document + * @param string $iso * @return int|string */ - function makeNormalYear( $iso ) { + public function makeNormalYear( $iso ) { if ( $iso[0] == '-' ) { $text = ( intval( substr( $iso, 1 ) ) + 1 ) . ' BC'; } else { diff --git a/includes/parser/LinkHolderArray.php b/includes/parser/LinkHolderArray.php index f1a0b258..7794fae4 100644 --- a/includes/parser/LinkHolderArray.php +++ b/includes/parser/LinkHolderArray.php @@ -25,19 +25,27 @@ * @ingroup Parser */ class LinkHolderArray { - var $internals = array(), $interwikis = array(); - var $size = 0; - var $parent; + public $internals = array(); + public $interwikis = array(); + public $size = 0; + + /** + * @var Parser + */ + public $parent; protected $tempIdOffset; - function __construct( $parent ) { + /** + * @param Parser $parent + */ + public function __construct( $parent ) { $this->parent = $parent; } /** * Reduce memory usage to reduce the impact of circular references */ - function __destruct() { + public function __destruct() { foreach ( $this as $name => $value ) { unset( $this->$name ); } @@ -49,9 +57,9 @@ class LinkHolderArray { * serializing at present. * * Compact the titles, only serialize the text form. - * @return array - */ - function __sleep() { + * @return array + */ + public function __sleep() { foreach ( $this->internals as &$nsLinks ) { foreach ( $nsLinks as &$entry ) { unset( $entry['title'] ); @@ -71,7 +79,7 @@ class LinkHolderArray { /** * Recreate the Title objects */ - function __wakeup() { + public function __wakeup() { foreach ( $this->internals as &$nsLinks ) { foreach ( $nsLinks as &$entry ) { $entry['title'] = Title::newFromText( $entry['pdbk'] ); @@ -88,9 +96,9 @@ class LinkHolderArray { /** * Merge another LinkHolderArray into this one - * @param $other LinkHolderArray + * @param LinkHolderArray $other */ - function merge( $other ) { + public function merge( $other ) { foreach ( $other->internals as $ns => $entries ) { $this->size += count( $entries ); if ( !isset( $this->internals[$ns] ) ) { @@ -110,11 +118,11 @@ class LinkHolderArray { * converted for use in the destination link holder. The resulting array of * strings will be returned. * - * @param $other LinkHolderArray - * @param array $texts of strings - * @return Array + * @param LinkHolderArray $other + * @param array $texts Array of strings + * @return array */ - function mergeForeign( $other, $texts ) { + public function mergeForeign( $other, $texts ) { $this->tempIdOffset = $idOffset = $this->parent->nextLinkID(); $maxId = 0; @@ -144,6 +152,10 @@ class LinkHolderArray { return $texts; } + /** + * @param array $m + * @return string + */ protected function mergeForeignCallback( $m ) { return $m[1] . ( $m[2] + $this->tempIdOffset ) . $m[3]; } @@ -151,17 +163,18 @@ class LinkHolderArray { /** * Get a subset of the current LinkHolderArray which is sufficient to * interpret the given text. + * @param string $text * @return LinkHolderArray */ - function getSubArray( $text ) { + public function getSubArray( $text ) { $sub = new LinkHolderArray( $this->parent ); # Internal links $pos = 0; while ( $pos < strlen( $text ) ) { if ( !preg_match( '//', - $text, $m, PREG_OFFSET_CAPTURE, $pos ) ) - { + $text, $m, PREG_OFFSET_CAPTURE, $pos ) + ) { break; } $ns = $m[1][0]; @@ -187,7 +200,7 @@ class LinkHolderArray { * Returns true if the memory requirements of this object are getting large * @return bool */ - function isBig() { + public function isBig() { global $wgLinkHolderBatchSize; return $this->size > $wgLinkHolderBatchSize; } @@ -196,7 +209,7 @@ class LinkHolderArray { * Clear all stored link holders. * Make sure you don't have any text left using these link holders, before you call this */ - function clear() { + public function clear() { $this->internals = array(); $this->interwikis = array(); $this->size = 0; @@ -208,14 +221,14 @@ class LinkHolderArray { * parsing of interwiki links, and secondly to allow all existence checks and * article length checks (for stub links) to be bundled into a single query. * - * @param $nt Title - * @param $text String + * @param Title $nt + * @param string $text * @param array $query [optional] * @param string $trail [optional] * @param string $prefix [optional] * @return string */ - function makeHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) { + public function makeHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) { wfProfileIn( __METHOD__ ); if ( !is_object( $nt ) ) { # Fail gracefully @@ -251,15 +264,16 @@ class LinkHolderArray { } /** - * @todo FIXME: Update documentation. makeLinkObj() is deprecated. * Replace link placeholders with actual links, in the buffer - * Placeholders created in Skin::makeLinkObj() - * @return array of link CSS classes, indexed by PDBK. + * + * @param string $text + * @return array Array of link CSS classes, indexed by PDBK. */ - function replace( &$text ) { + public function replace( &$text ) { wfProfileIn( __METHOD__ ); - $colours = $this->replaceInternal( $text ); // FIXME: replaceInternal doesn't return a value + /** @todo FIXME: replaceInternal doesn't return a value */ + $colours = $this->replaceInternal( $text ); $this->replaceInterwiki( $text ); wfProfileOut( __METHOD__ ); @@ -268,6 +282,7 @@ class LinkHolderArray { /** * Replace internal links + * @param string $text */ protected function replaceInternal( &$text ) { if ( !$this->internals ) { @@ -275,93 +290,99 @@ class LinkHolderArray { } wfProfileIn( __METHOD__ ); - global $wgContLang; + global $wgContLang, $wgContentHandlerUseDB; $colours = array(); $linkCache = LinkCache::singleton(); $output = $this->parent->getOutput(); - if ( $linkCache->useDatabase() ) { - wfProfileIn( __METHOD__ . '-check' ); - $dbr = wfGetDB( DB_SLAVE ); - $threshold = $this->parent->getOptions()->getStubThreshold(); + wfProfileIn( __METHOD__ . '-check' ); + $dbr = wfGetDB( DB_SLAVE ); + $threshold = $this->parent->getOptions()->getStubThreshold(); - # Sort by namespace - ksort( $this->internals ); + # Sort by namespace + ksort( $this->internals ); - $linkcolour_ids = array(); + $linkcolour_ids = array(); - # Generate query - $queries = array(); - foreach ( $this->internals as $ns => $entries ) { - foreach ( $entries as $entry ) { - $title = $entry['title']; - $pdbk = $entry['pdbk']; + # Generate query + $queries = array(); + foreach ( $this->internals as $ns => $entries ) { + foreach ( $entries as $entry ) { + /** @var Title $title */ + $title = $entry['title']; + $pdbk = $entry['pdbk']; - # Skip invalid entries. - # Result will be ugly, but prevents crash. - if ( is_null( $title ) ) { - continue; - } + # Skip invalid entries. + # Result will be ugly, but prevents crash. + if ( is_null( $title ) ) { + continue; + } - # Check if it's a static known link, e.g. interwiki - if ( $title->isAlwaysKnown() ) { - $colours[$pdbk] = ''; - } elseif ( $ns == NS_SPECIAL ) { - $colours[$pdbk] = 'new'; - } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) { - $colours[$pdbk] = Linker::getLinkColour( $title, $threshold ); - $output->addLink( $title, $id ); - $linkcolour_ids[$id] = $pdbk; - } elseif ( $linkCache->isBadLink( $pdbk ) ) { - $colours[$pdbk] = 'new'; - } else { - # Not in the link cache, add it to the query - $queries[$ns][] = $title->getDBkey(); - } + # Check if it's a static known link, e.g. interwiki + if ( $title->isAlwaysKnown() ) { + $colours[$pdbk] = ''; + } elseif ( $ns == NS_SPECIAL ) { + $colours[$pdbk] = 'new'; + } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) { + $colours[$pdbk] = Linker::getLinkColour( $title, $threshold ); + $output->addLink( $title, $id ); + $linkcolour_ids[$id] = $pdbk; + } elseif ( $linkCache->isBadLink( $pdbk ) ) { + $colours[$pdbk] = 'new'; + } else { + # Not in the link cache, add it to the query + $queries[$ns][] = $title->getDBkey(); } } - if ( $queries ) { - $where = array(); - foreach ( $queries as $ns => $pages ) { - $where[] = $dbr->makeList( - array( - 'page_namespace' => $ns, - 'page_title' => $pages, - ), - LIST_AND - ); - } - - $res = $dbr->select( - 'page', - array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect', 'page_len', 'page_latest' ), - $dbr->makeList( $where, LIST_OR ), - __METHOD__ + } + if ( $queries ) { + $where = array(); + foreach ( $queries as $ns => $pages ) { + $where[] = $dbr->makeList( + array( + 'page_namespace' => $ns, + 'page_title' => array_unique( $pages ), + ), + LIST_AND ); + } - # Fetch data and form into an associative array - # non-existent = broken - foreach ( $res as $s ) { - $title = Title::makeTitle( $s->page_namespace, $s->page_title ); - $pdbk = $title->getPrefixedDBkey(); - $linkCache->addGoodLinkObjFromRow( $title, $s ); - $output->addLink( $title, $s->page_id ); - # @todo FIXME: Convoluted data flow - # The redirect status and length is passed to getLinkColour via the LinkCache - # Use formal parameters instead - $colours[$pdbk] = Linker::getLinkColour( $title, $threshold ); - //add id to the extension todolist - $linkcolour_ids[$s->page_id] = $pdbk; - } - unset( $res ); + $fields = array( 'page_id', 'page_namespace', 'page_title', + 'page_is_redirect', 'page_len', 'page_latest' ); + + if ( $wgContentHandlerUseDB ) { + $fields[] = 'page_content_model'; } - if ( count( $linkcolour_ids ) ) { - //pass an array of page_ids to an extension - wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) ); + + $res = $dbr->select( + 'page', + $fields, + $dbr->makeList( $where, LIST_OR ), + __METHOD__ + ); + + # Fetch data and form into an associative array + # non-existent = broken + foreach ( $res as $s ) { + $title = Title::makeTitle( $s->page_namespace, $s->page_title ); + $pdbk = $title->getPrefixedDBkey(); + $linkCache->addGoodLinkObjFromRow( $title, $s ); + $output->addLink( $title, $s->page_id ); + # @todo FIXME: Convoluted data flow + # The redirect status and length is passed to getLinkColour via the LinkCache + # Use formal parameters instead + $colours[$pdbk] = Linker::getLinkColour( $title, $threshold ); + //add id to the extension todolist + $linkcolour_ids[$s->page_id] = $pdbk; } - wfProfileOut( __METHOD__ . '-check' ); + unset( $res ); } + if ( count( $linkcolour_ids ) ) { + //pass an array of page_ids to an extension + wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) ); + } + wfProfileOut( __METHOD__ . '-check' ); # Do a second query for different language variants of links and categories if ( $wgContLang->hasVariants() ) { @@ -421,6 +442,7 @@ class LinkHolderArray { /** * Replace interwiki links + * @param string $text */ protected function replaceInterwiki( &$text ) { if ( empty( $this->interwikis ) ) { @@ -446,9 +468,10 @@ class LinkHolderArray { /** * Modify $this->internals and $colours according to language variant linking rules + * @param array $colours */ protected function doVariants( &$colours ) { - global $wgContLang; + global $wgContLang, $wgContentHandlerUseDB; $linkBatch = new LinkBatch(); $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders) $output = $this->parent->getOutput(); @@ -486,6 +509,7 @@ class LinkHolderArray { // Then add variants of links to link batch $parentTitle = $this->parent->getTitle(); foreach ( $titlesAttrs as $i => $attrs ) { + /** @var Title $title */ list( $index, $title ) = $attrs; $ns = $title->getNamespace(); $text = $title->getText(); @@ -504,7 +528,7 @@ class LinkHolderArray { // Self-link checking for mixed/different variant titles. At this point, we // already know the exact title does not exist, so the link cannot be to a // variant of the current title that exists as a separate page. - if ( $variantTitle->equals( $parentTitle ) && $title->getFragment() === '' ) { + if ( $variantTitle->equals( $parentTitle ) && !$title->hasFragment() ) { $this->internals[$ns][$index]['selflink'] = true; continue 2; } @@ -536,8 +560,15 @@ class LinkHolderArray { if ( !$linkBatch->isEmpty() ) { // construct query $dbr = wfGetDB( DB_SLAVE ); + $fields = array( 'page_id', 'page_namespace', 'page_title', + 'page_is_redirect', 'page_len', 'page_latest' ); + + if ( $wgContentHandlerUseDB ) { + $fields[] = 'page_content_model'; + } + $varRes = $dbr->select( 'page', - array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect', 'page_len', 'page_latest' ), + $fields, $linkBatch->constructSet( 'page', $dbr ), __METHOD__ ); @@ -609,10 +640,10 @@ class LinkHolderArray { * Replace link placeholders with plain text of links * (not HTML-formatted). * - * @param $text String - * @return String + * @param string $text + * @return string */ - function replaceText( $text ) { + public function replaceText( $text ) { wfProfileIn( __METHOD__ ); $text = preg_replace_callback( @@ -627,11 +658,11 @@ class LinkHolderArray { /** * Callback for replaceText() * - * @param $matches Array + * @param array $matches * @return string * @private */ - function replaceTextCallback( $matches ) { + public function replaceTextCallback( $matches ) { $type = $matches[1]; $key = $matches[2]; if ( $type == 'LINK' ) { diff --git a/includes/parser/MWTidy.php b/includes/parser/MWTidy.php new file mode 100644 index 00000000..b310862f --- /dev/null +++ b/includes/parser/MWTidy.php @@ -0,0 +1,291 @@ +mTokens = null; + $this->mUniqPrefix = null; + } + + /** + * @param string $text + * @return string + */ + public function getWrapped( $text ) { + $this->mTokens = new ReplacementArray; + $this->mUniqPrefix = "\x7fUNIQ" . + dechex( mt_rand( 0, 0x7fffffff ) ) . dechex( mt_rand( 0, 0x7fffffff ) ); + $this->mMarkerIndex = 0; + + // Replace elements with placeholders + $wrappedtext = preg_replace_callback( ParserOutput::EDITSECTION_REGEX, + array( &$this, 'replaceCallback' ), $text ); + // ...and markers + $wrappedtext = preg_replace_callback( '/\<\\/?mw:toc\>/', + array( &$this, 'replaceCallback' ), $wrappedtext ); + // ... and tags + $wrappedtext = preg_replace_callback( '/\/s', + array( &$this, 'replaceCallback' ), $wrappedtext ); + // Modify inline Microdata and elements so they say and so + // we can trick Tidy into not stripping them out by including them in tidy's new-empty-tags config + $wrappedtext = preg_replace( '!<(link|meta)([^>]*?)(/{0,1}>)!', '' . + 'test' . $wrappedtext . ''; + + return $wrappedtext; + } + + /** + * @param array $m + * + * @return string + */ + public function replaceCallback( $m ) { + $marker = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}" . Parser::MARKER_SUFFIX; + $this->mMarkerIndex++; + $this->mTokens->setPair( $marker, $m[0] ); + return $marker; + } + + /** + * @param string $text + * @return string + */ + public function postprocess( $text ) { + // Revert back to <{link,meta}> + $text = preg_replace( '!]*?)(/{0,1}>)!', '<$1$2$3', $text ); + + // Restore the contents of placeholder tokens + $text = $this->mTokens->replace( $text ); + + return $text; + } + +} + +/** + * Class to interact with HTML tidy + * + * Either the external tidy program or the in-process tidy extension + * will be used depending on availability. Override the default + * $wgTidyInternal setting to disable the internal if it's not working. + * + * @ingroup Parser + */ +class MWTidy { + /** + * Interface with html tidy, used if $wgUseTidy = true. + * If tidy isn't able to correct the markup, the original will be + * returned in all its glory with a warning comment appended. + * + * @param string $text Hideous HTML input + * @return string Corrected HTML output + */ + public static function tidy( $text ) { + global $wgTidyInternal; + + $wrapper = new MWTidyWrapper; + $wrappedtext = $wrapper->getWrapped( $text ); + + $retVal = null; + if ( $wgTidyInternal ) { + $correctedtext = self::execInternalTidy( $wrappedtext, false, $retVal ); + } else { + $correctedtext = self::execExternalTidy( $wrappedtext, false, $retVal ); + } + + if ( $retVal < 0 ) { + wfDebug( "Possible tidy configuration error!\n" ); + return $text . "\n\n"; + } elseif ( is_null( $correctedtext ) ) { + wfDebug( "Tidy error detected!\n" ); + return $text . "\n\n"; + } + + $correctedtext = $wrapper->postprocess( $correctedtext ); // restore any hidden tokens + + return $correctedtext; + } + + /** + * Check HTML for errors, used if $wgValidateAllHtml = true. + * + * @param string $text + * @param string &$errorStr Return the error string + * @return bool Whether the HTML is valid + */ + public static function checkErrors( $text, &$errorStr = null ) { + global $wgTidyInternal; + + $retval = 0; + if ( $wgTidyInternal ) { + $errorStr = self::execInternalTidy( $text, true, $retval ); + } else { + $errorStr = self::execExternalTidy( $text, true, $retval ); + } + + return ( $retval < 0 && $errorStr == '' ) || $retval == 0; + } + + /** + * Spawn an external HTML tidy process and get corrected markup back from it. + * Also called in OutputHandler.php for full page validation + * + * @param string $text HTML to check + * @param bool $stderr Whether to read result from STDERR rather than STDOUT + * @param int &$retval Exit code (-1 on internal error) + * @return string|null + */ + private static function execExternalTidy( $text, $stderr = false, &$retval = null ) { + global $wgTidyConf, $wgTidyBin, $wgTidyOpts; + wfProfileIn( __METHOD__ ); + + $cleansource = ''; + $opts = ' -utf8'; + + if ( $stderr ) { + $descriptorspec = array( + 0 => array( 'pipe', 'r' ), + 1 => array( 'file', wfGetNull(), 'a' ), + 2 => array( 'pipe', 'w' ) + ); + } else { + $descriptorspec = array( + 0 => array( 'pipe', 'r' ), + 1 => array( 'pipe', 'w' ), + 2 => array( 'file', wfGetNull(), 'a' ) + ); + } + + $readpipe = $stderr ? 2 : 1; + $pipes = array(); + + $process = proc_open( + "$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes ); + + //NOTE: At least on linux, the process will be created even if tidy is not installed. + // This means that missing tidy will be treated as a validation failure. + + if ( is_resource( $process ) ) { + // Theoretically, this style of communication could cause a deadlock + // here. If the stdout buffer fills up, then writes to stdin could + // block. This doesn't appear to happen with tidy, because tidy only + // writes to stdout after it's finished reading from stdin. Search + // for tidyParseStdin and tidySaveStdout in console/tidy.c + fwrite( $pipes[0], $text ); + fclose( $pipes[0] ); + while ( !feof( $pipes[$readpipe] ) ) { + $cleansource .= fgets( $pipes[$readpipe], 1024 ); + } + fclose( $pipes[$readpipe] ); + $retval = proc_close( $process ); + } else { + wfWarn( "Unable to start external tidy process" ); + $retval = -1; + } + + if ( !$stderr && $cleansource == '' && $text != '' ) { + // Some kind of error happened, so we couldn't get the corrected text. + // Just give up; we'll use the source text and append a warning. + $cleansource = null; + } + + wfProfileOut( __METHOD__ ); + return $cleansource; + } + + /** + * Use the HTML tidy extension to use the tidy library in-process, + * saving the overhead of spawning a new process. + * + * @param string $text HTML to check + * @param bool $stderr Whether to read result from error status instead of output + * @param int &$retval Exit code (-1 on internal error) + * @return string|null + */ + private static function execInternalTidy( $text, $stderr = false, &$retval = null ) { + global $wgTidyConf, $wgDebugTidy; + wfProfileIn( __METHOD__ ); + + if ( !class_exists( 'tidy' ) ) { + wfWarn( "Unable to load internal tidy class." ); + $retval = -1; + + wfProfileOut( __METHOD__ ); + return null; + } + + $tidy = new tidy; + $tidy->parseString( $text, $wgTidyConf, 'utf8' ); + + if ( $stderr ) { + $retval = $tidy->getStatus(); + + wfProfileOut( __METHOD__ ); + return $tidy->errorBuffer; + } + + $tidy->cleanRepair(); + $retval = $tidy->getStatus(); + if ( $retval == 2 ) { + // 2 is magic number for fatal error + // http://www.php.net/manual/en/function.tidy-get-status.php + $cleansource = null; + } else { + $cleansource = tidy_get_output( $tidy ); + if ( $wgDebugTidy && $retval > 0 ) { + $cleansource .= "', '-->', $tidy->errorBuffer ) . + "\n-->"; + } + } + + wfProfileOut( __METHOD__ ); + return $cleansource; + } +} diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index 1f14223d..84bb2243 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -120,62 +120,61 @@ class Parser { const TOC_END = ''; # Persistent: - var $mTagHooks = array(); - var $mTransparentTagHooks = array(); - var $mFunctionHooks = array(); - var $mFunctionSynonyms = array( 0 => array(), 1 => array() ); - var $mFunctionTagHooks = array(); - var $mStripList = array(); - var $mDefaultStripList = array(); - var $mVarCache = array(); - var $mImageParams = array(); - var $mImageParamsMagicArray = array(); - var $mMarkerIndex = 0; - var $mFirstCall = true; + public $mTagHooks = array(); + public $mTransparentTagHooks = array(); + public $mFunctionHooks = array(); + public $mFunctionSynonyms = array( 0 => array(), 1 => array() ); + public $mFunctionTagHooks = array(); + public $mStripList = array(); + public $mDefaultStripList = array(); + public $mVarCache = array(); + public $mImageParams = array(); + public $mImageParamsMagicArray = array(); + public $mMarkerIndex = 0; + public $mFirstCall = true; # Initialised by initialiseVariables() /** * @var MagicWordArray */ - var $mVariables; + public $mVariables; /** * @var MagicWordArray */ - var $mSubstWords; - var $mConf, $mPreprocessor, $mExtLinkBracketedRegex, $mUrlProtocols; # Initialised in constructor + public $mSubstWords; + public $mConf, $mPreprocessor, $mExtLinkBracketedRegex, $mUrlProtocols; # Initialised in constructor # Cleared with clearState(): /** * @var ParserOutput */ - var $mOutput; - var $mAutonumber, $mDTopen; + public $mOutput; + public $mAutonumber, $mDTopen; /** * @var StripState */ - var $mStripState; + public $mStripState; - var $mIncludeCount, $mArgStack, $mLastSection, $mInPre; + public $mIncludeCount, $mArgStack, $mLastSection, $mInPre; /** * @var LinkHolderArray */ - var $mLinkHolders; + public $mLinkHolders; - var $mLinkID; - var $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth; - var $mDefaultSort; - var $mTplExpandCache; # empty-frame expansion cache - var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores; - var $mExpensiveFunctionCount; # number of expensive parser function calls - var $mShowToc, $mForceTocPosition; + public $mLinkID; + public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth; + public $mDefaultSort; + public $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores; + public $mExpensiveFunctionCount; # number of expensive parser function calls + public $mShowToc, $mForceTocPosition; /** * @var User */ - var $mUser; # User object; only used when doing pre-save transform + public $mUser; # User object; only used when doing pre-save transform # Temporary # These are variables reset at least once per parse regardless of $clearState @@ -183,38 +182,42 @@ class Parser { /** * @var ParserOptions */ - var $mOptions; + public $mOptions; /** * @var Title */ - var $mTitle; # Title context, used for self-link rendering and similar things - var $mOutputType; # Output type, one of the OT_xxx constants - var $ot; # Shortcut alias, see setOutputType() - var $mRevisionObject; # The revision object of the specified revision ID - var $mRevisionId; # ID to display in {{REVISIONID}} tags - var $mRevisionTimestamp; # The timestamp of the specified revision ID - var $mRevisionUser; # User to display in {{REVISIONUSER}} tag - var $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable - var $mRevIdForTs; # The revision ID which was used to fetch the timestamp - var $mInputSize = false; # For {{PAGESIZE}} on current page. + public $mTitle; # Title context, used for self-link rendering and similar things + public $mOutputType; # Output type, one of the OT_xxx constants + public $ot; # Shortcut alias, see setOutputType() + public $mRevisionObject; # The revision object of the specified revision ID + public $mRevisionId; # ID to display in {{REVISIONID}} tags + public $mRevisionTimestamp; # The timestamp of the specified revision ID + public $mRevisionUser; # User to display in {{REVISIONUSER}} tag + public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable + public $mRevIdForTs; # The revision ID which was used to fetch the timestamp + public $mInputSize = false; # For {{PAGESIZE}} on current page. /** * @var string */ - var $mUniqPrefix; + public $mUniqPrefix; /** - * @var Array with the language name of each language link (i.e. the + * @var array Array with the language name of each language link (i.e. the * interwiki prefix) in the key, value arbitrary. Used to avoid sending * duplicate language links to the ParserOutput. */ - var $mLangLinkLanguages; + public $mLangLinkLanguages; /** - * Constructor - * - * @param $conf array + * @var bool Recursive call protection. + * This variable should be treated as if it were private. + */ + public $mInParse = false; + + /** + * @param array $conf */ public function __construct( $conf = array() ) { $this->mConf = $conf; @@ -241,7 +244,7 @@ class Parser { /** * Reduce memory usage to reduce the impact of circular references */ - function __destruct() { + public function __destruct() { if ( isset( $this->mLinkHolders ) ) { unset( $this->mLinkHolders ); } @@ -253,14 +256,15 @@ class Parser { /** * Allow extensions to clean up when the parser is cloned */ - function __clone() { + public function __clone() { + $this->mInParse = false; wfRunHooks( 'ParserCloned', array( $this ) ); } /** * Do various kinds of initialisation on the first call of the parser */ - function firstCallInit() { + public function firstCallInit() { if ( !$this->mFirstCall ) { return; } @@ -281,7 +285,7 @@ class Parser { * * @private */ - function clearState() { + public function clearState() { wfProfileIn( __METHOD__ ); if ( $this->mFirstCall ) { $this->firstCallInit(); @@ -316,7 +320,7 @@ class Parser { $this->mStripState = new StripState( $this->mUniqPrefix ); # Clear these on every parse, bug 4549 - $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array(); + $this->mTplRedirCache = $this->mTplDomCache = array(); $this->mShowToc = true; $this->mForceTocPosition = false; @@ -345,15 +349,17 @@ class Parser { * Convert wikitext to HTML * Do not call this function recursively. * - * @param string $text text we want to parse - * @param $title Title object - * @param $options ParserOptions - * @param $linestart boolean - * @param $clearState boolean - * @param int $revid number to pass in {{REVISIONID}} - * @return ParserOutput a ParserOutput - */ - public function parse( $text, Title $title, ParserOptions $options, $linestart = true, $clearState = true, $revid = null ) { + * @param string $text Text we want to parse + * @param Title $title + * @param ParserOptions $options + * @param bool $linestart + * @param bool $clearState + * @param int $revid Number to pass in {{REVISIONID}} + * @return ParserOutput A ParserOutput + */ + public function parse( $text, Title $title, ParserOptions $options, + $linestart = true, $clearState = true, $revid = null + ) { /** * First pass--just handle sections, pass the rest off * to internalParse() which does all the real work. @@ -364,6 +370,10 @@ class Parser { wfProfileIn( __METHOD__ ); wfProfileIn( $fname ); + if ( $clearState ) { + $magicScopeVariable = $this->lock(); + } + $this->startParse( $title, $options, self::OT_HTML, $clearState ); $this->mInputSize = strlen( $text ); @@ -420,8 +430,8 @@ class Parser { * d) it is an interface message (which is in the user language) */ if ( !( $options->getDisableContentConversion() - || isset( $this->mDoubleUnderscores['nocontentconvert'] ) ) ) - { + || isset( $this->mDoubleUnderscores['nocontentconvert'] ) ) + ) { if ( !$this->mOptions->getInterfaceMessage() ) { # The position of the convert() call should not be changed. it # assumes that the links are all replaced and the only thing left @@ -438,10 +448,10 @@ class Parser { * automatic link conversion. */ if ( !( $options->getDisableTitleConversion() - || isset( $this->mDoubleUnderscores['nocontentconvert'] ) - || isset( $this->mDoubleUnderscores['notitleconvert'] ) - || $this->mOutput->getDisplayTitle() !== false ) ) - { + || isset( $this->mDoubleUnderscores['nocontentconvert'] ) + || isset( $this->mDoubleUnderscores['notitleconvert'] ) + || $this->mOutput->getDisplayTitle() !== false ) + ) { $convruletitle = $this->getConverterLanguage()->getConvRuleTitle(); if ( $convruletitle ) { $this->mOutput->setTitleText( $convruletitle ); @@ -541,7 +551,7 @@ class Parser { } foreach ( $this->mOutput->getLimitReportData() as $key => $value ) { if ( wfRunHooks( 'ParserLimitReportFormat', - array( $key, $value, &$limitReport, false, false ) + array( $key, &$value, &$limitReport, false, false ) ) ) { $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false ); $valueMsg = wfMessage( array( "$key-value-text", "$key-value" ) ) @@ -590,12 +600,12 @@ class Parser { * * If $frame is not provided, then template variables (e.g., {{{1}}}) within $text are not expanded * - * @param string $text text extension wants to have parsed - * @param $frame PPFrame: The frame to use for expanding any template variables + * @param string $text Text extension wants to have parsed + * @param bool|PPFrame $frame The frame to use for expanding any template variables * * @return string */ - function recursiveTagParse( $text, $frame = false ) { + public function recursiveTagParse( $text, $frame = false ) { wfProfileIn( __METHOD__ ); wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); @@ -607,17 +617,24 @@ class Parser { /** * Expand templates and variables in the text, producing valid, static wikitext. * Also removes comments. + * Do not call this function recursively. + * @param string $text + * @param Title $title + * @param ParserOptions $options + * @param int|null $revid + * @param bool|PPFrame $frame * @return mixed|string */ - function preprocess( $text, Title $title = null, ParserOptions $options, $revid = null ) { + public function preprocess( $text, Title $title = null, ParserOptions $options, $revid = null, $frame = false ) { wfProfileIn( __METHOD__ ); + $magicScopeVariable = $this->lock(); $this->startParse( $title, $options, self::OT_PREPROCESS, true ); if ( $revid !== null ) { $this->mRevisionId = $revid; } wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); - $text = $this->replaceVariables( $text ); + $text = $this->replaceVariables( $text, $frame ); $text = $this->mStripState->unstripBoth( $text ); wfProfileOut( __METHOD__ ); return $text; @@ -627,9 +644,9 @@ class Parser { * Recursive parser entry point that can be called from an extension tag * hook. * - * @param string $text text to be expanded - * @param $frame PPFrame: The frame to use for expanding any template variables - * @return String + * @param string $text Text to be expanded + * @param bool|PPFrame $frame The frame to use for expanding any template variables + * @return string * @since 1.19 */ public function recursivePreprocess( $text, $frame = false ) { @@ -647,13 +664,18 @@ class Parser { * transclusion, comments, templates, arguments, tags hooks and parser * functions are untouched. * - * @param $text String - * @param $title Title - * @param $options ParserOptions - * @return String + * @param string $text + * @param Title $title + * @param ParserOptions $options + * @param array $params + * @return string */ - public function getPreloadText( $text, Title $title, ParserOptions $options ) { + public function getPreloadText( $text, Title $title, ParserOptions $options, $params = array() ) { + $msg = new RawMessage( $text ); + $text = $msg->params( $params )->plain(); + # Parser (re)initialisation + $magicScopeVariable = $this->lock(); $this->startParse( $title, $options, self::OT_PLAIN, true ); $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES; @@ -676,16 +698,16 @@ class Parser { * Set the current user. * Should only be used when doing pre-save transform. * - * @param $user Mixed: User object or null (to reset) + * @param User|null $user User object or null (to reset) */ - function setUser( $user ) { + public function setUser( $user ) { $this->mUser = $user; } /** * Accessor for mUniqPrefix. * - * @return String + * @return string */ public function uniqPrefix() { if ( !isset( $this->mUniqPrefix ) ) { @@ -703,14 +725,14 @@ class Parser { /** * Set the context title * - * @param $t Title + * @param Title $t */ - function setTitle( $t ) { - if ( !$t || $t instanceof FakeTitle ) { + public function setTitle( $t ) { + if ( !$t ) { $t = Title::newFromText( 'NO TITLE' ); } - if ( strval( $t->getFragment() ) !== '' ) { + if ( $t->hasFragment() ) { # Strip the fragment to avoid various odd effects $this->mTitle = clone $t; $this->mTitle->setFragment( '' ); @@ -722,28 +744,28 @@ class Parser { /** * Accessor for the Title object * - * @return Title object + * @return Title */ - function getTitle() { + public function getTitle() { return $this->mTitle; } /** * Accessor/mutator for the Title object * - * @param $x Title object or null to just get the current one - * @return Title object + * @param Title $x Title object or null to just get the current one + * @return Title */ - function Title( $x = null ) { + public function Title( $x = null ) { return wfSetVar( $this->mTitle, $x ); } /** * Set the output type * - * @param $ot Integer: new value + * @param int $ot New value */ - function setOutputType( $ot ) { + public function setOutputType( $ot ) { $this->mOutputType = $ot; # Shortcut alias $this->ot = array( @@ -758,51 +780,51 @@ class Parser { * Accessor/mutator for the output type * * @param int|null $x New value or null to just get the current one - * @return Integer + * @return int */ - function OutputType( $x = null ) { + public function OutputType( $x = null ) { return wfSetVar( $this->mOutputType, $x ); } /** * Get the ParserOutput object * - * @return ParserOutput object + * @return ParserOutput */ - function getOutput() { + public function getOutput() { return $this->mOutput; } /** * Get the ParserOptions object * - * @return ParserOptions object + * @return ParserOptions */ - function getOptions() { + public function getOptions() { return $this->mOptions; } /** * Accessor/mutator for the ParserOptions object * - * @param $x ParserOptions New value or null to just get the current one + * @param ParserOptions $x New value or null to just get the current one * @return ParserOptions Current ParserOptions object */ - function Options( $x = null ) { + public function Options( $x = null ) { return wfSetVar( $this->mOptions, $x ); } /** * @return int */ - function nextLinkID() { + public function nextLinkID() { return $this->mLinkID++; } /** - * @param $id int + * @param int $id */ - function setLinkID( $id ) { + public function setLinkID( $id ) { $this->mLinkID = $id; } @@ -810,7 +832,7 @@ class Parser { * Get a language object for use in parser functions such as {{FORMATNUM:}} * @return Language */ - function getFunctionLang() { + public function getFunctionLang() { return $this->getTargetLanguage(); } @@ -821,7 +843,7 @@ class Parser { * @since 1.19 * * @throws MWException - * @return Language|null + * @return Language */ public function getTargetLanguage() { $target = $this->mOptions->getTargetLanguage(); @@ -839,8 +861,9 @@ class Parser { /** * Get the language object for language conversion + * @return Language|null */ - function getConverterLanguage() { + public function getConverterLanguage() { return $this->getTargetLanguage(); } @@ -848,9 +871,9 @@ class Parser { * Get a User object either from $this->mUser, if set, or from the * ParserOptions object otherwise * - * @return User object + * @return User */ - function getUser() { + public function getUser() { if ( !is_null( $this->mUser ) ) { return $this->mUser; } @@ -860,9 +883,9 @@ class Parser { /** * Get a preprocessor object * - * @return Preprocessor instance + * @return Preprocessor */ - function getPreprocessor() { + public function getPreprocessor() { if ( !isset( $this->mPreprocessor ) ) { $class = $this->mPreprocessorClass; $this->mPreprocessor = new $class( $this ); @@ -884,11 +907,11 @@ class Parser { * 'tag content' ) ) * @endcode * - * @param array $elements list of element names. Comments are always extracted. + * @param array $elements List of element names. Comments are always extracted. * @param string $text Source text string. * @param array $matches Out parameter, Array: extracted tags - * @param $uniq_prefix string - * @return String: stripped text + * @param string $uniq_prefix + * @return string Stripped text */ public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = '' ) { static $n = 1; @@ -957,7 +980,7 @@ class Parser { * * @return array */ - function getStripList() { + public function getStripList() { return $this->mStripList; } @@ -966,11 +989,11 @@ class Parser { * Returns the unique tag which must be inserted into the stripped text * The tag will be replaced with the original text in unstrip() * - * @param $text string + * @param string $text * * @return string */ - function insertStripItem( $text ) { + public function insertStripItem( $text ) { $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX; $this->mMarkerIndex++; $this->mStripState->addGeneral( $rnd, $text ); @@ -981,9 +1004,10 @@ class Parser { * parse the wiki syntax used to render tables * * @private + * @param string $text * @return string */ - function doTableStuff( $text ) { + public function doTableStuff( $text ) { wfProfileIn( __METHOD__ ); $lines = StringUtils::explode( "\n", $text ); @@ -1068,7 +1092,10 @@ class Parser { array_push( $tr_history, false ); array_push( $td_history, false ); array_push( $last_tag_history, '' ); - } elseif ( $first_character === '|' || $first_character === '!' || substr( $line, 0, 2 ) === '|+' ) { + } elseif ( $first_character === '|' + || $first_character === '!' + || substr( $line, 0, 2 ) === '|+' + ) { # This might be cell elements, td, th or captions if ( substr( $line, 0, 2 ) === '|+' ) { $first_character = '+'; @@ -1179,13 +1206,13 @@ class Parser { * * @private * - * @param $text string - * @param $isMain bool - * @param $frame bool + * @param string $text + * @param bool $isMain + * @param bool $frame * * @return string */ - function internalParse( $text, $isMain = true, $frame = false ) { + public function internalParse( $text, $isMain = true, $frame = false ) { wfProfileIn( __METHOD__ ); $origText = $text; @@ -1213,7 +1240,12 @@ class Parser { } wfRunHooks( 'InternalParseBeforeSanitize', array( &$this, &$text, &$this->mStripState ) ); - $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) ); + $text = Sanitizer::removeHTMLtags( + $text, + array( &$this, 'attributeStripCallback' ), + false, + array_keys( $this->mTransparentTagHooks ) + ); wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) ); # Tables need to come after variable replacement for things to work @@ -1249,11 +1281,11 @@ class Parser { * DML * @private * - * @param $text string + * @param string $text * * @return string */ - function doMagicLinks( $text ) { + public function doMagicLinks( $text ) { wfProfileIn( __METHOD__ ); $prots = wfUrlProtocolsWithoutProtRel(); $urlChar = self::EXT_LINK_URL_CLASS; @@ -1275,10 +1307,10 @@ class Parser { /** * @throws MWException - * @param $m array + * @param array $m * @return HTML|string */ - function magicLinkCallback( $m ) { + public function magicLinkCallback( $m ) { if ( isset( $m[1] ) && $m[1] !== '' ) { # Skip anchor return $m[0]; @@ -1293,19 +1325,19 @@ class Parser { if ( substr( $m[0], 0, 3 ) === 'RFC' ) { $keyword = 'RFC'; $urlmsg = 'rfcurl'; - $CssClass = 'mw-magiclink-rfc'; + $cssClass = 'mw-magiclink-rfc'; $id = $m[4]; } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) { $keyword = 'PMID'; $urlmsg = 'pubmedurl'; - $CssClass = 'mw-magiclink-pmid'; + $cssClass = 'mw-magiclink-pmid'; $id = $m[4]; } else { throw new MWException( __METHOD__ . ': unrecognised match type "' . substr( $m[0], 0, 20 ) . '"' ); } $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text(); - return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $CssClass ); + return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass ); } elseif ( isset( $m[5] ) && $m[5] !== '' ) { # ISBN $isbn = $m[5]; @@ -1326,12 +1358,12 @@ class Parser { /** * Make a free external link, given a user-supplied URL * - * @param $url string + * @param string $url * * @return string HTML * @private */ - function makeFreeExternalLink( $url ) { + public function makeFreeExternalLink( $url ) { wfProfileIn( __METHOD__ ); $trail = ''; @@ -1370,7 +1402,7 @@ class Parser { $this->getExternalLinkAttribs( $url ) ); # Register it in the output object... # Replace unnecessary URL escape codes with their equivalent characters - $pasteurized = self::replaceUnusualEscapes( $url ); + $pasteurized = self::normalizeLinkUrl( $url ); $this->mOutput->addExternalLink( $pasteurized ); } wfProfileOut( __METHOD__ ); @@ -1382,11 +1414,11 @@ class Parser { * * @private * - * @param $text string + * @param string $text * * @return string */ - function doHeadings( $text ) { + public function doHeadings( $text ) { wfProfileIn( __METHOD__ ); for ( $i = 6; $i >= 1; --$i ) { $h = str_repeat( '=', $i ); @@ -1400,11 +1432,11 @@ class Parser { * Replace single quotes with HTML markup * @private * - * @param $text string + * @param string $text * - * @return string the altered text + * @return string The altered text */ - function doAllQuotes( $text ) { + public function doAllQuotes( $text ) { wfProfileIn( __METHOD__ ); $outtext = ''; $lines = StringUtils::explode( "\n", $text ); @@ -1419,7 +1451,7 @@ class Parser { /** * Helper function for doAllQuotes() * - * @param $text string + * @param string $text * * @return string */ @@ -1608,18 +1640,19 @@ class Parser { * * @private * - * @param $text string + * @param string $text * * @throws MWException * @return string */ - function replaceExternalLinks( $text ) { + public function replaceExternalLinks( $text ) { wfProfileIn( __METHOD__ ); $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); if ( $bits === false ) { wfProfileOut( __METHOD__ ); - throw new MWException( "PCRE needs to be compiled with --enable-unicode-properties in order for MediaWiki to function" ); + throw new MWException( "PCRE needs to be compiled with " + . "--enable-unicode-properties in order for MediaWiki to function" ); } $s = array_shift( $bits ); @@ -1677,43 +1710,45 @@ class Parser { # Register link in the output object. # Replace unnecessary URL escape codes with the referenced character # This prevents spammers from hiding links from the filters - $pasteurized = self::replaceUnusualEscapes( $url ); + $pasteurized = self::normalizeLinkUrl( $url ); $this->mOutput->addExternalLink( $pasteurized ); } wfProfileOut( __METHOD__ ); return $s; } + /** * Get the rel attribute for a particular external link. * * @since 1.21 - * @param string|bool $url optional URL, to extract the domain from for rel => + * @param string|bool $url Optional URL, to extract the domain from for rel => * nofollow if appropriate - * @param $title Title optional Title, for wgNoFollowNsExceptions lookups - * @return string|null rel attribute for $url + * @param Title $title Optional Title, for wgNoFollowNsExceptions lookups + * @return string|null Rel attribute for $url */ public static function getExternalLinkRel( $url = false, $title = null ) { global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions; $ns = $title ? $title->getNamespace() : false; - if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions ) && - !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions ) ) - { + if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions ) + && !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions ) + ) { return 'nofollow'; } return null; } + /** * Get an associative array of additional HTML attributes appropriate for a * particular external link. This currently may include rel => nofollow * (depending on configuration, namespace, and the URL's domain) and/or a * target attribute (depending on configuration). * - * @param string|bool $url optional URL, to extract the domain from for rel => + * @param string|bool $url Optional URL, to extract the domain from for rel => * nofollow if appropriate - * @return Array associative array of HTML attributes + * @return array Associative array of HTML attributes */ - function getExternalLinkAttribs( $url = false ) { + public function getExternalLinkAttribs( $url = false ) { $attribs = array(); $attribs['rel'] = self::getExternalLinkRel( $url, $this->mTitle ); @@ -1724,52 +1759,86 @@ class Parser { } /** - * Replace unusual URL escape codes with their equivalent characters - * - * @param $url String - * @return String + * Replace unusual escape codes in a URL with their equivalent characters * - * @todo This can merge genuinely required bits in the path or query string, - * breaking legit URLs. A proper fix would treat the various parts of - * the URL differently; as a workaround, just use the output for - * statistical records, not for actual linking/output. + * @deprecated since 1.24, use normalizeLinkUrl + * @param string $url + * @return string */ - static function replaceUnusualEscapes( $url ) { - return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', - array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url ); + public static function replaceUnusualEscapes( $url ) { + wfDeprecated( __METHOD__, '1.24' ); + return self::normalizeLinkUrl( $url ); } /** - * Callback function used in replaceUnusualEscapes(). - * Replaces unusual URL escape codes with their equivalent character + * Replace unusual escape codes in a URL with their equivalent characters * - * @param $matches array + * This generally follows the syntax defined in RFC 3986, with special + * consideration for HTTP query strings. * + * @param string $url * @return string */ - private static function replaceUnusualEscapesCallback( $matches ) { - $char = urldecode( $matches[0] ); - $ord = ord( $char ); - # Is it an unsafe or HTTP reserved character according to RFC 1738? - if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) { - # No, shouldn't be escaped - return $char; - } else { - # Yes, leave it escaped - return $matches[0]; + public static function normalizeLinkUrl( $url ) { + # First, make sure unsafe characters are encoded + $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/', + function ( $m ) { + return rawurlencode( $m[0] ); + }, + $url + ); + + $ret = ''; + $end = strlen( $url ); + + # Fragment part - 'fragment' + $start = strpos( $url, '#' ); + if ( $start !== false && $start < $end ) { + $ret = self::normalizeUrlComponent( + substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret; + $end = $start; } + + # Query part - 'query' minus &=+; + $start = strpos( $url, '?' ); + if ( $start !== false && $start < $end ) { + $ret = self::normalizeUrlComponent( + substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret; + $end = $start; + } + + # Scheme and path part - 'pchar' + # (we assume no userinfo or encoded colons in the host) + $ret = self::normalizeUrlComponent( + substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret; + + return $ret; + } + + private static function normalizeUrlComponent( $component, $unsafe ) { + $callback = function ( $matches ) use ( $unsafe ) { + $char = urldecode( $matches[0] ); + $ord = ord( $char ); + if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) { + # Unescape it + return $char; + } else { + # Leave it escaped, but use uppercase for a-f + return strtoupper( $matches[0] ); + } + }; + return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component ); } /** * make an image if it's allowed, either through the global * option, through the exception, or through the on-wiki whitelist - * @private * - * $param $url string + * @param string $url * * @return string */ - function maybeMakeExternalImage( $url ) { + private function maybeMakeExternalImage( $url ) { $imagesfrom = $this->mOptions->getAllowExternalImagesFrom(); $imagesexception = !empty( $imagesfrom ); $text = false; @@ -1787,16 +1856,23 @@ class Parser { } else { $imagematch = false; } + if ( $this->mOptions->getAllowExternalImages() - || ( $imagesexception && $imagematch ) ) { + || ( $imagesexception && $imagematch ) + ) { if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) { # Image found $text = Linker::makeExternalImage( $url ); } } if ( !$text && $this->mOptions->getEnableImageWhitelist() - && preg_match( self::EXT_IMAGE_REGEX, $url ) ) { - $whitelist = explode( "\n", wfMessage( 'external_image_whitelist' )->inContentLanguage()->text() ); + && preg_match( self::EXT_IMAGE_REGEX, $url ) + ) { + $whitelist = explode( + "\n", + wfMessage( 'external_image_whitelist' )->inContentLanguage()->text() + ); + foreach ( $whitelist as $entry ) { # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments if ( strpos( $entry, '#' ) === 0 || $entry === '' ) { @@ -1815,26 +1891,27 @@ class Parser { /** * Process [[ ]] wikilinks * - * @param $s string + * @param string $s * - * @return String: processed text + * @return string Processed text * * @private */ - function replaceInternalLinks( $s ) { + public function replaceInternalLinks( $s ) { $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) ); return $s; } /** * Process [[ ]] wikilinks (RIL) - * @param $s + * @param string $s * @throws MWException * @return LinkHolderArray * * @private */ - function replaceInternalLinks2( &$s ) { + public function replaceInternalLinks2( &$s ) { + global $wgExtraInterlanguageLinkPrefixes; wfProfileIn( __METHOD__ ); wfProfileIn( __METHOD__ . '-setup' ); @@ -1863,7 +1940,9 @@ class Parser { if ( $useLinkPrefixExtension ) { # Match the end of a line for a word that's not followed by whitespace, # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched - $e2 = wfMessage( 'linkprefix' )->inContentLanguage()->text(); + global $wgContLang; + $charset = $wgContLang->linkPrefixCharset(); + $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu"; } if ( is_null( $this->mTitle ) ) { @@ -1887,8 +1966,11 @@ class Parser { $useSubpages = $this->areSubpagesAllowed(); wfProfileOut( __METHOD__ . '-setup' ); + // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect # Loop for each link for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) { + // @codingStandardsIgnoreStart + # Check for excessive memory usage if ( $holders->isBig() ) { # Too big @@ -1926,11 +2008,10 @@ class Parser { # Still some problems for cases where the ] is meant to be outside punctuation, # and no image is in sight. See bug 2095. # - if ( $text !== '' && - substr( $m[3], 0, 1 ) === ']' && - strpos( $text, '[' ) !== false - ) - { + if ( $text !== '' + && substr( $m[3], 0, 1 ) === ']' + && strpos( $text, '[' ) !== false + ) { $text .= ']'; # so that replaceExternalLinks($text) works later $m[3] = substr( $m[3], 1 ); } @@ -1940,7 +2021,8 @@ class Parser { $m[1] = str_replace( array( '<', '>' ), array( '<', '>' ), rawurldecode( $m[1] ) ); } $trail = $m[3]; - } elseif ( preg_match( $e1_img, $line, $m ) ) { # Invalid, but might be an image with a link in its caption + } elseif ( preg_match( $e1_img, $line, $m ) ) { + # Invalid, but might be an image with a link in its caption $might_be_img = true; $text = $m[2]; if ( strpos( $m[1], '%' ) !== false ) { @@ -1955,10 +2037,12 @@ class Parser { wfProfileOut( __METHOD__ . "-e1" ); wfProfileIn( __METHOD__ . "-misc" ); + $origLink = $m[1]; + # Don't allow internal links to pages containing # PROTO: where PROTO is a valid URL protocol; these # should be external links. - if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $m[1] ) ) { + if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) { $s .= $prefix . '[[' . $line; wfProfileOut( __METHOD__ . "-misc" ); continue; @@ -1966,12 +2050,12 @@ class Parser { # Make subpage if necessary if ( $useSubpages ) { - $link = $this->maybeDoSubpageLink( $m[1], $text ); + $link = $this->maybeDoSubpageLink( $origLink, $text ); } else { - $link = $m[1]; + $link = $origLink; } - $noforce = ( substr( $m[1], 0, 1 ) !== ':' ); + $noforce = ( substr( $origLink, 0, 1 ) !== ':' ); if ( !$noforce ) { # Strip off leading ':' $link = substr( $link, 1 ); @@ -1987,7 +2071,7 @@ class Parser { } $ns = $nt->getNamespace(); - $iw = $nt->getInterWiki(); + $iw = $nt->getInterwiki(); wfProfileOut( __METHOD__ . "-title" ); if ( $might_be_img ) { # if this is actually an invalid link @@ -2047,12 +2131,15 @@ class Parser { } # Link not escaped by : , create the various objects - if ( $noforce ) { + if ( $noforce && !$nt->wasLocalInterwiki() ) { # Interwikis wfProfileIn( __METHOD__ . "-interwiki" ); - if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) { - // XXX: the above check prevents links to sites with identifiers that are not language codes - + if ( + $iw && $this->mOptions->getInterwikiMagic() && $nottalk && ( + Language::fetchLanguageName( $iw, null, 'mw' ) || + in_array( $iw, $wgExtraInterlanguageLinkPrefixes ) + ) + ) { # Bug 24502: filter duplicates if ( !isset( $this->mLangLinkLanguages[$iw] ) ) { $this->mLangLinkLanguages[$iw] = true; @@ -2108,7 +2195,6 @@ class Parser { /** * Strip the whitespace Category links produce, see bug 87 - * @todo We might want to use trim($tmp, "\n") here. */ $s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail; @@ -2120,7 +2206,7 @@ class Parser { # Self-link checking. For some languages, variants of the title are checked in # LinkHolderArray::doVariants() to allow batching the existence checks necessary # for linking to a different variant. - if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && $nt->getFragment() === '' ) { + if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) { $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail ); continue; } @@ -2169,14 +2255,14 @@ class Parser { * breaking URLs in the following text without breaking trails on the * wiki links, it's been made into a horrible function. * - * @param $nt Title - * @param $text String - * @param array $query or String - * @param $trail String - * @param $prefix String - * @return String: HTML-wikitext mix oh yuck + * @param Title $nt + * @param string $text + * @param array|string $query + * @param string $trail + * @param string $prefix + * @return string HTML-wikitext mix oh yuck */ - function makeKnownLinkHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) { + public function makeKnownLinkHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) { list( $inside, $trail ) = Linker::splitTrail( $trail ); if ( is_string( $query ) ) { @@ -2198,19 +2284,19 @@ class Parser { * Not needed quite as much as it used to be since free links are a bit * more sensible these days. But bracketed links are still an issue. * - * @param string $text more-or-less HTML - * @return String: less-or-more HTML with NOPARSE bits + * @param string $text More-or-less HTML + * @return string Less-or-more HTML with NOPARSE bits */ - function armorLinks( $text ) { + public function armorLinks( $text ) { return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/', "{$this->mUniqPrefix}NOPARSE$1", $text ); } /** * Return true if subpage links should be expanded on this page. - * @return Boolean + * @return bool */ - function areSubpagesAllowed() { + public function areSubpagesAllowed() { # Some namespaces don't allow subpages return MWNamespace::hasSubpages( $this->mTitle->getNamespace() ); } @@ -2218,12 +2304,12 @@ class Parser { /** * Handle link to subpage if necessary * - * @param string $target the source of the link - * @param &$text String: the link text, modified as necessary - * @return string the full name of the link + * @param string $target The source of the link + * @param string &$text The link text, modified as necessary + * @return string The full name of the link * @private */ - function maybeDoSubpageLink( $target, &$text ) { + public function maybeDoSubpageLink( $target, &$text ) { return Linker::normalizeSubpageLink( $this->mTitle, $target, $text ); } @@ -2233,7 +2319,7 @@ class Parser { * * @return string */ - function closeParagraph() { + public function closeParagraph() { $result = ''; if ( $this->mLastSection != '' ) { $result = 'mLastSection . ">\n"; @@ -2248,12 +2334,12 @@ class Parser { * of both arguments, starting at the beginning of both. * @private * - * @param $st1 string - * @param $st2 string + * @param string $st1 + * @param string $st2 * * @return int */ - function getCommon( $st1, $st2 ) { + public function getCommon( $st1, $st2 ) { $fl = strlen( $st1 ); $shorter = strlen( $st2 ); if ( $fl < $shorter ) { @@ -2273,21 +2359,21 @@ class Parser { * element appropriate to the prefix character passed into them. * @private * - * @param $char string + * @param string $char * * @return string */ - function openList( $char ) { + public function openList( $char ) { $result = $this->closeParagraph(); if ( '*' === $char ) { - $result .= "

    \n
  • "; + $result .= "
    • "; } elseif ( '#' === $char ) { - $result .= "
        \n
      1. "; + $result .= "
        1. "; } elseif ( ':' === $char ) { - $result .= "
          \n
          "; + $result .= "
          "; } elseif ( ';' === $char ) { - $result .= "
          \n
          "; + $result .= "
          "; $this->mDTopen = true; } else { $result = ''; @@ -2298,12 +2384,12 @@ class Parser { /** * TODO: document - * @param $char String + * @param string $char * @private * * @return string */ - function nextItem( $char ) { + public function nextItem( $char ) { if ( '*' === $char || '#' === $char ) { return "
        2. \n
        3. "; } elseif ( ':' === $char || ';' === $char ) { @@ -2323,40 +2409,40 @@ class Parser { } /** - * TODO: document - * @param $char String + * @todo Document + * @param string $char * @private * * @return string */ - function closeList( $char ) { + public function closeList( $char ) { if ( '*' === $char ) { - $text = "
        4. \n
    "; + $text = "
"; } elseif ( '#' === $char ) { - $text = "\n"; + $text = ""; } elseif ( ':' === $char ) { if ( $this->mDTopen ) { $this->mDTopen = false; - $text = "\n"; + $text = ""; } else { - $text = "\n"; + $text = ""; } } else { return ''; } - return $text . "\n"; + return $text; } /**#@-*/ /** * Make lists from lines starting with ':', '*', '#', etc. (DBL) * - * @param $text String - * @param $linestart Boolean: whether or not this is at the start of a line. + * @param string $text + * @param bool $linestart Whether or not this is at the start of a line. * @private - * @return string the lists rendered as HTML + * @return string The lists rendered as HTML */ - function doBlockLevels( $text, $linestart ) { + public function doBlockLevels( $text, $linestart ) { wfProfileIn( __METHOD__ ); # Parsing through the text line by line. The main thing @@ -2442,6 +2528,9 @@ class Parser { } # Open prefixes where appropriate. + if ( $lastPrefix && $prefixLength > $commonPrefixLength ) { + $output .= "\n"; + } while ( $prefixLength > $commonPrefixLength ) { $char = substr( $prefix, $commonPrefixLength, 1 ); $output .= $this->openList( $char ); @@ -2455,6 +2544,9 @@ class Parser { } ++$commonPrefixLength; } + if ( !$prefixLength && $lastPrefix ) { + $output .= "\n"; + } $lastPrefix = $prefix2; } @@ -2463,13 +2555,22 @@ class Parser { wfProfileIn( __METHOD__ . "-paragraph" ); # No prefix (not in list)--go to paragraph mode # XXX: use a stack for nestable elements like span, table and div - $openmatch = preg_match( '/(?:mUniqPrefix . '-pre|<\\/li|<\\/ul|<\\/ol|<\\/dl|<\\/?center)/iS', $t ); + '/(?:<\\/table|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|' + . 'mUniqPrefix + . '-pre|<\\/li|<\\/ul|<\\/ol|<\\/dl|<\\/?center)/iS', + $t + ); + if ( $openmatch or $closematch ) { $paragraphStack = false; - # TODO bug 5718: paragraph closed + # @todo bug 5718: paragraph closed $output .= $this->closeParagraph(); if ( $preOpenMatch and !$preCloseMatch ) { $this->mInPre = true; @@ -2481,7 +2582,10 @@ class Parser { } $inBlockElem = !$closematch; } elseif ( !$inBlockElem && !$this->mInPre ) { - if ( ' ' == substr( $t, 0, 1 ) and ( $this->mLastSection === 'pre' || trim( $t ) != '' ) and !$inBlockquote ) { + if ( ' ' == substr( $t, 0, 1 ) + && ( $this->mLastSection === 'pre' || trim( $t ) != '' ) + && !$inBlockquote + ) { # pre if ( $this->mLastSection !== 'pre' ) { $paragraphStack = false; @@ -2524,12 +2628,18 @@ class Parser { $this->mInPre = false; } if ( $paragraphStack === false ) { - $output .= $t . "\n"; + $output .= $t; + if ( $prefixLength === 0 ) { + $output .= "\n"; + } } } while ( $prefixLength ) { $output .= $this->closeList( $prefix2[$prefixLength - 1] ); --$prefixLength; + if ( !$prefixLength ) { + $output .= "\n"; + } } if ( $this->mLastSection != '' ) { $output .= 'mLastSection . '>'; @@ -2544,13 +2654,13 @@ class Parser { * Split up a string on ':', ignoring any occurrences inside tags * to prevent illegal overlapping. * - * @param string $str the string to split - * @param &$before String set to everything before the ':' - * @param &$after String set to everything after the ':' + * @param string $str The string to split + * @param string &$before Set to everything before the ':' + * @param string &$after Set to everything after the ':' * @throws MWException - * @return String the position of the ':', or false if none found + * @return string The position of the ':', or false if none found */ - function findColonNoLinks( $str, &$before, &$after ) { + public function findColonNoLinks( $str, &$before, &$after ) { wfProfileIn( __METHOD__ ); $pos = strpos( $str, ':' ); @@ -2712,14 +2822,14 @@ class Parser { * * @private * - * @param $index integer - * @param bool|\PPFrame $frame + * @param int $index + * @param bool|PPFrame $frame * * @throws MWException * @return string */ - function getVariableValue( $index, $frame = false ) { - global $wgContLang, $wgSitename, $wgServer; + public function getVariableValue( $index, $frame = false ) { + global $wgContLang, $wgSitename, $wgServer, $wgServerName; global $wgArticlePath, $wgScriptPath, $wgStylePath; if ( is_null( $this->mTitle ) ) { @@ -2747,6 +2857,9 @@ class Parser { $pageLang = $this->getFunctionLang(); switch ( $index ) { + case '!': + $value = '|'; + break; case 'currentmonth': $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) ); break; @@ -2811,13 +2924,21 @@ class Parser { $value = wfEscapeWikiText( $this->mTitle->getRootText() ); break; case 'rootpagenamee': - $value = wfEscapeWikiText( wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getRootText() ) ) ); + $value = wfEscapeWikiText( wfUrlEncode( str_replace( + ' ', + '_', + $this->mTitle->getRootText() + ) ) ); break; case 'basepagename': $value = wfEscapeWikiText( $this->mTitle->getBaseText() ); break; case 'basepagenamee': - $value = wfEscapeWikiText( wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) ) ); + $value = wfEscapeWikiText( wfUrlEncode( str_replace( + ' ', + '_', + $this->mTitle->getBaseText() + ) ) ); break; case 'talkpagename': if ( $this->mTitle->canTalk() ) { @@ -2928,7 +3049,9 @@ class Parser { $value = $this->mTitle->getNamespace(); break; case 'talkspace': - $value = $this->mTitle->canTalk() ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() ) : ''; + $value = $this->mTitle->canTalk() + ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() ) + : ''; break; case 'talkspacee': $value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : ''; @@ -2940,7 +3063,7 @@ class Parser { $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) ); break; case 'currentdayname': - $value = $pageLang->getWeekdayName( MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 ); + $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 ); break; case 'currentyear': $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true ); @@ -2960,13 +3083,19 @@ class Parser { $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) ); break; case 'localdayname': - $value = $pageLang->getWeekdayName( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1 ); + $value = $pageLang->getWeekdayName( + (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1 + ); break; case 'localyear': $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true ); break; case 'localtime': - $value = $pageLang->time( MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ), false, false ); + $value = $pageLang->time( + MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ), + false, + false + ); break; case 'localhour': $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true ); @@ -3020,8 +3149,7 @@ class Parser { case 'server': return $wgServer; case 'servername': - $serverParts = wfParseUrl( $wgServer ); - return $serverParts && isset( $serverParts['host'] ) ? $serverParts['host'] : $wgServer; + return $wgServerName; case 'scriptpath': return $wgScriptPath; case 'stylepath': @@ -3031,13 +3159,17 @@ class Parser { case 'contentlanguage': global $wgLanguageCode; return $wgLanguageCode; + case 'cascadingsources': + $value = CoreParserFunctions::cascadingsources( $this ); + break; default: $ret = null; - if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret, &$frame ) ) ) { - return $ret; - } else { - return null; - } + wfRunHooks( + 'ParserGetVariableValueSwitch', + array( &$this, &$this->mVarCache, &$index, &$ret, &$frame ) + ); + + return $ret; } if ( $index ) { @@ -3052,7 +3184,7 @@ class Parser { * * @private */ - function initialiseVariables() { + public function initialiseVariables() { wfProfileIn( __METHOD__ ); $variableIDs = MagicWord::getVariableIDs(); $substIDs = MagicWord::getSubstIDs(); @@ -3067,9 +3199,9 @@ class Parser { * This is the ghost of replace_variables(). * * @param string $text The text to parse - * @param $flags Integer: bitwise combination of: - * self::PTD_FOR_INCLUSION Handle "" and "" as if the text is being - * included. Default is to assume a direct page view. + * @param int $flags Bitwise combination of: + * - self::PTD_FOR_INCLUSION: Handle "" and "" as if the text is being + * included. Default is to assume a direct page view. * * The generated DOM tree must depend only on the input text and the flags. * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899. @@ -3082,11 +3214,9 @@ class Parser { * cache may be implemented at a later date which takes further advantage of these strict * dependency requirements. * - * @private - * * @return PPNode */ - function preprocessToDom( $text, $flags = 0 ) { + public function preprocessToDom( $text, $flags = 0 ) { $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags ); return $dom; } @@ -3094,7 +3224,7 @@ class Parser { /** * Return a three-element array: leading whitespace, string contents, trailing whitespace * - * @param $s string + * @param string $s * * @return array */ @@ -3121,16 +3251,17 @@ class Parser { * self::OT_PREPROCESS: templates but not extension tags * self::OT_HTML: all templates and extension tags * - * @param string $text the text to transform - * @param $frame PPFrame Object describing the arguments passed to the template. - * Arguments may also be provided as an associative array, as was the usual case before MW1.12. - * Providing arguments this way may be useful for extensions wishing to perform variable replacement explicitly. - * @param $argsOnly Boolean only do argument (triple-brace) expansion, not double-brace expansion - * @private - * + * @param string $text The text to transform + * @param bool|PPFrame $frame Object describing the arguments passed to the + * template. Arguments may also be provided as an associative array, as + * was the usual case before MW1.12. Providing arguments this way may be + * useful for extensions wishing to perform variable replacement + * explicitly. + * @param bool $argsOnly Only do argument (triple-brace) expansion, not + * double-brace expansion. * @return string */ - function replaceVariables( $text, $frame = false, $argsOnly = false ) { + public function replaceVariables( $text, $frame = false, $argsOnly = false ) { # Is there any text? Also, Prevent too big inclusions! if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) { return $text; @@ -3140,7 +3271,8 @@ class Parser { if ( $frame === false ) { $frame = $this->getPreprocessor()->newFrame(); } elseif ( !( $frame instanceof PPFrame ) ) { - wfDebug( __METHOD__ . " called using plain parameters instead of a PPFrame instance. Creating custom frame.\n" ); + wfDebug( __METHOD__ . " called using plain parameters instead of " + . "a PPFrame instance. Creating custom frame.\n" ); $frame = $this->getPreprocessor()->newCustomFrame( $frame ); } @@ -3155,11 +3287,11 @@ class Parser { /** * Clean up argument array - refactored in 1.9 so parserfunctions can use it, too. * - * @param $args array + * @param array $args * * @return array */ - static function createAssocArgs( $args ) { + public static function createAssocArgs( $args ) { $assocArgs = array(); $index = 1; foreach ( $args as $arg ) { @@ -3185,7 +3317,7 @@ class Parser { * Warn the user when a parser limitation is reached * Will warn at most once the user per limitation type * - * @param string $limitationType should be one of: + * @param string $limitationType Should be one of: * 'expensive-parserfunction' (corresponding messages: * 'expensive-parserfunction-warning', * 'expensive-parserfunction-category') @@ -3201,11 +3333,11 @@ class Parser { * 'expansion-depth-exceeded' (corresponding messages: * 'expansion-depth-exceeded-warning', * 'expansion-depth-exceeded-category') - * @param int|null $current Current value - * @param int|null $max Maximum allowed, when an explicit limit has been + * @param string|int|null $current Current value + * @param string|int|null $max Maximum allowed, when an explicit limit has been * exceeded, provide the values (optional) */ - function limitationWarn( $limitationType, $current = '', $max = '' ) { + public function limitationWarn( $limitationType, $current = '', $max = '' ) { # does no harm if $current and $max are present but are unnecessary for the message $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max ) ->inLanguage( $this->mOptions->getUserLangObj() )->text(); @@ -3217,26 +3349,32 @@ class Parser { * Return the text of a template, after recursively * replacing any variables or templates within the template. * - * @param array $piece the parts of the template - * $piece['title']: the title, i.e. the part before the | - * $piece['parts']: the parameter array - * $piece['lineStart']: whether the brace was at the start of a line - * @param $frame PPFrame The current frame, contains template arguments - * @throws MWException - * @return String: the text of the template - * @private + * @param array $piece The parts of the template + * $piece['title']: the title, i.e. the part before the | + * $piece['parts']: the parameter array + * $piece['lineStart']: whether the brace was at the start of a line + * @param PPFrame $frame The current frame, contains template arguments + * @throws Exception + * @return string The text of the template */ - function braceSubstitution( $piece, $frame ) { + public function braceSubstitution( $piece, $frame ) { wfProfileIn( __METHOD__ ); wfProfileIn( __METHOD__ . '-setup' ); - # Flags - $found = false; # $text has been filled - $nowiki = false; # wiki markup in $text should be escaped - $isHTML = false; # $text is HTML, armour it against wikitext transformation - $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered - $isChildObj = false; # $text is a DOM node needing expansion in a child frame - $isLocalObj = false; # $text is a DOM node needing expansion in the current frame + // Flags + + // $text has been filled + $found = false; + // wiki markup in $text should be escaped + $nowiki = false; + // $text is HTML, armour it against wikitext transformation + $isHTML = false; + // Force interwiki transclusion to be done in raw mode not rendered + $forceRawInterwiki = false; + // $text is a DOM node needing expansion in a child frame + $isChildObj = false; + // $text is a DOM node needing expansion in the current frame + $isLocalObj = false; # Title object, where $text came from $title = false; @@ -3251,7 +3389,8 @@ class Parser { $originalTitle = $part1; # $args is a list of argument nodes, starting from index 0, not including $part1 - # @todo FIXME: If piece['parts'] is null then the call to getLength() below won't work b/c this $args isn't an object + # @todo FIXME: If piece['parts'] is null then the call to getLength() + # below won't work b/c this $args isn't an object $args = ( null == $piece['parts'] ) ? array() : $piece['parts']; wfProfileOut( __METHOD__ . '-setup' ); @@ -3385,13 +3524,14 @@ class Parser { if ( !$title->isExternal() ) { if ( $title->isSpecialPage() && $this->mOptions->getAllowSpecialInclusion() - && $this->ot['html'] ) - { + && $this->ot['html'] + ) { // Pass the template arguments as URL parameters. // "uselang" will have no effect since the Language object // is forced to the one defined in ParserOptions. $pageArgs = array(); - for ( $i = 0; $i < $args->getLength(); $i++ ) { + $argsLength = $args->getLength(); + for ( $i = 0; $i < $argsLength; $i++ ) { $bits = $args->item( $i )->splitArg(); if ( strval( $bits['index'] ) === '' ) { $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) ); @@ -3416,7 +3556,8 @@ class Parser { } } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) { $found = false; # access denied - wfDebug( __METHOD__ . ": template inclusion denied for " . $title->getPrefixedDBkey() ); + wfDebug( __METHOD__ . ": template inclusion denied for " . + $title->getPrefixedDBkey() . "\n" ); } else { list( $text, $title ) = $this->getTemplateDom( $title ); if ( $text !== false ) { @@ -3476,12 +3617,7 @@ class Parser { $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG ); } elseif ( $titleText !== false && $newFrame->isEmpty() ) { # Expansion is eligible for the empty-frame cache - if ( isset( $this->mTplExpandCache[$titleText] ) ) { - $text = $this->mTplExpandCache[$titleText]; - } else { - $text = $newFrame->expand( $text ); - $this->mTplExpandCache[$titleText] = $text; - } + $text = $newFrame->cachedExpand( $titleText, $text ); } else { # Uncached expansion $text = $newFrame->expand( $text ); @@ -3504,8 +3640,8 @@ class Parser { $text = wfEscapeWikiText( $text ); } elseif ( is_string( $text ) && !$piece['lineStart'] - && preg_match( '/^(?:{\\||:|;|#|\*)/', $text ) ) - { + && preg_match( '/^(?:{\\||:|;|#|\*)/', $text ) + ) { # Bug 529: if the template begins with a table or block-level # element, it should be treated as beginning a new line. # This behavior is somewhat controversial. @@ -3523,7 +3659,8 @@ class Parser { preg_replace( '/^:/', '', $originalTitle ); $text = "[[:$originalTitle]]"; } - $text .= $this->insertStripItem( '' ); + $text .= $this->insertStripItem( '' ); $this->limitationWarn( 'post-expand-template-inclusion' ); } @@ -3550,9 +3687,10 @@ class Parser { * nowiki: bool, wiki markup in $text should be escaped * * @since 1.21 - * @param $frame PPFrame The current frame, contains template arguments - * @param $function string Function name - * @param $args array Arguments to the function + * @param PPFrame $frame The current frame, contains template arguments + * @param string $function Function name + * @param array $args Arguments to the function + * @throws MWException * @return array */ public function callParserFunction( $frame, $function, array $args = array() ) { @@ -3655,11 +3793,11 @@ class Parser { * Get the semi-parsed DOM representation of a template with a given title, * and its redirect destination title. Cached. * - * @param $title Title + * @param Title $title * * @return array */ - function getTemplateDom( $title ) { + public function getTemplateDom( $title ) { $cacheTitle = $title; $titleText = $title->getPrefixedDBkey(); @@ -3694,10 +3832,11 @@ class Parser { /** * Fetch the unparsed text of a template and register a reference to it. * @param Title $title - * @return Array ( string or false, Title ) + * @return array ( string or false, Title ) */ - function fetchTemplateAndTitle( $title ) { - $templateCb = $this->mOptions->getTemplateCallback(); # Defaults to Parser::statelessFetchTemplate() + public function fetchTemplateAndTitle( $title ) { + // Defaults to Parser::statelessFetchTemplate() + $templateCb = $this->mOptions->getTemplateCallback(); $stuff = call_user_func( $templateCb, $title, $this ); $text = $stuff['text']; $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title; @@ -3717,9 +3856,9 @@ class Parser { /** * Fetch the unparsed text of a template and register a reference to it. * @param Title $title - * @return mixed string or false + * @return string|bool */ - function fetchTemplate( $title ) { + public function fetchTemplate( $title ) { $rv = $this->fetchTemplateAndTitle( $title ); return $rv[0]; } @@ -3728,12 +3867,12 @@ class Parser { * Static function to get a template * Can be overridden via ParserOptions::setTemplateCallback(). * - * @param $title Title - * @param $parser Parser + * @param Title $title + * @param bool|Parser $parser * * @return array */ - static function statelessFetchTemplate( $title, $parser = false ) { + public static function statelessFetchTemplate( $title, $parser = false ) { $text = $skip = false; $finalTitle = $title; $deps = array(); @@ -3817,7 +3956,7 @@ class Parser { * @param array $options Array of options to RepoGroup::findFile * @return File|bool */ - function fetchFile( $title, $options = array() ) { + public function fetchFile( $title, $options = array() ) { $res = $this->fetchFileAndTitle( $title, $options ); return $res[0]; } @@ -3827,9 +3966,9 @@ class Parser { * If 'broken' is a key in $options then the file will appear as a broken thumbnail. * @param Title $title * @param array $options Array of options to RepoGroup::findFile - * @return Array ( File or false, Title of file ) + * @return array ( File or false, Title of file ) */ - function fetchFileAndTitle( $title, $options = array() ) { + public function fetchFileAndTitle( $title, $options = array() ) { $file = $this->fetchFileNoRegister( $title, $options ); $time = $file ? $file->getTimestamp() : false; @@ -3839,12 +3978,7 @@ class Parser { if ( $file && !$title->equals( $file->getTitle() ) ) { # Update fetched file title $title = $file->getTitle(); - if ( is_null( $file->getRedirectedTitle() ) ) { - # This file was not a redirect, but the title does not match. - # Register under the new name because otherwise the link will - # get lost. - $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 ); - } + $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 ); } return array( $file, $title ); } @@ -3857,7 +3991,7 @@ class Parser { * * @param Title $title * @param array $options Array of options to RepoGroup::findFile - * @return File or false + * @return File|bool */ protected function fetchFileNoRegister( $title, $options = array() ) { if ( isset( $options['broken'] ) ) { @@ -3873,12 +4007,12 @@ class Parser { /** * Transclude an interwiki link. * - * @param $title Title - * @param $action + * @param Title $title + * @param string $action * * @return string */ - function interwikiTransclude( $title, $action ) { + public function interwikiTransclude( $title, $action ) { global $wgEnableScaryTranscluding; if ( !$wgEnableScaryTranscluding ) { @@ -3894,10 +4028,10 @@ class Parser { } /** - * @param $url string - * @return Mixed|String + * @param string $url + * @return mixed|string */ - function fetchScaryTemplateMaybeFromCache( $url ) { + public function fetchScaryTemplateMaybeFromCache( $url ) { global $wgTranscludeCacheExpiry; $dbr = wfGetDB( DB_SLAVE ); $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry ); @@ -3911,8 +4045,10 @@ class Parser { $status = $req->execute(); // Status object if ( $status->isOK() ) { $text = $req->getContent(); - } elseif ( $req->getStatus() != 200 ) { // Though we failed to fetch the content, this status is useless. - return wfMessage( 'scarytranscludefailed-httpstatus', $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text(); + } elseif ( $req->getStatus() != 200 ) { + // Though we failed to fetch the content, this status is useless. + return wfMessage( 'scarytranscludefailed-httpstatus' ) + ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text(); } else { return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text(); } @@ -3930,12 +4066,12 @@ class Parser { * Triple brace replacement -- used for template arguments * @private * - * @param $piece array - * @param $frame PPFrame + * @param array $piece + * @param PPFrame $frame * * @return array */ - function argSubstitution( $piece, $frame ) { + public function argSubstitution( $piece, $frame ) { wfProfileIn( __METHOD__ ); $error = false; @@ -3945,11 +4081,10 @@ class Parser { $object = false; $text = $frame->getArgument( $argName ); if ( $text === false && $parts->getLength() > 0 - && ( - $this->ot['html'] - || $this->ot['pre'] - || ( $this->ot['wiki'] && $frame->isTemplate() ) - ) + && ( $this->ot['html'] + || $this->ot['pre'] + || ( $this->ot['wiki'] && $frame->isTemplate() ) + ) ) { # No match in frame, use the supplied default $object = $parts->item( 0 )->getChildren(); @@ -3986,16 +4121,17 @@ class Parser { * attributes Optional associative array of parsed attributes * inner Contents of extension element * noClose Original text did not have a close tag - * @param $frame PPFrame + * @param PPFrame $frame * * @throws MWException * @return string */ - function extensionSubstitution( $params, $frame ) { + public function extensionSubstitution( $params, $frame ) { $name = $frame->expand( $params['name'] ); $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] ); $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] ); - $marker = "{$this->mUniqPrefix}-$name-" . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX; + $marker = "{$this->mUniqPrefix}-$name-" + . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX; $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) && ( $this->ot['html'] || $this->ot['pre'] ); @@ -4070,11 +4206,11 @@ class Parser { /** * Increment an include size counter * - * @param string $type the type of expansion - * @param $size Integer: the size of the text - * @return Boolean: false if this inclusion would take it over the maximum, true otherwise + * @param string $type The type of expansion + * @param int $size The size of the text + * @return bool False if this inclusion would take it over the maximum, true otherwise */ - function incrementIncludeSize( $type, $size ) { + public function incrementIncludeSize( $type, $size ) { if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) { return false; } else { @@ -4086,9 +4222,9 @@ class Parser { /** * Increment the expensive function count * - * @return Boolean: false if the limit has been exceeded + * @return bool False if the limit has been exceeded */ - function incrementExpensiveFunctionCount() { + public function incrementExpensiveFunctionCount() { $this->mExpensiveFunctionCount++; return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit(); } @@ -4097,11 +4233,11 @@ class Parser { * Strip double-underscore items like __NOGALLERY__ and __NOTOC__ * Fills $this->mDoubleUnderscores, returns the modified text * - * @param $text string + * @param string $text * * @return string */ - function doDoubleUnderscore( $text ) { + public function doDoubleUnderscore( $text ) { wfProfileIn( __METHOD__ ); # The position of __TOC__ needs to be recorded @@ -4127,7 +4263,9 @@ class Parser { if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) { $this->mShowToc = false; } - if ( isset( $this->mDoubleUnderscores['hiddencat'] ) && $this->mTitle->getNamespace() == NS_CATEGORY ) { + if ( isset( $this->mDoubleUnderscores['hiddencat'] ) + && $this->mTitle->getNamespace() == NS_CATEGORY + ) { $this->addTrackingCategory( 'hidden-category-category' ); } # (bug 8068) Allow control over whether robots index a page. @@ -4156,8 +4294,12 @@ class Parser { * Add a tracking category, getting the title from a system message, * or print a debug message if the title is invalid. * - * @param string $msg message key - * @return Boolean: whether the addition was successful + * Please add any message that you use with this function to + * $wgTrackingCategories. That way they will be listed on + * Special:TrackingCategories. + * + * @param string $msg Message key + * @return bool Whether the addition was successful */ public function addTrackingCategory( $msg ) { if ( $this->mTitle->getNamespace() === NS_SPECIAL ) { @@ -4195,13 +4337,13 @@ class Parser { * It loops through all headlines, collects the necessary data, then splits up the * string and re-inserts the newly formatted headlines. * - * @param $text String - * @param string $origText original, untouched wikitext - * @param $isMain Boolean + * @param string $text + * @param string $origText Original, untouched wikitext + * @param bool $isMain * @return mixed|string * @private */ - function formatHeadings( $text, $origText, $isMain = true ) { + public function formatHeadings( $text, $origText, $isMain = true ) { global $wgMaxTocLevel, $wgExperimentalHtmlIds; # Inhibit editsection links if requested in the page @@ -4218,7 +4360,11 @@ class Parser { # Get all headlines for numbering them and adding funky stuff like [edit] # links - this is for later, but we need the number of headlines right now $matches = array(); - $numMatches = preg_match_all( '/[1-6])(?P.*?' . '>)\s*(?P
[\s\S]*?)\s*<\/H[1-6] *>/i', $text, $matches ); + $numMatches = preg_match_all( + '/[1-6])(?P.*?' . '>)\s*(?P
[\s\S]*?)\s*<\/H[1-6] *>/i', + $text, + $matches + ); # if there are fewer than 4 headlines in the article, do not show TOC # unless it's been explicitly enabled. @@ -4367,7 +4513,10 @@ class Parser { # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from , # to allow setting directionality in toc items. $tocline = preg_replace( - array( '#<(?!/?(span|sup|sub|i|b)(?: [^>]*)?>).*?' . '>#', '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|i|b))(?: .*?)?' . '>#' ), + array( + '#<(?!/?(span|sup|sub|i|b)(?: [^>]*)?>).*?' . '>#', + '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|i|b))(?: .*?)?' . '>#' + ), array( '', '<$1>' ), $safeHeadline ); @@ -4431,7 +4580,11 @@ class Parser { # Don't number the heading if it is the only one (looks silly) if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) { # the two are different if the line contains a link - $headline = Html::element( 'span', array( 'class' => 'mw-headline-number' ), $numbering ) . ' ' . $headline; + $headline = Html::element( + 'span', + array( 'class' => 'mw-headline-number' ), + $numbering + ) . ' ' . $headline; } # Create the anchor for linking from the TOC to the section @@ -4479,21 +4632,30 @@ class Parser { if ( $isTemplate ) { # Put a T flag in the section identifier, to indicate to extractSections() # that sections inside should be counted. - $editlinkArgs = array( $titleText, "T-$sectionIndex"/*, null */ ); + $editsectionPage = $titleText; + $editsectionSection = "T-$sectionIndex"; + $editsectionContent = null; } else { - $editlinkArgs = array( $this->mTitle->getPrefixedText(), $sectionIndex, $headlineHint ); + $editsectionPage = $this->mTitle->getPrefixedText(); + $editsectionSection = $sectionIndex; + $editsectionContent = $headlineHint; } - // We use a bit of pesudo-xml for editsection markers. The language converter is run later on - // Using a UNIQ style marker leads to the converter screwing up the tokens when it converts stuff - // And trying to insert strip tags fails too. At this point all real inputted tags have already been escaped - // so we don't have to worry about a user trying to input one of these markers directly. - // We use a page and section attribute to stop the language converter from converting these important bits - // of data, but put the headline hint inside a content block because the language converter is supposed to + // We use a bit of pesudo-xml for editsection markers. The + // language converter is run later on. Using a UNIQ style marker + // leads to the converter screwing up the tokens when it + // converts stuff. And trying to insert strip tags fails too. At + // this point all real inputted tags have already been escaped, + // so we don't have to worry about a user trying to input one of + // these markers directly. We use a page and section attribute + // to stop the language converter from converting these + // important bits of data, but put the headline hint inside a + // content block because the language converter is supposed to // be able to convert that piece of data. - $editlink = ''; + // Gets replaced with html in ParserOutput::getText + $editlink = ''; } else { $editlink .= '/>'; } @@ -4521,6 +4683,7 @@ class Parser { $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() ); $this->mOutput->setTOCHTML( $toc ); $toc = self::TOC_START . $toc . self::TOC_END; + $this->mOutput->addModules( 'mediawiki.toc' ); } if ( $isMain ) { @@ -4575,14 +4738,19 @@ class Parser { * Transform wiki markup when saving a page by doing "\r\n" -> "\n" * conversion, substitting signatures, {{subst:}} templates, etc. * - * @param string $text the text to transform - * @param $title Title: the Title object for the current article - * @param $user User: the User object describing the current user - * @param $options ParserOptions: parsing options - * @param $clearState Boolean: whether to clear the parser state first - * @return String: the altered wiki markup + * @param string $text The text to transform + * @param Title $title The Title object for the current article + * @param User $user The User object describing the current user + * @param ParserOptions $options Parsing options + * @param bool $clearState Whether to clear the parser state first + * @return string The altered wiki markup */ - public function preSaveTransform( $text, Title $title, User $user, ParserOptions $options, $clearState = true ) { + public function preSaveTransform( $text, Title $title, User $user, + ParserOptions $options, $clearState = true + ) { + if ( $clearState ) { + $magicScopeVariable = $this->lock(); + } $this->startParse( $title, $options, self::OT_WIKI, $clearState ); $this->setUser( $user ); @@ -4602,14 +4770,13 @@ class Parser { /** * Pre-save transform helper function - * @private * - * @param $text string - * @param $user User + * @param string $text + * @param User $user * * @return string */ - function pstPass2( $text, $user ) { + private function pstPass2( $text, $user ) { global $wgContLang; # Note: This is the timestamp saved as hardcoded wikitext to @@ -4652,10 +4819,14 @@ class Parser { $tc = '[' . Title::legalChars() . ']'; $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii! - $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/"; # [[ns:page (context)|]] - $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/"; # [[ns:page(context)|]] (double-width brackets, added in r40257) - $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/"; # [[ns:page (context), context|]] (using either single or double-width comma) - $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]] (reverse pipe trick: add context from page title) + // [[ns:page (context)|]] + $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/"; + // [[ns:page(context)|]] (double-width brackets, added in r40257) + $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/"; + // [[ns:page (context), context|]] (using either single or double-width comma) + $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/"; + // [[|page]] (reverse pipe trick: add context from page title) + $p2 = "/\[\[\\|($tc+)]]/"; # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]" $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text ); @@ -4687,13 +4858,13 @@ class Parser { * Do not reuse this parser instance after calling getUserSig(), * as it may have changed if it's the $wgParser. * - * @param $user User - * @param string|bool $nickname nickname to use or false to use user's default nickname - * @param $fancySig Boolean|null whether the nicknname is the complete signature - * or null to use default value + * @param User $user + * @param string|bool $nickname Nickname to use or false to use user's default nickname + * @param bool|null $fancySig whether the nicknname is the complete signature + * or null to use default value * @return string */ - function getUserSig( &$user, $nickname = false, $fancySig = null ) { + public function getUserSig( &$user, $nickname = false, $fancySig = null ) { global $wgMaxSigChars; $username = $user->getName(); @@ -4732,16 +4903,17 @@ class Parser { $nickText = wfEscapeWikiText( $nickname ); $msgName = $user->isAnon() ? 'signature-anon' : 'signature'; - return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()->title( $this->getTitle() )->text(); + return wfMessage( $msgName, $userText, $nickText )->inContentLanguage() + ->title( $this->getTitle() )->text(); } /** * Check that the user's signature contains no bad XML * - * @param $text String - * @return mixed An expanded string, or false if invalid. + * @param string $text + * @return string|bool An expanded string, or false if invalid. */ - function validateSig( $text ) { + public function validateSig( $text ) { return Xml::isWellFormedXmlFragment( $text ) ? $text : false; } @@ -4751,13 +4923,14 @@ class Parser { * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig * 2) Substitute all transclusions * - * @param $text String + * @param string $text * @param bool $parsing Whether we're cleaning (preferences save) or parsing - * @return String: signature text + * @return string Signature text */ public function cleanSig( $text, $parsing = false ) { if ( !$parsing ) { global $wgTitle; + $magicScopeVariable = $this->lock(); $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true ); } @@ -4788,8 +4961,8 @@ class Parser { /** * Strip ~~~, ~~~~ and ~~~~~ out of signatures * - * @param $text String - * @return String: signature text with /~{3,5}/ removed + * @param string $text + * @return string Signature text with /~{3,5}/ removed */ public static function cleanSigInSig( $text ) { $text = preg_replace( '/~{3,5}/', '', $text ); @@ -4800,22 +4973,26 @@ class Parser { * Set up some variables which are usually set up in parse() * so that an external function can call some class members with confidence * - * @param $title Title|null - * @param $options ParserOptions - * @param $outputType - * @param $clearState bool + * @param Title|null $title + * @param ParserOptions $options + * @param int $outputType + * @param bool $clearState */ - public function startExternalParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) { + public function startExternalParse( Title $title = null, ParserOptions $options, + $outputType, $clearState = true + ) { $this->startParse( $title, $options, $outputType, $clearState ); } /** - * @param $title Title|null - * @param $options ParserOptions - * @param $outputType - * @param $clearState bool + * @param Title|null $title + * @param ParserOptions $options + * @param int $outputType + * @param bool $clearState */ - private function startParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) { + private function startParse( Title $title = null, ParserOptions $options, + $outputType, $clearState = true + ) { $this->setTitle( $title ); $this->mOptions = $options; $this->setOutputType( $outputType ); @@ -4827,10 +5004,10 @@ class Parser { /** * Wrapper for preprocess() * - * @param string $text the text to preprocess - * @param $options ParserOptions: options - * @param $title Title object or null to use $wgTitle - * @return String + * @param string $text The text to preprocess + * @param ParserOptions $options Options + * @param Title|null $title Title object or null to use $wgTitle + * @return string */ public function transformMsg( $text, $options, $title = null ) { static $executing = false; @@ -4873,10 +5050,10 @@ class Parser { * this interface, as it is not documented and injudicious use could smash * private variables.** * - * @param $tag Mixed: the tag to use, e.g. 'hook' for "" - * @param $callback Mixed: the callback function (and object) to use for the tag + * @param string $tag The tag to use, e.g. 'hook' for "" + * @param callable $callback The callback function (and object) to use for the tag * @throws MWException - * @return Mixed|null The old value of the mTagHooks array associated with the hook + * @return callable|null The old value of the mTagHooks array associated with the hook */ public function setHook( $tag, $callback ) { $tag = strtolower( $tag ); @@ -4904,12 +5081,12 @@ class Parser { * @since 1.10 * @todo better document or deprecate this * - * @param $tag Mixed: the tag to use, e.g. 'hook' for "" - * @param $callback Mixed: the callback function (and object) to use for the tag + * @param string $tag The tag to use, e.g. 'hook' for "" + * @param callable $callback The callback function (and object) to use for the tag * @throws MWException - * @return Mixed|null The old value of the mTagHooks array associated with the hook + * @return callable|null The old value of the mTagHooks array associated with the hook */ - function setTransparentTagHook( $tag, $callback ) { + public function setTransparentTagHook( $tag, $callback ) { $tag = strtolower( $tag ); if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) { throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" ); @@ -4923,7 +5100,7 @@ class Parser { /** * Remove all tag hooks */ - function clearTagHooks() { + public function clearTagHooks() { $this->mTagHooks = array(); $this->mFunctionTagHooks = array(); $this->mStripList = $this->mDefaultStripList; @@ -4946,8 +5123,8 @@ class Parser { * isHTML The returned text is HTML, armour it against wikitext transformation * * @param string $id The magic word ID - * @param $callback Mixed: the callback function (and object) to use - * @param $flags Integer: a combination of the following flags: + * @param callable $callback The callback function (and object) to use + * @param int $flags A combination of the following flags: * SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}} * * SFH_OBJECT_ARGS Pass the template arguments as PPNode objects instead of text. This @@ -4970,7 +5147,7 @@ class Parser { * about the methods available in PPFrame and PPNode. * * @throws MWException - * @return string|callback The old callback function for this name, if any + * @return string|callable The old callback function for this name, if any */ public function setFunctionHook( $id, $callback, $flags = 0 ) { global $wgContLang; @@ -5008,9 +5185,9 @@ class Parser { /** * Get all registered function hook identifiers * - * @return Array + * @return array */ - function getFunctionHooks() { + public function getFunctionHooks() { return array_keys( $this->mFunctionHooks ); } @@ -5018,13 +5195,13 @@ class Parser { * Create a tag function, e.g. "some stuff". * Unlike tag hooks, tag functions are parsed at preprocessor level. * Unlike parser functions, their content is not preprocessed. - * @param $tag - * @param $callback - * @param $flags + * @param string $tag + * @param callable $callback + * @param int $flags * @throws MWException * @return null */ - function setFunctionTagHook( $tag, $callback, $flags ) { + public function setFunctionTagHook( $tag, $callback, $flags ) { $tag = strtolower( $tag ); if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) { throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" ); @@ -5045,12 +5222,12 @@ class Parser { * Replace "" link placeholders with actual links, in the buffer * Placeholders created in Skin::makeLinkObj() * - * @param $text string - * @param $options int + * @param string $text + * @param int $options * - * @return array of link CSS classes, indexed by PDBK. + * @return array Array of link CSS classes, indexed by PDBK. */ - function replaceLinkHolders( &$text, $options = 0 ) { + public function replaceLinkHolders( &$text, $options = 0 ) { return $this->mLinkHolders->replace( $text ); } @@ -5058,10 +5235,10 @@ class Parser { * Replace "" link placeholders with plain text of links * (not HTML-formatted). * - * @param $text String - * @return String + * @param string $text + * @return string */ - function replaceLinkHoldersText( $text ) { + public function replaceLinkHoldersText( $text ) { return $this->mLinkHolders->replaceText( $text ); } @@ -5078,7 +5255,7 @@ class Parser { * @param array $params * @return string HTML */ - function renderImageGallery( $text, $params ) { + public function renderImageGallery( $text, $params ) { wfProfileIn( __METHOD__ ); $mode = false; @@ -5203,7 +5380,7 @@ class Parser { } else { $localLinkTitle = Title::newFromText( $linkValue ); if ( $localLinkTitle !== null ) { - $link = $localLinkTitle->getLocalURL(); + $link = $localLinkTitle->getLinkURL(); } } break; @@ -5213,7 +5390,7 @@ class Parser { $handlerOptions[$paramName] = $match; } else { // Guess not. Append it to the caption. - wfDebug( "$parameterMatch failed parameter validation" ); + wfDebug( "$parameterMatch failed parameter validation\n" ); $label .= '|' . $parameterMatch; } } @@ -5230,15 +5407,16 @@ class Parser { $ig->add( $title, $label, $alt, $link, $handlerOptions ); } $html = $ig->toHTML(); + wfRunHooks( 'AfterParserFetchFileAndTitle', array( $this, $ig, &$html ) ); wfProfileOut( __METHOD__ ); return $html; } /** - * @param $handler + * @param string $handler * @return array */ - function getImageParams( $handler ) { + public function getImageParams( $handler ) { if ( $handler ) { $handlerClass = get_class( $handler ); } else { @@ -5281,12 +5459,12 @@ class Parser { /** * Parse image options text and use it to make an image * - * @param $title Title - * @param $options String - * @param $holders LinkHolderArray|bool + * @param Title $title + * @param string $options + * @param LinkHolderArray|bool $holders * @return string HTML */ - function makeImage( $title, $options, $holders = false ) { + public function makeImage( $title, $options, $holders = false ) { # Check if the options text is of the form "options|alt text" # Options are: # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang @@ -5336,6 +5514,7 @@ class Parser { $caption = ''; $params = array( 'frame' => array(), 'handler' => array(), 'horizAlign' => array(), 'vertAlign' => array() ); + $seenformat = false; foreach ( $parts as $part ) { $part = trim( $part ); list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part ); @@ -5384,7 +5563,7 @@ class Parser { $paramName = 'no-link'; $value = true; $validated = true; - } elseif ( preg_match( "/^(?i)$prots/", $value ) ) { + } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) { if ( preg_match( "/^((?i)$prots)$chars+$/u", $value, $m ) ) { $paramName = 'link-url'; $this->mOutput->addExternalLink( $value ); @@ -5403,6 +5582,13 @@ class Parser { } } break; + case 'frameless': + case 'framed': + case 'thumbnail': + // use first appearing option, discard others. + $validated = ! $seenformat; + $seenformat = true; + break; default: # Most other things appear to be empty or numeric... $validated = ( $value === false || is_numeric( trim( $value ) ) ); @@ -5430,10 +5616,10 @@ class Parser { $params['frame']['caption'] = $caption; # Will the image be presented in a frame, with the caption below? - $imageIsFramed = isset( $params['frame']['frame'] ) || - isset( $params['frame']['framed'] ) || - isset( $params['frame']['thumbnail'] ) || - isset( $params['frame']['manualthumb'] ); + $imageIsFramed = isset( $params['frame']['frame'] ) + || isset( $params['frame']['framed'] ) + || isset( $params['frame']['thumbnail'] ) + || isset( $params['frame']['manualthumb'] ); # In the old days, [[Image:Foo|text...]] would set alt text. Later it # came to also set the caption, ordinary text after the image -- which @@ -5490,9 +5676,9 @@ class Parser { } /** - * @param $caption - * @param $holders LinkHolderArray - * @return mixed|String + * @param string $caption + * @param LinkHolderArray|bool $holders + * @return mixed|string */ protected function stripAltText( $caption, $holders ) { # Strip bad stuff out of the title (tooltip). We can't just use @@ -5517,7 +5703,7 @@ class Parser { * Set a flag in the output object indicating that the content is dynamic and * shouldn't be cached. */ - function disableCache() { + public function disableCache() { wfDebug( "Parser output marked as uncacheable.\n" ); if ( !$this->mOutput ) { throw new MWException( __METHOD__ . @@ -5531,11 +5717,11 @@ class Parser { * Callback from the Sanitizer for expanding items found in HTML attribute * values, so they can be safely tested and escaped. * - * @param $text String - * @param $frame PPFrame - * @return String + * @param string $text + * @param bool|PPFrame $frame + * @return string */ - function attributeStripCallback( &$text, $frame = false ) { + public function attributeStripCallback( &$text, $frame = false ) { $text = $this->replaceVariables( $text, $frame ); $text = $this->mStripState->unstripBoth( $text ); return $text; @@ -5546,8 +5732,12 @@ class Parser { * * @return array */ - function getTags() { - return array_merge( array_keys( $this->mTransparentTagHooks ), array_keys( $this->mTagHooks ), array_keys( $this->mFunctionTagHooks ) ); + public function getTags() { + return array_merge( + array_keys( $this->mTransparentTagHooks ), + array_keys( $this->mTagHooks ), + array_keys( $this->mFunctionTagHooks ) + ); } /** @@ -5556,11 +5746,11 @@ class Parser { * Transparent tag hooks are like regular XML-style tag hooks, except they * operate late in the transformation sequence, on HTML instead of wikitext. * - * @param $text string + * @param string $text * * @return string */ - function replaceTransparentTags( $text ) { + public function replaceTransparentTags( $text ) { $matches = array(); $elements = array_keys( $this->mTransparentTagHooks ); $text = self::extractTagsAndParams( $elements, $text, $matches, $this->mUniqPrefix ); @@ -5570,7 +5760,10 @@ class Parser { list( $element, $content, $params, $tag ) = $data; $tagName = strtolower( $element ); if ( isset( $this->mTransparentTagHooks[$tagName] ) ) { - $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], array( $content, $params, $this ) ); + $output = call_user_func_array( + $this->mTransparentTagHooks[$tagName], + array( $content, $params, $this ) + ); } else { $output = $tag; } @@ -5586,7 +5779,7 @@ class Parser { * External callers should use the getSection and replaceSection methods. * * @param string $text Page wikitext - * @param string $section a section identifier string of the form: + * @param string|number $sectionId A section identifier string of the form: * " - - ... -
" * * Currently the only recognised flag is "T", which means the target section number @@ -5603,20 +5796,22 @@ class Parser { * string. If $text is the empty string and section 0 is replaced, $newText is * returned. * - * @param string $mode one of "get" or "replace" - * @param string $newText replacement text for section data. - * @return String: for "get", the extracted section text. - * for "replace", the whole page with the section replaced. + * @param string $mode One of "get" or "replace" + * @param string $newText Replacement text for section data. + * @return string For "get", the extracted section text. + * for "replace", the whole page with the section replaced. */ - private function extractSections( $text, $section, $mode, $newText = '' ) { + private function extractSections( $text, $sectionId, $mode, $newText = '' ) { global $wgTitle; # not generally used but removes an ugly failure mode + + $magicScopeVariable = $this->lock(); $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true ); $outText = ''; $frame = $this->getPreprocessor()->newFrame(); # Process section extraction flags $flags = 0; - $sectionParts = explode( '-', $section ); + $sectionParts = explode( '-', $sectionId ); $sectionIndex = array_pop( $sectionParts ); foreach ( $sectionParts as $part ) { if ( $part === 'T' ) { @@ -5724,13 +5919,15 @@ class Parser { * * If a section contains subsections, these are also returned. * - * @param string $text text to look in - * @param string $section section identifier - * @param string $deftext default to return if section is not found - * @return string text of the requested section + * @param string $text Text to look in + * @param string|number $sectionId Section identifier as a number or string + * (e.g. 0, 1 or 'T-1'). + * @param string $defaultText Default to return if section is not found + * + * @return string Text of the requested section */ - public function getSection( $text, $section, $deftext = '' ) { - return $this->extractSections( $text, $section, "get", $deftext ); + public function getSection( $text, $sectionId, $defaultText = '' ) { + return $this->extractSections( $text, $sectionId, 'get', $defaultText ); } /** @@ -5738,30 +5935,33 @@ class Parser { * specified by $section has been replaced with $text. If the target * section does not exist, $oldtext is returned unchanged. * - * @param string $oldtext former text of the article - * @param int $section section identifier - * @param string $text replacing text - * @return String: modified text + * @param string $oldText Former text of the article + * @param string|number $sectionId Section identifier as a number or string + * (e.g. 0, 1 or 'T-1'). + * @param string $newText Replacing text + * + * @return string Modified text */ - public function replaceSection( $oldtext, $section, $text ) { - return $this->extractSections( $oldtext, $section, "replace", $text ); + public function replaceSection( $oldText, $sectionId, $newText ) { + return $this->extractSections( $oldText, $sectionId, 'replace', $newText ); } /** * Get the ID of the revision we are parsing * - * @return Mixed: integer or null + * @return int|null */ - function getRevisionId() { + public function getRevisionId() { return $this->mRevisionId; } /** * Get the revision object for $this->mRevisionId * - * @return Revision|null either a Revision object or null + * @return Revision|null Either a Revision object or null + * @since 1.23 (public since 1.23) */ - protected function getRevisionObject() { + public function getRevisionObject() { if ( !is_null( $this->mRevisionObject ) ) { return $this->mRevisionObject; } @@ -5776,8 +5976,9 @@ class Parser { /** * Get the timestamp associated with the current revision, adjusted for * the default server-local timestamp + * @return string */ - function getRevisionTimestamp() { + public function getRevisionTimestamp() { if ( is_null( $this->mRevisionTimestamp ) ) { wfProfileIn( __METHOD__ ); @@ -5802,9 +6003,9 @@ class Parser { /** * Get the name of the user that edited the last revision * - * @return String: user name + * @return string User name */ - function getRevisionUser() { + public function getRevisionUser() { if ( is_null( $this->mRevisionUser ) ) { $revObject = $this->getRevisionObject(); @@ -5822,9 +6023,9 @@ class Parser { /** * Get the size of the revision * - * @return int|null revision size + * @return int|null Revision size */ - function getRevisionSize() { + public function getRevisionSize() { if ( is_null( $this->mRevisionSize ) ) { $revObject = $this->getRevisionObject(); @@ -5872,7 +6073,7 @@ class Parser { * Accessor for $mDefaultSort * Unlike getDefaultSort(), will return false if none is set * - * @return string or false + * @return string|bool */ public function getCustomDefaultSort() { return $this->mDefaultSort; @@ -5883,7 +6084,7 @@ class Parser { * presumably extracted from a heading, for example "Header" from * "== Header ==". * - * @param $text string + * @param string $text * * @return string */ @@ -5919,7 +6120,7 @@ class Parser { * to create valid section anchors by mimicing the output of the * parser when headings are parsed. * - * @param string $text text string to be stripped of wikitext + * @param string $text Text string to be stripped of wikitext * for use in a Section anchor * @return string Filtered text string */ @@ -5930,7 +6131,7 @@ class Parser { # Strip external link markup # @todo FIXME: Not tolerant to blank link text - # I.E. [http://www.mediawiki.org] will render as [1] or something depending + # I.E. [https://www.mediawiki.org] will render as [1] or something depending # on how many empty links there are on the page - need to figure that out. $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text ); @@ -5945,14 +6146,15 @@ class Parser { /** * strip/replaceVariables/unstrip for preprocessor regression testing * - * @param $text string - * @param $title Title - * @param $options ParserOptions - * @param $outputType int + * @param string $text + * @param Title $title + * @param ParserOptions $options + * @param int $outputType * * @return string */ - function testSrvus( $text, Title $title, ParserOptions $options, $outputType = self::OT_HTML ) { + public function testSrvus( $text, Title $title, ParserOptions $options, $outputType = self::OT_HTML ) { + $magicScopeVariable = $this->lock(); $this->startParse( $title, $options, $outputType, true ); $text = $this->replaceVariables( $text ); @@ -5962,22 +6164,22 @@ class Parser { } /** - * @param $text string - * @param $title Title - * @param $options ParserOptions + * @param string $text + * @param Title $title + * @param ParserOptions $options * @return string */ - function testPst( $text, Title $title, ParserOptions $options ) { + public function testPst( $text, Title $title, ParserOptions $options ) { return $this->preSaveTransform( $text, $title, $options->getUser(), $options ); } /** - * @param $text - * @param $title Title - * @param $options ParserOptions + * @param string $text + * @param Title $title + * @param ParserOptions $options * @return string */ - function testPreprocess( $text, Title $title, ParserOptions $options ) { + public function testPreprocess( $text, Title $title, ParserOptions $options ) { return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS ); } @@ -5992,12 +6194,12 @@ class Parser { * two strings will be replaced with the value returned by the callback in * each case. * - * @param $s string - * @param $callback + * @param string $s + * @param callable $callback * * @return string */ - function markerSkipCallback( $s, $callback ) { + public function markerSkipCallback( $s, $callback ) { $i = 0; $out = ''; while ( $i < strlen( $s ) ) { @@ -6024,10 +6226,10 @@ class Parser { /** * Remove any strip markers found in the given text. * - * @param $text Input string + * @param string $text Input string * @return string */ - function killMarkers( $text ) { + public function killMarkers( $text ) { return $this->mStripState->killMarkers( $text ); } @@ -6043,11 +6245,11 @@ class Parser { * unserializeHalfParsedText(). The text can then be safely incorporated into * the return value of a parser hook. * - * @param $text string + * @param string $text * * @return array */ - function serializeHalfParsedText( $text ) { + public function serializeHalfParsedText( $text ) { wfProfileIn( __METHOD__ ); $data = array( 'text' => $text, @@ -6072,9 +6274,9 @@ class Parser { * * @param array $data Serialized data * @throws MWException - * @return String + * @return string */ - function unserializeHalfParsedText( $data ) { + public function unserializeHalfParsedText( $data ) { if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) { throw new MWException( __METHOD__ . ': invalid version' ); } @@ -6095,18 +6297,18 @@ class Parser { * serializeHalfParsedText(), is compatible with the current version of the * parser. * - * @param $data Array + * @param array $data * * @return bool */ - function isValidHalfParsedText( $data ) { + public function isValidHalfParsedText( $data ) { return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION; } /** * Parsed a width param of imagelink like 300px or 200x300px * - * @param $value String + * @param string $value * * @return array * @since 1.20 @@ -6130,4 +6332,68 @@ class Parser { } return $parsedWidthParam; } + + /** + * Lock the current instance of the parser. + * + * This is meant to stop someone from calling the parser + * recursively and messing up all the strip state. + * + * @throws MWException If parser is in a parse + * @return ScopedCallback The lock will be released once the return value goes out of scope. + */ + protected function lock() { + if ( $this->mInParse ) { + throw new MWException( "Parser state cleared while parsing. " + . "Did you call Parser::parse recursively?" ); + } + $this->mInParse = true; + + $that = $this; + $recursiveCheck = new ScopedCallback( function() use ( $that ) { + $that->mInParse = false; + } ); + + return $recursiveCheck; + } + + /** + * Strip outer

tag from the HTML source of a single paragraph. + * + * Returns original HTML if the

tag has any attributes, if there's no wrapping

tag, + * or if there is more than one

tag in the input HTML. + * + * @param string $html + * @return string + * @since 1.24 + */ + public static function stripOuterParagraph( $html ) { + $m = array(); + if ( preg_match( '/^

(.*)\n?<\/p>\n?$/sU', $html, $m ) ) { + if ( strpos( $m[1], '

' ) === false ) { + $html = $m[1]; + } + } + + return $html; + } + + /** + * Return this parser if it is not doing anything, otherwise + * get a fresh parser. You can use this method by doing + * $myParser = $wgParser->getFreshParser(), or more simply + * $wgParser->getFreshParser()->parse( ... ); + * if you're unsure if $wgParser is safe to use. + * + * @since 1.24 + * @return Parser A parser object that is not parsing anything + */ + public function getFreshParser() { + global $wgParserConf; + if ( $this->mInParse ) { + return new $wgParserConf['class']( $wgParserConf ); + } else { + return $this; + } + } } diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php index 7053f134..79523003 100644 --- a/includes/parser/ParserCache.php +++ b/includes/parser/ParserCache.php @@ -26,9 +26,8 @@ * @todo document */ class ParserCache { + /** @var MWMemcached */ private $mMemc; - const try116cache = false; /* Only useful $wgParserCacheExpireTime after updating to 1.17 */ - /** * Get an instance of this object * @@ -47,7 +46,7 @@ class ParserCache { * Setup a cache pathway with a given back-end storage mechanism. * May be a memcached client or a BagOStuff derivative. * - * @param $memCached Object + * @param MWMemcached $memCached * @throws MWException */ protected function __construct( $memCached ) { @@ -58,8 +57,8 @@ class ParserCache { } /** - * @param $article Article - * @param $hash string + * @param Article $article + * @param string $hash * @return mixed|string */ protected function getParserOutputKey( $article, $hash ) { @@ -74,7 +73,7 @@ class ParserCache { } /** - * @param $article Article + * @param Article $article * @return mixed|string */ protected function getOptionsKey( $article ) { @@ -92,11 +91,11 @@ class ParserCache { * English preferences. That's why we take into account *all* user * options. (r70809 CR) * - * @param $article Article - * @param $popts ParserOptions + * @param Article $article + * @param ParserOptions $popts * @return string */ - function getETag( $article, $popts ) { + public function getETag( $article, $popts ) { return 'W/"' . $this->getParserOutputKey( $article, $popts->optionsHash( ParserOptions::legacyOptions(), $article->getTitle() ) ) . "--" . $article->getTouched() . '"'; @@ -104,8 +103,8 @@ class ParserCache { /** * Retrieve the ParserOutput from ParserCache, even if it's outdated. - * @param $article Article - * @param $popts ParserOptions + * @param Article $article + * @param ParserOptions $popts * @return ParserOutput|bool False on failure */ public function getDirty( $article, $popts ) { @@ -114,15 +113,22 @@ class ParserCache { } /** - * Used to provide a unique id for the PoolCounter. + * Generates a key for caching the given article considering + * the given parser options. + * + * @note Which parser options influence the cache key + * is controlled via ParserOutput::recordOption() or + * ParserOptions::addExtraKey(). + * + * @note Used by Article to provide a unique id for the PoolCounter. * It would be preferable to have this code in get() * instead of having Article looking in our internals. * * @todo Document parameter $useOutdated * - * @param $article Article - * @param $popts ParserOptions - * @param $useOutdated Boolean (default true) + * @param Article $article + * @param ParserOptions $popts + * @param bool $useOutdated (default true) * @return bool|mixed|string */ public function getKey( $article, $popts, $useOutdated = true ) { @@ -139,29 +145,40 @@ class ParserCache { if ( !$useOutdated && $optionsKey->expired( $article->getTouched() ) ) { wfIncrStats( "pcache_miss_expired" ); $cacheTime = $optionsKey->getCacheTime(); - wfDebug( "Parser options key expired, touched " . $article->getTouched() . ", epoch $wgCacheEpoch, cached $cacheTime\n" ); + wfDebug( "Parser options key expired, touched " . $article->getTouched() + . ", epoch $wgCacheEpoch, cached $cacheTime\n" ); + return false; + } elseif ( $optionsKey->isDifferentRevision( $article->getLatest() ) ) { + wfIncrStats( "pcache_miss_revid" ); + $revId = $article->getLatest(); + $cachedRevId = $optionsKey->getCacheRevisionId(); + wfDebug( "ParserOutput key is for an old revision, latest $revId, cached $cachedRevId\n" ); return false; } + // $optionsKey->mUsedOptions is set by save() by calling ParserOutput::getUsedOptions() $usedOptions = $optionsKey->mUsedOptions; wfDebug( "Parser cache options found.\n" ); } else { - if ( !$useOutdated && !self::try116cache ) { + if ( !$useOutdated ) { return false; } $usedOptions = ParserOptions::legacyOptions(); } - return $this->getParserOutputKey( $article, $popts->optionsHash( $usedOptions, $article->getTitle() ) ); + return $this->getParserOutputKey( + $article, + $popts->optionsHash( $usedOptions, $article->getTitle() ) + ); } /** * Retrieve the ParserOutput from ParserCache. * false if not found or outdated. * - * @param $article Article - * @param $popts ParserOptions - * @param $useOutdated Boolean (default false) + * @param Article $article + * @param ParserOptions $popts + * @param bool $useOutdated (default false) * * @return ParserOutput|bool False on failure */ @@ -186,12 +203,6 @@ class ParserCache { } $value = $this->mMemc->get( $parserOutputKey ); - if ( self::try116cache && !$value && strpos( $value, '*' ) !== -1 ) { - wfDebug( "New format parser cache miss.\n" ); - $parserOutputKey = $this->getParserOutputKey( $article, - $popts->optionsHash( ParserOptions::legacyOptions(), $article->getTitle() ) ); - $value = $this->mMemc->get( $parserOutputKey ); - } if ( !$value ) { wfDebug( "ParserOutput cache miss.\n" ); wfIncrStats( "pcache_miss_absent" ); @@ -209,7 +220,14 @@ class ParserCache { if ( !$useOutdated && $value->expired( $touched ) ) { wfIncrStats( "pcache_miss_expired" ); $cacheTime = $value->getCacheTime(); - wfDebug( "ParserOutput key expired, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" ); + wfDebug( "ParserOutput key expired, touched $touched, " + . "epoch $wgCacheEpoch, cached $cacheTime\n" ); + $value = false; + } elseif ( $value->isDifferentRevision( $article->getLatest() ) ) { + wfIncrStats( "pcache_miss_revid" ); + $revId = $article->getLatest(); + $cachedRevId = $value->getCacheRevisionId(); + wfDebug( "ParserOutput key is for an old revision, latest $revId, cached $cachedRevId\n" ); $value = false; } else { wfIncrStats( "pcache_hit" ); @@ -220,15 +238,20 @@ class ParserCache { } /** - * @param $parserOutput ParserOutput - * @param $article Article - * @param $popts ParserOptions - * @param $cacheTime Time when the cache was generated + * @param ParserOutput $parserOutput + * @param WikiPage $page + * @param ParserOptions $popts + * @param string $cacheTime Time when the cache was generated + * @param int $revId Revision ID that was parsed */ - public function save( $parserOutput, $article, $popts, $cacheTime = null ) { + public function save( $parserOutput, $page, $popts, $cacheTime = null, $revId = null ) { $expire = $parserOutput->getCacheExpiry(); if ( $expire > 0 ) { $cacheTime = $cacheTime ?: wfTimestampNow(); + if ( !$revId ) { + $revision = $page->getRevision(); + $revId = $revision ? $revision->getId() : null; + } $optionsKey = new CacheTime; $optionsKey->mUsedOptions = $parserOutput->getUsedOptions(); @@ -236,23 +259,30 @@ class ParserCache { $optionsKey->setCacheTime( $cacheTime ); $parserOutput->setCacheTime( $cacheTime ); + $optionsKey->setCacheRevisionId( $revId ); + $parserOutput->setCacheRevisionId( $revId ); $optionsKey->setContainsOldMagic( $parserOutput->containsOldMagic() ); - $parserOutputKey = $this->getParserOutputKey( $article, - $popts->optionsHash( $optionsKey->mUsedOptions, $article->getTitle() ) ); + $parserOutputKey = $this->getParserOutputKey( $page, + $popts->optionsHash( $optionsKey->mUsedOptions, $page->getTitle() ) ); // Save the timestamp so that we don't have to load the revision row on view - $parserOutput->setTimestamp( $article->getTimestamp() ); + $parserOutput->setTimestamp( $page->getTimestamp() ); + + $msg = "Saved in parser cache with key $parserOutputKey" . + " and timestamp $cacheTime" . + " and revision id $revId" . + "\n"; - $parserOutput->mText .= "\n\n"; - wfDebug( "Saved in parser cache with key $parserOutputKey and timestamp $cacheTime\n" ); + $parserOutput->mText .= "\n\n"; + wfDebug( $msg ); // Save the parser output $this->mMemc->set( $parserOutputKey, $parserOutput, $expire ); // ...and its pointer - $this->mMemc->set( $this->getOptionsKey( $article ), $optionsKey, $expire ); + $this->mMemc->set( $this->getOptionsKey( $page ), $optionsKey, $expire ); } else { wfDebug( "Parser output was marked as uncacheable and has not been saved.\n" ); } diff --git a/includes/parser/ParserDiffTest.php b/includes/parser/ParserDiffTest.php new file mode 100644 index 00000000..174c1d61 --- /dev/null +++ b/includes/parser/ParserDiffTest.php @@ -0,0 +1,143 @@ +conf = $conf; + } + + public function init() { + if ( !is_null( $this->parsers ) ) { + return; + } + + global $wgHooks; + static $doneHook = false; + if ( !$doneHook ) { + $doneHook = true; + $wgHooks['ParserClearState'][] = array( $this, 'onClearState' ); + } + if ( isset( $this->conf['shortOutput'] ) ) { + $this->shortOutput = $this->conf['shortOutput']; + } + + foreach ( $this->conf['parsers'] as $i => $parserConf ) { + if ( !is_array( $parserConf ) ) { + $class = $parserConf; + $parserConf = array( 'class' => $parserConf ); + } else { + $class = $parserConf['class']; + } + $this->parsers[$i] = new $class( $parserConf ); + } + } + + public function __call( $name, $args ) { + $this->init(); + $results = array(); + $mismatch = false; + $lastResult = null; + $first = true; + foreach ( $this->parsers as $i => $parser ) { + $currentResult = call_user_func_array( array( &$this->parsers[$i], $name ), $args ); + if ( $first ) { + $first = false; + } else { + if ( is_object( $lastResult ) ) { + if ( $lastResult != $currentResult ) { + $mismatch = true; + } + } else { + if ( $lastResult !== $currentResult ) { + $mismatch = true; + } + } + } + $results[$i] = $currentResult; + $lastResult = $currentResult; + } + if ( $mismatch ) { + if ( count( $results ) == 2 ) { + $resultsList = array(); + foreach ( $this->parsers as $i => $parser ) { + $resultsList[] = var_export( $results[$i], true ); + } + $diff = wfDiff( $resultsList[0], $resultsList[1] ); + } else { + $diff = '[too many parsers]'; + } + $msg = "ParserDiffTest: results mismatch on call to $name\n"; + if ( !$this->shortOutput ) { + $msg .= 'Arguments: ' . $this->formatArray( $args ) . "\n"; + } + $msg .= 'Results: ' . $this->formatArray( $results ) . "\n" . + "Diff: $diff\n"; + throw new MWException( $msg ); + } + return $lastResult; + } + + public function formatArray( $array ) { + if ( $this->shortOutput ) { + foreach ( $array as $key => $value ) { + if ( $value instanceof ParserOutput ) { + $array[$key] = "ParserOutput: {$value->getText()}"; + } + } + } + return var_export( $array, true ); + } + + public function setFunctionHook( $id, $callback, $flags = 0 ) { + $this->init(); + foreach ( $this->parsers as $parser ) { + $parser->setFunctionHook( $id, $callback, $flags ); + } + } + + /** + * @param Parser $parser + * @return bool + */ + public function onClearState( &$parser ) { + // hack marker prefixes to get identical output + if ( !isset( $this->dtUniqPrefix ) ) { + $this->dtUniqPrefix = $parser->uniqPrefix(); + } else { + $parser->mUniqPrefix = $this->dtUniqPrefix; + } + return true; + } +} diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php index e12f32d8..7e4059b8 100644 --- a/includes/parser/ParserOptions.php +++ b/includes/parser/ParserOptions.php @@ -22,9 +22,10 @@ */ /** - * \brief Set options of the Parser + * @brief Set options of the Parser * - * All member variables are supposed to be private in theory, although in practise this is not the case. + * All member variables are supposed to be private in theory, although in + * practise this is not the case. * * @ingroup Parser */ @@ -33,108 +34,108 @@ class ParserOptions { /** * Interlanguage links are removed and returned in an array */ - var $mInterwikiMagic; + public $mInterwikiMagic; /** * Allow external images inline? */ - var $mAllowExternalImages; + public $mAllowExternalImages; /** * If not, any exception? */ - var $mAllowExternalImagesFrom; + public $mAllowExternalImagesFrom; /** * If not or it doesn't match, should we check an on-wiki whitelist? */ - var $mEnableImageWhitelist; + public $mEnableImageWhitelist; /** * Date format index */ - var $mDateFormat = null; + public $mDateFormat = null; /** * Create "edit section" links? */ - var $mEditSection = true; + public $mEditSection = true; /** * Allow inclusion of special pages? */ - var $mAllowSpecialInclusion; + public $mAllowSpecialInclusion; /** * Use tidy to cleanup output HTML? */ - var $mTidy = false; + public $mTidy = false; /** * Which lang to call for PLURAL and GRAMMAR */ - var $mInterfaceMessage = false; + public $mInterfaceMessage = false; /** * Overrides $mInterfaceMessage with arbitrary language */ - var $mTargetLanguage = null; + public $mTargetLanguage = null; /** * Maximum size of template expansions, in bytes */ - var $mMaxIncludeSize; + public $mMaxIncludeSize; /** * Maximum number of nodes touched by PPFrame::expand() */ - var $mMaxPPNodeCount; + public $mMaxPPNodeCount; /** * Maximum number of nodes generated by Preprocessor::preprocessToObj() */ - var $mMaxGeneratedPPNodeCount; + public $mMaxGeneratedPPNodeCount; /** * Maximum recursion depth in PPFrame::expand() */ - var $mMaxPPExpandDepth; + public $mMaxPPExpandDepth; /** * Maximum recursion depth for templates within templates */ - var $mMaxTemplateDepth; + public $mMaxTemplateDepth; /** * Maximum number of calls per parse to expensive parser functions */ - var $mExpensiveParserFunctionLimit; + public $mExpensiveParserFunctionLimit; /** * Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS */ - var $mRemoveComments = true; + public $mRemoveComments = true; /** * Callback for template fetching. Used as first argument to call_user_func(). */ - var $mTemplateCallback = + public $mTemplateCallback = array( 'Parser', 'statelessFetchTemplate' ); /** * Enable limit report in an HTML comment on output */ - var $mEnableLimitReport = false; + public $mEnableLimitReport = false; /** * Timestamp used for {{CURRENTDAY}} etc. */ - var $mTimestamp; + public $mTimestamp; /** * Target attribute for external links */ - var $mExternalLinkTarget; + public $mExternalLinkTarget; /** * Clean up signature texts? @@ -142,37 +143,32 @@ class ParserOptions { * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures * 2) Substitute all transclusions */ - var $mCleanSignatures; + public $mCleanSignatures; /** * Transform wiki markup when saving the page? */ - var $mPreSaveTransform = true; + public $mPreSaveTransform = true; /** * Whether content conversion should be disabled */ - var $mDisableContentConversion; + public $mDisableContentConversion; /** * Whether title conversion should be disabled */ - var $mDisableTitleConversion; + public $mDisableTitleConversion; /** * Automatically number headings? */ - var $mNumberHeadings; - - /** - * User math preference (as integer). Not used (1.19) - */ - var $mMath; + public $mNumberHeadings; /** * Thumb size preferred by the user. */ - var $mThumbSize; + public $mThumbSize; /** * Maximum article size of an article to be marked as "stub" @@ -182,90 +178,176 @@ class ParserOptions { /** * Language object of the User language. */ - var $mUserLang; + public $mUserLang; /** * @var User * Stored user object */ - var $mUser; + public $mUser; /** * Parsing the page for a "preview" operation? */ - var $mIsPreview = false; + public $mIsPreview = false; /** * Parsing the page for a "preview" operation on a single section? */ - var $mIsSectionPreview = false; + public $mIsSectionPreview = false; /** * Parsing the printable version of the page? */ - var $mIsPrintable = false; + public $mIsPrintable = false; /** * Extra key that should be present in the caching key. */ - var $mExtraKey = ''; + public $mExtraKey = ''; /** * Function to be called when an option is accessed. */ protected $onAccessCallback = null; - function getInterwikiMagic() { return $this->mInterwikiMagic; } - function getAllowExternalImages() { return $this->mAllowExternalImages; } - function getAllowExternalImagesFrom() { return $this->mAllowExternalImagesFrom; } - function getEnableImageWhitelist() { return $this->mEnableImageWhitelist; } - function getEditSection() { return $this->mEditSection; } - function getNumberHeadings() { $this->optionUsed( 'numberheadings' ); - return $this->mNumberHeadings; } - function getAllowSpecialInclusion() { return $this->mAllowSpecialInclusion; } - function getTidy() { return $this->mTidy; } - function getInterfaceMessage() { return $this->mInterfaceMessage; } - function getTargetLanguage() { return $this->mTargetLanguage; } - function getMaxIncludeSize() { return $this->mMaxIncludeSize; } - function getMaxPPNodeCount() { return $this->mMaxPPNodeCount; } - function getMaxGeneratedPPNodeCount() { return $this->mMaxGeneratedPPNodeCount; } - function getMaxPPExpandDepth() { return $this->mMaxPPExpandDepth; } - function getMaxTemplateDepth() { return $this->mMaxTemplateDepth; } + /** + * If the page being parsed is a redirect, this should hold the redirect + * target. + * @var Title|null + */ + private $redirectTarget = null; + + public function getInterwikiMagic() { + return $this->mInterwikiMagic; + } + + public function getAllowExternalImages() { + return $this->mAllowExternalImages; + } + + public function getAllowExternalImagesFrom() { + return $this->mAllowExternalImagesFrom; + } + + public function getEnableImageWhitelist() { + return $this->mEnableImageWhitelist; + } + + public function getEditSection() { + return $this->mEditSection; + } + + public function getNumberHeadings() { + $this->optionUsed( 'numberheadings' ); + + return $this->mNumberHeadings; + } + + public function getAllowSpecialInclusion() { + return $this->mAllowSpecialInclusion; + } + + public function getTidy() { + return $this->mTidy; + } + + public function getInterfaceMessage() { + return $this->mInterfaceMessage; + } + + public function getTargetLanguage() { + return $this->mTargetLanguage; + } + + public function getMaxIncludeSize() { + return $this->mMaxIncludeSize; + } + + public function getMaxPPNodeCount() { + return $this->mMaxPPNodeCount; + } + + public function getMaxGeneratedPPNodeCount() { + return $this->mMaxGeneratedPPNodeCount; + } + + public function getMaxPPExpandDepth() { + return $this->mMaxPPExpandDepth; + } + + public function getMaxTemplateDepth() { + return $this->mMaxTemplateDepth; + } + /* @since 1.20 */ - function getExpensiveParserFunctionLimit() { return $this->mExpensiveParserFunctionLimit; } - function getRemoveComments() { return $this->mRemoveComments; } - function getTemplateCallback() { return $this->mTemplateCallback; } - function getEnableLimitReport() { return $this->mEnableLimitReport; } - function getCleanSignatures() { return $this->mCleanSignatures; } - function getExternalLinkTarget() { return $this->mExternalLinkTarget; } - function getDisableContentConversion() { return $this->mDisableContentConversion; } - function getDisableTitleConversion() { return $this->mDisableTitleConversion; } - /** @deprecated since 1.22 use User::getOption('math') instead */ - function getMath() { $this->optionUsed( 'math' ); - return $this->mMath; } - function getThumbSize() { $this->optionUsed( 'thumbsize' ); - return $this->mThumbSize; } - function getStubThreshold() { $this->optionUsed( 'stubthreshold' ); - return $this->mStubThreshold; } - - function getIsPreview() { return $this->mIsPreview; } - function getIsSectionPreview() { return $this->mIsSectionPreview; } - function getIsPrintable() { $this->optionUsed( 'printable' ); - return $this->mIsPrintable; } - function getUser() { return $this->mUser; } - function getPreSaveTransform() { return $this->mPreSaveTransform; } - - /** - * @param $title Title - * @return Skin - * @deprecated since 1.18 Use Linker::* instead - */ - function getSkin( $title = null ) { - wfDeprecated( __METHOD__, '1.18' ); - return new DummyLinker; - } - - function getDateFormat() { + public function getExpensiveParserFunctionLimit() { + return $this->mExpensiveParserFunctionLimit; + } + + public function getRemoveComments() { + return $this->mRemoveComments; + } + + public function getTemplateCallback() { + return $this->mTemplateCallback; + } + + public function getEnableLimitReport() { + return $this->mEnableLimitReport; + } + + public function getCleanSignatures() { + return $this->mCleanSignatures; + } + + public function getExternalLinkTarget() { + return $this->mExternalLinkTarget; + } + + public function getDisableContentConversion() { + return $this->mDisableContentConversion; + } + + public function getDisableTitleConversion() { + return $this->mDisableTitleConversion; + } + + public function getThumbSize() { + $this->optionUsed( 'thumbsize' ); + + return $this->mThumbSize; + } + + public function getStubThreshold() { + $this->optionUsed( 'stubthreshold' ); + + return $this->mStubThreshold; + } + + public function getIsPreview() { + return $this->mIsPreview; + } + + public function getIsSectionPreview() { + return $this->mIsSectionPreview; + } + + public function getIsPrintable() { + $this->optionUsed( 'printable' ); + + return $this->mIsPrintable; + } + + public function getUser() { + return $this->mUser; + } + + public function getPreSaveTransform() { + return $this->mPreSaveTransform; + } + + public function getDateFormat() { $this->optionUsed( 'dateformat' ); if ( !isset( $this->mDateFormat ) ) { $this->mDateFormat = $this->mUser->getDatePreference(); @@ -273,7 +355,7 @@ class ParserOptions { return $this->mDateFormat; } - function getTimestamp() { + public function getTimestamp() { if ( !isset( $this->mTimestamp ) ) { $this->mTimestamp = wfTimestampNow(); } @@ -293,10 +375,10 @@ class ParserOptions { * * {{int: }} uses this which used to produce inconsistent link tables (bug 14404). * - * @return Language object + * @return Language * @since 1.19 */ - function getUserLangObj() { + public function getUserLangObj() { $this->optionUsed( 'userlang' ); return $this->mUserLang; } @@ -304,70 +386,180 @@ class ParserOptions { /** * Same as getUserLangObj() but returns a string instead. * - * @return String Language code + * @return string Language code * @since 1.17 */ - function getUserLang() { + public function getUserLang() { return $this->getUserLangObj()->getCode(); } - function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); } - function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); } - function setAllowExternalImagesFrom( $x ) { return wfSetVar( $this->mAllowExternalImagesFrom, $x ); } - function setEnableImageWhitelist( $x ) { return wfSetVar( $this->mEnableImageWhitelist, $x ); } - function setDateFormat( $x ) { return wfSetVar( $this->mDateFormat, $x ); } - function setEditSection( $x ) { return wfSetVar( $this->mEditSection, $x ); } - function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); } - function setAllowSpecialInclusion( $x ) { return wfSetVar( $this->mAllowSpecialInclusion, $x ); } - function setTidy( $x ) { return wfSetVar( $this->mTidy, $x ); } - - /** @deprecated in 1.19 */ - function setSkin( $x ) { wfDeprecated( __METHOD__, '1.19' ); } - function setInterfaceMessage( $x ) { return wfSetVar( $this->mInterfaceMessage, $x ); } - function setTargetLanguage( $x ) { return wfSetVar( $this->mTargetLanguage, $x, true ); } - function setMaxIncludeSize( $x ) { return wfSetVar( $this->mMaxIncludeSize, $x ); } - function setMaxPPNodeCount( $x ) { return wfSetVar( $this->mMaxPPNodeCount, $x ); } - function setMaxGeneratedPPNodeCount( $x ) { return wfSetVar( $this->mMaxGeneratedPPNodeCount, $x ); } - function setMaxTemplateDepth( $x ) { return wfSetVar( $this->mMaxTemplateDepth, $x ); } + public function setInterwikiMagic( $x ) { + return wfSetVar( $this->mInterwikiMagic, $x ); + } + + public function setAllowExternalImages( $x ) { + return wfSetVar( $this->mAllowExternalImages, $x ); + } + + public function setAllowExternalImagesFrom( $x ) { + return wfSetVar( $this->mAllowExternalImagesFrom, $x ); + } + + public function setEnableImageWhitelist( $x ) { + return wfSetVar( $this->mEnableImageWhitelist, $x ); + } + + public function setDateFormat( $x ) { + return wfSetVar( $this->mDateFormat, $x ); + } + + public function setEditSection( $x ) { + return wfSetVar( $this->mEditSection, $x ); + } + + public function setNumberHeadings( $x ) { + return wfSetVar( $this->mNumberHeadings, $x ); + } + + public function setAllowSpecialInclusion( $x ) { + return wfSetVar( $this->mAllowSpecialInclusion, $x ); + } + + public function setTidy( $x ) { + return wfSetVar( $this->mTidy, $x ); + } + + public function setInterfaceMessage( $x ) { + return wfSetVar( $this->mInterfaceMessage, $x ); + } + + public function setTargetLanguage( $x ) { + return wfSetVar( $this->mTargetLanguage, $x, true ); + } + + public function setMaxIncludeSize( $x ) { + return wfSetVar( $this->mMaxIncludeSize, $x ); + } + + public function setMaxPPNodeCount( $x ) { + return wfSetVar( $this->mMaxPPNodeCount, $x ); + } + + public function setMaxGeneratedPPNodeCount( $x ) { + return wfSetVar( $this->mMaxGeneratedPPNodeCount, $x ); + } + + public function setMaxTemplateDepth( $x ) { + return wfSetVar( $this->mMaxTemplateDepth, $x ); + } + /* @since 1.20 */ - function setExpensiveParserFunctionLimit( $x ) { return wfSetVar( $this->mExpensiveParserFunctionLimit, $x ); } - function setRemoveComments( $x ) { return wfSetVar( $this->mRemoveComments, $x ); } - function setTemplateCallback( $x ) { return wfSetVar( $this->mTemplateCallback, $x ); } - function enableLimitReport( $x = true ) { return wfSetVar( $this->mEnableLimitReport, $x ); } - function setTimestamp( $x ) { return wfSetVar( $this->mTimestamp, $x ); } - function setCleanSignatures( $x ) { return wfSetVar( $this->mCleanSignatures, $x ); } - function setExternalLinkTarget( $x ) { return wfSetVar( $this->mExternalLinkTarget, $x ); } - function disableContentConversion( $x = true ) { return wfSetVar( $this->mDisableContentConversion, $x ); } - function disableTitleConversion( $x = true ) { return wfSetVar( $this->mDisableTitleConversion, $x ); } - /** @deprecated since 1.22 */ - function setMath( $x ) { return wfSetVar( $this->mMath, $x ); } - function setUserLang( $x ) { + public function setExpensiveParserFunctionLimit( $x ) { + return wfSetVar( $this->mExpensiveParserFunctionLimit, $x ); + } + + public function setRemoveComments( $x ) { + return wfSetVar( $this->mRemoveComments, $x ); + } + + public function setTemplateCallback( $x ) { + return wfSetVar( $this->mTemplateCallback, $x ); + } + + public function enableLimitReport( $x = true ) { + return wfSetVar( $this->mEnableLimitReport, $x ); + } + + public function setTimestamp( $x ) { + return wfSetVar( $this->mTimestamp, $x ); + } + + public function setCleanSignatures( $x ) { + return wfSetVar( $this->mCleanSignatures, $x ); + } + + public function setExternalLinkTarget( $x ) { + return wfSetVar( $this->mExternalLinkTarget, $x ); + } + + public function disableContentConversion( $x = true ) { + return wfSetVar( $this->mDisableContentConversion, $x ); + } + + public function disableTitleConversion( $x = true ) { + return wfSetVar( $this->mDisableTitleConversion, $x ); + } + + public function setUserLang( $x ) { if ( is_string( $x ) ) { $x = Language::factory( $x ); } + return wfSetVar( $this->mUserLang, $x ); } - function setThumbSize( $x ) { return wfSetVar( $this->mThumbSize, $x ); } - function setStubThreshold( $x ) { return wfSetVar( $this->mStubThreshold, $x ); } - function setPreSaveTransform( $x ) { return wfSetVar( $this->mPreSaveTransform, $x ); } - function setIsPreview( $x ) { return wfSetVar( $this->mIsPreview, $x ); } - function setIsSectionPreview( $x ) { return wfSetVar( $this->mIsSectionPreview, $x ); } - function setIsPrintable( $x ) { return wfSetVar( $this->mIsPrintable, $x ); } + public function setThumbSize( $x ) { + return wfSetVar( $this->mThumbSize, $x ); + } + + public function setStubThreshold( $x ) { + return wfSetVar( $this->mStubThreshold, $x ); + } + + public function setPreSaveTransform( $x ) { + return wfSetVar( $this->mPreSaveTransform, $x ); + } + + public function setIsPreview( $x ) { + return wfSetVar( $this->mIsPreview, $x ); + } + + public function setIsSectionPreview( $x ) { + return wfSetVar( $this->mIsSectionPreview, $x ); + } + + public function setIsPrintable( $x ) { + return wfSetVar( $this->mIsPrintable, $x ); + } + + /** + * Set the redirect target. + * + * Note that setting or changing this does not *make* the page a redirect + * or change its target, it merely records the information for reference + * during the parse. + * + * @since 1.24 + * @param Title|null $title + */ + function setRedirectTarget( $title ) { + $this->redirectTarget = $title; + } + + /** + * Get the previously-set redirect target. + * + * @since 1.24 + * @return Title|null + */ + function getRedirectTarget() { + return $this->redirectTarget; + } /** * Extra key that should be present in the parser cache key. + * @param string $key */ - function addExtraKey( $key ) { + public function addExtraKey( $key ) { $this->mExtraKey .= '!' . $key; } /** * Constructor - * @param $user User object - * @param $lang Language object + * @param User $user + * @param Language $lang */ - function __construct( $user = null, $lang = null ) { + public function __construct( $user = null, $lang = null ) { if ( $user === null ) { global $wgUser; if ( $wgUser === null ) { @@ -390,8 +582,8 @@ class ParserOptions { * Get a ParserOptions object from a given user. * Language will be taken from $wgLang. * - * @param $user User object - * @return ParserOptions object + * @param User $user + * @return ParserOptions */ public static function newFromUser( $user ) { return new ParserOptions( $user ); @@ -400,9 +592,9 @@ class ParserOptions { /** * Get a ParserOptions object from a given user and language * - * @param $user User object - * @param $lang Language object - * @return ParserOptions object + * @param User $user + * @param Language $lang + * @return ParserOptions */ public static function newFromUserAndLang( User $user, Language $lang ) { return new ParserOptions( $user, $lang ); @@ -411,8 +603,8 @@ class ParserOptions { /** * Get a ParserOptions object from a IContextSource object * - * @param $context IContextSource object - * @return ParserOptions object + * @param IContextSource $context + * @return ParserOptions */ public static function newFromContext( IContextSource $context ) { return new ParserOptions( $context->getUser(), $context->getLanguage() ); @@ -421,8 +613,8 @@ class ParserOptions { /** * Get user options * - * @param $user User object - * @param $lang Language object + * @param User $user + * @param Language $lang */ private function initialiseFromUser( $user, $lang ) { global $wgInterwikiMagic, $wgAllowExternalImages, @@ -451,7 +643,6 @@ class ParserOptions { $this->mUser = $user; $this->mNumberHeadings = $user->getOption( 'numberheadings' ); - $this->mMath = $user->getOption( 'math' ); $this->mThumbSize = $user->getOption( 'thumbsize' ); $this->mStubThreshold = $user->getStubThreshold(); $this->mUserLang = $lang; @@ -462,15 +653,17 @@ class ParserOptions { /** * Registers a callback for tracking which ParserOptions which are used. * This is a private API with the parser. + * @param callable $callback */ - function registerWatcher( $callback ) { + public function registerWatcher( $callback ) { $this->onAccessCallback = $callback; } /** * Called when an option is accessed. + * @param string $optionName Name of the option */ - protected function optionUsed( $optionName ) { + public function optionUsed( $optionName ) { if ( $this->onAccessCallback ) { call_user_func( $this->onAccessCallback, $optionName ); } @@ -483,37 +676,39 @@ class ParserOptions { * @return array */ public static function legacyOptions() { - return array( 'math', 'stubthreshold', 'numberheadings', 'userlang', 'thumbsize', 'editsection', 'printable' ); + return array( + 'stubthreshold', + 'numberheadings', + 'userlang', + 'thumbsize', + 'editsection', + 'printable' + ); } /** * Generate a hash string with the values set on these ParserOptions * for the keys given in the array. * This will be used as part of the hash key for the parser cache, - * so users sharign the options with vary for the same page share + * so users sharing the options with vary for the same page share * the same cached data safely. * - * Replaces User::getPageRenderingHash() - * * Extensions which require it should install 'PageRenderingHash' hook, * which will give them a chance to modify this key based on their own * settings. * * @since 1.17 - * @param $forOptions Array - * @param $title Title: used to get the content language of the page (since r97636) + * @param array $forOptions + * @param Title $title Used to get the content language of the page (since r97636) * @return string Page rendering hash */ public function optionsHash( $forOptions, $title = null ) { global $wgRenderHashAppend; - $confstr = ''; - - if ( in_array( 'math', $forOptions ) ) { - $confstr .= $this->mMath; - } else { - $confstr .= '*'; - } + // FIXME: Once the cache key is reorganized this argument + // can be dropped. It was used when the math extension was + // part of core. + $confstr = '*'; // Space assigned for the stubthreshold but unused // since it disables the parser cache, its value will always @@ -573,7 +768,7 @@ class ParserOptions { // Give a chance for extensions to modify the hash, if they have // extra options or other effects on the parser cache. - wfRunHooks( 'PageRenderingHash', array( &$confstr ) ); + wfRunHooks( 'PageRenderingHash', array( &$confstr, $this->getUser(), &$forOptions ) ); // Make it a valid memcached key fragment $confstr = str_replace( ' ', '_', $confstr ); diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php index 460f3211..5037ce18 100644 --- a/includes/parser/ParserOutput.php +++ b/includes/parser/ParserOutput.php @@ -22,7 +22,7 @@ * @ingroup Parser */ class ParserOutput extends CacheTime { - var $mText, # The output text + public $mText, # The output text $mLanguageLinks, # List of the full text of language links, in the order they appear $mCategories, # Map of category names to sort keys $mTitleText, # title text of the chosen language variant @@ -41,6 +41,7 @@ class ParserOutput extends CacheTime { $mModuleScripts = array(), # Modules of which only the JS will be loaded by the resource loader $mModuleStyles = array(), # Modules of which only the CSSS will be loaded by the resource loader $mModuleMessages = array(), # Modules of which only the messages will be loaded by the resource loader + $mJsConfigVars = array(), # JavaScript config variable for mw.config combined with this page $mOutputHooks = array(), # Hook tags as per $wgParserOutputHooks $mWarnings = array(), # Warning text to be returned to the user. Wikitext formatted, in the key only $mSections = array(), # Table of contents @@ -49,19 +50,20 @@ class ParserOutput extends CacheTime { $mTOCHTML = '', # HTML of the TOC $mTimestamp, # Timestamp of the revision $mTOCEnabled = true; # Whether TOC should be shown, can't override __NOTOC__ - private $mIndexPolicy = ''; # 'index' or 'noindex'? Any other value will result in no change. - private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys) - private $mSecondaryDataUpdates = array(); # List of DataUpdate, used to save info from the page somewhere else. - private $mExtensionData = array(); # extra data used by extensions - private $mLimitReportData = array(); # Parser limit report data - private $mParseStartTime = array(); # Timestamps for getTimeSinceStart() - private $mPreventClickjacking = false; # Whether to emit X-Frame-Options: DENY - - const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)())#'; - - function __construct( $text = '', $languageLinks = array(), $categoryLinks = array(), - $containsOldMagic = false, $titletext = '' ) - { + private $mIndexPolicy = ''; # 'index' or 'noindex'? Any other value will result in no change. + private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys) + private $mSecondaryDataUpdates = array(); # List of DataUpdate, used to save info from the page somewhere else. + private $mExtensionData = array(); # extra data used by extensions + private $mLimitReportData = array(); # Parser limit report data + private $mParseStartTime = array(); # Timestamps for getTimeSinceStart() + private $mPreventClickjacking = false; # Whether to emit X-Frame-Options: DENY + + const EDITSECTION_REGEX = + '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)())#'; + + public function __construct( $text = '', $languageLinks = array(), $categoryLinks = array(), + $containsOldMagic = false, $titletext = '' + ) { $this->mText = $text; $this->mLanguageLinks = $languageLinks; $this->mCategories = $categoryLinks; @@ -69,12 +71,31 @@ class ParserOutput extends CacheTime { $this->mTitleText = $titletext; } - function getText() { + public function getText() { wfProfileIn( __METHOD__ ); $text = $this->mText; if ( $this->mEditSectionTokens ) { - $text = preg_replace_callback( ParserOutput::EDITSECTION_REGEX, - array( &$this, 'replaceEditSectionLinksCallback' ), $text ); + $text = preg_replace_callback( + ParserOutput::EDITSECTION_REGEX, + function ( $m ) { + global $wgOut, $wgLang; + $editsectionPage = Title::newFromText( htmlspecialchars_decode( $m[1] ) ); + $editsectionSection = htmlspecialchars_decode( $m[2] ); + $editsectionContent = isset( $m[4] ) ? $m[3] : null; + + if ( !is_object( $editsectionPage ) ) { + throw new MWException( "Bad parser output text." ); + } + + $skin = $wgOut->getSkin(); + return call_user_func_array( + array( $skin, 'doEditSectionLink' ), + array( $editsectionPage, $editsectionSection, + $editsectionContent, $wgLang->getCode() ) + ); + }, + $text + ); } else { $text = preg_replace( ParserOutput::EDITSECTION_REGEX, '', $text ); } @@ -84,7 +105,7 @@ class ParserOutput extends CacheTime { $text = str_replace( array( Parser::TOC_START, Parser::TOC_END ), '', $text ); } else { $text = preg_replace( - '#'. preg_quote( Parser::TOC_START ) . '.*?' . preg_quote( Parser::TOC_END ) . '#s', + '#' . preg_quote( Parser::TOC_START ) . '.*?' . preg_quote( Parser::TOC_END ) . '#s', '', $text ); @@ -93,97 +114,192 @@ class ParserOutput extends CacheTime { return $text; } - /** - * callback used by getText to replace editsection tokens - * @private - * @param $m - * @throws MWException - * @return mixed - */ - function replaceEditSectionLinksCallback( $m ) { - global $wgOut, $wgLang; - $args = array( - htmlspecialchars_decode( $m[1] ), - htmlspecialchars_decode( $m[2] ), - isset( $m[4] ) ? $m[3] : null, - ); - $args[0] = Title::newFromText( $args[0] ); - if ( !is_object( $args[0] ) ) { - throw new MWException( "Bad parser output text." ); - } - $args[] = $wgLang->getCode(); - $skin = $wgOut->getSkin(); - return call_user_func_array( array( $skin, 'doEditSectionLink' ), $args ); - } - - function &getLanguageLinks() { return $this->mLanguageLinks; } - function getInterwikiLinks() { return $this->mInterwikiLinks; } - function getCategoryLinks() { return array_keys( $this->mCategories ); } - function &getCategories() { return $this->mCategories; } - function getTitleText() { return $this->mTitleText; } - function getSections() { return $this->mSections; } - function getEditSectionTokens() { return $this->mEditSectionTokens; } - function &getLinks() { return $this->mLinks; } - function &getTemplates() { return $this->mTemplates; } - function &getTemplateIds() { return $this->mTemplateIds; } - function &getImages() { return $this->mImages; } - function &getFileSearchOptions() { return $this->mFileSearchOptions; } - function &getExternalLinks() { return $this->mExternalLinks; } - function getNoGallery() { return $this->mNoGallery; } - function getHeadItems() { return $this->mHeadItems; } - function getModules() { return $this->mModules; } - function getModuleScripts() { return $this->mModuleScripts; } - function getModuleStyles() { return $this->mModuleStyles; } - function getModuleMessages() { return $this->mModuleMessages; } - function getOutputHooks() { return (array)$this->mOutputHooks; } - function getWarnings() { return array_keys( $this->mWarnings ); } - function getIndexPolicy() { return $this->mIndexPolicy; } - function getTOCHTML() { return $this->mTOCHTML; } - function getTimestamp() { return $this->mTimestamp; } - function getLimitReportData() { return $this->mLimitReportData; } - function getTOCEnabled() { return $this->mTOCEnabled; } - - function setText( $text ) { return wfSetVar( $this->mText, $text ); } - function setLanguageLinks( $ll ) { return wfSetVar( $this->mLanguageLinks, $ll ); } - function setCategoryLinks( $cl ) { return wfSetVar( $this->mCategories, $cl ); } - - function setTitleText( $t ) { return wfSetVar( $this->mTitleText, $t ); } - function setSections( $toc ) { return wfSetVar( $this->mSections, $toc ); } - function setEditSectionTokens( $t ) { return wfSetVar( $this->mEditSectionTokens, $t ); } - function setIndexPolicy( $policy ) { return wfSetVar( $this->mIndexPolicy, $policy ); } - function setTOCHTML( $tochtml ) { return wfSetVar( $this->mTOCHTML, $tochtml ); } - function setTimestamp( $timestamp ) { return wfSetVar( $this->mTimestamp, $timestamp ); } - function setTOCEnabled( $flag ) { return wfSetVar( $this->mTOCEnabled, $flag ); } - - function addCategory( $c, $sort ) { $this->mCategories[$c] = $sort; } - function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; } - function addWarning( $s ) { $this->mWarnings[$s] = 1; } - - function addOutputHook( $hook, $data = false ) { + public function &getLanguageLinks() { + return $this->mLanguageLinks; + } + + public function getInterwikiLinks() { + return $this->mInterwikiLinks; + } + + public function getCategoryLinks() { + return array_keys( $this->mCategories ); + } + + public function &getCategories() { + return $this->mCategories; + } + + public function getTitleText() { + return $this->mTitleText; + } + + public function getSections() { + return $this->mSections; + } + + public function getEditSectionTokens() { + return $this->mEditSectionTokens; + } + + public function &getLinks() { + return $this->mLinks; + } + + public function &getTemplates() { + return $this->mTemplates; + } + + public function &getTemplateIds() { + return $this->mTemplateIds; + } + + public function &getImages() { + return $this->mImages; + } + + public function &getFileSearchOptions() { + return $this->mFileSearchOptions; + } + + public function &getExternalLinks() { + return $this->mExternalLinks; + } + + public function getNoGallery() { + return $this->mNoGallery; + } + + public function getHeadItems() { + return $this->mHeadItems; + } + + public function getModules() { + return $this->mModules; + } + + public function getModuleScripts() { + return $this->mModuleScripts; + } + + public function getModuleStyles() { + return $this->mModuleStyles; + } + + public function getModuleMessages() { + return $this->mModuleMessages; + } + + /** @since 1.23 */ + public function getJsConfigVars() { + return $this->mJsConfigVars; + } + + public function getOutputHooks() { + return (array)$this->mOutputHooks; + } + + public function getWarnings() { + return array_keys( $this->mWarnings ); + } + + public function getIndexPolicy() { + return $this->mIndexPolicy; + } + + public function getTOCHTML() { + return $this->mTOCHTML; + } + + public function getTimestamp() { + return $this->mTimestamp; + } + + public function getLimitReportData() { + return $this->mLimitReportData; + } + + public function getTOCEnabled() { + return $this->mTOCEnabled; + } + + public function setText( $text ) { + return wfSetVar( $this->mText, $text ); + } + + public function setLanguageLinks( $ll ) { + return wfSetVar( $this->mLanguageLinks, $ll ); + } + + public function setCategoryLinks( $cl ) { + return wfSetVar( $this->mCategories, $cl ); + } + + public function setTitleText( $t ) { + return wfSetVar( $this->mTitleText, $t ); + } + + public function setSections( $toc ) { + return wfSetVar( $this->mSections, $toc ); + } + + public function setEditSectionTokens( $t ) { + return wfSetVar( $this->mEditSectionTokens, $t ); + } + + public function setIndexPolicy( $policy ) { + return wfSetVar( $this->mIndexPolicy, $policy ); + } + + public function setTOCHTML( $tochtml ) { + return wfSetVar( $this->mTOCHTML, $tochtml ); + } + + public function setTimestamp( $timestamp ) { + return wfSetVar( $this->mTimestamp, $timestamp ); + } + + public function setTOCEnabled( $flag ) { + return wfSetVar( $this->mTOCEnabled, $flag ); + } + + public function addCategory( $c, $sort ) { + $this->mCategories[$c] = $sort; + } + + public function addLanguageLink( $t ) { + $this->mLanguageLinks[] = $t; + } + + public function addWarning( $s ) { + $this->mWarnings[$s] = 1; + } + + public function addOutputHook( $hook, $data = false ) { $this->mOutputHooks[] = array( $hook, $data ); } - function setNewSection( $value ) { + public function setNewSection( $value ) { $this->mNewSection = (bool)$value; } - function hideNewSection( $value ) { + public function hideNewSection( $value ) { $this->mHideNewSection = (bool)$value; } - function getHideNewSection() { + public function getHideNewSection() { return (bool)$this->mHideNewSection; } - function getNewSection() { + public function getNewSection() { return (bool)$this->mNewSection; } /** * Checks, if a url is pointing to the own server * - * @param string $internal the server to check against - * @param string $url the url to check + * @param string $internal The server to check against + * @param string $url The url to check * @return bool */ - static function isLinkInternal( $internal, $url ) { + public static function isLinkInternal( $internal, $url ) { return (bool)preg_match( '/^' . # If server is proto relative, check also for http/https links ( substr( $internal, 0, 2 ) === '//' ? '(?:https?:)?' : '' ) . @@ -194,7 +310,7 @@ class ParserOutput extends CacheTime { ); } - function addExternalLink( $url ) { + public function addExternalLink( $url ) { # We don't register links pointing to our own server, unless... :-) global $wgServer, $wgRegisterInternalExternals; @@ -210,10 +326,10 @@ class ParserOutput extends CacheTime { /** * Record a local or interwiki inline link for saving in future link tables. * - * @param $title Title object - * @param $id Mixed: optional known page_id so we can skip the lookup + * @param Title $title + * @param int|null $id Optional known page_id so we can skip the lookup */ - function addLink( Title $title, $id = null ) { + public function addLink( Title $title, $id = null ) { if ( $title->isExternal() ) { // Don't record interwikis in pagelinks $this->addInterwikiLink( $title ); @@ -245,10 +361,10 @@ class ParserOutput extends CacheTime { * Register a file dependency for this output * @param string $name Title dbKey * @param string $timestamp MW timestamp of file creation (or false if non-existing) - * @param string $sha1 base 36 SHA-1 of file (or false if non-existing) + * @param string $sha1 Base 36 SHA-1 of file (or false if non-existing) * @return void */ - function addImage( $name, $timestamp = null, $sha1 = null ) { + public function addImage( $name, $timestamp = null, $sha1 = null ) { $this->mImages[$name] = 1; if ( $timestamp !== null && $sha1 !== null ) { $this->mFileSearchOptions[$name] = array( 'time' => $timestamp, 'sha1' => $sha1 ); @@ -257,12 +373,12 @@ class ParserOutput extends CacheTime { /** * Register a template dependency for this output - * @param $title Title - * @param $page_id - * @param $rev_id + * @param Title $title + * @param int $page_id + * @param int $rev_id * @return void */ - function addTemplate( $title, $page_id, $rev_id ) { + public function addTemplate( $title, $page_id, $rev_id ) { $ns = $title->getNamespace(); $dbk = $title->getDBkey(); if ( !isset( $this->mTemplates[$ns] ) ) { @@ -276,14 +392,14 @@ class ParserOutput extends CacheTime { } /** - * @param $title Title object, must be an interwiki link - * @throws MWException if given invalid input + * @param Title $title Title object, must be an interwiki link + * @throws MWException If given invalid input */ - function addInterwikiLink( $title ) { - $prefix = $title->getInterwiki(); - if ( $prefix == '' ) { + public function addInterwikiLink( $title ) { + if ( !$title->isExternal() ) { throw new MWException( 'Non-interwiki link passed, internal parser error.' ); } + $prefix = $title->getInterwiki(); if ( !isset( $this->mInterwikiLinks[$prefix] ) ) { $this->mInterwikiLinks[$prefix] = array(); } @@ -294,8 +410,10 @@ class ParserOutput extends CacheTime { * Add some text to the "". * If $tag is set, the section with that tag will only be included once * in a given page. + * @param string $section + * @param string|bool $tag */ - function addHeadItem( $section, $tag = false ) { + public function addHeadItem( $section, $tag = false ) { if ( $tag !== false ) { $this->mHeadItems[$tag] = $section; } else { @@ -319,16 +437,35 @@ class ParserOutput extends CacheTime { $this->mModuleMessages = array_merge( $this->mModuleMessages, (array)$modules ); } + /** + * Add one or more variables to be set in mw.config in JavaScript. + * + * @param string|array $keys Key or array of key/value pairs. + * @param mixed $value [optional] Value of the configuration variable. + * @since 1.23 + */ + public function addJsConfigVars( $keys, $value = null ) { + if ( is_array( $keys ) ) { + foreach ( $keys as $key => $value ) { + $this->mJsConfigVars[$key] = $value; + } + return; + } + + $this->mJsConfigVars[$keys] = $value; + } + /** * Copy items from the OutputPage object into this one * - * @param $out OutputPage object + * @param OutputPage $out */ public function addOutputPageMetadata( OutputPage $out ) { $this->addModules( $out->getModules() ); $this->addModuleScripts( $out->getModuleScripts() ); $this->addModuleStyles( $out->getModuleStyles() ); $this->addModuleMessages( $out->getModuleMessages() ); + $this->addJsConfigVars( $out->getJsConfigVars() ); $this->mHeadItems = array_merge( $this->mHeadItems, $out->getHeadItemsArray() ); $this->mPreventClickjacking = $this->mPreventClickjacking || $out->getPreventClickjacking(); @@ -339,7 +476,7 @@ class ParserOutput extends CacheTime { * -- this is assumed to have been validated * (check equal normalisation, etc.) * - * @param string $text desired title text + * @param string $text Desired title text */ public function setDisplayTitle( $text ) { $this->setTitleText( $text ); @@ -349,7 +486,7 @@ class ParserOutput extends CacheTime { /** * Get the title to be used for display * - * @return String + * @return string */ public function getDisplayTitle() { $t = $this->getTitleText(); @@ -361,6 +498,7 @@ class ParserOutput extends CacheTime { /** * Fairly generic flag setter thingy. + * @param string $flag */ public function setFlag( $flag ) { $this->mFlags[$flag] = true; @@ -378,6 +516,9 @@ class ParserOutput extends CacheTime { * retrieved given the page ID or via a DB join when given the page * title. * + * Since 1.23, page_props are also indexed by numeric value, to allow + * for efficient "top k" queries of pages wrt a given property. + * * setProperty() is thus used to propagate properties from the parsed * page to request contexts other than a page view of the currently parsed * article. @@ -395,10 +536,10 @@ class ParserOutput extends CacheTime { * Wikimedia Commons. * This is not actually implemented, yet but would be pretty cool. * - * @note: Do not use setProperty() to set a property which is only used + * @note Do not use setProperty() to set a property which is only used * in a context where the ParserOutput object itself is already available, * for example a normal page view. There is no need to save such a property - * in the database since it the text is already parsed. You can just hook + * in the database since the text is already parsed. You can just hook * OutputPageParserOutput and get your data out of the ParserOutput object. * * If you are writing an extension where you want to set a property in the @@ -431,10 +572,22 @@ class ParserOutput extends CacheTime { $this->mProperties[$name] = $value; } + /** + * @param string $name The property name to look up. + * + * @return mixed|bool The value previously set using setProperty(). False if null or no value + * was set for the given property name. + * + * @note You need to use getProperties() to check for boolean and null properties. + */ public function getProperty( $name ) { return isset( $this->mProperties[$name] ) ? $this->mProperties[$name] : false; } + public function unsetProperty( $name ) { + unset( $this->mProperties[$name] ); + } + public function getProperties() { if ( !isset( $this->mProperties ) ) { $this->mProperties = array(); @@ -445,7 +598,7 @@ class ParserOutput extends CacheTime { /** * Returns the options from its ParserOptions which have been taken * into account to produce this output or false if not available. - * @return mixed Array + * @return array */ public function getUsedOptions() { if ( !isset( $this->mAccessedOptions ) ) { @@ -455,16 +608,24 @@ class ParserOutput extends CacheTime { } /** - * Callback passed by the Parser to the ParserOptions to keep track of which options are used. - * @access private + * Tags a parser option for use in the cache key for this parser output. + * Registered as a watcher at ParserOptions::registerWatcher() by Parser::clearState(). + * + * @see ParserCache::getKey + * @see ParserCache::save + * @see ParserOptions::addExtraKey + * @see ParserOptions::optionsHash + * @param string $option */ - function recordOption( $option ) { + public function recordOption( $option ) { $this->mAccessedOptions[$option] = true; } /** - * Adds an update job to the output. Any update jobs added to the output will eventually bexecuted in order to - * store any secondary information extracted from the page's content. + * Adds an update job to the output. Any update jobs added to the output will + * eventually be executed in order to store any secondary information extracted + * from the page's content. This is triggered by calling getSecondaryDataUpdates() + * and is used for forward links updates on edit and backlink updates by jobs. * * @since 1.20 * @@ -479,16 +640,16 @@ class ParserOutput extends CacheTime { * extracted from the page's content, including a LinksUpdate object for all links stored in * this ParserOutput object. * - * @note: Avoid using this method directly, use ContentHandler::getSecondaryDataUpdates() instead! The content - * handler may provide additional update objects. + * @note Avoid using this method directly, use ContentHandler::getSecondaryDataUpdates() + * instead! The content handler may provide additional update objects. * * @since 1.20 * - * @param $title Title The title of the page we're updating. If not given, a title object will be created - * based on $this->getTitleText() - * @param $recursive Boolean: queue jobs for recursive updates? + * @param Title $title The title of the page we're updating. If not given, a title object will + * be created based on $this->getTitleText() + * @param bool $recursive Queue jobs for recursive updates? * - * @return Array. An array of instances of DataUpdate + * @return array An array of instances of DataUpdate */ public function getSecondaryDataUpdates( Title $title = null, $recursive = true ) { if ( is_null( $title ) ) { @@ -535,11 +696,10 @@ class ParserOutput extends CacheTime { * @since 1.21 * * @param string $key The key for accessing the data. Extensions should take care to avoid - * conflicts in naming keys. It is suggested to use the extension's name as a - * prefix. + * conflicts in naming keys. It is suggested to use the extension's name as a prefix. * * @param mixed $value The value to set. Setting a value to null is equivalent to removing - * the value. + * the value. */ public function setExtensionData( $key, $value ) { if ( $value === null ) { @@ -557,7 +717,7 @@ class ParserOutput extends CacheTime { * * @param string $key The key to look up. * - * @return mixed The value previously set for the given key using setExtensionData( $key ), + * @return mixed|null The value previously set for the given key using setExtensionData() * or null if no value was set for this key. */ public function getExtensionData( $key ) { @@ -573,10 +733,12 @@ class ParserOutput extends CacheTime { if ( !$clock || $clock === 'wall' ) { $ret['wall'] = microtime( true ); } - if ( ( !$clock || $clock === 'cpu' ) && function_exists( 'getrusage' ) ) { - $ru = getrusage(); - $ret['cpu'] = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6; - $ret['cpu'] += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6; + if ( !$clock || $clock === 'cpu' ) { + $ru = wfGetRusage(); + if ( $ru ) { + $ret['cpu'] = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6; + $ret['cpu'] += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6; + } } return $ret; } @@ -585,7 +747,7 @@ class ParserOutput extends CacheTime { * Resets the parse start timestamps for future calls to getTimeSinceStart() * @since 1.22 */ - function resetParseStartTime() { + public function resetParseStartTime() { $this->mParseStartTime = self::getTimes(); } @@ -600,7 +762,7 @@ class ParserOutput extends CacheTime { * @param string $clock * @return float|null */ - function getTimeSinceStart( $clock ) { + public function getTimeSinceStart( $clock ) { if ( !isset( $this->mParseStartTime[$clock] ) ) { return null; } @@ -628,7 +790,7 @@ class ParserOutput extends CacheTime { * @param string $key Message key * @param mixed $value Appropriate for Message::params() */ - function setLimitReportData( $key, $value ) { + public function setLimitReportData( $key, $value ) { $this->mLimitReportData[$key] = $value; } @@ -636,10 +798,21 @@ class ParserOutput extends CacheTime { * Get or set the prevent-clickjacking flag * * @since 1.24 - * @param boolean|null $flag New flag value, or null to leave it unchanged - * @return boolean Old flag value + * @param bool|null $flag New flag value, or null to leave it unchanged + * @return bool Old flag value */ public function preventClickjacking( $flag = null ) { return wfSetVar( $this->mPreventClickjacking, $flag ); } + + /** + * Save space for for serialization by removing useless values + * @return array + */ + public function __sleep() { + return array_diff( + array_keys( get_object_vars( $this ) ), + array( 'mSecondaryDataUpdates', 'mParseStartTime' ) + ); + } } diff --git a/includes/parser/Parser_DiffTest.php b/includes/parser/Parser_DiffTest.php deleted file mode 100644 index aeae234a..00000000 --- a/includes/parser/Parser_DiffTest.php +++ /dev/null @@ -1,143 +0,0 @@ -conf = $conf; - } - - function init() { - if ( !is_null( $this->parsers ) ) { - return; - } - - global $wgHooks; - static $doneHook = false; - if ( !$doneHook ) { - $doneHook = true; - $wgHooks['ParserClearState'][] = array( $this, 'onClearState' ); - } - if ( isset( $this->conf['shortOutput'] ) ) { - $this->shortOutput = $this->conf['shortOutput']; - } - - foreach ( $this->conf['parsers'] as $i => $parserConf ) { - if ( !is_array( $parserConf ) ) { - $class = $parserConf; - $parserConf = array( 'class' => $parserConf ); - } else { - $class = $parserConf['class']; - } - $this->parsers[$i] = new $class( $parserConf ); - } - } - - function __call( $name, $args ) { - $this->init(); - $results = array(); - $mismatch = false; - $lastResult = null; - $first = true; - foreach ( $this->parsers as $i => $parser ) { - $currentResult = call_user_func_array( array( &$this->parsers[$i], $name ), $args ); - if ( $first ) { - $first = false; - } else { - if ( is_object( $lastResult ) ) { - if ( $lastResult != $currentResult ) { - $mismatch = true; - } - } else { - if ( $lastResult !== $currentResult ) { - $mismatch = true; - } - } - } - $results[$i] = $currentResult; - $lastResult = $currentResult; - } - if ( $mismatch ) { - if ( count( $results ) == 2 ) { - $resultsList = array(); - foreach ( $this->parsers as $i => $parser ) { - $resultsList[] = var_export( $results[$i], true ); - } - $diff = wfDiff( $resultsList[0], $resultsList[1] ); - } else { - $diff = '[too many parsers]'; - } - $msg = "Parser_DiffTest: results mismatch on call to $name\n"; - if ( !$this->shortOutput ) { - $msg .= 'Arguments: ' . $this->formatArray( $args ) . "\n"; - } - $msg .= 'Results: ' . $this->formatArray( $results ) . "\n" . - "Diff: $diff\n"; - throw new MWException( $msg ); - } - return $lastResult; - } - - function formatArray( $array ) { - if ( $this->shortOutput ) { - foreach ( $array as $key => $value ) { - if ( $value instanceof ParserOutput ) { - $array[$key] = "ParserOutput: {$value->getText()}"; - } - } - } - return var_export( $array, true ); - } - - function setFunctionHook( $id, $callback, $flags = 0 ) { - $this->init(); - foreach ( $this->parsers as $parser ) { - $parser->setFunctionHook( $id, $callback, $flags ); - } - } - - /** - * @param $parser Parser - * @return bool - */ - function onClearState( &$parser ) { - // hack marker prefixes to get identical output - if ( !isset( $this->dtUniqPrefix ) ) { - $this->dtUniqPrefix = $parser->uniqPrefix(); - } else { - $parser->mUniqPrefix = $this->dtUniqPrefix; - } - return true; - } -} diff --git a/includes/parser/Preprocessor.php b/includes/parser/Preprocessor.php index aeacd2e1..b32593c9 100644 --- a/includes/parser/Preprocessor.php +++ b/includes/parser/Preprocessor.php @@ -28,42 +28,44 @@ interface Preprocessor { /** * Create a new preprocessor object based on an initialised Parser object * - * @param $parser Parser + * @param Parser $parser */ - function __construct( $parser ); + public function __construct( $parser ); /** * Create a new top-level frame for expansion of a page * * @return PPFrame */ - function newFrame(); + public function newFrame(); /** - * Create a new custom frame for programmatic use of parameter replacement as used in some extensions + * Create a new custom frame for programmatic use of parameter replacement + * as used in some extensions. * - * @param $args array + * @param array $args * * @return PPFrame */ - function newCustomFrame( $args ); + public function newCustomFrame( $args ); /** - * Create a new custom node for programmatic use of parameter replacement as used in some extensions + * Create a new custom node for programmatic use of parameter replacement + * as used in some extensions. * - * @param $values + * @param array $values */ - function newPartNodeArray( $values ); + public function newPartNodeArray( $values ); /** * Preprocess text to a PPNode * - * @param $text - * @param $flags + * @param string $text + * @param int $flags * * @return PPNode */ - function preprocessToObj( $text, $flags = 0 ); + public function preprocessToObj( $text, $flags = 0 ); } /** @@ -75,8 +77,9 @@ interface PPFrame { const STRIP_COMMENTS = 4; const NO_IGNORE = 8; const RECOVER_COMMENTS = 16; + const NO_TAGS = 32; - const RECOVER_ORIG = 27; // = 1|2|8|16 no constant expression support in PHP yet + const RECOVER_ORIG = 59; // = 1|2|8|16|32 no constant expression support in PHP yet /** This constant exists when $indexOffset is supported in newChild() */ const SUPPORTS_INDEX_OFFSET = 1; @@ -84,87 +87,168 @@ interface PPFrame { /** * Create a child frame * - * @param array $args - * @param Title $title + * @param array|bool $args + * @param bool|Title $title * @param int $indexOffset A number subtracted from the index attributes of the arguments * * @return PPFrame */ - function newChild( $args = false, $title = false, $indexOffset = 0 ); + public function newChild( $args = false, $title = false, $indexOffset = 0 ); + + /** + * Expand a document tree node, caching the result on its parent with the given key + * @param string|int $key + * @param string|PPNode $root + * @param int $flags + * @return string + */ + public function cachedExpand( $key, $root, $flags = 0 ); /** * Expand a document tree node + * @param string|PPNode $root + * @param int $flags + * @return string */ - function expand( $root, $flags = 0 ); + public function expand( $root, $flags = 0 ); /** * Implode with flags for expand() + * @param string $sep + * @param int $flags + * @param string|PPNode $args,... + * @return string */ - function implodeWithFlags( $sep, $flags /*, ... */ ); + public function implodeWithFlags( $sep, $flags /*, ... */ ); /** * Implode with no flags specified + * @param string $sep + * @param string|PPNode $args,... + * @return string */ - function implode( $sep /*, ... */ ); + public function implode( $sep /*, ... */ ); /** * Makes an object that, when expand()ed, will be the same as one obtained * with implode() + * @param string $sep + * @param string|PPNode $args,... + * @return PPNode */ - function virtualImplode( $sep /*, ... */ ); + public function virtualImplode( $sep /*, ... */ ); /** * Virtual implode with brackets + * @param string $start + * @param string $sep + * @param string $end + * @param string|PPNode $args,... + * @return PPNode */ - function virtualBracketedImplode( $start, $sep, $end /*, ... */ ); + public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ); /** * Returns true if there are no arguments in this frame * * @return bool */ - function isEmpty(); + public function isEmpty(); /** * Returns all arguments of this frame + * @return array */ - function getArguments(); + public function getArguments(); /** * Returns all numbered arguments of this frame + * @return array */ - function getNumberedArguments(); + public function getNumberedArguments(); /** * Returns all named arguments of this frame + * @return array */ - function getNamedArguments(); + public function getNamedArguments(); /** * Get an argument to this frame by name + * @param string $name + * @return bool */ - function getArgument( $name ); + public function getArgument( $name ); /** * Returns true if the infinite loop check is OK, false if a loop is detected * - * @param $title - * + * @param Title $title * @return bool */ - function loopCheck( $title ); + public function loopCheck( $title ); /** * Return true if the frame is a template frame + * @return bool */ - function isTemplate(); + public function isTemplate(); + + /** + * Set the "volatile" flag. + * + * Note that this is somewhat of a "hack" in order to make extensions + * with side effects (such as Cite) work with the PHP parser. New + * extensions should be written in a way that they do not need this + * function, because other parsers (such as Parsoid) are not guaranteed + * to respect it, and it may be removed in the future. + * + * @param bool $flag + */ + public function setVolatile( $flag = true ); + + /** + * Get the "volatile" flag. + * + * Callers should avoid caching the result of an expansion if it has the + * volatile flag set. + * + * @see self::setVolatile() + * @return bool + */ + public function isVolatile(); + + /** + * Get the TTL of the frame's output. + * + * This is the maximum amount of time, in seconds, that this frame's + * output should be cached for. A value of null indicates that no + * maximum has been specified. + * + * Note that this TTL only applies to caching frames as parts of pages. + * It is not relevant to caching the entire rendered output of a page. + * + * @return int|null + */ + public function getTTL(); + + /** + * Set the TTL of the output of this frame and all of its ancestors. + * Has no effect if the new TTL is greater than the one already set. + * Note that it is the caller's responsibility to change the cache + * expiry of the page as a whole, if such behavior is desired. + * + * @see self::getTTL() + * @param int $ttl + */ + public function setTTL( $ttl ); /** * Get a title of frame * * @return Title */ - function getTitle(); + public function getTitle(); } /** @@ -184,36 +268,42 @@ interface PPNode { /** * Get an array-type node containing the children of this node. * Returns false if this is not a tree node. + * @return PPNode */ - function getChildren(); + public function getChildren(); /** * Get the first child of a tree node. False if there isn't one. * * @return PPNode */ - function getFirstChild(); + public function getFirstChild(); /** * Get the next sibling of any node. False if there isn't one + * @return PPNode */ - function getNextSibling(); + public function getNextSibling(); /** * Get all children of this tree node which have a given name. * Returns an array-type node, or false if this is not a tree node. + * @param string $type + * @return bool|PPNode */ - function getChildrenOfType( $type ); + public function getChildrenOfType( $type ); /** * Returns the length of the array, or false if this is not an array-type node */ - function getLength(); + public function getLength(); /** * Returns an item of an array-type node + * @param int $i + * @return bool|PPNode */ - function item( $i ); + public function item( $i ); /** * Get the name of this node. The following names are defined here: @@ -226,25 +316,29 @@ interface PPNode { * #nodelist An array-type node * * The subclass may define various other names for tree and leaf nodes. + * @return string */ - function getName(); + public function getName(); /** * Split a "" node into an associative array containing: * name PPNode name * index String index * value PPNode value + * @return array */ - function splitArg(); + public function splitArg(); /** * Split an "" node into an associative array containing name, attr, inner and close * All values in the resulting array are PPNodes. Inner and close are optional. + * @return array */ - function splitExt(); + public function splitExt(); /** * Split an "" node + * @return array */ - function splitHeading(); + public function splitHeading(); } diff --git a/includes/parser/Preprocessor_DOM.php b/includes/parser/Preprocessor_DOM.php index 3138f483..2edb79a2 100644 --- a/includes/parser/Preprocessor_DOM.php +++ b/includes/parser/Preprocessor_DOM.php @@ -23,19 +23,21 @@ /** * @ingroup Parser + * @codingStandardsIgnoreStart */ class Preprocessor_DOM implements Preprocessor { + // @codingStandardsIgnoreEnd /** * @var Parser */ - var $parser; + public $parser; - var $memoryLimit; + public $memoryLimit; const CACHE_VERSION = 1; - function __construct( $parser ) { + public function __construct( $parser ) { $this->parser = $parser; $mem = ini_get( 'memory_limit' ); $this->memoryLimit = false; @@ -51,40 +53,57 @@ class Preprocessor_DOM implements Preprocessor { /** * @return PPFrame_DOM */ - function newFrame() { + public function newFrame() { return new PPFrame_DOM( $this ); } /** - * @param $args array + * @param array $args * @return PPCustomFrame_DOM */ - function newCustomFrame( $args ) { + public function newCustomFrame( $args ) { return new PPCustomFrame_DOM( $this, $args ); } /** - * @param $values + * @param array $values * @return PPNode_DOM */ - function newPartNodeArray( $values ) { + public function newPartNodeArray( $values ) { //NOTE: DOM manipulation is slower than building & parsing XML! (or so Tim sais) $xml = ""; foreach ( $values as $k => $val ) { if ( is_int( $k ) ) { - $xml .= "" . htmlspecialchars( $val ) . ""; + $xml .= "" + . htmlspecialchars( $val ) . ""; } else { - $xml .= "" . htmlspecialchars( $k ) . "=" . htmlspecialchars( $val ) . ""; + $xml .= "" . htmlspecialchars( $k ) + . "=" . htmlspecialchars( $val ) . ""; } } $xml .= ""; + wfProfileIn( __METHOD__ . '-loadXML' ); $dom = new DOMDocument(); - $dom->loadXML( $xml ); - $root = $dom->documentElement; + wfSuppressWarnings(); + $result = $dom->loadXML( $xml ); + wfRestoreWarnings(); + if ( !$result ) { + // Try running the XML through UtfNormal to get rid of invalid characters + $xml = UtfNormal::cleanUp( $xml ); + // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2 + // don't barf when the XML is >256 levels deep + $result = $dom->loadXML( $xml, 1 << 19 ); + } + wfProfileOut( __METHOD__ . '-loadXML' ); + if ( !$result ) { + throw new MWException( 'Parameters passed to ' . __METHOD__ . ' result in invalid XML' ); + } + + $root = $dom->documentElement; $node = new PPNode_DOM( $root->childNodes ); return $node; } @@ -93,7 +112,7 @@ class Preprocessor_DOM implements Preprocessor { * @throws MWException * @return bool */ - function memCheck() { + public function memCheck() { if ( $this->memoryLimit === false ) { return true; } @@ -109,10 +128,11 @@ class Preprocessor_DOM implements Preprocessor { * Preprocess some wikitext and return the document tree. * This is the ghost of Parser::replace_variables(). * - * @param string $text the text to parse - * @param $flags Integer: bitwise combination of: - * Parser::PTD_FOR_INCLUSION Handle "" and "" as if the text is being - * included. Default is to assume a direct page view. + * @param string $text The text to parse + * @param int $flags Bitwise combination of: + * Parser::PTD_FOR_INCLUSION Handle "" and "" + * as if the text is being included. Default + * is to assume a direct page view. * * The generated DOM tree must depend only on the input text and the flags. * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899. @@ -128,7 +148,7 @@ class Preprocessor_DOM implements Preprocessor { * @throws MWException * @return PPNode_DOM */ - function preprocessToObj( $text, $flags = 0 ) { + public function preprocessToObj( $text, $flags = 0 ) { wfProfileIn( __METHOD__ ); global $wgMemc, $wgPreprocessorCacheThreshold; @@ -160,7 +180,6 @@ class Preprocessor_DOM implements Preprocessor { $xml = $this->preprocessToXml( $text, $flags ); } - // Fail if the number of elements exceeds acceptable limits // Do not attempt to generate the DOM $this->parser->mGeneratedPPNodeCount += substr_count( $xml, '<' ); @@ -181,7 +200,8 @@ class Preprocessor_DOM implements Preprocessor { if ( !$result ) { // Try running the XML through UtfNormal to get rid of invalid characters $xml = UtfNormal::cleanUp( $xml ); - // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2 don't barf when the XML is >256 levels deep + // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2 + // don't barf when the XML is >256 levels deep. $result = $dom->loadXML( $xml, 1 << 19 ); } if ( $result ) { @@ -202,11 +222,11 @@ class Preprocessor_DOM implements Preprocessor { } /** - * @param $text string - * @param $flags int + * @param string $text + * @param int $flags * @return string */ - function preprocessToXml( $text, $flags = 0 ) { + public function preprocessToXml( $text, $flags = 0 ) { wfProfileIn( __METHOD__ ); $rules = array( '{' => array( @@ -234,7 +254,9 @@ class Preprocessor_DOM implements Preprocessor { $ignoredTags = array( 'includeonly', '/includeonly' ); $ignoredElements = array( 'noinclude' ); $xmlishElements[] = 'noinclude'; - if ( strpos( $text, '' ) !== false && strpos( $text, '' ) !== false ) { + if ( strpos( $text, '' ) !== false + && strpos( $text, '' ) !== false + ) { $enableOnlyinclude = true; } } else { @@ -250,19 +272,28 @@ class Preprocessor_DOM implements Preprocessor { $stack = new PPDStack; $searchBase = "[{<\n"; #} - $revText = strrev( $text ); // For fast reverse searches + // For fast reverse searches + $revText = strrev( $text ); $lengthText = strlen( $text ); - $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start - $accum =& $stack->getAccum(); # Current accumulator + // Input pointer, starts out pointing to a pseudo-newline before the start + $i = 0; + // Current accumulator + $accum =& $stack->getAccum(); $accum = ''; - $findEquals = false; # True to find equals signs in arguments - $findPipe = false; # True to take notice of pipe characters + // True to find equals signs in arguments + $findEquals = false; + // True to take notice of pipe characters + $findPipe = false; $headingIndex = 1; - $inHeading = false; # True if $i is inside a possible heading - $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i - $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next - $fakeLineStart = true; # Do a line-start run without outputting an LF character + // True if $i is inside a possible heading + $inHeading = false; + // True if there are no more greater-than (>) signs right of $i + $noMoreGT = false; + // True to ignore all input up to the next + $findOnlyinclude = $enableOnlyinclude; + // Do a line-start run without outputting an LF character + $fakeLineStart = true; while ( true ) { //$this->memCheck(); @@ -347,7 +378,9 @@ class Preprocessor_DOM implements Preprocessor { if ( $found == 'angle' ) { $matches = false; // Handle - if ( $enableOnlyinclude && substr( $text, $i, strlen( '' ) ) == '' ) { + if ( $enableOnlyinclude + && substr( $text, $i, strlen( '' ) ) == '' + ) { $findOnlyinclude = true; continue; } @@ -400,14 +433,14 @@ class Preprocessor_DOM implements Preprocessor { // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but // it's a possible beneficial b/c break. if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n" - && substr( $text, $wsEnd + 1, 1 ) == "\n" ) - { + && substr( $text, $wsEnd + 1, 1 ) == "\n" + ) { // Remove leading whitespace from the end of the accumulator // Sanity check first though $wsLength = $i - $wsStart; if ( $wsLength > 0 - && strspn( $accum, " \t", -$wsLength ) === $wsLength ) - { + && strspn( $accum, " \t", -$wsLength ) === $wsLength + ) { $accum = substr( $accum, 0, -$wsLength ); } @@ -461,7 +494,9 @@ class Preprocessor_DOM implements Preprocessor { // Handle ignored tags if ( in_array( $lowerName, $ignoredTags ) ) { - $accum .= '' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) ) . ''; + $accum .= '' + . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) ) + . ''; $i = $tagEndPos + 1; continue; } @@ -476,8 +511,8 @@ class Preprocessor_DOM implements Preprocessor { $attrEnd = $tagEndPos; // Find closing tag if ( preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i", - $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) - { + $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) + ) { $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 ); $i = $matches[0][1] + strlen( $matches[0][0] ); $close = '' . htmlspecialchars( $matches[0][0] ) . ''; @@ -521,9 +556,11 @@ class Preprocessor_DOM implements Preprocessor { $count = strspn( $text, '=', $i, 6 ); if ( $count == 1 && $findEquals ) { - // DWIM: This looks kind of like a name/value separator - // Let's let the equals handler have it and break the potential heading - // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex. + // DWIM: This looks kind of like a name/value separator. + // Let's let the equals handler have it and break the + // potential heading. This is heuristic, but AFAICT the + // methods for completely correct disambiguation are very + // complex. } elseif ( $count > 0 ) { $piece = array( 'open' => "\n", @@ -542,8 +579,9 @@ class Preprocessor_DOM implements Preprocessor { // A heading must be open, otherwise \n wouldn't have been in the search list assert( '$piece->open == "\n"' ); $part = $piece->getCurrentPart(); - // Search back through the input to see if it has a proper close - // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient + // Search back through the input to see if it has a proper close. + // Do this using the reversed string since the other solutions + // (end anchor, etc.) are inefficient. $wsLength = strspn( $revText, " \t", $lengthText - $i ); $searchStart = $i - $wsLength; if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { @@ -737,18 +775,18 @@ class Preprocessor_DOM implements Preprocessor { * @ingroup Parser */ class PPDStack { - var $stack, $rootAccum; + public $stack, $rootAccum; /** * @var PPDStack */ - var $top; - var $out; - var $elementClass = 'PPDStackElement'; + public $top; + public $out; + public $elementClass = 'PPDStackElement'; - static $false = false; + public static $false = false; - function __construct() { + public function __construct() { $this->stack = array(); $this->top = false; $this->rootAccum = ''; @@ -758,15 +796,15 @@ class PPDStack { /** * @return int */ - function count() { + public function count() { return count( $this->stack ); } - function &getAccum() { + public function &getAccum() { return $this->accum; } - function getCurrentPart() { + public function getCurrentPart() { if ( $this->top === false ) { return false; } else { @@ -774,7 +812,7 @@ class PPDStack { } } - function push( $data ) { + public function push( $data ) { if ( $data instanceof $this->elementClass ) { $this->stack[] = $data; } else { @@ -785,7 +823,7 @@ class PPDStack { $this->accum =& $this->top->getAccum(); } - function pop() { + public function pop() { if ( !count( $this->stack ) ) { throw new MWException( __METHOD__ . ': no elements remaining' ); } @@ -801,7 +839,7 @@ class PPDStack { return $temp; } - function addPart( $s = '' ) { + public function addPart( $s = '' ) { $this->top->addPart( $s ); $this->accum =& $this->top->getAccum(); } @@ -809,7 +847,7 @@ class PPDStack { /** * @return array */ - function getFlags() { + public function getFlags() { if ( !count( $this->stack ) ) { return array( 'findEquals' => false, @@ -826,15 +864,15 @@ class PPDStack { * @ingroup Parser */ class PPDStackElement { - var $open, // Opening character (\n for heading) + public $open, // Opening character (\n for heading) $close, // Matching closing character $count, // Number of opening characters found (number of "=" for heading) $parts, // Array of PPDPart objects describing pipe-separated parts. $lineStart; // True if the open char appeared at the start of the input line. Not set for headings. - var $partClass = 'PPDPart'; + public $partClass = 'PPDPart'; - function __construct( $data = array() ) { + public function __construct( $data = array() ) { $class = $this->partClass; $this->parts = array( new $class ); @@ -843,23 +881,23 @@ class PPDStackElement { } } - function &getAccum() { + public function &getAccum() { return $this->parts[count( $this->parts ) - 1]->out; } - function addPart( $s = '' ) { + public function addPart( $s = '' ) { $class = $this->partClass; $this->parts[] = new $class( $s ); } - function getCurrentPart() { + public function getCurrentPart() { return $this->parts[count( $this->parts ) - 1]; } /** * @return array */ - function getFlags() { + public function getFlags() { $partCount = count( $this->parts ); $findPipe = $this->open != "\n" && $this->open != '['; return array( @@ -872,9 +910,10 @@ class PPDStackElement { /** * Get the output string that would result if the close is not found. * + * @param bool|int $openingCount * @return string */ - function breakSyntax( $openingCount = false ) { + public function breakSyntax( $openingCount = false ) { if ( $this->open == "\n" ) { $s = $this->parts[0]->out; } else { @@ -900,14 +939,14 @@ class PPDStackElement { * @ingroup Parser */ class PPDPart { - var $out; // Output accumulator string + public $out; // Output accumulator string // Optional member variables: // eqpos Position of equals sign in output accumulator // commentEnd Past-the-end input pointer for the last comment encountered // visualEnd Past-the-end input pointer for the end of the accumulator minus comments - function __construct( $out = '' ) { + public function __construct( $out = '' ) { $this->out = $out; } } @@ -915,57 +954,71 @@ class PPDPart { /** * An expansion frame, used as a context to expand the result of preprocessToObj() * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPFrame_DOM implements PPFrame { + // @codingStandardsIgnoreEnd /** * @var Preprocessor */ - var $preprocessor; + public $preprocessor; /** * @var Parser */ - var $parser; + public $parser; /** * @var Title */ - var $title; - var $titleCache; + public $title; + public $titleCache; /** * Hashtable listing templates which are disallowed for expansion in this frame, * having been encountered previously in parent frames. */ - var $loopCheckHash; + public $loopCheckHash; /** * Recursion depth of this frame, top = 0 * Note that this is NOT the same as expansion depth in expand() */ - var $depth; + public $depth; + + private $volatile = false; + private $ttl = null; + + /** + * @var array + */ + protected $childExpansionCache; /** * Construct a new preprocessor frame. - * @param $preprocessor Preprocessor The parent preprocessor + * @param Preprocessor $preprocessor The parent preprocessor */ - function __construct( $preprocessor ) { + public function __construct( $preprocessor ) { $this->preprocessor = $preprocessor; $this->parser = $preprocessor->parser; $this->title = $this->parser->mTitle; $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false ); $this->loopCheckHash = array(); $this->depth = 0; + $this->childExpansionCache = array(); } /** * Create a new child frame * $args is optionally a multi-root PPNode or array containing the template arguments * + * @param bool|array $args + * @param Title|bool $title + * @param int $indexOffset * @return PPTemplateFrame_DOM */ - function newChild( $args = false, $title = false, $indexOffset = 0 ) { + public function newChild( $args = false, $title = false, $indexOffset = 0 ) { $namedArgs = array(); $numberedArgs = array(); if ( $title === false ) { @@ -980,7 +1033,7 @@ class PPFrame_DOM implements PPFrame { if ( $arg instanceof PPNode ) { $arg = $arg->node; } - if ( !$xpath ) { + if ( !$xpath || $xpath->document !== $arg->ownerDocument ) { $xpath = new DOMXPath( $arg->ownerDocument ); } @@ -1005,11 +1058,23 @@ class PPFrame_DOM implements PPFrame { /** * @throws MWException - * @param $root - * @param $flags int + * @param string|int $key + * @param string|PPNode_DOM|DOMDocument $root + * @param int $flags + * @return string + */ + public function cachedExpand( $key, $root, $flags = 0 ) { + // we don't have a parent, so we don't have a cache + return $this->expand( $root, $flags ); + } + + /** + * @throws MWException + * @param string|PPNode_DOM|DOMDocument $root + * @param int $flags * @return string */ - function expand( $root, $flags = 0 ) { + public function expand( $root, $flags = 0 ) { static $expansionDepth = 0; if ( is_string( $root ) ) { return $root; @@ -1142,17 +1207,16 @@ class PPFrame_DOM implements PPFrame { # Remove it in HTML, pre+remove and STRIP_COMMENTS modes if ( $this->parser->ot['html'] || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() ) - || ( $flags & PPFrame::STRIP_COMMENTS ) ) - { + || ( $flags & PPFrame::STRIP_COMMENTS ) + ) { $out .= ''; - } - # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result - # Not in RECOVER_COMMENTS mode (extractSections) though - elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) { + } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) { + # Add a strip marker in PST mode so that pstPass2() can + # run some old-fashioned regexes on the result. + # Not in RECOVER_COMMENTS mode (extractSections) though. $out .= $this->parser->insertStripItem( $contextNode->textContent ); - } - # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove - else { + } else { + # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove $out .= $contextNode->textContent; } } elseif ( $contextNode->nodeName == 'ignore' ) { @@ -1160,7 +1224,9 @@ class PPFrame_DOM implements PPFrame { # OT_WIKI will only respect in substed templates. # The other output types respect it unless NO_IGNORE is set. # extractSections() sets NO_IGNORE and so never respects it. - if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & PPFrame::NO_IGNORE ) ) { + if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) + || ( $flags & PPFrame::NO_IGNORE ) + ) { $out .= $contextNode->textContent; } else { $out .= ''; @@ -1172,13 +1238,29 @@ class PPFrame_DOM implements PPFrame { $attrs = $xpath->query( 'attr', $contextNode ); $inners = $xpath->query( 'inner', $contextNode ); $closes = $xpath->query( 'close', $contextNode ); - $params = array( - 'name' => new PPNode_DOM( $names->item( 0 ) ), - 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null, - 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null, - 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null, - ); - $out .= $this->parser->extensionSubstitution( $params, $this ); + if ( $flags & PPFrame::NO_TAGS ) { + $s = '<' . $this->expand( $names->item( 0 ), $flags ); + if ( $attrs->length > 0 ) { + $s .= $this->expand( $attrs->item( 0 ), $flags ); + } + if ( $inners->length > 0 ) { + $s .= '>' . $this->expand( $inners->item( 0 ), $flags ); + if ( $closes->length > 0 ) { + $s .= $this->expand( $closes->item( 0 ), $flags ); + } + } else { + $s .= '/>'; + } + $out .= $s; + } else { + $params = array( + 'name' => new PPNode_DOM( $names->item( 0 ) ), + 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null, + 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null, + 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null, + ); + $out .= $this->parser->extensionSubstitution( $params, $this ); + } } elseif ( $contextNode->nodeName == 'h' ) { # Heading $s = $this->expand( $contextNode->childNodes, $flags ); @@ -1231,11 +1313,12 @@ class PPFrame_DOM implements PPFrame { } /** - * @param $sep - * @param $flags + * @param string $sep + * @param int $flags + * @param string|PPNode_DOM|DOMDocument $args,... * @return string */ - function implodeWithFlags( $sep, $flags /*, ... */ ) { + public function implodeWithFlags( $sep, $flags /*, ... */ ) { $args = array_slice( func_get_args(), 2 ); $first = true; @@ -1263,9 +1346,11 @@ class PPFrame_DOM implements PPFrame { * Implode with no flags specified * This previously called implodeWithFlags but has now been inlined to reduce stack depth * + * @param string $sep + * @param string|PPNode_DOM|DOMDocument $args,... * @return string */ - function implode( $sep /*, ... */ ) { + public function implode( $sep /*, ... */ ) { $args = array_slice( func_get_args(), 1 ); $first = true; @@ -1293,9 +1378,11 @@ class PPFrame_DOM implements PPFrame { * Makes an object that, when expand()ed, will be the same as one obtained * with implode() * + * @param string $sep + * @param string|PPNode_DOM|DOMDocument $args,... * @return array */ - function virtualImplode( $sep /*, ... */ ) { + public function virtualImplode( $sep /*, ... */ ) { $args = array_slice( func_get_args(), 1 ); $out = array(); $first = true; @@ -1321,9 +1408,13 @@ class PPFrame_DOM implements PPFrame { /** * Virtual implode with brackets + * @param string $start + * @param string $sep + * @param string $end + * @param string|PPNode_DOM|DOMDocument $args,... * @return array */ - function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { + public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { $args = array_slice( func_get_args(), 3 ); $out = array( $start ); $first = true; @@ -1348,11 +1439,11 @@ class PPFrame_DOM implements PPFrame { return $out; } - function __toString() { + public function __toString() { return 'frame{}'; } - function getPDBK( $level = false ) { + public function getPDBK( $level = false ) { if ( $level === false ) { return $this->title->getPrefixedDBkey(); } else { @@ -1363,21 +1454,21 @@ class PPFrame_DOM implements PPFrame { /** * @return array */ - function getArguments() { + public function getArguments() { return array(); } /** * @return array */ - function getNumberedArguments() { + public function getNumberedArguments() { return array(); } /** * @return array */ - function getNamedArguments() { + public function getNamedArguments() { return array(); } @@ -1386,20 +1477,21 @@ class PPFrame_DOM implements PPFrame { * * @return bool */ - function isEmpty() { + public function isEmpty() { return true; } - function getArgument( $name ) { + public function getArgument( $name ) { return false; } /** * Returns true if the infinite loop check is OK, false if a loop is detected * + * @param Title $title * @return bool */ - function loopCheck( $title ) { + public function loopCheck( $title ) { return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] ); } @@ -1408,7 +1500,7 @@ class PPFrame_DOM implements PPFrame { * * @return bool */ - function isTemplate() { + public function isTemplate() { return false; } @@ -1417,32 +1509,75 @@ class PPFrame_DOM implements PPFrame { * * @return Title */ - function getTitle() { + public function getTitle() { return $this->title; } + + /** + * Set the volatile flag + * + * @param bool $flag + */ + public function setVolatile( $flag = true ) { + $this->volatile = $flag; + } + + /** + * Get the volatile flag + * + * @return bool + */ + public function isVolatile() { + return $this->volatile; + } + + /** + * Set the TTL + * + * @param int $ttl + */ + public function setTTL( $ttl ) { + if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) { + $this->ttl = $ttl; + } + } + + /** + * Get the TTL + * + * @return int|null + */ + public function getTTL() { + return $this->ttl; + } } /** * Expansion frame with template arguments * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPTemplateFrame_DOM extends PPFrame_DOM { - var $numberedArgs, $namedArgs; + // @codingStandardsIgnoreEnd + + public $numberedArgs, $namedArgs; /** * @var PPFrame_DOM */ - var $parent; - var $numberedExpansionCache, $namedExpansionCache; + public $parent; + public $numberedExpansionCache, $namedExpansionCache; /** - * @param $preprocessor - * @param $parent PPFrame_DOM - * @param $numberedArgs array - * @param $namedArgs array - * @param $title Title + * @param Preprocessor $preprocessor + * @param bool|PPFrame_DOM $parent + * @param array $numberedArgs + * @param array $namedArgs + * @param bool|Title $title */ - function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) { + public function __construct( $preprocessor, $parent = false, $numberedArgs = array(), + $namedArgs = array(), $title = false + ) { parent::__construct( $preprocessor ); $this->parent = $parent; @@ -1460,7 +1595,7 @@ class PPTemplateFrame_DOM extends PPFrame_DOM { $this->numberedExpansionCache = $this->namedExpansionCache = array(); } - function __toString() { + public function __toString() { $s = 'tplframe{'; $first = true; $args = $this->numberedArgs + $this->namedArgs; @@ -1477,16 +1612,34 @@ class PPTemplateFrame_DOM extends PPFrame_DOM { return $s; } + /** + * @throws MWException + * @param string|int $key + * @param string|PPNode_DOM|DOMDocument $root + * @param int $flags + * @return string + */ + public function cachedExpand( $key, $root, $flags = 0 ) { + if ( isset( $this->parent->childExpansionCache[$key] ) ) { + return $this->parent->childExpansionCache[$key]; + } + $retval = $this->expand( $root, $flags ); + if ( !$this->isVolatile() ) { + $this->parent->childExpansionCache[$key] = $retval; + } + return $retval; + } + /** * Returns true if there are no arguments in this frame * * @return bool */ - function isEmpty() { + public function isEmpty() { return !count( $this->numberedArgs ) && !count( $this->namedArgs ); } - function getArguments() { + public function getArguments() { $arguments = array(); foreach ( array_merge( array_keys( $this->numberedArgs ), @@ -1496,7 +1649,7 @@ class PPTemplateFrame_DOM extends PPFrame_DOM { return $arguments; } - function getNumberedArguments() { + public function getNumberedArguments() { $arguments = array(); foreach ( array_keys( $this->numberedArgs ) as $key ) { $arguments[$key] = $this->getArgument( $key ); @@ -1504,7 +1657,7 @@ class PPTemplateFrame_DOM extends PPFrame_DOM { return $arguments; } - function getNamedArguments() { + public function getNamedArguments() { $arguments = array(); foreach ( array_keys( $this->namedArgs ) as $key ) { $arguments[$key] = $this->getArgument( $key ); @@ -1512,18 +1665,21 @@ class PPTemplateFrame_DOM extends PPFrame_DOM { return $arguments; } - function getNumberedArgument( $index ) { + public function getNumberedArgument( $index ) { if ( !isset( $this->numberedArgs[$index] ) ) { return false; } if ( !isset( $this->numberedExpansionCache[$index] ) ) { # No trimming for unnamed arguments - $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], PPFrame::STRIP_COMMENTS ); + $this->numberedExpansionCache[$index] = $this->parent->expand( + $this->numberedArgs[$index], + PPFrame::STRIP_COMMENTS + ); } return $this->numberedExpansionCache[$index]; } - function getNamedArgument( $name ) { + public function getNamedArgument( $name ) { if ( !isset( $this->namedArgs[$name] ) ) { return false; } @@ -1535,7 +1691,7 @@ class PPTemplateFrame_DOM extends PPFrame_DOM { return $this->namedExpansionCache[$name]; } - function getArgument( $name ) { + public function getArgument( $name ) { $text = $this->getNumberedArgument( $name ); if ( $text === false ) { $text = $this->getNamedArgument( $name ); @@ -1548,24 +1704,37 @@ class PPTemplateFrame_DOM extends PPFrame_DOM { * * @return bool */ - function isTemplate() { + public function isTemplate() { return true; } + + public function setVolatile( $flag = true ) { + parent::setVolatile( $flag ); + $this->parent->setVolatile( $flag ); + } + + public function setTTL( $ttl ) { + parent::setTTL( $ttl ); + $this->parent->setTTL( $ttl ); + } } /** * Expansion frame with custom arguments * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPCustomFrame_DOM extends PPFrame_DOM { - var $args; + // @codingStandardsIgnoreEnd + + public $args; - function __construct( $preprocessor, $args ) { + public function __construct( $preprocessor, $args ) { parent::__construct( $preprocessor ); $this->args = $args; } - function __toString() { + public function __toString() { $s = 'cstmframe{'; $first = true; foreach ( $this->args as $name => $value ) { @@ -1584,48 +1753,50 @@ class PPCustomFrame_DOM extends PPFrame_DOM { /** * @return bool */ - function isEmpty() { + public function isEmpty() { return !count( $this->args ); } - function getArgument( $index ) { + public function getArgument( $index ) { if ( !isset( $this->args[$index] ) ) { return false; } return $this->args[$index]; } - function getArguments() { + public function getArguments() { return $this->args; } } /** * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPNode_DOM implements PPNode { + // @codingStandardsIgnoreEnd /** * @var DOMElement */ - var $node; - var $xpath; + public $node; + public $xpath; - function __construct( $node, $xpath = false ) { + public function __construct( $node, $xpath = false ) { $this->node = $node; } /** * @return DOMXPath */ - function getXPath() { + public function getXPath() { if ( $this->xpath === null ) { $this->xpath = new DOMXPath( $this->node->ownerDocument ); } return $this->xpath; } - function __toString() { + public function __toString() { if ( $this->node instanceof DOMNodeList ) { $s = ''; foreach ( $this->node as $node ) { @@ -1640,37 +1811,37 @@ class PPNode_DOM implements PPNode { /** * @return bool|PPNode_DOM */ - function getChildren() { + public function getChildren() { return $this->node->childNodes ? new self( $this->node->childNodes ) : false; } /** * @return bool|PPNode_DOM */ - function getFirstChild() { + public function getFirstChild() { return $this->node->firstChild ? new self( $this->node->firstChild ) : false; } /** * @return bool|PPNode_DOM */ - function getNextSibling() { + public function getNextSibling() { return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false; } /** - * @param $type + * @param string $type * * @return bool|PPNode_DOM */ - function getChildrenOfType( $type ) { + public function getChildrenOfType( $type ) { return new self( $this->getXPath()->query( $type, $this->node ) ); } /** * @return int */ - function getLength() { + public function getLength() { if ( $this->node instanceof DOMNodeList ) { return $this->node->length; } else { @@ -1679,10 +1850,10 @@ class PPNode_DOM implements PPNode { } /** - * @param $i + * @param int $i * @return bool|PPNode_DOM */ - function item( $i ) { + public function item( $i ) { $item = $this->node->item( $i ); return $item ? new self( $item ) : false; } @@ -1690,7 +1861,7 @@ class PPNode_DOM implements PPNode { /** * @return string */ - function getName() { + public function getName() { if ( $this->node instanceof DOMNodeList ) { return '#nodelist'; } else { @@ -1707,7 +1878,7 @@ class PPNode_DOM implements PPNode { * @throws MWException * @return array */ - function splitArg() { + public function splitArg() { $xpath = $this->getXPath(); $names = $xpath->query( 'name', $this->node ); $values = $xpath->query( 'value', $this->node ); @@ -1729,7 +1900,7 @@ class PPNode_DOM implements PPNode { * @throws MWException * @return array */ - function splitExt() { + public function splitExt() { $xpath = $this->getXPath(); $names = $xpath->query( 'name', $this->node ); $attrs = $xpath->query( 'attr', $this->node ); @@ -1755,7 +1926,7 @@ class PPNode_DOM implements PPNode { * @throws MWException * @return array */ - function splitHeading() { + public function splitHeading() { if ( $this->getName() !== 'h' ) { throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); } diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php index 2fc5e118..63763967 100644 --- a/includes/parser/Preprocessor_Hash.php +++ b/includes/parser/Preprocessor_Hash.php @@ -26,39 +26,42 @@ * * attribute nodes are children * * "" nodes that aren't at the top are replaced with * @ingroup Parser + * @codingStandardsIgnoreStart */ class Preprocessor_Hash implements Preprocessor { + // @codingStandardsIgnoreEnd + /** * @var Parser */ - var $parser; + public $parser; const CACHE_VERSION = 1; - function __construct( $parser ) { + public function __construct( $parser ) { $this->parser = $parser; } /** * @return PPFrame_Hash */ - function newFrame() { + public function newFrame() { return new PPFrame_Hash( $this ); } /** - * @param $args array + * @param array $args * @return PPCustomFrame_Hash */ - function newCustomFrame( $args ) { + public function newCustomFrame( $args ) { return new PPCustomFrame_Hash( $this, $args ); } /** - * @param $values array + * @param array $values * @return PPNode_Hash_Array */ - function newPartNodeArray( $values ) { + public function newPartNodeArray( $values ) { $list = array(); foreach ( $values as $k => $val ) { @@ -89,10 +92,10 @@ class Preprocessor_Hash implements Preprocessor { * Preprocess some wikitext and return the document tree. * This is the ghost of Parser::replace_variables(). * - * @param string $text the text to parse - * @param $flags Integer: bitwise combination of: - * Parser::PTD_FOR_INCLUSION Handle "" and "" as if the text is being - * included. Default is to assume a direct page view. + * @param string $text The text to parse + * @param int $flags Bitwise combination of: + * Parser::PTD_FOR_INCLUSION Handle "" and "" as if the text is being + * included. Default is to assume a direct page view. * * The generated DOM tree must depend only on the input text and the flags. * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899. @@ -108,13 +111,15 @@ class Preprocessor_Hash implements Preprocessor { * @throws MWException * @return PPNode_Hash_Tree */ - function preprocessToObj( $text, $flags = 0 ) { + public function preprocessToObj( $text, $flags = 0 ) { wfProfileIn( __METHOD__ ); // Check cache. global $wgMemc, $wgPreprocessorCacheThreshold; - $cacheable = $wgPreprocessorCacheThreshold !== false && strlen( $text ) > $wgPreprocessorCacheThreshold; + $cacheable = $wgPreprocessorCacheThreshold !== false + && strlen( $text ) > $wgPreprocessorCacheThreshold; + if ( $cacheable ) { wfProfileIn( __METHOD__ . '-cacheable' ); @@ -161,7 +166,9 @@ class Preprocessor_Hash implements Preprocessor { $ignoredTags = array( 'includeonly', '/includeonly' ); $ignoredElements = array( 'noinclude' ); $xmlishElements[] = 'noinclude'; - if ( strpos( $text, '' ) !== false && strpos( $text, '' ) !== false ) { + if ( strpos( $text, '' ) !== false + && strpos( $text, '' ) !== false + ) { $enableOnlyinclude = true; } } else { @@ -177,18 +184,27 @@ class Preprocessor_Hash implements Preprocessor { $stack = new PPDStack_Hash; $searchBase = "[{<\n"; - $revText = strrev( $text ); // For fast reverse searches + // For fast reverse searches + $revText = strrev( $text ); $lengthText = strlen( $text ); - $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start - $accum =& $stack->getAccum(); # Current accumulator - $findEquals = false; # True to find equals signs in arguments - $findPipe = false; # True to take notice of pipe characters + // Input pointer, starts out pointing to a pseudo-newline before the start + $i = 0; + // Current accumulator + $accum =& $stack->getAccum(); + // True to find equals signs in arguments + $findEquals = false; + // True to take notice of pipe characters + $findPipe = false; $headingIndex = 1; - $inHeading = false; # True if $i is inside a possible heading - $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i - $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next - $fakeLineStart = true; # Do a line-start run without outputting an LF character + // True if $i is inside a possible heading + $inHeading = false; + // True if there are no more greater-than (>) signs right of $i + $noMoreGT = false; + // True to ignore all input up to the next + $findOnlyinclude = $enableOnlyinclude; + // Do a line-start run without outputting an LF character + $fakeLineStart = true; while ( true ) { //$this->memCheck(); @@ -273,7 +289,9 @@ class Preprocessor_Hash implements Preprocessor { if ( $found == 'angle' ) { $matches = false; // Handle - if ( $enableOnlyinclude && substr( $text, $i, strlen( '' ) ) == '' ) { + if ( $enableOnlyinclude + && substr( $text, $i, strlen( '' ) ) == '' + ) { $findOnlyinclude = true; continue; } @@ -326,15 +344,15 @@ class Preprocessor_Hash implements Preprocessor { // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but // it's a possible beneficial b/c break. if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n" - && substr( $text, $wsEnd + 1, 1 ) == "\n" ) - { + && substr( $text, $wsEnd + 1, 1 ) == "\n" + ) { // Remove leading whitespace from the end of the accumulator // Sanity check first though $wsLength = $i - $wsStart; if ( $wsLength > 0 && $accum->lastNode instanceof PPNode_Hash_Text - && strspn( $accum->lastNode->value, " \t", -$wsLength ) === $wsLength ) - { + && strspn( $accum->lastNode->value, " \t", -$wsLength ) === $wsLength + ) { $accum->lastNode->value = substr( $accum->lastNode->value, 0, -$wsLength ); } @@ -404,8 +422,8 @@ class Preprocessor_Hash implements Preprocessor { $attrEnd = $tagEndPos; // Find closing tag if ( preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i", - $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) - { + $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) + ) { $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 ); $i = $matches[0][1] + strlen( $matches[0][0] ); $close = $matches[0][0]; @@ -440,9 +458,7 @@ class Preprocessor_Hash implements Preprocessor { $extNode->addChild( PPNode_Hash_Tree::newWithText( 'close', $close ) ); } $accum->addNode( $extNode ); - } - - elseif ( $found == 'line-start' ) { + } elseif ( $found == 'line-start' ) { // Is this the start of a heading? // Line break belongs before the heading element in any case if ( $fakeLineStart ) { @@ -454,9 +470,10 @@ class Preprocessor_Hash implements Preprocessor { $count = strspn( $text, '=', $i, 6 ); if ( $count == 1 && $findEquals ) { - // DWIM: This looks kind of like a name/value separator - // Let's let the equals handler have it and break the potential heading - // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex. + // DWIM: This looks kind of like a name/value separator. + // Let's let the equals handler have it and break the potential + // heading. This is heuristic, but AFAICT the methods for + // completely correct disambiguation are very complex. } elseif ( $count > 0 ) { $piece = array( 'open' => "\n", @@ -474,8 +491,9 @@ class Preprocessor_Hash implements Preprocessor { // A heading must be open, otherwise \n wouldn't have been in the search list assert( '$piece->open == "\n"' ); $part = $piece->getCurrentPart(); - // Search back through the input to see if it has a proper close - // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient + // Search back through the input to see if it has a proper close. + // Do this using the reversed string since the other solutions + // (end anchor, etc.) are inefficient. $wsLength = strspn( $revText, " \t", $lengthText - $i ); $searchStart = $i - $wsLength; if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { @@ -743,9 +761,12 @@ class Preprocessor_Hash implements Preprocessor { /** * Stack class to help Preprocessor::preprocessToObj() * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPDStack_Hash extends PPDStack { - function __construct() { + // @codingStandardsIgnoreEnd + + public function __construct() { $this->elementClass = 'PPDStackElement_Hash'; parent::__construct(); $this->rootAccum = new PPDAccum_Hash; @@ -754,9 +775,12 @@ class PPDStack_Hash extends PPDStack { /** * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPDStackElement_Hash extends PPDStackElement { - function __construct( $data = array() ) { + // @codingStandardsIgnoreENd + + public function __construct( $data = array() ) { $this->partClass = 'PPDPart_Hash'; parent::__construct( $data ); } @@ -764,9 +788,10 @@ class PPDStackElement_Hash extends PPDStackElement { /** * Get the accumulator that would result if the close is not found. * + * @param int|bool $openingCount * @return PPDAccum_Hash */ - function breakSyntax( $openingCount = false ) { + public function breakSyntax( $openingCount = false ) { if ( $this->open == "\n" ) { $accum = $this->parts[0]->out; } else { @@ -791,9 +816,12 @@ class PPDStackElement_Hash extends PPDStackElement { /** * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPDPart_Hash extends PPDPart { - function __construct( $out = '' ) { + // @codingStandardsIgnoreEnd + + public function __construct( $out = '' ) { $accum = new PPDAccum_Hash; if ( $out !== '' ) { $accum->addLiteral( $out ); @@ -804,18 +832,22 @@ class PPDPart_Hash extends PPDPart { /** * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPDAccum_Hash { - var $firstNode, $lastNode; + // @codingStandardsIgnoreEnd + + public $firstNode, $lastNode; - function __construct() { + public function __construct() { $this->firstNode = $this->lastNode = false; } /** * Append a string literal + * @param string $s */ - function addLiteral( $s ) { + public function addLiteral( $s ) { if ( $this->lastNode === false ) { $this->firstNode = $this->lastNode = new PPNode_Hash_Text( $s ); } elseif ( $this->lastNode instanceof PPNode_Hash_Text ) { @@ -828,8 +860,9 @@ class PPDAccum_Hash { /** * Append a PPNode + * @param PPNode $node */ - function addNode( PPNode $node ) { + public function addNode( PPNode $node ) { if ( $this->lastNode === false ) { $this->firstNode = $this->lastNode = $node; } else { @@ -840,18 +873,21 @@ class PPDAccum_Hash { /** * Append a tree node with text contents + * @param string $name + * @param string $value */ - function addNodeWithText( $name, $value ) { + public function addNodeWithText( $name, $value ) { $node = PPNode_Hash_Tree::newWithText( $name, $value ); $this->addNode( $node ); } /** - * Append a PPAccum_Hash + * Append a PPDAccum_Hash * Takes over ownership of the nodes in the source argument. These nodes may * subsequently be modified, especially nextSibling. + * @param PPDAccum_Hash $accum */ - function addAccum( $accum ) { + public function addAccum( $accum ) { if ( $accum->lastNode === false ) { // nothing to add } elseif ( $this->lastNode === false ) { @@ -867,62 +903,72 @@ class PPDAccum_Hash { /** * An expansion frame, used as a context to expand the result of preprocessToObj() * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPFrame_Hash implements PPFrame { + // @codingStandardsIgnoreEnd /** * @var Parser */ - var $parser; + public $parser; /** * @var Preprocessor */ - var $preprocessor; + public $preprocessor; /** * @var Title */ - var $title; - var $titleCache; + public $title; + public $titleCache; /** * Hashtable listing templates which are disallowed for expansion in this frame, * having been encountered previously in parent frames. */ - var $loopCheckHash; + public $loopCheckHash; /** * Recursion depth of this frame, top = 0 * Note that this is NOT the same as expansion depth in expand() */ - var $depth; + public $depth; + + private $volatile = false; + private $ttl = null; + + /** + * @var array + */ + protected $childExpansionCache; /** * Construct a new preprocessor frame. - * @param $preprocessor Preprocessor: the parent preprocessor + * @param Preprocessor $preprocessor The parent preprocessor */ - function __construct( $preprocessor ) { + public function __construct( $preprocessor ) { $this->preprocessor = $preprocessor; $this->parser = $preprocessor->parser; $this->title = $this->parser->mTitle; $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false ); $this->loopCheckHash = array(); $this->depth = 0; + $this->childExpansionCache = array(); } /** * Create a new child frame * $args is optionally a multi-root PPNode or array containing the template arguments * - * @param array|bool|\PPNode_Hash_Array $args PPNode_Hash_Array|array - * @param $title Title|bool - * + * @param array|bool|PPNode_Hash_Array $args + * @param Title|bool $title * @param int $indexOffset * @throws MWException * @return PPTemplateFrame_Hash */ - function newChild( $args = false, $title = false, $indexOffset = 0 ) { + public function newChild( $args = false, $title = false, $indexOffset = 0 ) { $namedArgs = array(); $numberedArgs = array(); if ( $title === false ) { @@ -954,11 +1000,23 @@ class PPFrame_Hash implements PPFrame { /** * @throws MWException - * @param $root - * @param $flags int + * @param string|int $key + * @param string|PPNode $root + * @param int $flags + * @return string + */ + public function cachedExpand( $key, $root, $flags = 0 ) { + // we don't have a parent, so we don't have a cache + return $this->expand( $root, $flags ); + } + + /** + * @throws MWException + * @param string|PPNode $root + * @param int $flags * @return string */ - function expand( $root, $flags = 0 ) { + public function expand( $root, $flags = 0 ) { static $expansionDepth = 0; if ( is_string( $root ) ) { return $root; @@ -1035,7 +1093,11 @@ class PPFrame_Hash implements PPFrame { # Double-brace expansion $bits = $contextNode->splitTemplate(); if ( $flags & PPFrame::NO_TEMPLATES ) { - $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $bits['title'], $bits['parts'] ); + $newIterator = $this->virtualBracketedImplode( + '{{', '|', '}}', + $bits['title'], + $bits['parts'] + ); } else { $ret = $this->parser->braceSubstitution( $bits, $this ); if ( isset( $ret['object'] ) ) { @@ -1048,7 +1110,11 @@ class PPFrame_Hash implements PPFrame { # Triple-brace expansion $bits = $contextNode->splitTemplate(); if ( $flags & PPFrame::NO_ARGS ) { - $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $bits['title'], $bits['parts'] ); + $newIterator = $this->virtualBracketedImplode( + '{{{', '|', '}}}', + $bits['title'], + $bits['parts'] + ); } else { $ret = $this->parser->argSubstitution( $bits, $this ); if ( isset( $ret['object'] ) ) { @@ -1062,17 +1128,16 @@ class PPFrame_Hash implements PPFrame { # Remove it in HTML, pre+remove and STRIP_COMMENTS modes if ( $this->parser->ot['html'] || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() ) - || ( $flags & PPFrame::STRIP_COMMENTS ) ) - { + || ( $flags & PPFrame::STRIP_COMMENTS ) + ) { $out .= ''; - } - # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result - # Not in RECOVER_COMMENTS mode (extractSections) though - elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) { + } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) { + # Add a strip marker in PST mode so that pstPass2() can + # run some old-fashioned regexes on the result. + # Not in RECOVER_COMMENTS mode (extractSections) though. $out .= $this->parser->insertStripItem( $contextNode->firstChild->value ); - } - # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove - else { + } else { + # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove $out .= $contextNode->firstChild->value; } } elseif ( $contextNode->name == 'ignore' ) { @@ -1080,7 +1145,9 @@ class PPFrame_Hash implements PPFrame { # OT_WIKI will only respect in substed templates. # The other output types respect it unless NO_IGNORE is set. # extractSections() sets NO_IGNORE and so never respects it. - if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & PPFrame::NO_IGNORE ) ) { + if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) + || ( $flags & PPFrame::NO_IGNORE ) + ) { $out .= $contextNode->firstChild->value; } else { //$out .= ''; @@ -1088,7 +1155,23 @@ class PPFrame_Hash implements PPFrame { } elseif ( $contextNode->name == 'ext' ) { # Extension tag $bits = $contextNode->splitExt() + array( 'attr' => null, 'inner' => null, 'close' => null ); - $out .= $this->parser->extensionSubstitution( $bits, $this ); + if ( $flags & PPFrame::NO_TAGS ) { + $s = '<' . $bits['name']->firstChild->value; + if ( $bits['attr'] ) { + $s .= $bits['attr']->firstChild->value; + } + if ( $bits['inner'] ) { + $s .= '>' . $bits['inner']->firstChild->value; + if ( $bits['close'] ) { + $s .= $bits['close']->firstChild->value; + } + } else { + $s .= '/>'; + } + $out .= $s; + } else { + $out .= $this->parser->extensionSubstitution( $bits, $this ); + } } elseif ( $contextNode->name == 'h' ) { # Heading if ( $this->parser->ot['html'] ) { @@ -1139,11 +1222,12 @@ class PPFrame_Hash implements PPFrame { } /** - * @param $sep - * @param $flags + * @param string $sep + * @param int $flags + * @param string|PPNode $args,... * @return string */ - function implodeWithFlags( $sep, $flags /*, ... */ ) { + public function implodeWithFlags( $sep, $flags /*, ... */ ) { $args = array_slice( func_get_args(), 2 ); $first = true; @@ -1170,9 +1254,11 @@ class PPFrame_Hash implements PPFrame { /** * Implode with no flags specified * This previously called implodeWithFlags but has now been inlined to reduce stack depth + * @param string $sep + * @param string|PPNode $args,... * @return string */ - function implode( $sep /*, ... */ ) { + public function implode( $sep /*, ... */ ) { $args = array_slice( func_get_args(), 1 ); $first = true; @@ -1200,9 +1286,11 @@ class PPFrame_Hash implements PPFrame { * Makes an object that, when expand()ed, will be the same as one obtained * with implode() * + * @param string $sep + * @param string|PPNode $args,... * @return PPNode_Hash_Array */ - function virtualImplode( $sep /*, ... */ ) { + public function virtualImplode( $sep /*, ... */ ) { $args = array_slice( func_get_args(), 1 ); $out = array(); $first = true; @@ -1229,9 +1317,13 @@ class PPFrame_Hash implements PPFrame { /** * Virtual implode with brackets * + * @param string $start + * @param string $sep + * @param string $end + * @param string|PPNode $args,... * @return PPNode_Hash_Array */ - function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { + public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { $args = array_slice( func_get_args(), 3 ); $out = array( $start ); $first = true; @@ -1256,15 +1348,15 @@ class PPFrame_Hash implements PPFrame { return new PPNode_Hash_Array( $out ); } - function __toString() { + public function __toString() { return 'frame{}'; } /** - * @param $level bool - * @return array|bool|String + * @param bool $level + * @return array|bool|string */ - function getPDBK( $level = false ) { + public function getPDBK( $level = false ) { if ( $level === false ) { return $this->title->getPrefixedDBkey(); } else { @@ -1275,21 +1367,21 @@ class PPFrame_Hash implements PPFrame { /** * @return array */ - function getArguments() { + public function getArguments() { return array(); } /** * @return array */ - function getNumberedArguments() { + public function getNumberedArguments() { return array(); } /** * @return array */ - function getNamedArguments() { + public function getNamedArguments() { return array(); } @@ -1298,26 +1390,26 @@ class PPFrame_Hash implements PPFrame { * * @return bool */ - function isEmpty() { + public function isEmpty() { return true; } /** - * @param $name + * @param string $name * @return bool */ - function getArgument( $name ) { + public function getArgument( $name ) { return false; } /** * Returns true if the infinite loop check is OK, false if a loop is detected * - * @param $title Title + * @param Title $title * * @return bool */ - function loopCheck( $title ) { + public function loopCheck( $title ) { return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] ); } @@ -1326,7 +1418,7 @@ class PPFrame_Hash implements PPFrame { * * @return bool */ - function isTemplate() { + public function isTemplate() { return false; } @@ -1335,27 +1427,70 @@ class PPFrame_Hash implements PPFrame { * * @return Title */ - function getTitle() { + public function getTitle() { return $this->title; } + + /** + * Set the volatile flag + * + * @param bool $flag + */ + public function setVolatile( $flag = true ) { + $this->volatile = $flag; + } + + /** + * Get the volatile flag + * + * @return bool + */ + public function isVolatile() { + return $this->volatile; + } + + /** + * Set the TTL + * + * @param int $ttl + */ + public function setTTL( $ttl ) { + if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) { + $this->ttl = $ttl; + } + } + + /** + * Get the TTL + * + * @return int|null + */ + public function getTTL() { + return $this->ttl; + } } /** * Expansion frame with template arguments * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPTemplateFrame_Hash extends PPFrame_Hash { - var $numberedArgs, $namedArgs, $parent; - var $numberedExpansionCache, $namedExpansionCache; + // @codingStandardsIgnoreEnd + + public $numberedArgs, $namedArgs, $parent; + public $numberedExpansionCache, $namedExpansionCache; /** - * @param $preprocessor - * @param $parent - * @param $numberedArgs array - * @param $namedArgs array - * @param $title Title + * @param Preprocessor $preprocessor + * @param bool|PPFrame $parent + * @param array $numberedArgs + * @param array $namedArgs + * @param bool|Title $title */ - function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) { + public function __construct( $preprocessor, $parent = false, $numberedArgs = array(), + $namedArgs = array(), $title = false + ) { parent::__construct( $preprocessor ); $this->parent = $parent; @@ -1373,7 +1508,7 @@ class PPTemplateFrame_Hash extends PPFrame_Hash { $this->numberedExpansionCache = $this->namedExpansionCache = array(); } - function __toString() { + public function __toString() { $s = 'tplframe{'; $first = true; $args = $this->numberedArgs + $this->namedArgs; @@ -1389,19 +1524,38 @@ class PPTemplateFrame_Hash extends PPFrame_Hash { $s .= '}'; return $s; } + + /** + * @throws MWException + * @param string|int $key + * @param string|PPNode $root + * @param int $flags + * @return string + */ + public function cachedExpand( $key, $root, $flags = 0 ) { + if ( isset( $this->parent->childExpansionCache[$key] ) ) { + return $this->parent->childExpansionCache[$key]; + } + $retval = $this->expand( $root, $flags ); + if ( !$this->isVolatile() ) { + $this->parent->childExpansionCache[$key] = $retval; + } + return $retval; + } + /** * Returns true if there are no arguments in this frame * * @return bool */ - function isEmpty() { + public function isEmpty() { return !count( $this->numberedArgs ) && !count( $this->namedArgs ); } /** * @return array */ - function getArguments() { + public function getArguments() { $arguments = array(); foreach ( array_merge( array_keys( $this->numberedArgs ), @@ -1414,7 +1568,7 @@ class PPTemplateFrame_Hash extends PPFrame_Hash { /** * @return array */ - function getNumberedArguments() { + public function getNumberedArguments() { $arguments = array(); foreach ( array_keys( $this->numberedArgs ) as $key ) { $arguments[$key] = $this->getArgument( $key ); @@ -1425,7 +1579,7 @@ class PPTemplateFrame_Hash extends PPFrame_Hash { /** * @return array */ - function getNamedArguments() { + public function getNamedArguments() { $arguments = array(); foreach ( array_keys( $this->namedArgs ) as $key ) { $arguments[$key] = $this->getArgument( $key ); @@ -1434,25 +1588,28 @@ class PPTemplateFrame_Hash extends PPFrame_Hash { } /** - * @param $index + * @param int $index * @return array|bool */ - function getNumberedArgument( $index ) { + public function getNumberedArgument( $index ) { if ( !isset( $this->numberedArgs[$index] ) ) { return false; } if ( !isset( $this->numberedExpansionCache[$index] ) ) { # No trimming for unnamed arguments - $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], PPFrame::STRIP_COMMENTS ); + $this->numberedExpansionCache[$index] = $this->parent->expand( + $this->numberedArgs[$index], + PPFrame::STRIP_COMMENTS + ); } return $this->numberedExpansionCache[$index]; } /** - * @param $name + * @param string $name * @return bool */ - function getNamedArgument( $name ) { + public function getNamedArgument( $name ) { if ( !isset( $this->namedArgs[$name] ) ) { return false; } @@ -1465,10 +1622,10 @@ class PPTemplateFrame_Hash extends PPFrame_Hash { } /** - * @param $name + * @param string $name * @return array|bool */ - function getArgument( $name ) { + public function getArgument( $name ) { $text = $this->getNumberedArgument( $name ); if ( $text === false ) { $text = $this->getNamedArgument( $name ); @@ -1481,24 +1638,37 @@ class PPTemplateFrame_Hash extends PPFrame_Hash { * * @return bool */ - function isTemplate() { + public function isTemplate() { return true; } + + public function setVolatile( $flag = true ) { + parent::setVolatile( $flag ); + $this->parent->setVolatile( $flag ); + } + + public function setTTL( $ttl ) { + parent::setTTL( $ttl ); + $this->parent->setTTL( $ttl ); + } } /** * Expansion frame with custom arguments * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPCustomFrame_Hash extends PPFrame_Hash { - var $args; + // @codingStandardsIgnoreEnd - function __construct( $preprocessor, $args ) { + public $args; + + public function __construct( $preprocessor, $args ) { parent::__construct( $preprocessor ); $this->args = $args; } - function __toString() { + public function __toString() { $s = 'cstmframe{'; $first = true; foreach ( $this->args as $name => $value ) { @@ -1517,38 +1687,41 @@ class PPCustomFrame_Hash extends PPFrame_Hash { /** * @return bool */ - function isEmpty() { + public function isEmpty() { return !count( $this->args ); } /** - * @param $index + * @param int $index * @return bool */ - function getArgument( $index ) { + public function getArgument( $index ) { if ( !isset( $this->args[$index] ) ) { return false; } return $this->args[$index]; } - function getArguments() { + public function getArguments() { return $this->args; } } /** * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPNode_Hash_Tree implements PPNode { - var $name, $firstChild, $lastChild, $nextSibling; + // @codingStandardsIgnoreEnd + + public $name, $firstChild, $lastChild, $nextSibling; - function __construct( $name ) { + public function __construct( $name ) { $this->name = $name; $this->firstChild = $this->lastChild = $this->nextSibling = false; } - function __toString() { + public function __toString() { $inner = ''; $attribs = ''; for ( $node = $this->firstChild; $node; $node = $node->nextSibling ) { @@ -1566,17 +1739,17 @@ class PPNode_Hash_Tree implements PPNode { } /** - * @param $name - * @param $text + * @param string $name + * @param string $text * @return PPNode_Hash_Tree */ - static function newWithText( $name, $text ) { + public static function newWithText( $name, $text ) { $obj = new self( $name ); $obj->addChild( new PPNode_Hash_Text( $text ) ); return $obj; } - function addChild( $node ) { + public function addChild( $node ) { if ( $this->lastChild === false ) { $this->firstChild = $this->lastChild = $node; } else { @@ -1588,7 +1761,7 @@ class PPNode_Hash_Tree implements PPNode { /** * @return PPNode_Hash_Array */ - function getChildren() { + public function getChildren() { $children = array(); for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { $children[] = $child; @@ -1596,15 +1769,15 @@ class PPNode_Hash_Tree implements PPNode { return new PPNode_Hash_Array( $children ); } - function getFirstChild() { + public function getFirstChild() { return $this->firstChild; } - function getNextSibling() { + public function getNextSibling() { return $this->nextSibling; } - function getChildrenOfType( $name ) { + public function getChildrenOfType( $name ) { $children = array(); for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { if ( isset( $child->name ) && $child->name === $name ) { @@ -1617,22 +1790,22 @@ class PPNode_Hash_Tree implements PPNode { /** * @return bool */ - function getLength() { + public function getLength() { return false; } /** - * @param $i + * @param int $i * @return bool */ - function item( $i ) { + public function item( $i ) { return false; } /** * @return string */ - function getName() { + public function getName() { return $this->name; } @@ -1645,7 +1818,7 @@ class PPNode_Hash_Tree implements PPNode { * @throws MWException * @return array */ - function splitArg() { + public function splitArg() { $bits = array(); for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { if ( !isset( $child->name ) ) { @@ -1654,8 +1827,8 @@ class PPNode_Hash_Tree implements PPNode { if ( $child->name === 'name' ) { $bits['name'] = $child; if ( $child->firstChild instanceof PPNode_Hash_Attr - && $child->firstChild->name === 'index' ) - { + && $child->firstChild->name === 'index' + ) { $bits['index'] = $child->firstChild->value; } } elseif ( $child->name === 'value' ) { @@ -1679,7 +1852,7 @@ class PPNode_Hash_Tree implements PPNode { * @throws MWException * @return array */ - function splitExt() { + public function splitExt() { $bits = array(); for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { if ( !isset( $child->name ) ) { @@ -1707,7 +1880,7 @@ class PPNode_Hash_Tree implements PPNode { * @throws MWException * @return array */ - function splitHeading() { + public function splitHeading() { if ( $this->name !== 'h' ) { throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); } @@ -1734,7 +1907,7 @@ class PPNode_Hash_Tree implements PPNode { * @throws MWException * @return array */ - function splitTemplate() { + public function splitTemplate() { $parts = array(); $bits = array( 'lineStart' => '' ); for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { @@ -1761,101 +1934,178 @@ class PPNode_Hash_Tree implements PPNode { /** * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPNode_Hash_Text implements PPNode { - var $value, $nextSibling; + // @codingStandardsIgnoreEnd - function __construct( $value ) { + public $value, $nextSibling; + + public function __construct( $value ) { if ( is_object( $value ) ) { throw new MWException( __CLASS__ . ' given object instead of string' ); } $this->value = $value; } - function __toString() { + public function __toString() { return htmlspecialchars( $this->value ); } - function getNextSibling() { + public function getNextSibling() { return $this->nextSibling; } - function getChildren() { return false; } - function getFirstChild() { return false; } - function getChildrenOfType( $name ) { return false; } - function getLength() { return false; } - function item( $i ) { return false; } - function getName() { return '#text'; } - function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); } - function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); } - function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); } + public function getChildren() { + return false; + } + + public function getFirstChild() { + return false; + } + + public function getChildrenOfType( $name ) { + return false; + } + + public function getLength() { + return false; + } + + public function item( $i ) { + return false; + } + + public function getName() { + return '#text'; + } + + public function splitArg() { + throw new MWException( __METHOD__ . ': not supported' ); + } + + public function splitExt() { + throw new MWException( __METHOD__ . ': not supported' ); + } + + public function splitHeading() { + throw new MWException( __METHOD__ . ': not supported' ); + } } /** * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPNode_Hash_Array implements PPNode { - var $value, $nextSibling; + // @codingStandardsIgnoreEnd + + public $value, $nextSibling; - function __construct( $value ) { + public function __construct( $value ) { $this->value = $value; } - function __toString() { + public function __toString() { return var_export( $this, true ); } - function getLength() { + public function getLength() { return count( $this->value ); } - function item( $i ) { + public function item( $i ) { return $this->value[$i]; } - function getName() { return '#nodelist'; } + public function getName() { + return '#nodelist'; + } - function getNextSibling() { + public function getNextSibling() { return $this->nextSibling; } - function getChildren() { return false; } - function getFirstChild() { return false; } - function getChildrenOfType( $name ) { return false; } - function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); } - function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); } - function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); } + public function getChildren() { + return false; + } + + public function getFirstChild() { + return false; + } + + public function getChildrenOfType( $name ) { + return false; + } + + public function splitArg() { + throw new MWException( __METHOD__ . ': not supported' ); + } + + public function splitExt() { + throw new MWException( __METHOD__ . ': not supported' ); + } + + public function splitHeading() { + throw new MWException( __METHOD__ . ': not supported' ); + } } /** * @ingroup Parser + * @codingStandardsIgnoreStart */ class PPNode_Hash_Attr implements PPNode { - var $name, $value, $nextSibling; + // @codingStandardsIgnoreEnd + + public $name, $value, $nextSibling; - function __construct( $name, $value ) { + public function __construct( $name, $value ) { $this->name = $name; $this->value = $value; } - function __toString() { + public function __toString() { return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "name}>"; } - function getName() { + public function getName() { return $this->name; } - function getNextSibling() { + public function getNextSibling() { return $this->nextSibling; } - function getChildren() { return false; } - function getFirstChild() { return false; } - function getChildrenOfType( $name ) { return false; } - function getLength() { return false; } - function item( $i ) { return false; } - function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); } - function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); } - function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); } + public function getChildren() { + return false; + } + + public function getFirstChild() { + return false; + } + + public function getChildrenOfType( $name ) { + return false; + } + + public function getLength() { + return false; + } + + public function item( $i ) { + return false; + } + + public function splitArg() { + throw new MWException( __METHOD__ . ': not supported' ); + } + + public function splitExt() { + throw new MWException( __METHOD__ . ': not supported' ); + } + + public function splitHeading() { + throw new MWException( __METHOD__ . ': not supported' ); + } } diff --git a/includes/parser/StripState.php b/includes/parser/StripState.php index 5f3f18ea..5d1743e6 100644 --- a/includes/parser/StripState.php +++ b/includes/parser/StripState.php @@ -37,9 +37,9 @@ class StripState { const UNSTRIP_RECURSION_LIMIT = 20; /** - * @param $prefix string + * @param string $prefix */ - function __construct( $prefix ) { + public function __construct( $prefix ) { $this->prefix = $prefix; $this->data = array( 'nowiki' => array(), @@ -51,26 +51,26 @@ class StripState { /** * Add a nowiki strip item - * @param $marker - * @param $value + * @param string $marker + * @param string $value */ - function addNoWiki( $marker, $value ) { + public function addNoWiki( $marker, $value ) { $this->addItem( 'nowiki', $marker, $value ); } /** - * @param $marker - * @param $value + * @param string $marker + * @param string $value */ - function addGeneral( $marker, $value ) { + public function addGeneral( $marker, $value ) { $this->addItem( 'general', $marker, $value ); } /** * @throws MWException - * @param $type - * @param $marker - * @param $value + * @param string $type + * @param string $marker + * @param string $value */ protected function addItem( $type, $marker, $value ) { if ( !preg_match( $this->regex, $marker, $m ) ) { @@ -81,34 +81,34 @@ class StripState { } /** - * @param $text + * @param string $text * @return mixed */ - function unstripGeneral( $text ) { + public function unstripGeneral( $text ) { return $this->unstripType( 'general', $text ); } /** - * @param $text + * @param string $text * @return mixed */ - function unstripNoWiki( $text ) { + public function unstripNoWiki( $text ) { return $this->unstripType( 'nowiki', $text ); } /** - * @param $text + * @param string $text * @return mixed */ - function unstripBoth( $text ) { + public function unstripBoth( $text ) { $text = $this->unstripType( 'general', $text ); $text = $this->unstripType( 'nowiki', $text ); return $text; } /** - * @param $type - * @param $text + * @param string $type + * @param string $text * @return mixed */ protected function unstripType( $type, $text ) { @@ -127,7 +127,7 @@ class StripState { } /** - * @param $m array + * @param array $m * @return array */ protected function unstripCallback( $m ) { @@ -159,11 +159,11 @@ class StripState { * Get a StripState object which is sufficient to unstrip the given text. * It will contain the minimum subset of strip items necessary. * - * @param $text string + * @param string $text * * @return StripState */ - function getSubState( $text ) { + public function getSubState( $text ) { $subState = new StripState( $this->prefix ); $pos = 0; while ( true ) { @@ -195,11 +195,11 @@ class StripState { * will not be preserved. The strings in the $texts array will have their * strip markers rewritten, the resulting array of strings will be returned. * - * @param $otherState StripState - * @param $texts Array - * @return Array + * @param StripState $otherState + * @param array $texts + * @return array */ - function merge( $otherState, $texts ) { + public function merge( $otherState, $texts ) { $mergePrefix = Parser::getRandomString(); foreach ( $otherState->data as $type => $items ) { @@ -215,7 +215,7 @@ class StripState { } /** - * @param $m + * @param array $m * @return string */ protected function mergeCallback( $m ) { @@ -226,10 +226,10 @@ class StripState { /** * Remove any strip markers found in the given text. * - * @param $text Input string + * @param string $text Input string * @return string */ - function killMarkers( $text ) { + public function killMarkers( $text ) { return preg_replace( $this->regex, '', $text ); } } diff --git a/includes/parser/Tidy.php b/includes/parser/Tidy.php deleted file mode 100644 index 32b16aaf..00000000 --- a/includes/parser/Tidy.php +++ /dev/null @@ -1,286 +0,0 @@ -mTokens = null; - $this->mUniqPrefix = null; - } - - /** - * @param $text string - * @return string - */ - public function getWrapped( $text ) { - $this->mTokens = new ReplacementArray; - $this->mUniqPrefix = "\x7fUNIQ" . - dechex( mt_rand( 0, 0x7fffffff ) ) . dechex( mt_rand( 0, 0x7fffffff ) ); - $this->mMarkerIndex = 0; - - // Replace elements with placeholders - $wrappedtext = preg_replace_callback( ParserOutput::EDITSECTION_REGEX, - array( &$this, 'replaceCallback' ), $text ); - // ...and markers - $wrappedtext = preg_replace_callback( '/\<\\/?mw:toc\>/', - array( &$this, 'replaceCallback' ), $wrappedtext ); - - // Modify inline Microdata and elements so they say and so - // we can trick Tidy into not stripping them out by including them in tidy's new-empty-tags config - $wrappedtext = preg_replace( '!<(link|meta)([^>]*?)(/{0,1}>)!', '' . - 'test' . $wrappedtext . ''; - - return $wrappedtext; - } - - /** - * @param $m array - * - * @return string - */ - function replaceCallback( $m ) { - $marker = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}" . Parser::MARKER_SUFFIX; - $this->mMarkerIndex++; - $this->mTokens->setPair( $marker, $m[0] ); - return $marker; - } - - /** - * @param $text string - * @return string - */ - public function postprocess( $text ) { - // Revert back to <{link,meta}> - $text = preg_replace( '!]*?)(/{0,1}>)!', '<$1$2$3', $text ); - - // Restore the contents of placeholder tokens - $text = $this->mTokens->replace( $text ); - - return $text; - } - -} - -/** - * Class to interact with HTML tidy - * - * Either the external tidy program or the in-process tidy extension - * will be used depending on availability. Override the default - * $wgTidyInternal setting to disable the internal if it's not working. - * - * @ingroup Parser - */ -class MWTidy { - /** - * Interface with html tidy, used if $wgUseTidy = true. - * If tidy isn't able to correct the markup, the original will be - * returned in all its glory with a warning comment appended. - * - * @param string $text hideous HTML input - * @return String: corrected HTML output - */ - public static function tidy( $text ) { - global $wgTidyInternal; - - $wrapper = new MWTidyWrapper; - $wrappedtext = $wrapper->getWrapped( $text ); - - $retVal = null; - if ( $wgTidyInternal ) { - $correctedtext = self::execInternalTidy( $wrappedtext, false, $retVal ); - } else { - $correctedtext = self::execExternalTidy( $wrappedtext, false, $retVal ); - } - - if ( $retVal < 0 ) { - wfDebug( "Possible tidy configuration error!\n" ); - return $text . "\n\n"; - } elseif ( is_null( $correctedtext ) ) { - wfDebug( "Tidy error detected!\n" ); - return $text . "\n\n"; - } - - $correctedtext = $wrapper->postprocess( $correctedtext ); // restore any hidden tokens - - return $correctedtext; - } - - /** - * Check HTML for errors, used if $wgValidateAllHtml = true. - * - * @param $text String - * @param &$errorStr String: return the error string - * @return Boolean: whether the HTML is valid - */ - public static function checkErrors( $text, &$errorStr = null ) { - global $wgTidyInternal; - - $retval = 0; - if ( $wgTidyInternal ) { - $errorStr = self::execInternalTidy( $text, true, $retval ); - } else { - $errorStr = self::execExternalTidy( $text, true, $retval ); - } - - return ( $retval < 0 && $errorStr == '' ) || $retval == 0; - } - - /** - * Spawn an external HTML tidy process and get corrected markup back from it. - * Also called in OutputHandler.php for full page validation - * - * @param string $text HTML to check - * @param $stderr Boolean: Whether to read result from STDERR rather than STDOUT - * @param &$retval int Exit code (-1 on internal error) - * @return mixed String or null - */ - private static function execExternalTidy( $text, $stderr = false, &$retval = null ) { - global $wgTidyConf, $wgTidyBin, $wgTidyOpts; - wfProfileIn( __METHOD__ ); - - $cleansource = ''; - $opts = ' -utf8'; - - if ( $stderr ) { - $descriptorspec = array( - 0 => array( 'pipe', 'r' ), - 1 => array( 'file', wfGetNull(), 'a' ), - 2 => array( 'pipe', 'w' ) - ); - } else { - $descriptorspec = array( - 0 => array( 'pipe', 'r' ), - 1 => array( 'pipe', 'w' ), - 2 => array( 'file', wfGetNull(), 'a' ) - ); - } - - $readpipe = $stderr ? 2 : 1; - $pipes = array(); - - $process = proc_open( - "$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes ); - - if ( is_resource( $process ) ) { - // Theoretically, this style of communication could cause a deadlock - // here. If the stdout buffer fills up, then writes to stdin could - // block. This doesn't appear to happen with tidy, because tidy only - // writes to stdout after it's finished reading from stdin. Search - // for tidyParseStdin and tidySaveStdout in console/tidy.c - fwrite( $pipes[0], $text ); - fclose( $pipes[0] ); - while ( !feof( $pipes[$readpipe] ) ) { - $cleansource .= fgets( $pipes[$readpipe], 1024 ); - } - fclose( $pipes[$readpipe] ); - $retval = proc_close( $process ); - } else { - wfWarn( "Unable to start external tidy process" ); - $retval = -1; - } - - if ( !$stderr && $cleansource == '' && $text != '' ) { - // Some kind of error happened, so we couldn't get the corrected text. - // Just give up; we'll use the source text and append a warning. - $cleansource = null; - } - - wfProfileOut( __METHOD__ ); - return $cleansource; - } - - /** - * Use the HTML tidy extension to use the tidy library in-process, - * saving the overhead of spawning a new process. - * - * @param string $text HTML to check - * @param $stderr Boolean: Whether to read result from error status instead of output - * @param &$retval int Exit code (-1 on internal error) - * @return mixed String or null - */ - private static function execInternalTidy( $text, $stderr = false, &$retval = null ) { - global $wgTidyConf, $wgDebugTidy; - wfProfileIn( __METHOD__ ); - - if ( !class_exists( 'tidy' ) ) { - wfWarn( "Unable to load internal tidy class." ); - $retval = -1; - - wfProfileOut( __METHOD__ ); - return null; - } - - $tidy = new tidy; - $tidy->parseString( $text, $wgTidyConf, 'utf8' ); - - if ( $stderr ) { - $retval = $tidy->getStatus(); - - wfProfileOut( __METHOD__ ); - return $tidy->errorBuffer; - } - - $tidy->cleanRepair(); - $retval = $tidy->getStatus(); - if ( $retval == 2 ) { - // 2 is magic number for fatal error - // http://www.php.net/manual/en/function.tidy-get-status.php - $cleansource = null; - } else { - $cleansource = tidy_get_output( $tidy ); - if ( $wgDebugTidy && $retval > 0 ) { - $cleansource .= "', '-->', $tidy->errorBuffer ) . - "\n-->"; - } - } - - wfProfileOut( __METHOD__ ); - return $cleansource; - } -} diff --git a/includes/password/BcryptPassword.php b/includes/password/BcryptPassword.php new file mode 100644 index 00000000..dd806e26 --- /dev/null +++ b/includes/password/BcryptPassword.php @@ -0,0 +1,88 @@ + $this->config['cost'], + ); + } + + protected function getDelimiter() { + return '$'; + } + + protected function parseHash( $hash ) { + parent::parseHash( $hash ); + + $this->params['rounds'] = (int)$this->params['rounds']; + } + + /** + * @param string $password Password to encrypt + * + * @throws PasswordError If bcrypt has an unknown error + * @throws MWException If bcrypt is not supported by PHP + */ + public function crypt( $password ) { + if ( !defined( 'CRYPT_BLOWFISH' ) ) { + throw new MWException( 'Bcrypt is not supported.' ); + } + + // Either use existing hash or make a new salt + // Bcrypt expects 22 characters of base64-encoded salt + // Note: bcrypt does not use MIME base64. It uses its own base64 without any '=' padding. + // It expects a 128 bit salt, so it will ignore anything after the first 128 bits + if ( !isset( $this->args[0] ) ) { + $this->args[] = substr( + // Replace + with ., because bcrypt uses a non-MIME base64 format + strtr( + // Random base64 encoded string + base64_encode( MWCryptRand::generate( 16, true ) ), + '+', '.' + ), + 0, 22 + ); + } + + $hash = crypt( $password, + sprintf( '$2y$%02d$%s', (int)$this->params['rounds'], $this->args[0] ) ); + + if ( !is_string( $hash ) || strlen( $hash ) <= 13 ) { + throw new PasswordError( 'Error when hashing password.' ); + } + + // Strip the $2y$ + $parts = explode( $this->getDelimiter(), substr( $hash, 4 ) ); + $this->params['rounds'] = (int)$parts[0]; + $this->args[0] = substr( $parts[1], 0, 22 ); + $this->hash = substr( $parts[1], 22 ); + } +} diff --git a/includes/password/EncryptedPassword.php b/includes/password/EncryptedPassword.php new file mode 100644 index 00000000..39da32d1 --- /dev/null +++ b/includes/password/EncryptedPassword.php @@ -0,0 +1,98 @@ + $this->config['cipher'], + 'secret' => count( $this->config['secrets'] ) - 1 + ); + } + + public function crypt( $password ) { + $secret = $this->config['secrets'][$this->params['secret']]; + + if ( $this->hash ) { + $underlyingPassword = $this->factory->newFromCiphertext( openssl_decrypt( + base64_decode( $this->hash ), $this->params['cipher'], + $secret, 0, base64_decode( $this->args[0] ) + ) ); + } else { + $underlyingPassword = $this->factory->newFromType( $this->config['underlying'], $this->config ); + } + + $underlyingPassword->crypt( $password ); + $iv = MWCryptRand::generate( openssl_cipher_iv_length( $this->params['cipher'] ), true ); + + $this->hash = openssl_encrypt( + $underlyingPassword->toString(), $this->params['cipher'], $secret, 0, $iv ); + $this->args = array( base64_encode( $iv ) ); + } + + /** + * Updates the underlying hash by encrypting it with the newest secret. + * + * @throws MWException If the configuration is not valid + * @return bool True if the password was updated + */ + public function update() { + if ( count( $this->args ) != 2 || $this->params == $this->getDefaultParams() ) { + // Hash does not need updating + return false; + } + + // Decrypt the underlying hash + $underlyingHash = openssl_decrypt( + base64_decode( $this->args[1] ), + $this->params['cipher'], + $this->config['secrets'][$this->params['secret']], + 0, + base64_decode( $this->args[0] ) + ); + + // Reset the params + $this->params = $this->getDefaultParams(); + + // Check the key size with the new params + $iv = MWCryptRand::generate( openssl_cipher_iv_length( $this->params['cipher'] ), true ); + $this->hash = base64_encode( openssl_encrypt( + $underlyingHash, + $this->params['cipher'], + $this->config['secrets'][$this->params['secret']], + 0, + $iv + ) ); + $this->args = array( base64_encode( $iv ) ); + + return true; + } +} diff --git a/includes/password/InvalidPassword.php b/includes/password/InvalidPassword.php new file mode 100644 index 00000000..e45b7744 --- /dev/null +++ b/includes/password/InvalidPassword.php @@ -0,0 +1,47 @@ +config['types'] as $type ) { + $passObj = $this->factory->newFromType( $type ); + + if ( !$passObj instanceof ParameterizedPassword ) { + throw new MWException( 'Underlying type must be a parameterized password.' ); + } elseif ( $passObj->getDelimiter() === $this->getDelimiter() ) { + throw new MWException( 'Underlying type cannot use same delimiter as encapsulating type.' ); + } + + $params[] = implode( $passObj->getDelimiter(), $passObj->getDefaultParams() ); + } + + return $params; + } + + public function crypt( $password ) { + $lastHash = $password; + foreach ( $this->config['types'] as $i => $type ) { + // Construct pseudo-hash based on params and arguments + /** @var ParameterizedPassword $passObj */ + $passObj = $this->factory->newFromType( $type ); + + $params = ''; + $args = ''; + if ( $this->params[$i] !== '' ) { + $params = $this->params[$i] . $passObj->getDelimiter(); + } + if ( isset( $this->args[$i] ) && $this->args[$i] !== '' ) { + $args = $this->args[$i] . $passObj->getDelimiter(); + } + $existingHash = ":$type:" . $params . $args . $this->hash; + + // Hash the last hash with the next type in the layer + $passObj = $this->factory->newFromCiphertext( $existingHash ); + $passObj->crypt( $lastHash ); + + // Move over the params and args + $this->params[$i] = implode( $passObj->getDelimiter(), $passObj->params ); + $this->args[$i] = implode( $passObj->getDelimiter(), $passObj->args ); + $lastHash = $passObj->hash; + } + + $this->hash = $lastHash; + } + + /** + * Finish the hashing of a partially hashed layered hash + * + * Given a password hash that is hashed using the first layer of this object's + * configuration, perform the remaining layers of password hashing in order to + * get an updated hash with all the layers. + * + * @param ParameterizedPassword $passObj Password hash of the first layer + * + * @throws MWException If the first parameter is not of the correct type + */ + public function partialCrypt( ParameterizedPassword $passObj ) { + $type = $passObj->config['type']; + if ( $type !== $this->config['types'][0] ) { + throw new MWException( 'Only a hash in the first layer can be finished.' ); + } + + // Gather info from the existing hash + $this->params[0] = implode( $passObj->getDelimiter(), $passObj->params ); + $this->args[0] = implode( $passObj->getDelimiter(), $passObj->args ); + $lastHash = $passObj->hash; + + // Layer the remaining types + foreach ( $this->config['types'] as $i => $type ) { + if ( $i == 0 ) { + continue; + }; + + // Construct pseudo-hash based on params and arguments + /** @var ParameterizedPassword $passObj */ + $passObj = $this->factory->newFromType( $type ); + + $params = ''; + $args = ''; + if ( $this->params[$i] !== '' ) { + $params = $this->params[$i] . $passObj->getDelimiter(); + } + if ( isset( $this->args[$i] ) && $this->args[$i] !== '' ) { + $args = $this->args[$i] . $passObj->getDelimiter(); + } + $existingHash = ":$type:" . $params . $args . $this->hash; + + // Hash the last hash with the next type in the layer + $passObj = $this->factory->newFromCiphertext( $existingHash ); + $passObj->crypt( $lastHash ); + + // Move over the params and args + $this->params[$i] = implode( $passObj->getDelimiter(), $passObj->params ); + $this->args[$i] = implode( $passObj->getDelimiter(), $passObj->args ); + $lastHash = $passObj->hash; + } + + $this->hash = $lastHash; + } +} diff --git a/includes/password/MWOldPassword.php b/includes/password/MWOldPassword.php new file mode 100644 index 00000000..afa5cacc --- /dev/null +++ b/includes/password/MWOldPassword.php @@ -0,0 +1,48 @@ +args ) === 1 ) { + $this->hash = md5( $this->args[0] . '-' . md5( $plaintext ) ); + } else { + $this->args = array(); + $this->hash = md5( $plaintext ); + } + } +} diff --git a/includes/password/MWSaltedPassword.php b/includes/password/MWSaltedPassword.php new file mode 100644 index 00000000..6c6895a2 --- /dev/null +++ b/includes/password/MWSaltedPassword.php @@ -0,0 +1,46 @@ +args ) == 0 ) { + $this->args[] = MWCryptRand::generateHex( 8 ); + } + + $this->hash = md5( $this->args[0] . '-' . md5( $plaintext ) ); + } +} diff --git a/includes/password/ParameterizedPassword.php b/includes/password/ParameterizedPassword.php new file mode 100644 index 00000000..187f8954 --- /dev/null +++ b/includes/password/ParameterizedPassword.php @@ -0,0 +1,119 @@ +:... as explained in the main Password + * class. This class is for hashes in the form of ::::... where + * , , etc. are parameters that determine how the password was hashed. + * Of course, the internal delimiter (which is : by convention and default), can be + * changed by overriding the ParameterizedPassword::getDelimiter() function. + * + * This class requires overriding an additional function: ParameterizedPassword::getDefaultParams(). + * See the function description for more details on the implementation. + * + * @since 1.24 + */ +abstract class ParameterizedPassword extends Password { + /** + * Named parameters that have default values for this password type + * @var array + */ + protected $params = array(); + + /** + * Extra arguments that were found in the hash. This may or may not make + * the hash invalid. + * @var array + */ + protected $args = array(); + + protected function parseHash( $hash ) { + parent::parseHash( $hash ); + + if ( $hash === null ) { + $this->params = $this->getDefaultParams(); + return; + } + + $parts = explode( $this->getDelimiter(), $hash ); + $paramKeys = array_keys( $this->getDefaultParams() ); + + if ( count( $parts ) < count( $paramKeys ) ) { + throw new PasswordError( 'Hash is missing required parameters.' ); + } + + if ( $paramKeys ) { + $this->args = array_splice( $parts, count( $paramKeys ) ); + $this->params = array_combine( $paramKeys, $parts ); + } else { + $this->args = $parts; + } + + if ( $this->args ) { + $this->hash = array_pop( $this->args ); + } else { + $this->hash = null; + } + } + + public function needsUpdate() { + return parent::needsUpdate() || $this->params !== $this->getDefaultParams(); + } + + public function toString() { + $str = ':' . $this->config['type'] . ':'; + + if ( count( $this->params ) || count( $this->args ) ) { + $str .= implode( $this->getDelimiter(), array_merge( $this->params, $this->args ) ); + $str .= $this->getDelimiter(); + } + + return $str . $this->hash; + } + + /** + * Returns the delimiter for the parameters inside the hash + * + * @return string + */ + abstract protected function getDelimiter(); + + /** + * Return an ordered array of default parameters for this password hash + * + * The keys should be the parameter names and the values should be the default + * values. Additionally, the order of the array should be the order in which they + * appear in the hash. + * + * When parsing a password hash, the constructor will split the hash based on + * the delimiter, and consume as many parts as it can, matching each to a parameter + * in this list. Once all the parameters have been filled, all remaining parts will + * be considered extra arguments, except, of course, for the very last part, which + * is the hash itself. + * + * @return array + */ + abstract protected function getDefaultParams(); +} diff --git a/includes/password/Password.php b/includes/password/Password.php new file mode 100644 index 00000000..4e395b51 --- /dev/null +++ b/includes/password/Password.php @@ -0,0 +1,186 @@ +:, where + * is the registered type of the hash. This prefix is stripped in the constructor + * and is added back in the toString() function. + * + * When inheriting this class, there are a couple of expectations + * to be fulfilled: + * * If Password::toString() is called on an object, and the result is passed back in + * to PasswordFactory::newFromCiphertext(), the result will be identical to the original. + * * The string representations of two Password objects are equal only if + * the original plaintext passwords match. In other words, if the toString() result of + * two objects match, the passwords are the same, and the user will be logged in. + * Since the string representation of a hash includes its type name (@see Password::toString), + * this property is preserved across all classes that inherit Password. + * If a hashing scheme does not fulfill this expectation, it must make sure to override the + * Password::equals() function and use custom comparison logic. However, this is not + * recommended unless absolutely required by the hashing mechanism. + * With these two points in mind, when creating a new Password sub-class, there are some functions + * you have to override (because they are abstract) and others that you may want to override. + * + * The abstract functions that must be overridden are: + * * Password::crypt(), which takes a plaintext password and hashes it into a string hash suitable + * for being passed to the constructor of that class, and then stores that hash (and whatever + * other data) into the internal state of the object. + * The functions that can optionally be overridden are: + * * Password::parseHash(), which can be useful to override if you need to extract values from or + * otherwise parse a password hash when it's passed to the constructor. + * * Password::needsUpdate(), which can be useful if a specific password hash has different + * logic for when the hash needs to be updated. + * * Password::toString(), which can be useful if the hash was changed in the constructor and + * needs to be re-assembled before being returned as a string. This function is expected to add + * the type back on to the hash, so make sure to do that if you override the function. + * * Password::equals() - This function compares two Password objects to see if they are equal. + * The default is to just do a timing-safe string comparison on the $this->hash values. + * + * After creating a new password hash type, it can be registered using the static + * Password::register() method. The default type is set using the Password::setDefaultType() type. + * Types must be registered before they can be set as the default. + * + * @since 1.24 + */ +abstract class Password { + /** + * @var PasswordFactory Factory that created the object + */ + protected $factory; + + /** + * String representation of the hash without the type + * @var string + */ + protected $hash; + + /** + * Array of configuration variables injected from the constructor + * @var array + */ + protected $config; + + /** + * Construct the Password object using a string hash + * + * It is strongly recommended not to call this function directly unless you + * have a reason to. Use the PasswordFactory class instead. + * + * @throws MWException If $config does not contain required parameters + * + * @param PasswordFactory $factory Factory object that created the password + * @param array $config Array of engine configuration options for hashing + * @param string|null $hash The raw hash, including the type + */ + final public function __construct( PasswordFactory $factory, array $config, $hash = null ) { + if ( !isset( $config['type'] ) ) { + throw new MWException( 'Password configuration must contain a type name.' ); + } + $this->config = $config; + $this->factory = $factory; + + if ( $hash !== null && strlen( $hash ) >= 3 ) { + // Strip the type from the hash for parsing + $hash = substr( $hash, strpos( $hash, ':', 1 ) + 1 ); + } + + $this->hash = $hash; + $this->parseHash( $hash ); + } + + /** + * Get the type name of the password + * + * @return string Password type + */ + final public function getType() { + return $this->config['type']; + } + + /** + * Perform any parsing necessary on the hash to see if the hash is valid + * and/or to perform logic for seeing if the hash needs updating. + * + * @param string $hash The hash, with the :: prefix stripped + * @throws PasswordError If there is an error in parsing the hash + */ + protected function parseHash( $hash ) { + } + + /** + * Determine if the hash needs to be updated + * + * @return bool True if needs update, false otherwise + */ + public function needsUpdate() { + } + + /** + * Compare one Password object to this object + * + * By default, do a timing-safe string comparison on the result of + * Password::toString() for each object. This can be overridden to do + * custom comparison, but it is not recommended unless necessary. + * + * @param Password|string $other The other password + * @return bool True if equal, false otherwise + */ + public function equals( $other ) { + if ( !$other instanceof self ) { + // No need to use the factory because we're definitely making + // an object of the same type. + $obj = clone $this; + $obj->crypt( $other ); + $other = $obj; + } + + return hash_equals( $this->toString(), $other->toString() ); + } + + /** + * Convert this hash to a string that can be stored in the database + * + * The resulting string should be considered the seralized representation + * of this hash, i.e., if the return value were recycled back into + * PasswordFactory::newFromCiphertext, the returned object would be equivalent to + * this; also, if two objects return the same value from this function, they + * are considered equivalent. + * + * @return string + */ + public function toString() { + return ':' . $this->config['type'] . ':' . $this->hash; + } + + /** + * Hash a password and store the result in this object + * + * The result of the password hash should be put into the internal + * state of the hash object. + * + * @param string $password Password to hash + * @throws PasswordError If an internal error occurs in hashing + */ + abstract public function crypt( $password ); +} diff --git a/includes/password/PasswordError.php b/includes/password/PasswordError.php new file mode 100644 index 00000000..c9707adb --- /dev/null +++ b/includes/password/PasswordError.php @@ -0,0 +1,28 @@ + array( 'type' => '', 'class' => 'InvalidPassword' ), + ); + + /** + * Register a new type of password hash + * + * @param string $type Unique type name for the hash + * @param array $config Array of configuration options + */ + public function register( $type, array $config ) { + $config['type'] = $type; + $this->types[$type] = $config; + } + + /** + * Set the default password type + * + * @throws InvalidArgumentException If the type is not registered + * @param string $type Password hash type + */ + public function setDefaultType( $type ) { + if ( !isset( $this->types[$type] ) ) { + throw new InvalidArgumentException( "Invalid password type $type." ); + } + $this->default = $type; + } + + /** + * Initialize the internal static variables using the global variables + * + * @param Config $config Configuration object to load data from + */ + public function init( Config $config ) { + foreach ( $config->get( 'PasswordConfig' ) as $type => $options ) { + $this->register( $type, $options ); + } + + $this->setDefaultType( $config->get( 'PasswordDefault' ) ); + } + + /** + * Get the list of types of passwords + * + * @return array + */ + public function getTypes() { + return $this->types; + } + + /** + * Create a new Hash object from an existing string hash + * + * Parse the type of a hash and create a new hash object based on the parsed type. + * Pass the raw hash to the constructor of the new object. Use InvalidPassword type + * if a null hash is given. + * + * @param string|null $hash Existing hash or null for an invalid password + * @return Password + * @throws PasswordError If hash is invalid or type is not recognized + */ + public function newFromCiphertext( $hash ) { + if ( $hash === null || $hash === false || $hash === '' ) { + return new InvalidPassword( $this, array( 'type' => '' ), null ); + } elseif ( $hash[0] !== ':' ) { + throw new PasswordError( 'Invalid hash given' ); + } + + $type = substr( $hash, 1, strpos( $hash, ':', 1 ) - 1 ); + if ( !isset( $this->types[$type] ) ) { + throw new PasswordError( "Unrecognized password hash type $type." ); + } + + $config = $this->types[$type]; + + return new $config['class']( $this, $config, $hash ); + } + + /** + * Make a new default password of the given type. + * + * @param string $type Existing type + * @return Password + * @throws PasswordError If hash is invalid or type is not recognized + */ + public function newFromType( $type ) { + if ( !isset( $this->types[$type] ) ) { + throw new PasswordError( "Unrecognized password hash type $type." ); + } + + $config = $this->types[$type]; + + return new $config['class']( $this, $config ); + } + + /** + * Create a new Hash object from a plaintext password + * + * If no existing object is given, make a new default object. If one is given, clone that + * object. Then pass the plaintext to Password::crypt(). + * + * @param string $password Plaintext password + * @param Password|null $existing Optional existing hash to get options from + * @return Password + */ + public function newFromPlaintext( $password, Password $existing = null ) { + if ( $existing === null ) { + $config = $this->types[$this->default]; + $obj = new $config['class']( $this, $config ); + } else { + $obj = clone $existing; + } + + $obj->crypt( $password ); + + return $obj; + } + + /** + * Determine whether a password object needs updating + * + * Check whether the given password is of the default type. If it is, + * pass off further needsUpdate checks to Password::needsUpdate. + * + * @param Password $password + * + * @return bool True if needs update, false otherwise + */ + public function needsUpdate( Password $password ) { + if ( $password->getType() !== $this->default ) { + return true; + } else { + return $password->needsUpdate(); + } + } +} diff --git a/includes/password/Pbkdf2Password.php b/includes/password/Pbkdf2Password.php new file mode 100644 index 00000000..080e3b0d --- /dev/null +++ b/includes/password/Pbkdf2Password.php @@ -0,0 +1,85 @@ + $this->config['algo'], + 'rounds' => $this->config['cost'], + 'length' => $this->config['length'] + ); + } + + protected function getDelimiter() { + return ':'; + } + + public function crypt( $password ) { + if ( count( $this->args ) == 0 ) { + $this->args[] = base64_encode( MWCryptRand::generate( 16, true ) ); + } + + if ( function_exists( 'hash_pbkdf2' ) ) { + $hash = hash_pbkdf2( + $this->params['algo'], + $password, + base64_decode( $this->args[0] ), + (int)$this->params['rounds'], + (int)$this->params['length'], + true + ); + } else { + $hashLen = strlen( hash( $this->params['algo'], '', true ) ); + $blockCount = ceil( $this->params['length'] / $hashLen ); + + $hash = ''; + $salt = base64_decode( $this->args[0] ); + for ( $i = 1; $i <= $blockCount; ++$i ) { + $roundTotal = $lastRound = hash_hmac( + $this->params['algo'], + $salt . pack( 'N', $i ), + $password, + true + ); + + for ( $j = 1; $j < $this->params['rounds']; ++$j ) { + $lastRound = hash_hmac( $this->params['algo'], $lastRound, $password, true ); + $roundTotal ^= $lastRound; + } + + $hash .= $roundTotal; + } + + $hash = substr( $hash, 0, $this->params['length'] ); + } + + $this->hash = base64_encode( $hash ); + } +} diff --git a/includes/poolcounter/PoolCounter.php b/includes/poolcounter/PoolCounter.php new file mode 100644 index 00000000..e77ffd7c --- /dev/null +++ b/includes/poolcounter/PoolCounter.php @@ -0,0 +1,173 @@ +workers = $conf['workers']; + $this->maxqueue = $conf['maxqueue']; + $this->timeout = $conf['timeout']; + if ( isset( $conf['slots'] ) ) { + $this->slots = $conf['slots']; + } + + if ( $this->slots ) { + $key = $this->hashKeyIntoSlots( $key, $this->slots ); + } + $this->key = $key; + } + + /** + * Create a Pool counter. This should only be called from the PoolWorks. + * + * @param string $type + * @param string $key + * + * @return PoolCounter + */ + public static function factory( $type, $key ) { + global $wgPoolCounterConf; + if ( !isset( $wgPoolCounterConf[$type] ) ) { + return new PoolCounter_Stub; + } + $conf = $wgPoolCounterConf[$type]; + $class = $conf['class']; + + return new $class( $conf, $type, $key ); + } + + /** + * @return string + */ + public function getKey() { + return $this->key; + } + + /** + * I want to do this task and I need to do it myself. + * + * @return Status Value is one of Locked/Error + */ + abstract public function acquireForMe(); + + /** + * I want to do this task, but if anyone else does it + * instead, it's also fine for me. I will read its cached data. + * + * @return Status Value is one of Locked/Done/Error + */ + abstract public function acquireForAnyone(); + + /** + * I have successfully finished my task. + * Lets another one grab the lock, and returns the workers + * waiting on acquireForAnyone() + * + * @return Status Value is one of Released/NotLocked/Error + */ + abstract public function release(); + + /** + * Given a key (any string) and the number of lots, returns a slot number (an integer from the [0..($slots-1)] range). + * This is used for a global limit on the number of instances of a given type that can acquire a lock. + * The hashing is deterministic so that PoolCounter::$workers is always an upper limit of how many instances with + * the same key can acquire a lock. + * + * @param string $key PoolCounter instance key (any string) + * @param int $slots The number of slots (max allowed value is 65536) + * @return int + */ + protected function hashKeyIntoSlots( $key, $slots ) { + return hexdec( substr( sha1( $key ), 0, 4 ) ) % $slots; + } +} + +// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps +class PoolCounter_Stub extends PoolCounter { + // @codingStandardsIgnoreEnd + + public function __construct() { + /* No parameters needed */ + } + + public function acquireForMe() { + return Status::newGood( PoolCounter::LOCKED ); + } + + public function acquireForAnyone() { + return Status::newGood( PoolCounter::LOCKED ); + } + + public function release() { + return Status::newGood( PoolCounter::RELEASED ); + } +} diff --git a/includes/poolcounter/PoolCounterRedis.php b/includes/poolcounter/PoolCounterRedis.php new file mode 100644 index 00000000..d609f614 --- /dev/null +++ b/includes/poolcounter/PoolCounterRedis.php @@ -0,0 +1,417 @@ + host) map */ + protected $serversByLabel; + /** @var string SHA-1 of the key */ + protected $keySha1; + /** @var int TTL for locks to expire (work should finish in this time) */ + protected $lockTTL; + + /** @var RedisConnRef */ + protected $conn; + /** @var string Pool slot value */ + protected $slot; + /** @var int AWAKE_* constant */ + protected $onRelease; + /** @var string Unique string to identify this process */ + protected $session; + /** @var int UNIX timestamp */ + protected $slotTime; + + const AWAKE_ONE = 1; // wake-up if when a slot can be taken from an existing process + const AWAKE_ALL = 2; // wake-up if an existing process finishes and wake up such others + + /** @var array List of active PoolCounterRedis objects in this script */ + protected static $active = null; + + function __construct( $conf, $type, $key ) { + parent::__construct( $conf, $type, $key ); + + $this->serversByLabel = $conf['servers']; + $this->ring = new HashRing( array_fill_keys( array_keys( $conf['servers'] ), 100 ) ); + + $conf['redisConfig']['serializer'] = 'none'; // for use with Lua + $this->pool = RedisConnectionPool::singleton( $conf['redisConfig'] ); + + $this->keySha1 = sha1( $this->key ); + $met = ini_get( 'max_execution_time' ); // usually 0 in CLI mode + $this->lockTTL = $met ? 2 * $met : 3600; + + if ( self::$active === null ) { + self::$active = array(); + register_shutdown_function( array( __CLASS__, 'releaseAll' ) ); + } + } + + /** + * @return Status Uses RediConnRef as value on success + */ + protected function getConnection() { + if ( !isset( $this->conn ) ) { + $conn = false; + $servers = $this->ring->getLocations( $this->key, 3 ); + ArrayUtils::consistentHashSort( $servers, $this->key ); + foreach ( $servers as $server ) { + $conn = $this->pool->getConnection( $this->serversByLabel[$server] ); + if ( $conn ) { + break; + } + } + if ( !$conn ) { + return Status::newFatal( 'pool-servererror', implode( ', ', $servers ) ); + } + $this->conn = $conn; + } + return Status::newGood( $this->conn ); + } + + function acquireForMe() { + $section = new ProfileSection( __METHOD__ ); + + return $this->waitForSlotOrNotif( self::AWAKE_ONE ); + } + + function acquireForAnyone() { + $section = new ProfileSection( __METHOD__ ); + + return $this->waitForSlotOrNotif( self::AWAKE_ALL ); + } + + function release() { + $section = new ProfileSection( __METHOD__ ); + + if ( $this->slot === null ) { + return Status::newGood( PoolCounter::NOT_LOCKED ); // not locked + } + + $status = $this->getConnection(); + if ( !$status->isOK() ) { + return $status; + } + $conn = $status->value; + + static $script = +<<= 1*rMaxWorkers then + -- Slots somehow got out of sync; reset the list for sanity + redis.call('del',kSlots,kSlotsNextRelease) + elseif redis.call('lLen',kSlots) == (1*rMaxWorkers - 1) and redis.call('zCard',kWaiting) == 0 then + -- Slot list will be made full; clear it to save space (it re-inits as needed) + -- since nothing is waiting on being unblocked by a push to the list + redis.call('del',kSlots,kSlotsNextRelease) + else + -- Add slot back to pool and update the "next release" time + redis.call('rPush',kSlots,rSlot) + redis.call('zAdd',kSlotsNextRelease,rTime + 30,rSlot) + -- Always keep renewing the expiry on use + redis.call('expireAt',kSlots,math.ceil(rTime + rExpiry)) + redis.call('expireAt',kSlotsNextRelease,math.ceil(rTime + rExpiry)) + end + end + -- Update an ephemeral list to wake up other clients that can + -- reuse any cached work from this process. Only do this if no + -- slots are currently free (e.g. clients could be waiting). + if 1*rAwakeAll == 1 then + local count = redis.call('zCard',kWaiting) + for i = 1,count do + redis.call('rPush',kWakeup,'w') + end + redis.call('pexpire',kWakeup,1) + end + return 1 +LUA; + try { + $res = $conn->luaEval( $script, + array( + $this->getSlotListKey(), + $this->getSlotRTimeSetKey(), + $this->getWakeupListKey(), + $this->getWaitSetKey(), + $this->workers, + $this->lockTTL, + $this->slot, + $this->slotTime, // used for CAS-style sanity check + ( $this->onRelease === self::AWAKE_ALL ) ? 1 : 0, + microtime( true ) + ), + 4 # number of first argument(s) that are keys + ); + } catch ( RedisException $e ) { + return Status::newFatal( 'pool-error-unknown', $e->getMessage() ); + } + + $this->slot = null; + $this->slotTime = null; + $this->onRelease = null; + unset( self::$active[$this->session] ); + + return Status::newGood( PoolCounter::RELEASED ); + } + + /** + * @param int $doWakeup AWAKE_* constant + * @return Status + */ + protected function waitForSlotOrNotif( $doWakeup ) { + if ( $this->slot !== null ) { + return Status::newGood( PoolCounter::LOCK_HELD ); // already acquired + } + + $status = $this->getConnection(); + if ( !$status->isOK() ) { + return $status; + } + $conn = $status->value; + + $now = microtime( true ); + try { + $slot = $this->initAndPopPoolSlotList( $conn, $now ); + if ( ctype_digit( $slot ) ) { + // Pool slot acquired by this process + $slotTime = $now; + } elseif ( $slot === 'QUEUE_FULL' ) { + // Too many processes are waiting for pooled processes to finish + return Status::newGood( PoolCounter::QUEUE_FULL ); + } elseif ( $slot === 'QUEUE_WAIT' ) { + // This process is now registered as waiting + $keys = ( $doWakeup == self::AWAKE_ALL ) + // Wait for an open slot or wake-up signal (preferring the later) + ? array( $this->getWakeupListKey(), $this->getSlotListKey() ) + // Just wait for an actual pool slot + : array( $this->getSlotListKey() ); + + $res = $conn->blPop( $keys, $this->timeout ); + if ( $res === array() ) { + $conn->zRem( $this->getWaitSetKey(), $this->session ); // no longer waiting + return Status::newGood( PoolCounter::TIMEOUT ); + } + + $slot = $res[1]; // pool slot or "w" for wake-up notifications + $slotTime = microtime( true ); // last microtime() was a few RTTs ago + // Unregister this process as waiting and bump slot "next release" time + $this->registerAcquisitionTime( $conn, $slot, $slotTime ); + } else { + return Status::newFatal( 'pool-error-unknown', "Server gave slot '$slot'." ); + } + } catch ( RedisException $e ) { + return Status::newFatal( 'pool-error-unknown', $e->getMessage() ); + } + + if ( $slot !== 'w' ) { + $this->slot = $slot; + $this->slotTime = $slotTime; + $this->onRelease = $doWakeup; + self::$active[$this->session] = $this; + } + + return Status::newGood( $slot === 'w' ? PoolCounter::DONE : PoolCounter::LOCKED ); + } + + /** + * @param RedisConnRef $conn + * @param float $now UNIX timestamp + * @return string|bool False on failure + */ + protected function initAndPopPoolSlotList( RedisConnRef $conn, $now ) { + static $script = +<< 0 then + slot = redis.call('lPop',kSlots) + -- Update the slot "next release" time + redis.call('zAdd',kSlotsNextRelease,rTime + rExpiry,slot) + elseif redis.call('zCard',kSlotWaits) >= 1*rMaxQueue then + slot = 'QUEUE_FULL' + else + slot = 'QUEUE_WAIT' + -- Register this process as waiting + redis.call('zAdd',kSlotWaits,rTime,rSess) + redis.call('expireAt',kSlotWaits,math.ceil(rTime + 2*rTimeout)) + end + -- Always keep renewing the expiry on use + redis.call('expireAt',kSlots,math.ceil(rTime + rExpiry)) + redis.call('expireAt',kSlotsNextRelease,math.ceil(rTime + rExpiry)) + return slot +LUA; + return $conn->luaEval( $script, + array( + $this->getSlotListKey(), + $this->getSlotRTimeSetKey(), + $this->getWaitSetKey(), + $this->workers, + $this->maxqueue, + $this->timeout, + $this->lockTTL, + $this->session, + $now + ), + 3 # number of first argument(s) that are keys + ); + } + + /** + * @param RedisConnRef $conn + * @param string $slot + * @param float $now + * @return int|bool False on failure + */ + protected function registerAcquisitionTime( RedisConnRef $conn, $slot, $now ) { + static $script = +<<luaEval( $script, + array( + $this->getSlotListKey(), + $this->getSlotRTimeSetKey(), + $this->getWaitSetKey(), + $slot, + $this->lockTTL, + $this->session, + $now + ), + 3 # number of first argument(s) that are keys + ); + } + + /** + * @return string + */ + protected function getSlotListKey() { + return "poolcounter:l-slots-{$this->keySha1}-{$this->workers}"; + } + + /** + * @return string + */ + protected function getSlotRTimeSetKey() { + return "poolcounter:z-renewtime-{$this->keySha1}-{$this->workers}"; + } + + /** + * @return string + */ + protected function getWaitSetKey() { + return "poolcounter:z-wait-{$this->keySha1}-{$this->workers}"; + } + + /** + * @return string + */ + protected function getWakeupListKey() { + return "poolcounter:l-wakeup-{$this->keySha1}-{$this->workers}"; + } + + /** + * Try to make sure that locks get released (even with exceptions and fatals) + */ + public static function releaseAll() { + foreach ( self::$active as $poolCounter ) { + try { + if ( $poolCounter->slot !== null ) { + $poolCounter->release(); + } + } catch ( Exception $e ) { + } + } + } +} diff --git a/includes/poolcounter/PoolCounterWork.php b/includes/poolcounter/PoolCounterWork.php new file mode 100644 index 00000000..c0be7a1b --- /dev/null +++ b/includes/poolcounter/PoolCounterWork.php @@ -0,0 +1,160 @@ +type = $type; + $this->poolCounter = PoolCounter::factory( $type, $key ); + } + + /** + * Actually perform the work, caching it if needed + * @return mixed Work result or false + */ + abstract public function doWork(); + + /** + * Retrieve the work from cache + * @return mixed Work result or false + */ + public function getCachedWork() { + return false; + } + + /** + * A work not so good (eg. expired one) but better than an error + * message. + * @return mixed Work result or false + */ + public function fallback() { + return false; + } + + /** + * Do something with the error, like showing it to the user. + * + * @param Status $status + * + * @return bool + */ + public function error( $status ) { + return false; + } + + /** + * Log an error + * + * @param Status $status + * @return void + */ + public function logError( $status ) { + $key = $this->poolCounter->getKey(); + + wfDebugLog( 'poolcounter', "Pool key '$key' ({$this->type}): " + . $status->getMessage()->inLanguage( 'en' )->useDatabase( false )->text() ); + } + + /** + * Get the result of the work (whatever it is), or the result of the error() function. + * This returns the result of the first applicable method that returns a non-false value, + * where the methods are checked in the following order: + * - a) doWork() : Applies if the work is exclusive or no another process + * is doing it, and on the condition that either this process + * successfully entered the pool or the pool counter is down. + * - b) doCachedWork() : Applies if the work is cacheable and this blocked on another + * process which finished the work. + * - c) fallback() : Applies for all remaining cases. + * If these all fall through (by returning false), then the result of error() is returned. + * + * @param bool $skipcache + * @return mixed + */ + public function execute( $skipcache = false ) { + if ( $this->cacheable && !$skipcache ) { + $status = $this->poolCounter->acquireForAnyone(); + } else { + $status = $this->poolCounter->acquireForMe(); + } + + if ( !$status->isOK() ) { + // Respond gracefully to complete server breakage: just log it and do the work + $this->logError( $status ); + return $this->doWork(); + } + + switch ( $status->value ) { + case PoolCounter::LOCK_HELD: + // Better to ignore nesting pool counter limits than to fail. + // Assume that the outer pool limiting is reasonable enough. + /* no break */ + case PoolCounter::LOCKED: + $result = $this->doWork(); + $this->poolCounter->release(); + return $result; + + case PoolCounter::DONE: + $result = $this->getCachedWork(); + if ( $result === false ) { + /* That someone else work didn't serve us. + * Acquire the lock for me + */ + return $this->execute( true ); + } + return $result; + + case PoolCounter::QUEUE_FULL: + case PoolCounter::TIMEOUT: + $result = $this->fallback(); + + if ( $result !== false ) { + return $result; + } + /* no break */ + + /* These two cases should never be hit... */ + case PoolCounter::ERROR: + default: + $errors = array( + PoolCounter::QUEUE_FULL => 'pool-queuefull', + PoolCounter::TIMEOUT => 'pool-timeout' ); + + $status = Status::newFatal( isset( $errors[$status->value] ) + ? $errors[$status->value] + : 'pool-errorunknown' ); + $this->logError( $status ); + return $this->error( $status ); + } + } +} diff --git a/includes/poolcounter/PoolCounterWorkViaCallback.php b/includes/poolcounter/PoolCounterWorkViaCallback.php new file mode 100644 index 00000000..af83d2e0 --- /dev/null +++ b/includes/poolcounter/PoolCounterWorkViaCallback.php @@ -0,0 +1,92 @@ +$name = $callbacks[$name]; + } + } + if ( !isset( $this->doWork ) ) { + throw new MWException( "No callback provided for 'doWork' function." ); + } + $this->cacheable = isset( $this->doCachedWork ); + } + + public function doWork() { + return call_user_func_array( $this->doWork, array() ); + } + + public function getCachedWork() { + if ( $this->doCachedWork ) { + return call_user_func_array( $this->doCachedWork, array() ); + } + return false; + } + + public function fallback() { + if ( $this->fallback ) { + return call_user_func_array( $this->fallback, array() ); + } + return false; + } + + public function error( $status ) { + if ( $this->error ) { + return call_user_func_array( $this->error, array( $status ) ); + } + return false; + } +} diff --git a/includes/poolcounter/PoolWorkArticleView.php b/includes/poolcounter/PoolWorkArticleView.php new file mode 100644 index 00000000..5e7e3912 --- /dev/null +++ b/includes/poolcounter/PoolWorkArticleView.php @@ -0,0 +1,208 @@ +getRevision()->getContentModel(); + $format = $page->getRevision()->getContentFormat(); + $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format ); + } + + $this->page = $page; + $this->revid = $revid; + $this->cacheable = $useParserCache; + $this->parserOptions = $parserOptions; + $this->content = $content; + $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions ); + parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid ); + } + + /** + * Get the ParserOutput from this object, or false in case of failure + * + * @return ParserOutput + */ + public function getParserOutput() { + return $this->parserOutput; + } + + /** + * Get whether the ParserOutput is a dirty one (i.e. expired) + * + * @return bool + */ + public function getIsDirty() { + return $this->isDirty; + } + + /** + * Get a Status object in case of error or false otherwise + * + * @return Status|bool + */ + public function getError() { + return $this->error; + } + + /** + * @return bool + */ + public function doWork() { + global $wgUseFileCache; + + // @todo several of the methods called on $this->page are not declared in Page, but present + // in WikiPage and delegated by Article. + + $isCurrent = $this->revid === $this->page->getLatest(); + + if ( $this->content !== null ) { + $content = $this->content; + } elseif ( $isCurrent ) { + // XXX: why use RAW audience here, and PUBLIC (default) below? + $content = $this->page->getContent( Revision::RAW ); + } else { + $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid ); + + if ( $rev === null ) { + $content = null; + } else { + // XXX: why use PUBLIC audience here (default), and RAW above? + $content = $rev->getContent(); + } + } + + if ( $content === null ) { + return false; + } + + // Reduce effects of race conditions for slow parses (bug 46014) + $cacheTime = wfTimestampNow(); + + $time = - microtime( true ); + $this->parserOutput = $content->getParserOutput( + $this->page->getTitle(), + $this->revid, + $this->parserOptions + ); + $time += microtime( true ); + + // Timing hack + if ( $time > 3 ) { + wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, + $this->page->getTitle()->getPrefixedDBkey() ) ); + } + + if ( $this->cacheable && $this->parserOutput->isCacheable() && $isCurrent ) { + ParserCache::singleton()->save( + $this->parserOutput, $this->page, $this->parserOptions, $cacheTime, $this->revid ); + } + + // Make sure file cache is not used on uncacheable content. + // Output that has magic words in it can still use the parser cache + // (if enabled), though it will generally expire sooner. + if ( !$this->parserOutput->isCacheable() || $this->parserOutput->containsOldMagic() ) { + $wgUseFileCache = false; + } + + if ( $isCurrent ) { + $this->page->doCascadeProtectionUpdates( $this->parserOutput ); + } + + return true; + } + + /** + * @return bool + */ + public function getCachedWork() { + $this->parserOutput = ParserCache::singleton()->get( $this->page, $this->parserOptions ); + + if ( $this->parserOutput === false ) { + wfDebug( __METHOD__ . ": parser cache miss\n" ); + return false; + } else { + wfDebug( __METHOD__ . ": parser cache hit\n" ); + return true; + } + } + + /** + * @return bool + */ + public function fallback() { + $this->parserOutput = ParserCache::singleton()->getDirty( $this->page, $this->parserOptions ); + + if ( $this->parserOutput === false ) { + wfDebugLog( 'dirty', 'dirty missing' ); + wfDebug( __METHOD__ . ": no dirty cache\n" ); + return false; + } else { + wfDebug( __METHOD__ . ": sending dirty output\n" ); + wfDebugLog( 'dirty', "dirty output {$this->cacheKey}" ); + $this->isDirty = true; + return true; + } + } + + /** + * @param Status $status + * @return bool + */ + public function error( $status ) { + $this->error = $status; + return false; + } +} diff --git a/includes/profiler/Profiler.php b/includes/profiler/Profiler.php index 2282a3af..418b5d48 100644 --- a/includes/profiler/Profiler.php +++ b/includes/profiler/Profiler.php @@ -19,16 +19,30 @@ * * @file * @ingroup Profiler - * This file is only included if profiling is enabled + * @defgroup Profiler Profiler */ /** - * @defgroup Profiler Profiler + * Get system resource usage of current request context. + * Invokes the getrusage(2) system call, requesting RUSAGE_SELF if on PHP5 + * or RUSAGE_THREAD if on HHVM. Returns false if getrusage is not available. + * + * @since 1.24 + * @return array|bool Resource usage data or false if no data available. */ +function wfGetRusage() { + if ( !function_exists( 'getrusage' ) ) { + return false; + } elseif ( defined ( 'HHVM_VERSION' ) ) { + return getrusage( 2 /* RUSAGE_THREAD */ ); + } else { + return getrusage( 0 /* RUSAGE_SELF */ ); + } +} /** * Begin profiling of a function - * @param string $functionname name of the function we will profile + * @param string $functionname Name of the function we will profile */ function wfProfileIn( $functionname ) { if ( Profiler::$__instance === null ) { // use this directly to reduce overhead @@ -41,7 +55,7 @@ function wfProfileIn( $functionname ) { /** * Stop profiling of a function - * @param string $functionname name of the function we have profiled + * @param string $functionname Name of the function we have profiled */ function wfProfileOut( $functionname = 'missing' ) { if ( Profiler::$__instance === null ) { // use this directly to reduce overhead @@ -91,45 +105,46 @@ class ProfileSection { } /** + * Profiler base class that defines the interface and some trivial functionality + * * @ingroup Profiler - * @todo document */ -class Profiler { - protected $mStack = array(), $mWorkStack = array(), $mCollated = array(), - $mCalls = array(), $mTotals = array(); - protected $mTimeMetric = 'wall'; - protected $mProfileID = false, $mCollateDone = false, $mTemplated = false; - - protected $mDBLockThreshold = 5.0; // float; seconds - /** @var Array DB/server name => (active trx count,timestamp) */ - protected $mDBTrxHoldingLocks = array(); - /** @var Array DB/server name => list of (method, elapsed time) */ - protected $mDBTrxMethodTimes = array(); +abstract class Profiler { + /** @var string|bool Profiler ID for bucketing data */ + protected $mProfileID = false; + /** @var bool Whether MediaWiki is in a SkinTemplate output context */ + protected $mTemplated = false; - /** @var Profiler */ - public static $__instance = null; // do not call this outside Profiler and ProfileSection + /** @var TransactionProfiler */ + protected $trxProfiler; - function __construct( $params ) { - if ( isset( $params['timeMetric'] ) ) { - $this->mTimeMetric = $params['timeMetric']; - } + // @codingStandardsIgnoreStart PSR2.Classes.PropertyDeclaration.Underscore + /** @var Profiler Do not call this outside Profiler and ProfileSection */ + public static $__instance = null; + // @codingStandardsIgnoreEnd + + /** + * @param array $params + */ + public function __construct( array $params ) { if ( isset( $params['profileID'] ) ) { $this->mProfileID = $params['profileID']; } - - $this->addInitialStack(); + $this->trxProfiler = new TransactionProfiler(); } /** * Singleton * @return Profiler */ - public static function instance() { + final public static function instance() { if ( self::$__instance === null ) { global $wgProfiler; if ( is_array( $wgProfiler ) ) { if ( !isset( $wgProfiler['class'] ) ) { $class = 'ProfilerStub'; + } elseif ( $wgProfiler['class'] === 'Profiler' ) { + $class = 'ProfilerStub'; // b/c; don't explode } else { $class = $wgProfiler['class']; } @@ -137,7 +152,7 @@ class Profiler { } elseif ( $wgProfiler instanceof Profiler ) { self::$__instance = $wgProfiler; // back-compat } else { - self::$__instance = new ProfilerStub( $wgProfiler ); + self::$__instance = new ProfilerStub( array() ); } } return self::$__instance; @@ -145,35 +160,41 @@ class Profiler { /** * Set the profiler to a specific profiler instance. Mostly for dumpHTML - * @param $p Profiler object + * @param Profiler $p */ - public static function setInstance( Profiler $p ) { + final public static function setInstance( Profiler $p ) { self::$__instance = $p; } /** * Return whether this a stub profiler * - * @return Boolean + * @return bool */ - public function isStub() { - return false; - } + abstract public function isStub(); /** * Return whether this profiler stores data * + * Called by Parser::braceSubstitution. If true, the parser will not + * generate per-title profiling sections, to avoid overloading the + * profiling data collector. + * * @see Profiler::logData() - * @return Boolean + * @return bool */ - public function isPersistent() { - return true; - } + abstract public function isPersistent(); + /** + * @param string $id + */ public function setProfileID( $id ) { $this->mProfileID = $id; } + /** + * @return string + */ public function getProfileID() { if ( $this->mProfileID === false ) { return wfWikiID(); @@ -182,77 +203,19 @@ class Profiler { } } - /** - * Add the inital item in the stack. - */ - protected function addInitialStack() { - // Push an entry for the pre-profile setup time onto the stack - $initial = $this->getInitialTime(); - if ( $initial !== null ) { - $this->mWorkStack[] = array( '-total', 0, $initial, 0 ); - $this->mStack[] = array( '-setup', 1, $initial, 0, $this->getTime(), 0 ); - } else { - $this->profileIn( '-total' ); - } - } - /** * Called by wfProfieIn() * - * @param $functionname String + * @param string $functionname */ - public function profileIn( $functionname ) { - global $wgDebugFunctionEntry; - if ( $wgDebugFunctionEntry ) { - $this->debug( str_repeat( ' ', count( $this->mWorkStack ) ) . 'Entering ' . $functionname . "\n" ); - } - - $this->mWorkStack[] = array( $functionname, count( $this->mWorkStack ), $this->getTime(), memory_get_usage() ); - } + abstract public function profileIn( $functionname ); /** * Called by wfProfieOut() * - * @param $functionname String - */ - public function profileOut( $functionname ) { - global $wgDebugFunctionEntry; - $memory = memory_get_usage(); - $time = $this->getTime(); - - if ( $wgDebugFunctionEntry ) { - $this->debug( str_repeat( ' ', count( $this->mWorkStack ) - 1 ) . 'Exiting ' . $functionname . "\n" ); - } - - $bit = array_pop( $this->mWorkStack ); - - if ( !$bit ) { - $this->debug( "Profiling error, !\$bit: $functionname\n" ); - } else { - if ( $functionname == 'close' ) { - $message = "Profile section ended by close(): {$bit[0]}"; - $this->debug( "$message\n" ); - $this->mStack[] = array( $message, 0, 0.0, 0, 0.0, 0 ); - } elseif ( $bit[0] != $functionname ) { - $message = "Profiling error: in({$bit[0]}), out($functionname)"; - $this->debug( "$message\n" ); - $this->mStack[] = array( $message, 0, 0.0, 0, 0.0, 0 ); - } - $bit[] = $time; - $bit[] = $memory; - $this->mStack[] = $bit; - $this->updateTrxProfiling( $functionname, $time ); - } - } - - /** - * Close opened profiling sections + * @param string $functionname */ - public function close() { - while ( count( $this->mWorkStack ) ) { - $this->profileOut( 'close' ); - } - } + abstract public function profileOut( $functionname ); /** * Mark a DB as in a transaction with one or more writes pending @@ -261,39 +224,10 @@ class Profiler { * * @param string $server DB server * @param string $db DB name + * @param string $id Resource ID string of connection */ - public function transactionWritingIn( $server, $db ) { - $name = "{$server} ({$db})"; - if ( isset( $this->mDBTrxHoldingLocks[$name] ) ) { - ++$this->mDBTrxHoldingLocks[$name]['refs']; - } else { - $this->mDBTrxHoldingLocks[$name] = array( 'refs' => 1, 'start' => microtime( true ) ); - $this->mDBTrxMethodTimes[$name] = array(); - } - } - - /** - * Register the name and time of a method for slow DB trx detection - * - * @param string $method Function name - * @param float $realtime Wal time ellapsed - */ - protected function updateTrxProfiling( $method, $realtime ) { - if ( !$this->mDBTrxHoldingLocks ) { - return; // short-circuit - // @TODO: hardcoded check is a tad janky (what about FOR UPDATE?) - } elseif ( !preg_match( '/^query-m: (?!SELECT)/', $method ) - && $realtime < $this->mDBLockThreshold ) - { - return; // not a DB master query nor slow enough - } - $now = microtime( true ); - foreach ( $this->mDBTrxHoldingLocks as $name => $info ) { - // Hacky check to exclude entries from before the first TRX write - if ( ( $now - $realtime ) >= $info['start'] ) { - $this->mDBTrxMethodTimes[$name][] = array( $method, $realtime ); - } - } + public function transactionWritingIn( $server, $db, $id = '' ) { + $this->trxProfiler->transactionWritingIn( $server, $db, $id ); } /** @@ -305,144 +239,60 @@ class Profiler { * * @param string $server DB server * @param string $db DB name + * @param string $id Resource ID string of connection */ - public function transactionWritingOut( $server, $db ) { - $name = "{$server} ({$db})"; - if ( --$this->mDBTrxHoldingLocks[$name]['refs'] <= 0 ) { - $slow = false; - foreach ( $this->mDBTrxMethodTimes[$name] as $info ) { - list( $method, $realtime ) = $info; - if ( $realtime >= $this->mDBLockThreshold ) { - $slow = true; - break; - } - } - if ( $slow ) { - $dbs = implode( ', ', array_keys( $this->mDBTrxHoldingLocks ) ); - $msg = "Sub-optimal transaction on DB(s) {$dbs}:\n"; - foreach ( $this->mDBTrxMethodTimes[$name] as $i => $info ) { - list( $method, $realtime ) = $info; - $msg .= sprintf( "%d\t%.6f\t%s\n", $i, $realtime, $method ); - } - wfDebugLog( 'DBPerformance', $msg ); - } - unset( $this->mDBTrxHoldingLocks[$name] ); - unset( $this->mDBTrxMethodTimes[$name] ); - } + public function transactionWritingOut( $server, $db, $id = '' ) { + $this->trxProfiler->transactionWritingOut( $server, $db, $id ); } /** - * Mark this call as templated or not - * - * @param $t Boolean + * Close opened profiling sections */ - function setTemplated( $t ) { - $this->mTemplated = $t; - } + abstract public function close(); /** - * Returns a profiling output to be stored in debug file - * - * @return String + * Log the data to some store or even the page output */ - public function getOutput() { - global $wgDebugFunctionEntry, $wgProfileCallTree; - $wgDebugFunctionEntry = false; - - if ( !count( $this->mStack ) && !count( $this->mCollated ) ) { - return "No profiling output\n"; - } - - if ( $wgProfileCallTree ) { - return $this->getCallTree(); - } else { - return $this->getFunctionReport(); - } - } + abstract public function logData(); /** - * Returns a tree of function call instead of a list of functions - * @return string + * Mark this call as templated or not + * + * @param bool $t */ - function getCallTree() { - return implode( '', array_map( array( &$this, 'getCallTreeLine' ), $this->remapCallTree( $this->mStack ) ) ); + public function setTemplated( $t ) { + $this->mTemplated = $t; } /** - * Recursive function the format the current profiling array into a tree + * Returns a profiling output to be stored in debug file * - * @param array $stack profiling array - * @return array + * @return string */ - function remapCallTree( $stack ) { - if ( count( $stack ) < 2 ) { - return $stack; - } - $outputs = array(); - for ( $max = count( $stack ) - 1; $max > 0; ) { - /* Find all items under this entry */ - $level = $stack[$max][1]; - $working = array(); - for ( $i = $max -1; $i >= 0; $i-- ) { - if ( $stack[$i][1] > $level ) { - $working[] = $stack[$i]; - } else { - break; - } - } - $working = $this->remapCallTree( array_reverse( $working ) ); - $output = array(); - foreach ( $working as $item ) { - array_push( $output, $item ); - } - array_unshift( $output, $stack[$max] ); - $max = $i; - - array_unshift( $outputs, $output ); - } - $final = array(); - foreach ( $outputs as $output ) { - foreach ( $output as $item ) { - $final[] = $item; - } - } - return $final; - } + abstract public function getOutput(); /** - * Callback to get a formatted line for the call tree - * @return string + * @return array */ - function getCallTreeLine( $entry ) { - list( $fname, $level, $start, /* $x */, $end ) = $entry; - $delta = $end - $start; - $space = str_repeat( ' ', $level ); - # The ugly double sprintf is to work around a PHP bug, - # which has been fixed in recent releases. - return sprintf( "%10s %s %s\n", trim( sprintf( "%7.3f", $delta * 1000.0 ) ), $space, $fname ); - } + abstract public function getRawData(); /** * Get the initial time of the request, based either on $wgRequestTime or * $wgRUstart. Will return null if not able to find data. * - * @param string|false $metric metric to use, with the following possibilities: + * @param string|bool $metric Metric to use, with the following possibilities: * - user: User CPU time (without system calls) * - cpu: Total CPU time (user and system calls) * - wall (or any other string): elapsed time * - false (default): will fall back to default metric * @return float|null */ - function getTime( $metric = false ) { - if ( $metric === false ) { - $metric = $this->mTimeMetric; - } - - if ( $metric === 'cpu' || $this->mTimeMetric === 'user' ) { - if ( !function_exists( 'getrusage' ) ) { + protected function getTime( $metric = 'wall' ) { + if ( $metric === 'cpu' || $metric === 'user' ) { + $ru = wfGetRusage(); + if ( !$ru ) { return 0; } - $ru = getrusage(); $time = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6; if ( $metric === 'cpu' ) { # This is the time of system calls, added to the user time @@ -459,21 +309,17 @@ class Profiler { * Get the initial time of the request, based either on $wgRequestTime or * $wgRUstart. Will return null if not able to find data. * - * @param string|false $metric metric to use, with the following possibilities: + * @param string|bool $metric Metric to use, with the following possibilities: * - user: User CPU time (without system calls) * - cpu: Total CPU time (user and system calls) * - wall (or any other string): elapsed time * - false (default): will fall back to default metric * @return float|null */ - protected function getInitialTime( $metric = false ) { + protected function getInitialTime( $metric = 'wall' ) { global $wgRequestTime, $wgRUstart; - if ( $metric === false ) { - $metric = $this->mTimeMetric; - } - - if ( $metric === 'cpu' || $this->mTimeMetric === 'user' ) { + if ( $metric === 'cpu' || $metric === 'user' ) { if ( !count( $wgRUstart ) ) { return null; } @@ -494,243 +340,130 @@ class Profiler { } } - protected function collateData() { - if ( $this->mCollateDone ) { - return; - } - $this->mCollateDone = true; - - $this->close(); - - $this->mCollated = array(); - $this->mCalls = array(); - $this->mMemory = array(); - - # Estimate profiling overhead - $profileCount = count( $this->mStack ); - self::calculateOverhead( $profileCount ); - - # First, subtract the overhead! - $overheadTotal = $overheadMemory = $overheadInternal = array(); - foreach ( $this->mStack as $entry ) { - $fname = $entry[0]; - $start = $entry[2]; - $end = $entry[4]; - $elapsed = $end - $start; - $memory = $entry[5] - $entry[3]; - - if ( $fname == '-overhead-total' ) { - $overheadTotal[] = $elapsed; - $overheadMemory[] = $memory; - } elseif ( $fname == '-overhead-internal' ) { - $overheadInternal[] = $elapsed; - } - } - $overheadTotal = $overheadTotal ? array_sum( $overheadTotal ) / count( $overheadInternal ) : 0; - $overheadMemory = $overheadMemory ? array_sum( $overheadMemory ) / count( $overheadInternal ) : 0; - $overheadInternal = $overheadInternal ? array_sum( $overheadInternal ) / count( $overheadInternal ) : 0; - - # Collate - foreach ( $this->mStack as $index => $entry ) { - $fname = $entry[0]; - $start = $entry[2]; - $end = $entry[4]; - $elapsed = $end - $start; - - $memory = $entry[5] - $entry[3]; - $subcalls = $this->calltreeCount( $this->mStack, $index ); - - if ( !preg_match( '/^-overhead/', $fname ) ) { - # Adjust for profiling overhead (except special values with elapsed=0 - if ( $elapsed ) { - $elapsed -= $overheadInternal; - $elapsed -= ( $subcalls * $overheadTotal ); - $memory -= ( $subcalls * $overheadMemory ); - } - } - - if ( !array_key_exists( $fname, $this->mCollated ) ) { - $this->mCollated[$fname] = 0; - $this->mCalls[$fname] = 0; - $this->mMemory[$fname] = 0; - $this->mMin[$fname] = 1 << 24; - $this->mMax[$fname] = 0; - $this->mOverhead[$fname] = 0; - } - - $this->mCollated[$fname] += $elapsed; - $this->mCalls[$fname]++; - $this->mMemory[$fname] += $memory; - $this->mMin[$fname] = min( $this->mMin[$fname], $elapsed ); - $this->mMax[$fname] = max( $this->mMax[$fname], $elapsed ); - $this->mOverhead[$fname] += $subcalls; - } - - $this->mCalls['-overhead-total'] = $profileCount; - arsort( $this->mCollated, SORT_NUMERIC ); - } - /** - * Returns a list of profiled functions. + * Add an entry in the debug log file * - * @return string - */ - function getFunctionReport() { - $this->collateData(); - - $width = 140; - $nameWidth = $width - 65; - $format = "%-{$nameWidth}s %6d %13.3f %13.3f %13.3f%% %9d (%13.3f -%13.3f) [%d]\n"; - $titleFormat = "%-{$nameWidth}s %6s %13s %13s %13s %9s\n"; - $prof = "\nProfiling data\n"; - $prof .= sprintf( $titleFormat, 'Name', 'Calls', 'Total', 'Each', '%', 'Mem' ); - - $total = isset( $this->mCollated['-total'] ) ? $this->mCollated['-total'] : 0; - - foreach ( $this->mCollated as $fname => $elapsed ) { - $calls = $this->mCalls[$fname]; - $percent = $total ? 100. * $elapsed / $total : 0; - $memory = $this->mMemory[$fname]; - $prof .= sprintf( $format, - substr( $fname, 0, $nameWidth ), - $calls, - (float)( $elapsed * 1000 ), - (float)( $elapsed * 1000 ) / $calls, - $percent, - $memory, - ( $this->mMin[$fname] * 1000.0 ), - ( $this->mMax[$fname] * 1000.0 ), - $this->mOverhead[$fname] - ); - } - $prof .= "\nTotal: $total\n\n"; - - return $prof; - } - - /** - * Dummy calls to wfProfileIn/wfProfileOut to calculate its overhead + * @param string $s String to output */ - protected static function calculateOverhead( $profileCount ) { - wfProfileIn( '-overhead-total' ); - for ( $i = 0; $i < $profileCount; $i++ ) { - wfProfileIn( '-overhead-internal' ); - wfProfileOut( '-overhead-internal' ); + protected function debug( $s ) { + if ( function_exists( 'wfDebug' ) ) { + wfDebug( $s ); } - wfProfileOut( '-overhead-total' ); } /** - * Counts the number of profiled function calls sitting under - * the given point in the call graph. Not the most efficient algo. + * Add an entry in the debug log group * - * @param $stack Array: - * @param $start Integer: - * @return Integer - * @private + * @param string $group Group to send the message to + * @param string $s String to output */ - function calltreeCount( $stack, $start ) { - $level = $stack[$start][1]; - $count = 0; - for ( $i = $start -1; $i >= 0 && $stack[$i][1] > $level; $i-- ) { - $count ++; + protected function debugGroup( $group, $s ) { + if ( function_exists( 'wfDebugLog' ) ) { + wfDebugLog( $group, $s ); } - return $count; } +} + +/** + * Helper class that detects high-contention DB queries via profiling calls + * + * This class is meant to work with a Profiler, as the later already knows + * when methods start and finish (which may take place during transactions). + * + * @since 1.24 + */ +class TransactionProfiler { + /** @var float Seconds */ + protected $mDBLockThreshold = 3.0; + /** @var array DB/server name => (active trx count, time, DBs involved) */ + protected $mDBTrxHoldingLocks = array(); + /** @var array DB/server name => list of (function name, elapsed time) */ + protected $mDBTrxMethodTimes = array(); /** - * Log the whole profiling data into the database. + * Mark a DB as in a transaction with one or more writes pending + * + * Note that there can be multiple connections to a single DB. + * + * @param string $server DB server + * @param string $db DB name + * @param string $id ID string of transaction */ - public function logData() { - global $wgProfilePerHost, $wgProfileToDatabase; - - # Do not log anything if database is readonly (bug 5375) - if ( wfReadOnly() || !$wgProfileToDatabase ) { - return; - } - - $dbw = wfGetDB( DB_MASTER ); - if ( !is_object( $dbw ) ) { - return; + public function transactionWritingIn( $server, $db, $id ) { + $name = "{$server} ({$db}) (TRX#$id)"; + if ( isset( $this->mDBTrxHoldingLocks[$name] ) ) { + wfDebugLog( 'DBPerformance', "Nested transaction for '$name' - out of sync." ); } + $this->mDBTrxHoldingLocks[$name] = + array( 'start' => microtime( true ), 'conns' => array() ); + $this->mDBTrxMethodTimes[$name] = array(); - if ( $wgProfilePerHost ) { - $pfhost = wfHostname(); - } else { - $pfhost = ''; + foreach ( $this->mDBTrxHoldingLocks as $name => &$info ) { + $info['conns'][$name] = 1; // track all DBs in transactions for this transaction } - - try { - $this->collateData(); - - foreach ( $this->mCollated as $name => $elapsed ) { - $eventCount = $this->mCalls[$name]; - $timeSum = (float)( $elapsed * 1000 ); - $memorySum = (float)$this->mMemory[$name]; - $name = substr( $name, 0, 255 ); - - // Kludge - $timeSum = $timeSum >= 0 ? $timeSum : 0; - $memorySum = $memorySum >= 0 ? $memorySum : 0; - - $dbw->update( 'profiling', - array( - "pf_count=pf_count+{$eventCount}", - "pf_time=pf_time+{$timeSum}", - "pf_memory=pf_memory+{$memorySum}", - ), - array( - 'pf_name' => $name, - 'pf_server' => $pfhost, - ), - __METHOD__ ); - - $rc = $dbw->affectedRows(); - if ( $rc == 0 ) { - $dbw->insert( 'profiling', array( 'pf_name' => $name, 'pf_count' => $eventCount, - 'pf_time' => $timeSum, 'pf_memory' => $memorySum, 'pf_server' => $pfhost ), - __METHOD__, array( 'IGNORE' ) ); - } - // When we upgrade to mysql 4.1, the insert+update - // can be merged into just a insert with this construct added: - // "ON DUPLICATE KEY UPDATE ". - // "pf_count=pf_count + VALUES(pf_count), ". - // "pf_time=pf_time + VALUES(pf_time)"; - } - } catch ( DBError $e ) {} } /** - * Get the function name of the current profiling section - * @return - */ - function getCurrentSection() { - $elt = end( $this->mWorkStack ); - return $elt[0]; - } - - /** - * Add an entry in the debug log file + * Register the name and time of a method for slow DB trx detection * - * @param string $s to output + * This method is only to be called by the Profiler class as methods finish + * + * @param string $method Function name + * @param float $realtime Wal time ellapsed */ - function debug( $s ) { - if ( function_exists( 'wfDebug' ) ) { - wfDebug( $s ); + public function recordFunctionCompletion( $method, $realtime ) { + if ( !$this->mDBTrxHoldingLocks ) { + return; // short-circuit + // @todo hardcoded check is a tad janky (what about FOR UPDATE?) + } elseif ( !preg_match( '/^query-m: (?!SELECT)/', $method ) + && $realtime < $this->mDBLockThreshold + ) { + return; // not a DB master query nor slow enough + } + $now = microtime( true ); + foreach ( $this->mDBTrxHoldingLocks as $name => $info ) { + // Hacky check to exclude entries from before the first TRX write + if ( ( $now - $realtime ) >= $info['start'] ) { + $this->mDBTrxMethodTimes[$name][] = array( $method, $realtime ); + } } } /** - * Get the content type sent out to the client. - * Used for profilers that output instead of store data. - * @return string + * Mark a DB as no longer in a transaction + * + * This will check if locks are possibly held for longer than + * needed and log any affected transactions to a special DB log. + * Note that there can be multiple connections to a single DB. + * + * @param string $server DB server + * @param string $db DB name + * @param string $id ID string of transaction */ - protected function getContentType() { - foreach ( headers_list() as $header ) { - if ( preg_match( '#^content-type: (\w+/\w+);?#i', $header, $m ) ) { - return $m[1]; + public function transactionWritingOut( $server, $db, $id ) { + $name = "{$server} ({$db}) (TRX#$id)"; + if ( !isset( $this->mDBTrxMethodTimes[$name] ) ) { + wfDebugLog( 'DBPerformance', "Detected no transaction for '$name' - out of sync." ); + return; + } + $slow = false; + foreach ( $this->mDBTrxMethodTimes[$name] as $info ) { + $realtime = $info[1]; + if ( $realtime >= $this->mDBLockThreshold ) { + $slow = true; + break; + } + } + if ( $slow ) { + $dbs = implode( ', ', array_keys( $this->mDBTrxHoldingLocks[$name]['conns'] ) ); + $msg = "Sub-optimal transaction on DB(s) [{$dbs}]:\n"; + foreach ( $this->mDBTrxMethodTimes[$name] as $i => $info ) { + list( $method, $realtime ) = $info; + $msg .= sprintf( "%d\t%.6f\t%s\n", $i, $realtime, $method ); } + wfDebugLog( 'DBPerformance', $msg ); } - return null; + unset( $this->mDBTrxHoldingLocks[$name] ); + unset( $this->mDBTrxMethodTimes[$name] ); } } diff --git a/includes/profiler/ProfilerMwprof.php b/includes/profiler/ProfilerMwprof.php new file mode 100644 index 00000000..af3c7741 --- /dev/null +++ b/includes/profiler/ProfilerMwprof.php @@ -0,0 +1,256 @@ + aggregate data array) */ + protected $mCollated = array(); + /** @var array Cache of a standard broken collation entry */ + protected $mErrorEntry; + + // Message types + const TYPE_SINGLE = 1; + const TYPE_RUNNING = 2; + + public function isStub() { + return false; + } + + public function isPersistent() { + return true; + } + + /** + * Start a profiling section. + * + * Marks the beginning of the function or code-block that should be time + * and logged under some specific name. + * + * @param string $inName Section to start + */ + public function profileIn( $inName ) { + $this->mWorkStack[] = array( $inName, count( $this->mWorkStack ), + $this->getTime(), $this->getTime( 'cpu' ), 0 ); + } + + /** + * Close a profiling section. + * + * Marks the end of the function or code-block that should be timed and + * logged under some specific name. + * + * @param string $outName Section to close + */ + public function profileOut( $outName ) { + list( $inName, $inCount, $inWall, $inCpu ) = array_pop( $this->mWorkStack ); + + // Check for unbalanced profileIn / profileOut calls. + // Bad entries are logged but not sent. + if ( $inName !== $outName ) { + $this->debugGroup( 'ProfilerUnbalanced', json_encode( array( $inName, $outName ) ) ); + return; + } + + $elapsedCpu = $this->getTime( 'cpu' ) - $inCpu; + $elapsedWall = $this->getTime() - $inWall; + $this->updateRunningEntry( $outName, $elapsedCpu, $elapsedWall ); + $this->trxProfiler->recordFunctionCompletion( $outName, $elapsedWall ); + } + + /** + * Update an entry with timing data. + * + * @param string $name Section name + * @param float $elapsedCpu Elapsed CPU time + * @param float $elapsedWall Elapsed wall-clock time + */ + public function updateRunningEntry( $name, $elapsedCpu, $elapsedWall ) { + // If this is the first measurement for this entry, store plain values. + // Many profiled functions will only be called once per request. + if ( !isset( $this->mCollated[$name] ) ) { + $this->mCollated[$name] = array( + 'cpu' => $elapsedCpu, + 'wall' => $elapsedWall, + 'count' => 1, + ); + return; + } + + $entry = &$this->mCollated[$name]; + + // If it's the second measurement, convert the plain values to + // RunningStat instances, so we can push the incoming values on top. + if ( $entry['count'] === 1 ) { + $cpu = new RunningStat(); + $cpu->push( $entry['cpu'] ); + $entry['cpu'] = $cpu; + + $wall = new RunningStat(); + $wall->push( $entry['wall'] ); + $entry['wall'] = $wall; + } + + $entry['count']++; + $entry['cpu']->push( $elapsedCpu ); + $entry['wall']->push( $elapsedWall ); + } + + /** + * @return array + */ + public function getRawData() { + // This method is called before shutdown in the footer method on Skins. + // If some outer methods have not yet called wfProfileOut(), work around + // that by clearing anything in the work stack to just the "-total" entry. + if ( count( $this->mWorkStack ) > 1 ) { + $oldWorkStack = $this->mWorkStack; + $this->mWorkStack = array( $this->mWorkStack[0] ); // just the "-total" one + } else { + $oldWorkStack = null; + } + $this->close(); + // If this trick is used, then the old work stack is swapped back afterwards. + // This means that logData() will still make use of all the method data since + // the missing wfProfileOut() calls should be made by the time it is called. + if ( $oldWorkStack ) { + $this->mWorkStack = $oldWorkStack; + } + + $totalWall = 0.0; + $profile = array(); + foreach ( $this->mCollated as $fname => $data ) { + if ( $data['count'] == 1 ) { + $profile[] = array( + 'name' => $fname, + 'calls' => $data['count'], + 'elapsed' => $data['wall'] * 1000, + 'memory' => 0, // not supported + 'min' => $data['wall'] * 1000, + 'max' => $data['wall'] * 1000, + 'overhead' => 0, // not supported + 'periods' => array() // not supported + ); + $totalWall += $data['wall']; + } else { + $profile[] = array( + 'name' => $fname, + 'calls' => $data['count'], + 'elapsed' => $data['wall']->n * $data['wall']->getMean() * 1000, + 'memory' => 0, // not supported + 'min' => $data['wall']->min * 1000, + 'max' => $data['wall']->max * 1000, + 'overhead' => 0, // not supported + 'periods' => array() // not supported + ); + $totalWall += $data['wall']->n * $data['wall']->getMean(); + } + } + $totalWall = $totalWall * 1000; + + foreach ( $profile as &$item ) { + $item['percent'] = $totalWall ? 100 * $item['elapsed'] / $totalWall : 0; + } + + return $profile; + } + + /** + * Serialize profiling data and send to a profiling data aggregator. + * + * Individual entries are represented as arrays and then encoded using + * MessagePack, an efficient binary data-interchange format. Encoded + * entries are accumulated into a buffer and sent in batch via UDP to the + * profiling data aggregator. + */ + public function logData() { + global $wgUDPProfilerHost, $wgUDPProfilerPort; + + $this->close(); + + if ( !function_exists( 'socket_create' ) ) { + return; // avoid fatal + } + + $sock = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); + socket_connect( $sock, $wgUDPProfilerHost, $wgUDPProfilerPort ); + $bufferLength = 0; + $buffer = ''; + foreach ( $this->mCollated as $name => $entry ) { + $count = $entry['count']; + $cpu = $entry['cpu']; + $wall = $entry['wall']; + + if ( $count === 1 ) { + $data = array( self::TYPE_SINGLE, $name, $cpu, $wall ); + } else { + $data = array( self::TYPE_RUNNING, $name, $count, + $cpu->m1, $cpu->m2, $cpu->min, $cpu->max, + $wall->m1, $wall->m2, $wall->min, $wall->max ); + } + + $encoded = MWMessagePack::pack( $data ); + $length = strlen( $encoded ); + + // If adding this entry would cause the size of the buffer to + // exceed the standard ethernet MTU size less the UDP header, + // send all pending data and reset the buffer. Otherwise, continue + // accumulating entries into the current buffer. + if ( $length + $bufferLength > 1450 ) { + socket_send( $sock, $buffer, $bufferLength, 0 ); + $buffer = ''; + $bufferLength = 0; + } + $buffer .= $encoded; + $bufferLength += $length; + } + if ( $bufferLength !== 0 ) { + socket_send( $sock, $buffer, $bufferLength, 0 ); + } + } + + /** + * Close opened profiling sections + */ + public function close() { + while ( count( $this->mWorkStack ) ) { + $this->profileOut( 'close' ); + } + } + + public function getOutput() { + return ''; // no report + } +} diff --git a/includes/profiler/ProfilerSimple.php b/includes/profiler/ProfilerSimple.php deleted file mode 100644 index 805c60f4..00000000 --- a/includes/profiler/ProfilerSimple.php +++ /dev/null @@ -1,133 +0,0 @@ - 0.0, 'cpu_sq' => 0.0, 'real' => 0.0, 'real_sq' => 0.0, 'count' => 0 ); - var $errorEntry; - - public function isPersistent() { - /* Implement in output subclasses */ - return false; - } - - protected function addInitialStack() { - $this->errorEntry = $this->zeroEntry; - $this->errorEntry['count'] = 1; - - $initialTime = $this->getInitialTime(); - $initialCpu = $this->getInitialTime( 'cpu' ); - if ( $initialTime !== null && $initialCpu !== null ) { - $this->mWorkStack[] = array( '-total', 0, $initialTime, $initialCpu ); - $this->mWorkStack[] = array( '-setup', 1, $initialTime, $initialCpu ); - - $this->profileOut( '-setup' ); - } else { - $this->profileIn( '-total' ); - } - } - - function setMinimum( $min ) { - $this->mMinimumTime = $min; - } - - function profileIn( $functionname ) { - global $wgDebugFunctionEntry; - if ( $wgDebugFunctionEntry ) { - $this->debug( str_repeat( ' ', count( $this->mWorkStack ) ) . 'Entering ' . $functionname . "\n" ); - } - $this->mWorkStack[] = array( $functionname, count( $this->mWorkStack ), $this->getTime(), $this->getTime( 'cpu' ) ); - } - - function profileOut( $functionname ) { - global $wgDebugFunctionEntry; - - if ( $wgDebugFunctionEntry ) { - $this->debug( str_repeat( ' ', count( $this->mWorkStack ) - 1 ) . 'Exiting ' . $functionname . "\n" ); - } - - list( $ofname, /* $ocount */, $ortime, $octime ) = array_pop( $this->mWorkStack ); - - if ( !$ofname ) { - $this->debug( "Profiling error: $functionname\n" ); - } else { - if ( $functionname == 'close' ) { - $message = "Profile section ended by close(): {$ofname}"; - $functionname = $ofname; - $this->debug( "$message\n" ); - $this->mCollated[$message] = $this->errorEntry; - } - elseif ( $ofname != $functionname ) { - $message = "Profiling error: in({$ofname}), out($functionname)"; - $this->debug( "$message\n" ); - $this->mCollated[$message] = $this->errorEntry; - } - $entry =& $this->mCollated[$functionname]; - $elapsedcpu = $this->getTime( 'cpu' ) - $octime; - $elapsedreal = $this->getTime() - $ortime; - if ( !is_array( $entry ) ) { - $entry = $this->zeroEntry; - $this->mCollated[$functionname] =& $entry; - } - $entry['cpu'] += $elapsedcpu; - $entry['cpu_sq'] += $elapsedcpu * $elapsedcpu; - $entry['real'] += $elapsedreal; - $entry['real_sq'] += $elapsedreal * $elapsedreal; - $entry['count']++; - - $this->updateTrxProfiling( $functionname, $elapsedreal ); - } - } - - public function getFunctionReport() { - /* Implement in output subclasses */ - return ''; - } - - public function logData() { - /* Implement in subclasses */ - } - - /** - * Get the actual CPU time or the initial one if $ru is set. - * - * @deprecated in 1.20 - * @return float|null - */ - function getCpuTime( $ru = null ) { - wfDeprecated( __METHOD__, '1.20' ); - - if ( $ru === null ) { - return $this->getTime( 'cpu' ); - } else { - # It theory we should use $ru here, but it always $wgRUstart that is passed here - return $this->getInitialTime( 'cpu' ); - } - } -} diff --git a/includes/profiler/ProfilerSimpleDB.php b/includes/profiler/ProfilerSimpleDB.php new file mode 100644 index 00000000..7ef0ad05 --- /dev/null +++ b/includes/profiler/ProfilerSimpleDB.php @@ -0,0 +1,111 @@ +collateData(); + + $dbw = wfGetDB( DB_MASTER ); + $useTrx = ( $dbw->getType() === 'sqlite' ); // much faster + if ( $useTrx ) { + $dbw->startAtomic( __METHOD__ ); + } + foreach ( $this->mCollated as $name => $data ) { + $eventCount = $data['count']; + $timeSum = (float)( $data['real'] * 1000 ); + $memorySum = (float)$data['memory']; + $name = substr( $name, 0, 255 ); + + // Kludge + $timeSum = $timeSum >= 0 ? $timeSum : 0; + $memorySum = $memorySum >= 0 ? $memorySum : 0; + + $dbw->update( 'profiling', + array( + "pf_count=pf_count+{$eventCount}", + "pf_time=pf_time+{$timeSum}", + "pf_memory=pf_memory+{$memorySum}", + ), + array( + 'pf_name' => $name, + 'pf_server' => $pfhost, + ), + __METHOD__ ); + + $rc = $dbw->affectedRows(); + if ( $rc == 0 ) { + $dbw->insert( 'profiling', + array( + 'pf_name' => $name, + 'pf_count' => $eventCount, + 'pf_time' => $timeSum, + 'pf_memory' => $memorySum, + 'pf_server' => $pfhost + ), + __METHOD__, + array( 'IGNORE' ) + ); + } + // When we upgrade to mysql 4.1, the insert+update + // can be merged into just a insert with this construct added: + // "ON DUPLICATE KEY UPDATE ". + // "pf_count=pf_count + VALUES(pf_count), ". + // "pf_time=pf_time + VALUES(pf_time)"; + } + if ( $useTrx ) { + $dbw->endAtomic( __METHOD__ ); + } + } catch ( DBError $e ) { + } + } +} diff --git a/includes/profiler/ProfilerSimpleText.php b/includes/profiler/ProfilerSimpleText.php index 1d57ea8d..0ee7aad2 100644 --- a/includes/profiler/ProfilerSimpleText.php +++ b/includes/profiler/ProfilerSimpleText.php @@ -31,7 +31,7 @@ * * @ingroup Profiler */ -class ProfilerSimpleText extends ProfilerSimple { +class ProfilerSimpleText extends ProfilerStandard { public $visible = false; /* Show as
 or ";
-		} elseif ( $this->getContentType() === 'text/html' ) {
-			print "";
-		} elseif ( $this->getContentType() === 'text/javascript' ) {
-			print "\n/*\n {$this->trace}\n*/";
-		} elseif ( $this->getContentType() === 'text/css' ) {
-			print "\n/*\n {$this->trace}\n*/";
+	public function logData() {
+		if ( $this->mTemplated ) {
+			if ( PHP_SAPI === 'cli' ) {
+				print "";
+			} elseif ( $this->getContentType() === 'text/html' ) {
+				print "";
+			} elseif ( $this->getContentType() === 'text/javascript' ) {
+				print "\n/*\n {$this->trace}\n*/";
+			} elseif ( $this->getContentType() === 'text/css' ) {
+				print "\n/*\n {$this->trace}\n*/";
+			}
 		}
 	}
 }
diff --git a/includes/profiler/ProfilerSimpleUDP.php b/includes/profiler/ProfilerSimpleUDP.php
index 0a1f3b10..627b4de2 100644
--- a/includes/profiler/ProfilerSimpleUDP.php
+++ b/includes/profiler/ProfilerSimpleUDP.php
@@ -23,10 +23,15 @@
 
 /**
  * ProfilerSimpleUDP class, that sends out messages for 'udpprofile' daemon
- * (the one from mediawiki/trunk/udpprofile SVN )
+ * (the one from
+ *  http://git.wikimedia.org/tree/operations%2Fsoftware.git/master/udpprofile)
  * @ingroup Profiler
  */
-class ProfilerSimpleUDP extends ProfilerSimple {
+class ProfilerSimpleUDP extends ProfilerStandard {
+	protected function collateOnly() {
+		return true;
+	}
+
 	public function isPersistent() {
 		return true;
 	}
@@ -36,11 +41,6 @@ class ProfilerSimpleUDP extends ProfilerSimple {
 
 		$this->close();
 
-		if ( isset( $this->mCollated['-total'] ) && $this->mCollated['-total']['real'] < $this->mMinimumTime ) {
-			# Less than minimum, ignore
-			return;
-		}
-
 		if ( !function_exists( 'socket_create' ) ) {
 			# Sockets are not enabled
 			return;
@@ -58,7 +58,8 @@ class ProfilerSimpleUDP extends ProfilerSimple {
 				continue;
 			}
 			$pfline = sprintf( $wgUDPProfilerFormatString, $this->getProfileID(), $pfdata['count'],
-				$pfdata['cpu'], $pfdata['cpu_sq'], $pfdata['real'], $pfdata['real_sq'], $entry );
+				$pfdata['cpu'], $pfdata['cpu_sq'], $pfdata['real'], $pfdata['real_sq'], $entry,
+				$pfdata['memory'] );
 			$length = strlen( $pfline );
 			/* printf(""); */
 			if ( $length + $plength > 1400 ) {
diff --git a/includes/profiler/ProfilerStandard.php b/includes/profiler/ProfilerStandard.php
new file mode 100644
index 00000000..cc134165
--- /dev/null
+++ b/includes/profiler/ProfilerStandard.php
@@ -0,0 +1,559 @@
+ aggregate data array) */
+	protected $mCollated = array();
+	/** @var bool */
+	protected $mCollateDone = false;
+	/** @var bool */
+	protected $mCollateOnly = false;
+	/** @var array Cache of a standard broken collation entry */
+	protected $mErrorEntry;
+
+	/**
+	 * @param array $params
+	 */
+	public function __construct( array $params ) {
+		parent::__construct( $params );
+
+		$this->mCollateOnly = $this->collateOnly();
+
+		$this->addInitialStack();
+	}
+
+	/**
+	 * Return whether this a stub profiler
+	 *
+	 * @return bool
+	 */
+	public function isStub() {
+		return false;
+	}
+
+	/**
+	 * Return whether this profiler stores data
+	 *
+	 * @see Profiler::logData()
+	 * @return bool
+	 */
+	public function isPersistent() {
+		return false;
+	}
+
+	/**
+	 * Whether to internally just track aggregates and ignore the full stack trace
+	 *
+	 * Only doing collation saves memory overhead but limits the use of certain
+	 * features like that of graph generation for the debug toolbar.
+	 *
+	 * @return bool
+	 */
+	protected function collateOnly() {
+		return false;
+	}
+
+	/**
+	 * Add the inital item in the stack.
+	 */
+	protected function addInitialStack() {
+		$this->mErrorEntry = $this->getErrorEntry();
+
+		$initialTime = $this->getInitialTime( 'wall' );
+		$initialCpu = $this->getInitialTime( 'cpu' );
+		if ( $initialTime !== null && $initialCpu !== null ) {
+			$this->mWorkStack[] = array( '-total', 0, $initialTime, $initialCpu, 0 );
+			if ( $this->mCollateOnly ) {
+				$this->mWorkStack[] = array( '-setup', 1, $initialTime, $initialCpu, 0 );
+				$this->profileOut( '-setup' );
+			} else {
+				$this->mStack[] = array( '-setup', 1, $initialTime, $initialCpu, 0,
+					$this->getTime( 'wall' ), $this->getTime( 'cpu' ), 0 );
+			}
+		} else {
+			$this->profileIn( '-total' );
+		}
+	}
+
+	/**
+	 * @return array Initial collation entry
+	 */
+	protected function getZeroEntry() {
+		return array(
+			'cpu'      => 0.0,
+			'cpu_sq'   => 0.0,
+			'real'     => 0.0,
+			'real_sq'  => 0.0,
+			'memory'   => 0,
+			'count'    => 0,
+			'min_cpu'  => 0.0,
+			'max_cpu'  => 0.0,
+			'min_real' => 0.0,
+			'max_real' => 0.0,
+			'periods'  => array(), // not filled if mCollateOnly
+			'overhead' => 0 // not filled if mCollateOnly
+		);
+	}
+
+	/**
+	 * @return array Initial collation entry for errors
+	 */
+	protected function getErrorEntry() {
+		$entry = $this->getZeroEntry();
+		$entry['count'] = 1;
+		return $entry;
+	}
+
+	/**
+	 * Update the collation entry for a given method name
+	 *
+	 * @param string $name
+	 * @param float $elapsedCpu
+	 * @param float $elapsedReal
+	 * @param int $memChange
+	 * @param int $subcalls
+	 * @param array|null $period Map of ('start','end','memory','subcalls')
+	 */
+	protected function updateEntry(
+		$name, $elapsedCpu, $elapsedReal, $memChange, $subcalls = 0, $period = null
+	) {
+		$entry =& $this->mCollated[$name];
+		if ( !is_array( $entry ) ) {
+			$entry = $this->getZeroEntry();
+			$this->mCollated[$name] =& $entry;
+		}
+		$entry['cpu'] += $elapsedCpu;
+		$entry['cpu_sq'] += $elapsedCpu * $elapsedCpu;
+		$entry['real'] += $elapsedReal;
+		$entry['real_sq'] += $elapsedReal * $elapsedReal;
+		$entry['memory'] += $memChange > 0 ? $memChange : 0;
+		$entry['count']++;
+		$entry['min_cpu'] = $elapsedCpu < $entry['min_cpu'] ? $elapsedCpu : $entry['min_cpu'];
+		$entry['max_cpu'] = $elapsedCpu > $entry['max_cpu'] ? $elapsedCpu : $entry['max_cpu'];
+		$entry['min_real'] = $elapsedReal < $entry['min_real'] ? $elapsedReal : $entry['min_real'];
+		$entry['max_real'] = $elapsedReal > $entry['max_real'] ? $elapsedReal : $entry['max_real'];
+		// Apply optional fields
+		$entry['overhead'] += $subcalls;
+		if ( $period ) {
+			$entry['periods'][] = $period;
+		}
+	}
+
+	/**
+	 * Called by wfProfieIn()
+	 *
+	 * @param string $functionname
+	 */
+	public function profileIn( $functionname ) {
+		global $wgDebugFunctionEntry;
+
+		if ( $wgDebugFunctionEntry ) {
+			$this->debug( str_repeat( ' ', count( $this->mWorkStack ) ) .
+				'Entering ' . $functionname . "\n" );
+		}
+
+		$this->mWorkStack[] = array(
+			$functionname,
+			count( $this->mWorkStack ),
+			$this->getTime( 'time' ),
+			$this->getTime( 'cpu' ),
+			memory_get_usage()
+		);
+	}
+
+	/**
+	 * Called by wfProfieOut()
+	 *
+	 * @param string $functionname
+	 */
+	public function profileOut( $functionname ) {
+		global $wgDebugFunctionEntry;
+
+		if ( $wgDebugFunctionEntry ) {
+			$this->debug( str_repeat( ' ', count( $this->mWorkStack ) - 1 ) .
+				'Exiting ' . $functionname . "\n" );
+		}
+
+		$item = array_pop( $this->mWorkStack );
+		list( $ofname, /* $ocount */, $ortime, $octime, $omem ) = $item;
+
+		if ( $item === null ) {
+			$this->debugGroup( 'profileerror', "Profiling error: $functionname" );
+		} else {
+			if ( $functionname === 'close' ) {
+				if ( $ofname !== '-total' ) {
+					$message = "Profile section ended by close(): {$ofname}";
+					$this->debugGroup( 'profileerror', $message );
+					if ( $this->mCollateOnly ) {
+						$this->mCollated[$message] = $this->mErrorEntry;
+					} else {
+						$this->mStack[] = array( $message, 0, 0.0, 0.0, 0, 0.0, 0.0, 0 );
+					}
+				}
+				$functionname = $ofname;
+			} elseif ( $ofname !== $functionname ) {
+				$message = "Profiling error: in({$ofname}), out($functionname)";
+				$this->debugGroup( 'profileerror', $message );
+				if ( $this->mCollateOnly ) {
+					$this->mCollated[$message] = $this->mErrorEntry;
+				} else {
+					$this->mStack[] = array( $message, 0, 0.0, 0.0, 0, 0.0, 0.0, 0 );
+				}
+			}
+			$realTime = $this->getTime( 'wall' );
+			$cpuTime = $this->getTime( 'cpu' );
+			if ( $this->mCollateOnly ) {
+				$elapsedcpu = $cpuTime - $octime;
+				$elapsedreal = $realTime - $ortime;
+				$memchange = memory_get_usage() - $omem;
+				$this->updateEntry( $functionname, $elapsedcpu, $elapsedreal, $memchange );
+			} else {
+				$this->mStack[] = array_merge( $item,
+					array( $realTime, $cpuTime,	memory_get_usage() ) );
+			}
+			$this->trxProfiler->recordFunctionCompletion( $functionname, $realTime - $ortime );
+		}
+	}
+
+	/**
+	 * Close opened profiling sections
+	 */
+	public function close() {
+		while ( count( $this->mWorkStack ) ) {
+			$this->profileOut( 'close' );
+		}
+	}
+
+	/**
+	 * Log the data to some store or even the page output
+	 */
+	public function logData() {
+		/* Implement in subclasses */
+	}
+
+	/**
+	 * Returns a profiling output to be stored in debug file
+	 *
+	 * @return string
+	 */
+	public function getOutput() {
+		global $wgDebugFunctionEntry, $wgProfileCallTree;
+
+		$wgDebugFunctionEntry = false; // hack
+
+		if ( !count( $this->mStack ) && !count( $this->mCollated ) ) {
+			return "No profiling output\n";
+		}
+
+		if ( $wgProfileCallTree ) {
+			return $this->getCallTree();
+		} else {
+			return $this->getFunctionReport();
+		}
+	}
+
+	/**
+	 * Returns a tree of function call instead of a list of functions
+	 * @return string
+	 */
+	protected function getCallTree() {
+		return implode( '', array_map(
+			array( &$this, 'getCallTreeLine' ), $this->remapCallTree( $this->mStack )
+		) );
+	}
+
+	/**
+	 * Recursive function the format the current profiling array into a tree
+	 *
+	 * @param array $stack Profiling array
+	 * @return array
+	 */
+	protected function remapCallTree( array $stack ) {
+		if ( count( $stack ) < 2 ) {
+			return $stack;
+		}
+		$outputs = array();
+		for ( $max = count( $stack ) - 1; $max > 0; ) {
+			/* Find all items under this entry */
+			$level = $stack[$max][1];
+			$working = array();
+			for ( $i = $max -1; $i >= 0; $i-- ) {
+				if ( $stack[$i][1] > $level ) {
+					$working[] = $stack[$i];
+				} else {
+					break;
+				}
+			}
+			$working = $this->remapCallTree( array_reverse( $working ) );
+			$output = array();
+			foreach ( $working as $item ) {
+				array_push( $output, $item );
+			}
+			array_unshift( $output, $stack[$max] );
+			$max = $i;
+
+			array_unshift( $outputs, $output );
+		}
+		$final = array();
+		foreach ( $outputs as $output ) {
+			foreach ( $output as $item ) {
+				$final[] = $item;
+			}
+		}
+		return $final;
+	}
+
+	/**
+	 * Callback to get a formatted line for the call tree
+	 * @param array $entry
+	 * @return string
+	 */
+	protected function getCallTreeLine( $entry ) {
+		list( $fname, $level, $startreal, , , $endreal ) = $entry;
+		$delta = $endreal - $startreal;
+		$space = str_repeat( ' ', $level );
+		# The ugly double sprintf is to work around a PHP bug,
+		# which has been fixed in recent releases.
+		return sprintf( "%10s %s %s\n",
+			trim( sprintf( "%7.3f", $delta * 1000.0 ) ), $space, $fname );
+	}
+
+	/**
+	 * Populate mCollated
+	 */
+	protected function collateData() {
+		if ( $this->mCollateDone ) {
+			return;
+		}
+		$this->mCollateDone = true;
+		$this->close(); // set "-total" entry
+
+		if ( $this->mCollateOnly ) {
+			return; // already collated as methods exited
+		}
+
+		$this->mCollated = array();
+
+		# Estimate profiling overhead
+		$profileCount = count( $this->mStack );
+		self::calculateOverhead( $profileCount );
+
+		# First, subtract the overhead!
+		$overheadTotal = $overheadMemory = $overheadInternal = array();
+		foreach ( $this->mStack as $entry ) {
+			// $entry is (name,pos,rtime0,cputime0,mem0,rtime1,cputime1,mem1)
+			$fname = $entry[0];
+			$elapsed = $entry[5] - $entry[2];
+			$memchange = $entry[7] - $entry[4];
+
+			if ( $fname === '-overhead-total' ) {
+				$overheadTotal[] = $elapsed;
+				$overheadMemory[] = max( 0, $memchange );
+			} elseif ( $fname === '-overhead-internal' ) {
+				$overheadInternal[] = $elapsed;
+			}
+		}
+		$overheadTotal = $overheadTotal ?
+			array_sum( $overheadTotal ) / count( $overheadInternal ) : 0;
+		$overheadMemory = $overheadMemory ?
+			array_sum( $overheadMemory ) / count( $overheadInternal ) : 0;
+		$overheadInternal = $overheadInternal ?
+			array_sum( $overheadInternal ) / count( $overheadInternal ) : 0;
+
+		# Collate
+		foreach ( $this->mStack as $index => $entry ) {
+			// $entry is (name,pos,rtime0,cputime0,mem0,rtime1,cputime1,mem1)
+			$fname = $entry[0];
+			$elapsedCpu = $entry[6] - $entry[3];
+			$elapsedReal = $entry[5] - $entry[2];
+			$memchange = $entry[7] - $entry[4];
+			$subcalls = $this->calltreeCount( $this->mStack, $index );
+
+			if ( substr( $fname, 0, 9 ) !== '-overhead' ) {
+				# Adjust for profiling overhead (except special values with elapsed=0
+				if ( $elapsed ) {
+					$elapsed -= $overheadInternal;
+					$elapsed -= ( $subcalls * $overheadTotal );
+					$memchange -= ( $subcalls * $overheadMemory );
+				}
+			}
+
+			$period = array( 'start' => $entry[2], 'end' => $entry[5],
+				'memory' => $memchange, 'subcalls' => $subcalls );
+			$this->updateEntry( $fname, $elapsedCpu, $elapsedReal, $memchange, $subcalls, $period );
+		}
+
+		$this->mCollated['-overhead-total']['count'] = $profileCount;
+		arsort( $this->mCollated, SORT_NUMERIC );
+	}
+
+	/**
+	 * Returns a list of profiled functions.
+	 *
+	 * @return string
+	 */
+	protected function getFunctionReport() {
+		$this->collateData();
+
+		$width = 140;
+		$nameWidth = $width - 65;
+		$format = "%-{$nameWidth}s %6d %13.3f %13.3f %13.3f%% %9d  (%13.3f -%13.3f) [%d]\n";
+		$titleFormat = "%-{$nameWidth}s %6s %13s %13s %13s %9s\n";
+		$prof = "\nProfiling data\n";
+		$prof .= sprintf( $titleFormat, 'Name', 'Calls', 'Total', 'Each', '%', 'Mem' );
+
+		$total = isset( $this->mCollated['-total'] )
+			? $this->mCollated['-total']['real']
+			: 0;
+
+		foreach ( $this->mCollated as $fname => $data ) {
+			$calls = $data['count'];
+			$percent = $total ? 100 * $data['real'] / $total : 0;
+			$memory = $data['memory'];
+			$prof .= sprintf( $format,
+				substr( $fname, 0, $nameWidth ),
+				$calls,
+				(float)( $data['real'] * 1000 ),
+				(float)( $data['real'] * 1000 ) / $calls,
+				$percent,
+				$memory,
+				( $data['min_real'] * 1000.0 ),
+				( $data['max_real'] * 1000.0 ),
+				$data['overhead']
+			);
+		}
+		$prof .= "\nTotal: $total\n\n";
+
+		return $prof;
+	}
+
+	/**
+	 * @return array
+	 */
+	public function getRawData() {
+		// This method is called before shutdown in the footer method on Skins.
+		// If some outer methods have not yet called wfProfileOut(), work around
+		// that by clearing anything in the work stack to just the "-total" entry.
+		// Collate after doing this so the results do not include profile errors.
+		if ( count( $this->mWorkStack ) > 1 ) {
+			$oldWorkStack = $this->mWorkStack;
+			$this->mWorkStack = array( $this->mWorkStack[0] ); // just the "-total" one
+		} else {
+			$oldWorkStack = null;
+		}
+		$this->collateData();
+		// If this trick is used, then the old work stack is swapped back afterwards
+		// and mCollateDone is reset to false. This means that logData() will still
+		// make use of all the method data since the missing wfProfileOut() calls
+		// should be made by the time it is called.
+		if ( $oldWorkStack ) {
+			$this->mWorkStack = $oldWorkStack;
+			$this->mCollateDone = false;
+		}
+
+		$total = isset( $this->mCollated['-total'] )
+			? $this->mCollated['-total']['real']
+			: 0;
+
+		$profile = array();
+		foreach ( $this->mCollated as $fname => $data ) {
+			$periods = array();
+			foreach ( $data['periods'] as $period ) {
+				$period['start'] *= 1000;
+				$period['end'] *= 1000;
+				$periods[] = $period;
+			}
+			$profile[] = array(
+				'name' => $fname,
+				'calls' => $data['count'],
+				'elapsed' => $data['real'] * 1000,
+				'percent' => $total ? 100 * $data['real'] / $total : 0,
+				'memory' => $data['memory'],
+				'min' => $data['min_real'] * 1000,
+				'max' => $data['max_real'] * 1000,
+				'overhead' => $data['overhead'],
+				'periods' => $periods
+			);
+		}
+
+		return $profile;
+	}
+
+	/**
+	 * Dummy calls to wfProfileIn/wfProfileOut to calculate its overhead
+	 * @param int $profileCount
+	 */
+	protected static function calculateOverhead( $profileCount ) {
+		wfProfileIn( '-overhead-total' );
+		for ( $i = 0; $i < $profileCount; $i++ ) {
+			wfProfileIn( '-overhead-internal' );
+			wfProfileOut( '-overhead-internal' );
+		}
+		wfProfileOut( '-overhead-total' );
+	}
+
+	/**
+	 * Counts the number of profiled function calls sitting under
+	 * the given point in the call graph. Not the most efficient algo.
+	 *
+	 * @param array $stack
+	 * @param int $start
+	 * @return int
+	 */
+	protected function calltreeCount( $stack, $start ) {
+		$level = $stack[$start][1];
+		$count = 0;
+		for ( $i = $start -1; $i >= 0 && $stack[$i][1] > $level; $i-- ) {
+			$count ++;
+		}
+		return $count;
+	}
+
+	/**
+	 * Get the content type sent out to the client.
+	 * Used for profilers that output instead of store data.
+	 * @return string
+	 */
+	protected function getContentType() {
+		foreach ( headers_list() as $header ) {
+			if ( preg_match( '#^content-type: (\w+/\w+);?#i', $header, $m ) ) {
+				return $m[1];
+			}
+		}
+		return null;
+	}
+}
diff --git a/includes/profiler/ProfilerStub.php b/includes/profiler/ProfilerStub.php
index 3697f352..1d3b65d2 100644
--- a/includes/profiler/ProfilerStub.php
+++ b/includes/profiler/ProfilerStub.php
@@ -30,15 +30,37 @@ class ProfilerStub extends Profiler {
 	public function isStub() {
 		return true;
 	}
+
 	public function isPersistent() {
 		return false;
 	}
-	public function profileIn( $fn ) {}
-	public function profileOut( $fn ) {}
-	public function getOutput() {}
-	public function close() {}
-	public function logData() {}
-	public function getCurrentSection() { return ''; }
-	public function transactionWritingIn( $server, $db ) {}
-	public function transactionWritingOut( $server, $db ) {}
+
+	public function profileIn( $fn ) {
+	}
+
+	public function profileOut( $fn ) {
+	}
+
+	public function getOutput() {
+	}
+
+	public function close() {
+	}
+
+	public function logData() {
+	}
+
+	public function getCurrentSection() {
+		return '';
+	}
+
+	public function transactionWritingIn( $server, $db, $id = '' ) {
+	}
+
+	public function transactionWritingOut( $server, $db, $id = '' ) {
+	}
+
+	public function getRawData() {
+		return array();
+	}
 }
diff --git a/includes/rcfeed/IRCColourfulRCFeedFormatter.php b/includes/rcfeed/IRCColourfulRCFeedFormatter.php
index 507369f3..02a8d7eb 100644
--- a/includes/rcfeed/IRCColourfulRCFeedFormatter.php
+++ b/includes/rcfeed/IRCColourfulRCFeedFormatter.php
@@ -1,11 +1,36 @@
 getAttributes();
 		if ( $attribs['rc_type'] == RC_LOG ) {
@@ -31,7 +56,7 @@ class IRCColourfulRCFeedFormatter implements RCFeedFormatter {
 				$query .= '&rcid=' . $attribs['rc_id'];
 			}
 			// HACK: We need this hook for WMF's secure server setup
-			wfRunHooks( 'IRCLineURL', array( &$url, &$query ) );
+			wfRunHooks( 'IRCLineURL', array( &$url, &$query, $rc ) );
 			$url .= $query;
 		}
 
@@ -52,19 +77,27 @@ class IRCColourfulRCFeedFormatter implements RCFeedFormatter {
 
 		if ( $attribs['rc_type'] == RC_LOG ) {
 			$targetText = $rc->getTitle()->getPrefixedText();
-			$comment = self::cleanupForIRC( str_replace( "[[$targetText]]", "[[\00302$targetText\00310]]", $actionComment ) );
+			$comment = self::cleanupForIRC( str_replace(
+				"[[$targetText]]",
+				"[[\00302$targetText\00310]]",
+				$actionComment
+			) );
 			$flag = $attribs['rc_log_action'];
 		} else {
 			$comment = self::cleanupForIRC( $attribs['rc_comment'] );
 			$flag = '';
-			if ( !$attribs['rc_patrolled'] && ( $wgUseRCPatrol || $attribs['rc_type'] == RC_NEW && $wgUseNPPatrol ) ) {
+			if ( !$attribs['rc_patrolled']
+				&& ( $wgUseRCPatrol || $attribs['rc_type'] == RC_NEW && $wgUseNPPatrol )
+			) {
 				$flag .= '!';
 			}
-			$flag .= ( $attribs['rc_type'] == RC_NEW ? "N" : "" ) . ( $attribs['rc_minor'] ? "M" : "" ) . ( $attribs['rc_bot'] ? "B" : "" );
+			$flag .= ( $attribs['rc_type'] == RC_NEW ? "N" : "" )
+				. ( $attribs['rc_minor'] ? "M" : "" ) . ( $attribs['rc_bot'] ? "B" : "" );
 		}
 
-		if ( $feed['add_interwiki_prefix'] === true && $wgLocalInterwiki !== false ) {
-			$prefix = $wgLocalInterwiki;
+		if ( $feed['add_interwiki_prefix'] === true && $wgLocalInterwikis ) {
+			// we use the first entry in $wgLocalInterwikis in recent changes feeds
+			$prefix = $wgLocalInterwikis[0];
 		} elseif ( $feed['add_interwiki_prefix'] ) {
 			$prefix = $feed['add_interwiki_prefix'];
 		} else {
diff --git a/includes/rcfeed/JSONRCFeedFormatter.php b/includes/rcfeed/JSONRCFeedFormatter.php
index f4cb9921..98d3f025 100644
--- a/includes/rcfeed/JSONRCFeedFormatter.php
+++ b/includes/rcfeed/JSONRCFeedFormatter.php
@@ -1,90 +1,32 @@
 getAttributes();
-
-		$packet = array(
-			// Usually, RC ID is exposed only for patrolling purposes,
-			// but there is no real reason not to expose it in other cases,
-			// and I can see how this may be potentially useful for clients.
-			'id' => $attrib['rc_id'],
-			'type' => $attrib['rc_type'],
-			'namespace' => $rc->getTitle()->getNamespace(),
-			'title' => $rc->getTitle()->getPrefixedText(),
-			'comment' => $attrib['rc_comment'],
-			'timestamp' => (int)wfTimestamp( TS_UNIX, $attrib['rc_timestamp'] ),
-			'user' => $attrib['rc_user_text'],
-			'bot' => (bool)$attrib['rc_bot'],
-		);
-
-		if ( isset( $feed['channel'] ) ) {
-			$packet['channel'] = $feed['channel'];
-		}
-
-		$type = $attrib['rc_type'];
-		if ( $type == RC_EDIT || $type == RC_NEW ) {
-			global $wgUseRCPatrol, $wgUseNPPatrol;
-
-			$packet['minor'] = $attrib['rc_minor'];
-			if ( $wgUseRCPatrol || ( $type == RC_NEW && $wgUseNPPatrol ) ) {
-				$packet['patrolled'] = $attrib['rc_patrolled'];
-			}
-		}
-
-		switch ( $type ) {
-			case RC_EDIT:
-				$packet['length'] = array( 'old' => $attrib['rc_old_len'], 'new' => $attrib['rc_new_len'] );
-				$packet['revision'] = array( 'old' => $attrib['rc_last_oldid'], 'new' => $attrib['rc_this_oldid'] );
-				break;
-
-			case RC_NEW:
-				$packet['length'] = array( 'old' => NULL, 'new' => $attrib['rc_new_len'] );
-				$packet['revision'] = array( 'old' => NULL, 'new' => $attrib['rc_this_oldid'] );
-				break;
-
-			case RC_LOG:
-				$packet['log_type'] = $attrib['rc_log_type'];
-				$packet['log_action'] = $attrib['rc_log_action'];
-				if ( $attrib['rc_params'] ) {
-					wfSuppressWarnings();
-					$params = unserialize( $attrib['rc_params'] );
-					wfRestoreWarnings();
-					if (
-						// If it's an actual serialised false...
-						$attrib['rc_params'] == serialize( false ) ||
-						// Or if we did not get false back when trying to unserialise
-						$params !== false
-					) {
-						// From ApiQueryLogEvents::addLogParams
-						$logParams = array();
-						// Keys like "4::paramname" can't be used for output so we change them to "paramname"
-						foreach ( $params as $key => $value ) {
-							if ( strpos( $key, ':' ) === false ) {
-								$logParams[$key] = $value;
-								continue;
-							}
-							$logParam = explode( ':', $key, 3 );
-							$logParams[$logParam[2]] = $value;
-						}
-						$packet['log_params'] = $logParams;
-					} else {
-						$packet['log_params'] = explode( "\n", $attrib['rc_params'] );
-					}
-				}
-				$packet['log_action_comment'] = $actionComment;
-				break;
-		}
-
-		$packet['server_url'] = $wgCanonicalServer;
-		$packet['server_script_path'] = $wgScriptPath ?: '/';
-		$packet['wiki'] = $wgDBname;
-
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Formats a notification into the JSON format (http://www.json.org)
+ *
+ * @since 1.22
+ */
+class JSONRCFeedFormatter extends MachineReadableRCFeedFormatter {
+
+	protected function formatArray( array $packet ) {
 		return FormatJson::encode( $packet );
 	}
 }
diff --git a/includes/rcfeed/MachineReadableRCFeedFormatter.php b/includes/rcfeed/MachineReadableRCFeedFormatter.php
new file mode 100644
index 00000000..519606ca
--- /dev/null
+++ b/includes/rcfeed/MachineReadableRCFeedFormatter.php
@@ -0,0 +1,130 @@
+ $rc->getAttribute( 'rc_id' ),
+			'type' => RecentChange::parseFromRCType( $rc->getAttribute( 'rc_type' ) ),
+			'namespace' => $rc->getTitle()->getNamespace(),
+			'title' => $rc->getTitle()->getPrefixedText(),
+			'comment' => $rc->getAttribute( 'rc_comment' ),
+			'timestamp' => (int)wfTimestamp( TS_UNIX, $rc->getAttribute( 'rc_timestamp' ) ),
+			'user' => $rc->getAttribute( 'rc_user_text' ),
+			'bot' => (bool)$rc->getAttribute( 'rc_bot' ),
+		);
+
+		if ( isset( $feed['channel'] ) ) {
+			$packet['channel'] = $feed['channel'];
+		}
+
+		$type = $rc->getAttribute( 'rc_type' );
+		if ( $type == RC_EDIT || $type == RC_NEW ) {
+			global $wgUseRCPatrol, $wgUseNPPatrol;
+
+			$packet['minor'] = (bool)$rc->getAttribute( 'rc_minor' );
+			if ( $wgUseRCPatrol || ( $type == RC_NEW && $wgUseNPPatrol ) ) {
+				$packet['patrolled'] = (bool)$rc->getAttribute( 'rc_patrolled' );
+			}
+		}
+
+		switch ( $type ) {
+			case RC_EDIT:
+				$packet['length'] = array(
+					'old' => $rc->getAttribute( 'rc_old_len' ),
+					'new' => $rc->getAttribute( 'rc_new_len' )
+				);
+				$packet['revision'] = array(
+					'old' => $rc->getAttribute( 'rc_last_oldid' ),
+					'new' => $rc->getAttribute( 'rc_this_oldid' )
+				);
+				break;
+
+			case RC_NEW:
+				$packet['length'] = array( 'old' => null, 'new' => $rc->getAttribute( 'rc_new_len' ) );
+				$packet['revision'] = array( 'old' => null, 'new' => $rc->getAttribute( 'rc_this_oldid' ) );
+				break;
+
+			case RC_LOG:
+				$packet['log_id'] = $rc->getAttribute( 'rc_logid' );
+				$packet['log_type'] = $rc->getAttribute( 'rc_log_type' );
+				$packet['log_action'] = $rc->getAttribute( 'rc_log_action' );
+				if ( $rc->getAttribute( 'rc_params' ) ) {
+					wfSuppressWarnings();
+					$params = unserialize( $rc->getAttribute( 'rc_params' ) );
+					wfRestoreWarnings();
+					if (
+						// If it's an actual serialised false...
+						$rc->getAttribute( 'rc_params' ) == serialize( false ) ||
+						// Or if we did not get false back when trying to unserialise
+						$params !== false
+					) {
+						// From ApiQueryLogEvents::addLogParams
+						$logParams = array();
+						// Keys like "4::paramname" can't be used for output so we change them to "paramname"
+						foreach ( $params as $key => $value ) {
+							if ( strpos( $key, ':' ) === false ) {
+								$logParams[$key] = $value;
+								continue;
+							}
+							$logParam = explode( ':', $key, 3 );
+							$logParams[$logParam[2]] = $value;
+						}
+						$packet['log_params'] = $logParams;
+					} else {
+						$packet['log_params'] = explode( "\n", $rc->getAttribute( 'rc_params' ) );
+					}
+				}
+				$packet['log_action_comment'] = $actionComment;
+				break;
+		}
+
+		$packet['server_url'] = $wgCanonicalServer;
+		$packet['server_name'] = $wgServerName;
+
+		$packet['server_script_path'] = $wgScriptPath ?: '/';
+		$packet['wiki'] = wfWikiID();
+
+		return $this->formatArray( $packet );
+	}
+}
diff --git a/includes/rcfeed/RCFeedEngine.php b/includes/rcfeed/RCFeedEngine.php
index f733bcb7..0b0cd869 100644
--- a/includes/rcfeed/RCFeedEngine.php
+++ b/includes/rcfeed/RCFeedEngine.php
@@ -1,12 +1,37 @@
  'JSONRCFeedFormatter',
+ *      'uri'       => "redis://127.0.0.1:6379/rc.$wgDBname",
+ * );
+ *
+ * @since 1.22
+ */
 class RedisPubSubFeedEngine implements RCFeedEngine {
+
 	/**
-	 * Emit a recent change notification via Redis Pub/Sub
-	 *
-	 * If the feed URI contains a path component, it will be used to generate a
-	 * channel name by stripping the leading slash and replacing any remaining
-	 * slashes with '.'. If no path component is present, the channel is set to
-	 * 'rc'. If the URI contains a query string, its parameters will be parsed
-	 * as RedisConnectionPool options.
-	 *
-	 * @example $wgRCFeeds['redis'] = array(
-	 *      'formatter' => 'JSONRCFeedFormatter',
-	 *      'uri'       => "redis://127.0.0.1:6379/rc.$wgDBname",
-	 * );
-	 *
-	 * @since 1.22
+	 * @see RCFeedEngine::send
 	 */
 	public function send( array $feed, $line ) {
-		$parsed = parse_url( $feed['uri'] );
+		$parsed = wfParseUrl( $feed['uri'] );
 		$server = $parsed['host'];
 		$options = array( 'serializer' => 'none' );
 		$channel = 'rc';
@@ -36,6 +61,11 @@ class RedisPubSubFeedEngine implements RCFeedEngine {
 		}
 		$pool = RedisConnectionPool::singleton( $options );
 		$conn = $pool->getConnection( $server );
-		$conn->publish( $channel, $line );
+		if ( $conn !== false ) {
+			$conn->publish( $channel, $line );
+			return true;
+		} else {
+			return false;
+		}
 	}
 }
diff --git a/includes/rcfeed/UDPRCFeedEngine.php b/includes/rcfeed/UDPRCFeedEngine.php
index beeb73bd..8554670e 100644
--- a/includes/rcfeed/UDPRCFeedEngine.php
+++ b/includes/rcfeed/UDPRCFeedEngine.php
@@ -1,7 +1,31 @@
 context = $context;
+	}
+
+	public function getModules() {
+		if ( !is_null( $this->modules ) ) {
+			return $this->modules;
+		} else {
+			return $this->context->getModules();
+		}
+	}
+
+	/**
+	 * @param string[] $modules
+	 */
+	public function setModules( array $modules ) {
+		$this->modules = $modules;
+	}
+
+	public function getLanguage() {
+		if ( !is_null( $this->language ) ) {
+			return $this->language;
+		} else {
+			return $this->context->getLanguage();
+		}
+	}
+
+	/**
+	 * @param string $language
+	 */
+	public function setLanguage( $language ) {
+		$this->language = $language;
+		$this->direction = null; // Invalidate direction since it might be based on language
+		$this->hash = null;
+	}
+
+	public function getDirection() {
+		if ( !is_null( $this->direction ) ) {
+			return $this->direction;
+		} else {
+			return $this->context->getDirection();
+		}
+	}
+
+	/**
+	 * @param string $direction
+	 */
+	public function setDirection( $direction ) {
+		$this->direction = $direction;
+		$this->hash = null;
+	}
+
+	public function getSkin() {
+		if ( !is_null( $this->skin ) ) {
+			return $this->skin;
+		} else {
+			return $this->context->getSkin();
+		}
+	}
+
+	/**
+	 * @param string $skin
+	 */
+	public function setSkin( $skin ) {
+		$this->skin = $skin;
+		$this->hash = null;
+	}
+
+	public function getUser() {
+		if ( !is_null( $this->user ) ) {
+			return $this->user;
+		} else {
+			return $this->context->getUser();
+		}
+	}
+
+	/**
+	 * @param string $user
+	 */
+	public function setUser( $user ) {
+		$this->user = $user;
+		$this->hash = null;
+	}
+
+	public function getDebug() {
+		if ( !is_null( $this->debug ) ) {
+			return $this->debug;
+		} else {
+			return $this->context->getDebug();
+		}
+	}
+
+	/**
+	 * @param bool $debug
+	 */
+	public function setDebug( $debug ) {
+		$this->debug = $debug;
+		$this->hash = null;
+	}
+
+	public function getOnly() {
+		if ( !is_null( $this->only ) ) {
+			return $this->only;
+		} else {
+			return $this->context->getOnly();
+		}
+	}
+
+	/**
+	 * @param string $only
+	 */
+	public function setOnly( $only ) {
+		$this->only = $only;
+		$this->hash = null;
+	}
+
+	public function getVersion() {
+		if ( !is_null( $this->version ) ) {
+			return $this->version;
+		} else {
+			return $this->context->getVersion();
+		}
+	}
+
+	/**
+	 * @param string $version
+	 */
+	public function setVersion( $version ) {
+		$this->version = $version;
+		$this->hash = null;
+	}
+
+	public function getRaw() {
+		if ( !is_null( $this->raw ) ) {
+			return $this->raw;
+		} else {
+			return $this->context->getRaw();
+		}
+	}
+
+	/**
+	 * @param bool $raw
+	 */
+	public function setRaw( $raw ) {
+		$this->raw = $raw;
+	}
+
+	public function getRequest() {
+		return $this->context->getRequest();
+	}
+
+	public function getResourceLoader() {
+		return $this->context->getResourceLoader();
+	}
+
+}
diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php
index 6380efcf..4f1414bc 100644
--- a/includes/resourceloader/ResourceLoader.php
+++ b/includes/resourceloader/ResourceLoader.php
@@ -25,35 +25,39 @@
 /**
  * Dynamic JavaScript and CSS resource loading system.
  *
- * Most of the documention is on the MediaWiki documentation wiki starting at:
- *    http://www.mediawiki.org/wiki/ResourceLoader
+ * Most of the documentation is on the MediaWiki documentation wiki starting at:
+ *    https://www.mediawiki.org/wiki/ResourceLoader
  */
 class ResourceLoader {
-
-	/* Protected Static Members */
+	/** @var int */
 	protected static $filterCacheVersion = 7;
-	protected static $requiredSourceProperties = array( 'loadScript' );
 
-	/** Array: List of module name/ResourceLoaderModule object pairs */
+	/** @var bool */
+	protected static $debugMode = null;
+
+	/** @var array Module name/ResourceLoaderModule object pairs */
 	protected $modules = array();
 
-	/** Associative array mapping module name to info associative array */
+	/** @var array Associative array mapping module name to info associative array */
 	protected $moduleInfos = array();
 
-	/** Associative array mapping framework ids to a list of names of test suite modules */
-	/** like array( 'qunit' => array( 'mediawiki.tests.qunit.suites', 'ext.foo.tests', .. ), .. ) */
+	/** @var Config $config */
+	private $config;
+
+	/**
+	 * @var array Associative array mapping framework ids to a list of names of test suite modules
+	 *      like array( 'qunit' => array( 'mediawiki.tests.qunit.suites', 'ext.foo.tests', .. ), .. )
+	 */
 	protected $testModuleNames = array();
 
-	/** array( 'source-id' => array( 'loadScript' => 'http://.../load.php' ) ) **/
+	/** @var array E.g. array( 'source-id' => 'http://.../load.php' ) */
 	protected $sources = array();
 
 	/** @var bool */
 	protected $hasErrors = false;
 
-	/* Protected Methods */
-
 	/**
-	 * Loads information stored in the database about modules.
+	 * Load information stored in the database about modules.
 	 *
 	 * This method grabs modules dependencies from the database and updates modules
 	 * objects.
@@ -64,11 +68,12 @@ class ResourceLoader {
 	 * performance improvement.
 	 *
 	 * @param array $modules List of module names to preload information for
-	 * @param $context ResourceLoaderContext: Context to load the information within
+	 * @param ResourceLoaderContext $context Context to load the information within
 	 */
 	public function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) {
 		if ( !count( $modules ) ) {
-			return; // or else Database*::select() will explode, plus it's cheaper!
+			// Or else Database*::select() will explode, plus it's cheaper!
+			return;
 		}
 		$dbr = wfGetDB( DB_SLAVE );
 		$skin = $context->getSkin();
@@ -84,21 +89,26 @@ class ResourceLoader {
 		// Set modules' dependencies
 		$modulesWithDeps = array();
 		foreach ( $res as $row ) {
-			$this->getModule( $row->md_module )->setFileDependencies( $skin,
-				FormatJson::decode( $row->md_deps, true )
-			);
-			$modulesWithDeps[] = $row->md_module;
+			$module = $this->getModule( $row->md_module );
+			if ( $module ) {
+				$module->setFileDependencies( $skin, FormatJson::decode( $row->md_deps, true ) );
+				$modulesWithDeps[] = $row->md_module;
+			}
 		}
 
 		// Register the absence of a dependency row too
 		foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) {
-			$this->getModule( $name )->setFileDependencies( $skin, array() );
+			$module = $this->getModule( $name );
+			if ( $module ) {
+				$this->getModule( $name )->setFileDependencies( $skin, array() );
+			}
 		}
 
 		// Get message blob mtimes. Only do this for modules with messages
 		$modulesWithMessages = array();
 		foreach ( $modules as $name ) {
-			if ( count( $this->getModule( $name )->getMessages() ) ) {
+			$module = $this->getModule( $name );
+			if ( $module && count( $module->getMessages() ) ) {
 				$modulesWithMessages[] = $name;
 			}
 		}
@@ -110,39 +120,43 @@ class ResourceLoader {
 				), __METHOD__
 			);
 			foreach ( $res as $row ) {
-				$this->getModule( $row->mr_resource )->setMsgBlobMtime( $lang,
-					wfTimestamp( TS_UNIX, $row->mr_timestamp ) );
-				unset( $modulesWithoutMessages[$row->mr_resource] );
+				$module = $this->getModule( $row->mr_resource );
+				if ( $module ) {
+					$module->setMsgBlobMtime( $lang, wfTimestamp( TS_UNIX, $row->mr_timestamp ) );
+					unset( $modulesWithoutMessages[$row->mr_resource] );
+				}
 			}
 		}
 		foreach ( array_keys( $modulesWithoutMessages ) as $name ) {
-			$this->getModule( $name )->setMsgBlobMtime( $lang, 0 );
+			$module = $this->getModule( $name );
+			if ( $module ) {
+				$module->setMsgBlobMtime( $lang, 0 );
+			}
 		}
 	}
 
 	/**
-	 * Runs JavaScript or CSS data through a filter, caching the filtered result for future calls.
+	 * Run JavaScript or CSS data through a filter, caching the filtered result for future calls.
 	 *
 	 * Available filters are:
-	 *  - minify-js \see JavaScriptMinifier::minify
-	 *  - minify-css \see CSSMin::minify
+	 *
+	 *    - minify-js \see JavaScriptMinifier::minify
+	 *    - minify-css \see CSSMin::minify
 	 *
 	 * If $data is empty, only contains whitespace or the filter was unknown,
 	 * $data is returned unmodified.
 	 *
 	 * @param string $filter Name of filter to run
 	 * @param string $data Text to filter, such as JavaScript or CSS text
-	 * @return String: Filtered data, or a comment containing an error message
+	 * @param string $cacheReport Whether to include the cache key report
+	 * @return string Filtered data, or a comment containing an error message
 	 */
-	protected function filter( $filter, $data ) {
-		global $wgResourceLoaderMinifierStatementsOnOwnLine, $wgResourceLoaderMinifierMaxLineLength;
+	public function filter( $filter, $data, $cacheReport = true ) {
 		wfProfileIn( __METHOD__ );
 
 		// For empty/whitespace-only data or for unknown filters, don't perform
 		// any caching or processing
-		if ( trim( $data ) === ''
-			|| !in_array( $filter, array( 'minify-js', 'minify-css' ) ) )
-		{
+		if ( trim( $data ) === '' || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) ) {
 			wfProfileOut( __METHOD__ );
 			return $data;
 		}
@@ -165,14 +179,18 @@ class ResourceLoader {
 			switch ( $filter ) {
 				case 'minify-js':
 					$result = JavaScriptMinifier::minify( $data,
-						$wgResourceLoaderMinifierStatementsOnOwnLine,
-						$wgResourceLoaderMinifierMaxLineLength
+						$this->config->get( 'ResourceLoaderMinifierStatementsOnOwnLine' ),
+						$this->config->get( 'ResourceLoaderMinifierMaxLineLength' )
 					);
-					$result .= "\n/* cache key: $key */";
+					if ( $cacheReport ) {
+						$result .= "\n/* cache key: $key */";
+					}
 					break;
 				case 'minify-css':
 					$result = CSSMin::minify( $data );
-					$result .= "\n/* cache key: $key */";
+					if ( $cacheReport ) {
+						$result .= "\n/* cache key: $key */";
+					}
 					break;
 			}
 
@@ -194,26 +212,34 @@ class ResourceLoader {
 	/* Methods */
 
 	/**
-	 * Registers core modules and runs registration hooks.
+	 * Register core modules and runs registration hooks.
+	 * @param Config|null $config
 	 */
-	public function __construct() {
-		global $IP, $wgResourceModules, $wgResourceLoaderSources, $wgLoadScript, $wgEnableJavaScriptTest;
+	public function __construct( Config $config = null ) {
+		global $IP;
 
 		wfProfileIn( __METHOD__ );
 
+		if ( $config === null ) {
+			wfDebug( __METHOD__ . ' was called without providing a Config instance' );
+			$config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
+		}
+
+		$this->config = $config;
+
 		// Add 'local' source first
-		$this->addSource( 'local', array( 'loadScript' => $wgLoadScript, 'apiScript' => wfScript( 'api' ) ) );
+		$this->addSource( 'local', wfScript( 'load' ) );
 
 		// Add other sources
-		$this->addSource( $wgResourceLoaderSources );
+		$this->addSource( $config->get( 'ResourceLoaderSources' ) );
 
 		// Register core modules
 		$this->register( include "$IP/resources/Resources.php" );
 		// Register extension modules
 		wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
-		$this->register( $wgResourceModules );
+		$this->register( $config->get( 'ResourceModules' ) );
 
-		if ( $wgEnableJavaScriptTest === true ) {
+		if ( $config->get( 'EnableJavaScriptTest' ) === true ) {
 			$this->registerTestModules();
 		}
 
@@ -221,17 +247,24 @@ class ResourceLoader {
 	}
 
 	/**
-	 * Registers a module with the ResourceLoader system.
+	 * @return Config
+	 */
+	public function getConfig() {
+		return $this->config;
+	}
+
+	/**
+	 * Register a module with the ResourceLoader system.
 	 *
-	 * @param $name Mixed: Name of module as a string or List of name/object pairs as an array
+	 * @param mixed $name Name of module as a string or List of name/object pairs as an array
 	 * @param array $info Module info array. For backwards compatibility with 1.17alpha,
 	 *   this may also be a ResourceLoaderModule object. Optional when using
 	 *   multiple-registration calling style.
-	 * @throws MWException: If a duplicate module registration is attempted
-	 * @throws MWException: If a module name contains illegal characters (pipes or commas)
-	 * @throws MWException: If something other than a ResourceLoaderModule is being registered
-	 * @return Boolean: False if there were any errors, in which case one or more modules were not
-	 *     registered
+	 * @throws MWException If a duplicate module registration is attempted
+	 * @throws MWException If a module name contains illegal characters (pipes or commas)
+	 * @throws MWException If something other than a ResourceLoaderModule is being registered
+	 * @return bool False if there were any errors, in which case one or more modules were
+	 *   not registered
 	 */
 	public function register( $name, $info = null ) {
 		wfProfileIn( __METHOD__ );
@@ -252,25 +285,64 @@ class ResourceLoader {
 			// Check $name for validity
 			if ( !self::isValidModuleName( $name ) ) {
 				wfProfileOut( __METHOD__ );
-				throw new MWException( "ResourceLoader module name '$name' is invalid, see ResourceLoader::isValidModuleName()" );
+				throw new MWException( "ResourceLoader module name '$name' is invalid, "
+					. "see ResourceLoader::isValidModuleName()" );
 			}
 
 			// Attach module
-			if ( is_object( $info ) ) {
-				// Old calling convention
-				// Validate the input
-				if ( !( $info instanceof ResourceLoaderModule ) ) {
-					wfProfileOut( __METHOD__ );
-					throw new MWException( 'ResourceLoader invalid module error. ' .
-						'Instances of ResourceLoaderModule expected.' );
-				}
-
+			if ( $info instanceof ResourceLoaderModule ) {
 				$this->moduleInfos[$name] = array( 'object' => $info );
 				$info->setName( $name );
 				$this->modules[$name] = $info;
-			} else {
+			} elseif ( is_array( $info ) ) {
 				// New calling convention
 				$this->moduleInfos[$name] = $info;
+			} else {
+				wfProfileOut( __METHOD__ );
+				throw new MWException(
+					'ResourceLoader module info type error for module \'' . $name .
+					'\': expected ResourceLoaderModule or array (got: ' . gettype( $info ) . ')'
+				);
+			}
+
+			// Last-minute changes
+
+			// Apply custom skin-defined styles to existing modules.
+			if ( $this->isFileModule( $name ) ) {
+				foreach ( $this->config->get( 'ResourceModuleSkinStyles' ) as $skinName => $skinStyles ) {
+					// If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles.
+					if ( isset( $this->moduleInfos[$name]['skinStyles'][$skinName] ) ) {
+						continue;
+					}
+
+					// If $name is preceded with a '+', the defined style files will be added to 'default'
+					// skinStyles, otherwise 'default' will be ignored as it normally would be.
+					if ( isset( $skinStyles[$name] ) ) {
+						$paths = (array)$skinStyles[$name];
+						$styleFiles = array();
+					} elseif ( isset( $skinStyles['+' . $name] ) ) {
+						$paths = (array)$skinStyles['+' . $name];
+						$styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ?
+							$this->moduleInfos[$name]['skinStyles']['default'] :
+							array();
+					} else {
+						continue;
+					}
+
+					// Add new file paths, remapping them to refer to our directories and not use settings
+					// from the module we're modifying. These can come from the base definition or be defined
+					// for each module.
+					list( $localBasePath, $remoteBasePath ) =
+						ResourceLoaderFileModule::extractBasePaths( $skinStyles );
+					list( $localBasePath, $remoteBasePath ) =
+						ResourceLoaderFileModule::extractBasePaths( $paths, $localBasePath, $remoteBasePath );
+
+					foreach ( $paths as $path ) {
+						$styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath );
+					}
+
+					$this->moduleInfos[$name]['skinStyles'][$skinName] = $styleFiles;
+				}
 			}
 		}
 
@@ -280,17 +352,19 @@ class ResourceLoader {
 	/**
 	 */
 	public function registerTestModules() {
-		global $IP, $wgEnableJavaScriptTest;
+		global $IP;
 
-		if ( $wgEnableJavaScriptTest !== true ) {
-			throw new MWException( 'Attempt to register JavaScript test modules but $wgEnableJavaScriptTest is false. Edit your LocalSettings.php to enable it.' );
+		if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) {
+			throw new MWException( 'Attempt to register JavaScript test modules '
+				. 'but $wgEnableJavaScriptTest is false. '
+				. 'Edit your LocalSettings.php to enable it.' );
 		}
 
 		wfProfileIn( __METHOD__ );
 
 		// Get core test suites
 		$testModules = array();
-		$testModules['qunit'] = include "$IP/tests/qunit/QUnitTestResources.php";
+		$testModules['qunit'] = array();
 		// Get other test suites (e.g. from extensions)
 		wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) );
 
@@ -301,9 +375,12 @@ class ResourceLoader {
 			// on document-ready, it will run once and finish. If some tests arrive
 			// later (possibly after QUnit has already finished) they will be ignored.
 			$module['position'] = 'top';
-			$module['dependencies'][] = 'mediawiki.tests.qunit.testrunner';
+			$module['dependencies'][] = 'test.mediawiki.qunit.testrunner';
 		}
 
+		$testModules['qunit'] =
+			( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules['qunit'];
+
 		foreach ( $testModules as $id => $names ) {
 			// Register test modules
 			$this->register( $testModules[$id] );
@@ -318,14 +395,12 @@ class ResourceLoader {
 	/**
 	 * Add a foreign source of modules.
 	 *
-	 * Source properties:
-	 * 'loadScript': URL (either fully-qualified or protocol-relative) of load.php for this source
-	 *
-	 * @param $id Mixed: source ID (string), or array( id1 => props1, id2 => props2, ... )
-	 * @param array $properties source properties
+	 * @param array|string $id Source ID (string), or array( id1 => loadUrl, id2 => loadUrl, ... )
+	 * @param string|array $loadUrl load.php url (string), or array with loadUrl key for
+	 *  backwards-compatibility.
 	 * @throws MWException
 	 */
-	public function addSource( $id, $properties = null ) {
+	public function addSource( $id, $loadUrl = null ) {
 		// Allow multiple sources to be registered in one call
 		if ( is_array( $id ) ) {
 			foreach ( $id as $key => $value ) {
@@ -342,20 +417,24 @@ class ResourceLoader {
 			);
 		}
 
-		// Validate properties
-		foreach ( self::$requiredSourceProperties as $prop ) {
-			if ( !isset( $properties[$prop] ) ) {
-				throw new MWException( "Required property $prop missing from source ID $id" );
+		// Pre 1.24 backwards-compatibility
+		if ( is_array( $loadUrl ) ) {
+			if ( !isset( $loadUrl['loadScript'] ) ) {
+				throw new MWException(
+					__METHOD__ . ' was passed an array with no "loadScript" key.'
+				);
 			}
+
+			$loadUrl = $loadUrl['loadScript'];
 		}
 
-		$this->sources[$id] = $properties;
+		$this->sources[$id] = $loadUrl;
 	}
 
 	/**
-	 * Get a list of module names
+	 * Get a list of module names.
 	 *
-	 * @return Array: List of module names
+	 * @return array List of module names
 	 */
 	public function getModuleNames() {
 		return array_keys( $this->moduleInfos );
@@ -363,18 +442,21 @@ class ResourceLoader {
 
 	/**
 	 * Get a list of test module names for one (or all) frameworks.
+	 *
 	 * If the given framework id is unknkown, or if the in-object variable is not an array,
 	 * then it will return an empty array.
 	 *
-	 * @param string $framework Optional. Get only the test module names for one
-	 * particular framework.
-	 * @return Array
+	 * @param string $framework Get only the test module names for one
+	 *   particular framework (optional)
+	 * @return array
 	 */
 	public function getTestModuleNames( $framework = 'all' ) {
-		/// @todo api siteinfo prop testmodulenames modulenames
+		/** @todo api siteinfo prop testmodulenames modulenames */
 		if ( $framework == 'all' ) {
 			return $this->testModuleNames;
-		} elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) {
+		} elseif ( isset( $this->testModuleNames[$framework] )
+			&& is_array( $this->testModuleNames[$framework] )
+		) {
 			return $this->testModuleNames[$framework];
 		} else {
 			return array();
@@ -384,8 +466,13 @@ class ResourceLoader {
 	/**
 	 * Get the ResourceLoaderModule object for a given module name.
 	 *
+	 * If an array of module parameters exists but a ResourceLoaderModule object has not
+	 * yet been instantiated, this method will instantiate and cache that object such that
+	 * subsequent calls simply return the same object.
+	 *
 	 * @param string $name Module name
-	 * @return ResourceLoaderModule if module has been registered, null otherwise
+	 * @return ResourceLoaderModule|null If module has been registered, return a
+	 *  ResourceLoaderModule instance. Otherwise, return null.
 	 */
 	public function getModule( $name ) {
 		if ( !isset( $this->modules[$name] ) ) {
@@ -405,7 +492,9 @@ class ResourceLoader {
 				} else {
 					$class = $info['class'];
 				}
+				/** @var ResourceLoaderModule $object */
 				$object = new $class( $info );
+				$object->setConfig( $this->getConfig() );
 			}
 			$object->setName( $name );
 			$this->modules[$name] = $object;
@@ -415,24 +504,55 @@ class ResourceLoader {
 	}
 
 	/**
-	 * Get the list of sources
+	 * Return whether the definition of a module corresponds to a simple ResourceLoaderFileModule.
 	 *
-	 * @return Array: array( id => array of properties, .. )
+	 * @param string $name Module name
+	 * @return bool
+	 */
+	protected function isFileModule( $name ) {
+		if ( !isset( $this->moduleInfos[$name] ) ) {
+			return false;
+		}
+		$info = $this->moduleInfos[$name];
+		if ( isset( $info['object'] ) || isset( $info['class'] ) ) {
+			return false;
+		}
+		return true;
+	}
+
+	/**
+	 * Get the list of sources.
+	 *
+	 * @return array Like array( id => load.php url, .. )
 	 */
 	public function getSources() {
 		return $this->sources;
 	}
 
 	/**
-	 * Outputs a response to a resource load-request, including a content-type header.
+	 * Get the URL to the load.php endpoint for the given
+	 * ResourceLoader source
 	 *
-	 * @param $context ResourceLoaderContext: Context in which a response should be formed
+	 * @since 1.24
+	 * @param string $source
+	 * @throws MWException On an invalid $source name
+	 * @return string
 	 */
-	public function respond( ResourceLoaderContext $context ) {
-		global $wgCacheEpoch, $wgUseFileCache;
+	public function getLoadScript( $source ) {
+		if ( !isset( $this->sources[$source] ) ) {
+			throw new MWException( "The $source source was never registered in ResourceLoader." );
+		}
+		return $this->sources[$source];
+	}
 
+	/**
+	 * Output a response to a load request, including the content-type header.
+	 *
+	 * @param ResourceLoaderContext $context Context in which a response should be formed
+	 */
+	public function respond( ResourceLoaderContext $context ) {
 		// Use file cache if enabled and available...
-		if ( $wgUseFileCache ) {
+		if ( $this->config->get( 'UseFileCache' ) ) {
 			$fileCache = ResourceFileCache::newFromContext( $context );
 			if ( $this->tryRespondFromFileCache( $fileCache, $context ) ) {
 				return; // output handled
@@ -451,12 +571,12 @@ class ResourceLoader {
 		wfProfileIn( __METHOD__ );
 		$errors = '';
 
-		// Split requested modules into two groups, modules and missing
+		// Find out which modules are missing and instantiate the others
 		$modules = array();
 		$missing = array();
 		foreach ( $context->getModules() as $name ) {
-			if ( isset( $this->moduleInfos[$name] ) ) {
-				$module = $this->getModule( $name );
+			$module = $this->getModule( $name );
+			if ( $module ) {
 				// Do not allow private modules to be loaded from the web.
 				// This is a security issue, see bug 34907.
 				if ( $module->getGroup() === 'private' ) {
@@ -488,7 +608,7 @@ class ResourceLoader {
 
 		// To send Last-Modified and support If-Modified-Since, we need to detect
 		// the last modified time
-		$mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch );
+		$mtime = wfTimestamp( TS_UNIX, $this->config->get( 'CacheEpoch' ) );
 		foreach ( $modules as $module ) {
 			/**
 			 * @var $module ResourceLoaderModule
@@ -527,7 +647,7 @@ class ResourceLoader {
 		}
 
 		// Save response to file cache unless there are errors
-		if ( isset( $fileCache ) && !$errors && !$missing ) {
+		if ( isset( $fileCache ) && !$errors && !count( $missing ) ) {
 			// Cache single modules...and other requests if there are enough hits
 			if ( ResourceFileCache::useFileCache( $context ) ) {
 				if ( $fileCache->isCacheWorthy() ) {
@@ -550,24 +670,24 @@ class ResourceLoader {
 
 	/**
 	 * Send content type and last modified headers to the client.
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @param string $mtime TS_MW timestamp to use for last-modified
 	 * @param bool $errors Whether there are commented-out errors in the response
 	 * @return void
 	 */
 	protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) {
-		global $wgResourceLoaderMaxage;
+		$rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
 		// If a version wasn't specified we need a shorter expiry time for updates
 		// to propagate to clients quickly
 		// If there were errors, we also need a shorter expiry time so we can recover quickly
 		if ( is_null( $context->getVersion() ) || $errors ) {
-			$maxage = $wgResourceLoaderMaxage['unversioned']['client'];
-			$smaxage = $wgResourceLoaderMaxage['unversioned']['server'];
+			$maxage = $rlMaxage['unversioned']['client'];
+			$smaxage = $rlMaxage['unversioned']['server'];
 		// If a version was specified we can use a longer expiry time since changing
 		// version numbers causes cache misses
 		} else {
-			$maxage = $wgResourceLoaderMaxage['versioned']['client'];
-			$smaxage = $wgResourceLoaderMaxage['versioned']['server'];
+			$maxage = $rlMaxage['versioned']['client'];
+			$smaxage = $rlMaxage['versioned']['server'];
 		}
 		if ( $context->getOnly() === 'styles' ) {
 			header( 'Content-Type: text/css; charset=utf-8' );
@@ -588,9 +708,12 @@ class ResourceLoader {
 	}
 
 	/**
+	 * Respond with 304 Last Modified if appropiate.
+	 *
 	 * If there's an If-Modified-Since header, respond with a 304 appropriately
 	 * and clear out the output buffer. If the client cache is too old then do nothing.
-	 * @param $context ResourceLoaderContext
+	 *
+	 * @param ResourceLoaderContext $context
 	 * @param string $mtime The TS_MW timestamp to check the header against
 	 * @return bool True if 304 header sent and output handled
 	 */
@@ -623,22 +746,22 @@ class ResourceLoader {
 	}
 
 	/**
-	 * Send out code for a response from file cache if possible
+	 * Send out code for a response from file cache if possible.
 	 *
-	 * @param $fileCache ResourceFileCache: Cache object for this request URL
-	 * @param $context ResourceLoaderContext: Context in which to generate a response
+	 * @param ResourceFileCache $fileCache Cache object for this request URL
+	 * @param ResourceLoaderContext $context Context in which to generate a response
 	 * @return bool If this found a cache file and handled the response
 	 */
 	protected function tryRespondFromFileCache(
 		ResourceFileCache $fileCache, ResourceLoaderContext $context
 	) {
-		global $wgResourceLoaderMaxage;
+		$rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
 		// Buffer output to catch warnings.
 		ob_start();
 		// Get the maximum age the cache can be
 		$maxage = is_null( $context->getVersion() )
-			? $wgResourceLoaderMaxage['unversioned']['server']
-			: $wgResourceLoaderMaxage['versioned']['server'];
+			? $rlMaxage['unversioned']['server']
+			: $rlMaxage['versioned']['server'];
 		// Minimum timestamp the cache file must have
 		$good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) );
 		if ( !$good ) {
@@ -674,10 +797,11 @@ class ResourceLoader {
 	}
 
 	/**
-	 * Generate a CSS or JS comment block. Only use this for public data,
-	 * not error message details.
+	 * Generate a CSS or JS comment block.
 	 *
-	 * @param $text string
+	 * Only use this for public data, not error message details.
+	 *
+	 * @param string $text
 	 * @return string
 	 */
 	public static function makeComment( $text ) {
@@ -686,10 +810,10 @@ class ResourceLoader {
 	}
 
 	/**
-	 * Handle exception display
+	 * Handle exception display.
 	 *
-	 * @param Exception $e to be shown to the user
-	 * @return string sanitized text that can be returned to the user
+	 * @param Exception $e Exception to be shown to the user
+	 * @return string Sanitized text that can be returned to the user
 	 */
 	public static function formatException( $e ) {
 		global $wgShowExceptionDetails;
@@ -702,30 +826,38 @@ class ResourceLoader {
 	}
 
 	/**
-	 * Generates code for a response
+	 * Generate code for a response.
 	 *
-	 * @param $context ResourceLoaderContext: Context in which to generate a response
+	 * @param ResourceLoaderContext $context Context in which to generate a response
 	 * @param array $modules List of module objects keyed by module name
-	 * @param array $missing List of unavailable modules (optional)
-	 * @return String: Response data
+	 * @param array $missing List of requested module names that are unregistered (optional)
+	 * @return string Response data
 	 */
 	public function makeModuleResponse( ResourceLoaderContext $context,
-		array $modules, $missing = array()
+		array $modules, array $missing = array()
 	) {
 		$out = '';
 		$exceptions = '';
-		if ( $modules === array() && $missing === array() ) {
-			return '/* No modules requested. Max made me put this here */';
+		$states = array();
+
+		if ( !count( $modules ) && !count( $missing ) ) {
+			return "/* This file is the Web entry point for MediaWiki's ResourceLoader:
+   . In this request,
+   no modules were requested. Max made me put this here. */";
 		}
 
 		wfProfileIn( __METHOD__ );
+
 		// Pre-fetch blobs
 		if ( $context->shouldIncludeMessages() ) {
 			try {
-				$blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() );
+				$blobs = MessageBlobStore::getInstance()->get( $this, $modules, $context->getLanguage() );
 			} catch ( Exception $e ) {
 				MWExceptionHandler::logException( $e );
-				wfDebugLog( 'resourceloader', __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e" );
+				wfDebugLog(
+					'resourceloader',
+					__METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e"
+				);
 				$this->hasErrors = true;
 				// Add exception to the output as a comment
 				$exceptions .= self::formatException( $e );
@@ -734,6 +866,10 @@ class ResourceLoader {
 			$blobs = array();
 		}
 
+		foreach ( $missing as $name ) {
+			$states[$name] = 'missing';
+		}
+
 		// Generate output
 		$isRaw = false;
 		foreach ( $modules as $name => $module ) {
@@ -753,9 +889,15 @@ class ResourceLoader {
 						$scripts = $module->getScriptURLsForDebug( $context );
 					} else {
 						$scripts = $module->getScript( $context );
-						if ( is_string( $scripts ) && strlen( $scripts ) && substr( $scripts, -1 ) !== ';' ) {
-							// bug 27054: Append semicolon to prevent weird bugs
-							// caused by files not terminating their statements right
+						// rtrim() because there are usually a few line breaks
+						// after the last ';'. A new line at EOF, a new line
+						// added by ResourceLoaderFileModule::readScriptFiles, etc.
+						if ( is_string( $scripts )
+							&& strlen( $scripts )
+							&& substr( rtrim( $scripts ), -1 ) !== ';'
+						) {
+							// Append semicolon to prevent weird bugs caused by files not
+							// terminating their statements right (bug 27054)
 							$scripts .= ";\n";
 						}
 					}
@@ -766,7 +908,7 @@ class ResourceLoader {
 					// Don't create empty stylesheets like array( '' => '' ) for modules
 					// that don't *have* any stylesheets (bug 38024).
 					$stylePairs = $module->getStyles( $context );
-					if ( count ( $stylePairs ) ) {
+					if ( count( $stylePairs ) ) {
 						// If we are in debug mode without &only= set, we'll want to return an array of URLs
 						// See comment near shouldIncludeScripts() for more details
 						if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
@@ -838,8 +980,8 @@ class ResourceLoader {
 				// Add exception to the output as a comment
 				$exceptions .= self::formatException( $e );
 
-				// Register module as missing
-				$missing[] = $name;
+				// Respond to client with error-state instead of module implementation
+				$states[$name] = 'error';
 				unset( $modules[$name] );
 			}
 			$isRaw |= $module->isRaw();
@@ -848,14 +990,23 @@ class ResourceLoader {
 
 		// Update module states
 		if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) {
-			// Set the state of modules loaded as only scripts to ready
 			if ( count( $modules ) && $context->getOnly() === 'scripts' ) {
-				$out .= self::makeLoaderStateScript(
-					array_fill_keys( array_keys( $modules ), 'ready' ) );
+				// Set the state of modules loaded as only scripts to ready as
+				// they don't have an mw.loader.implement wrapper that sets the state
+				foreach ( $modules as $name => $module ) {
+					$states[$name] = 'ready';
+				}
+			}
+
+			// Set the state of modules we didn't respond to with mw.loader.implement
+			if ( count( $states ) ) {
+				$out .= self::makeLoaderStateScript( $states );
 			}
-			// Set the state of modules which were requested but unavailable as missing
-			if ( is_array( $missing ) && count( $missing ) ) {
-				$out .= self::makeLoaderStateScript( array_fill_keys( $missing, 'missing' ) );
+		} else {
+			if ( count( $states ) ) {
+				$exceptions .= self::makeComment(
+					'Problematic modules: ' . FormatJson::encode( $states, ResourceLoader::inDebugMode() )
+				);
 			}
 		}
 
@@ -874,23 +1025,21 @@ class ResourceLoader {
 	/* Static Methods */
 
 	/**
-	 * Returns JS code to call to mw.loader.implement for a module with
-	 * given properties.
+	 * Return JS code that calls mw.loader.implement with given module properties.
 	 *
 	 * @param string $name Module name
-	 * @param $scripts Mixed: List of URLs to JavaScript files or String of JavaScript code
-	 * @param $styles Mixed: Array of CSS strings keyed by media type, or an array of lists of URLs to
-	 * CSS files keyed by media type
-	 * @param $messages Mixed: List of messages associated with this module. May either be an
-	 *     associative array mapping message key to value, or a JSON-encoded message blob containing
-	 *     the same data, wrapped in an XmlJsCode object.
-	 *
+	 * @param mixed $scripts List of URLs to JavaScript files or String of JavaScript code
+	 * @param mixed $styles Array of CSS strings keyed by media type, or an array of lists of URLs
+	 *   to CSS files keyed by media type
+	 * @param mixed $messages List of messages associated with this module. May either be an
+	 *   associative array mapping message key to value, or a JSON-encoded message blob containing
+	 *   the same data, wrapped in an XmlJsCode object.
 	 * @throws MWException
 	 * @return string
 	 */
 	public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
 		if ( is_string( $scripts ) ) {
-			$scripts = new XmlJsCode( "function () {\n{$scripts}\n}" );
+			$scripts = new XmlJsCode( "function ( $, jQuery ) {\n{$scripts}\n}" );
 		} elseif ( !is_array( $scripts ) ) {
 			throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
 		}
@@ -914,24 +1063,26 @@ class ResourceLoader {
 	/**
 	 * Returns JS code which, when called, will register a given list of messages.
 	 *
-	 * @param $messages Mixed: Either an associative array mapping message key to value, or a
-	 *     JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object.
-	 *
+	 * @param mixed $messages Either an associative array mapping message key to value, or a
+	 *   JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object.
 	 * @return string
 	 */
 	public static function makeMessageSetScript( $messages ) {
-		return Xml::encodeJsCall( 'mw.messages.set', array( (object)$messages ) );
+		return Xml::encodeJsCall(
+			'mw.messages.set',
+			array( (object)$messages ),
+			ResourceLoader::inDebugMode()
+		);
 	}
 
 	/**
 	 * Combines an associative array mapping media type to CSS into a
 	 * single stylesheet with "@media" blocks.
 	 *
-	 * @param array $stylePairs Array keyed by media type containing (arrays of) CSS strings.
-	 *
-	 * @return Array
+	 * @param array $stylePairs Array keyed by media type containing (arrays of) CSS strings
+	 * @return array
 	 */
-	private static function makeCombinedStyles( array $stylePairs ) {
+	public static function makeCombinedStyles( array $stylePairs ) {
 		$out = array();
 		foreach ( $stylePairs as $media => $styles ) {
 			// ResourceLoaderFileModule::getStyle can return the styles
@@ -968,16 +1119,23 @@ class ResourceLoader {
 	 *    - ResourceLoader::makeLoaderStateScript( array( $name => $state, ... ) ):
 	 *         Set the state of modules with the given names to the given states
 	 *
-	 * @param $name string
-	 * @param $state
-	 *
+	 * @param string $name
+	 * @param string $state
 	 * @return string
 	 */
 	public static function makeLoaderStateScript( $name, $state = null ) {
 		if ( is_array( $name ) ) {
-			return Xml::encodeJsCall( 'mw.loader.state', array( $name ) );
+			return Xml::encodeJsCall(
+				'mw.loader.state',
+				array( $name ),
+				ResourceLoader::inDebugMode()
+			);
 		} else {
-			return Xml::encodeJsCall( 'mw.loader.state', array( $name, $state ) );
+			return Xml::encodeJsCall(
+				'mw.loader.state',
+				array( $name, $state ),
+				ResourceLoader::inDebugMode()
+			);
 		}
 	}
 
@@ -988,55 +1146,67 @@ class ResourceLoader {
 	 * and $group as supplied.
 	 *
 	 * @param string $name Module name
-	 * @param $version Integer: Module version number as a timestamp
+	 * @param int $version Module version number as a timestamp
 	 * @param array $dependencies List of module names on which this module depends
 	 * @param string $group Group which the module is in.
 	 * @param string $source Source of the module, or 'local' if not foreign.
 	 * @param string $script JavaScript code
-	 *
 	 * @return string
 	 */
-	public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $source, $script ) {
+	public static function makeCustomLoaderScript( $name, $version, $dependencies,
+		$group, $source, $script
+	) {
 		$script = str_replace( "\n", "\n\t", trim( $script ) );
 		return Xml::encodeJsCall(
 			"( function ( name, version, dependencies, group, source ) {\n\t$script\n} )",
-			array( $name, $version, $dependencies, $group, $source ) );
+			array( $name, $version, $dependencies, $group, $source ),
+			ResourceLoader::inDebugMode()
+		);
 	}
 
 	/**
 	 * Returns JS code which calls mw.loader.register with the given
 	 * parameters. Has three calling conventions:
 	 *
-	 *   - ResourceLoader::makeLoaderRegisterScript( $name, $version, $dependencies, $group, $source ):
-	 *       Register a single module.
+	 *   - ResourceLoader::makeLoaderRegisterScript( $name, $version,
+	 *        $dependencies, $group, $source, $skip
+	 *     ):
+	 *        Register a single module.
 	 *
 	 *   - ResourceLoader::makeLoaderRegisterScript( array( $name1, $name2 ) ):
-	 *       Register modules with the given names.
+	 *        Register modules with the given names.
 	 *
 	 *   - ResourceLoader::makeLoaderRegisterScript( array(
-	 *        array( $name1, $version1, $dependencies1, $group1, $source1 ),
-	 *        array( $name2, $version2, $dependencies1, $group2, $source2 ),
+	 *        array( $name1, $version1, $dependencies1, $group1, $source1, $skip1 ),
+	 *        array( $name2, $version2, $dependencies1, $group2, $source2, $skip2 ),
 	 *        ...
 	 *     ) ):
 	 *        Registers modules with the given names and parameters.
 	 *
 	 * @param string $name Module name
-	 * @param $version Integer: Module version number as a timestamp
+	 * @param int $version Module version number as a timestamp
 	 * @param array $dependencies List of module names on which this module depends
-	 * @param string $group group which the module is in.
-	 * @param string $source source of the module, or 'local' if not foreign
-	 *
+	 * @param string $group Group which the module is in
+	 * @param string $source Source of the module, or 'local' if not foreign
+	 * @param string $skip Script body of the skip function
 	 * @return string
 	 */
 	public static function makeLoaderRegisterScript( $name, $version = null,
-		$dependencies = null, $group = null, $source = null
+		$dependencies = null, $group = null, $source = null, $skip = null
 	) {
 		if ( is_array( $name ) ) {
-			return Xml::encodeJsCall( 'mw.loader.register', array( $name ) );
+			return Xml::encodeJsCall(
+				'mw.loader.register',
+				array( $name ),
+				ResourceLoader::inDebugMode()
+			);
 		} else {
 			$version = (int)$version > 1 ? (int)$version : 1;
-			return Xml::encodeJsCall( 'mw.loader.register',
-				array( $name, $version, $dependencies, $group, $source ) );
+			return Xml::encodeJsCall(
+				'mw.loader.register',
+				array( $name, $version, $dependencies, $group, $source, $skip ),
+				ResourceLoader::inDebugMode()
+			);
 		}
 	}
 
@@ -1047,19 +1217,26 @@ class ResourceLoader {
 	 *   - ResourceLoader::makeLoaderSourcesScript( $id, $properties ):
 	 *       Register a single source
 	 *
-	 *   - ResourceLoader::makeLoaderSourcesScript( array( $id1 => $props1, $id2 => $props2, ... ) );
+	 *   - ResourceLoader::makeLoaderSourcesScript( array( $id1 => $loadUrl, $id2 => $loadUrl, ... ) );
 	 *       Register sources with the given IDs and properties.
 	 *
-	 * @param string $id source ID
-	 * @param array $properties source properties (see addSource())
-	 *
+	 * @param string $id Source ID
+	 * @param array $properties Source properties (see addSource())
 	 * @return string
 	 */
 	public static function makeLoaderSourcesScript( $id, $properties = null ) {
 		if ( is_array( $id ) ) {
-			return Xml::encodeJsCall( 'mw.loader.addSource', array( $id ) );
+			return Xml::encodeJsCall(
+				'mw.loader.addSource',
+				array( $id ),
+				ResourceLoader::inDebugMode()
+			);
 		} else {
-			return Xml::encodeJsCall( 'mw.loader.addSource', array( $id, $properties ) );
+			return Xml::encodeJsCall(
+				'mw.loader.addSource',
+				array( $id, $properties ),
+				ResourceLoader::inDebugMode()
+			);
 		}
 	}
 
@@ -1068,7 +1245,6 @@ class ResourceLoader {
 	 * present.
 	 *
 	 * @param string $script JavaScript code
-	 *
 	 * @return string
 	 */
 	public static function makeLoaderConditionalScript( $script ) {
@@ -1080,11 +1256,14 @@ class ResourceLoader {
 	 * the given value.
 	 *
 	 * @param array $configuration List of configuration values keyed by variable name
-	 *
 	 * @return string
 	 */
 	public static function makeConfigSetScript( array $configuration ) {
-		return Xml::encodeJsCall( 'mw.config.set', array( $configuration ), ResourceLoader::inDebugMode() );
+		return Xml::encodeJsCall(
+			'mw.config.set',
+			array( $configuration ),
+			ResourceLoader::inDebugMode()
+		);
 	}
 
 	/**
@@ -1092,7 +1271,7 @@ class ResourceLoader {
 	 *
 	 * For example, array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' )
 	 * becomes 'foo.bar,baz|bar.baz,quux'
-	 * @param array $modules of module names (strings)
+	 * @param array $modules List of module names (strings)
 	 * @return string Packed query string
 	 */
 	public static function makePackedModulesString( $modules ) {
@@ -1119,18 +1298,50 @@ class ResourceLoader {
 	 * @return bool
 	 */
 	public static function inDebugMode() {
-		global $wgRequest, $wgResourceLoaderDebug;
-		static $retval = null;
-		if ( !is_null( $retval ) ) {
-			return $retval;
+		if ( self::$debugMode === null ) {
+			global $wgRequest, $wgResourceLoaderDebug;
+			self::$debugMode = $wgRequest->getFuzzyBool( 'debug',
+				$wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug )
+			);
 		}
-		return $retval = $wgRequest->getFuzzyBool( 'debug',
-			$wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) );
+		return self::$debugMode;
+	}
+
+	/**
+	 * Reset static members used for caching.
+	 *
+	 * Global state and $wgRequest are evil, but we're using it right
+	 * now and sometimes we need to be able to force ResourceLoader to
+	 * re-evaluate the context because it has changed (e.g. in the test suite).
+	 */
+	public static function clearCache() {
+		self::$debugMode = null;
 	}
 
 	/**
 	 * Build a load.php URL
-	 * @param array $modules of module names (strings)
+	 *
+	 * @since 1.24
+	 * @param string $source Name of the ResourceLoader source
+	 * @param ResourceLoaderContext $context
+	 * @param array $extraQuery
+	 * @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative)
+	 */
+	public function createLoaderURL( $source, ResourceLoaderContext $context,
+		$extraQuery = array()
+	) {
+		$query = self::createLoaderQuery( $context, $extraQuery );
+		$script = $this->getLoadScript( $source );
+
+		// Prevent the IE6 extension check from being triggered (bug 28840)
+		// by appending a character that's invalid in Windows extensions ('*')
+		return wfExpandUrl( wfAppendQuery( $script, $query ) . '&*', PROTO_RELATIVE );
+	}
+
+	/**
+	 * Build a load.php URL
+	 * @deprecated since 1.24, use createLoaderURL instead
+	 * @param array $modules Array of module names (strings)
 	 * @param string $lang Language code
 	 * @param string $skin Skin name
 	 * @param string|null $user User name. If null, the &user= parameter is omitted
@@ -1142,9 +1353,12 @@ class ResourceLoader {
 	 * @param array $extraQuery Extra query parameters to add
 	 * @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative)
 	 */
-	public static function makeLoaderURL( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
-			$printable = false, $handheld = false, $extraQuery = array() ) {
+	public static function makeLoaderURL( $modules, $lang, $skin, $user = null,
+		$version = null, $debug = false, $only = null, $printable = false,
+		$handheld = false, $extraQuery = array()
+	) {
 		global $wgLoadScript;
+
 		$query = self::makeLoaderQuery( $modules, $lang, $skin, $user, $version, $debug,
 			$only, $printable, $handheld, $extraQuery
 		);
@@ -1154,6 +1368,30 @@ class ResourceLoader {
 		return wfExpandUrl( wfAppendQuery( $wgLoadScript, $query ) . '&*', PROTO_RELATIVE );
 	}
 
+	/**
+	 * Helper for createLoaderURL()
+	 *
+	 * @since 1.24
+	 * @see makeLoaderQuery
+	 * @param ResourceLoaderContext $context
+	 * @param array $extraQuery
+	 * @return array
+	 */
+	public static function createLoaderQuery( ResourceLoaderContext $context, $extraQuery = array() ) {
+		return self::makeLoaderQuery(
+			$context->getModules(),
+			$context->getLanguage(),
+			$context->getSkin(),
+			$context->getUser(),
+			$context->getVersion(),
+			$context->getDebug(),
+			$context->getOnly(),
+			$context->getRequest()->getBool( 'printable' ),
+			$context->getRequest()->getBool( 'handheld' ),
+			$extraQuery
+		);
+	}
+
 	/**
 	 * Build a query array (array representation of query string) for load.php. Helper
 	 * function for makeLoaderURL().
@@ -1171,8 +1409,10 @@ class ResourceLoader {
 	 *
 	 * @return array
 	 */
-	public static function makeLoaderQuery( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
-			$printable = false, $handheld = false, $extraQuery = array() ) {
+	public static function makeLoaderQuery( $modules, $lang, $skin, $user = null,
+		$version = null, $debug = false, $only = null, $printable = false,
+		$handheld = false, $extraQuery = array()
+	) {
 		$query = array(
 			'modules' => self::makePackedModulesString( $modules ),
 			'lang' => $lang,
@@ -1217,12 +1457,12 @@ class ResourceLoader {
 	/**
 	 * Returns LESS compiler set up for use with MediaWiki
 	 *
+	 * @param Config $config
+	 * @throws MWException
 	 * @since 1.22
 	 * @return lessc
 	 */
-	public static function getLessCompiler() {
-		global $wgResourceLoaderLESSFunctions, $wgResourceLoaderLESSImportPaths;
-
+	public static function getLessCompiler( Config $config ) {
 		// When called from the installer, it is possible that a required PHP extension
 		// is missing (at least for now; see bug 47564). If this is the case, throw an
 		// exception (caught by the installer) to prevent a fatal error later on.
@@ -1232,9 +1472,9 @@ class ResourceLoader {
 
 		$less = new lessc();
 		$less->setPreserveComments( true );
-		$less->setVariables( self::getLESSVars() );
-		$less->setImportDir( $wgResourceLoaderLESSImportPaths );
-		foreach ( $wgResourceLoaderLESSFunctions as $name => $func ) {
+		$less->setVariables( self::getLessVars( $config ) );
+		$less->setImportDir( $config->get( 'ResourceLoaderLESSImportPaths' ) );
+		foreach ( $config->get( 'ResourceLoaderLESSFunctions' ) as $name => $func ) {
 			$less->registerFunction( $name, $func );
 		}
 		return $less;
@@ -1243,18 +1483,14 @@ class ResourceLoader {
 	/**
 	 * Get global LESS variables.
 	 *
-	 * $since 1.22
-	 * @return array: Map of variable names to string CSS values.
+	 * @param Config $config
+	 * @since 1.22
+	 * @return array Map of variable names to string CSS values.
 	 */
-	public static function getLESSVars() {
-		global $wgResourceLoaderLESSVars;
-
-		static $lessVars = null;
-		if ( $lessVars === null ) {
-			$lessVars = $wgResourceLoaderLESSVars;
-			// Sort by key to ensure consistent hashing for cache lookups.
-			ksort( $lessVars );
-		}
+	public static function getLessVars( Config $config ) {
+		$lessVars = $config->get( 'ResourceLoaderLESSVars' );
+		// Sort by key to ensure consistent hashing for cache lookups.
+		ksort( $lessVars );
 		return $lessVars;
 	}
 }
diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php
index 22ff6a7e..7af7b898 100644
--- a/includes/resourceloader/ResourceLoaderContext.php
+++ b/includes/resourceloader/ResourceLoaderContext.php
@@ -27,7 +27,6 @@
  * of a specific loader request
  */
 class ResourceLoaderContext {
-
 	/* Protected Members */
 
 	protected $resourceLoader;
@@ -46,12 +45,10 @@ class ResourceLoaderContext {
 	/* Methods */
 
 	/**
-	 * @param $resourceLoader ResourceLoader
-	 * @param $request WebRequest
+	 * @param ResourceLoader $resourceLoader
+	 * @param WebRequest $request
 	 */
-	public function __construct( $resourceLoader, WebRequest $request ) {
-		global $wgDefaultSkin, $wgResourceLoaderDebug;
-
+	public function __construct( ResourceLoader $resourceLoader, WebRequest $request ) {
 		$this->resourceLoader = $resourceLoader;
 		$this->request = $request;
 
@@ -62,7 +59,9 @@ class ResourceLoaderContext {
 		// Various parameters
 		$this->skin = $request->getVal( 'skin' );
 		$this->user = $request->getVal( 'user' );
-		$this->debug = $request->getFuzzyBool( 'debug', $wgResourceLoaderDebug );
+		$this->debug = $request->getFuzzyBool(
+			'debug', $resourceLoader->getConfig()->get( 'ResourceLoaderDebug' )
+		);
 		$this->only = $request->getVal( 'only' );
 		$this->version = $request->getVal( 'version' );
 		$this->raw = $request->getFuzzyBool( 'raw' );
@@ -70,7 +69,7 @@ class ResourceLoaderContext {
 		$skinnames = Skin::getSkinNames();
 		// If no skin is specified, or we don't recognize the skin, use the default skin
 		if ( !$this->skin || !isset( $skinnames[$this->skin] ) ) {
-			$this->skin = $wgDefaultSkin;
+			$this->skin = $resourceLoader->getConfig()->get( 'DefaultSkin' );
 		}
 	}
 
@@ -79,12 +78,10 @@ class ResourceLoaderContext {
 	 * an array of module names like array( 'jquery.foo', 'jquery.bar',
 	 * 'jquery.ui.baz', 'jquery.ui.quux' )
 	 * @param string $modules Packed module name list
-	 * @return array of module names
+	 * @return array Array of module names
 	 */
 	public static function expandModuleNames( $modules ) {
 		$retval = array();
-		// For backwards compatibility with an earlier hack, replace ! with .
-		$modules = str_replace( '!', '.', $modules );
 		$exploded = explode( '|', $modules );
 		foreach ( $exploded as $group ) {
 			if ( strpos( $group, ',' ) === false ) {
@@ -111,11 +108,14 @@ class ResourceLoaderContext {
 	}
 
 	/**
-	 * Return a dummy ResourceLoaderContext object suitable for passing into things that don't "really" need a context
+	 * Return a dummy ResourceLoaderContext object suitable for passing into
+	 * things that don't "really" need a context.
 	 * @return ResourceLoaderContext
 	 */
 	public static function newDummyContext() {
-		return new self( null, new FauxRequest( array() ) );
+		return new self( new ResourceLoader(
+			ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
+		), new FauxRequest( array() ) );
 	}
 
 	/**
@@ -144,11 +144,8 @@ class ResourceLoaderContext {
 	 */
 	public function getLanguage() {
 		if ( $this->language === null ) {
-			global $wgLang;
-			$this->language = $this->request->getVal( 'lang' );
-			if ( !$this->language ) {
-				$this->language = $wgLang->getCode();
-			}
+			// Must be a valid language code after this point (bug 62849)
+			$this->language = RequestContext::sanitizeLangCode( $this->request->getVal( 'lang' ) );
 		}
 		return $this->language;
 	}
@@ -160,7 +157,7 @@ class ResourceLoaderContext {
 		if ( $this->direction === null ) {
 			$this->direction = $this->request->getVal( 'dir' );
 			if ( !$this->direction ) {
-				# directionality based on user language (see bug 6100)
+				// Determine directionality based on user language (bug 6100)
 				$this->direction = Language::factory( $this->getLanguage() )->getDir();
 			}
 		}
@@ -189,14 +186,14 @@ class ResourceLoaderContext {
 	}
 
 	/**
-	 * @return String|null
+	 * @return string|null
 	 */
 	public function getOnly() {
 		return $this->only;
 	}
 
 	/**
-	 * @return String|null
+	 * @return string|null
 	 */
 	public function getVersion() {
 		return $this->version;
@@ -213,21 +210,21 @@ class ResourceLoaderContext {
 	 * @return bool
 	 */
 	public function shouldIncludeScripts() {
-		return is_null( $this->only ) || $this->only === 'scripts';
+		return is_null( $this->getOnly() ) || $this->getOnly() === 'scripts';
 	}
 
 	/**
 	 * @return bool
 	 */
 	public function shouldIncludeStyles() {
-		return is_null( $this->only ) || $this->only === 'styles';
+		return is_null( $this->getOnly() ) || $this->getOnly() === 'styles';
 	}
 
 	/**
 	 * @return bool
 	 */
 	public function shouldIncludeMessages() {
-		return is_null( $this->only ) || $this->only === 'messages';
+		return is_null( $this->getOnly() ) || $this->getOnly() === 'messages';
 	}
 
 	/**
@@ -236,8 +233,8 @@ class ResourceLoaderContext {
 	public function getHash() {
 		if ( !isset( $this->hash ) ) {
 			$this->hash = implode( '|', array(
-				$this->getLanguage(), $this->getDirection(), $this->skin, $this->user,
-				$this->debug, $this->only, $this->version
+				$this->getLanguage(), $this->getDirection(), $this->getSkin(), $this->getUser(),
+				$this->getDebug(), $this->getOnly(), $this->getVersion()
 			) );
 		}
 		return $this->hash;
diff --git a/includes/resourceloader/ResourceLoaderEditToolbarModule.php b/includes/resourceloader/ResourceLoaderEditToolbarModule.php
new file mode 100644
index 00000000..2e07911c
--- /dev/null
+++ b/includes/resourceloader/ResourceLoaderEditToolbarModule.php
@@ -0,0 +1,102 @@
+ '\\\\', '"' => '\\"' ) );
+		$value = preg_replace_callback( '/[\x01-\x1f\x7f-\x9f]/', function ( $match ) {
+			return '\\' . base_convert( ord( $match[0] ), 10, 16 ) . ' ';
+		}, $value );
+		return '"' . $value . '"';
+	}
+
+	/**
+	 * Get language-specific LESS variables for this module.
+	 *
+	 * @return array
+	 */
+	private function getLessVars( ResourceLoaderContext $context ) {
+		$language = Language::factory( $context->getLanguage() );
+
+		// This is very conveniently formatted and we can pass it right through
+		$vars = $language->getImageFiles();
+
+		// lessc tries to be helpful and parse our variables as LESS source code
+		foreach ( $vars as $key => &$value ) {
+			$value = self::cssSerializeString( $value );
+		}
+
+		return $vars;
+	}
+
+	/**
+	 * @param ResourceLoaderContext $context
+	 * @return int UNIX timestamp
+	 */
+	public function getModifiedTime( ResourceLoaderContext $context ) {
+		return max(
+			parent::getModifiedTime( $context ),
+			$this->getHashMtime( $context )
+		);
+	}
+
+	/**
+	 * @param ResourceLoaderContext $context
+	 * @return string Hash
+	 */
+	public function getModifiedHash( ResourceLoaderContext $context ) {
+		return md5(
+			parent::getModifiedHash( $context ) .
+			serialize( $this->getLessVars( $context ) )
+		);
+	}
+
+	/**
+	 * Get a LESS compiler instance for this module.
+	 *
+	 * Set our variables in it.
+	 *
+	 * @throws MWException
+	 * @param ResourceLoaderContext $context
+	 * @return lessc
+	 */
+	protected function getLessCompiler( ResourceLoaderContext $context = null ) {
+		$compiler = parent::getLessCompiler();
+		$compiler->setVariables( $this->getLessVars( $context ) );
+		return $compiler;
+	}
+}
diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php
index 9ed181ed..dc8b14a2 100644
--- a/includes/resourceloader/ResourceLoaderFileModule.php
+++ b/includes/resourceloader/ResourceLoaderFileModule.php
@@ -26,111 +26,131 @@
  * ResourceLoader module based on local JavaScript/CSS files.
  */
 class ResourceLoaderFileModule extends ResourceLoaderModule {
-
 	/* Protected Members */
 
-	/** String: Local base path, see __construct() */
+	/** @var string Local base path, see __construct() */
 	protected $localBasePath = '';
-	/** String: Remote base path, see __construct() */
+
+	/** @var string Remote base path, see __construct() */
 	protected $remoteBasePath = '';
+
 	/**
-	 * Array: List of paths to JavaScript files to always include
+	 * @var array List of paths to JavaScript files to always include
 	 * @par Usage:
 	 * @code
 	 * array( [file-path], [file-path], ... )
 	 * @endcode
 	 */
 	protected $scripts = array();
+
 	/**
-	 * Array: List of JavaScript files to include when using a specific language
+	 * @var array List of JavaScript files to include when using a specific language
 	 * @par Usage:
 	 * @code
 	 * array( [language-code] => array( [file-path], [file-path], ... ), ... )
 	 * @endcode
 	 */
 	protected $languageScripts = array();
+
 	/**
-	 * Array: List of JavaScript files to include when using a specific skin
+	 * @var array List of JavaScript files to include when using a specific skin
 	 * @par Usage:
 	 * @code
 	 * array( [skin-name] => array( [file-path], [file-path], ... ), ... )
 	 * @endcode
 	 */
 	protected $skinScripts = array();
+
 	/**
-	 * Array: List of paths to JavaScript files to include in debug mode
+	 * @var array List of paths to JavaScript files to include in debug mode
 	 * @par Usage:
 	 * @code
 	 * array( [skin-name] => array( [file-path], [file-path], ... ), ... )
 	 * @endcode
 	 */
 	protected $debugScripts = array();
+
 	/**
-	 * Array: List of paths to JavaScript files to include in the startup module
+	 * @var array List of paths to JavaScript files to include in the startup module
 	 * @par Usage:
 	 * @code
 	 * array( [file-path], [file-path], ... )
 	 * @endcode
 	 */
 	protected $loaderScripts = array();
+
 	/**
-	 * Array: List of paths to CSS files to always include
+	 * @var array List of paths to CSS files to always include
 	 * @par Usage:
 	 * @code
 	 * array( [file-path], [file-path], ... )
 	 * @endcode
 	 */
 	protected $styles = array();
+
 	/**
-	 * Array: List of paths to CSS files to include when using specific skins
+	 * @var array List of paths to CSS files to include when using specific skins
 	 * @par Usage:
 	 * @code
 	 * array( [file-path], [file-path], ... )
 	 * @endcode
 	 */
 	protected $skinStyles = array();
+
 	/**
-	 * Array: List of modules this module depends on
+	 * @var array List of modules this module depends on
 	 * @par Usage:
 	 * @code
 	 * array( [file-path], [file-path], ... )
 	 * @endcode
 	 */
 	protected $dependencies = array();
+
+	/**
+	 * @var string File name containing the body of the skip function
+	 */
+	protected $skipFunction = null;
+
 	/**
-	 * Array: List of message keys used by this module
+	 * @var array List of message keys used by this module
 	 * @par Usage:
 	 * @code
 	 * array( [message-key], [message-key], ... )
 	 * @endcode
 	 */
 	protected $messages = array();
-	/** String: Name of group to load this module in */
+
+	/** @var string Name of group to load this module in */
 	protected $group;
-	/** String: Position on the page to load this module at */
+
+	/** @var string Position on the page to load this module at */
 	protected $position = 'bottom';
-	/** Boolean: Link to raw files in debug mode */
+
+	/** @var bool Link to raw files in debug mode */
 	protected $debugRaw = true;
-	/** Boolean: Whether mw.loader.state() call should be omitted */
+
+	/** @var bool Whether mw.loader.state() call should be omitted */
 	protected $raw = false;
+
 	protected $targets = array( 'desktop' );
 
 	/**
-	 * Boolean: Whether getStyleURLsForDebug should return raw file paths,
+	 * @var bool Whether getStyleURLsForDebug should return raw file paths,
 	 * or return load.php urls
 	 */
 	protected $hasGeneratedStyles = false;
 
 	/**
-	 * Array: Cache for mtime
+	 * @var array Cache for mtime
 	 * @par Usage:
 	 * @code
 	 * array( [hash] => [mtime], [hash] => [mtime], ... )
 	 * @endcode
 	 */
 	protected $modifiedTime = array();
+
 	/**
-	 * Array: Place where readStyleFile() tracks file dependencies
+	 * @var array Place where readStyleFile() tracks file dependencies
 	 * @par Usage:
 	 * @code
 	 * array( [file-path], [file-path], ... )
@@ -148,7 +168,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	 * @param string $localBasePath Base path to prepend to all local paths in $options. Defaults
 	 *     to $IP
 	 * @param string $remoteBasePath Base path to prepend to all remote paths in $options. Defaults
-	 *     to $wgScriptPath
+	 *     to $wgResourceBasePath
 	 *
 	 * Below is a description for the $options array:
 	 * @throws MWException
@@ -157,10 +177,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	 *     array(
 	 *         // Base path to prepend to all local paths in $options. Defaults to $IP
 	 *         'localBasePath' => [base path],
-	 *         // Base path to prepend to all remote paths in $options. Defaults to $wgScriptPath
+	 *         // Base path to prepend to all remote paths in $options. Defaults to $wgResourceBasePath
 	 *         'remoteBasePath' => [base path],
 	 *         // Equivalent of remoteBasePath, but relative to $wgExtensionAssetsPath
 	 *         'remoteExtPath' => [base path],
+	 *         // Equivalent of remoteBasePath, but relative to $wgStylePath
+	 *         'remoteSkinPath' => [base path],
 	 *         // Scripts to always include
 	 *         'scripts' => [file path string or array of file path strings],
 	 *         // Scripts to include in specific language contexts
@@ -189,25 +211,24 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	 *         'group' => [group name string],
 	 *         // Position on the page to load this module at
 	 *         'position' => ['bottom' (default) or 'top']
+	 *         // Function that, if it returns true, makes the loader skip this module.
+	 *         // The file must contain valid JavaScript for execution in a private function.
+	 *         // The file must not contain the "function () {" and "}" wrapper though.
+	 *         'skipFunction' => [file path]
 	 *     )
 	 * @endcode
 	 */
-	public function __construct( $options = array(), $localBasePath = null,
+	public function __construct(
+		$options = array(),
+		$localBasePath = null,
 		$remoteBasePath = null
 	) {
-		global $IP, $wgScriptPath, $wgResourceBasePath;
-		$this->localBasePath = $localBasePath === null ? $IP : $localBasePath;
-		if ( $remoteBasePath !== null ) {
-			$this->remoteBasePath = $remoteBasePath;
-		} else {
-			$this->remoteBasePath = $wgResourceBasePath === null ? $wgScriptPath : $wgResourceBasePath;
-		}
-
-		if ( isset( $options['remoteExtPath'] ) ) {
-			global $wgExtensionAssetsPath;
-			$this->remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath'];
-		}
+		// localBasePath and remoteBasePath both have unbelievably long fallback chains
+		// and need to be handled separately.
+		list( $this->localBasePath, $this->remoteBasePath ) =
+			self::extractBasePaths( $options, $localBasePath, $remoteBasePath );
 
+		// Extract, validate and normalise remaining options
 		foreach ( $options as $member => $option ) {
 			switch ( $member ) {
 				// Lists of file paths
@@ -241,13 +262,16 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 				case 'dependencies':
 				case 'messages':
 				case 'targets':
-					$this->{$member} = (array)$option;
+					// Normalise
+					$option = array_values( array_unique( (array)$option ) );
+					sort( $option );
+
+					$this->{$member} = $option;
 					break;
 				// Single strings
 				case 'group':
 				case 'position':
-				case 'localBasePath':
-				case 'remoteBasePath':
+				case 'skipFunction':
 					$this->{$member} = (string)$option;
 					break;
 				// Single booleans
@@ -257,16 +281,64 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 					break;
 			}
 		}
+	}
+
+	/**
+	 * Extract a pair of local and remote base paths from module definition information.
+	 * Implementation note: the amount of global state used in this function is staggering.
+	 *
+	 * @param array $options Module definition
+	 * @param string $localBasePath Path to use if not provided in module definition. Defaults
+	 *     to $IP
+	 * @param string $remoteBasePath Path to use if not provided in module definition. Defaults
+	 *     to $wgResourceBasePath
+	 * @return array Array( localBasePath, remoteBasePath )
+	 */
+	public static function extractBasePaths(
+		$options = array(),
+		$localBasePath = null,
+		$remoteBasePath = null
+	) {
+		global $IP, $wgResourceBasePath;
+
+		// The different ways these checks are done, and their ordering, look very silly,
+		// but were preserved for backwards-compatibility just in case. Tread lightly.
+
+		$localBasePath = $localBasePath === null ? $IP : $localBasePath;
+		if ( $remoteBasePath === null ) {
+			$remoteBasePath = $wgResourceBasePath;
+		}
+
+		if ( isset( $options['remoteExtPath'] ) ) {
+			global $wgExtensionAssetsPath;
+			$remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath'];
+		}
+
+		if ( isset( $options['remoteSkinPath'] ) ) {
+			global $wgStylePath;
+			$remoteBasePath = $wgStylePath . '/' . $options['remoteSkinPath'];
+		}
+
+		if ( array_key_exists( 'localBasePath', $options ) ) {
+			$localBasePath = (string)$options['localBasePath'];
+		}
+
+		if ( array_key_exists( 'remoteBasePath', $options ) ) {
+			$remoteBasePath = (string)$options['remoteBasePath'];
+		}
+
 		// Make sure the remote base path is a complete valid URL,
 		// but possibly protocol-relative to avoid cache pollution
-		$this->remoteBasePath = wfExpandUrl( $this->remoteBasePath, PROTO_RELATIVE );
+		$remoteBasePath = wfExpandUrl( $remoteBasePath, PROTO_RELATIVE );
+
+		return array( $localBasePath, $remoteBasePath );
 	}
 
 	/**
 	 * Gets all scripts for a given context concatenated together.
 	 *
 	 * @param ResourceLoaderContext $context Context in which to generate script
-	 * @return string: JavaScript code for $context
+	 * @return string JavaScript code for $context
 	 */
 	public function getScript( ResourceLoaderContext $context ) {
 		$files = $this->getScriptFiles( $context );
@@ -293,27 +365,28 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	}
 
 	/**
-	 * Gets loader script.
+	 * Get loader script.
 	 *
-	 * @return string: JavaScript code to be added to startup module
+	 * @return string|bool JavaScript code to be added to startup module
 	 */
 	public function getLoaderScript() {
-		if ( count( $this->loaderScripts ) == 0 ) {
+		if ( count( $this->loaderScripts ) === 0 ) {
 			return false;
 		}
 		return $this->readScriptFiles( $this->loaderScripts );
 	}
 
 	/**
-	 * Gets all styles for a given context concatenated together.
+	 * Get all styles for a given context.
 	 *
-	 * @param ResourceLoaderContext $context Context in which to generate styles
-	 * @return string: CSS code for $context
+	 * @param ResourceLoaderContext $context
+	 * @return array CSS code for $context as an associative array mapping media type to CSS text.
 	 */
 	public function getStyles( ResourceLoaderContext $context ) {
 		$styles = $this->readStyleFiles(
 			$this->getStyleFiles( $context ),
-			$this->getFlip( $context )
+			$this->getFlip( $context ),
+			$context
 		);
 		// Collect referenced files
 		$this->localFileRefs = array_unique( $this->localFileRefs );
@@ -360,7 +433,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	/**
 	 * Gets list of message keys used by this module.
 	 *
-	 * @return array: List of message keys
+	 * @return array List of message keys
 	 */
 	public function getMessages() {
 		return $this->messages;
@@ -369,7 +442,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	/**
 	 * Gets the name of the group this module should be loaded in.
 	 *
-	 * @return string: Group name
+	 * @return string Group name
 	 */
 	public function getGroup() {
 		return $this->group;
@@ -385,12 +458,33 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	/**
 	 * Gets list of names of modules this module depends on.
 	 *
-	 * @return array: List of module names
+	 * @return array List of module names
 	 */
 	public function getDependencies() {
 		return $this->dependencies;
 	}
 
+	/**
+	 * Get the skip function.
+	 *
+	 * @return string|null
+	 */
+	public function getSkipFunction() {
+		if ( !$this->skipFunction ) {
+			return null;
+		}
+
+		$localPath = $this->getLocalPath( $this->skipFunction );
+		if ( !file_exists( $localPath ) ) {
+			throw new MWException( __METHOD__ . ": skip function file not found: \"$localPath\"" );
+		}
+		$contents = file_get_contents( $localPath );
+		if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) {
+			$contents = $this->validateScriptFile( $localPath, $contents );
+		}
+		return $contents;
+	}
+
 	/**
 	 * @return bool
 	 */
@@ -409,7 +503,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	 *
 	 * @param ResourceLoaderContext $context Context in which to calculate
 	 *     the modified time
-	 * @return int: UNIX timestamp
+	 * @return int UNIX timestamp
 	 * @see ResourceLoaderModule::getFileDependencies
 	 */
 	public function getModifiedTime( ResourceLoaderContext $context ) {
@@ -425,10 +519,11 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 		foreach ( $styles as $styleFiles ) {
 			$files = array_merge( $files, $styleFiles );
 		}
-		$skinFiles = self::tryForKey(
-			self::collateFilePathListByOption( $this->skinStyles, 'media', 'all' ),
-			$context->getSkin(),
-			'default'
+
+		$skinFiles = self::collateFilePathListByOption(
+			self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
+			'media',
+			'all'
 		);
 		foreach ( $skinFiles as $styleFiles ) {
 			$files = array_merge( $files, $styleFiles );
@@ -443,6 +538,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 			self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ),
 			$this->loaderScripts
 		);
+		if ( $this->skipFunction ) {
+			$files[] = $this->skipFunction;
+		}
 		$files = array_map( array( $this, 'getLocalPath' ), $files );
 		// File deps need to be treated separately because they're already prefixed
 		$files = array_merge( $files, $this->getFileDependencies( $context->getSkin() ) );
@@ -450,36 +548,82 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 		// If a module is nothing but a list of dependencies, we need to avoid
 		// giving max() an empty array
 		if ( count( $files ) === 0 ) {
+			$this->modifiedTime[$context->getHash()] = 1;
 			wfProfileOut( __METHOD__ );
-			return $this->modifiedTime[$context->getHash()] = 1;
+			return $this->modifiedTime[$context->getHash()];
 		}
 
 		wfProfileIn( __METHOD__ . '-filemtime' );
 		$filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) );
 		wfProfileOut( __METHOD__ . '-filemtime' );
+
 		$this->modifiedTime[$context->getHash()] = max(
 			$filesMtime,
-			$this->getMsgBlobMtime( $context->getLanguage() ) );
+			$this->getMsgBlobMtime( $context->getLanguage() ),
+			$this->getDefinitionMtime( $context )
+		);
 
 		wfProfileOut( __METHOD__ );
 		return $this->modifiedTime[$context->getHash()];
 	}
 
+	/**
+	 * Get the definition summary for this module.
+	 *
+	 * @param ResourceLoaderContext $context
+	 * @return array
+	 */
+	public function getDefinitionSummary( ResourceLoaderContext $context ) {
+		$summary = array(
+			'class' => get_class( $this ),
+		);
+		foreach ( array(
+			'scripts',
+			'debugScripts',
+			'loaderScripts',
+			'styles',
+			'languageScripts',
+			'skinScripts',
+			'skinStyles',
+			'dependencies',
+			'messages',
+			'targets',
+			'group',
+			'position',
+			'skipFunction',
+			'localBasePath',
+			'remoteBasePath',
+			'debugRaw',
+			'raw',
+		) as $member ) {
+			$summary[$member] = $this->{$member};
+		};
+		return $summary;
+	}
+
 	/* Protected Methods */
 
 	/**
-	 * @param string $path
+	 * @param string|ResourceLoaderFilePath $path
 	 * @return string
 	 */
 	protected function getLocalPath( $path ) {
+		if ( $path instanceof ResourceLoaderFilePath ) {
+			return $path->getLocalPath();
+		}
+
 		return "{$this->localBasePath}/$path";
 	}
 
 	/**
-	 * @param string $path
+	 * @param string|ResourceLoaderFilePath $path
 	 * @return string
 	 */
 	protected function getRemotePath( $path ) {
+		if ( $path instanceof ResourceLoaderFilePath ) {
+			return $path->getRemotePath();
+		}
+
 		return "{$this->remoteBasePath}/$path";
 	}
 
@@ -488,7 +632,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	 *
 	 * @since 1.22
 	 * @param string $path
-	 * @return string: the stylesheet language name
+	 * @return string The stylesheet language name
 	 */
 	public function getStyleSheetLang( $path ) {
 		return preg_match( '/\.less$/i', $path ) ? 'less' : 'css';
@@ -499,9 +643,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	 *
 	 * @param array $list List of file paths in any combination of index/path
 	 *     or path/options pairs
-	 * @param string $option option name
-	 * @param mixed $default default value if the option isn't set
-	 * @return array: List of file paths, collated by $option
+	 * @param string $option Option name
+	 * @param mixed $default Default value if the option isn't set
+	 * @return array List of file paths, collated by $option
 	 */
 	protected static function collateFilePathListByOption( array $list, $option, $default ) {
 		$collatedFiles = array();
@@ -525,31 +669,31 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	}
 
 	/**
-	 * Gets a list of element that match a key, optionally using a fallback key.
+	 * Get a list of element that match a key, optionally using a fallback key.
 	 *
 	 * @param array $list List of lists to select from
 	 * @param string $key Key to look for in $map
 	 * @param string $fallback Key to look for in $list if $key doesn't exist
-	 * @return array: List of elements from $map which matched $key or $fallback,
-	 *     or an empty list in case of no match
+	 * @return array List of elements from $map which matched $key or $fallback,
+	 *  or an empty list in case of no match
 	 */
 	protected static function tryForKey( array $list, $key, $fallback = null ) {
 		if ( isset( $list[$key] ) && is_array( $list[$key] ) ) {
 			return $list[$key];
 		} elseif ( is_string( $fallback )
 			&& isset( $list[$fallback] )
-			&& is_array( $list[$fallback] ) )
-		{
+			&& is_array( $list[$fallback] )
+		) {
 			return $list[$fallback];
 		}
 		return array();
 	}
 
 	/**
-	 * Gets a list of file paths for all scripts in this module, in order of propper execution.
+	 * Get a list of file paths for all scripts in this module, in order of proper execution.
 	 *
 	 * @param ResourceLoaderContext $context
-	 * @return array: List of file paths
+	 * @return array List of file paths
 	 */
 	protected function getScriptFiles( ResourceLoaderContext $context ) {
 		$files = array_merge(
@@ -561,39 +705,82 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 			$files = array_merge( $files, $this->debugScripts );
 		}
 
-		return array_unique( $files );
+		return array_unique( $files, SORT_REGULAR );
 	}
 
 	/**
-	 * Gets a list of file paths for all styles in this module, in order of propper inclusion.
+	 * Get a list of file paths for all styles in this module, in order of proper inclusion.
 	 *
 	 * @param ResourceLoaderContext $context
-	 * @return array: List of file paths
+	 * @return array List of file paths
 	 */
-	protected function getStyleFiles( ResourceLoaderContext $context ) {
+	public function getStyleFiles( ResourceLoaderContext $context ) {
 		return array_merge_recursive(
 			self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
 			self::collateFilePathListByOption(
-				self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 'media', 'all'
+				self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
+				'media',
+				'all'
 			)
 		);
 	}
 
 	/**
-	 * Returns all style files used by this module
+	 * Gets a list of file paths for all skin styles in the module used by
+	 * the skin.
+	 *
+	 * @param string $skinName The name of the skin
+	 * @return array A list of file paths collated by media type
+	 */
+	protected function getSkinStyleFiles( $skinName ) {
+		return self::collateFilePathListByOption(
+			self::tryForKey( $this->skinStyles, $skinName ),
+			'media',
+			'all'
+		);
+	}
+
+	/**
+	 * Gets a list of file paths for all skin style files in the module,
+	 * for all available skins.
+	 *
+	 * @return array A list of file paths collated by media type
+	 */
+	protected function getAllSkinStyleFiles() {
+		$styleFiles = array();
+		$internalSkinNames = array_keys( Skin::getSkinNames() );
+		$internalSkinNames[] = 'default';
+
+		foreach ( $internalSkinNames as $internalSkinName ) {
+			$styleFiles = array_merge_recursive(
+				$styleFiles,
+				$this->getSkinStyleFiles( $internalSkinName )
+			);
+		}
+
+		return $styleFiles;
+	}
+
+	/**
+	 * Returns all style files and all skin style files used by this module.
+	 *
 	 * @return array
 	 */
 	public function getAllStyleFiles() {
-		$files = array();
-		foreach( (array)$this->styles as $key => $value ) {
-			if ( is_array( $value ) ) {
-				$path = $key;
-			} else {
-				$path = $value;
+		$collatedStyleFiles = array_merge_recursive(
+			self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
+			$this->getAllSkinStyleFiles()
+		);
+
+		$result = array();
+
+		foreach ( $collatedStyleFiles as $media => $styleFiles ) {
+			foreach ( $styleFiles as $styleFile ) {
+				$result[] = $this->getLocalPath( $styleFile );
 			}
-			$files[] = $this->getLocalPath( $path );
 		}
-		return $files;
+
+		return $result;
 	}
 
 	/**
@@ -601,21 +788,20 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	 *
 	 * @param array $scripts List of file paths to scripts to read, remap and concetenate
 	 * @throws MWException
-	 * @return string: Concatenated and remapped JavaScript data from $scripts
+	 * @return string Concatenated and remapped JavaScript data from $scripts
 	 */
 	protected function readScriptFiles( array $scripts ) {
-		global $wgResourceLoaderValidateStaticJS;
 		if ( empty( $scripts ) ) {
 			return '';
 		}
 		$js = '';
-		foreach ( array_unique( $scripts ) as $fileName ) {
+		foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) {
 			$localPath = $this->getLocalPath( $fileName );
 			if ( !file_exists( $localPath ) ) {
 				throw new MWException( __METHOD__ . ": script file not found: \"$localPath\"" );
 			}
 			$contents = file_get_contents( $localPath );
-			if ( $wgResourceLoaderValidateStaticJS ) {
+			if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) {
 				// Static files don't really need to be checked as often; unlike
 				// on-wiki module they shouldn't change unexpectedly without
 				// admin interference.
@@ -631,26 +817,24 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	 *
 	 * @param array $styles List of media type/list of file paths pairs, to read, remap and
 	 * concetenate
-	 *
 	 * @param bool $flip
+	 * @param ResourceLoaderContext $context (optional)
 	 *
-	 * @return array: List of concatenated and remapped CSS data from $styles,
+	 * @throws MWException
+	 * @return array List of concatenated and remapped CSS data from $styles,
 	 *     keyed by media type
 	 */
-	protected function readStyleFiles( array $styles, $flip ) {
+	public function readStyleFiles( array $styles, $flip, $context = null ) {
 		if ( empty( $styles ) ) {
 			return array();
 		}
 		foreach ( $styles as $media => $files ) {
-			$uniqueFiles = array_unique( $files );
-			$styles[$media] = implode(
-				"\n",
-				array_map(
-					array( $this, 'readStyleFile' ),
-					$uniqueFiles,
-					array_fill( 0, count( $uniqueFiles ), $flip )
-				)
-			);
+			$uniqueFiles = array_unique( $files, SORT_REGULAR );
+			$styleFiles = array();
+			foreach ( $uniqueFiles as $file ) {
+				$styleFiles[] = $this->readStyleFile( $file, $flip, $context );
+			}
+			$styles[$media] = implode( "\n", $styleFiles );
 		}
 		return $styles;
 	}
@@ -662,20 +846,23 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	 *
 	 * @param string $path File path of style file to read
 	 * @param bool $flip
+	 * @param ResourceLoaderContext $context (optional)
 	 *
-	 * @return string: CSS data in script file
-	 * @throws MWException if the file doesn't exist
+	 * @return string CSS data in script file
+	 * @throws MWException If the file doesn't exist
 	 */
-	protected function readStyleFile( $path, $flip ) {
+	protected function readStyleFile( $path, $flip, $context = null ) {
 		$localPath = $this->getLocalPath( $path );
+		$remotePath = $this->getRemotePath( $path );
 		if ( !file_exists( $localPath ) ) {
 			$msg = __METHOD__ . ": style file not found: \"$localPath\"";
 			wfDebugLog( 'resourceloader', $msg );
 			throw new MWException( $msg );
 		}
 
-		if ( $this->getStyleSheetLang( $path ) === 'less' ) {
-			$style = $this->compileLESSFile( $localPath );
+		if ( $this->getStyleSheetLang( $localPath ) === 'less' ) {
+			$compiler = $this->getLessCompiler( $context );
+			$style = $this->compileLessFile( $localPath, $compiler );
 			$this->hasGeneratedStyles = true;
 		} else {
 			$style = file_get_contents( $localPath );
@@ -684,20 +871,15 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 		if ( $flip ) {
 			$style = CSSJanus::transform( $style, true, false );
 		}
-		$dirname = dirname( $path );
-		if ( $dirname == '.' ) {
-			// If $path doesn't have a directory component, don't prepend a dot
-			$dirname = '';
-		}
-		$dir = $this->getLocalPath( $dirname );
-		$remoteDir = $this->getRemotePath( $dirname );
+		$localDir = dirname( $localPath );
+		$remoteDir = dirname( $remotePath );
 		// Get and register local file references
 		$this->localFileRefs = array_merge(
 			$this->localFileRefs,
-			CSSMin::getLocalFileReferences( $style, $dir )
+			CSSMin::getLocalFileReferences( $style, $localDir )
 		);
 		return CSSMin::remap(
-			$style, $dir, $remoteDir, true
+			$style, $localDir, $remoteDir, true
 		);
 	}
 
@@ -713,64 +895,43 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 	/**
 	 * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
 	 *
-	 * @return array of strings
+	 * @return array Array of strings
 	 */
 	public function getTargets() {
 		return $this->targets;
 	}
 
 	/**
-	 * Generate a cache key for a LESS file.
+	 * Compile a LESS file into CSS.
 	 *
-	 * The cache key varies on the file name and the names and values of global
-	 * LESS variables.
+	 * Keeps track of all used files and adds them to localFileRefs.
 	 *
 	 * @since 1.22
-	 * @param string $fileName File name of root LESS file.
-	 * @return string: Cache key
+	 * @throws Exception If lessc encounters a parse error
+	 * @param string $fileName File path of LESS source
+	 * @param lessc $compiler Compiler to use, if not default
+	 * @return string CSS source
 	 */
-	protected static function getLESSCacheKey( $fileName ) {
-		$vars = json_encode( ResourceLoader::getLESSVars() );
-		$hash = md5( $fileName . $vars );
-		return wfMemcKey( 'resourceloader', 'less', $hash );
+	protected function compileLessFile( $fileName, $compiler = null ) {
+		if ( !$compiler ) {
+			$compiler = $this->getLessCompiler();
+		}
+		$result = $compiler->compileFile( $fileName );
+		$this->localFileRefs += array_keys( $compiler->allParsedFiles() );
+		return $result;
 	}
 
 	/**
-	 * Compile a LESS file into CSS.
+	 * Get a LESS compiler instance for this module in given context.
 	 *
-	 * If invalid, returns replacement CSS source consisting of the compilation
-	 * error message encoded as a comment. To save work, we cache a result object
-	 * which comprises the compiled CSS and the names & mtimes of the files
-	 * that were processed. lessphp compares the cached & current mtimes and
-	 * recompiles as necessary.
+	 * Just calls ResourceLoader::getLessCompiler() by default to get a global compiler.
 	 *
-	 * @since 1.22
-	 * @param string $fileName File path of LESS source
-	 * @return string: CSS source
+	 * @param ResourceLoaderContext $context
+	 * @throws MWException
+	 * @since 1.24
+	 * @return lessc
 	 */
-	protected function compileLESSFile( $fileName ) {
-		$key = self::getLESSCacheKey( $fileName );
-		$cache = wfGetCache( CACHE_ANYTHING );
-
-		// The input to lessc. Either an associative array representing the
-		// cached results of a previous compilation, or the string file name if
-		// no cache result exists.
-		$source = $cache->get( $key );
-		if ( !is_array( $source ) || !isset( $source['root'] ) ) {
-			$source = $fileName;
-		}
-
-		$compiler = ResourceLoader::getLessCompiler();
-		$result = null;
-
-		$result = $compiler->cachedCompile( $source );
-
-		if ( !is_array( $result ) ) {
-			throw new MWException( 'LESS compiler result has type ' . gettype( $result ) . '; array expected.' );
-		}
-
-		$this->localFileRefs += array_keys( $result['files'] );
-		$cache->set( $key, $result );
-		return $result['compiled'];
+	protected function getLessCompiler( ResourceLoaderContext $context = null ) {
+		return ResourceLoader::getLessCompiler( $this->getConfig() );
 	}
 }
diff --git a/includes/resourceloader/ResourceLoaderFilePageModule.php b/includes/resourceloader/ResourceLoaderFilePageModule.php
index 61ed5206..8c7fbe76 100644
--- a/includes/resourceloader/ResourceLoaderFilePageModule.php
+++ b/includes/resourceloader/ResourceLoaderFilePageModule.php
@@ -26,7 +26,7 @@
 class ResourceLoaderFilePageModule extends ResourceLoaderWikiModule {
 
 	/**
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @return array
 	 */
 	protected function getPages( ResourceLoaderContext $context ) {
diff --git a/includes/resourceloader/ResourceLoaderFilePath.php b/includes/resourceloader/ResourceLoaderFilePath.php
new file mode 100644
index 00000000..dd239d09
--- /dev/null
+++ b/includes/resourceloader/ResourceLoaderFilePath.php
@@ -0,0 +1,74 @@
+path = $path;
+		$this->localBasePath = $localBasePath;
+		$this->remoteBasePath = $remoteBasePath;
+	}
+
+	/**
+	 * @return string
+	 */
+	public function getLocalPath() {
+		return "{$this->localBasePath}/{$this->path}";
+	}
+
+	/**
+	 * @return string
+	 */
+	public function getRemotePath() {
+		return "{$this->remoteBasePath}/{$this->path}";
+	}
+
+	/**
+	 * @return string
+	 */
+	public function getPath() {
+		return $this->path;
+	}
+}
diff --git a/includes/resourceloader/ResourceLoaderLESSFunctions.php b/includes/resourceloader/ResourceLoaderLESSFunctions.php
deleted file mode 100644
index c7570f64..00000000
--- a/includes/resourceloader/ResourceLoaderLESSFunctions.php
+++ /dev/null
@@ -1,67 +0,0 @@
-parser->sourceName, PATHINFO_DIRNAME );
-		$url = $frame[2][0];
-		$file = realpath( $base . '/' . $url );
-		return $less->toBool( $file
-			&& strpos( $url, '//' ) === false
-			&& filesize( $file ) < CSSMin::EMBED_SIZE_LIMIT
-			&& CSSMin::getMimeType( $file ) !== false );
-	}
-
-	/**
-	 * Convert an image URI to a base64-encoded data URI.
-	 *
-	 * @par Example:
-	 * @code
-	 *   .fancy-button {
-	 *       background-image: embed('../images/button-bg.png');
-	 *   }
-	 * @endcode
-	 */
-	public static function embed( $frame, $less ) {
-		$base = pathinfo( $less->parser->sourceName, PATHINFO_DIRNAME );
-		$url = $frame[2][0];
-		$file = realpath( $base . '/' . $url );
-
-		$data = CSSMin::encodeImageAsDataURI( $file );
-		$less->addParsedFile( $file );
-		return 'url(' . $data . ')';
-	}
-}
diff --git a/includes/resourceloader/ResourceLoaderLanguageDataModule.php b/includes/resourceloader/ResourceLoaderLanguageDataModule.php
index fa0fbf85..09d90d6e 100644
--- a/includes/resourceloader/ResourceLoaderLanguageDataModule.php
+++ b/includes/resourceloader/ResourceLoaderLanguageDataModule.php
@@ -27,99 +27,51 @@
  */
 class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
 
-	protected $language;
 	protected $targets = array( 'desktop', 'mobile' );
-	/**
-	 * Get the grammar forms for the site content language.
-	 *
-	 * @return array
-	 */
-	protected function getSiteLangGrammarForms() {
-		return $this->language->getGrammarForms();
-	}
-
-	/**
-	 * Get the plural forms for the site content language.
-	 *
-	 * @return array
-	 */
-	protected function getPluralRules() {
-		return $this->language->getPluralRules();
-	}
-
-	/**
-	 * Get the digit groupin Pattern for the site content language.
-	 *
-	 * @return array
-	 */
-	protected function getDigitGroupingPattern() {
-		return $this->language->digitGroupingPattern();
-	}
-
-	/**
-	 * Get the digit transform table for the content language
-	 *
-	 * @return array
-	 */
-	protected function getDigitTransformTable() {
-		return $this->language->digitTransformTable();
-	}
-
-	/**
-	 * Get seperator transform table required for converting
-	 * the . and , sign to appropriate forms in site content language.
-	 *
-	 * @return array
-	 */
-	protected function getSeparatorTransformTable() {
-		return $this->language->separatorTransformTable();
-	}
 
 	/**
 	 * Get all the dynamic data for the content language to an array.
 	 *
-	 * NOTE: Before calling this you HAVE to make sure $this->language is set.
-	 *
+	 * @param ResourceLoaderContext $context
 	 * @return array
 	 */
-	protected function getData() {
+	protected function getData( ResourceLoaderContext $context ) {
+		$language = Language::factory( $context->getLanguage() );
 		return array(
-			'digitTransformTable' => $this->getDigitTransformTable(),
-			'separatorTransformTable' => $this->getSeparatorTransformTable(),
-			'grammarForms' => $this->getSiteLangGrammarForms(),
-			'pluralRules' => $this->getPluralRules(),
-			'digitGroupingPattern' => $this->getDigitGroupingPattern(),
+			'digitTransformTable' => $language->digitTransformTable(),
+			'separatorTransformTable' => $language->separatorTransformTable(),
+			'grammarForms' => $language->getGrammarForms(),
+			'pluralRules' => $language->getPluralRules(),
+			'digitGroupingPattern' => $language->digitGroupingPattern(),
+			'fallbackLanguages' => $language->getFallbackLanguages(),
 		);
 	}
 
 	/**
-	 * @param $context ResourceLoaderContext
-	 * @return string: JavaScript code
+	 * @param ResourceLoaderContext $context
+	 * @return string JavaScript code
 	 */
 	public function getScript( ResourceLoaderContext $context ) {
-		$this->language = Language::factory( $context->getLanguage() );
 		return Xml::encodeJsCall( 'mw.language.setData', array(
-			$this->language->getCode(),
-			$this->getData()
+			$context->getLanguage(),
+			$this->getData( $context )
 		) );
 	}
 
 	/**
-	 * @param $context ResourceLoaderContext
-	 * @return int: UNIX timestamp
+	 * @param ResourceLoaderContext $context
+	 * @return int UNIX timestamp
 	 */
 	public function getModifiedTime( ResourceLoaderContext $context ) {
 		return max( 1, $this->getHashMtime( $context ) );
 	}
 
 	/**
-	 * @param $context ResourceLoaderContext
-	 * @return string: Hash
+	 * @param ResourceLoaderContext $context
+	 * @return string Hash
 	 */
 	public function getModifiedHash( ResourceLoaderContext $context ) {
-		$this->language = Language::factory( $context->getLanguage() );
-
-		return md5( serialize( $this->getData() ) );
+		return md5( serialize( $this->getData( $context ) ) );
 	}
 
 	/**
diff --git a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php
new file mode 100644
index 00000000..fe0c8454
--- /dev/null
+++ b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php
@@ -0,0 +1,79 @@
+getLanguage(),
+			'all'
+		);
+	}
+
+	/**
+	 * @param ResourceLoaderContext $context
+	 * @return string JavaScript code
+	 */
+	public function getScript( ResourceLoaderContext $context ) {
+		return Xml::encodeJsCall( 'mw.language.setData', array(
+			$context->getLanguage(),
+			'languageNames',
+			$this->getData( $context )
+		) );
+	}
+
+	public function getDependencies() {
+		return array( 'mediawiki.language.init' );
+	}
+
+	/**
+	 * @param ResourceLoaderContext $context
+	 * @return int UNIX timestamp
+	 */
+	public function getModifiedTime( ResourceLoaderContext $context ) {
+		return max( 1, $this->getHashMtime( $context ) );
+	}
+
+	/**
+	 * @param ResourceLoaderContext $context
+	 * @return string Hash
+	 */
+	public function getModifiedHash( ResourceLoaderContext $context ) {
+		return md5( serialize( $this->getData( $context ) ) );
+	}
+
+}
diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php
index 11264fc8..45eb70f8 100644
--- a/includes/resourceloader/ResourceLoaderModule.php
+++ b/includes/resourceloader/ResourceLoaderModule.php
@@ -26,7 +26,6 @@
  * Abstraction for resource loader modules, with name registration and maxage functionality.
  */
 abstract class ResourceLoaderModule {
-
 	# Type of resource
 	const TYPE_SCRIPTS = 'scripts';
 	const TYPE_STYLES = 'styles';
@@ -65,13 +64,18 @@ abstract class ResourceLoaderModule {
 	// In-object cache for message blob mtime
 	protected $msgBlobMtime = array();
 
+	/**
+	 * @var Config
+	 */
+	protected $config;
+
 	/* Methods */
 
 	/**
 	 * Get this module's name. This is set when the module is registered
 	 * with ResourceLoader::register()
 	 *
-	 * @return mixed: Name (string) or null if no name was set
+	 * @return string|null Name (string) or null if no name was set
 	 */
 	public function getName() {
 		return $this->name;
@@ -91,7 +95,7 @@ abstract class ResourceLoaderModule {
 	 * Get this module's origin. This is set when the module is registered
 	 * with ResourceLoader::register()
 	 *
-	 * @return int: ResourceLoaderModule class constant, the subclass default
+	 * @return int ResourceLoaderModule class constant, the subclass default
 	 *     if not set manually
 	 */
 	public function getOrigin() {
@@ -102,7 +106,7 @@ abstract class ResourceLoaderModule {
 	 * Set this module's origin. This is called by ResourceLoader::register()
 	 * when registering the module. Other code should not call this.
 	 *
-	 * @param int $origin origin
+	 * @param int $origin Origin
 	 */
 	public function setOrigin( $origin ) {
 		$this->origin = $origin;
@@ -123,13 +127,34 @@ abstract class ResourceLoaderModule {
 	 * Includes all relevant JS except loader scripts.
 	 *
 	 * @param ResourceLoaderContext $context
-	 * @return string: JavaScript code
+	 * @return string JavaScript code
 	 */
 	public function getScript( ResourceLoaderContext $context ) {
 		// Stub, override expected
 		return '';
 	}
 
+	/**
+	 * @return Config
+	 * @since 1.24
+	 */
+	public function getConfig() {
+		if ( $this->config === null ) {
+			// Ugh, fall back to default
+			$this->config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
+		}
+
+		return $this->config;
+	}
+
+	/**
+	 * @param Config $config
+	 * @since 1.24
+	 */
+	public function setConfig( Config $config ) {
+		$this->config = $config;
+	}
+
 	/**
 	 * Get the URL or URLs to load for this module's JS in debug mode.
 	 * The default behavior is to return a load.php?only=scripts URL for
@@ -142,20 +167,20 @@ abstract class ResourceLoaderModule {
 	 * MUST return either an only= URL or a non-load.php URL.
 	 *
 	 * @param ResourceLoaderContext $context
-	 * @return array: Array of URLs
+	 * @return array Array of URLs
 	 */
 	public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
-		$url = ResourceLoader::makeLoaderURL(
-			array( $this->getName() ),
-			$context->getLanguage(),
-			$context->getSkin(),
-			$context->getUser(),
-			$context->getVersion(),
-			true, // debug
-			'scripts', // only
-			$context->getRequest()->getBool( 'printable' ),
-			$context->getRequest()->getBool( 'handheld' )
+		$resourceLoader = $context->getResourceLoader();
+		$derivative = new DerivativeResourceLoaderContext( $context );
+		$derivative->setModules( array( $this->getName() ) );
+		$derivative->setOnly( 'scripts' );
+		$derivative->setDebug( true );
+
+		$url = $resourceLoader->createLoaderURL(
+			$this->getSource(),
+			$derivative
 		);
+
 		return array( $url );
 	}
 
@@ -173,7 +198,7 @@ abstract class ResourceLoaderModule {
 	 * Get all CSS for this module for a given skin.
 	 *
 	 * @param ResourceLoaderContext $context
-	 * @return array: List of CSS strings or array of CSS strings keyed by media type.
+	 * @return array List of CSS strings or array of CSS strings keyed by media type.
 	 *  like array( 'screen' => '.foo { width: 0 }' );
 	 *  or array( 'screen' => array( '.foo { width: 0 }' ) );
 	 */
@@ -189,20 +214,20 @@ abstract class ResourceLoaderModule {
 	 * load the files directly. See also getScriptURLsForDebug()
 	 *
 	 * @param ResourceLoaderContext $context
-	 * @return array: array( mediaType => array( URL1, URL2, ... ), ... )
+	 * @return array Array( mediaType => array( URL1, URL2, ... ), ... )
 	 */
 	public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
-		$url = ResourceLoader::makeLoaderURL(
-			array( $this->getName() ),
-			$context->getLanguage(),
-			$context->getSkin(),
-			$context->getUser(),
-			$context->getVersion(),
-			true, // debug
-			'styles', // only
-			$context->getRequest()->getBool( 'printable' ),
-			$context->getRequest()->getBool( 'handheld' )
+		$resourceLoader = $context->getResourceLoader();
+		$derivative = new DerivativeResourceLoaderContext( $context );
+		$derivative->setModules( array( $this->getName() ) );
+		$derivative->setOnly( 'styles' );
+		$derivative->setDebug( true );
+
+		$url = $resourceLoader->createLoaderURL(
+			$this->getSource(),
+			$derivative
 		);
+
 		return array( 'all' => array( $url ) );
 	}
 
@@ -211,7 +236,7 @@ abstract class ResourceLoaderModule {
 	 *
 	 * To get a JSON blob with messages, use MessageBlobStore::get()
 	 *
-	 * @return array: List of message keys. Keys may occur more than once
+	 * @return array List of message keys. Keys may occur more than once
 	 */
 	public function getMessages() {
 		// Stub, override expected
@@ -221,7 +246,7 @@ abstract class ResourceLoaderModule {
 	/**
 	 * Get the group this module is in.
 	 *
-	 * @return string: Group name
+	 * @return string Group name
 	 */
 	public function getGroup() {
 		// Stub, override expected
@@ -231,7 +256,7 @@ abstract class ResourceLoaderModule {
 	/**
 	 * Get the origin of this module. Should only be overridden for foreign modules.
 	 *
-	 * @return string: Origin name, 'local' for local modules
+	 * @return string Origin name, 'local' for local modules
 	 */
 	public function getSource() {
 		// Stub, override expected
@@ -263,7 +288,7 @@ abstract class ResourceLoaderModule {
 	/**
 	 * Get the loader JS for this module, if set.
 	 *
-	 * @return mixed: JavaScript loader code as a string or boolean false if no custom loader set
+	 * @return mixed JavaScript loader code as a string or boolean false if no custom loader set
 	 */
 	public function getLoaderScript() {
 		// Stub, override expected
@@ -278,7 +303,7 @@ abstract class ResourceLoaderModule {
 	 *
 	 * To add dependencies dynamically on the client side, use a custom
 	 * loader script, see getLoaderScript()
-	 * @return array: List of module names as strings
+	 * @return array List of module names as strings
 	 */
 	public function getDependencies() {
 		// Stub, override expected
@@ -288,18 +313,36 @@ abstract class ResourceLoaderModule {
 	/**
 	 * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
 	 *
-	 * @return array: Array of strings
+	 * @return array Array of strings
 	 */
 	public function getTargets() {
 		return $this->targets;
 	}
 
+	/**
+	 * Get the skip function.
+	 *
+	 * Modules that provide fallback functionality can provide a "skip function". This
+	 * function, if provided, will be passed along to the module registry on the client.
+	 * When this module is loaded (either directly or as a dependency of another module),
+	 * then this function is executed first. If the function returns true, the module will
+	 * instantly be considered "ready" without requesting the associated module resources.
+	 *
+	 * The value returned here must be valid javascript for execution in a private function.
+	 * It must not contain the "function () {" and "}" wrapper though.
+	 *
+	 * @return string|null A JavaScript function body returning a boolean value, or null
+	 */
+	public function getSkipFunction() {
+		return null;
+	}
+
 	/**
 	 * Get the files this module depends on indirectly for a given skin.
 	 * Currently these are only image files referenced by the module's CSS.
 	 *
 	 * @param string $skin Skin name
-	 * @return array: List of files
+	 * @return array List of files
 	 */
 	public function getFileDependencies( $skin ) {
 		// Try in-object cache first
@@ -335,7 +378,7 @@ abstract class ResourceLoaderModule {
 	 * Get the last modification timestamp of the message blob for this
 	 * module in a given language.
 	 * @param string $lang Language code
-	 * @return int: UNIX timestamp, or 0 if the module doesn't have messages
+	 * @return int UNIX timestamp, or 0 if the module doesn't have messages
 	 */
 	public function getMsgBlobMtime( $lang ) {
 		if ( !isset( $this->msgBlobMtime[$lang] ) ) {
@@ -363,7 +406,7 @@ abstract class ResourceLoaderModule {
 	 * Set a preloaded message blob last modification timestamp. Used so we
 	 * can load this information for all modules at once.
 	 * @param string $lang Language code
-	 * @param $mtime Integer: UNIX timestamp or 0 if there is no such blob
+	 * @param int $mtime UNIX timestamp or 0 if there is no such blob
 	 */
 	public function setMsgBlobMtime( $lang, $mtime ) {
 		$this->msgBlobMtime[$lang] = $mtime;
@@ -387,7 +430,7 @@ abstract class ResourceLoaderModule {
 	 * yourself and take its result into consideration.
 	 *
 	 * @param ResourceLoaderContext $context Context object
-	 * @return integer UNIX timestamp
+	 * @return int UNIX timestamp
 	 */
 	public function getModifiedTime( ResourceLoaderContext $context ) {
 		// 0 would mean now
@@ -398,7 +441,8 @@ abstract class ResourceLoaderModule {
 	 * Helper method for calculating when the module's hash (if it has one) changed.
 	 *
 	 * @param ResourceLoaderContext $context
-	 * @return integer: UNIX timestamp or 0 if there is no hash provided
+	 * @return int UNIX timestamp or 0 if no hash was provided
+	 *  by getModifiedHash()
 	 */
 	public function getHashMtime( ResourceLoaderContext $context ) {
 		$hash = $this->getModifiedHash( $context );
@@ -407,7 +451,7 @@ abstract class ResourceLoaderModule {
 		}
 
 		$cache = wfGetCache( CACHE_ANYTHING );
-		$key = wfMemcKey( 'resourceloader', 'modulemodifiedhash', $this->getName() );
+		$key = wfMemcKey( 'resourceloader', 'modulemodifiedhash', $this->getName(), $hash );
 
 		$data = $cache->get( $key );
 		if ( is_array( $data ) && $data['hash'] === $hash ) {
@@ -425,16 +469,100 @@ abstract class ResourceLoaderModule {
 	}
 
 	/**
-	 * Get the last modification timestamp of the message blob for this
-	 * module in a given language.
+	 * Get the hash for whatever this module may contain.
+	 *
+	 * This is the method subclasses should implement if they want to make
+	 * use of getHashMTime() inside getModifiedTime().
 	 *
 	 * @param ResourceLoaderContext $context
-	 * @return string|null: Hash
+	 * @return string|null Hash
 	 */
 	public function getModifiedHash( ResourceLoaderContext $context ) {
 		return null;
 	}
 
+	/**
+	 * Helper method for calculating when this module's definition summary was last changed.
+	 *
+	 * @since 1.23
+	 *
+	 * @param ResourceLoaderContext $context
+	 * @return int UNIX timestamp or 0 if no definition summary was provided
+	 *  by getDefinitionSummary()
+	 */
+	public function getDefinitionMtime( ResourceLoaderContext $context ) {
+		wfProfileIn( __METHOD__ );
+		$summary = $this->getDefinitionSummary( $context );
+		if ( $summary === null ) {
+			wfProfileOut( __METHOD__ );
+			return 0;
+		}
+
+		$hash = md5( json_encode( $summary ) );
+
+		$cache = wfGetCache( CACHE_ANYTHING );
+
+		// Embed the hash itself in the cache key. This allows for a few nifty things:
+		// - During deployment, servers with old and new versions of the code communicating
+		//   with the same memcached will not override the same key repeatedly increasing
+		//   the timestamp.
+		// - In case of the definition changing and then changing back in a short period of time
+		//   (e.g. in case of a revert or a corrupt server) the old timestamp and client-side cache
+		//   url will be re-used.
+		// - If different context-combinations (e.g. same skin, same language or some combination
+		//   thereof) result in the same definition, they will use the same hash and timestamp.
+		$key = wfMemcKey( 'resourceloader', 'moduledefinition', $this->getName(), $hash );
+
+		$data = $cache->get( $key );
+		if ( is_int( $data ) && $data > 0 ) {
+			// We've seen this hash before, re-use the timestamp of when we first saw it.
+			wfProfileOut( __METHOD__ );
+			return $data;
+		}
+
+		wfDebugLog( 'resourceloader', __METHOD__ . ": New definition hash for module "
+			. "{$this->getName()} in context {$context->getHash()}: $hash." );
+
+		$timestamp = time();
+		$cache->set( $key, $timestamp );
+
+		wfProfileOut( __METHOD__ );
+		return $timestamp;
+	}
+
+	/**
+	 * Get the definition summary for this module.
+	 *
+	 * This is the method subclasses should implement if they want to make
+	 * use of getDefinitionMTime() inside getModifiedTime().
+	 *
+	 * Return an array containing values from all significant properties of this
+	 * module's definition. Be sure to include things that are explicitly ordered,
+	 * in their actaul order (bug 37812).
+	 *
+	 * Avoid including things that are insiginificant (e.g. order of message
+	 * keys is insignificant and should be sorted to avoid unnecessary cache
+	 * invalidation).
+	 *
+	 * Avoid including things already considered by other methods inside your
+	 * getModifiedTime(), such as file mtime timestamps.
+	 *
+	 * Serialisation is done using json_encode, which means object state is not
+	 * taken into account when building the hash. This data structure must only
+	 * contain arrays and scalars as values (avoid object instances) which means
+	 * it requires abstraction.
+	 *
+	 * @since 1.23
+	 *
+	 * @param ResourceLoaderContext $context
+	 * @return array|null
+	 */
+	public function getDefinitionSummary( ResourceLoaderContext $context ) {
+		return array(
+			'class' => get_class( $this ),
+		);
+	}
+
 	/**
 	 * Check whether this module is known to be empty. If a child class
 	 * has an easy and cheap way to determine that this module is
@@ -448,7 +576,7 @@ abstract class ResourceLoaderModule {
 		return false;
 	}
 
-	/** @var JSParser lazy-initialized; use self::javaScriptParser() */
+	/** @var JSParser Lazy-initialized; use self::javaScriptParser() */
 	private static $jsParser;
 	private static $parseCacheVersion = 1;
 
@@ -458,11 +586,10 @@ abstract class ResourceLoaderModule {
 	 *
 	 * @param string $fileName
 	 * @param string $contents
-	 * @return string: JS with the original, or a replacement error
+	 * @return string JS with the original, or a replacement error
 	 */
 	protected function validateScriptFile( $fileName, $contents ) {
-		global $wgResourceLoaderValidateJS;
-		if ( $wgResourceLoaderValidateJS ) {
+		if ( $this->getConfig()->get( 'ResourceLoaderValidateJS' ) ) {
 			// Try for cache hit
 			// Use CACHE_ANYTHING since filtering is very slow compared to DB queries
 			$key = wfMemcKey( 'resourceloader', 'jsparse', self::$parseCacheVersion, md5( $contents ) );
diff --git a/includes/resourceloader/ResourceLoaderNoscriptModule.php b/includes/resourceloader/ResourceLoaderNoscriptModule.php
index bd026f3f..61927d77 100644
--- a/includes/resourceloader/ResourceLoaderNoscriptModule.php
+++ b/includes/resourceloader/ResourceLoaderNoscriptModule.php
@@ -33,9 +33,9 @@ class ResourceLoaderNoscriptModule extends ResourceLoaderWikiModule {
 	 * Gets list of pages used by this module.  Obviously, it makes absolutely no
 	 * sense to include JavaScript files here... :D
 	 *
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 *
-	 * @return Array: List of pages
+	 * @return array List of pages
 	 */
 	protected function getPages( ResourceLoaderContext $context ) {
 		return array( 'MediaWiki:Noscript.css' => array( 'type' => 'style' ) );
@@ -46,7 +46,7 @@ class ResourceLoaderNoscriptModule extends ResourceLoaderWikiModule {
 	/**
 	 * Gets group name
 	 *
-	 * @return String: Name of group
+	 * @return string Name of group
 	 */
 	public function getGroup() {
 		return 'noscript';
diff --git a/includes/resourceloader/ResourceLoaderSiteModule.php b/includes/resourceloader/ResourceLoaderSiteModule.php
index 05754d37..1d9721aa 100644
--- a/includes/resourceloader/ResourceLoaderSiteModule.php
+++ b/includes/resourceloader/ResourceLoaderSiteModule.php
@@ -32,19 +32,17 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
 	/**
 	 * Gets list of pages used by this module
 	 *
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 *
-	 * @return Array: List of pages
+	 * @return array List of pages
 	 */
 	protected function getPages( ResourceLoaderContext $context ) {
-		global $wgUseSiteJs, $wgUseSiteCss;
-
 		$pages = array();
-		if ( $wgUseSiteJs ) {
+		if ( $this->getConfig()->get( 'UseSiteJs' ) ) {
 			$pages['MediaWiki:Common.js'] = array( 'type' => 'script' );
 			$pages['MediaWiki:' . ucfirst( $context->getSkin() ) . '.js'] = array( 'type' => 'script' );
 		}
-		if ( $wgUseSiteCss ) {
+		if ( $this->getConfig()->get( 'UseSiteCss' ) ) {
 			$pages['MediaWiki:Common.css'] = array( 'type' => 'style' );
 			$pages['MediaWiki:' . ucfirst( $context->getSkin() ) . '.css'] = array( 'type' => 'style' );
 
@@ -58,7 +56,7 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
 	/**
 	 * Gets group name
 	 *
-	 * @return String: Name of group
+	 * @return string Name of group
 	 */
 	public function getGroup() {
 		return 'site';
diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php
index 20f6e0ba..78fe8e01 100644
--- a/includes/resourceloader/ResourceLoaderStartUpModule.php
+++ b/includes/resourceloader/ResourceLoaderStartUpModule.php
@@ -27,21 +27,23 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
 	/* Protected Members */
 
 	protected $modifiedTime = array();
+	protected $configVars = array();
 	protected $targets = array( 'desktop', 'mobile' );
 
 	/* Protected Methods */
 
 	/**
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @return array
 	 */
-	protected function getConfig( $context ) {
-		global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension,
-			$wgArticlePath, $wgScriptPath, $wgServer, $wgContLang,
-			$wgVariantArticlePath, $wgActionPaths, $wgVersion,
-			$wgEnableAPI, $wgEnableWriteAPI, $wgDBname,
-			$wgSitename, $wgFileExtensions, $wgExtensionAssetsPath,
-			$wgCookiePrefix, $wgResourceLoaderMaxQueryLength;
+	protected function getConfigSettings( $context ) {
+
+		$hash = $context->getHash();
+		if ( isset( $this->configVars[$hash] ) ) {
+			return $this->configVars[$hash];
+		}
+
+		global $wgContLang;
 
 		$mainPage = Title::newMainPage();
 
@@ -59,113 +61,262 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
 			}
 		}
 
+		$conf = $this->getConfig();
 		// Build list of variables
 		$vars = array(
-			'wgLoadScript' => $wgLoadScript,
+			'wgLoadScript' => wfScript( 'load' ),
 			'debug' => $context->getDebug(),
 			'skin' => $context->getSkin(),
-			'stylepath' => $wgStylePath,
+			'stylepath' => $conf->get( 'StylePath' ),
 			'wgUrlProtocols' => wfUrlProtocols(),
-			'wgArticlePath' => $wgArticlePath,
-			'wgScriptPath' => $wgScriptPath,
-			'wgScriptExtension' => $wgScriptExtension,
-			'wgScript' => $wgScript,
-			'wgVariantArticlePath' => $wgVariantArticlePath,
+			'wgArticlePath' => $conf->get( 'ArticlePath' ),
+			'wgScriptPath' => $conf->get( 'ScriptPath' ),
+			'wgScriptExtension' => $conf->get( 'ScriptExtension' ),
+			'wgScript' => wfScript(),
+			'wgSearchType' => $conf->get( 'SearchType' ),
+			'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ),
 			// Force object to avoid "empty" associative array from
 			// becoming [] instead of {} in JS (bug 34604)
-			'wgActionPaths' => (object)$wgActionPaths,
-			'wgServer' => $wgServer,
+			'wgActionPaths' => (object)$conf->get( 'ActionPaths' ),
+			'wgServer' => $conf->get( 'Server' ),
+			'wgServerName' => $conf->get( 'ServerName' ),
 			'wgUserLanguage' => $context->getLanguage(),
 			'wgContentLanguage' => $wgContLang->getCode(),
-			'wgVersion' => $wgVersion,
-			'wgEnableAPI' => $wgEnableAPI,
-			'wgEnableWriteAPI' => $wgEnableWriteAPI,
+			'wgVersion' => $conf->get( 'Version' ),
+			'wgEnableAPI' => $conf->get( 'EnableAPI' ),
+			'wgEnableWriteAPI' => $conf->get( 'EnableWriteAPI' ),
 			'wgMainPageTitle' => $mainPage->getPrefixedText(),
 			'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
 			'wgNamespaceIds' => $namespaceIds,
-			'wgSiteName' => $wgSitename,
-			'wgFileExtensions' => array_values( array_unique( $wgFileExtensions ) ),
-			'wgDBname' => $wgDBname,
+			'wgContentNamespaces' => MWNamespace::getContentNamespaces(),
+			'wgSiteName' => $conf->get( 'Sitename' ),
+			'wgFileExtensions' => array_values( array_unique( $conf->get( 'FileExtensions' ) ) ),
+			'wgDBname' => $conf->get( 'DBname' ),
 			// This sucks, it is only needed on Special:Upload, but I could
 			// not find a way to add vars only for a certain module
-			'wgFileCanRotate' => BitmapHandler::canRotate(),
+			'wgFileCanRotate' => SpecialUpload::rotationEnabled(),
 			'wgAvailableSkins' => Skin::getSkinNames(),
-			'wgExtensionAssetsPath' => $wgExtensionAssetsPath,
+			'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
 			// MediaWiki sets cookies to have this prefix by default
-			'wgCookiePrefix' => $wgCookiePrefix,
-			'wgResourceLoaderMaxQueryLength' => $wgResourceLoaderMaxQueryLength,
+			'wgCookiePrefix' => $conf->get( 'CookiePrefix' ),
+			'wgCookieDomain' => $conf->get( 'CookieDomain' ),
+			'wgCookiePath' => $conf->get( 'CookiePath' ),
+			'wgCookieExpiration' => $conf->get( 'CookieExpiration' ),
+			'wgResourceLoaderMaxQueryLength' => $conf->get( 'ResourceLoaderMaxQueryLength' ),
 			'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
 			'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
+			'wgResourceLoaderStorageVersion' => $conf->get( 'ResourceLoaderStorageVersion' ),
+			'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ),
 		);
 
 		wfRunHooks( 'ResourceLoaderGetConfigVars', array( &$vars ) );
 
-		return $vars;
+		$this->configVars[$hash] = $vars;
+		return $this->configVars[$hash];
+	}
+
+	/**
+	 * Recursively get all explicit and implicit dependencies for to the given module.
+	 *
+	 * @param array $registryData
+	 * @param string $moduleName
+	 * @return array
+	 */
+	protected static function getImplicitDependencies( array $registryData, $moduleName ) {
+		static $dependencyCache = array();
+
+		// The list of implicit dependencies won't be altered, so we can
+		// cache them without having to worry.
+		if ( !isset( $dependencyCache[$moduleName] ) ) {
+
+			if ( !isset( $registryData[$moduleName] ) ) {
+				// Dependencies may not exist
+				$dependencyCache[$moduleName] = array();
+			} else {
+				$data = $registryData[$moduleName];
+				$dependencyCache[$moduleName] = $data['dependencies'];
+
+				foreach ( $data['dependencies'] as $dependency ) {
+					// Recursively get the dependencies of the dependencies
+					$dependencyCache[$moduleName] = array_merge(
+						$dependencyCache[$moduleName],
+						self::getImplicitDependencies( $registryData, $dependency )
+					);
+				}
+			}
+		}
+
+		return $dependencyCache[$moduleName];
+	}
+
+	/**
+	 * Optimize the dependency tree in $this->modules and return it.
+	 *
+	 * The optimization basically works like this:
+	 *	Given we have module A with the dependencies B and C
+	 *		and module B with the dependency C.
+	 *	Now we don't have to tell the client to explicitly fetch module
+	 *		C as that's already included in module B.
+	 *
+	 * This way we can reasonably reduce the amout of module registration
+	 * data send to the client.
+	 *
+	 * @param array &$registryData Modules keyed by name with properties:
+	 *  - string 'version'
+	 *  - array 'dependencies'
+	 *  - string|null 'group'
+	 *  - string 'source'
+	 *  - string|false 'loader'
+	 */
+	public static function compileUnresolvedDependencies( array &$registryData ) {
+		foreach ( $registryData as $name => &$data ) {
+			if ( $data['loader'] !== false ) {
+				continue;
+			}
+			$dependencies = $data['dependencies'];
+			foreach ( $data['dependencies'] as $dependency ) {
+				$implicitDependencies = self::getImplicitDependencies( $registryData, $dependency );
+				$dependencies = array_diff( $dependencies, $implicitDependencies );
+			}
+			// Rebuild keys
+			$data['dependencies'] = array_values( $dependencies );
+		}
 	}
 
+
 	/**
-	 * Gets registration code for all modules
+	 * Get registration code for all modules.
 	 *
-	 * @param $context ResourceLoaderContext object
-	 * @return String: JavaScript code for registering all modules with the client loader
+	 * @param ResourceLoaderContext $context
+	 * @return string JavaScript code for registering all modules with the client loader
 	 */
-	public static function getModuleRegistrations( ResourceLoaderContext $context ) {
-		global $wgCacheEpoch;
+	public function getModuleRegistrations( ResourceLoaderContext $context ) {
 		wfProfileIn( __METHOD__ );
 
-		$out = '';
-		$registrations = array();
 		$resourceLoader = $context->getResourceLoader();
 		$target = $context->getRequest()->getVal( 'target', 'desktop' );
 
-		// Register sources
-		$out .= ResourceLoader::makeLoaderSourcesScript( $resourceLoader->getSources() );
+		$out = '';
+		$registryData = array();
 
-		// Register modules
+		// Get registry data
 		foreach ( $resourceLoader->getModuleNames() as $name ) {
 			$module = $resourceLoader->getModule( $name );
 			$moduleTargets = $module->getTargets();
 			if ( !in_array( $target, $moduleTargets ) ) {
 				continue;
 			}
-			$deps = $module->getDependencies();
-			$group = $module->getGroup();
-			$source = $module->getSource();
-			// Support module loader scripts
-			$loader = $module->getLoaderScript();
-			if ( $loader !== false ) {
-				$version = wfTimestamp( TS_ISO_8601_BASIC,
-					$module->getModifiedTime( $context ) );
-				$out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $source, $loader );
+
+			if ( $module->isRaw() ) {
+				// Don't register "raw" modules (like 'jquery' and 'mediawiki') client-side because
+				// depending on them is illegal anyway and would only lead to them being reloaded
+				// causing any state to be lost (like jQuery plugins, mw.config etc.)
 				continue;
 			}
 
-			// Automatically register module
 			// getModifiedTime() is supposed to return a UNIX timestamp, but it doesn't always
 			// seem to do that, and custom implementations might forget. Coerce it to TS_UNIX
 			$moduleMtime = wfTimestamp( TS_UNIX, $module->getModifiedTime( $context ) );
-			$mtime = max( $moduleMtime, wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
-			// Modules without dependencies, a group or a foreign source pass two arguments (name, timestamp) to
-			// mw.loader.register()
-			if ( !count( $deps ) && $group === null && $source === 'local' ) {
-				$registrations[] = array( $name, $mtime );
-			}
-			// Modules with dependencies but no group or foreign source pass three arguments
-			// (name, timestamp, dependencies) to mw.loader.register()
-			elseif ( $group === null && $source === 'local' ) {
-				$registrations[] = array( $name, $mtime, $deps );
+			$mtime = max( $moduleMtime, wfTimestamp( TS_UNIX, $this->getConfig()->get( 'CacheEpoch' ) ) );
+
+			// FIXME: Convert to numbers, wfTimestamp always gives us stings, even for TS_UNIX
+
+			$skipFunction = $module->getSkipFunction();
+			if ( $skipFunction !== null && !ResourceLoader::inDebugMode() ) {
+				$skipFunction = $resourceLoader->filter( 'minify-js',
+					$skipFunction,
+					// There will potentially be lots of these little string in the registrations
+					// manifest, we don't want to blow up the startup module with
+					// "/* cache key: ... */" all over it in non-debug mode.
+					/* cacheReport = */ false
+				);
 			}
-			// Modules with a group but no foreign source pass four arguments (name, timestamp, dependencies, group)
-			// to mw.loader.register()
-			elseif ( $source === 'local' ) {
-				$registrations[] = array( $name, $mtime, $deps, $group );
+
+			$registryData[$name] = array(
+				'version' => $mtime,
+				'dependencies' => $module->getDependencies(),
+				'group' => $module->getGroup(),
+				'source' => $module->getSource(),
+				'loader' => $module->getLoaderScript(),
+				'skip' => $skipFunction,
+			);
+		}
+
+		self::compileUnresolvedDependencies( $registryData );
+
+		// Register sources
+		$out .= ResourceLoader::makeLoaderSourcesScript( $resourceLoader->getSources() );
+
+		// Concatenate module loader scripts and figure out the different call
+		// signatures for mw.loader.register
+		$registrations = array();
+		foreach ( $registryData as $name => $data ) {
+			if ( $data['loader'] !== false ) {
+				$out .= ResourceLoader::makeCustomLoaderScript(
+					$name,
+					wfTimestamp( TS_ISO_8601_BASIC, $data['version'] ),
+					$data['dependencies'],
+					$data['group'],
+					$data['source'],
+					$data['loader']
+				);
+				continue;
 			}
-			// Modules with a foreign source pass five arguments (name, timestamp, dependencies, group, source)
-			// to mw.loader.register()
-			else {
-				$registrations[] = array( $name, $mtime, $deps, $group, $source );
+
+			if (
+				!count( $data['dependencies'] ) &&
+				$data['group'] === null &&
+				$data['source'] === 'local' &&
+				$data['skip'] === null
+			) {
+				// Modules with no dependencies, group, foreign source or skip function;
+				// call mw.loader.register(name, timestamp)
+				$registrations[] = array( $name, $data['version'] );
+			} elseif (
+				$data['group'] === null &&
+				$data['source'] === 'local' &&
+				$data['skip'] === null
+			) {
+				// Modules with dependencies but no group, foreign source or skip function;
+				// call mw.loader.register(name, timestamp, dependencies)
+				$registrations[] = array( $name, $data['version'], $data['dependencies'] );
+			} elseif (
+				$data['source'] === 'local' &&
+				$data['skip'] === null
+			) {
+				// Modules with a group but no foreign source or skip function;
+				// call mw.loader.register(name, timestamp, dependencies, group)
+				$registrations[] = array(
+					$name,
+					$data['version'],
+					$data['dependencies'],
+					$data['group']
+				);
+			} elseif ( $data['skip'] === null ) {
+				// Modules with a foreign source but no skip function;
+				// call mw.loader.register(name, timestamp, dependencies, group, source)
+				$registrations[] = array(
+					$name,
+					$data['version'],
+					$data['dependencies'],
+					$data['group'],
+					$data['source']
+				);
+			} else {
+				// Modules with a skip function;
+				// call mw.loader.register(name, timestamp, dependencies, group, source, skip)
+				$registrations[] = array(
+					$name,
+					$data['version'],
+					$data['dependencies'],
+					$data['group'],
+					$data['source'],
+					$data['skip']
+				);
 			}
 		}
+
+		// Register modules
 		$out .= ResourceLoader::makeLoaderRegisterScript( $registrations );
 
 		wfProfileOut( __METHOD__ );
@@ -182,55 +333,75 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
 	}
 
 	/**
-	 * @param $context ResourceLoaderContext
+	 * Base modules required for the the base environment of ResourceLoader
+	 *
+	 * @return array
+	 */
+	public static function getStartupModules() {
+		return array( 'jquery', 'mediawiki' );
+	}
+
+	/**
+	 * Get the load URL of the startup modules.
+	 *
+	 * This is a helper for getScript(), but can also be called standalone, such
+	 * as when generating an AppCache manifest.
+	 *
+	 * @param ResourceLoaderContext $context
 	 * @return string
 	 */
-	public function getScript( ResourceLoaderContext $context ) {
-		global $IP, $wgLegacyJavaScriptGlobals;
+	public static function getStartupModulesUrl( ResourceLoaderContext $context ) {
+		$moduleNames = self::getStartupModules();
 
-		$out = file_get_contents( "$IP/resources/startup.js" );
-		if ( $context->getOnly() === 'scripts' ) {
+		// Get the latest version
+		$loader = $context->getResourceLoader();
+		$version = 0;
+		foreach ( $moduleNames as $moduleName ) {
+			$version = max( $version,
+				$loader->getModule( $moduleName )->getModifiedTime( $context )
+			);
+		}
+
+		$query = array(
+			'modules' => ResourceLoader::makePackedModulesString( $moduleNames ),
+			'only' => 'scripts',
+			'lang' => $context->getLanguage(),
+			'skin' => $context->getSkin(),
+			'debug' => $context->getDebug() ? 'true' : 'false',
+			'version' => wfTimestamp( TS_ISO_8601_BASIC, $version )
+		);
+		// Ensure uniform query order
+		ksort( $query );
+		return wfAppendQuery( wfScript( 'load' ), $query );
+	}
 
-			// The core modules:
-			$moduleNames = array( 'jquery', 'mediawiki' );
-			wfRunHooks( 'ResourceLoaderGetStartupModules', array( &$moduleNames ) );
+	/**
+	 * @param ResourceLoaderContext $context
+	 * @return string
+	 */
+	public function getScript( ResourceLoaderContext $context ) {
+		global $IP;
 
-			// Get the latest version
-			$loader = $context->getResourceLoader();
-			$version = 0;
-			foreach ( $moduleNames as $moduleName ) {
-				$version = max( $version,
-					$loader->getModule( $moduleName )->getModifiedTime( $context )
-				);
-			}
-			// Build load query for StartupModules
-			$query = array(
-				'modules' => ResourceLoader::makePackedModulesString( $moduleNames ),
-				'only' => 'scripts',
-				'lang' => $context->getLanguage(),
-				'skin' => $context->getSkin(),
-				'debug' => $context->getDebug() ? 'true' : 'false',
-				'version' => wfTimestamp( TS_ISO_8601_BASIC, $version )
-			);
-			// Ensure uniform query order
-			ksort( $query );
+		$out = file_get_contents( "$IP/resources/src/startup.js" );
+		if ( $context->getOnly() === 'scripts' ) {
 
 			// Startup function
-			$configuration = $this->getConfig( $context );
-			$registrations = self::getModuleRegistrations( $context );
-			$registrations = str_replace( "\n", "\n\t", trim( $registrations ) ); // fix indentation
-			$out .= "var startUp = function() {\n" .
-				"\tmw.config = new " . Xml::encodeJsCall( 'mw.Map', array( $wgLegacyJavaScriptGlobals ) ) . "\n" .
+			$configuration = $this->getConfigSettings( $context );
+			$registrations = $this->getModuleRegistrations( $context );
+			// Fix indentation
+			$registrations = str_replace( "\n", "\n\t", trim( $registrations ) );
+			$out .= "var startUp = function () {\n" .
+				"\tmw.config = new " .
+				Xml::encodeJsCall( 'mw.Map', array( $this->getConfig()->get( 'LegacyJavaScriptGlobals' ) ) ) . "\n" .
 				"\t$registrations\n" .
 				"\t" . Xml::encodeJsCall( 'mw.config.set', array( $configuration ) ) .
 				"};\n";
 
 			// Conditional script injection
-			$scriptTag = Html::linkedScript( wfAppendQuery( wfScript( 'load' ), $query ) );
+			$scriptTag = Html::linkedScript( self::getStartupModulesUrl( $context ) );
 			$out .= "if ( isCompatible() ) {\n" .
 				"\t" . Xml::encodeJsCall( 'document.write', array( $scriptTag ) ) .
-				"}\n" .
-				"delete isCompatible;";
+				"}";
 		}
 
 		return $out;
@@ -244,11 +415,11 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
 	}
 
 	/**
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @return array|mixed
 	 */
 	public function getModifiedTime( ResourceLoaderContext $context ) {
-		global $IP, $wgCacheEpoch;
+		global $IP;
 
 		$hash = $context->getHash();
 		if ( isset( $this->modifiedTime[$hash] ) ) {
@@ -260,19 +431,44 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
 		$loader = $context->getResourceLoader();
 		$loader->preloadModuleInfo( $loader->getModuleNames(), $context );
 
-		$this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" );
-		// ATTENTION!: Because of the line above, this is not going to cause
+		$time = max(
+			wfTimestamp( TS_UNIX, $this->getConfig()->get( 'CacheEpoch' ) ),
+			filemtime( "$IP/resources/src/startup.js" ),
+			$this->getHashMtime( $context )
+		);
+
+		// ATTENTION!: Because of the line below, this is not going to cause
 		// infinite recursion - think carefully before making changes to this
 		// code!
-		$time = wfTimestamp( TS_UNIX, $wgCacheEpoch );
+		// Pre-populate modifiedTime with something because the the loop over
+		// all modules below includes the the startup module (this module).
+		$this->modifiedTime[$hash] = 1;
+
 		foreach ( $loader->getModuleNames() as $name ) {
 			$module = $loader->getModule( $name );
 			$time = max( $time, $module->getModifiedTime( $context ) );
 		}
-		return $this->modifiedTime[$hash] = $time;
+
+		$this->modifiedTime[$hash] = $time;
+		return $this->modifiedTime[$hash];
 	}
 
-	/* Methods */
+	/**
+	 * Hash of all dynamic data embedded in getScript().
+	 *
+	 * Detect changes to mw.config settings embedded in #getScript (bug 28899).
+	 *
+	 * @param ResourceLoaderContext $context
+	 * @return string Hash
+	 */
+	public function getModifiedHash( ResourceLoaderContext $context ) {
+		$data = array(
+			'vars' => $this->getConfigSettings( $context ),
+			'wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
+		);
+
+		return md5( serialize( $data ) );
+	}
 
 	/**
 	 * @return string
diff --git a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
index bda86539..40274c63 100644
--- a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
@@ -36,27 +36,27 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule {
 	/* Methods */
 
 	/**
-	 * @param $context ResourceLoaderContext
-	 * @return array|int|Mixed
+	 * @param ResourceLoaderContext $context
+	 * @return array|int|mixed
 	 */
 	public function getModifiedTime( ResourceLoaderContext $context ) {
 		$hash = $context->getHash();
-		if ( isset( $this->modifiedTime[$hash] ) ) {
-			return $this->modifiedTime[$hash];
+		if ( !isset( $this->modifiedTime[$hash] ) ) {
+			global $wgUser;
+			$this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() );
 		}
 
-		global $wgUser;
-		return $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() );
+		return $this->modifiedTime[$hash];
 	}
 
 	/**
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @return array
 	 */
 	public function getStyles( ResourceLoaderContext $context ) {
-		global $wgAllowUserCssPrefs, $wgUser;
+		global $wgUser;
 
-		if ( !$wgAllowUserCssPrefs ) {
+		if ( !$this->getConfig()->get( 'AllowUserCssPrefs' ) ) {
 			return array();
 		}
 
@@ -71,17 +71,8 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule {
 				( $options['underline'] ? 'underline' : 'none' ) . "; }";
 		} else {
 			# The scripts of these languages are very hard to read with underlines
-			$rules[] = 'a:lang(ar), a:lang(ckb), a:lang(kk-arab), ' .
-			'a:lang(mzn), a:lang(ps), a:lang(ur) { text-decoration: none; }';
-		}
-		if ( $options['justify'] ) {
-			$rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
-		}
-		if ( !$options['showtoc'] ) {
-			$rules[] = "#toc { display: none; }\n";
-		}
-		if ( !$options['editsection'] ) {
-			$rules[] = ".mw-editsection { display: none; }\n";
+			$rules[] = 'a:lang(ar), a:lang(kk-arab), a:lang(mzn), ' .
+			'a:lang(ps), a:lang(ur) { text-decoration: none; }';
 		}
 		if ( $options['editfont'] !== 'default' ) {
 			// Double-check that $options['editfont'] consists of safe characters only
diff --git a/includes/resourceloader/ResourceLoaderUserGroupsModule.php b/includes/resourceloader/ResourceLoaderUserGroupsModule.php
index 9064263f..7cf19420 100644
--- a/includes/resourceloader/ResourceLoaderUserGroupsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserGroupsModule.php
@@ -25,21 +25,28 @@
  */
 class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule {
 
-	/* Protected Methods */
+	/* Protected Members */
+
 	protected $origin = self::ORIGIN_USER_SITEWIDE;
+	protected $targets = array( 'desktop', 'mobile' );
+
+	/* Protected Methods */
 
 	/**
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @return array
 	 */
 	protected function getPages( ResourceLoaderContext $context ) {
-		global $wgUser, $wgUseSiteJs, $wgUseSiteCss;
+		global $wgUser;
 
 		$userName = $context->getUser();
 		if ( $userName === null ) {
 			return array();
 		}
-		if ( !$wgUseSiteJs && !$wgUseSiteCss ) {
+
+		$useSiteJs = $this->getConfig()->get( 'UseSiteJs' );
+		$useSiteCss = $this->getConfig()->get( 'UseSiteCss' );
+		if ( !$useSiteJs && !$useSiteCss ) {
 			return array();
 		}
 
@@ -55,13 +62,13 @@ class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule {
 
 		$pages = array();
 		foreach ( $user->getEffectiveGroups() as $group ) {
-			if ( in_array( $group, array( '*', 'user' ) ) ) {
+			if ( $group == '*' ) {
 				continue;
 			}
-			if ( $wgUseSiteJs ) {
+			if ( $useSiteJs ) {
 				$pages["MediaWiki:Group-$group.js"] = array( 'type' => 'script' );
 			}
-			if ( $wgUseSiteCss ) {
+			if ( $useSiteCss ) {
 				$pages["MediaWiki:Group-$group.css"] = array( 'type' => 'style' );
 			}
 		}
diff --git a/includes/resourceloader/ResourceLoaderUserModule.php b/includes/resourceloader/ResourceLoaderUserModule.php
index 7a04e473..1b6d1de0 100644
--- a/includes/resourceloader/ResourceLoaderUserModule.php
+++ b/includes/resourceloader/ResourceLoaderUserModule.php
@@ -27,21 +27,27 @@
  */
 class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
 
-	/* Protected Methods */
+	/* Protected Members */
+
 	protected $origin = self::ORIGIN_USER_INDIVIDUAL;
 
+	/* Protected Methods */
+
 	/**
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @return array
 	 */
 	protected function getPages( ResourceLoaderContext $context ) {
-		global $wgAllowUserJs, $wgAllowUserCss;
 		$username = $context->getUser();
 
 		if ( $username === null ) {
 			return array();
 		}
-		if ( !$wgAllowUserJs && !$wgAllowUserCss ) {
+
+		$allowUserJs = $this->getConfig()->get( 'AllowUserJs' );
+		$allowUserCss = $this->getConfig()->get( 'AllowUserCss' );
+
+		if ( !$allowUserJs && !$allowUserCss ) {
 			return array();
 		}
 
@@ -55,11 +61,11 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
 		$userpage = $userpageTitle->getPrefixedDBkey(); // Needed so $excludepages works
 
 		$pages = array();
-		if ( $wgAllowUserJs ) {
+		if ( $allowUserJs ) {
 			$pages["$userpage/common.js"] = array( 'type' => 'script' );
 			$pages["$userpage/" . $context->getSkin() . '.js'] = array( 'type' => 'script' );
 		}
-		if ( $wgAllowUserCss ) {
+		if ( $allowUserCss ) {
 			$pages["$userpage/common.css"] = array( 'type' => 'style' );
 			$pages["$userpage/" . $context->getSkin() . '.css'] = array( 'type' => 'style' );
 		}
diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php
index 0b7e1964..bd97a8e5 100644
--- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php
@@ -33,24 +33,26 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
 
 	protected $origin = self::ORIGIN_CORE_INDIVIDUAL;
 
+	protected $targets = array( 'desktop', 'mobile' );
+
 	/* Methods */
 
 	/**
-	 * @param $context ResourceLoaderContext
-	 * @return array|int|Mixed
+	 * @param ResourceLoaderContext $context
+	 * @return array|int|mixed
 	 */
 	public function getModifiedTime( ResourceLoaderContext $context ) {
 		$hash = $context->getHash();
-		if ( isset( $this->modifiedTime[$hash] ) ) {
-			return $this->modifiedTime[$hash];
+		if ( !isset( $this->modifiedTime[$hash] ) ) {
+			global $wgUser;
+			$this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() );
 		}
 
-		global $wgUser;
-		return $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() );
+		return $this->modifiedTime[$hash];
 	}
 
 	/**
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @return string
 	 */
 	public function getScript( ResourceLoaderContext $context ) {
diff --git a/includes/resourceloader/ResourceLoaderUserTokensModule.php b/includes/resourceloader/ResourceLoaderUserTokensModule.php
index 92ebbe93..668467ca 100644
--- a/includes/resourceloader/ResourceLoaderUserTokensModule.php
+++ b/includes/resourceloader/ResourceLoaderUserTokensModule.php
@@ -30,25 +30,27 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule {
 
 	protected $origin = self::ORIGIN_CORE_INDIVIDUAL;
 
+	protected $targets = array( 'desktop', 'mobile' );
+
 	/* Methods */
 
 	/**
 	 * Fetch the tokens for the current user.
 	 *
-	 * @return array: List of tokens keyed by token type
+	 * @return array List of tokens keyed by token type
 	 */
 	protected function contextUserTokens() {
 		global $wgUser;
 
 		return array(
 			'editToken' => $wgUser->getEditToken(),
-			'patrolToken' => ApiQueryRecentChanges::getPatrolToken( null, null ),
-			'watchToken' => ApiQueryInfo::getWatchToken( null, null ),
+			'patrolToken' => $wgUser->getEditToken( 'patrol' ),
+			'watchToken' => $wgUser->getEditToken( 'watch' ),
 		);
 	}
 
 	/**
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @return string
 	 */
 	public function getScript( ResourceLoaderContext $context ) {
diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php
index 3f10ae53..de61fc55 100644
--- a/includes/resourceloader/ResourceLoaderWikiModule.php
+++ b/includes/resourceloader/ResourceLoaderWikiModule.php
@@ -36,8 +36,8 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 	# Origin is user-supplied code
 	protected $origin = self::ORIGIN_USER_SITEWIDE;
 
-	// In-object cache for title mtimes
-	protected $titleMtimes = array();
+	// In-object cache for title info
+	protected $titleInfo = array();
 
 	/* Abstract Protected Methods */
 
@@ -54,7 +54,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 	 * There is an optional media key, the value of which can be the
 	 * medium ('screen', 'print', etc.) of the stylesheet.
 	 *
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @return array
 	 */
 	abstract protected function getPages( ResourceLoaderContext $context );
@@ -77,7 +77,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 	}
 
 	/**
-	 * @param $title Title
+	 * @param Title $title
 	 * @return null|string
 	 */
 	protected function getContent( $title ) {
@@ -96,20 +96,20 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 			return null;
 		}
 
-		$model = $content->getModel();
-
-		if ( $model !== CONTENT_MODEL_CSS && $model !== CONTENT_MODEL_JAVASCRIPT ) {
-			wfDebugLog( 'resourceloader', __METHOD__ . ': bad content model $model for JS/CSS page!' );
+		if ( $content->isSupportedFormat( CONTENT_FORMAT_JAVASCRIPT ) ) {
+			return $content->serialize( CONTENT_FORMAT_JAVASCRIPT );
+		} elseif ( $content->isSupportedFormat( CONTENT_FORMAT_CSS ) ) {
+			return $content->serialize( CONTENT_FORMAT_CSS );
+		} else {
+			wfDebugLog( 'resourceloader', __METHOD__ . ": bad content model {$content->getModel()} for JS/CSS page!" );
 			return null;
 		}
-
-		return $content->getNativeData(); //NOTE: this is safe, we know it's JS or CSS
 	}
 
 	/* Methods */
 
 	/**
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @return string
 	 */
 	public function getScript( ResourceLoaderContext $context ) {
@@ -125,22 +125,17 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 			$script = $this->getContent( $title );
 			if ( strval( $script ) !== '' ) {
 				$script = $this->validateScriptFile( $titleText, $script );
-				if ( strpos( $titleText, '*/' ) === false ) {
-					$scripts .= "/* $titleText */\n";
-				}
-				$scripts .= $script . "\n";
+				$scripts .= ResourceLoader::makeComment( $titleText ) . $script . "\n";
 			}
 		}
 		return $scripts;
 	}
 
 	/**
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @return array
 	 */
 	public function getStyles( ResourceLoaderContext $context ) {
-		global $wgScriptPath;
-
 		$styles = array();
 		foreach ( $this->getPages( $context ) as $titleText => $options ) {
 			if ( $options['type'] !== 'style' ) {
@@ -158,47 +153,84 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 			if ( $this->getFlip( $context ) ) {
 				$style = CSSJanus::transform( $style, true, false );
 			}
-			$style = CSSMin::remap( $style, false, $wgScriptPath, true );
+			$style = CSSMin::remap( $style, false, $this->getConfig()->get( 'ScriptPath' ), true );
 			if ( !isset( $styles[$media] ) ) {
 				$styles[$media] = array();
 			}
-			if ( strpos( $titleText, '*/' ) === false ) {
-				$style = "/* $titleText */\n" . $style;
-			}
+			$style = ResourceLoader::makeComment( $titleText ) . $style;
 			$styles[$media][] = $style;
 		}
 		return $styles;
 	}
 
 	/**
-	 * @param $context ResourceLoaderContext
+	 * @param ResourceLoaderContext $context
 	 * @return int|mixed
 	 */
 	public function getModifiedTime( ResourceLoaderContext $context ) {
 		$modifiedTime = 1; // wfTimestamp() interprets 0 as "now"
-		$mtimes = $this->getTitleMtimes( $context );
-		if ( count( $mtimes ) ) {
+		$titleInfo = $this->getTitleInfo( $context );
+		if ( count( $titleInfo ) ) {
+			$mtimes = array_map( function( $value ) {
+				return $value['timestamp'];
+			}, $titleInfo );
 			$modifiedTime = max( $modifiedTime, max( $mtimes ) );
 		}
-		$modifiedTime = max( $modifiedTime, $this->getMsgBlobMtime( $context->getLanguage() ) );
+		$modifiedTime = max(
+			$modifiedTime,
+			$this->getMsgBlobMtime( $context->getLanguage() ),
+			$this->getDefinitionMtime( $context )
+		);
 		return $modifiedTime;
 	}
 
 	/**
-	 * @param $context ResourceLoaderContext
+	 * Get the definition summary for this module.
+	 *
+	 * @param ResourceLoaderContext $context
+	 * @return array
+	 */
+	public function getDefinitionSummary( ResourceLoaderContext $context ) {
+		return array(
+			'class' => get_class( $this ),
+			'pages' => $this->getPages( $context ),
+		);
+	}
+
+	/**
+	 * @param ResourceLoaderContext $context
 	 * @return bool
 	 */
 	public function isKnownEmpty( ResourceLoaderContext $context ) {
-		return count( $this->getTitleMtimes( $context ) ) == 0;
+		$titleInfo = $this->getTitleInfo( $context );
+		// Bug 68488: For modules in the "user" group, we should actually
+		// check that the pages are empty (page_len == 0), but for other
+		// groups, just check the pages exist so that we don't end up
+		// caching temporarily-blank pages without the appropriate
+		//