From a1789ddde42033f1b05cc4929491214ee6e79383 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Thu, 17 Dec 2015 09:15:42 +0100 Subject: Update to MediaWiki 1.26.0 --- includes/AjaxDispatcher.php | 12 +- includes/AjaxResponse.php | 30 +- includes/AuthPlugin.php | 16 + includes/Block.php | 158 +- includes/CategoryFinder.php | 4 +- includes/CategoryViewer.php | 2 +- includes/CdbCompat.php | 45 - includes/ChangeTags.php | 1214 ------------ includes/Collation.php | 8 +- includes/DefaultSettings.php | 563 ++++-- includes/Defines.php | 7 +- includes/EditPage.php | 137 +- includes/Export.php | 4 +- includes/FileDeleteForm.php | 4 +- includes/GitInfo.php | 4 +- includes/GlobalFunctions.php | 265 ++- includes/HistoryBlob.php | 8 +- includes/Hooks.php | 21 +- includes/Html.php | 111 +- includes/HttpFunctions.php | 6 +- includes/Import.php | 69 +- includes/Linker.php | 88 +- includes/MWNamespace.php | 12 + includes/MWTimestamp.php | 42 +- includes/MagicWord.php | 13 +- includes/MediaWiki.php | 282 ++- includes/Message.php | 81 +- includes/MessageBlobStore.php | 390 ---- includes/MimeMagic.php | 14 +- includes/MovePage.php | 67 +- includes/OutputPage.php | 525 +++--- includes/PHPVersionCheck.php | 146 +- includes/Preferences.php | 135 +- includes/PrefixSearch.php | 6 +- includes/ProtectionForm.php | 57 +- includes/Revision.php | 30 +- includes/RevisionList.php | 4 +- includes/Sanitizer.php | 111 +- includes/Setup.php | 74 +- includes/SiteStats.php | 4 +- includes/SquidPurgeClient.php | 24 +- includes/Status.php | 10 +- includes/StreamFile.php | 8 +- includes/StubObject.php | 2 +- includes/TemplateParser.php | 9 +- includes/Title.php | 270 +-- includes/User.php | 528 +++--- includes/UserRightsProxy.php | 4 +- includes/WatchedItem.php | 79 +- includes/WebRequest.php | 113 +- includes/WebResponse.php | 19 +- includes/WebStart.php | 6 +- includes/WikiMap.php | 62 +- includes/Xml.php | 125 +- includes/XmlSelect.php | 132 ++ includes/ZhConversion.php | 1949 +++++++++----------- includes/actions/Action.php | 10 + includes/actions/DeleteAction.php | 2 + includes/actions/EditAction.php | 2 + includes/actions/HistoryAction.php | 33 +- includes/actions/InfoAction.php | 95 +- includes/actions/RawAction.php | 52 +- includes/actions/RevertAction.php | 2 + includes/actions/RollbackAction.php | 3 + includes/actions/UnprotectAction.php | 1 - includes/actions/WatchAction.php | 2 +- includes/api/ApiBase.php | 169 +- includes/api/ApiBlock.php | 16 +- includes/api/ApiCreateAccount.php | 16 +- includes/api/ApiDelete.php | 4 +- includes/api/ApiEditPage.php | 86 +- includes/api/ApiEmailUser.php | 2 +- includes/api/ApiExpandTemplates.php | 32 +- includes/api/ApiFeedRecentChanges.php | 12 +- includes/api/ApiFeedWatchlist.php | 17 +- includes/api/ApiFileRevert.php | 2 + includes/api/ApiFormatBase.php | 23 +- includes/api/ApiFormatDump.php | 58 - includes/api/ApiFormatFeedWrapper.php | 9 - includes/api/ApiFormatJson.php | 16 +- includes/api/ApiFormatPhp.php | 3 +- includes/api/ApiFormatRaw.php | 28 +- includes/api/ApiFormatWddx.php | 162 -- includes/api/ApiFormatXml.php | 11 +- includes/api/ApiHelp.php | 188 +- includes/api/ApiImageRotate.php | 4 +- includes/api/ApiImport.php | 7 +- includes/api/ApiLogin.php | 19 +- includes/api/ApiMain.php | 209 ++- includes/api/ApiMessage.php | 30 + includes/api/ApiMove.php | 2 + includes/api/ApiOptions.php | 14 + includes/api/ApiPageSet.php | 69 +- includes/api/ApiParamInfo.php | 49 +- includes/api/ApiParse.php | 126 +- includes/api/ApiProtect.php | 9 +- includes/api/ApiQuery.php | 23 +- includes/api/ApiQueryAllCategories.php | 3 +- includes/api/ApiQueryAllDeletedRevisions.php | 45 +- includes/api/ApiQueryAllLinks.php | 1 + includes/api/ApiQueryAllMessages.php | 8 +- includes/api/ApiQueryAllPages.php | 12 +- includes/api/ApiQueryAllUsers.php | 11 +- includes/api/ApiQueryBacklinksprop.php | 8 +- includes/api/ApiQueryBase.php | 21 +- includes/api/ApiQueryBlocks.php | 9 +- includes/api/ApiQueryCategories.php | 5 +- includes/api/ApiQueryCategoryInfo.php | 2 +- includes/api/ApiQueryCategoryMembers.php | 11 +- includes/api/ApiQueryContributors.php | 6 +- includes/api/ApiQueryDeletedRevisions.php | 8 +- includes/api/ApiQueryDeletedrevs.php | 2 +- includes/api/ApiQueryDuplicateFiles.php | 2 +- includes/api/ApiQueryExtLinksUsage.php | 3 +- includes/api/ApiQueryExternalLinks.php | 2 +- includes/api/ApiQueryFileRepoInfo.php | 24 +- includes/api/ApiQueryFilearchive.php | 5 +- includes/api/ApiQueryIWBacklinks.php | 1 + includes/api/ApiQueryIWLinks.php | 3 +- includes/api/ApiQueryImageInfo.php | 6 +- includes/api/ApiQueryImages.php | 2 +- includes/api/ApiQueryInfo.php | 19 +- includes/api/ApiQueryLangBacklinks.php | 1 + includes/api/ApiQueryLangLinks.php | 5 +- includes/api/ApiQueryLinks.php | 4 +- includes/api/ApiQueryLogEvents.php | 3 +- includes/api/ApiQueryPageProps.php | 4 +- includes/api/ApiQueryPagesWithProp.php | 3 +- includes/api/ApiQueryProtectedTitles.php | 5 +- includes/api/ApiQueryRandom.php | 181 +- includes/api/ApiQueryRecentChanges.php | 5 +- includes/api/ApiQueryRevisions.php | 101 +- includes/api/ApiQueryRevisionsBase.php | 38 +- includes/api/ApiQuerySearch.php | 32 +- includes/api/ApiQuerySiteinfo.php | 18 +- includes/api/ApiQueryStashImageInfo.php | 4 + includes/api/ApiQueryTags.php | 3 +- includes/api/ApiQueryTokens.php | 4 + includes/api/ApiQueryUserContributions.php | 5 +- includes/api/ApiQueryUserInfo.php | 52 +- includes/api/ApiQueryUsers.php | 7 +- includes/api/ApiQueryWatchlist.php | 5 +- includes/api/ApiQueryWatchlistRaw.php | 31 +- includes/api/ApiResult.php | 164 +- includes/api/ApiRevisionDelete.php | 4 +- includes/api/ApiRollback.php | 2 + includes/api/ApiSetNotificationTimestamp.php | 4 +- includes/api/ApiStashEdit.php | 7 +- includes/api/ApiUnblock.php | 8 +- includes/api/ApiUndelete.php | 9 +- includes/api/ApiUpload.php | 24 +- includes/api/i18n/ar.json | 4 +- includes/api/i18n/ast.json | 10 + includes/api/i18n/ba.json | 13 + includes/api/i18n/bcl.json | 10 + includes/api/i18n/be-tarask.json | 4 +- includes/api/i18n/br.json | 13 + includes/api/i18n/bs.json | 5 +- includes/api/i18n/ca.json | 17 +- includes/api/i18n/ce.json | 3 +- includes/api/i18n/ckb.json | 8 + includes/api/i18n/cs.json | 105 +- includes/api/i18n/de.json | 107 +- includes/api/i18n/el.json | 89 +- includes/api/i18n/en-gb.json | 2 +- includes/api/i18n/en.json | 457 ++++- includes/api/i18n/es.json | 564 +++++- includes/api/i18n/et.json | 42 + includes/api/i18n/eu.json | 11 +- includes/api/i18n/fa.json | 7 +- includes/api/i18n/fi.json | 8 +- includes/api/i18n/fo.json | 39 + includes/api/i18n/fr.json | 421 ++++- includes/api/i18n/gl.json | 414 ++++- includes/api/i18n/he.json | 1185 +++++++++++- includes/api/i18n/ht.json | 8 + includes/api/i18n/hu.json | 13 +- includes/api/i18n/ia.json | 3 + includes/api/i18n/is.json | 10 + includes/api/i18n/it.json | 90 +- includes/api/i18n/ja.json | 374 +++- includes/api/i18n/ko.json | 78 +- includes/api/i18n/ksh.json | 669 ++++++- includes/api/i18n/ku-latn.json | 23 +- includes/api/i18n/ky.json | 19 + includes/api/i18n/lb.json | 65 +- includes/api/i18n/lv.json | 7 +- includes/api/i18n/mk.json | 241 +-- includes/api/i18n/mr.json | 12 + includes/api/i18n/nap.json | 12 +- includes/api/i18n/nb.json | 91 +- includes/api/i18n/ne.json | 13 + includes/api/i18n/nl.json | 42 +- includes/api/i18n/oc.json | 73 +- includes/api/i18n/olo.json | 12 + includes/api/i18n/or.json | 14 + includes/api/i18n/pl.json | 256 ++- includes/api/i18n/ps.json | 21 + includes/api/i18n/pt-br.json | 276 ++- includes/api/i18n/pt.json | 15 +- includes/api/i18n/qqq.json | 366 +++- includes/api/i18n/ru.json | 197 +- includes/api/i18n/shn.json | 8 + includes/api/i18n/si.json | 1 + includes/api/i18n/sq.json | 12 + includes/api/i18n/sv.json | 75 +- includes/api/i18n/ta.json | 13 + includes/api/i18n/tr.json | 12 +- includes/api/i18n/uk.json | 1286 ++++++++++++- includes/api/i18n/vi.json | 75 +- includes/api/i18n/wuu.json | 8 + includes/api/i18n/yi.json | 10 + includes/api/i18n/zh-hans.json | 743 ++++++-- includes/api/i18n/zh-hant.json | 42 +- includes/cache/BacklinkCache.php | 6 +- includes/cache/CacheDependency.php | 8 +- includes/cache/FileCacheBase.php | 4 +- includes/cache/HTMLFileCache.php | 1 + includes/cache/LCStoreStaticArray.php | 140 ++ includes/cache/LinkBatch.php | 2 +- includes/cache/LinkCache.php | 87 +- includes/cache/LocalisationCache.php | 13 +- includes/cache/MessageBlobStore.php | 425 +++++ includes/cache/MessageCache.php | 557 +++--- includes/cache/ResourceFileCache.php | 4 +- includes/cache/UserCache.php | 4 +- includes/changes/ChangesFeed.php | 4 + includes/changes/ChangesList.php | 39 +- includes/changes/EnhancedChangesList.php | 262 ++- includes/changes/RecentChange.php | 113 +- includes/changetags/ChangeTags.php | 1258 +++++++++++++ includes/changetags/ChangeTagsLogList.php | 3 +- includes/changetags/ChangeTagsRevisionList.php | 3 +- includes/clientpool/RedisConnectionPool.php | 108 +- includes/compat/CdbCompat.php | 45 + includes/compat/IPSetCompat.php | 28 + includes/compat/normal/UtfNormal.php | 129 ++ includes/compat/normal/UtfNormalDefines.php | 186 ++ includes/compat/normal/UtfNormalUtil.php | 100 + includes/config/ConfigFactory.php | 3 + includes/content/ContentHandler.php | 54 +- includes/content/CssContent.php | 44 + includes/content/CssContentHandler.php | 19 + includes/content/JavaScriptContent.php | 47 + includes/content/JavaScriptContentHandler.php | 18 + includes/content/TextContentHandler.php | 9 + includes/content/WikitextContent.php | 4 +- includes/context/ContextSource.php | 1 + includes/context/DerivativeContext.php | 7 +- includes/context/IContextSource.php | 3 +- includes/context/MutableContext.php | 82 + includes/context/RequestContext.php | 20 +- includes/dao/DBAccessObjectUtils.php | 59 + includes/db/DBConnRef.php | 524 ++++++ includes/db/Database.php | 310 ++-- includes/db/DatabaseError.php | 19 +- includes/db/DatabaseMssql.php | 8 +- includes/db/DatabaseMysql.php | 40 +- includes/db/DatabaseMysqlBase.php | 82 +- includes/db/DatabaseMysqli.php | 43 +- includes/db/DatabaseOracle.php | 38 +- includes/db/DatabasePostgres.php | 25 +- includes/db/DatabaseSqlite.php | 49 +- includes/db/IDatabase.php | 1513 +++++++++++++++ includes/db/LBFactory.php | 11 +- includes/db/LBFactoryMulti.php | 2 +- includes/db/LoadBalancer.php | 147 +- includes/db/LoadMonitor.php | 87 - includes/db/LoadMonitorMySQL.php | 124 ++ includes/debug/MWDebug.php | 6 +- includes/debug/logger/LegacyLogger.php | 101 +- includes/debug/logger/LegacySpi.php | 4 +- includes/debug/logger/LoggerFactory.php | 14 +- includes/debug/logger/MonologSpi.php | 35 +- includes/debug/logger/NullSpi.php | 8 +- includes/debug/logger/Spi.php | 8 +- includes/debug/logger/monolog/AvroFormatter.php | 139 ++ includes/debug/logger/monolog/BufferHandler.php | 47 + includes/debug/logger/monolog/KafkaHandler.php | 224 +++ includes/debug/logger/monolog/LegacyFormatter.php | 4 +- includes/debug/logger/monolog/LineFormatter.php | 177 ++ includes/deferred/DeferredUpdates.php | 68 +- includes/deferred/HTMLCacheUpdate.php | 3 +- includes/deferred/LinksDeletionUpdate.php | 105 ++ includes/deferred/LinksUpdate.php | 85 - includes/deferred/SiteStatsUpdate.php | 14 +- includes/diff/DifferenceEngine.php | 27 +- includes/diff/TableDiffFormatter.php | 2 +- includes/diff/UnifiedDiffFormatter.php | 10 + includes/exception/BadTitleError.php | 16 +- includes/exception/HttpError.php | 20 +- includes/exception/MWException.php | 8 +- includes/exception/MWExceptionHandler.php | 367 ++-- includes/filebackend/FSFile.php | 9 +- includes/filebackend/FileBackend.php | 15 +- includes/filebackend/FileBackendGroup.php | 2 + includes/filebackend/FileBackendMultiWrite.php | 176 +- includes/filebackend/FileBackendStore.php | 80 +- includes/filebackend/FileOp.php | 4 +- includes/filebackend/MemoryFileBackend.php | 8 +- includes/filebackend/SwiftFileBackend.php | 151 +- includes/filebackend/TempFSFile.php | 15 +- includes/filebackend/lockmanager/DBLockManager.php | 2 +- includes/filebackend/lockmanager/FSLockManager.php | 4 +- includes/filebackend/lockmanager/LockManager.php | 1 - includes/filerepo/FileBackendDBRepoWrapper.php | 356 ++++ includes/filerepo/FileRepo.php | 43 +- includes/filerepo/ForeignAPIRepo.php | 4 +- includes/filerepo/ForeignDBRepo.php | 32 +- includes/filerepo/ForeignDBViaLBRepo.php | 10 + includes/filerepo/LocalRepo.php | 113 +- includes/filerepo/file/ArchivedFile.php | 4 +- includes/filerepo/file/File.php | 65 +- includes/filerepo/file/ForeignAPIFile.php | 19 +- includes/filerepo/file/LocalFile.php | 289 +-- includes/gallery/PackedImageGallery.php | 4 +- includes/gallery/PackedOverlayImageGallery.php | 2 +- includes/gallery/TraditionalImageGallery.php | 4 +- includes/htmlform/HTMLAutoCompleteSelectField.php | 14 +- includes/htmlform/HTMLButtonField.php | 36 +- includes/htmlform/HTMLCheckField.php | 62 +- includes/htmlform/HTMLCheckMatrix.php | 42 +- includes/htmlform/HTMLForm.php | 185 +- includes/htmlform/HTMLFormField.php | 193 +- includes/htmlform/HTMLFormFieldWithButton.php | 73 + includes/htmlform/HTMLInfoField.php | 10 + includes/htmlform/HTMLMultiSelectField.php | 60 +- includes/htmlform/HTMLRadioField.php | 17 + includes/htmlform/HTMLSelectAndOtherField.php | 5 +- includes/htmlform/HTMLSelectField.php | 22 + includes/htmlform/HTMLSelectNamespace.php | 16 +- .../htmlform/HTMLSelectNamespaceWithButton.php | 17 + includes/htmlform/HTMLSelectOrOtherField.php | 4 + includes/htmlform/HTMLSubmitField.php | 2 + includes/htmlform/HTMLTextAreaField.php | 44 + includes/htmlform/HTMLTextField.php | 61 +- includes/htmlform/HTMLTextFieldWithButton.php | 17 + includes/htmlform/HTMLTitleTextField.php | 81 + includes/htmlform/HTMLUserTextField.php | 47 + includes/htmlform/OOUIHTMLForm.php | 221 +++ includes/htmlform/VFormHTMLForm.php | 6 +- includes/installer/DatabaseInstaller.php | 4 +- includes/installer/DatabaseUpdater.php | 3 +- includes/installer/Installer.php | 51 +- includes/installer/LocalSettingsGenerator.php | 11 +- includes/installer/MssqlInstaller.php | 1 - includes/installer/MssqlUpdater.php | 2 +- includes/installer/MysqlInstaller.php | 10 +- includes/installer/MysqlUpdater.php | 5 + includes/installer/PostgresInstaller.php | 11 +- includes/installer/PostgresUpdater.php | 10 + includes/installer/SqliteInstaller.php | 4 +- includes/installer/WebInstaller.php | 29 +- includes/installer/WebInstallerPage.php | 37 +- includes/installer/i18n/ar.json | 9 +- includes/installer/i18n/ast.json | 8 +- includes/installer/i18n/azb.json | 30 + includes/installer/i18n/ba.json | 7 + includes/installer/i18n/bcl.json | 1 + includes/installer/i18n/be-tarask.json | 3 +- includes/installer/i18n/bg.json | 43 +- includes/installer/i18n/bn.json | 17 +- includes/installer/i18n/bs.json | 27 +- includes/installer/i18n/ca.json | 3 +- includes/installer/i18n/ce.json | 9 +- includes/installer/i18n/ckb.json | 1 + includes/installer/i18n/cs.json | 3 +- includes/installer/i18n/cu.json | 4 + includes/installer/i18n/da.json | 11 +- includes/installer/i18n/de-ch.json | 6 +- includes/installer/i18n/de.json | 2 +- includes/installer/i18n/el.json | 63 +- includes/installer/i18n/en-gb.json | 1 - includes/installer/i18n/en.json | 2 - includes/installer/i18n/eo.json | 10 +- includes/installer/i18n/es.json | 56 +- includes/installer/i18n/et.json | 7 +- includes/installer/i18n/eu.json | 4 +- includes/installer/i18n/fa.json | 19 +- includes/installer/i18n/fo.json | 15 +- includes/installer/i18n/fr.json | 3 +- includes/installer/i18n/fy.json | 1 + includes/installer/i18n/gl.json | 7 +- includes/installer/i18n/he.json | 3 +- includes/installer/i18n/hi.json | 8 +- includes/installer/i18n/hu.json | 19 +- includes/installer/i18n/hy.json | 35 +- includes/installer/i18n/ia.json | 7 +- includes/installer/i18n/id.json | 1 - includes/installer/i18n/it.json | 17 +- includes/installer/i18n/ja.json | 11 +- includes/installer/i18n/jut.json | 5 +- includes/installer/i18n/km.json | 3 + includes/installer/i18n/kn.json | 4 +- includes/installer/i18n/ko.json | 76 +- includes/installer/i18n/ksh.json | 99 +- includes/installer/i18n/ku-latn.json | 48 +- includes/installer/i18n/lb.json | 3 +- includes/installer/i18n/lrc.json | 4 + includes/installer/i18n/lv.json | 15 +- includes/installer/i18n/mai.json | 4 +- includes/installer/i18n/mg.json | 12 + includes/installer/i18n/mk.json | 6 +- includes/installer/i18n/mr.json | 5 +- includes/installer/i18n/nan.json | 11 +- includes/installer/i18n/nap.json | 102 +- includes/installer/i18n/nb.json | 1 - includes/installer/i18n/ne.json | 55 +- includes/installer/i18n/nl.json | 6 +- includes/installer/i18n/nn.json | 4 +- includes/installer/i18n/olo.json | 59 + includes/installer/i18n/or.json | 17 +- includes/installer/i18n/pl.json | 19 +- includes/installer/i18n/pms.json | 4 +- includes/installer/i18n/ps.json | 22 + includes/installer/i18n/pt-br.json | 19 +- includes/installer/i18n/pt.json | 6 +- includes/installer/i18n/qqq.json | 6 +- includes/installer/i18n/ro.json | 14 +- includes/installer/i18n/ru.json | 3 +- includes/installer/i18n/sah.json | 9 +- includes/installer/i18n/sco.json | 3 +- includes/installer/i18n/sd.json | 8 + includes/installer/i18n/sk.json | 5 +- includes/installer/i18n/sq.json | 40 +- includes/installer/i18n/sr-ec.json | 5 +- includes/installer/i18n/su.json | 7 + includes/installer/i18n/sv.json | 5 +- includes/installer/i18n/tokipona.json | 8 + includes/installer/i18n/tr.json | 53 +- includes/installer/i18n/tt-cyrl.json | 7 +- includes/installer/i18n/udm.json | 12 +- includes/installer/i18n/uk.json | 13 +- includes/installer/i18n/vi.json | 5 +- includes/installer/i18n/wuu.json | 4 +- includes/installer/i18n/xmf.json | 33 + includes/installer/i18n/yi.json | 15 + includes/installer/i18n/zh-hans.json | 5 +- includes/installer/i18n/zh-hant.json | 12 +- includes/interwiki/Interwiki.php | 21 +- includes/jobqueue/Job.php | 46 +- includes/jobqueue/JobQueue.php | 81 +- includes/jobqueue/JobQueueDB.php | 98 +- includes/jobqueue/JobQueueFederated.php | 35 +- includes/jobqueue/JobQueueGroup.php | 143 +- includes/jobqueue/JobQueueRedis.php | 151 +- includes/jobqueue/JobRunner.php | 171 +- includes/jobqueue/JobSpecification.php | 71 +- .../jobqueue/aggregator/JobQueueAggregator.php | 2 +- .../aggregator/JobQueueAggregatorRedis.php | 8 + includes/jobqueue/jobs/ActivityUpdateJob.php | 75 + includes/jobqueue/jobs/AssembleUploadChunksJob.php | 2 +- includes/jobqueue/jobs/DoubleRedirectJob.php | 20 +- includes/jobqueue/jobs/DuplicateJob.php | 2 +- includes/jobqueue/jobs/EmaillingJob.php | 4 +- includes/jobqueue/jobs/EnotifNotifyJob.php | 2 +- includes/jobqueue/jobs/EnqueueJob.php | 21 +- includes/jobqueue/jobs/HTMLCacheUpdateJob.php | 2 +- includes/jobqueue/jobs/NullJob.php | 2 +- includes/jobqueue/jobs/PublishStashedFileJob.php | 2 +- includes/jobqueue/jobs/RecentChangesUpdateJob.php | 8 +- includes/jobqueue/jobs/RefreshLinksJob.php | 51 +- includes/jobqueue/jobs/ThumbnailRenderJob.php | 14 +- includes/jobqueue/jobs/UploadFromUrlJob.php | 6 +- includes/json/FormatJson.php | 2 +- includes/libs/BufferingStatsdDataFactory.php | 34 +- includes/libs/CSSMin.php | 73 +- includes/libs/HashRing.php | 4 +- includes/libs/HttpStatus.php | 28 +- includes/libs/IPSet.php | 276 --- includes/libs/JavaScriptMinifier.php | 8 + includes/libs/MapCacheLRU.php | 7 +- includes/libs/MultiHttpClient.php | 55 +- includes/libs/ObjectFactory.php | 49 +- includes/libs/ProcessCacheLRU.php | 7 +- includes/libs/ReplacementArray.php | 9 +- includes/libs/RiffExtractor.php | 100 + includes/libs/SamplingStatsdClient.php | 133 ++ includes/libs/ScopedPHPTimeout.php | 84 - includes/libs/XmlTypeCheck.php | 35 +- includes/libs/composer/ComposerLock.php | 3 + includes/libs/eventrelayer/EventRelayer.php | 65 + includes/libs/eventrelayer/EventRelayerMCRD.php | 66 + includes/libs/normal/UtfNormal.php | 129 -- includes/libs/normal/UtfNormalDefines.php | 186 -- includes/libs/normal/UtfNormalUtil.php | 99 - includes/libs/objectcache/APCBagOStuff.php | 31 +- includes/libs/objectcache/BagOStuff.php | 201 +- includes/libs/objectcache/EmptyBagOStuff.php | 2 +- includes/libs/objectcache/HashBagOStuff.php | 14 +- includes/libs/objectcache/ReplicatedBagOStuff.php | 129 ++ includes/libs/objectcache/WANObjectCache.php | 746 ++++++++ includes/libs/objectcache/WinCacheBagOStuff.php | 33 +- includes/libs/objectcache/XCacheBagOStuff.php | 23 +- .../libs/virtualrest/ParsoidVirtualRESTService.php | 221 ++- .../virtualrest/RestbaseVirtualRESTService.php | 242 ++- .../libs/virtualrest/SwiftVirtualRESTService.php | 6 +- includes/libs/virtualrest/VirtualRESTService.php | 15 +- .../libs/virtualrest/VirtualRESTServiceClient.php | 12 +- includes/logging/ContentModelLogFormatter.php | 34 + includes/logging/LogEntry.php | 34 +- includes/logging/LogEventsList.php | 8 +- includes/logging/LogFormatter.php | 10 +- includes/logging/LogPager.php | 5 +- includes/logging/PatrolLogFormatter.php | 6 +- includes/logging/ProtectLogFormatter.php | 70 + includes/mail/EmailNotification.php | 61 +- includes/mail/UserMailer.php | 140 +- includes/media/Bitmap.php | 7 +- includes/media/BitmapMetadataHandler.php | 8 +- includes/media/DjVu.php | 96 +- includes/media/DjVuImage.php | 10 +- includes/media/Exif.php | 12 +- includes/media/ExifBitmap.php | 78 +- includes/media/FormatMetadata.php | 35 +- includes/media/GIF.php | 8 +- includes/media/GIFMetadataExtractor.php | 4 +- includes/media/IPTC.php | 4 +- includes/media/ImageHandler.php | 4 +- includes/media/Jpeg.php | 2 +- includes/media/JpegMetadataExtractor.php | 4 +- includes/media/MediaHandler.php | 4 +- .../MediaTransformInvalidParametersException.php | 3 +- includes/media/PNG.php | 8 +- includes/media/PNGMetadataExtractor.php | 16 +- includes/media/SVG.php | 15 +- includes/media/SVGMetadataExtractor.php | 17 +- includes/media/TransformationalImageHandler.php | 2 +- includes/media/WebP.php | 306 +++ includes/media/XCF.php | 6 +- includes/media/XMP.php | 245 ++- includes/media/XMPInfo.php | 11 +- includes/media/XMPValidate.php | 60 +- includes/media/tinyrgb.icc | Bin 0 -> 524 bytes includes/mime.info | 2 +- includes/objectcache/MemcachedBagOStuff.php | 15 +- includes/objectcache/MemcachedClient.php | 15 +- includes/objectcache/MemcachedPeclBagOStuff.php | 22 +- includes/objectcache/MemcachedPhpBagOStuff.php | 14 +- includes/objectcache/MultiWriteBagOStuff.php | 88 +- includes/objectcache/ObjectCache.php | 189 +- includes/objectcache/ObjectCacheSessionHandler.php | 45 +- includes/objectcache/RedisBagOStuff.php | 130 +- includes/objectcache/SqlBagOStuff.php | 78 +- includes/page/Article.php | 129 +- includes/page/ImagePage.php | 35 +- includes/page/WikiPage.php | 417 +++-- includes/pager/ReverseChronologicalPager.php | 6 +- includes/pager/TablePager.php | 7 + includes/parser/CacheTime.php | 2 +- includes/parser/CoreParserFunctions.php | 25 +- includes/parser/LinkHolderArray.php | 1 - includes/parser/MWTidy.php | 323 +--- includes/parser/Parser.php | 221 ++- includes/parser/ParserCache.php | 51 +- includes/parser/ParserDiffTest.php | 21 - includes/parser/ParserOptions.php | 99 +- includes/parser/ParserOutput.php | 37 +- includes/parser/Preprocessor_DOM.php | 26 +- includes/parser/Preprocessor_Hash.php | 24 +- includes/parser/StripState.php | 27 +- includes/password/EncryptedPassword.php | 2 +- includes/password/PasswordPolicyChecks.php | 115 ++ includes/password/UserPasswordPolicy.php | 201 ++ includes/poolcounter/PoolCounter.php | 9 +- includes/poolcounter/PoolCounterRedis.php | 7 +- includes/poolcounter/PoolWorkArticleView.php | 8 +- includes/profiler/ProfileSection.php | 5 +- includes/profiler/Profiler.php | 26 +- includes/profiler/ProfilerFunctions.php | 2 +- includes/profiler/ProfilerStub.php | 3 + includes/profiler/ProfilerXhprof.php | 45 +- includes/profiler/SectionProfiler.php | 9 +- includes/profiler/TransactionProfiler.php | 56 +- includes/profiler/output/ProfilerOutputDump.php | 6 +- includes/profiler/output/ProfilerOutputStats.php | 6 +- includes/profiler/output/ProfilerOutputText.php | 2 +- includes/profiler/output/ProfilerOutputUdp.php | 2 +- includes/rcfeed/MachineReadableRCFeedFormatter.php | 4 +- includes/rcfeed/RCFeedFormatter.php | 3 +- includes/registration/CoreVersionChecker.php | 68 + includes/registration/ExtensionProcessor.php | 27 +- includes/registration/ExtensionRegistry.php | 52 +- includes/registration/Processor.php | 20 +- .../DerivativeResourceLoaderContext.php | 76 +- includes/resourceloader/ResourceLoader.php | 553 +++--- includes/resourceloader/ResourceLoaderContext.php | 33 +- .../ResourceLoaderEditToolbarModule.php | 31 +- .../resourceloader/ResourceLoaderFileModule.php | 122 +- .../ResourceLoaderForeignApiModule.php | 33 + includes/resourceloader/ResourceLoaderImage.php | 25 +- .../resourceloader/ResourceLoaderImageModule.php | 193 +- .../ResourceLoaderJqueryMsgModule.php | 66 + .../ResourceLoaderLanguageDataModule.php | 16 +- .../ResourceLoaderLanguageNamesModule.php | 18 +- includes/resourceloader/ResourceLoaderModule.php | 479 +++-- .../ResourceLoaderOOUIImageModule.php | 86 + .../resourceloader/ResourceLoaderRawFileModule.php | 52 + .../resourceloader/ResourceLoaderSiteModule.php | 9 - .../resourceloader/ResourceLoaderSkinModule.php | 13 +- .../ResourceLoaderSpecialCharacterDataModule.php | 19 +- .../resourceloader/ResourceLoaderStartUpModule.php | 191 +- .../ResourceLoaderUserCSSPrefsModule.php | 18 +- .../ResourceLoaderUserDefaultsModule.php | 19 +- .../ResourceLoaderUserOptionsModule.php | 21 +- .../resourceloader/ResourceLoaderWikiModule.php | 198 +- includes/revisiondelete/RevDelItem.php | 2 + includes/revisiondelete/RevDelList.php | 24 +- includes/revisiondelete/RevDelLogItem.php | 27 +- includes/revisiondelete/RevDelRevisionItem.php | 2 +- includes/revisiondelete/RevisionDeleter.php | 4 +- includes/search/SearchEngine.php | 4 +- includes/search/SearchHighlighter.php | 2 +- includes/search/SearchMySQL.php | 6 +- includes/search/SearchPostgres.php | 2 +- includes/search/SearchResultSet.php | 35 +- includes/search/SearchSqlite.php | 4 +- includes/site/CachingSiteStore.php | 6 +- includes/site/DBSiteStore.php | 6 +- includes/site/SiteExporter.php | 20 +- includes/site/SiteSQLStore.php | 2 +- includes/skins/MediaWikiI18N.php | 4 +- includes/skins/Skin.php | 48 +- includes/skins/SkinFallbackTemplate.php | 6 +- includes/skins/SkinTemplate.php | 23 +- includes/specialpage/ChangesListSpecialPage.php | 3 +- includes/specialpage/FormSpecialPage.php | 6 +- includes/specialpage/QueryPage.php | 49 +- includes/specialpage/RedirectSpecialPage.php | 33 +- includes/specialpage/SpecialPage.php | 27 +- includes/specialpage/SpecialPageFactory.php | 61 +- includes/specials/SpecialActiveusers.php | 29 +- includes/specials/SpecialAllMessages.php | 18 +- includes/specials/SpecialAllPages.php | 83 +- includes/specials/SpecialAncientpages.php | 6 +- includes/specials/SpecialBlock.php | 39 +- includes/specials/SpecialBlockList.php | 77 +- includes/specials/SpecialBrokenRedirects.php | 4 +- includes/specials/SpecialChangeContentModel.php | 223 +++ includes/specials/SpecialChangeEmail.php | 13 +- includes/specials/SpecialChangePassword.php | 3 +- includes/specials/SpecialComparePages.php | 3 + includes/specials/SpecialConfirmemail.php | 8 + includes/specials/SpecialContributions.php | 8 +- includes/specials/SpecialDeletedContributions.php | 8 +- includes/specials/SpecialDiff.php | 8 +- includes/specials/SpecialDoubleRedirects.php | 5 +- includes/specials/SpecialEditTags.php | 20 +- includes/specials/SpecialEditWatchlist.php | 7 +- includes/specials/SpecialEmailuser.php | 7 +- includes/specials/SpecialExport.php | 207 +-- includes/specials/SpecialFewestrevisions.php | 4 +- includes/specials/SpecialFileDuplicateSearch.php | 6 +- includes/specials/SpecialFilepath.php | 11 +- includes/specials/SpecialImport.php | 144 +- includes/specials/SpecialJavaScriptTest.php | 108 +- includes/specials/SpecialLinkSearch.php | 68 +- includes/specials/SpecialListDuplicatedFiles.php | 4 +- includes/specials/SpecialListfiles.php | 14 +- includes/specials/SpecialListredirects.php | 4 +- includes/specials/SpecialListusers.php | 16 +- includes/specials/SpecialLockdb.php | 4 +- includes/specials/SpecialMIMEsearch.php | 7 +- includes/specials/SpecialMediaStatistics.php | 12 +- includes/specials/SpecialMergeHistory.php | 2 + includes/specials/SpecialMostcategories.php | 4 +- includes/specials/SpecialMostinterwikis.php | 4 +- includes/specials/SpecialMostlinked.php | 4 +- includes/specials/SpecialMostlinkedcategories.php | 2 +- includes/specials/SpecialMovepage.php | 338 ++-- includes/specials/SpecialMyLanguage.php | 26 +- includes/specials/SpecialMyRedirectPages.php | 55 +- includes/specials/SpecialNewpages.php | 4 +- includes/specials/SpecialPageLanguage.php | 7 +- includes/specials/SpecialPagesWithProp.php | 8 +- includes/specials/SpecialPasswordReset.php | 2 +- includes/specials/SpecialPermanentLink.php | 8 +- includes/specials/SpecialPreferences.php | 9 +- includes/specials/SpecialProtectedtitles.php | 12 +- includes/specials/SpecialRandomInCategory.php | 16 +- includes/specials/SpecialRandompage.php | 20 +- includes/specials/SpecialRecentchanges.php | 23 +- includes/specials/SpecialRecentchangeslinked.php | 1 + includes/specials/SpecialResetTokens.php | 5 + includes/specials/SpecialRevisiondelete.php | 36 +- includes/specials/SpecialRunJobs.php | 15 +- includes/specials/SpecialSearch.php | 152 +- includes/specials/SpecialShortpages.php | 2 +- includes/specials/SpecialSpecialpages.php | 27 +- includes/specials/SpecialStatistics.php | 43 +- includes/specials/SpecialTags.php | 97 +- includes/specials/SpecialUndelete.php | 15 +- includes/specials/SpecialUnlockdb.php | 4 +- includes/specials/SpecialUnusedcategories.php | 4 +- includes/specials/SpecialUnusedtemplates.php | 4 +- includes/specials/SpecialUnwatchedpages.php | 4 +- includes/specials/SpecialUpload.php | 14 +- includes/specials/SpecialUploadStash.php | 4 +- includes/specials/SpecialUserlogin.php | 115 +- includes/specials/SpecialUserrights.php | 8 +- includes/specials/SpecialVersion.php | 76 +- includes/specials/SpecialWantedfiles.php | 2 +- includes/specials/SpecialWantedtemplates.php | 5 +- includes/specials/SpecialWatchlist.php | 1 + includes/specials/SpecialWhatlinkshere.php | 53 +- includes/templates/Usercreate.php | 8 +- includes/templates/Userlogin.php | 5 + includes/tidy.conf | 22 - includes/tidy/Html5Depurate.php | 45 + includes/tidy/RaggettBase.php | 47 + includes/tidy/RaggettExternal.php | 73 + includes/tidy/RaggettInternalHHVM.php | 29 + includes/tidy/RaggettInternalPHP.php | 52 + includes/tidy/RaggettWrapper.php | 89 + includes/tidy/TidyDriverBase.php | 40 + includes/tidy/tidy.conf | 22 + includes/title/MalformedTitleException.php | 54 +- includes/title/MediaWikiTitleCodec.php | 34 +- includes/title/TitleValue.php | 24 +- includes/upload/UploadBase.php | 93 +- includes/upload/UploadFromUrl.php | 4 +- includes/utils/AutoloadGenerator.php | 74 +- includes/utils/AvroValidator.php | 184 ++ includes/utils/BatchRowIterator.php | 278 +++ includes/utils/BatchRowUpdate.php | 133 ++ includes/utils/BatchRowWriter.php | 71 + includes/utils/IP.php | 6 +- includes/utils/MWCryptHKDF.php | 8 +- includes/utils/MWCryptRand.php | 8 +- includes/utils/MWFunction.php | 40 - includes/utils/RowUpdateGenerator.php | 39 + includes/utils/UIDGenerator.php | 21 +- includes/utils/iterators/IteratorDecorator.php | 50 + includes/utils/iterators/NotRecursiveIterator.php | 35 + includes/widget/AUTHORS.txt | 11 + includes/widget/ComplexNamespaceInputWidget.php | 110 ++ includes/widget/ComplexTitleInputWidget.php | 67 + includes/widget/LICENSE.txt | 25 + includes/widget/NamespaceInputWidget.php | 66 + includes/widget/TitleInputWidget.php | 60 + includes/widget/UserInputWidget.php | 29 + 741 files changed, 34305 insertions(+), 13055 deletions(-) delete mode 100644 includes/CdbCompat.php delete mode 100644 includes/ChangeTags.php delete mode 100644 includes/MessageBlobStore.php create mode 100644 includes/XmlSelect.php delete mode 100644 includes/api/ApiFormatDump.php delete mode 100644 includes/api/ApiFormatWddx.php create mode 100644 includes/api/i18n/ast.json create mode 100644 includes/api/i18n/ba.json create mode 100644 includes/api/i18n/bcl.json create mode 100644 includes/api/i18n/br.json create mode 100644 includes/api/i18n/ckb.json create mode 100644 includes/api/i18n/et.json create mode 100644 includes/api/i18n/fo.json create mode 100644 includes/api/i18n/ht.json create mode 100644 includes/api/i18n/is.json create mode 100644 includes/api/i18n/ky.json create mode 100644 includes/api/i18n/mr.json create mode 100644 includes/api/i18n/ne.json create mode 100644 includes/api/i18n/olo.json create mode 100644 includes/api/i18n/or.json create mode 100644 includes/api/i18n/shn.json create mode 100644 includes/api/i18n/sq.json create mode 100644 includes/api/i18n/ta.json create mode 100644 includes/api/i18n/wuu.json create mode 100644 includes/api/i18n/yi.json create mode 100644 includes/cache/LCStoreStaticArray.php create mode 100644 includes/cache/MessageBlobStore.php create mode 100644 includes/changetags/ChangeTags.php create mode 100644 includes/compat/CdbCompat.php create mode 100644 includes/compat/IPSetCompat.php create mode 100644 includes/compat/normal/UtfNormal.php create mode 100644 includes/compat/normal/UtfNormalDefines.php create mode 100644 includes/compat/normal/UtfNormalUtil.php create mode 100644 includes/context/MutableContext.php create mode 100644 includes/dao/DBAccessObjectUtils.php create mode 100644 includes/db/DBConnRef.php create mode 100644 includes/db/IDatabase.php create mode 100644 includes/db/LoadMonitorMySQL.php create mode 100644 includes/debug/logger/monolog/AvroFormatter.php create mode 100644 includes/debug/logger/monolog/BufferHandler.php create mode 100644 includes/debug/logger/monolog/KafkaHandler.php create mode 100644 includes/debug/logger/monolog/LineFormatter.php create mode 100644 includes/deferred/LinksDeletionUpdate.php create mode 100644 includes/filerepo/FileBackendDBRepoWrapper.php create mode 100644 includes/htmlform/HTMLFormFieldWithButton.php create mode 100644 includes/htmlform/HTMLSelectNamespaceWithButton.php create mode 100644 includes/htmlform/HTMLTextFieldWithButton.php create mode 100644 includes/htmlform/HTMLTitleTextField.php create mode 100644 includes/htmlform/HTMLUserTextField.php create mode 100644 includes/htmlform/OOUIHTMLForm.php create mode 100644 includes/installer/i18n/azb.json create mode 100644 includes/installer/i18n/olo.json create mode 100644 includes/installer/i18n/sd.json create mode 100644 includes/installer/i18n/tokipona.json create mode 100644 includes/installer/i18n/xmf.json create mode 100644 includes/jobqueue/jobs/ActivityUpdateJob.php delete mode 100644 includes/libs/IPSet.php create mode 100644 includes/libs/RiffExtractor.php create mode 100644 includes/libs/SamplingStatsdClient.php delete mode 100644 includes/libs/ScopedPHPTimeout.php create mode 100644 includes/libs/eventrelayer/EventRelayer.php create mode 100644 includes/libs/eventrelayer/EventRelayerMCRD.php delete mode 100644 includes/libs/normal/UtfNormal.php delete mode 100644 includes/libs/normal/UtfNormalDefines.php delete mode 100644 includes/libs/normal/UtfNormalUtil.php create mode 100644 includes/libs/objectcache/ReplicatedBagOStuff.php create mode 100644 includes/libs/objectcache/WANObjectCache.php create mode 100644 includes/logging/ContentModelLogFormatter.php create mode 100644 includes/logging/ProtectLogFormatter.php create mode 100644 includes/media/WebP.php create mode 100644 includes/media/tinyrgb.icc create mode 100644 includes/password/PasswordPolicyChecks.php create mode 100644 includes/password/UserPasswordPolicy.php create mode 100644 includes/registration/CoreVersionChecker.php create mode 100644 includes/resourceloader/ResourceLoaderForeignApiModule.php create mode 100644 includes/resourceloader/ResourceLoaderJqueryMsgModule.php create mode 100644 includes/resourceloader/ResourceLoaderOOUIImageModule.php create mode 100644 includes/resourceloader/ResourceLoaderRawFileModule.php create mode 100644 includes/specials/SpecialChangeContentModel.php delete mode 100644 includes/tidy.conf create mode 100644 includes/tidy/Html5Depurate.php create mode 100644 includes/tidy/RaggettBase.php create mode 100644 includes/tidy/RaggettExternal.php create mode 100644 includes/tidy/RaggettInternalHHVM.php create mode 100644 includes/tidy/RaggettInternalPHP.php create mode 100644 includes/tidy/RaggettWrapper.php create mode 100644 includes/tidy/TidyDriverBase.php create mode 100644 includes/tidy/tidy.conf create mode 100644 includes/utils/AvroValidator.php create mode 100644 includes/utils/BatchRowIterator.php create mode 100644 includes/utils/BatchRowUpdate.php create mode 100644 includes/utils/BatchRowWriter.php delete mode 100644 includes/utils/MWFunction.php create mode 100644 includes/utils/RowUpdateGenerator.php create mode 100644 includes/utils/iterators/IteratorDecorator.php create mode 100644 includes/utils/iterators/NotRecursiveIterator.php create mode 100644 includes/widget/AUTHORS.txt create mode 100644 includes/widget/ComplexNamespaceInputWidget.php create mode 100644 includes/widget/ComplexTitleInputWidget.php create mode 100644 includes/widget/LICENSE.txt create mode 100644 includes/widget/NamespaceInputWidget.php create mode 100644 includes/widget/TitleInputWidget.php create mode 100644 includes/widget/UserInputWidget.php (limited to 'includes') diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index b14114d7..96892d71 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -124,9 +124,9 @@ class AjaxDispatcher { $result = call_user_func_array( $this->func_name, $this->args ); if ( $result === false || $result === null ) { - wfDebug( __METHOD__ . ' ERROR while dispatching ' - . $this->func_name . "(" . var_export( $this->args, true ) . "): " - . "no data returned\n" ); + wfDebug( __METHOD__ . ' ERROR while dispatching ' . + $this->func_name . "(" . var_export( $this->args, true ) . "): " . + "no data returned\n" ); wfHttpError( 500, 'Internal Error', "{$this->func_name} returned no data" ); @@ -141,9 +141,9 @@ class AjaxDispatcher { wfDebug( __METHOD__ . ' dispatch complete for ' . $this->func_name . "\n" ); } } catch ( Exception $e ) { - wfDebug( __METHOD__ . ' ERROR while dispatching ' - . $this->func_name . "(" . var_export( $this->args, true ) . "): " - . get_class( $e ) . ": " . $e->getMessage() . "\n" ); + wfDebug( __METHOD__ . ' ERROR while dispatching ' . + $this->func_name . "(" . var_export( $this->args, true ) . "): " . + get_class( $e ) . ": " . $e->getMessage() . "\n" ); if ( !headers_sent() ) { wfHttpError( 500, 'Internal Error', diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index 8e9f490f..6c2efc29 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -86,7 +86,7 @@ class AjaxResponse { $this->mDisabled = false; $this->mText = ''; - $this->mResponseCode = '200 OK'; + $this->mResponseCode = 200; $this->mLastModified = false; $this->mContentType = 'application/x-wiki'; @@ -158,16 +158,20 @@ class AjaxResponse { */ function sendHeaders() { if ( $this->mResponseCode ) { - $n = preg_replace( '/^ *(\d+)/', '\1', $this->mResponseCode ); - header( "Status: " . $this->mResponseCode, true, (int)$n ); + // For back-compat, it is supported that mResponseCode be a string like " 200 OK" + // (with leading space and the status message after). Cast response code to an integer + // to take advantage of PHP's conversion rules which will turn " 200 OK" into 200. + // http://php.net/string#language.types.string.conversion + $n = intval( trim( $this->mResponseCode ) ); + HttpStatus::header( $n ); } - header ( "Content-Type: " . $this->mContentType ); + header( "Content-Type: " . $this->mContentType ); if ( $this->mLastModified ) { - header ( "Last-Modified: " . $this->mLastModified ); + header( "Last-Modified: " . $this->mLastModified ); } else { - header ( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" ); + header( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" ); } if ( $this->mCacheDuration ) { @@ -189,20 +193,20 @@ 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}," . + 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}" ); } } else { # always expired, always modified - header ( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); // Date in the past - header ( "Cache-Control: no-cache, must-revalidate" ); // HTTP/1.1 - header ( "Pragma: no-cache" ); // HTTP/1.0 + header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); // Date in the past + header( "Cache-Control: no-cache, must-revalidate" ); // HTTP/1.1 + header( "Pragma: no-cache" ); // HTTP/1.0 } if ( $this->mVary ) { - header ( "Vary: " . $this->mVary ); + header( "Vary: " . $this->mVary ); } } @@ -246,7 +250,7 @@ class AjaxResponse { $ismodsince >= $wgCacheEpoch ) { ini_set( 'zlib.output_compression', 0 ); - $this->setResponseCode( "304 Not Modified" ); + $this->setResponseCode( 304 ); $this->disable(); $this->mLastModified = $lastmod; diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php index 45ad4d1b..badf47c3 100644 --- a/includes/AuthPlugin.php +++ b/includes/AuthPlugin.php @@ -120,6 +120,8 @@ class AuthPlugin { * The User object is passed by reference so it can be modified; don't * forget the & on your function declaration. * + * @deprecated since 1.26, use the UserLoggedIn hook instead. And assigning + * a different User object to $user is no longer supported. * @param User $user * @return bool */ @@ -204,6 +206,7 @@ class AuthPlugin { * Update user information in the external authentication database. * Return true if successful. * + * @deprecated since 1.26, use the UserSaveSettings hook instead. * @param User $user * @return bool */ @@ -215,6 +218,7 @@ class AuthPlugin { * Update user groups in the external authentication database. * Return true if successful. * + * @deprecated since 1.26, use the UserGroupsChanged hook instead. * @param User $user * @param array $addgroups Groups to add. * @param array $delgroups Groups to remove. @@ -278,6 +282,8 @@ class AuthPlugin { * The User object is passed by reference so it can be modified; don't * forget the & on your function declaration. * + * @deprecated since 1.26, use the UserLoggedIn hook instead. And assigning + * a different User object to $user is no longer supported. * @param User $user * @param bool $autocreate True if user is being autocreated on login */ @@ -326,11 +332,21 @@ class AuthPluginUser { return -1; } + /** + * Indicate whether the user is locked + * @deprecated since 1.26, use the UserIsLocked hook instead. + * @return bool + */ public function isLocked() { # Override this! return false; } + /** + * Indicate whether the user is hidden + * @deprecated since 1.26, use the UserIsHidden hook instead. + * @return bool + */ public function isHidden() { # Override this! return false; diff --git a/includes/Block.php b/includes/Block.php index 873a26d8..c5a16fce 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -23,15 +23,16 @@ class Block { /** @var string */ public $mReason; - /** @var bool|string */ + /** @var string */ public $mTimestamp; - /** @var int */ + /** @var bool */ public $mAuto; - /** @var bool|string */ + /** @var string */ public $mExpiry; + /** @var bool */ public $mHideName; /** @var int */ @@ -65,10 +66,10 @@ class Block { protected $blocker; /** @var bool */ - protected $isHardblock = true; + protected $isHardblock; /** @var bool */ - protected $isAutoblocking = true; + protected $isAutoblocking; # TYPE constants const TYPE_USER = 1; @@ -78,59 +79,84 @@ class Block { const TYPE_ID = 5; /** - * @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 + * Create a new block with specified parameters on a user, IP or IP range. + * + * @param array $options Parameters of the block: + * address string|User Target user name, User object, IP address or IP range + * user int Override target user ID (for foreign users) + * by int User ID of the blocker + * reason string Reason of the block + * timestamp string The time at which the block comes into effect + * auto bool Is this an automatic block? + * expiry string Timestamp of expiration of the block or 'infinity' + * anonOnly bool Only disallow anonymous actions + * createAccount bool Disallow creation of new accounts + * enableAutoblock bool Enable automatic blocking + * hideName bool Hide the target user name + * blockEmail bool Disallow sending emails + * allowUsertalk bool Allow the target to edit its own talk page + * byText string Username of the blocker (for foreign users) + * + * @since 1.26 accepts $options array instead of individual parameters; order + * of parameters above reflects the original order */ - 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 = '' - ) { - if ( $timestamp === 0 ) { - $timestamp = wfTimestampNow(); - } + function __construct( $options = array() ) { + $defaults = array( + 'address' => '', + 'user' => null, + 'by' => null, + 'reason' => '', + 'timestamp' => '', + 'auto' => false, + 'expiry' => '', + 'anonOnly' => false, + 'createAccount' => false, + 'enableAutoblock' => false, + 'hideName' => false, + 'blockEmail' => false, + 'allowUsertalk' => false, + 'byText' => '', + ); - if ( count( func_get_args() ) > 0 ) { - # Soon... :D - # wfDeprecated( __METHOD__ . " with arguments" ); + if ( func_num_args() > 1 || !is_array( $options ) ) { + $options = array_combine( + array_slice( array_keys( $defaults ), 0, func_num_args() ), + func_get_args() + ); + wfDeprecated( __METHOD__ . ' with multiple arguments', '1.26' ); } - $this->setTarget( $address ); - if ( $this->target instanceof User && $user ) { - $this->forcedTargetID = $user; // needed for foreign users - } - if ( $by ) { // local user - $this->setBlocker( User::newFromId( $by ) ); - } else { // foreign user - $this->setBlocker( $byText ); + $options += $defaults; + + $this->setTarget( $options['address'] ); + + if ( $this->target instanceof User && $options['user'] ) { + # Needed for foreign users + $this->forcedTargetID = $options['user']; } - $this->mReason = $reason; - $this->mTimestamp = wfTimestamp( TS_MW, $timestamp ); - $this->mAuto = $auto; - $this->isHardblock( !$anonOnly ); - $this->prevents( 'createaccount', $createAccount ); - if ( $expiry == 'infinity' || $expiry == wfGetDB( DB_SLAVE )->getInfinity() ) { - $this->mExpiry = 'infinity'; + + if ( $options['by'] ) { + # Local user + $this->setBlocker( User::newFromID( $options['by'] ) ); } else { - $this->mExpiry = wfTimestamp( TS_MW, $expiry ); + # Foreign user + $this->setBlocker( $options['byText'] ); } - $this->isAutoblocking( $enableAutoblock ); - $this->mHideName = $hideName; - $this->prevents( 'sendemail', $blockEmail ); - $this->prevents( 'editownusertalk', !$allowUsertalk ); + + $this->mReason = $options['reason']; + $this->mTimestamp = wfTimestamp( TS_MW, $options['timestamp'] ); + $this->mExpiry = wfGetDB( DB_SLAVE )->decodeExpiry( $options['expiry'] ); + + # Boolean settings + $this->mAuto = (bool)$options['auto']; + $this->mHideName = (bool)$options['hideName']; + $this->isHardblock( !$options['anonOnly'] ); + $this->isAutoblocking( (bool)$options['enableAutoblock'] ); + + # Prevention measures + $this->prevents( 'sendemail', (bool)$options['blockEmail'] ); + $this->prevents( 'editownusertalk', !$options['allowUsertalk'] ); + $this->prevents( 'createaccount', (bool)$options['createAccount'] ); $this->mFromMaster = false; } @@ -375,16 +401,11 @@ class Block { $this->mTimestamp = wfTimestamp( TS_MW, $row->ipb_timestamp ); $this->mAuto = $row->ipb_auto; $this->mHideName = $row->ipb_deleted; - $this->mId = $row->ipb_id; + $this->mId = (int)$row->ipb_id; $this->mParentBlockId = $row->ipb_parent_block_id; // I wish I didn't have to do this - $db = wfGetDB( DB_SLAVE ); - if ( $row->ipb_expiry == $db->getInfinity() ) { - $this->mExpiry = 'infinity'; - } else { - $this->mExpiry = wfTimestamp( TS_MW, $row->ipb_expiry ); - } + $this->mExpiry = wfGetDB( DB_SLAVE )->decodeExpiry( $row->ipb_expiry ); $this->isHardblock( !$row->ipb_anon_only ); $this->isAutoblocking( $row->ipb_enable_autoblock ); @@ -452,11 +473,15 @@ class Block { $dbw->insert( 'ipblocks', $row, __METHOD__, array( 'IGNORE' ) ); $affected = $dbw->affectedRows(); + $this->mId = $dbw->insertId(); # Don't collide with expired blocks. - # Do this after trying to insert to avoid pointless gap locks. + # Do this after trying to insert to avoid locking. if ( !$affected ) { - $dbw->delete( 'ipblocks', + # T96428: The ipb_address index uses a prefix on a field, so + # use a standard SELECT + DELETE to avoid annoying gap locks. + $ids = $dbw->selectFieldValues( 'ipblocks', + 'ipb_id', array( 'ipb_address' => $row['ipb_address'], 'ipb_user' => $row['ipb_user'], @@ -464,13 +489,14 @@ class Block { ), __METHOD__ ); - - $dbw->insert( 'ipblocks', $row, __METHOD__, array( 'IGNORE' ) ); - $affected = $dbw->affectedRows(); + if ( $ids ) { + $dbw->delete( 'ipblocks', array( 'ipb_id' => $ids ), __METHOD__ ); + $dbw->insert( 'ipblocks', $row, __METHOD__, array( 'IGNORE' ) ); + $affected = $dbw->affectedRows(); + $this->mId = $dbw->insertId(); + } } - $this->mId = $dbw->insertId(); - if ( $affected ) { $auto_ipd_ids = $this->doRetroactiveAutoblock(); return array( 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ); @@ -1113,7 +1139,7 @@ class Block { $blocks = array(); foreach ( $rows as $row ) { $block = self::newFromRow( $row ); - if ( !$block->deleteIfExpired() ) { + if ( !$block->deleteIfExpired() ) { $blocks[] = $block; } } diff --git a/includes/CategoryFinder.php b/includes/CategoryFinder.php index 33de7404..77c43bf0 100644 --- a/includes/CategoryFinder.php +++ b/includes/CategoryFinder.php @@ -27,7 +27,7 @@ * 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 * @@ -39,7 +39,7 @@ * ); * $a = $cf->run(); * print implode( ',' , $a ); - * + * @endcode * */ class CategoryFinder { diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php index 66079c01..e2c31a66 100644 --- a/includes/CategoryViewer.php +++ b/includes/CategoryViewer.php @@ -329,7 +329,7 @@ class CategoryViewer extends ContextSource { 'category' => array( 'LEFT JOIN', array( 'cat_title = page_title', 'page_namespace' => NS_CATEGORY - )) + ) ) ) ); diff --git a/includes/CdbCompat.php b/includes/CdbCompat.php deleted file mode 100644 index 0074cc96..00000000 --- a/includes/CdbCompat.php +++ /dev/null @@ -1,45 +0,0 @@ - 'mw-tag-marker ' . - Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ), - self::tagDescription( $tag ) - ); - $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" ); - } - $markers = wfMessage( 'tag-list-wrapper' ) - ->numParams( count( $displayTags ) ) - ->rawParams( $wgLang->commaList( $displayTags ) ) - ->parse(); - $markers = Xml::tags( 'span', array( 'class' => 'mw-tag-markers' ), $markers ); - - return array( $markers, $classes ); - } - - /** - * Get a short description for a 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 - */ - public static function tagDescription( $tag ) { - $msg = wfMessage( "tag-$tag" ); - return $msg->exists() ? $msg->parse() : htmlspecialchars( $tag ); - } - - /** - * 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 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 - */ - public static function addTags( $tags, $rc_id = null, $rev_id = null, - $log_id = null, $params = null - ) { - $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params ); - return (bool)$result[0]; - } - - /** - * Add and remove tags to/from a change given its rc_id, rev_id and/or log_id, - * without verifying that the tags exist or are valid. If a tag is present in - * both $tagsToAdd and $tagsToRemove, it will be removed. - * - * This function should only be used by extensions to manipulate tags they - * have registered using the ListDefinedTags hook. When dealing with user - * input, call updateTagsWithChecks() instead. - * - * @param string|array|null $tagsToAdd Tags to add to the change - * @param string|array|null $tagsToRemove Tags to remove from the change - * @param int|null &$rc_id The rc_id of the change to add the tags to. - * Pass a variable whose value is null if the rc_id is not relevant or unknown. - * @param int|null &$rev_id The rev_id of the change to add the tags to. - * Pass a variable whose value is null if the rev_id is not relevant or unknown. - * @param int|null &$log_id The log_id of the change to add the tags to. - * Pass a variable whose value is null if the log_id is not relevant or unknown. - * @param string $params Params to put in the ct_params field of table - * 'change_tag' when adding tags - * - * @throws MWException When $rc_id, $rev_id and $log_id are all null - * @return array Index 0 is an array of tags actually added, index 1 is an - * array of tags actually removed, index 2 is an array of tags present on the - * revision or log entry before any changes were made - * - * @since 1.25 - */ - public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null, - &$rev_id = null, &$log_id = null, $params = null ) { - - $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags... - $tagsToRemove = array_filter( (array)$tagsToRemove ); - - if ( !$rc_id && !$rev_id && !$log_id ) { - throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' . - 'specified when adding or removing a tag from a change!' ); - } - - $dbw = wfGetDB( DB_MASTER ); - - // Might as well look for rcids and so on. - if ( !$rc_id ) { - // Info might be out of date, somewhat fractionally, on slave. - if ( $log_id ) { - $rc_id = $dbw->selectField( - 'recentchanges', - 'rc_id', - array( 'rc_logid' => $log_id ), - __METHOD__ - ); - } elseif ( $rev_id ) { - $rc_id = $dbw->selectField( - 'recentchanges', - 'rc_id', - array( 'rc_this_oldid' => $rev_id ), - __METHOD__ - ); - } - } elseif ( !$log_id && !$rev_id ) { - // 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__ - ); - } - - // update the tag_summary row - $prevTags = array(); - if ( !self::updateTagSummaryRow( $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, - $log_id, $prevTags ) ) { - - // nothing to do - return array( array(), array(), $prevTags ); - } - - // insert a row into change_tag for each new tag - if ( count( $tagsToAdd ) ) { - $tagsRows = array(); - foreach ( $tagsToAdd as $tag ) { - // Filter so we don't insert NULLs as zero accidentally. - // Keep in mind that $rc_id === null means "I don't care/know about the - // rc_id, just delete $tag on this revision/log entry". It doesn't - // mean "only delete tags on this revision/log WHERE rc_id IS NULL". - $tagsRows[] = array_filter( - array( - 'ct_tag' => $tag, - 'ct_rc_id' => $rc_id, - 'ct_log_id' => $log_id, - 'ct_rev_id' => $rev_id, - 'ct_params' => $params - ) - ); - } - - $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array( 'IGNORE' ) ); - } - - // delete from change_tag - if ( count( $tagsToRemove ) ) { - foreach ( $tagsToRemove as $tag ) { - $conds = array_filter( - array( - 'ct_tag' => $tag, - 'ct_rc_id' => $rc_id, - 'ct_log_id' => $log_id, - 'ct_rev_id' => $rev_id - ) - ); - $dbw->delete( 'change_tag', $conds, __METHOD__ ); - } - } - - self::purgeTagUsageCache(); - return array( $tagsToAdd, $tagsToRemove, $prevTags ); - } - - /** - * Adds or removes a given set of tags to/from the relevant row of the - * tag_summary table. Modifies the tagsToAdd and tagsToRemove arrays to - * reflect the tags that were actually added and/or removed. - * - * @param array &$tagsToAdd - * @param array &$tagsToRemove If a tag is present in both $tagsToAdd and - * $tagsToRemove, it will be removed - * @param int|null $rc_id Null if not known or not applicable - * @param int|null $rev_id Null if not known or not applicable - * @param int|null $log_id Null if not known or not applicable - * @param array &$prevTags Optionally outputs a list of the tags that were - * in the tag_summary row to begin with - * @return bool True if any modifications were made, otherwise false - * @since 1.25 - */ - protected static function updateTagSummaryRow( &$tagsToAdd, &$tagsToRemove, - $rc_id, $rev_id, $log_id, &$prevTags = array() ) { - - $dbw = wfGetDB( DB_MASTER ); - - $tsConds = array_filter( array( - 'ts_rc_id' => $rc_id, - 'ts_rev_id' => $rev_id, - 'ts_log_id' => $log_id - ) ); - - // Can't both add and remove a tag at the same time... - $tagsToAdd = array_diff( $tagsToAdd, $tagsToRemove ); - - // 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 ) ); - - // add tags - $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) ); - $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) ); - - // remove tags - $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) ); - $newTags = array_values( array_diff( $newTags, $tagsToRemove ) ); - - sort( $prevTags ); - sort( $newTags ); - if ( $prevTags == $newTags ) { - // No change. - return false; - } - - if ( !$newTags ) { - // no tags left, so delete the row altogether - $dbw->delete( 'tag_summary', $tsConds, __METHOD__ ); - } else { - $dbw->replace( 'tag_summary', - array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ), - array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ), - __METHOD__ - ); - } - - return true; - } - - /** - * Helper function to generate a fatal status with a 'not-allowed' type error. - * - * @param string $msgOne Message key to use in the case of one tag - * @param string $msgMulti Message key to use in the case of more than one tag - * @param array $tags Restricted tags (passed as $1 into the message, count of - * $tags passed as $2) - * @return Status - * @since 1.25 - */ - protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) { - $lang = RequestContext::getMain()->getLanguage(); - $count = count( $tags ); - return Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne, - $lang->commaList( $tags ), $count ); - } - - /** - * Is it OK to allow the user to apply all the specified tags at the same time - * as they edit/make the change? - * - * @param array $tags Tags that you are interested in applying - * @param User|null $user User whose permission you wish to check, or null if - * you don't care (e.g. maintenance scripts) - * @return Status - * @since 1.25 - */ - public static function canAddTagsAccompanyingChange( array $tags, - User $user = null ) { - - if ( !is_null( $user ) && !$user->isAllowed( 'applychangetags' ) ) { - return Status::newFatal( 'tags-apply-no-permission' ); - } - - // to be applied, a tag has to be explicitly defined - // @todo Allow extensions to define tags that can be applied by users... - $allowedTags = self::listExplicitlyDefinedTags(); - $disallowedTags = array_diff( $tags, $allowedTags ); - if ( $disallowedTags ) { - return self::restrictedTagError( 'tags-apply-not-allowed-one', - 'tags-apply-not-allowed-multi', $disallowedTags ); - } - - return Status::newGood(); - } - - /** - * Adds tags to a given change, checking whether it is allowed first, but - * without adding a log entry. Useful for cases where the tag is being added - * along with the action that generated the change (e.g. tagging an edit as - * it is being made). - * - * Extensions should not use this function, unless directly handling a user - * request to add a particular tag. Normally, extensions should call - * ChangeTags::updateTags() instead. - * - * @param array $tags Tags to apply - * @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' when adding tags - * @param User $user Who to give credit for the action - * @return Status - * @since 1.25 - */ - public static function addTagsAccompanyingChangeWithChecks( array $tags, - $rc_id, $rev_id, $log_id, $params, User $user ) { - - // are we allowed to do this? - $result = self::canAddTagsAccompanyingChange( $tags, $user ); - if ( !$result->isOK() ) { - $result->value = null; - return $result; - } - - // do it! - self::addTags( $tagsToAdd, $rc_id, $rev_id, $log_id, $params ); - - return Status::newGood( true ); - } - - /** - * Is it OK to allow the user to adds and remove the given tags tags to/from a - * change? - * - * @param array $tagsToAdd Tags that you are interested in adding - * @param array $tagsToRemove Tags that you are interested in removing - * @param User|null $user User whose permission you wish to check, or null if - * you don't care (e.g. maintenance scripts) - * @return Status - * @since 1.25 - */ - public static function canUpdateTags( array $tagsToAdd, array $tagsToRemove, - User $user = null ) { - - if ( !is_null( $user ) && !$user->isAllowed( 'changetags' ) ) { - return Status::newFatal( 'tags-update-no-permission' ); - } - - // to be added, a tag has to be explicitly defined - // @todo Allow extensions to define tags that can be applied by users... - $explicitlyDefinedTags = self::listExplicitlyDefinedTags(); - $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags ); - if ( $diff ) { - return self::restrictedTagError( 'tags-update-add-not-allowed-one', - 'tags-update-add-not-allowed-multi', $diff ); - } - - // to be removed, a tag has to be either explicitly defined or not defined - // at all - $definedTags = self::listDefinedTags(); - $diff = array_diff( $tagsToRemove, $explicitlyDefinedTags ); - if ( $diff ) { - $intersect = array_intersect( $diff, $definedTags ); - if ( $intersect ) { - return self::restrictedTagError( 'tags-update-remove-not-allowed-one', - 'tags-update-remove-not-allowed-multi', $intersect ); - } - } - - return Status::newGood(); - } - - /** - * Adds and/or removes tags to/from a given change, checking whether it is - * allowed first, and adding a log entry afterwards. - * - * Includes a call to ChangeTag::canUpdateTags(), so your code doesn't need - * to do that. However, it doesn't check whether the *_id parameters are a - * valid combination. That is up to you to enforce. See ApiTag::execute() for - * an example. - * - * @param array|null $tagsToAdd If none, pass array() or null - * @param array|null $tagsToRemove If none, pass array() or null - * @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' when adding tags - * @param string $reason Comment for the log - * @param User $user Who to give credit for the action - * @return Status If successful, the value of this Status object will be an - * object (stdClass) with the following fields: - * - logId: the ID of the added log entry, or null if no log entry was added - * (i.e. no operation was performed) - * - addedTags: an array containing the tags that were actually added - * - removedTags: an array containing the tags that were actually removed - * @since 1.25 - */ - public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove, - $rc_id, $rev_id, $log_id, $params, $reason, User $user ) { - - if ( is_null( $tagsToAdd ) ) { - $tagsToAdd = array(); - } - if ( is_null( $tagsToRemove ) ) { - $tagsToRemove = array(); - } - if ( !$tagsToAdd && !$tagsToRemove ) { - // no-op, don't bother - return Status::newGood( (object)array( - 'logId' => null, - 'addedTags' => array(), - 'removedTags' => array(), - ) ); - } - - // are we allowed to do this? - $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $user ); - if ( !$result->isOK() ) { - $result->value = null; - return $result; - } - - // basic rate limiting - if ( $user->pingLimiter( 'changetag' ) ) { - return Status::newFatal( 'actionthrottledtext' ); - } - - // do it! - list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd, - $tagsToRemove, $rc_id, $rev_id, $log_id, $params ); - if ( !$tagsAdded && !$tagsRemoved ) { - // no-op, don't log it - return Status::newGood( (object)array( - 'logId' => null, - 'addedTags' => array(), - 'removedTags' => array(), - ) ); - } - - // log it - $logEntry = new ManualLogEntry( 'tag', 'update' ); - $logEntry->setPerformer( $user ); - $logEntry->setComment( $reason ); - - // find the appropriate target page - if ( $rev_id ) { - $rev = Revision::newFromId( $rev_id ); - if ( $rev ) { - $title = $rev->getTitle(); - $logEntry->setTarget( $rev->getTitle() ); - } - } elseif ( $log_id ) { - // This function is from revision deletion logic and has nothing to do with - // change tags, but it appears to be the only other place in core where we - // perform logged actions on log items. - $logEntry->setTarget( RevDelLogList::suggestTarget( 0, array( $log_id ) ) ); - } - - if ( !$logEntry->getTarget() ) { - // target is required, so we have to set something - $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) ); - } - - $logParams = array( - '4::revid' => $rev_id, - '5::logid' => $log_id, - '6:list:tagsAdded' => $tagsAdded, - '7:number:tagsAddedCount' => count( $tagsAdded ), - '8:list:tagsRemoved' => $tagsRemoved, - '9:number:tagsRemovedCount' => count( $tagsRemoved ), - 'initialTags' => $initialTags, - ); - $logEntry->setParameters( $logParams ); - $logEntry->setRelations( array( 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ) ); - - $dbw = wfGetDB( DB_MASTER ); - $logId = $logEntry->insert( $dbw ); - // Only send this to UDP, not RC, similar to patrol events - $logEntry->publish( $logId, 'udp' ); - - return Status::newGood( (object)array( - 'logId' => $logId, - 'addedTags' => $tagsAdded, - 'removedTags' => $tagsRemoved, - ) ); - } - - /** - * Applies all tags-related changes to a query. - * Handles selecting tags, and filtering. - * Needs $tables to be set up properly, so we can figure out which join conditions to use. - * - * @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 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 - */ - public static function modifyDisplayQuery( &$tables, &$fields, &$conds, - &$join_conds, &$options, $filter_tag = false ) { - global $wgRequest, $wgUseTagFilter; - - if ( $filter_tag === false ) { - $filter_tag = $wgRequest->getVal( 'tagfilter' ); - } - - // Figure out which conditions can be done. - if ( in_array( 'recentchanges', $tables ) ) { - $join_cond = 'ct_rc_id=rc_id'; - } elseif ( in_array( 'logging', $tables ) ) { - $join_cond = 'ct_log_id=log_id'; - } elseif ( in_array( 'revision', $tables ) ) { - $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.' ); - } - - $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 - - $tables[] = 'change_tag'; - $join_conds['change_tag'] = array( 'INNER JOIN', $join_cond ); - $conds['ct_tag'] = $filter_tag; - } - } - - /** - * Build a text box to select a change tag - * - * @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 Title object to send the form to. - * Used when, and only when $fullForm is true. - * @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 - ) { - 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 mw-ui-input mw-ui-input-inline', '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" . Html::hidden( 'title', $title->getPrefixedText() ); - $html = Xml::tags( - 'form', - array( 'action' => $title->getLocalURL(), 'class' => 'mw-tagfilter-form', 'method' => 'get' ), - $html - ); - - return $html; - } - - /** - * Defines a tag in the valid_tag table, without checking that the tag name - * is valid. - * Extensions should NOT use this function; they can use the ListDefinedTags - * hook instead. - * - * @param string $tag Tag to create - * @since 1.25 - */ - public static function defineTag( $tag ) { - $dbw = wfGetDB( DB_MASTER ); - $dbw->replace( 'valid_tag', - array( 'vt_tag' ), - array( 'vt_tag' => $tag ), - __METHOD__ ); - - // clear the memcache of defined tags - self::purgeTagCacheAll(); - } - - /** - * Removes a tag from the valid_tag table. The tag may remain in use by - * extensions, and may still show up as 'defined' if an extension is setting - * it from the ListDefinedTags hook. - * - * @param string $tag Tag to remove - * @since 1.25 - */ - public static function undefineTag( $tag ) { - $dbw = wfGetDB( DB_MASTER ); - $dbw->delete( 'valid_tag', array( 'vt_tag' => $tag ), __METHOD__ ); - - // clear the memcache of defined tags - self::purgeTagCacheAll(); - } - - /** - * Writes a tag action into the tag management log. - * - * @param string $action - * @param string $tag - * @param string $reason - * @param User $user Who to attribute the action to - * @param int $tagCount For deletion only, how many usages the tag had before - * it was deleted. - * @since 1.25 - */ - protected static function logTagManagementAction( $action, $tag, $reason, - User $user, $tagCount = null ) { - - $dbw = wfGetDB( DB_MASTER ); - - $logEntry = new ManualLogEntry( 'managetags', $action ); - $logEntry->setPerformer( $user ); - // target page is not relevant, but it has to be set, so we just put in - // the title of Special:Tags - $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) ); - $logEntry->setComment( $reason ); - - $params = array( '4::tag' => $tag ); - if ( !is_null( $tagCount ) ) { - $params['5:number:count'] = $tagCount; - } - $logEntry->setParameters( $params ); - $logEntry->setRelations( array( 'Tag' => $tag ) ); - - $logId = $logEntry->insert( $dbw ); - $logEntry->publish( $logId ); - return $logId; - } - - /** - * Is it OK to allow the user to activate this tag? - * - * @param string $tag Tag that you are interested in activating - * @param User|null $user User whose permission you wish to check, or null if - * you don't care (e.g. maintenance scripts) - * @return Status - * @since 1.25 - */ - public static function canActivateTag( $tag, User $user = null ) { - if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) { - return Status::newFatal( 'tags-manage-no-permission' ); - } - - // non-existing tags cannot be activated - $tagUsage = self::tagUsageStatistics(); - if ( !isset( $tagUsage[$tag] ) ) { - return Status::newFatal( 'tags-activate-not-found', $tag ); - } - - // defined tags cannot be activated (a defined tag is either extension- - // defined, in which case the extension chooses whether or not to active it; - // or user-defined, in which case it is considered active) - $definedTags = self::listDefinedTags(); - if ( in_array( $tag, $definedTags ) ) { - return Status::newFatal( 'tags-activate-not-allowed', $tag ); - } - - return Status::newGood(); - } - - /** - * Activates a tag, checking whether it is allowed first, and adding a log - * entry afterwards. - * - * Includes a call to ChangeTag::canActivateTag(), so your code doesn't need - * to do that. - * - * @param string $tag - * @param string $reason - * @param User $user Who to give credit for the action - * @param bool $ignoreWarnings Can be used for API interaction, default false - * @return Status If successful, the Status contains the ID of the added log - * entry as its value - * @since 1.25 - */ - public static function activateTagWithChecks( $tag, $reason, User $user, - $ignoreWarnings = false ) { - - // are we allowed to do this? - $result = self::canActivateTag( $tag, $user ); - if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) { - $result->value = null; - return $result; - } - - // do it! - self::defineTag( $tag ); - - // log it - $logId = self::logTagManagementAction( 'activate', $tag, $reason, $user ); - return Status::newGood( $logId ); - } - - /** - * Is it OK to allow the user to deactivate this tag? - * - * @param string $tag Tag that you are interested in deactivating - * @param User|null $user User whose permission you wish to check, or null if - * you don't care (e.g. maintenance scripts) - * @return Status - * @since 1.25 - */ - public static function canDeactivateTag( $tag, User $user = null ) { - if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) { - return Status::newFatal( 'tags-manage-no-permission' ); - } - - // only explicitly-defined tags can be deactivated - $explicitlyDefinedTags = self::listExplicitlyDefinedTags(); - if ( !in_array( $tag, $explicitlyDefinedTags ) ) { - return Status::newFatal( 'tags-deactivate-not-allowed', $tag ); - } - return Status::newGood(); - } - - /** - * Deactivates a tag, checking whether it is allowed first, and adding a log - * entry afterwards. - * - * Includes a call to ChangeTag::canDeactivateTag(), so your code doesn't need - * to do that. - * - * @param string $tag - * @param string $reason - * @param User $user Who to give credit for the action - * @param bool $ignoreWarnings Can be used for API interaction, default false - * @return Status If successful, the Status contains the ID of the added log - * entry as its value - * @since 1.25 - */ - public static function deactivateTagWithChecks( $tag, $reason, User $user, - $ignoreWarnings = false ) { - - // are we allowed to do this? - $result = self::canDeactivateTag( $tag, $user ); - if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) { - $result->value = null; - return $result; - } - - // do it! - self::undefineTag( $tag ); - - // log it - $logId = self::logTagManagementAction( 'deactivate', $tag, $reason, $user ); - return Status::newGood( $logId ); - } - - /** - * Is it OK to allow the user to create this tag? - * - * @param string $tag Tag that you are interested in creating - * @param User|null $user User whose permission you wish to check, or null if - * you don't care (e.g. maintenance scripts) - * @return Status - * @since 1.25 - */ - public static function canCreateTag( $tag, User $user = null ) { - if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) { - return Status::newFatal( 'tags-manage-no-permission' ); - } - - // no empty tags - if ( $tag === '' ) { - return Status::newFatal( 'tags-create-no-name' ); - } - - // tags cannot contain commas (used as a delimiter in tag_summary table) or - // slashes (would break tag description messages in MediaWiki namespace) - if ( strpos( $tag, ',' ) !== false || strpos( $tag, '/' ) !== false ) { - return Status::newFatal( 'tags-create-invalid-chars' ); - } - - // could the MediaWiki namespace description messages be created? - $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" ); - if ( is_null( $title ) ) { - return Status::newFatal( 'tags-create-invalid-title-chars' ); - } - - // does the tag already exist? - $tagUsage = self::tagUsageStatistics(); - if ( isset( $tagUsage[$tag] ) ) { - return Status::newFatal( 'tags-create-already-exists', $tag ); - } - - // check with hooks - $canCreateResult = Status::newGood(); - Hooks::run( 'ChangeTagCanCreate', array( $tag, $user, &$canCreateResult ) ); - return $canCreateResult; - } - - /** - * Creates a tag by adding a row to the `valid_tag` table. - * - * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to - * do that. - * - * @param string $tag - * @param string $reason - * @param User $user Who to give credit for the action - * @param bool $ignoreWarnings Can be used for API interaction, default false - * @return Status If successful, the Status contains the ID of the added log - * entry as its value - * @since 1.25 - */ - public static function createTagWithChecks( $tag, $reason, User $user, - $ignoreWarnings = false ) { - - // are we allowed to do this? - $result = self::canCreateTag( $tag, $user ); - if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) { - $result->value = null; - return $result; - } - - // do it! - self::defineTag( $tag ); - - // log it - $logId = self::logTagManagementAction( 'create', $tag, $reason, $user ); - return Status::newGood( $logId ); - } - - /** - * Permanently removes all traces of a tag from the DB. Good for removing - * misspelt or temporary tags. - * - * This function should be directly called by maintenance scripts only, never - * by user-facing code. See deleteTagWithChecks() for functionality that can - * safely be exposed to users. - * - * @param string $tag Tag to remove - * @return Status The returned status will be good unless a hook changed it - * @since 1.25 - */ - public static function deleteTagEverywhere( $tag ) { - $dbw = wfGetDB( DB_MASTER ); - $dbw->begin( __METHOD__ ); - - // delete from valid_tag - self::undefineTag( $tag ); - - // find out which revisions use this tag, so we can delete from tag_summary - $result = $dbw->select( 'change_tag', - array( 'ct_rc_id', 'ct_log_id', 'ct_rev_id', 'ct_tag' ), - array( 'ct_tag' => $tag ), - __METHOD__ ); - foreach ( $result as $row ) { - // remove the tag from the relevant row of tag_summary - $tagsToAdd = array(); - $tagsToRemove = array( $tag ); - self::updateTagSummaryRow( $tagsToAdd, $tagsToRemove, $row->ct_rc_id, - $row->ct_rev_id, $row->ct_log_id ); - } - - // delete from change_tag - $dbw->delete( 'change_tag', array( 'ct_tag' => $tag ), __METHOD__ ); - - $dbw->commit( __METHOD__ ); - - // give extensions a chance - $status = Status::newGood(); - Hooks::run( 'ChangeTagAfterDelete', array( $tag, &$status ) ); - // let's not allow error results, as the actual tag deletion succeeded - if ( !$status->isOK() ) { - wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' ); - $status->ok = true; - } - - // clear the memcache of defined tags - self::purgeTagCacheAll(); - - return $status; - } - - /** - * Is it OK to allow the user to delete this tag? - * - * @param string $tag Tag that you are interested in deleting - * @param User|null $user User whose permission you wish to check, or null if - * you don't care (e.g. maintenance scripts) - * @return Status - * @since 1.25 - */ - public static function canDeleteTag( $tag, User $user = null ) { - $tagUsage = self::tagUsageStatistics(); - - if ( !is_null( $user ) && !$user->isAllowed( 'managechangetags' ) ) { - return Status::newFatal( 'tags-manage-no-permission' ); - } - - if ( !isset( $tagUsage[$tag] ) ) { - return Status::newFatal( 'tags-delete-not-found', $tag ); - } - - if ( $tagUsage[$tag] > self::MAX_DELETE_USES ) { - return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES ); - } - - $extensionDefined = self::listExtensionDefinedTags(); - if ( in_array( $tag, $extensionDefined ) ) { - // extension-defined tags can't be deleted unless the extension - // specifically allows it - $status = Status::newFatal( 'tags-delete-not-allowed' ); - } else { - // user-defined tags are deletable unless otherwise specified - $status = Status::newGood(); - } - - Hooks::run( 'ChangeTagCanDelete', array( $tag, $user, &$status ) ); - return $status; - } - - /** - * Deletes a tag, checking whether it is allowed first, and adding a log entry - * afterwards. - * - * Includes a call to ChangeTag::canDeleteTag(), so your code doesn't need to - * do that. - * - * @param string $tag - * @param string $reason - * @param User $user Who to give credit for the action - * @param bool $ignoreWarnings Can be used for API interaction, default false - * @return Status If successful, the Status contains the ID of the added log - * entry as its value - * @since 1.25 - */ - public static function deleteTagWithChecks( $tag, $reason, User $user, - $ignoreWarnings = false ) { - - // are we allowed to do this? - $result = self::canDeleteTag( $tag, $user ); - if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) { - $result->value = null; - return $result; - } - - // store the tag usage statistics - $tagUsage = self::tagUsageStatistics(); - - // do it! - $deleteResult = self::deleteTagEverywhere( $tag ); - if ( !$deleteResult->isOK() ) { - return $deleteResult; - } - - // log it - $logId = self::logTagManagementAction( 'delete', $tag, $reason, $user, $tagUsage[$tag] ); - $deleteResult->value = $logId; - return $deleteResult; - } - - /** - * Lists those tags which extensions report as being "active". - * - * @return array - * @since 1.25 - */ - public static function listExtensionActivatedTags() { - // Caching... - global $wgMemc; - $key = wfMemcKey( 'active-tags' ); - $tags = $wgMemc->get( $key ); - if ( $tags ) { - return $tags; - } - - // ask extensions which tags they consider active - $extensionActive = array(); - Hooks::run( 'ChangeTagsListActive', array( &$extensionActive ) ); - - // Short-term caching. - $wgMemc->set( $key, $extensionActive, 300 ); - return $extensionActive; - } - - /** - * Basically lists defined tags which count even if they aren't applied to anything. - * It returns a union of the results of listExplicitlyDefinedTags() and - * listExtensionDefinedTags(). - * - * @return string[] Array of strings: tags - */ - public static function listDefinedTags() { - $tags1 = self::listExplicitlyDefinedTags(); - $tags2 = self::listExtensionDefinedTags(); - return array_values( array_unique( array_merge( $tags1, $tags2 ) ) ); - } - - /** - * Lists tags explicitly defined in the `valid_tag` table of the database. - * Tags in table 'change_tag' which are not in table 'valid_tag' are not - * included. - * - * Tries memcached first. - * - * @return string[] Array of strings: tags - * @since 1.25 - */ - public static function listExplicitlyDefinedTags() { - // Caching... - global $wgMemc; - $key = wfMemcKey( 'valid-tags-db' ); - $tags = $wgMemc->get( $key ); - if ( $tags ) { - return $tags; - } - - $emptyTags = array(); - - // Some DB stuff - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'valid_tag', 'vt_tag', array(), __METHOD__ ); - foreach ( $res as $row ) { - $emptyTags[] = $row->vt_tag; - } - - $emptyTags = array_filter( array_unique( $emptyTags ) ); - - // Short-term caching. - $wgMemc->set( $key, $emptyTags, 300 ); - return $emptyTags; - } - - /** - * Lists tags defined by extensions using the ListDefinedTags hook. - * Extensions need only define those tags they deem to be in active use. - * - * Tries memcached first. - * - * @return string[] Array of strings: tags - * @since 1.25 - */ - public static function listExtensionDefinedTags() { - // Caching... - global $wgMemc; - $key = wfMemcKey( 'valid-tags-hook' ); - $tags = $wgMemc->get( $key ); - if ( $tags ) { - return $tags; - } - - $emptyTags = array(); - Hooks::run( 'ListDefinedTags', array( &$emptyTags ) ); - $emptyTags = array_filter( array_unique( $emptyTags ) ); - - // Short-term caching. - $wgMemc->set( $key, $emptyTags, 300 ); - return $emptyTags; - } - - /** - * Invalidates the short-term cache of defined tags used by the - * list*DefinedTags functions, as well as the tag statistics cache. - * @since 1.25 - */ - public static function purgeTagCacheAll() { - global $wgMemc; - $wgMemc->delete( wfMemcKey( 'active-tags' ) ); - $wgMemc->delete( wfMemcKey( 'valid-tags-db' ) ); - $wgMemc->delete( wfMemcKey( 'valid-tags-hook' ) ); - self::purgeTagUsageCache(); - } - - /** - * Invalidates the tag statistics cache only. - * @since 1.25 - */ - public static function purgeTagUsageCache() { - global $wgMemc; - $wgMemc->delete( wfMemcKey( 'change-tag-statistics' ) ); - } - - /** - * Returns a map of any tags used on the wiki to number of edits - * tagged with them, ordered descending by the hitcount. - * - * Keeps a short-term cache in memory, so calling this multiple times in the - * same request should be fine. - * - * @return array Array of string => int - */ - public static function tagUsageStatistics() { - // Caching... - global $wgMemc; - $key = wfMemcKey( 'change-tag-statistics' ); - $stats = $wgMemc->get( $key ); - if ( $stats ) { - return $stats; - } - - $out = array(); - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( - 'change_tag', - array( 'ct_tag', 'hitcount' => 'count(*)' ), - array(), - __METHOD__, - array( 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' ) - ); - - foreach ( $res as $row ) { - $out[$row->ct_tag] = $row->hitcount; - } - foreach ( self::listDefinedTags() as $tag ) { - if ( !isset( $out[$tag] ) ) { - $out[$tag] = 0; - } - } - - // Cache for a very short time - $wgMemc->set( $key, $out, 300 ); - return $out; - } - - /** - * Indicate whether change tag editing UI is relevant - * - * Returns true if the user has the necessary right and there are any - * editable tags defined. - * - * This intentionally doesn't check "any addable || any deletable", because - * it seems like it would be more confusing than useful if the checkboxes - * suddenly showed up because some abuse filter stopped defining a tag and - * then suddenly disappeared when someone deleted all uses of that tag. - * - * @param User $user - * @return bool - */ - public static function showTagEditingUI( User $user ) { - return $user->isAllowed( 'changetags' ) && (bool)self::listExplicitlyDefinedTags(); - } -} diff --git a/includes/Collation.php b/includes/Collation.php index 481d8e70..c1f0b388 100644 --- a/includes/Collation.php +++ b/includes/Collation.php @@ -320,16 +320,16 @@ class IcuCollation extends Collation { // intl extension produces non null-terminated // strings. Appending '' fixes it so that it doesn't generate // a warning on each access in debug php. - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $key = $this->mainCollator->getSortKey( $string ) . ''; - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); return $key; } function getPrimarySortKey( $string ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $key = $this->primaryCollator->getSortKey( $string ) . ''; - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); return $key; } diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index c13aa5f4..268a8d19 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -75,13 +75,21 @@ $wgConfigRegistry = array( * MediaWiki version number * @since 1.2 */ -$wgVersion = '1.25.3'; +$wgVersion = '1.26.0'; /** * Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; +/** + * When the wiki is running behind a proxy and this is set to true, assumes that the proxy exposes + * the wiki on the standard ports (443 for https and 80 for http). + * @var bool + * @since 1.26 + */ +$wgAssumeProxiesUseDefaultProtocolPorts = true; + /** * URL of the server. * @@ -203,7 +211,7 @@ $wgLoadScript = false; /** * The URL path of the skins directory. - * Defaults to "{$wgScriptPath}/skins". + * Defaults to "{$wgResourceBasePath}/skins". * @since 1.3 */ $wgStylePath = false; @@ -218,7 +226,7 @@ $wgLocalStylePath = false; /** * The URL path of the extensions directory. - * Defaults to "{$wgScriptPath}/extensions". + * Defaults to "{$wgResourceBasePath}/extensions". * @since 1.16 */ $wgExtensionAssetsPath = false; @@ -472,13 +480,13 @@ $wgImgAuthUrlPathMap = array(); * * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored * for local repositories: - * - descBaseUrl URL of image description pages, e.g. http://en.wikipedia.org/wiki/File: + * - descBaseUrl URL of image description pages, e.g. https://en.wikipedia.org/wiki/File: * - scriptDirUrl URL of the MediaWiki installation, equivalent to $wgScriptPath, e.g. - * http://en.wikipedia.org/w + * https://en.wikipedia.org/w * - scriptExtension Script extension of the MediaWiki installation, equivalent to * $wgScriptExtension, e.g. .php5 defaults to .php * - * - articleUrl Equivalent to $wgArticlePath, e.g. http://en.wikipedia.org/wiki/$1 + * - articleUrl Equivalent to $wgArticlePath, e.g. https://en.wikipedia.org/wiki/$1 * - fetchDescription Fetch the text of the remote file description page. Equivalent to * $wgFetchCommonsDescriptions. * - abbrvThreshold File names over this size will use the short form of thumbnail names. @@ -517,6 +525,16 @@ $wgForeignFileRepos = array(); */ $wgUseInstantCommons = false; +/** + * Array of foreign file repo names (set in $wgForeignFileRepos above) that + * are allowable upload targets. These wikis must have some method of + * authentication (i.e. CentralAuth), and be CORS-enabled for this wiki. + * + * Example: + * $wgForeignUploadTargets = array( 'shared' ); + */ +$wgForeignUploadTargets = array(); + /** * File backend structure configuration. * @@ -717,7 +735,7 @@ $wgMinUploadChunkSize = 1024; # 1KB * * @par Example: * @code - * $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload'; + * $wgUploadNavigationUrl = 'https://commons.wikimedia.org/wiki/Special:Upload'; * @endcode */ $wgUploadNavigationUrl = false; @@ -777,7 +795,7 @@ $wgHashedSharedUploadDirectory = true; * * Please specify the namespace, as in the example below. */ -$wgRepositoryBaseUrl = "http://commons.wikimedia.org/wiki/File:"; +$wgRepositoryBaseUrl = "https://commons.wikimedia.org/wiki/File:"; /** * This is the list of preferred extensions for uploading files. Uploading files @@ -884,6 +902,7 @@ $wgMediaHandlers = array( 'image/png' => 'PNGHandler', 'image/gif' => 'GIFHandler', 'image/tiff' => 'TiffHandler', + 'image/webp' => 'WebPHandler', 'image/x-ms-bmp' => 'BmpHandler', 'image/x-bmp' => 'BmpHandler', 'image/x-xcf' => 'XCFHandler', @@ -978,6 +997,14 @@ $wgJpegTran = '/usr/bin/jpegtran'; */ $wgExiv2Command = '/usr/bin/exiv2'; + +/** + * Path to exiftool binary. Used for lossless ICC profile swapping. + * + * @since 1.26 + */ +$wgExiftool = '/usr/bin/exiftool'; + /** * Scalable Vector Graphics (SVG) may be uploaded as images. * Since SVG support is not yet standard in browsers, it is @@ -1012,7 +1039,7 @@ $wgSVGConverterPath = ''; /** * Don't scale a SVG larger than this */ -$wgSVGMaxSize = 2048; +$wgSVGMaxSize = 5120; /** * Don't read SVG metadata beyond this point. @@ -1330,6 +1357,14 @@ $wgUploadThumbnailRenderHttpCustomHost = false; */ $wgUploadThumbnailRenderHttpCustomDomain = false; +/** + * When this variable is true and JPGs use the sRGB ICC profile, swaps it for the more lightweight + * (and free) TinyRGB profile when generating thumbnails. + * + * @since 1.26 + */ +$wgUseTinyRGBForJPGThumbnails = false; + /** * Default parameters for the "" tag */ @@ -1580,7 +1615,8 @@ $wgEnotifRevealEditorAddress = false; /** * Send notification mails on minor edits to watchlist pages. This is enabled - * by default. Does not affect user talk notifications. + * by default. User talk notifications are affected by this, $wgEnotifUserTalk, and + * the nominornewtalk user right. */ $wgEnotifMinorEdits = true; @@ -1842,12 +1878,6 @@ $wgDBservers = false; */ $wgLBFactoryConf = array( 'class' => 'LBFactorySimple' ); -/** - * How long to wait for a slave to catch up to the master - * @deprecated since 1.24 - */ -$wgMasterWaitTimeout = 10; - /** * File to log database errors to */ @@ -1862,24 +1892,17 @@ $wgDBerrorLog = false; * * @par Examples: * @code - * $wgLocaltimezone = 'UTC'; - * $wgLocaltimezone = 'GMT'; - * $wgLocaltimezone = 'PST8PDT'; - * $wgLocaltimezone = 'Europe/Sweden'; - * $wgLocaltimezone = 'CET'; + * $wgDBerrorLogTZ = 'UTC'; + * $wgDBerrorLogTZ = 'GMT'; + * $wgDBerrorLogTZ = 'PST8PDT'; + * $wgDBerrorLogTZ = 'Europe/Sweden'; + * $wgDBerrorLogTZ = 'CET'; * @endcode * * @since 1.20 */ $wgDBerrorLogTZ = false; -/** - * Scale load balancer polling time so that under overload conditions, the - * database server receives a SHOW STATUS query at an average interval of this - * many microseconds - */ -$wgDBAvgStatusPoll = 2000; - /** * Set to true to engage MySQL 4.1/5.0 charset-related features; * for now will just cause sending of 'SET NAMES=utf8' on connect. @@ -2067,6 +2090,14 @@ $wgMaxArticleSize = 2048; */ $wgMemoryLimit = "50M"; +/** + * The minimum amount of time that MediaWiki needs for "slow" write request, + * particularly ones with multiple non-atomic writes that *should* be as + * transactional as possible; MediaWiki will call set_time_limit() if needed. + * @since 1.26 + */ +$wgTransactionalTimeLimit = 120; + /** @} */ # end performance hacks } /************************************************************************//** @@ -2086,8 +2117,8 @@ $wgCacheDirectory = false; /** * Main cache type. This should be a cache with fast access, but it may have - * limited space. By default, it is disabled, since the database is not fast - * enough to make it worthwhile. + * limited space. By default, it is disabled, since the stock database cache + * is not fast enough to make it worthwhile. * * The options are: * @@ -2157,6 +2188,19 @@ $wgObjectCaches = array( CACHE_ACCEL => array( 'factory' => 'ObjectCache::newAccelerator' ), CACHE_MEMCACHED => array( 'factory' => 'ObjectCache::newMemcached', 'loggroup' => 'memcached' ), + 'db-replicated' => array( + 'class' => 'ReplicatedBagOStuff', + 'readFactory' => array( + 'class' => 'SqlBagOStuff', + 'args' => array( array( 'slaveOnly' => true ) ) + ), + 'writeFactory' => array( + 'class' => 'SqlBagOStuff', + 'args' => array( array( 'slaveOnly' => false ) ) + ), + 'loggroup' => 'SQLBagOStuff' + ), + 'apc' => array( 'class' => 'APCBagOStuff' ), 'xcache' => array( 'class' => 'XCacheBagOStuff' ), 'wincache' => array( 'class' => 'WinCacheBagOStuff' ), @@ -2165,6 +2209,71 @@ $wgObjectCaches = array( 'hash' => array( 'class' => 'HashBagOStuff' ), ); +/** + * Main Wide-Area-Network cache type. This should be a cache with fast access, + * but it may have limited space. By default, it is disabled, since the basic stock + * cache is not fast enough to make it worthwhile. For single data-center setups, this can + * simply be pointed to a cache in $wgWANObjectCaches that uses a local $wgObjectCaches + * cache with a relayer of type EventRelayerNull. + * + * The options are: + * - false: Configure the cache using $wgMainCacheType, without using + * a relayer (only matters if there are multiple data-centers) + * - CACHE_NONE: Do not cache + * - (other): A string may be used which identifies a cache + * configuration in $wgWANObjectCaches + * @since 1.26 + */ +$wgMainWANCache = false; + +/** + * Advanced WAN object cache configuration. + * + * Each WAN cache wraps a registered object cache (for the local cluster) + * and it must also be configured to point to a PubSub instance. Subscribers + * must be configured to relay purges to the actual cache servers. + * + * The format is an associative array where the key is a cache identifier, and + * the value is an associative array of parameters. The "cacheId" parameter is + * a cache identifier from $wgObjectCaches. The "relayerConfig" parameter is an + * array used to construct an EventRelayer object. The "pool" parameter is a + * string that is used as a PubSub channel prefix. + * + * @since 1.26 + */ +$wgWANObjectCaches = array( + CACHE_NONE => array( + 'class' => 'WANObjectCache', + 'cacheId' => CACHE_NONE, + 'pool' => 'mediawiki-main-none', + 'relayerConfig' => array( 'class' => 'EventRelayerNull' ) + ) + /* Example of a simple single data-center cache: + 'memcached-php' => array( + 'class' => 'WANObjectCache', + 'cacheId' => 'memcached-php', + 'pool' => 'mediawiki-main-memcached', + 'relayerConfig' => array( 'class' => 'EventRelayerNull' ) + ) + */ +); + +/** + * Main object stash type. This should be a fast storage system for storing + * lightweight data like hit counters and user activity. Sites with multiple + * data-centers should have this use a store that replicates all writes. The + * store should have enough consistency for CAS operations to be usable. + * Reads outside of those needed for merge() may be eventually consistent. + * + * The options are: + * - db: Store cache objects in the DB + * - (other): A string may be used which identifies a cache + * configuration in $wgObjectCaches + * + * @since 1.26 + */ +$wgMainStash = 'db-replicated'; + /** * The expiry time for the parser cache, in seconds. * The default is 86400 (one day). @@ -2239,11 +2348,13 @@ $wgAdaptiveMessageCache = false; * Localisation cache configuration. Associative array with keys: * class: The class to use. May be overridden by extensions. * - * store: The location to store cache data. May be 'files', 'db' or + * store: The location to store cache data. May be 'files', 'array', 'db' or * 'detect'. If set to "files", data will be in CDB files. If set * to "db", data will be stored to the database. If set to * "detect", files will be used if $wgCacheDirectory is set, * otherwise the database will be used. + * "array" is an experimental option that uses PHP files that + * store static arrays. * * storeClass: The class name for the underlying storage. If set to a class * name, it overrides the "store" setting. @@ -2311,13 +2422,8 @@ $wgUseFileCache = false; $wgFileCacheDepth = 2; /** - * Keep parsed pages in a cache (objectcache table or memcached) - * to speed up output of the same page viewed by another user with the - * same options. - * - * This can provide a significant speedup for medium to large pages, - * so you probably want to keep it on. Extensions that conflict with the - * parser cache should disable the cache on a per-page basis instead. + * Kept for extension compatibility; see $wgParserCacheType + * @deprecated 1.26 */ $wgEnableParserCache = true; @@ -2447,13 +2553,16 @@ $wgInternalServer = false; /** * Cache timeout for the squid, will be sent as s-maxage (without ESI) or * Surrogate-Control (with ESI). Without ESI, you should strip out s-maxage in - * the Squid config. 18000 seconds = 5 hours, more cache hits with 2678400 = 31 - * days + * the Squid config. + * +* 18000 seconds = 5 hours, more cache hits with 2678400 = 31 days. */ $wgSquidMaxage = 18000; /** * Default maximum age for raw CSS/JS accesses + * + * 300 seconds = 5 minutes. */ $wgForcedRawSMaxage = 300; @@ -2742,14 +2851,14 @@ $wgBrowserBlackList = array( * - Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC) * - [...] * - * @link http://en.wikipedia.org/w/index.php?diff=12356041&oldid=12355864 - * @link http://en.wikipedia.org/wiki/Template%3AOS9 + * @link https://en.wikipedia.org/w/index.php?diff=12356041&oldid=12355864 + * @link https://en.wikipedia.org/wiki/Template%3AOS9 */ '/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/', /** * Google wireless transcoder, seems to eat a lot of chars alive - * http://it.wikipedia.org/w/index.php?title=Luciano_Ligabue&diff=prev&oldid=8857361 + * https://it.wikipedia.org/w/index.php?title=Luciano_Ligabue&diff=prev&oldid=8857361 */ '/^Mozilla\/4\.0 \(compatible; MSIE 6.0; Windows NT 5.0; Google Wireless Transcoder;\)/' ); @@ -3382,8 +3491,8 @@ $wgResourceModuleSkinStyles = array(); $wgResourceLoaderSources = array(); /** - * Default 'remoteBasePath' value for instances of ResourceLoaderFileModule. - * If not set, then $wgScriptPath will be used as a fallback. + * The default 'remoteBasePath' value for instances of ResourceLoaderFileModule. + * Defaults to $wgScriptPath. */ $wgResourceBasePath = null; @@ -3421,13 +3530,6 @@ $wgResourceLoaderMaxage = array( */ $wgResourceLoaderDebug = false; -/** - * Enable embedding of certain resources using Edge Side Includes. This will - * improve performance but only works if there is something in front of the - * web server (e..g a Squid or Varnish server) configured to process the ESI. - */ -$wgResourceLoaderUseESI = false; - /** * Put each statement on its own line when minifying JavaScript. This makes * debugging in non-debug mode a bit easier. @@ -3442,28 +3544,27 @@ $wgResourceLoaderMinifierStatementsOnOwnLine = false; $wgResourceLoaderMinifierMaxLineLength = 1000; /** - * Whether to include the mediawiki.legacy JS library (old wikibits.js), and its - * dependencies. + * Whether to ensure the mediawiki.legacy library is loaded before other modules. + * + * @deprecated since 1.26: Always declare dependencies. */ $wgIncludeLegacyJavaScript = true; /** - * Whether to preload the mediawiki.util module as blocking module in the top - * queue. + * Whether to ensure the mediawiki.util is loaded before other modules. * - * Before MediaWiki 1.19, modules used to load slower/less asynchronous which - * allowed modules to lack dependencies on 'popular' modules that were likely - * loaded already. + * Before MediaWiki 1.19, modules used to load less asynchronous which allowed + * modules to lack dependencies on 'popular' modules that were likely loaded already. * * This setting is to aid scripts during migration by providing mediawiki.util - * unconditionally (which was the most commonly missed dependency). - * It doesn't cover all missing dependencies obviously but should fix most of - * them. + * unconditionally (which was the most commonly missed dependency). It doesn't + * cover all missing dependencies obviously but should fix most of them. * * This should be removed at some point after site/user scripts have been fixed. * Enable this if your wiki has a large amount of user/site scripts that are * lacking dependencies. - * @todo Deprecate + * + * @deprecated since 1.26: Always declare dependencies. */ $wgPreloadJavaScriptMwUtil = false; @@ -3528,13 +3629,6 @@ $wgResourceLoaderValidateJS = true; */ $wgResourceLoaderValidateStaticJS = false; -/** - * If set to true, asynchronous loading of bottom-queue scripts in the "" - * will be enabled. This is an experimental feature that's supposed to make - * JavaScript load faster. - */ -$wgResourceLoaderExperimentalAsyncLoading = false; - /** * Global LESS variables. An associative array binding variable names to * LESS code snippets representing their values. @@ -3560,18 +3654,6 @@ $wgResourceLoaderExperimentalAsyncLoading = false; */ $wgResourceLoaderLESSVars = array(); -/** - * Custom LESS functions. An associative array mapping function name to PHP - * callable. - * - * 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(); - /** * Default import paths for LESS modules. LESS files referenced in @import * statements will be looked up here first, and relative to the importing file @@ -3880,6 +3962,15 @@ $wgTrackingCategories = array(); */ $wgContentNamespaces = array( NS_MAIN ); +/** + * Array of namespaces, in addition to the talk namespaces, where signatures + * (~~~~) are likely to be used. This determines whether to display the + * Signature button on the edit toolbar, and may also be used by extensions. + * For example, "traditional" style wikis, where content and discussion are + * intermixed, could place NS_MAIN and NS_PROJECT namespaces in this array. + */ +$wgExtraSignatureNamespaces = array(); + /** * Max number of redirects to follow when resolving redirects. * 1 means only the first redirect is followed (default behavior). @@ -4031,44 +4122,55 @@ $wgEnableImageWhitelist = true; $wgAllowImageTag = false; /** - * $wgUseTidy: use tidy to make sure HTML output is sane. - * Tidy is a free tool that fixes broken HTML. - * See http://www.w3.org/People/Raggett/tidy/ + * Configuration for HTML postprocessing tool. Set this to a configuration + * array to enable an external tool. Dave Raggett's "HTML Tidy" is typically + * used. See http://www.w3.org/People/Raggett/tidy/ * - * - $wgTidyBin should be set to the path of the binary and - * - $wgTidyConf to the path of the configuration file. - * - $wgTidyOpts can include any number of parameters. - * - $wgTidyInternal controls the use of the PECL extension or the - * libtidy (PHP >= 5) extension to use an in-process tidy library instead - * of spawning a separate program. - * Normally you shouldn't need to override the setting except for - * debugging. To install, use 'pear install tidy' and add a line - * 'extension=tidy.so' to php.ini. + * If this is null and $wgUseTidy is true, the deprecated configuration + * parameters will be used instead. + * + * If this is null and $wgUseTidy is false, a pure PHP fallback will be used. + * + * Keys are: + * - driver: May be: + * - RaggettInternalHHVM: Use the limited-functionality HHVM extension + * - RaggettInternalPHP: Use the PECL extension + * - RaggettExternal: Shell out to an external binary (tidyBin) + * + * - tidyConfigFile: Path to configuration file for any of the Raggett drivers + * - debugComment: True to add a comment to the output with warning messages + * - tidyBin: For RaggettExternal, the path to the tidy binary. + * - tidyCommandLine: For RaggettExternal, additional command line options. */ -$wgUseTidy = false; +$wgTidyConfig = null; /** - * @see $wgUseTidy + * Set this to true to use the deprecated tidy configuration parameters. + * @deprecated use $wgTidyConfig */ -$wgAlwaysUseTidy = false; +$wgUseTidy = false; /** - * @see $wgUseTidy + * The path to the tidy binary. + * @deprecated Use $wgTidyConfig['tidyBin'] */ $wgTidyBin = 'tidy'; /** - * @see $wgUseTidy + * The path to the tidy config file + * @deprecated Use $wgTidyConfig['tidyConfigFile'] */ -$wgTidyConf = $IP . '/includes/tidy.conf'; +$wgTidyConf = $IP . '/includes/tidy/tidy.conf'; /** - * @see $wgUseTidy + * The command line options to the tidy binary + * @deprecated Use $wgTidyConfig['tidyCommandLine'] */ $wgTidyOpts = ''; /** - * @see $wgUseTidy + * Set this to true to use the tidy extension + * @deprecated Use $wgTidyConfig['driver'] */ $wgTidyInternal = extension_loaded( 'tidy' ); @@ -4197,6 +4299,59 @@ $wgActiveUserDays = 30; * @{ */ +/** + * Password policy for local wiki users. A user's effective policy + * is the superset of all policy statements from the policies for the + * groups where the user is a member. If more than one group policy + * include the same policy statement, the value is the max() of the + * values. Note true > false. The 'default' policy group is required, + * and serves as the minimum policy for all users. New statements can + * be added by appending to $wgPasswordPolicy['checks']. + * Statements: + * - MinimalPasswordLength - minimum length a user can set + * - MinimumPasswordLengthToLogin - passwords shorter than this will + * not be allowed to login, regardless if it is correct. + * - MaximalPasswordLength - maximum length password a user is allowed + * to attempt. Prevents DoS attacks with pbkdf2. + * - PasswordCannotMatchUsername - Password cannot match username to + * - PasswordCannotMatchBlacklist - Username/password combination cannot + * match a specific, hardcoded blacklist. + * @since 1.26 + */ +$wgPasswordPolicy = array( + 'policies' => array( + 'bureaucrat' => array( + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + ), + 'sysop' => array( + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + ), + 'bot' => array( + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + ), + 'default' => array( + 'MinimalPasswordLength' => 1, + 'PasswordCannotMatchUsername' => true, + 'PasswordCannotMatchBlacklist' => true, + 'MaximalPasswordLength' => 4096, + ), + ), + 'checks' => array( + 'MinimalPasswordLength' => 'PasswordPolicyChecks::checkMinimalPasswordLength', + 'MinimumPasswordLengthToLogin' => 'PasswordPolicyChecks::checkMinimumPasswordLengthToLogin', + 'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername', + 'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist', + 'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength', + ), +); + + /** * For compatibility with old installations set to false * @deprecated since 1.24 will be removed in future @@ -4206,8 +4361,9 @@ $wgPasswordSalt = true; /** * Specifies the minimal length of a user password. If set to 0, empty pass- * words are allowed. + * @deprecated since 1.26, use $wgPasswordPolicy's MinimalPasswordLength. */ -$wgMinimalPasswordLength = 1; +$wgMinimalPasswordLength = false; /** * Specifies the maximal length of a user password (T64685). @@ -4218,8 +4374,9 @@ $wgMinimalPasswordLength = 1; * * @warning Unlike other password settings, user with passwords greater than * the maximum will not be able to log in. + * @deprecated since 1.26, use $wgPasswordPolicy's MaximalPasswordLength. */ -$wgMaximalPasswordLength = 4096; +$wgMaximalPasswordLength = false; /** * Specifies if users should be sent to a password-reset form on login, if their @@ -4295,7 +4452,7 @@ $wgPasswordConfig = array( */ $wgPasswordResetRoutes = array( 'username' => true, - 'email' => false, + 'email' => true, ); /** @@ -4322,6 +4479,7 @@ $wgReservedUsernames = array( 'msg:double-redirect-fixer', // Automatic double redirect fix 'msg:usermessage-editor', // Default user for leaving user messages 'msg:proxyblocker', // For $wgProxyList and Special:Blockme (removed in 1.22) + 'msg:spambot_username', // Used by cleanupSpam.php ); /** @@ -4397,7 +4555,7 @@ $wgHiddenPrefs = array(); * This is used in a regular expression character class during * registration (regex metacharacters like / are escaped). */ -$wgInvalidUsernameCharacters = '@'; +$wgInvalidUsernameCharacters = '@:'; /** * Character used as a delimiter when testing for interwiki userrights @@ -4413,7 +4571,7 @@ $wgUserrightsInterwikiDelimiter = '@'; /** * This is to let user authenticate using https when they come from http. * Based on an idea by George Herbert on wikitech-l: - * http://lists.wikimedia.org/pipermail/wikitech-l/2010-October/050039.html + * https://lists.wikimedia.org/pipermail/wikitech-l/2010-October/050039.html * @since 1.17 */ $wgSecureLogin = false; @@ -4433,7 +4591,7 @@ $wgAutoblockExpiry = 86400; /** * Set this to true to allow blocked users to edit their own user talk page. */ -$wgBlockAllowsUTEdit = false; +$wgBlockAllowsUTEdit = true; /** * Allow sysops to ban users from accessing Emailuser @@ -4940,7 +5098,7 @@ $wgAccountCreationThrottle = 0; * There's no administrator override on-wiki, so be careful what you set. :) * May be an array of regexes or a single string for backwards compatibility. * - * @see http://en.wikipedia.org/wiki/Regular_expression + * @see https://en.wikipedia.org/wiki/Regular_expression * * @note Each regex needs a beginning/end delimiter, eg: # or / */ @@ -5144,6 +5302,22 @@ $wgProxyList = array(); */ $wgCookieExpiration = 180 * 86400; +/** + * The identifiers of the login cookies that can have their lifetimes + * extended independently of all other login cookies. + * + * @var string[] + */ +$wgExtendedLoginCookies = array( 'UserID', 'Token' ); + +/** + * Default login cookie lifetime, in seconds. Setting + * $wgExtendLoginCookieExpiration to null will use $wgCookieExpiration to + * calculate the cookie lifetime. As with $wgCookieExpiration, 0 will make + * login cookies session-only. + */ +$wgExtendedLoginCookieExpiration = null; + /** * Set to set an explicit domain on the login cookies eg, "justthis.domain.org" * or ".any.subdomain.net" @@ -5281,6 +5455,36 @@ $wgDebugDumpSql = false; */ $wgDebugDumpSqlLength = 500; +/** + * Performance expectations for DB usage + * + * @since 1.26 + */ +$wgTrxProfilerLimits = array( + // Basic GET and POST requests + 'GET' => array( + 'masterConns' => 0, + 'writes' => 0, + 'readQueryTime' => 5 + ), + 'POST' => array( + 'readQueryTime' => 5, + 'writeQueryTime' => 1, + 'maxAffected' => 500 + ), + // Background job runner + 'JobRunner' => array( + 'readQueryTime' => 30, + 'writeQueryTime' => 5, + 'maxAffected' => 500 + ), + // Command-line scripts + 'Maintenance' => array( + 'writeQueryTime' => 5, + 'maxAffected' => 1000 + ) +); + /** * Map of string log group names to log destinations. * @@ -5447,7 +5651,7 @@ $wgProfilePerHost = null; * * The host should be running a daemon which can be obtained from MediaWiki * Git at: - * http://git.wikimedia.org/tree/operations%2Fsoftware.git/master/udpprofile + * https://git.wikimedia.org/tree/operations%2Fsoftware.git/master/udpprofile * * @deprecated set $wgProfiler['udphost'] instead */ @@ -5503,6 +5707,29 @@ $wgAggregateStatsID = false; */ $wgStatsFormatString = "stats/%s - %s 1 1 1 1 %s\n"; +/** + * Destination of statsd metrics. + * + * A host or host:port of a statsd server. Port defaults to 8125. + * + * If not set, statsd metrics will not be collected. + * + * @see wfLogProfilingData + * @since 1.25 + */ +$wgStatsdServer = false; + +/** + * Prefix for metric names sent to wgStatsdServer. + * + * Defaults to "MediaWiki". + * + * @see RequestContext::getStats + * @see BufferingStatsdDataFactory + * @since 1.25 + */ +$wgStatsdMetricPrefix = false; + /** * InfoAction retrieves a list of transclusion links (both to and from). * This number puts a limit on that query in the case of highly transcluded @@ -5835,6 +6062,21 @@ $wgGitRepositoryViewers = array( */ $wgRCMaxAge = 90 * 24 * 3600; +/** + * Page watchers inactive for more than this many seconds are considered inactive. + * Used mainly by action=info. Default: 180 days = about six months. + * @since 1.26 + */ +$wgWatchersMaxAge = 180 * 24 * 3600; + +/** + * If active watchers (per above) are this number or less, do not disclose it. + * Left to 1, prevents unprivileged users from knowing for sure that there are 0. + * Set to -1 if you want to always complement watchers count with this info. + * @since 1.26 + */ +$wgUnwatchedPageSecret = 1; + /** * Filter $wgRCLinkDays by $wgRCMaxAge to avoid showing links for numbers * higher than what will be stored. Note that this is disabled by default @@ -6453,6 +6695,7 @@ $wgJobClasses = array( 'ThumbnailRender' => 'ThumbnailRenderJob', 'recentChangesUpdate' => 'RecentChangesUpdateJob', 'refreshLinksPrioritized' => 'RefreshLinksJob', // for cascading protection + 'activityUpdateJob' => 'ActivityUpdateJob', 'enqueue' => 'EnqueueJob', // local queue for multi-DC setups 'null' => 'NullJob' ); @@ -6481,6 +6724,21 @@ $wgJobTypesExcludedFromDefaultQueue = array( 'AssembleUploadChunks', 'PublishSta */ $wgJobBackoffThrottling = array(); +/** + * Make job runners commit changes for slave-lag prone jobs one job at a time. + * This is useful if there are many job workers that race on slave lag checks. + * If set, jobs taking this many seconds of DB write time have serialized commits. + * + * Note that affected jobs may have worse lock contention. Also, if they affect + * several DBs at once they may have a smaller chance of being atomic due to the + * possibility of connection loss while queueing up to commit. Affected jobs may + * also fail due to the commit lock acquisition timeout. + * + * @var float|bool + * @since 1.26 + */ +$wgJobSerialCommitThreshold = false; + /** * Map of job types to configuration arrays. * This determines which queue class and storage system is used for each job type. @@ -6488,7 +6746,7 @@ $wgJobBackoffThrottling = array(); * These settings should be global to all wikis. */ $wgJobTypeConf = array( - 'default' => array( 'class' => 'JobQueueDB', 'order' => 'random' ), + 'default' => array( 'class' => 'JobQueueDB', 'order' => 'random', 'claimTTL' => 3600 ), ); /** @@ -6604,6 +6862,7 @@ $wgLogTypes = array( 'suppress', 'tag', 'managetags', + 'contentmodel', ); /** @@ -6679,15 +6938,15 @@ $wgLogNames = array( $wgLogHeaders = array( '' => 'alllogstext', 'block' => 'blocklogtext', - 'protect' => 'protectlogtext', - 'rights' => 'rightslogtext', 'delete' => 'dellogpagetext', - 'upload' => 'uploadlogpagetext', - 'move' => 'movelogpagetext', 'import' => 'importlogpagetext', - 'patrol' => 'patrol-log-header', 'merge' => 'mergelogpagetext', + 'move' => 'movelogpagetext', + 'patrol' => 'patrol-log-header', + 'protect' => 'protectlogtext', + 'rights' => 'rightslogtext', 'suppress' => 'suppressionlogtext', + 'upload' => 'uploadlogpagetext', ); /** @@ -6697,10 +6956,9 @@ $wgLogHeaders = array( * Extensions with custom log types may add to this array. */ $wgLogActions = array( - 'protect/protect' => 'protectedarticle', 'protect/modify' => 'modifiedarticleprotection', + 'protect/protect' => 'protectedarticle', 'protect/unprotect' => 'unprotectedarticle', - 'protect/move_prot' => 'movedarticleprotection', ); /** @@ -6710,34 +6968,36 @@ $wgLogActions = array( * @see LogFormatter */ $wgLogActionsHandlers = array( - 'move/move' => 'MoveLogFormatter', - 'move/move_redir' => 'MoveLogFormatter', + 'block/block' => 'BlockLogFormatter', + 'block/reblock' => 'BlockLogFormatter', + 'block/unblock' => 'BlockLogFormatter', + 'contentmodel/change' => 'ContentModelLogFormatter', 'delete/delete' => 'DeleteLogFormatter', + 'delete/event' => 'DeleteLogFormatter', 'delete/restore' => 'DeleteLogFormatter', 'delete/revision' => 'DeleteLogFormatter', - 'delete/event' => 'DeleteLogFormatter', - 'suppress/revision' => 'DeleteLogFormatter', - 'suppress/event' => 'DeleteLogFormatter', - 'suppress/delete' => 'DeleteLogFormatter', - 'patrol/patrol' => 'PatrolLogFormatter', - 'rights/rights' => 'RightsLogFormatter', - 'rights/autopromote' => 'RightsLogFormatter', - 'upload/upload' => 'UploadLogFormatter', - 'upload/overwrite' => 'UploadLogFormatter', - 'upload/revert' => 'UploadLogFormatter', - 'merge/merge' => 'MergeLogFormatter', - 'tag/update' => 'TagLogFormatter', - 'managetags/create' => 'LogFormatter', - 'managetags/delete' => 'LogFormatter', + 'import/interwiki' => 'LogFormatter', + 'import/upload' => 'LogFormatter', 'managetags/activate' => 'LogFormatter', + 'managetags/create' => 'LogFormatter', 'managetags/deactivate' => 'LogFormatter', - 'block/block' => 'BlockLogFormatter', - 'block/unblock' => 'BlockLogFormatter', - 'block/reblock' => 'BlockLogFormatter', + 'managetags/delete' => 'LogFormatter', + 'merge/merge' => 'MergeLogFormatter', + 'move/move' => 'MoveLogFormatter', + 'move/move_redir' => 'MoveLogFormatter', + 'patrol/patrol' => 'PatrolLogFormatter', + 'protect/move_prot' => 'ProtectLogFormatter', + 'rights/autopromote' => 'RightsLogFormatter', + 'rights/rights' => 'RightsLogFormatter', 'suppress/block' => 'BlockLogFormatter', + 'suppress/delete' => 'DeleteLogFormatter', + 'suppress/event' => 'DeleteLogFormatter', 'suppress/reblock' => 'BlockLogFormatter', - 'import/upload' => 'LogFormatter', - 'import/interwiki' => 'LogFormatter', + 'suppress/revision' => 'DeleteLogFormatter', + 'tag/update' => 'TagLogFormatter', + 'upload/overwrite' => 'UploadLogFormatter', + 'upload/revert' => 'UploadLogFormatter', + 'upload/upload' => 'UploadLogFormatter', ); /** @@ -6763,14 +7023,6 @@ $wgAllowSpecialInclusion = true; */ $wgDisableQueryPageUpdate = false; -/** - * List of special pages, followed by what subtitle they should go under - * at Special:SpecialPages - * - * @deprecated since 1.21 Override SpecialPage::getGroupName instead - */ -$wgSpecialPageGroups = array(); - /** * On Special:Unusedimages, consider images "used", if they are put * into a category. Default (false) is not to count those as used. @@ -7008,12 +7260,6 @@ $wgAPIPropModules = array(); */ $wgAPIListModules = array(); -/** - * This variable is ignored. To add your module to the API, please add it to $wgAPI*Modules - * @deprecated since 1.21 - */ -$wgAPIGeneratorModules = array(); - /** * Maximum amount of rows to scan in a DB query in the API * The default value is generally fine @@ -7464,6 +7710,7 @@ $wgUseLinkNamespaceDBFields = true; * $wgVirtualRestConfig['modules']['parsoid'] = array( * 'url' => 'http://localhost:8000', * 'prefix' => 'enwiki', + * 'domain' => 'en.wikipedia.org', * ); * * @var array @@ -7474,11 +7721,21 @@ $wgVirtualRestConfig = array( 'global' => array( # Timeout in seconds 'timeout' => 360, + # 'domain' is set to $wgCanonicalServer in Setup.php 'forwardCookies' => false, 'HTTPProxy' => null ) ); +/** + * Controls whether zero-result search queries with suggestions should display results for + * these suggestions. + * + * @var bool + * @since 1.26 + */ +$wgSearchRunSuggestedQuery = true; + /** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker diff --git a/includes/Defines.php b/includes/Defines.php index c9263da9..d55bbcf8 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -24,11 +24,6 @@ * @defgroup Constants MediaWiki constants */ -/** - * Version constants for the benefit of extensions - */ -define( 'MW_SPECIALPAGE_VERSION', 2 ); - /**@{ * Database related constants */ @@ -203,7 +198,7 @@ define( 'LIST_OR', 4 ); /** * Unicode and normalisation related */ -require_once __DIR__ . '/libs/normal/UtfNormalDefines.php'; +require_once __DIR__ . '/compat/normal/UtfNormalDefines.php'; /**@{ * Hook support constants diff --git a/includes/EditPage.php b/includes/EditPage.php index 8d27eac8..05e0ac0e 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -167,6 +167,12 @@ class EditPage { */ const AS_PARSE_ERROR = 240; + /** + * Status: when changing the content model is disallowed due to + * $wgContentHandlerUseDB being false + */ + const AS_CANNOT_USE_CUSTOM_MODEL = 241; + /** * HTML id and name for the beginning of the edit form. */ @@ -380,12 +386,14 @@ class EditPage { public $suppressIntro = false; - /** @var bool Set to true to allow editing of non-text content types. */ - public $allowNonTextContent = false; - /** @var bool */ protected $edit; + /** + * @var bool Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing + */ + private $enableApiEditOverride = false; + /** * @param Article $article */ @@ -447,8 +455,18 @@ class EditPage { * @throws MWException If $modelId has no known handler */ public function isSupportedContentModel( $modelId ) { - return $this->allowNonTextContent || - ContentHandler::getForModelID( $modelId ) instanceof TextContentHandler; + return $this->enableApiEditOverride === true || + ContentHandler::getForModelID( $modelId )->supportsDirectEditing(); + } + + /** + * Allow editing of content that supports API direct editing, but not general + * direct editing. Set to false by default. + * + * @param bool $enableOverride + */ + public function setApiEditOverride( $enableOverride ) { + $this->enableApiEditOverride = $enableOverride; } function submit() { @@ -509,7 +527,10 @@ class EditPage { if ( $permErrors ) { wfDebug( __METHOD__ . ": User can't edit\n" ); // Auto-block user's IP if the account was "hard" blocked - $wgUser->spreadAnyEditBlock(); + $user = $wgUser; + DeferredUpdates::addCallableUpdate( function() use ( $user ) { + $user->spreadAnyEditBlock(); + } ); $this->displayPermissionsError( $permErrors ); @@ -634,6 +655,9 @@ class EditPage { $this->getContextTitle()->getPrefixedText() ) ); $wgOut->addBacklinkSubtitle( $this->getContextTitle() ); + $wgOut->addHTML( $this->editFormPageTop ); + $wgOut->addHTML( $this->editFormTextTop ); + $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' ) ); $wgOut->addHTML( "
\n" ); @@ -647,13 +671,16 @@ class EditPage { $wgOut->addWikiMsg( 'viewsourcetext' ); } + $wgOut->addHTML( $this->editFormTextBeforeContent ); $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) ); + $wgOut->addHTML( $this->editFormTextAfterContent ); $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ), Linker::formatTemplates( $this->getTemplates() ) ) ); $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' ); + $wgOut->addHTML( $this->editFormTextBottom ); if ( $this->mTitle->exists() ) { $wgOut->returnToMain( null, $this->mTitle ); } @@ -1025,7 +1052,6 @@ class EditPage { $undo = $wgRequest->getInt( 'undo' ); if ( $undo > 0 && $undoafter > 0 ) { - $undorev = Revision::newFromId( $undo ); $oldrev = Revision::newFromId( $undoafter ); @@ -1034,8 +1060,8 @@ class EditPage { # Otherwise, $content will be left as-is. if ( !is_null( $undorev ) && !is_null( $oldrev ) && !$undorev->isDeleted( Revision::DELETED_TEXT ) && - !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { - + !$oldrev->isDeleted( Revision::DELETED_TEXT ) + ) { $content = $this->mArticle->getUndoContent( $undorev, $oldrev ); if ( $content === false ) { @@ -1230,9 +1256,9 @@ class EditPage { if ( !$converted ) { //TODO: somehow show a warning to the user! - wfDebug( "Attempt to preload incompatible content: " - . "can't convert " . $content->getModel() - . " to " . $handler->getModelID() ); + wfDebug( "Attempt to preload incompatible content: " . + "can't convert " . $content->getModel() . + " to " . $handler->getModelID() ); return $handler->makeEmptyContent(); } @@ -1350,6 +1376,7 @@ class EditPage { case self::AS_HOOK_ERROR: return false; + case self::AS_CANNOT_USE_CUSTOM_MODEL: case self::AS_PARSE_ERROR: $wgOut->addWikiText( '
' . $status->getWikiText() . '
' ); return true; @@ -1532,6 +1559,7 @@ class EditPage { */ function internalAttemptSave( &$result, $bot = false ) { global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize; + global $wgContentHandlerUseDB; $status = Status::newGood(); @@ -1652,11 +1680,19 @@ class EditPage { } } - if ( $this->contentModel !== $this->mTitle->getContentModel() - && !$wgUser->isAllowed( 'editcontentmodel' ) - ) { - $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); - return $status; + $changingContentModel = false; + if ( $this->contentModel !== $this->mTitle->getContentModel() ) { + if ( !$wgContentHandlerUseDB ) { + $status->fatal( 'editpage-cannot-use-custom-model' ); + $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL; + return $status; + } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) { + $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); + return $status; + + } + $changingContentModel = true; + $oldContentModel = $this->mTitle->getContentModel(); } if ( $this->changeTags ) { @@ -1916,7 +1952,7 @@ class EditPage { $this->summary, $flags, false, - null, + $wgUser, $content->getDefaultFormat() ); @@ -1946,16 +1982,48 @@ class EditPage { if ( $this->changeTags && isset( $doEditStatus->value['revision'] ) ) { // If a revision was created, apply any change tags that were requested - ChangeTags::addTags( - $this->changeTags, - isset( $doEditStatus->value['rc'] ) ? $doEditStatus->value['rc']->mAttribs['rc_id'] : null, - $doEditStatus->value['revision']->getId() + $addTags = $this->changeTags; + $revId = $doEditStatus->value['revision']->getId(); + // Defer this both for performance and so that addTags() sees the rc_id + // since the recentchange entry addition is deferred first (bug T100248) + DeferredUpdates::addCallableUpdate( function() use ( $addTags, $revId ) { + ChangeTags::addTags( $addTags, null, $revId ); + } ); + } + + // If the content model changed, add a log entry + if ( $changingContentModel ) { + $this->addContentModelChangeLogEntry( + $wgUser, + $oldContentModel, + $this->contentModel, + $this->summary ); } return $status; } + /** + * @param Title $title + * @param string $oldModel + * @param string $newModel + * @param string $reason + */ + protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) { + $log = new ManualLogEntry( 'contentmodel', 'change' ); + $log->setPerformer( $user ); + $log->setTarget( $this->mTitle ); + $log->setComment( $reason ); + $log->setParameters( array( + '4::oldmodel' => $oldModel, + '5::newmodel' => $newModel + ) ); + $logid = $log->insert(); + $log->publish( $logid ); + } + + /** * Register the change of watch status */ @@ -2486,7 +2554,7 @@ class EditPage { $wgOut->addHTML( $this->editFormTextBeforeContent ); if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) { - $wgOut->addHTML( EditPage::getEditToolbar() ); + $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) ); } if ( $this->blankArticle ) { @@ -3386,7 +3454,7 @@ HTML $this->deletedSinceEdit = false; - if ( $this->mTitle->isDeletedQuick() ) { + if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) { $this->lastDelete = $this->getLastDelete(); if ( $this->lastDelete ) { $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp ); @@ -3450,6 +3518,8 @@ HTML global $wgOut, $wgUser, $wgRawHtml, $wgLang; global $wgAllowUserCss, $wgAllowUserJs; + $stats = $wgOut->getContext()->getStats(); + if ( $wgRawHtml && !$this->mTokenOk ) { // Could be an offsite preview attempt. This is very unsafe if // HTML is enabled, as it could be an attack. @@ -3461,6 +3531,7 @@ HTML $parsedNote = $wgOut->parse( "
" . wfMessage( 'session_fail_preview_html' )->text() . "
", true, /* interface */true ); } + $stats->increment( 'edit.failures.session_loss' ); return $parsedNote; } @@ -3484,11 +3555,16 @@ HTML if ( $this->mTriedSave && !$this->mTokenOk ) { if ( $this->mTokenOkExceptSuffix ) { $note = wfMessage( 'token_suffix_mismatch' )->plain(); + $stats->increment( 'edit.failures.bad_token' ); } else { $note = wfMessage( 'session_fail_preview' )->plain(); + $stats->increment( 'edit.failures.session_loss' ); } } elseif ( $this->incompleteForm ) { $note = wfMessage( 'edit_form_incomplete' )->plain(); + if ( $this->mTriedSave ) { + $stats->increment( 'edit.failures.incomplete_form' ); + } } else { $note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing; } @@ -3619,13 +3695,18 @@ HTML * Shows a bulletin board style toolbar for common editing functions. * It can be disabled in the user preferences. * + * @param $title Title object for the page being edited (optional) * @return string */ - static function getEditToolbar() { + static function getEditToolbar( $title = null ) { global $wgContLang, $wgOut; global $wgEnableUploads, $wgForeignFileRepos; $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos ); + $showSignature = true; + if ( $title ) { + $showSignature = MWNamespace::wantSignatures( $title->getNamespace() ); + } /** * $toolarray is an array of arrays each of which includes the @@ -3693,13 +3774,13 @@ HTML 'sample' => wfMessage( 'nowiki_sample' )->text(), 'tip' => wfMessage( 'nowiki_tip' )->text(), ), - array( + $showSignature ? array( 'id' => 'mw-editbutton-signature', 'open' => '--~~~~', 'close' => '', 'sample' => '', 'tip' => wfMessage( 'sig_tip' )->text(), - ), + ) : false, array( 'id' => 'mw-editbutton-hr', 'open' => "\n----\n", @@ -3737,7 +3818,7 @@ HTML } $script .= '});'; - $wgOut->addScript( Html::inlineScript( ResourceLoader::makeLoaderConditionalScript( $script ) ) ); + $wgOut->addScript( ResourceLoader::makeInlineScript( $script ) ); $toolbar = '
'; diff --git a/includes/Export.php b/includes/Export.php index 4600feb5..adab21c3 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -874,7 +874,7 @@ class XmlDumpWriter { } global $wgContLang; - $prefix = str_replace( '_', ' ', $wgContLang->getNsText( $title->getNamespace() ) ); + $prefix = $wgContLang->getFormattedNsText( $title->getNamespace() ); if ( $prefix !== '' ) { $prefix .= ':'; @@ -1191,7 +1191,7 @@ class Dump7ZipOutput extends DumpPipeOutput { * @return string */ function setup7zCommand( $file ) { - $command = "7za a -bd -si " . wfEscapeShellArg( $file ); + $command = "7za a -bd -si -mx=4 " . wfEscapeShellArg( $file ); // Suppress annoying useless crap from p7zip // Unfortunately this could suppress real error messages too $command .= ' >' . wfGetNull() . ' 2>&1'; diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index c1d14db0..bcd6db20 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -296,8 +296,8 @@ class FileDeleteForm { Xml::closeElement( 'form' ); if ( $wgUser->isAllowed( 'editinterface' ) ) { - $title = Title::makeTitle( NS_MEDIAWIKI, 'Filedelete-reason-dropdown' ); - $link = Linker::link( + $title = wfMessage( 'filedelete-reason-dropdown' )->inContentLanguage()->getTitle(); + $link = Linker::linkKnown( $title, wfMessage( 'filedelete-edit-reasonlist' )->escaped(), array(), diff --git a/includes/GitInfo.php b/includes/GitInfo.php index fb298cfe..7f05bb0f 100644 --- a/includes/GitInfo.php +++ b/includes/GitInfo.php @@ -281,9 +281,9 @@ class GitInfo { $config = "{$this->basedir}/config"; $url = false; if ( is_readable( $config ) ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $configArray = parse_ini_file( $config, true ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); $remote = false; // Use the "origin" remote repo if available or any other repo if not. diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index ab3f019f..64aa87ec 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -24,7 +24,6 @@ if ( !defined( 'MEDIAWIKI' ) ) { die( "This file is part of MediaWiki, it is not a valid entry point" ); } -use Liuggio\StatsdClient\StatsdClient; use Liuggio\StatsdClient\Sender\SocketSender; use MediaWiki\Logger\LoggerFactory; @@ -172,6 +171,7 @@ if ( !function_exists( 'hash_equals' ) ) { * * @param string $ext Name of the extension to load * @param string|null $path Absolute path of where to find the extension.json file + * @since 1.25 */ function wfLoadExtension( $ext, $path = null ) { if ( !$path ) { @@ -192,6 +192,7 @@ function wfLoadExtension( $ext, $path = null ) { * * @see wfLoadExtension * @param string[] $exts Array of extension names to load + * @since 1.25 */ function wfLoadExtensions( array $exts ) { global $wgExtensionDirectory; @@ -207,6 +208,7 @@ function wfLoadExtensions( array $exts ) { * @see wfLoadExtension * @param string $skin Name of the extension to load * @param string|null $path Absolute path of where to find the skin.json file + * @since 1.25 */ function wfLoadSkin( $skin, $path = null ) { if ( !$path ) { @@ -221,6 +223,7 @@ function wfLoadSkin( $skin, $path = null ) { * * @see wfLoadExtensions * @param string[] $skins Array of extension names to load + * @since 1.25 */ function wfLoadSkins( array $skins ) { global $wgStyleDirectory; @@ -402,12 +405,17 @@ function wfRandomString( $length = 32 ) { * * ;:@&=$-_.+!*'(), * + * RFC 1738 says ~ is unsafe, however RFC 3986 considers it an unreserved + * character which should not be encoded. More importantly, google chrome + * always converts %7E back to ~, and converting it in this function can + * cause a redirect loop (T105265). + * * But + is not safe because it's used to indicate a space; &= are only safe in * paths and not in queries (and we don't distinguish here); ' seems kind of * scary; and urlencode() doesn't touch -_. to begin with. Plus, although / * is reserved, we don't care. So the list we unescape is: * - * ;:@$!*(),/ + * ;:@$!*(),/~ * * However, IIS7 redirects fail when the url contains a colon (Bug 22709), * so no fancy : for IIS7. @@ -426,7 +434,7 @@ function wfUrlencode( $s ) { } if ( is_null( $needle ) ) { - $needle = array( '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%2F' ); + $needle = array( '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%2F', '%7E' ); if ( !isset( $_SERVER['SERVER_SOFTWARE'] ) || ( strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/7' ) === false ) ) { @@ -437,7 +445,7 @@ function wfUrlencode( $s ) { $s = urlencode( $s ); $s = str_ireplace( $needle, - array( ';', '@', '$', '!', '*', '(', ')', ',', '/', ':' ), + array( ';', '@', '$', '!', '*', '(', ')', ',', '/', '~', ':' ), $s ); @@ -860,9 +868,9 @@ function wfParseUrl( $url ) { if ( $wasRelative ) { $url = "http:$url"; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $bits = parse_url( $url ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); // parse_url() returns an array without scheme for some invalid URLs, e.g. // parse_url("%0Ahttp://example.com") == array( 'host' => '%0Ahttp', 'path' => 'example.com' ) if ( !$bits || !isset( $bits['scheme'] ) ) { @@ -1248,13 +1256,17 @@ function wfLogProfilingData() { $profiler->logData(); $config = $context->getConfig(); - if ( $config->has( 'StatsdServer' ) ) { - $statsdServer = explode( ':', $config->get( 'StatsdServer' ) ); - $statsdHost = $statsdServer[0]; - $statsdPort = isset( $statsdServer[1] ) ? $statsdServer[1] : 8125; - $statsdSender = new SocketSender( $statsdHost, $statsdPort ); - $statsdClient = new StatsdClient( $statsdSender ); - $statsdClient->send( $context->getStats()->getBuffer() ); + if ( $config->get( 'StatsdServer' ) ) { + try { + $statsdServer = explode( ':', $config->get( 'StatsdServer' ) ); + $statsdHost = $statsdServer[0]; + $statsdPort = isset( $statsdServer[1] ) ? $statsdServer[1] : 8125; + $statsdSender = new SocketSender( $statsdHost, $statsdPort ); + $statsdClient = new SamplingStatsdClient( $statsdSender, true, false ); + $statsdClient->send( $context->getStats()->getBuffer() ); + } catch ( Exception $ex ) { + MWExceptionHandler::logException( $ex ); + } } # Profiling must actually be enabled... @@ -1344,6 +1356,17 @@ function wfReadOnlyReason() { } else { $wgReadOnly = false; } + // Callers use this method to be aware that data presented to a user + // may be very stale and thus allowing submissions can be problematic. + try { + if ( $wgReadOnly === false && wfGetLB()->getLaggedSlaveMode() ) { + $wgReadOnly = 'The database has been automatically locked ' . + 'while the slave database servers catch up to the master'; + } + } catch ( DBConnectionError $e ) { + $wgReadOnly = 'The database has been automatically locked ' . + 'until the slave database servers become available'; + } } return $wgReadOnly; @@ -1405,7 +1428,7 @@ function wfGetLangObj( $langcode = false ) { * * This function replaces all old wfMsg* functions. * - * @param string|string[] $key Message key, or array of keys + * @param string|string[]|MessageSpecifier $key Message key, or array of keys, or a MessageSpecifier * @param mixed $params,... Normal message parameters * @return Message * @@ -1745,7 +1768,7 @@ function wfMsgExt( $key, $options ) { } if ( in_array( 'escape', $options, true ) ) { - $string = htmlspecialchars ( $string ); + $string = htmlspecialchars( $string ); } elseif ( in_array( 'escapenoentities', $options, true ) ) { $string = Sanitizer::escapeHtmlAllowEntities( $string ); } @@ -2118,15 +2141,14 @@ function wfVarDump( $var ) { */ function wfHttpError( $code, $label, $desc ) { global $wgOut; - header( "HTTP/1.0 $code $label" ); - header( "Status: $code $label" ); + HttpStatus::header( $code ); if ( $wgOut ) { $wgOut->disable(); $wgOut->sendCacheControl(); } header( 'Content-type: text/html; charset=utf-8' ); - print "" . + print '' . '' . htmlspecialchars( $label ) . '

' . @@ -2161,14 +2183,24 @@ function wfResetOutputBuffers( $resetGzipEncoding = true ) { $wgDisableOutputCompression = true; } while ( $status = ob_get_status() ) { - if ( $status['type'] == 0 /* PHP_OUTPUT_HANDLER_INTERNAL */ ) { - // Probably from zlib.output_compression or other - // PHP-internal setting which can't be removed. - // + if ( isset( $status['flags'] ) ) { + $flags = PHP_OUTPUT_HANDLER_CLEANABLE | PHP_OUTPUT_HANDLER_REMOVABLE; + $deleteable = ( $status['flags'] & $flags ) === $flags; + } elseif ( isset( $status['del'] ) ) { + $deleteable = $status['del']; + } else { + // Guess that any PHP-internal setting can't be removed. + $deleteable = $status['type'] !== 0; /* PHP_OUTPUT_HANDLER_INTERNAL */ + } + if ( !$deleteable ) { // Give up, and hope the result doesn't break // output behavior. break; } + if ( $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier' ) { + // Unit testing barrier to prevent this function from breaking PHPUnit. + break; + } if ( !ob_end_clean() ) { // Could not remove output buffer handler; abort now // to avoid getting in some kind of infinite loop. @@ -2312,40 +2344,19 @@ function wfNegotiateType( $cprefs, $sprefs ) { /** * Reference-counted warning suppression * + * @deprecated since 1.26, use MediaWiki\suppressWarnings() directly * @param bool $end */ function wfSuppressWarnings( $end = false ) { - static $suppressCount = 0; - static $originalLevel = false; - - if ( $end ) { - if ( $suppressCount ) { - --$suppressCount; - if ( !$suppressCount ) { - error_reporting( $originalLevel ); - } - } - } 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 - ) ); - } - ++$suppressCount; - } + MediaWiki\suppressWarnings( $end ); } /** + * @deprecated since 1.26, use MediaWiki\restoreWarnings() directly * Restore error level to previous value */ function wfRestoreWarnings() { - wfSuppressWarnings( true ); + MediaWiki\suppressWarnings( true ); } # Autodetect, convert and provide timestamps of various types @@ -2453,7 +2464,7 @@ function wfTimestampNow() { function wfIsWindows() { static $isWindows = null; if ( $isWindows === null ) { - $isWindows = substr( php_uname(), 0, 7 ) == 'Windows'; + $isWindows = strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN'; } return $isWindows; } @@ -2515,7 +2526,7 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) { wfDebug( "$caller: called wfMkdirParents($dir)\n" ); } - if ( strval( $dir ) === '' || ( file_exists( $dir ) && is_dir( $dir ) ) ) { + if ( strval( $dir ) === '' || is_dir( $dir ) ) { return true; } @@ -2526,9 +2537,9 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) { } // Turn off the normal warning, we're doing our own below - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $ok = mkdir( $dir, $mode, true ); // PHP5 <3 - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$ok ) { //directory may have been created on another request since we last checked @@ -2769,7 +2780,7 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), $useLogPipe = false; if ( is_executable( '/bin/bash' ) ) { - $time = intval ( isset( $limits['time'] ) ? $limits['time'] : $wgMaxShellTime ); + $time = intval( isset( $limits['time'] ) ? $limits['time'] : $wgMaxShellTime ); if ( isset( $limits['walltime'] ) ) { $wallTime = intval( $limits['walltime'] ); } elseif ( isset( $limits['time'] ) ) { @@ -2777,8 +2788,8 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), } else { $wallTime = intval( $wgMaxShellWallClockTime ); } - $mem = intval ( isset( $limits['memory'] ) ? $limits['memory'] : $wgMaxShellMemory ); - $filesize = intval ( isset( $limits['filesize'] ) ? $limits['filesize'] : $wgMaxShellFileSize ); + $mem = intval( isset( $limits['memory'] ) ? $limits['memory'] : $wgMaxShellMemory ); + $filesize = intval( isset( $limits['filesize'] ) ? $limits['filesize'] : $wgMaxShellFileSize ); if ( $time > 0 || $mem > 0 || $filesize > 0 || $wallTime > 0 ) { $cmd = '/bin/bash ' . escapeshellarg( "$IP/includes/limit.sh" ) . ' ' . @@ -3023,9 +3034,9 @@ function wfMerge( $old, $mine, $yours, &$result ) { # This check may also protect against code injection in # case of broken installations. - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$haveDiff3 ) { wfDebug( "diff3 not found\n" ); @@ -3102,9 +3113,9 @@ function wfDiff( $before, $after, $params = '-u' ) { } global $wgDiff; - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $haveDiff = $wgDiff && file_exists( $wgDiff ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); # This check may also protect against code injection in # case of broken installations. @@ -3205,6 +3216,7 @@ function wfUsePHP( $req_ver ) { * * @see perldoc -f use * + * @deprecated since 1.26, use the "requires' property of extension.json * @param string|int|float $req_ver The version to check, can be a string, an integer, or a float * @throws MWException */ @@ -3455,7 +3467,6 @@ function wfResetSessionID() { $_SESSION = $tmp; } $newSessionId = session_id(); - Hooks::run( 'ResetSessionID', array( $oldSessionId, $newSessionId ) ); } /** @@ -3464,15 +3475,17 @@ function wfResetSessionID() { * @param bool $sessionId */ function wfSetupSession( $sessionId = false ) { - global $wgSessionsInMemcached, $wgSessionsInObjectCache, $wgCookiePath, $wgCookieDomain, - $wgCookieSecure, $wgCookieHttpOnly, $wgSessionHandler; - if ( $wgSessionsInObjectCache || $wgSessionsInMemcached ) { + global $wgSessionsInObjectCache, $wgSessionHandler; + global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly; + + if ( $wgSessionsInObjectCache ) { ObjectCacheSessionHandler::install(); } elseif ( $wgSessionHandler && $wgSessionHandler != ini_get( 'session.save_handler' ) ) { # Only set this if $wgSessionHandler isn't null and session.save_handler # hasn't already been set to the desired value (that causes errors) ini_set( 'session.save_handler', $wgSessionHandler ); } + session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly ); session_cache_limiter( 'private, must-revalidate' ); @@ -3481,9 +3494,14 @@ function wfSetupSession( $sessionId = false ) { } else { wfFixSessionID(); } - wfSuppressWarnings(); + + MediaWiki\suppressWarnings(); session_start(); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); + + if ( $wgSessionsInObjectCache ) { + ObjectCacheSessionHandler::renewCurrentSession(); + } } /** @@ -3506,7 +3524,7 @@ function wfGetPrecompiledData( $name ) { } /** - * Get a cache key + * Make a cache key for the local wiki. * * @param string $args,... * @return string @@ -3516,12 +3534,13 @@ function wfMemcKey( /*...*/ ) { $prefix = $wgCachePrefix === false ? wfWikiID() : $wgCachePrefix; $args = func_get_args(); $key = $prefix . ':' . implode( ':', $args ); - $key = str_replace( ' ', '_', $key ); - return $key; + return strtr( $key, ' ', '_' ); } /** - * Get a cache key for a foreign DB + * Make a cache key for a foreign DB. + * + * Must match what wfMemcKey() would produce in context of the foreign wiki. * * @param string $db * @param string $prefix @@ -3531,11 +3550,29 @@ function wfMemcKey( /*...*/ ) { function wfForeignMemcKey( $db, $prefix /*...*/ ) { $args = array_slice( func_get_args(), 2 ); if ( $prefix ) { + // Match wfWikiID() logic $key = "$db-$prefix:" . implode( ':', $args ); } else { $key = $db . ':' . implode( ':', $args ); } - return str_replace( ' ', '_', $key ); + return strtr( $key, ' ', '_' ); +} + +/** + * Make a cache key with database-agnostic prefix. + * + * Doesn't have a wiki-specific namespace. Uses a generic 'global' prefix + * instead. Must have a prefix as otherwise keys that use a database name + * in the first segment will clash with wfMemcKey/wfForeignMemcKey. + * + * @since 1.26 + * @param string $args,... + * @return string + */ +function wfGlobalCacheKey( /*...*/ ) { + $args = func_get_args(); + $key = 'global:' . implode( ':', $args ); + return strtr( $key, ' ', '_' ); } /** @@ -3744,6 +3781,7 @@ function wfWaitForSlaves( } // Figure out which clusters need to be checked + /** @var LoadBalancer[] $lbs */ $lbs = array(); if ( $cluster === '*' ) { wfGetLBFactory()->forEachLB( function ( LoadBalancer $lb ) use ( &$lbs ) { @@ -3760,20 +3798,14 @@ function wfWaitForSlaves( // time needed to wait on the next clusters. $masterPositions = array_fill( 0, count( $lbs ), false ); foreach ( $lbs as $i => $lb ) { - // 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() ) { - continue; // assume no writes done - } - // Use the empty string to not trigger selectDB() since the connection - // may have been to a server that does not have a DB for the current wiki. - $dbw = $lb->getConnection( DB_MASTER, array(), '' ); - if ( $ifWritesSince && $dbw->lastDoneWrites() < $ifWritesSince ) { - continue; // no writes since the last wait - } - $masterPositions[$i] = $dbw->getMasterPos(); + if ( $lb->getServerCount() <= 1 ) { + // Bug 27975 - Don't try to wait for slaves if there are none + // Prevents permission error when getting master position + continue; + } elseif ( $ifWritesSince && $lb->lastMasterChangeTimestamp() < $ifWritesSince ) { + continue; // no writes since the last wait } + $masterPositions[$i] = $lb->getMasterPos(); } $ok = true; @@ -3830,9 +3862,9 @@ function wfStripIllegalFilenameChars( $name ) { } /** - * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit; + * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit * - * @return int Value the memory limit was set to. + * @return int Resulting value of the memory limit. */ function wfMemoryLimit() { global $wgMemoryLimit; @@ -3841,21 +3873,41 @@ function wfMemoryLimit() { $conflimit = wfShorthandToInteger( $wgMemoryLimit ); if ( $conflimit == -1 ) { wfDebug( "Removing PHP's memory limit\n" ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); ini_set( 'memory_limit', $conflimit ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); return $conflimit; } elseif ( $conflimit > $memlimit ) { wfDebug( "Raising PHP's memory limit to $conflimit bytes\n" ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); ini_set( 'memory_limit', $conflimit ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); return $conflimit; } } return $memlimit; } +/** + * Set PHP's time limit to the larger of php.ini or $wgTransactionalTimeLimit + * + * @return int Prior time limit + * @since 1.26 + */ +function wfTransactionalTimeLimit() { + global $wgTransactionalTimeLimit; + + $timeLimit = ini_get( 'max_execution_time' ); + // Note that CLI scripts use 0 + if ( $timeLimit > 0 && $wgTransactionalTimeLimit > $timeLimit ) { + set_time_limit( $wgTransactionalTimeLimit ); + } + + ignore_user_abort( true ); // ignore client disconnects + + return $timeLimit; +} + /** * Converts shorthand byte notation to integer form * @@ -3917,13 +3969,13 @@ function wfBCP47( $code ) { } /** - * Get a cache object. + * Get a specific cache object. * - * @param int $inputType Cache type, one of the CACHE_* constants. + * @param int|string $cacheType A CACHE_* constants, or other key in $wgObjectCaches * @return BagOStuff */ -function wfGetCache( $inputType ) { - return ObjectCache::getInstance( $inputType ); +function wfGetCache( $cacheType ) { + return ObjectCache::getInstance( $cacheType ); } /** @@ -3995,9 +4047,9 @@ function wfUnpack( $format, $data, $length = false ) { } } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $result = unpack( $format, $data ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $result === false ) { // If it cannot extract the packed data. @@ -4236,3 +4288,28 @@ function wfThumbIsStandard( File $file, array $params ) { return true; } + +/** + * Merges two (possibly) 2 dimensional arrays into the target array ($baseArray). + * + * Values that exist in both values will be combined with += (all values of the array + * of $newValues will be added to the values of the array of $baseArray, while values, + * that exists in both, the value of $baseArray will be used). + * + * @param array $baseArray The array where you want to add the values of $newValues to + * @param array $newValues An array with new values + * @return array The combined array + * @since 1.26 + */ +function wfArrayPlus2d( array $baseArray, array $newValues ) { + // First merge items that are in both arrays + foreach ( $baseArray as $name => &$groupVal ) { + if ( isset( $newValues[$name] ) ) { + $groupVal += $newValues[$name]; + } + } + // Now add items that didn't exist yet + $baseArray += $newValues; + + return $baseArray; +} diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php index 69f1120d..494cbfaf 100644 --- a/includes/HistoryBlob.php +++ b/includes/HistoryBlob.php @@ -522,9 +522,9 @@ class DiffHistoryBlob implements HistoryBlob { function diff( $t1, $t2 ) { # Need to do a null concatenation with warnings off, due to bugs in the current version of xdiff # "String is not zero-terminated" - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $diff = xdiff_string_rabdiff( $t1, $t2 ) . ''; - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); return $diff; } @@ -535,9 +535,9 @@ class DiffHistoryBlob implements HistoryBlob { */ function patch( $base, $diff ) { if ( function_exists( 'xdiff_string_bpatch' ) ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $text = xdiff_string_bpatch( $base, $diff ) . ''; - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); return $text; } diff --git a/includes/Hooks.php b/includes/Hooks.php index dffc7bcf..a4145624 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -135,9 +135,6 @@ class Hooks { * returning null) is equivalent to returning true. */ public static function run( $event, array $args = array(), $deprecatedVersion = null ) { - $profiler = Profiler::instance(); - $eventPS = $profiler->scopedProfileIn( 'hook: ' . $event ); - foreach ( self::getHandlers( $event ) as $hook ) { // Turn non-array values into an array. (Can't use casting because of objects.) if ( !is_array( $hook ) ) { @@ -196,8 +193,6 @@ class Hooks { $badhookmsg = null; $hook_args = array_merge( $hook, $args ); - // Profile first in case the Profiler causes errors - $funcPS = $profiler->scopedProfileIn( $func ); set_error_handler( 'Hooks::hookErrorHandler' ); // mark hook as deprecated, if deprecation version is specified @@ -215,7 +210,6 @@ class Hooks { } restore_error_handler(); - $profiler->scopedProfileOut( $funcPS ); // Process the return value. if ( is_string( $retval ) ) { @@ -237,22 +231,25 @@ class Hooks { } /** - * Handle PHP errors issued inside a hook. Catch errors that have to do with - * a function expecting a reference, and let all others pass through. - * - * This REALLY should be protected... but it's public for compatibility + * Handle PHP errors issued inside a hook. Catch errors that have to do + * with a function expecting a reference, and pass all others through to + * MWExceptionHandler::handleError() for default processing. * * @since 1.18 * * @param int $errno Error number (unused) * @param string $errstr Error message * @throws MWHookException If the error has to do with the function signature - * @return bool Always returns false + * @return bool */ public static function hookErrorHandler( $errno, $errstr ) { if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) { throw new MWHookException( $errstr, $errno ); } - return false; + + // Delegate unhandled errors to the default MW handler + return call_user_func_array( + 'MWExceptionHandler::handleError', func_get_args() + ); } } diff --git a/includes/Html.php b/includes/Html.php index d312e0a6..62ae0b85 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -104,27 +104,26 @@ class Html { /** * Modifies a set of attributes meant for button elements * and apply a set of default attributes when $wgUseMediaWikiUIEverywhere enabled. - * @param array $attrs - * @param string[] $modifiers to add to the button + * @param array $attrs HTML attributes in an associative array + * @param string[] $modifiers classes to add to the button * @see https://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers * @return array $attrs A modified attribute array */ - public static function buttonAttributes( $attrs, $modifiers = array() ) { + public static function buttonAttributes( array $attrs, array $modifiers = array() ) { global $wgUseMediaWikiUIEverywhere; if ( $wgUseMediaWikiUIEverywhere ) { if ( isset( $attrs['class'] ) ) { if ( is_array( $attrs['class'] ) ) { $attrs['class'][] = 'mw-ui-button'; - $attrs = array_merge( $attrs, $modifiers ); + $attrs['class'] = array_merge( $attrs['class'], $modifiers ); // ensure compatibility with Xml $attrs['class'] = implode( ' ', $attrs['class'] ); } else { $attrs['class'] .= ' mw-ui-button ' . implode( ' ', $modifiers ); } } else { - $attrs['class'] = array( 'mw-ui-button' ); // ensure compatibility with Xml - $attrs['class'] = implode( ' ', array_merge( $attrs['class'], $modifiers ) ); + $attrs['class'] = 'mw-ui-button ' . implode( ' ', $modifiers ); } } return $attrs; @@ -137,11 +136,8 @@ class Html { * @param array $attrs An attribute array. * @return array $attrs A modified attribute array */ - public static function getTextInputAttributes( $attrs ) { + public static function getTextInputAttributes( array $attrs ) { global $wgUseMediaWikiUIEverywhere; - if ( !$attrs ) { - $attrs = array(); - } if ( $wgUseMediaWikiUIEverywhere ) { if ( isset( $attrs['class'] ) ) { if ( is_array( $attrs['class'] ) ) { @@ -165,11 +161,11 @@ class Html { * @param array $attrs Associative array of attributes, e.g., array( * 'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for * further documentation. - * @param string[] $modifiers to add to the button + * @param string[] $modifiers classes to add to the button * @see http://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers * @return string Raw HTML */ - public static function linkButton( $contents, $attrs, $modifiers = array() ) { + public static function linkButton( $contents, array $attrs, array $modifiers = array() ) { return self::element( 'a', self::buttonAttributes( $attrs, $modifiers ), $contents @@ -185,11 +181,11 @@ class Html { * @param array $attrs Associative array of attributes, e.g., array( * 'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for * further documentation. - * @param string[] $modifiers to add to the button + * @param string[] $modifiers classes to add to the button * @see http://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers * @return string Raw HTML */ - public static function submitButton( $contents, $attrs, $modifiers = array() ) { + public static function submitButton( $contents, array $attrs, array $modifiers = array() ) { $attrs['type'] = 'submit'; $attrs['value'] = $contents; return self::element( 'input', self::buttonAttributes( $attrs, $modifiers ) ); @@ -337,8 +333,7 @@ class Html { * further documentation. * @return array An array of attributes functionally identical to $attribs */ - private static function dropDefaults( $element, $attribs ) { - + private static function dropDefaults( $element, array $attribs ) { // Whenever altering this array, please provide a covering test case // in HtmlTest::provideElementsWithAttributesHavingDefaultValues static $attribDefaults = array( @@ -485,11 +480,10 @@ class Html { * @return string HTML fragment that goes between element name and '>' * (starting with a space if at least one attribute is output) */ - public static function expandAttributes( $attribs ) { + public static function expandAttributes( array $attribs ) { global $wgWellFormedXml; $ret = ''; - $attribs = (array)$attribs; foreach ( $attribs as $key => $value ) { // Support intuitive array( 'checked' => true/false ) form if ( $value === false || is_null( $value ) ) { @@ -714,13 +708,16 @@ class Html { * attributes, passed to Html::element() * @return string Raw HTML */ - public static function input( $name, $value = '', $type = 'text', $attribs = array() ) { + public static function input( $name, $value = '', $type = 'text', array $attribs = array() ) { $attribs['type'] = $type; $attribs['value'] = $value; $attribs['name'] = $name; if ( in_array( $type, array( 'text', 'search', 'email', 'password', 'number' ) ) ) { $attribs = self::getTextInputAttributes( $attribs ); } + if ( in_array( $type, array( 'button', 'reset', 'submit' ) ) ) { + $attribs = self::buttonAttributes( $attribs ); + } return self::element( 'input', $attribs ); } @@ -794,7 +791,7 @@ class Html { * attributes, passed to Html::element() * @return string Raw HTML */ - public static function hidden( $name, $value, $attribs = array() ) { + public static function hidden( $name, $value, array $attribs = array() ) { return self::input( $name, $value, 'hidden', $attribs ); } @@ -810,7 +807,7 @@ class Html { * attributes, passed to Html::element() * @return string Raw HTML */ - public static function textarea( $name, $value = '', $attribs = array() ) { + public static function textarea( $name, $value = '', array $attribs = array() ) { $attribs['name'] = $name; if ( substr( $value, 0, 1 ) == "\n" ) { @@ -825,6 +822,47 @@ class Html { return self::element( 'textarea', self::getTextInputAttributes( $attribs ), $spacedValue ); } + /** + * Helper for Html::namespaceSelector(). + * @param array $params See Html::namespaceSelector() + * @return array + */ + public static function namespaceSelectorOptions( array $params = array() ) { + global $wgContLang; + + $options = array(); + + if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) { + $params['exclude'] = array(); + } + + if ( isset( $params['all'] ) ) { + // add an option that would let the user select all namespaces. + // Value is provided by user, the name shown is localized for the user. + $options[$params['all']] = wfMessage( 'namespacesall' )->text(); + } + // Add all namespaces as options (in the content language) + $options += $wgContLang->getFormattedNamespaces(); + + $optionsOut = array(); + // Filter out namespaces below 0 and massage labels + foreach ( $options as $nsId => $nsName ) { + if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) { + continue; + } + if ( $nsId === NS_MAIN ) { + // For other namespaces use the namespace prefix as label, but for + // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)") + $nsName = wfMessage( 'blanknamespace' )->text(); + } elseif ( is_int( $nsId ) ) { + $nsName = $wgContLang->convertNamespace( $nsId ); + } + $optionsOut[ $nsId ] = $nsName; + } + + return $optionsOut; + } + /** * Build a drop-down box for selecting a namespace * @@ -844,8 +882,6 @@ class Html { public static function namespaceSelector( array $params = array(), array $selectAttribs = array() ) { - global $wgContLang; - ksort( $selectAttribs ); // Is a namespace selected? @@ -862,37 +898,16 @@ class Html { $params['selected'] = ''; } - if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) { - $params['exclude'] = array(); - } if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) { $params['disable'] = array(); } // Associative array between option-values and option-labels - $options = array(); - - if ( isset( $params['all'] ) ) { - // add an option that would let the user select all namespaces. - // Value is provided by user, the name shown is localized for the user. - $options[$params['all']] = wfMessage( 'namespacesall' )->text(); - } - // Add all namespaces as options (in the content language) - $options += $wgContLang->getFormattedNamespaces(); + $options = self::namespaceSelectorOptions( $params ); - // Convert $options to HTML and filter out namespaces below 0 + // Convert $options to HTML $optionsHtml = array(); foreach ( $options as $nsId => $nsName ) { - if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) { - continue; - } - if ( $nsId === NS_MAIN ) { - // For other namespaces use the namespace prefix as label, but for - // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)") - $nsName = wfMessage( 'blanknamespace' )->text(); - } elseif ( is_int( $nsId ) ) { - $nsName = $wgContLang->convertNamespace( $nsId ); - } $optionsHtml[] = self::element( 'option', array( 'disabled' => in_array( $nsId, $params['disable'] ), @@ -937,7 +952,7 @@ class Html { * attributes, passed to Html::element() of html tag. * @return string Raw HTML */ - public static function htmlHeader( $attribs = array() ) { + public static function htmlHeader( array $attribs = array() ) { $ret = ''; global $wgHtml5Version, $wgMimeType, $wgXhtmlNamespaces; @@ -1047,7 +1062,7 @@ class Html { * @param string[] $urls * @return string */ - static function srcSet( $urls ) { + static function srcSet( array $urls ) { $candidates = array(); foreach ( $urls as $density => $url ) { // Cast density to float to strip 'x'. diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index fa54487a..bc5a9570 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -257,7 +257,7 @@ class MWHttpRequest { $this->parsedUrl = wfParseUrl( $this->url ); if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) { - $this->status = Status::newFatal( 'http-invalid-url' ); + $this->status = Status::newFatal( 'http-invalid-url', $url ); } else { $this->status = Status::newGood( 100 ); // continue } @@ -797,14 +797,14 @@ class CurlHttpRequest extends MWHttpRequest { } if ( $this->followRedirects && $this->canFollowRedirects() ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); if ( !curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) { wfDebug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " . "Probably safe_mode or open_basedir is set.\n" ); // Continue the processing. If it were in curl_setopt_array, // processing would have halted on its entry } - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); } if ( $this->profiler ) { diff --git a/includes/Import.php b/includes/Import.php index d31be43b..6a0bfd09 100644 --- a/includes/Import.php +++ b/includes/Import.php @@ -34,7 +34,7 @@ class WikiImporter { private $reader = null; private $foreignNamespaces = null; private $mLogItemCallback, $mUploadCallback, $mRevisionCallback, $mPageCallback; - private $mSiteInfoCallback, $mTargetNamespace, $mPageOutCallback; + private $mSiteInfoCallback, $mPageOutCallback; private $mNoticeCallback, $mDebug; private $mImportUploads, $mImageBasePath; private $mNoUpdates = false; @@ -49,8 +49,13 @@ class WikiImporter { * Creates an ImportXMLReader drawing from the source provided * @param ImportSource $source * @param Config $config + * @throws Exception */ function __construct( ImportSource $source, Config $config = null ) { + if ( !class_exists( 'XMLReader' ) ) { + throw new Exception( 'Import requires PHP to have been compiled with libxml support' ); + } + $this->reader = new XMLReader(); if ( !$config ) { wfDeprecated( __METHOD__ . ' without a Config instance', '1.25' ); @@ -62,11 +67,22 @@ class WikiImporter { stream_wrapper_register( 'uploadsource', 'UploadSourceAdapter' ); } $id = UploadSourceAdapter::registerSource( $source ); + + // Enable the entity loader, as it is needed for loading external URLs via + // XMLReader::open (T86036) + $oldDisable = libxml_disable_entity_loader( false ); if ( defined( 'LIBXML_PARSEHUGE' ) ) { - $this->reader->open( "uploadsource://$id", null, LIBXML_PARSEHUGE ); + $status = $this->reader->open( "uploadsource://$id", null, LIBXML_PARSEHUGE ); } else { - $this->reader->open( "uploadsource://$id" ); + $status = $this->reader->open( "uploadsource://$id" ); } + if ( !$status ) { + $error = libxml_get_last_error(); + libxml_disable_entity_loader( $oldDisable ); + throw new MWException( 'Encountered an internal error while initializing WikiImporter object: ' . + $error->message ); + } + libxml_disable_entity_loader( $oldDisable ); // Default callbacks $this->setPageCallback( array( $this, 'beforeImportPage' ) ); @@ -224,7 +240,6 @@ class WikiImporter { public function setTargetNamespace( $namespace ) { if ( is_null( $namespace ) ) { // Don't override namespaces - $this->mTargetNamespace = null; $this->setImportTitleFactory( new NaiveImportTitleFactory() ); return true; } elseif ( @@ -232,7 +247,6 @@ class WikiImporter { MWNamespace::exists( intval( $namespace ) ) ) { $namespace = intval( $namespace ); - $this->mTargetNamespace = $namespace; $this->setImportTitleFactory( new NamespaceImportTitleFactory( $namespace ) ); return true; } else { @@ -252,10 +266,7 @@ class WikiImporter { $this->setImportTitleFactory( new NaiveImportTitleFactory() ); } elseif ( $rootpage !== '' ) { $rootpage = rtrim( $rootpage, '/' ); //avoid double slashes - $title = Title::newFromText( $rootpage, !is_null( $this->mTargetNamespace ) - ? $this->mTargetNamespace - : NS_MAIN - ); + $title = Title::newFromText( $rootpage ); if ( !$title || $title->isExternal() ) { $status->fatal( 'import-rootpage-invalid' ); @@ -383,9 +394,9 @@ class WikiImporter { $countKey = 'title_' . $title->getPrefixedText(); $countable = $page->isCountable( $editInfo ); if ( array_key_exists( $countKey, $this->countableCache ) && - $countable != $this->countableCache[ $countKey ] ) { + $countable != $this->countableCache[$countKey] ) { DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( - 'articles' => ( (int)$countable - (int)$this->countableCache[ $countKey ] ) + 'articles' => ( (int)$countable - (int)$this->countableCache[$countKey] ) ) ) ); } } @@ -528,10 +539,10 @@ class WikiImporter { $oldDisable = libxml_disable_entity_loader( true ); $this->reader->read(); - if ( $this->reader->name != 'mediawiki' ) { + if ( $this->reader->localName != 'mediawiki' ) { libxml_disable_entity_loader( $oldDisable ); throw new MWException( "Expected tag, got " . - $this->reader->name ); + $this->reader->localName ); } $this->debug( " tag is correct." ); @@ -542,7 +553,7 @@ class WikiImporter { $rethrow = null; try { while ( $keepReading ) { - $tag = $this->reader->name; + $tag = $this->reader->localName; $type = $this->reader->nodeType; if ( !Hooks::run( 'ImportHandleToplevelXMLTag', array( $this ) ) ) { @@ -593,14 +604,14 @@ class WikiImporter { while ( $this->reader->read() ) { if ( $this->reader->nodeType == XmlReader::END_ELEMENT && - $this->reader->name == 'siteinfo' ) { + $this->reader->localName == 'siteinfo' ) { break; } - $tag = $this->reader->name; + $tag = $this->reader->localName; if ( $tag == 'namespace' ) { - $this->foreignNamespaces[ $this->nodeAttribute( 'key' ) ] = + $this->foreignNamespaces[$this->nodeAttribute( 'key' )] = $this->nodeContents(); } elseif ( in_array( $tag, $normalFields ) ) { $siteInfo[$tag] = $this->nodeContents(); @@ -621,11 +632,11 @@ class WikiImporter { while ( $this->reader->read() ) { if ( $this->reader->nodeType == XMLReader::END_ELEMENT && - $this->reader->name == 'logitem' ) { + $this->reader->localName == 'logitem' ) { break; } - $tag = $this->reader->name; + $tag = $this->reader->localName; if ( !Hooks::run( 'ImportHandleLogItemXMLTag', array( $this, $logInfo @@ -685,13 +696,13 @@ class WikiImporter { while ( $skip ? $this->reader->next() : $this->reader->read() ) { if ( $this->reader->nodeType == XMLReader::END_ELEMENT && - $this->reader->name == 'page' ) { + $this->reader->localName == 'page' ) { break; } $skip = false; - $tag = $this->reader->name; + $tag = $this->reader->localName; if ( $badTitle ) { // The title is invalid, bail out of this page @@ -758,11 +769,11 @@ class WikiImporter { while ( $skip ? $this->reader->next() : $this->reader->read() ) { if ( $this->reader->nodeType == XMLReader::END_ELEMENT && - $this->reader->name == 'revision' ) { + $this->reader->localName == 'revision' ) { break; } - $tag = $this->reader->name; + $tag = $this->reader->localName; if ( !Hooks::run( 'ImportHandleRevisionXMLTag', array( $this, $pageInfo, $revisionInfo @@ -850,11 +861,11 @@ class WikiImporter { while ( $skip ? $this->reader->next() : $this->reader->read() ) { if ( $this->reader->nodeType == XMLReader::END_ELEMENT && - $this->reader->name == 'upload' ) { + $this->reader->localName == 'upload' ) { break; } - $tag = $this->reader->name; + $tag = $this->reader->localName; if ( !Hooks::run( 'ImportHandleUploadXMLTag', array( $this, $pageInfo @@ -948,11 +959,11 @@ class WikiImporter { while ( $this->reader->read() ) { if ( $this->reader->nodeType == XMLReader::END_ELEMENT && - $this->reader->name == 'contributor' ) { + $this->reader->localName == 'contributor' ) { break; } - $tag = $this->reader->name; + $tag = $this->reader->localName; if ( in_array( $tag, $fields ) ) { $info[$tag] = $this->nodeContents(); @@ -1846,9 +1857,9 @@ class ImportStreamSource implements ImportSource { * @return Status */ static function newFromFile( $filename ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $file = fopen( $filename, 'rt' ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$file ) { return Status::newFatal( "importcantopen" ); } diff --git a/includes/Linker.php b/includes/Linker.php index b58dabab..9b5ff27b 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -77,7 +77,7 @@ class Linker { wfDeprecated( __METHOD__, '1.25' ); $title = urldecode( $title ); - $title = str_replace( '_', ' ', $title ); + $title = strtr( $title, '_', ' ' ); return self::getLinkAttributesInternal( $title, $class ); } @@ -1276,9 +1276,11 @@ class Linker { * @param string $comment * @param Title|null $title Title object (to generate link to the section in autocomment) or null * @param bool $local Whether section links should refer to local page + * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to. For use with external changes. + * * @return mixed|string */ - public static function formatComment( $comment, $title = null, $local = false ) { + public static function formatComment( $comment, $title = null, $local = false, $wikiId = null ) { # Sanitize text a bit: $comment = str_replace( "\n", " ", $comment ); @@ -1286,8 +1288,8 @@ class Linker { $comment = Sanitizer::escapeHtmlAllowEntities( $comment ); # Render autocomments and make links: - $comment = self::formatAutocomments( $comment, $title, $local ); - $comment = self::formatLinksInComment( $comment, $title, $local ); + $comment = self::formatAutocomments( $comment, $title, $local, $wikiId ); + $comment = self::formatLinksInComment( $comment, $title, $local, $wikiId ); return $comment; } @@ -1304,9 +1306,11 @@ class Linker { * @param string $comment Comment text * @param Title|null $title An optional title object used to links to sections * @param bool $local Whether section links should refer to local page - * @return string Formatted comment + * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap. + * + * @return string Formatted comment (wikitext) */ - private static function formatAutocomments( $comment, $title = null, $local = false ) { + private static function formatAutocomments( $comment, $title = null, $local = false, $wikiId = null ) { // @todo $append here is something of a hack to preserve the status // quo. Someone who knows more about bidi and such should decide // (1) what sane rendering even *is* for an LTR edit summary on an RTL @@ -1320,7 +1324,7 @@ class Linker { // zero-width assertions optional, so wrap them in a non-capturing // group. '!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!', - function ( $match ) use ( $title, $local, &$append ) { + function ( $match ) use ( $title, $local, $wikiId, &$append ) { global $wgLang; // Ensure all match positions are defined @@ -1330,7 +1334,7 @@ class Linker { $auto = $match[2]; $post = $match[3] !== ''; $comment = null; - Hooks::run( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local ) ); + Hooks::run( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local, $wikiId ) ); if ( $comment === null ) { $link = ''; if ( $title ) { @@ -1349,9 +1353,7 @@ class Linker { $title->getDBkey(), $section ); } if ( $sectionTitle ) { - $link = Linker::link( $sectionTitle, - $wgLang->getArrow(), array(), array(), - 'noclasses' ); + $link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' ); } else { $link = ''; } @@ -1384,7 +1386,7 @@ class Linker { * @param string $comment Text to format links in * @param Title|null $title An optional title object used to links to sections * @param bool $local Whether section links should refer to local page - * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap + * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap. * * @return string */ @@ -1414,10 +1416,9 @@ class Linker { # fix up urlencoded title texts (copied from Parser::replaceInternalLinks) if ( strpos( $match[1], '%' ) !== false ) { - $match[1] = str_replace( - array( '<', '>' ), - array( '<', '>' ), - rawurldecode( $match[1] ) + $match[1] = strtr( + rawurldecode( $match[1] ), + array( '<' => '<', '>' => '>' ) ); } @@ -1460,22 +1461,9 @@ class Linker { $newTarget = clone ( $title ); $newTarget->setFragment( '#' . $target->getFragment() ); $target = $newTarget; - - } - - if ( $wikiId !== null ) { - $thelink = Linker::makeExternalLink( - WikiMap::getForeignURL( $wikiId, $target->getFullText() ), - $linkText . $inside, - /* escape = */ false // Already escaped - ) . $trail; - } else { - $thelink = Linker::link( - $target, - $linkText . $inside - ) . $trail; } + $thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail; } } if ( $thelink ) { @@ -1494,6 +1482,32 @@ class Linker { ); } + /** + * Generates a link to the given Title + * + * @note This is only public for technical reasons. It's not intended for use outside Linker. + * + * @param Title $title + * @param string $text + * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap. + * @param string|string[] $options See the $options parameter in Linker::link. + * + * @return string HTML link + */ + public static function makeCommentLink( Title $title, $text, $wikiId = null, $options = array() ) { + if ( $wikiId !== null && !$title->isExternal() ) { + $link = Linker::makeExternalLink( + WikiMap::getForeignURL( $wikiId, $title->getPrefixedText(), $title->getFragment() ), + $text, + /* escape = */ false // Already escaped + ); + } else { + $link = Linker::link( $title, $text, array(), array(), $options ); + } + + return $link; + } + /** * @param Title $contextTitle * @param string $target @@ -1580,17 +1594,18 @@ class Linker { * @param string $comment * @param Title|null $title Title object (to generate link to section in autocomment) or null * @param bool $local Whether section links should refer to local page + * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to. For use with external changes. * * @return string */ - public static function commentBlock( $comment, $title = null, $local = false ) { + public static function commentBlock( $comment, $title = null, $local = false, $wikiId = null ) { // '*' used to be the comment inserted by the software way back // in antiquity in case none was provided, here for backwards // compatibility, acc. to brion -ævar if ( $comment == '' || $comment == '*' ) { return ''; } else { - $formatted = self::formatComment( $comment, $title, $local ); + $formatted = self::formatComment( $comment, $title, $local, $wikiId ); $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped(); return " $formatted"; } @@ -1705,13 +1720,13 @@ class Linker { } /** - * Generate a table of contents from a section tree - * Currently unused. + * Generate a table of contents from a section tree. * * @param array $tree Return value of ParserOutput::getSections() + * @param string|Language|bool $lang Language for the toc title, defaults to user language * @return string HTML fragment */ - public static function generateTOC( $tree ) { + public static function generateTOC( $tree, $lang = false ) { $toc = ''; $lastLevel = 0; foreach ( $tree as $section ) { @@ -1730,7 +1745,7 @@ class Linker { $lastLevel = $section['toclevel']; } $toc .= self::tocLineEnd(); - return self::tocList( $toc ); + return self::tocList( $toc, $lang ); } /** @@ -2383,6 +2398,7 @@ class Linker { 'title' => $tooltip ) ); } + } /** diff --git a/includes/MWNamespace.php b/includes/MWNamespace.php index e370bf10..8ca205ab 100644 --- a/includes/MWNamespace.php +++ b/includes/MWNamespace.php @@ -298,6 +298,18 @@ class MWNamespace { return $index == NS_MAIN || in_array( $index, $wgContentNamespaces ); } + /** + * Might pages in this namespace require the use of the Signature button on + * the edit toolbar? + * + * @param int $index Index to check + * @return bool + */ + public static function wantSignatures( $index ) { + global $wgExtraSignatureNamespaces; + return self::isTalk( $index ) || in_array( $index, $wgExtraSignatureNamespaces ); + } + /** * Can pages in a namespace be watched? * diff --git a/includes/MWTimestamp.php b/includes/MWTimestamp.php index ea91470e..d28f88e5 100644 --- a/includes/MWTimestamp.php +++ b/includes/MWTimestamp.php @@ -56,7 +56,7 @@ class MWTimestamp { * * @since 1.20 * - * @param bool|string $timestamp Timestamp to set, or false for current time + * @param bool|string|int|float $timestamp Timestamp to set, or false for current time */ public function __construct( $timestamp = false ) { $this->setTimestamp( $timestamp ); @@ -74,6 +74,7 @@ class MWTimestamp { * @throws TimestampException */ public function setTimestamp( $ts = false ) { + $m = array(); $da = array(); $strtime = ''; @@ -87,9 +88,9 @@ class MWTimestamp { # TS_EXIF } elseif ( preg_match( '/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D', $ts, $da ) ) { # TS_MW - } elseif ( preg_match( '/^-?\d{1,13}$/D', $ts ) ) { + } elseif ( preg_match( '/^(-?\d{1,13})(\.\d+)?$/D', $ts, $m ) ) { # TS_UNIX - $strtime = "@$ts"; // http://php.net/manual/en/datetime.formats.compound.php + $strtime = "@{$m[1]}"; // http://php.net/manual/en/datetime.formats.compound.php } elseif ( preg_match( '/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}.\d{6}$/', $ts ) ) { # TS_ORACLE // session altered to DD-MM-YYYY HH24:MI:SS.FF6 $strtime = preg_replace( '/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3", @@ -199,42 +200,19 @@ class MWTimestamp { * * @since 1.20 * @since 1.22 Uses Language::getHumanTimestamp to produce the timestamp + * @deprecated since 1.26 Use Language::getHumanTimestamp directly * - * @param MWTimestamp|null $relativeTo The base timestamp to compare to - * (defaults to now). - * @param User|null $user User the timestamp is being generated for (or null - * to use main context's user). - * @param Language|null $lang Language to use to make the human timestamp - * (or null to use main context's language). + * @param MWTimestamp|null $relativeTo The base timestamp to compare to (defaults to now) + * @param User|null $user User the timestamp is being generated for (or null to use main context's user) + * @param Language|null $lang Language to use to make the human timestamp (or null to use main context's language) * @return string Formatted timestamp */ - public function getHumanTimestamp( MWTimestamp $relativeTo = null, - User $user = null, Language $lang = null - ) { - if ( $relativeTo === null ) { - $relativeTo = new self(); - } - if ( $user === null ) { - $user = RequestContext::getMain()->getUser(); - } + public function getHumanTimestamp( MWTimestamp $relativeTo = null, User $user = null, Language $lang = null ) { if ( $lang === null ) { $lang = RequestContext::getMain()->getLanguage(); } - // Adjust for the user's timezone. - $offsetThis = $this->offsetForUser( $user ); - $offsetRel = $relativeTo->offsetForUser( $user ); - - $ts = ''; - if ( Hooks::run( 'GetHumanTimestamp', array( &$ts, $this, $relativeTo, $user, $lang ) ) ) { - $ts = $lang->getHumanTimestamp( $this, $relativeTo, $user ); - } - - // Reset the timezone on the objects. - $this->timestamp->sub( $offsetThis ); - $relativeTo->timestamp->sub( $offsetRel ); - - return $ts; + return $lang->getHumanTimestamp( $this, $relativeTo, $user ); } /** diff --git a/includes/MagicWord.php b/includes/MagicWord.php index 186821de..2c7ba91b 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -718,9 +718,6 @@ class MagicWordArray { private $regex; - /** @todo Unused? */ - private $matches; - /** * @param array $names */ @@ -953,10 +950,12 @@ class MagicWordArray { if ( $regex === '' ) { continue; } - preg_match_all( $regex, $text, $matches, PREG_SET_ORDER ); - foreach ( $matches as $m ) { - list( $name, $param ) = $this->parseMatch( $m ); - $found[$name] = $param; + $matches = array(); + if ( preg_match_all( $regex, $text, $matches, PREG_SET_ORDER ) ) { + foreach ( $matches as $m ) { + list( $name, $param ) = $this->parseMatch( $m ); + $found[$name] = $param; + } } $text = preg_replace( $regex, '', $text ); } diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index ec2f40f6..fbacb250 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -51,6 +51,7 @@ class MediaWiki { /** * Parse the request to get the Title object * + * @throws MalformedTitleException If a title has been provided by the user, but is invalid. * @return Title Title object to be $wgTitle */ private function parseTitle() { @@ -110,7 +111,10 @@ class MediaWiki { } if ( $ret === null || ( $ret->getDBkey() == '' && !$ret->isExternal() ) ) { - $ret = SpecialPage::getTitleFor( 'Badtitle' ); + // If we get here, we definitely don't have a valid title; throw an exception. + // Try to get detailed invalid title exception first, fall back to MalformedTitleException. + Title::newFromTextThrow( $title ); + throw new MalformedTitleException( 'badtitletext', $title ); } return $ret; @@ -122,7 +126,11 @@ class MediaWiki { */ public function getTitle() { if ( !$this->context->hasTitle() ) { - $this->context->setTitle( $this->parseTitle() ); + try { + $this->context->setTitle( $this->parseTitle() ); + } catch ( MalformedTitleException $ex ) { + $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) ); + } } return $this->context->getTitle(); } @@ -174,6 +182,11 @@ class MediaWiki { || $title->isSpecial( 'Badtitle' ) ) { $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) ); + try { + $this->parseTitle(); + } catch ( MalformedTitleException $ex ) { + throw new BadTitleError( $ex ); + } throw new BadTitleError(); } @@ -219,65 +232,116 @@ class MediaWiki { $output->redirect( $url, 301 ); } else { $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) ); - throw new BadTitleError(); - } - // Redirect loops, no title in URL, $wgUsePathInfo URLs, and URLs with a variant - } elseif ( $request->getVal( 'action', 'view' ) == 'view' && !$request->wasPosted() - && ( $request->getVal( 'title' ) === null - || $title->getPrefixedDBkey() != $request->getVal( 'title' ) ) - && !count( $request->getValueNames( array( 'action', 'title' ) ) ) - && Hooks::run( 'TestCanonicalRedirect', array( $request, $title, $output ) ) - ) { - if ( $title->isSpecialPage() ) { - list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); - if ( $name ) { - $title = SpecialPage::getTitleFor( $name, $subpage ); + try { + $this->parseTitle(); + } catch ( MalformedTitleException $ex ) { + throw new BadTitleError( $ex ); } + throw new BadTitleError(); } - $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); - // Redirect to canonical url, make it a 301 to allow caching - if ( $targetUrl == $request->getFullRequestURL() ) { - $message = "Redirect loop detected!\n\n" . - "This means the wiki got confused about what page was " . - "requested; this sometimes happens when moving a wiki " . - "to a new server or changing the server configuration.\n\n"; - - if ( $this->config->get( 'UsePathInfo' ) ) { - $message .= "The wiki is trying to interpret the page " . - "title from the URL path portion (PATH_INFO), which " . - "sometimes fails depending on the web server. Try " . - "setting \"\$wgUsePathInfo = false;\" in your " . - "LocalSettings.php, or check that \$wgArticlePath " . - "is correct."; + // Handle any other redirects. + // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant + } elseif ( !$this->tryNormaliseRedirect( $title ) ) { + + // Special pages + if ( NS_SPECIAL == $title->getNamespace() ) { + // Actions that need to be made when we have a special pages + SpecialPageFactory::executePath( $title, $this->context ); + } else { + // ...otherwise treat it as an article view. The article + // may still be a wikipage redirect to another article or URL. + $article = $this->initializeArticle(); + if ( is_object( $article ) ) { + $this->performAction( $article, $requestTitle ); + } elseif ( is_string( $article ) ) { + $output->redirect( $article ); } else { - $message .= "Your web server was detected as possibly not " . - "supporting URL path components (PATH_INFO) correctly; " . - "check your LocalSettings.php for a customized " . - "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " . - "to true."; + throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()" + . " returned neither an object nor a URL" ); } - throw new HttpError( 500, $message ); - } else { - $output->setSquidMaxage( 1200 ); - $output->redirect( $targetUrl, '301' ); } - // Special pages - } elseif ( NS_SPECIAL == $title->getNamespace() ) { - // Actions that need to be made when we have a special pages - SpecialPageFactory::executePath( $title, $this->context ); - } else { - // ...otherwise treat it as an article view. The article - // may be a redirect to another article or URL. - $article = $this->initializeArticle(); - if ( is_object( $article ) ) { - $this->performAction( $article, $requestTitle ); - } elseif ( is_string( $article ) ) { - $output->redirect( $article ); + } + } + + /** + * Handle redirects for uncanonical title requests. + * + * Handles: + * - Redirect loops. + * - No title in URL. + * - $wgUsePathInfo URLs. + * - URLs with a variant. + * - Other non-standard URLs (as long as they have no extra query parameters). + * + * Behaviour: + * - Normalise title values: + * /wiki/Foo%20Bar -> /wiki/Foo_Bar + * - Normalise empty title: + * /wiki/ -> /wiki/Main + * /w/index.php?title= -> /wiki/Main + * - Normalise non-standard title urls: + * /w/index.php?title=Foo_Bar -> /wiki/Foo_Bar + * - Don't redirect anything with query parameters other than 'title' or 'action=view'. + * + * @param Title $title + * @return bool True if a redirect was set. + * @throws HttpError + */ + private function tryNormaliseRedirect( Title $title ) { + $request = $this->context->getRequest(); + $output = $this->context->getOutput(); + + if ( $request->getVal( 'action', 'view' ) != 'view' + || $request->wasPosted() + || count( $request->getValueNames( array( 'action', 'title' ) ) ) + || !Hooks::run( 'TestCanonicalRedirect', array( $request, $title, $output ) ) + ) { + return false; + } + + if ( $title->isSpecialPage() ) { + list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); + if ( $name ) { + $title = SpecialPage::getTitleFor( $name, $subpage ); + } + } + // Redirect to canonical url, make it a 301 to allow caching + $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); + + if ( $targetUrl != $request->getFullRequestURL() ) { + $output->setSquidMaxage( 1200 ); + $output->redirect( $targetUrl, '301' ); + return true; + } + + // If there is no title, or the title is in a non-standard encoding, we demand + // a redirect. If cgi somehow changed the 'title' query to be non-standard while + // the url is standard, the server is misconfigured. + if ( $request->getVal( 'title' ) === null + || $title->getPrefixedDBkey() != $request->getVal( 'title' ) + ) { + $message = "Redirect loop detected!\n\n" . + "This means the wiki got confused about what page was " . + "requested; this sometimes happens when moving a wiki " . + "to a new server or changing the server configuration.\n\n"; + + if ( $this->config->get( 'UsePathInfo' ) ) { + $message .= "The wiki is trying to interpret the page " . + "title from the URL path portion (PATH_INFO), which " . + "sometimes fails depending on the web server. Try " . + "setting \"\$wgUsePathInfo = false;\" in your " . + "LocalSettings.php, or check that \$wgArticlePath " . + "is correct."; } else { - throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()" - . " returned neither an object nor a URL" ); + $message .= "Your web server was detected as possibly not " . + "supporting URL path components (PATH_INFO) correctly; " . + "check your LocalSettings.php for a customized " . + "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " . + "to true."; } + throw new HttpError( 500, $message ); } + return false; } /** @@ -301,9 +365,8 @@ class MediaWiki { $this->context->setWikiPage( $article->getPage() ); } - // NS_MEDIAWIKI has no redirects. - // It is also used for CSS/JS, so performance matters here... - if ( $title->getNamespace() == NS_MEDIAWIKI ) { + // Skip some unnecessary code if the content model doesn't support redirects + if ( !ContentHandler::getForTitle( $title )->supportsRedirects() ) { return $article; } @@ -404,8 +467,7 @@ class MediaWiki { } /** - * Run the current MediaWiki instance - * index.php just calls this + * Run the current MediaWiki instance; index.php just calls this */ public function run() { try { @@ -416,16 +478,71 @@ class MediaWiki { // Bug 62091: while exceptions are convenient to bubble up GUI errors, // they are not internal application faults. As with normal requests, this // should commit, print the output, do deferred updates, jobs, and profiling. - wfGetLBFactory()->commitMasterChanges(); + $this->doPreOutputCommit(); $e->report(); // display the GUI error } + } catch ( Exception $e ) { + MWExceptionHandler::handleException( $e ); + } + + $this->doPostOutputShutdown( 'normal' ); + } + + /** + * This function commits all DB changes as needed before + * the user can receive a response (in case commit fails) + * + * @since 1.26 + */ + public function doPreOutputCommit() { + // Either all DBs should commit or none + ignore_user_abort( true ); + + // Commit all changes and record ChronologyProtector positions + $factory = wfGetLBFactory(); + $factory->commitMasterChanges(); + $factory->shutdown(); + + wfDebug( __METHOD__ . ' completed; all transactions committed' ); + } + + /** + * This function does work that can be done *after* the + * user gets the HTTP response so they don't block on it + * + * This manages deferred updates, job insertion, + * final commit, and the logging of profiling data + * + * @param string $mode Use 'fast' to always skip job running + * @since 1.26 + */ + public function doPostOutputShutdown( $mode = 'normal' ) { + // Show visible profiling data if enabled (which cannot be post-send) + Profiler::instance()->logDataPageOutputOnly(); + + $that = $this; + $callback = function () use ( $that, $mode ) { + try { + $that->restInPeace( $mode ); + } catch ( Exception $e ) { + MWExceptionHandler::handleException( $e ); + } + }; + + // Defer everything else... + if ( function_exists( 'register_postsend_function' ) ) { + // https://github.com/facebook/hhvm/issues/1230 + register_postsend_function( $callback ); + } else { if ( function_exists( 'fastcgi_finish_request' ) ) { fastcgi_finish_request(); + } else { + // Either all DB and deferred updates should happen or none. + // The later should not be cancelled due to client disconnect. + ignore_user_abort( true ); } - $this->triggerJobs(); - $this->restInPeace(); - } catch ( Exception $e ) { - MWExceptionHandler::handleException( $e ); + + $callback(); } } @@ -440,7 +557,7 @@ class MediaWiki { list( $host, $lag ) = wfGetLB()->getMaxLag(); if ( $lag > $maxLag ) { $resp = $this->context->getRequest()->response(); - $resp->header( 'HTTP/1.1 503 Service Unavailable' ); + $resp->statusHeader( 503 ); $resp->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) ); $resp->header( 'X-Database-Lag: ' . intval( $lag ) ); $resp->header( 'Content-Type: text/plain' ); @@ -457,7 +574,7 @@ class MediaWiki { } private function main() { - global $wgTitle; + global $wgTitle, $wgTrxProfilerLimits; $request = $this->context->getRequest(); @@ -489,10 +606,9 @@ class MediaWiki { if ( !$request->wasPosted() && in_array( $action, array( 'view', 'edit', 'history' ) ) ) { - $trxProfiler->setExpectation( 'masterConns', 0, __METHOD__ ); - $trxProfiler->setExpectation( 'writes', 0, __METHOD__ ); + $trxProfiler->setExpectations( $wgTrxProfilerLimits['GET'], __METHOD__ ); } else { - $trxProfiler->setExpectation( 'maxAffected', 500, __METHOD__ ); + $trxProfiler->setExpectations( $wgTrxProfilerLimits['POST'], __METHOD__ ); } // If the user has forceHTTPS set to true, or if the user @@ -565,22 +681,23 @@ class MediaWiki { // Actually do the work of the request and build up any output $this->performRequest(); - // Either all DB and deferred updates should happen or none. - // The later should not be cancelled due to client disconnect. - ignore_user_abort( true ); // Now commit any transactions, so that unreported errors after - // output() don't roll back the whole DB transaction - wfGetLBFactory()->commitMasterChanges(); + // output() don't roll back the whole DB transaction and so that + // we avoid having both success and error text in the response + $this->doPreOutputCommit(); // Output everything! $this->context->getOutput()->output(); - } /** * Ends this task peacefully + * @param string $mode Use 'fast' to always skip job running */ - public function restInPeace() { + public function restInPeace( $mode = 'fast' ) { + // Assure deferred updates are not in the main transaction + wfGetLBFactory()->commitMasterChanges(); + // Ignore things like master queries/connections on GET requests // as long as they are in deferred updates (which catch errors). Profiler::instance()->getTransactionProfiler()->resetExpectations(); @@ -588,6 +705,15 @@ class MediaWiki { // Do any deferred jobs DeferredUpdates::doUpdates( 'commit' ); + // Make sure any lazy jobs are pushed + JobQueueGroup::pushLazyJobs(); + + // Now that everything specific to this request is done, + // try to occasionally run jobs (if enabled) from the queues + if ( $mode === 'normal' ) { + $this->triggerJobs(); + } + // Log profiling data, e.g. in the database or UDP wfLogProfilingData(); @@ -604,7 +730,7 @@ class MediaWiki { * to run a specified number of jobs. This registers a callback to cleanup * the socket once it's done. */ - protected function triggerJobs() { + public function triggerJobs() { $jobRunRate = $this->config->get( 'JobRunRate' ); if ( $jobRunRate <= 0 || wfReadOnly() ) { return; @@ -647,7 +773,7 @@ class MediaWiki { $errno = $errstr = null; $info = wfParseUrl( $this->config->get( 'Server' ) ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $sock = fsockopen( $info['host'], isset( $info['port'] ) ? $info['port'] : 80, @@ -657,7 +783,7 @@ class MediaWiki { // is a problem elsewhere. 0.1 ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$sock ) { $runJobsLogger->error( "Failed to start cron API (socket error $errno): $errstr" ); // Fall back to running the job here while the user waits diff --git a/includes/Message.php b/includes/Message.php index 134af0ed..54abfd15 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -156,7 +156,7 @@ * * @since 1.17 */ -class Message implements MessageSpecifier { +class Message implements MessageSpecifier, Serializable { /** * In which language to get this message. True, which is the default, @@ -226,8 +226,9 @@ class Message implements MessageSpecifier { /** * @since 1.17 * - * @param string|string[] $key Message key or array of message keys to try and use the first - * non-empty message for. + * @param string|string[]|MessageSpecifier $key Message key, or array of + * message keys to try and use the first non-empty message for, or a + * MessageSpecifier to copy from. * @param array $params Message parameters. * @param Language $language Optional language of the message, defaults to $wgLang. * @@ -236,6 +237,16 @@ class Message implements MessageSpecifier { public function __construct( $key, $params = array(), Language $language = null ) { global $wgLang; + if ( $key instanceof MessageSpecifier ) { + if ( $params ) { + throw new InvalidArgumentException( + '$params must be empty if $key is a MessageSpecifier' + ); + } + $params = $key->getParams(); + $key = $key->getKey(); + } + if ( !is_string( $key ) && !is_array( $key ) ) { throw new InvalidArgumentException( '$key must be a string or an array' ); } @@ -252,6 +263,41 @@ class Message implements MessageSpecifier { $this->language = $language ?: $wgLang; } + /** + * @see Serializable::serialize() + * @since 1.26 + * @return string + */ + public function serialize() { + return serialize( array( + 'interface' => $this->interface, + 'language' => $this->language->getCode(), + 'key' => $this->key, + 'keysToTry' => $this->keysToTry, + 'parameters' => $this->parameters, + 'format' => $this->format, + 'useDatabase' => $this->useDatabase, + 'title' => $this->title, + ) ); + } + + /** + * @see Serializable::unserialize() + * @since 1.26 + * @param string $serialized + */ + public function unserialize( $serialized ) { + $data = unserialize( $serialized ); + $this->interface = $data['interface']; + $this->key = $data['key']; + $this->keysToTry = $data['keysToTry']; + $this->parameters = $data['parameters']; + $this->format = $data['format']; + $this->useDatabase = $data['useDatabase']; + $this->language = Language::factory( $data['language'] ); + $this->title = $data['title']; + } + /** * @since 1.24 * @@ -327,7 +373,7 @@ class Message implements MessageSpecifier { * * @since 1.17 * - * @param string|string[] $key Message key or array of keys. + * @param string|string[]|MessageSpecifier $key * @param mixed $param,... Parameters as strings. * * @return Message @@ -364,6 +410,31 @@ class Message implements MessageSpecifier { return new self( $keys ); } + /** + * Get a title object for a mediawiki message, where it can be found in the mediawiki namespace. + * The title will be for the current language, if the message key is in + * $wgForceUIMsgAsContentMsg it will be append with the language code (except content + * language), because Message::inContentLanguage will also return in user language. + * + * @see $wgForceUIMsgAsContentMsg + * @return Title + * @since 1.26 + */ + public function getTitle() { + global $wgContLang, $wgForceUIMsgAsContentMsg; + + $code = $this->language->getCode(); + $title = $this->key; + if ( + $wgContLang->getCode() !== $code + && in_array( $this->key, (array)$wgForceUIMsgAsContentMsg ) + ) { + $title .= '/' . $code; + } + + return Title::makeTitle( NS_MEDIAWIKI, $wgContLang->ucfirst( strtr( $title, ' ', '_' ) ) ); + } + /** * Adds parameters to the parameter list of this message. * @@ -597,7 +668,7 @@ class Message implements MessageSpecifier { if ( $lang instanceof Language || $lang instanceof StubUserLang ) { $this->language = $lang; } elseif ( is_string( $lang ) ) { - if ( $this->language->getCode() != $lang ) { + if ( !$this->language instanceof Language || $this->language->getCode() != $lang ) { $this->language = Language::factory( $lang ); } } else { diff --git a/includes/MessageBlobStore.php b/includes/MessageBlobStore.php deleted file mode 100644 index 011cae66..00000000 --- a/includes/MessageBlobStore.php +++ /dev/null @@ -1,390 +0,0 @@ -getFromDB( $resourceLoader, array_keys( $modules ), $lang ); - - // Generate blobs for any missing modules and store them in the DB - $missing = array_diff( array_keys( $modules ), array_keys( $blobs ) ); - foreach ( $missing as $name ) { - $blob = $this->insertMessageBlob( $name, $modules[$name], $lang ); - if ( $blob ) { - $blobs[$name] = $blob; - } - } - - return $blobs; - } - - /** - * Generate and insert a new message blob. If the blob was already - * present, it is not regenerated; instead, the preexisting blob - * is fetched and returned. - * - * @param string $name Module name - * @param ResourceLoaderModule $module - * @param string $lang Language code - * @return mixed Message blob or false if the module has no messages - */ - public function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) { - $blob = $this->generateMessageBlob( $module, $lang ); - - if ( !$blob ) { - return false; - } - - try { - $dbw = wfGetDB( DB_MASTER ); - $success = $dbw->insert( 'msg_resource', array( - 'mr_lang' => $lang, - 'mr_resource' => $name, - 'mr_blob' => $blob, - 'mr_timestamp' => $dbw->timestamp() - ), - __METHOD__, - array( 'IGNORE' ) - ); - - if ( $success ) { - if ( $dbw->affectedRows() == 0 ) { - // Blob was already present, fetch it - $blob = $dbw->selectField( 'msg_resource', 'mr_blob', array( - 'mr_resource' => $name, - 'mr_lang' => $lang, - ), - __METHOD__ - ); - } else { - // Update msg_resource_links - $rows = array(); - - foreach ( $module->getMessages() as $key ) { - $rows[] = array( - 'mrl_resource' => $name, - 'mrl_message' => $key - ); - } - $dbw->insert( 'msg_resource_links', $rows, - __METHOD__, array( 'IGNORE' ) - ); - } - } - } catch ( DBError $e ) { - wfDebug( __METHOD__ . " failed to update DB: $e\n" ); - } - return $blob; - } - - /** - * Update the message blob for a given module in a given language - * - * @param string $name Module name - * @param ResourceLoaderModule $module - * @param string $lang Language code - * @return string Regenerated message blob, or null if there was no blob for - * the given module/language pair. - */ - public function updateModule( $name, ResourceLoaderModule $module, $lang ) { - $dbw = wfGetDB( DB_MASTER ); - $row = $dbw->selectRow( 'msg_resource', 'mr_blob', - array( 'mr_resource' => $name, 'mr_lang' => $lang ), - __METHOD__ - ); - if ( !$row ) { - return null; - } - - // Save the old and new blobs for later - $oldBlob = $row->mr_blob; - $newBlob = $this->generateMessageBlob( $module, $lang ); - - try { - $newRow = array( - 'mr_resource' => $name, - 'mr_lang' => $lang, - 'mr_blob' => $newBlob, - 'mr_timestamp' => $dbw->timestamp() - ); - - $dbw->replace( 'msg_resource', - array( array( 'mr_resource', 'mr_lang' ) ), - $newRow, __METHOD__ - ); - - // Figure out which messages were added and removed - $oldMessages = array_keys( FormatJson::decode( $oldBlob, true ) ); - $newMessages = array_keys( FormatJson::decode( $newBlob, true ) ); - $added = array_diff( $newMessages, $oldMessages ); - $removed = array_diff( $oldMessages, $newMessages ); - - // Delete removed messages, insert added ones - if ( $removed ) { - $dbw->delete( 'msg_resource_links', array( - 'mrl_resource' => $name, - 'mrl_message' => $removed - ), __METHOD__ - ); - } - - $newLinksRows = array(); - - foreach ( $added as $message ) { - $newLinksRows[] = array( - 'mrl_resource' => $name, - 'mrl_message' => $message - ); - } - - if ( $newLinksRows ) { - $dbw->insert( 'msg_resource_links', $newLinksRows, __METHOD__, - array( 'IGNORE' ) // just in case - ); - } - } catch ( Exception $e ) { - wfDebug( __METHOD__ . " failed to update DB: $e\n" ); - } - return $newBlob; - } - - /** - * Update a single message in all message blobs it occurs in. - * - * @param string $key Message key - */ - public function updateMessage( $key ) { - try { - $dbw = wfGetDB( DB_MASTER ); - - // Keep running until the updates queue is empty. - // Due to update conflicts, the queue might not be emptied - // in one iteration. - $updates = null; - do { - $updates = $this->getUpdatesForMessage( $key, $updates ); - - foreach ( $updates as $k => $update ) { - // Update the row on the condition that it - // didn't change since we fetched it by putting - // the timestamp in the WHERE clause. - $success = $dbw->update( 'msg_resource', - array( - 'mr_blob' => $update['newBlob'], - 'mr_timestamp' => $dbw->timestamp() ), - array( - 'mr_resource' => $update['resource'], - 'mr_lang' => $update['lang'], - 'mr_timestamp' => $update['timestamp'] ), - __METHOD__ - ); - - // Only requeue conflicted updates. - // If update() returned false, don't retry, for - // fear of getting into an infinite loop - if ( !( $success && $dbw->affectedRows() == 0 ) ) { - // Not conflicted - unset( $updates[$k] ); - } - } - } while ( count( $updates ) ); - - // No need to update msg_resource_links because we didn't add - // or remove any messages, we just changed their contents. - } catch ( Exception $e ) { - wfDebug( __METHOD__ . " failed to update DB: $e\n" ); - } - } - - public function clear() { - // TODO: Give this some more thought - try { - // Not using TRUNCATE, because that needs extra permissions, - // which maybe not granted to the database user. - $dbw = wfGetDB( DB_MASTER ); - $dbw->delete( 'msg_resource', '*', __METHOD__ ); - $dbw->delete( 'msg_resource_links', '*', __METHOD__ ); - } catch ( Exception $e ) { - wfDebug( __METHOD__ . " failed to update DB: $e\n" ); - } - } - - /** - * Create an update queue for updateMessage() - * - * @param string $key Message key - * @param array $prevUpdates Updates queue to refresh or null to build a fresh update queue - * @return array Updates queue - */ - private function getUpdatesForMessage( $key, $prevUpdates = null ) { - $dbw = wfGetDB( DB_MASTER ); - - if ( is_null( $prevUpdates ) ) { - // Fetch all blobs referencing $key - $res = $dbw->select( - array( 'msg_resource', 'msg_resource_links' ), - array( 'mr_resource', 'mr_lang', 'mr_blob', 'mr_timestamp' ), - array( 'mrl_message' => $key, 'mr_resource=mrl_resource' ), - __METHOD__ - ); - } else { - // Refetch the blobs referenced by $prevUpdates - - // Reorganize the (resource, lang) pairs in the format - // expected by makeWhereFrom2d() - $twoD = array(); - - foreach ( $prevUpdates as $update ) { - $twoD[$update['resource']][$update['lang']] = true; - } - - $res = $dbw->select( 'msg_resource', - array( 'mr_resource', 'mr_lang', 'mr_blob', 'mr_timestamp' ), - $dbw->makeWhereFrom2d( $twoD, 'mr_resource', 'mr_lang' ), - __METHOD__ - ); - } - - // Build the new updates queue - $updates = array(); - - foreach ( $res as $row ) { - $updates[] = array( - 'resource' => $row->mr_resource, - 'lang' => $row->mr_lang, - 'timestamp' => $row->mr_timestamp, - 'newBlob' => $this->reencodeBlob( $row->mr_blob, $key, $row->mr_lang ) - ); - } - - return $updates; - } - - /** - * Reencode a message blob with the updated value for a message - * - * @param string $blob Message blob (JSON object) - * @param string $key Message key - * @param string $lang Language code - * @return string Message blob with $key replaced with its new value - */ - private function reencodeBlob( $blob, $key, $lang ) { - $decoded = FormatJson::decode( $blob, true ); - $decoded[$key] = wfMessage( $key )->inLanguage( $lang )->plain(); - - return FormatJson::encode( (object)$decoded ); - } - - /** - * Get the message blobs for a set of modules from the database. - * Modules whose blobs are not in the database are silently dropped. - * - * @param ResourceLoader $resourceLoader - * @param array $modules Array of module names - * @param string $lang Language code - * @throws MWException - * @return array Array mapping module names to blobs - */ - private function getFromDB( ResourceLoader $resourceLoader, $modules, $lang ) { - $config = $resourceLoader->getConfig(); - $retval = array(); - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'msg_resource', - array( 'mr_blob', 'mr_resource', 'mr_timestamp' ), - array( 'mr_resource' => $modules, 'mr_lang' => $lang ), - __METHOD__ - ); - - foreach ( $res as $row ) { - $module = $resourceLoader->getModule( $row->mr_resource ); - if ( !$module ) { - // This shouldn't be possible - throw new MWException( __METHOD__ . ' passed an invalid module name' ); - } - - // Update the module's blobs if the set of messages changed or if the blob is - // older than the CacheEpoch setting - $keys = array_keys( FormatJson::decode( $row->mr_blob, true ) ); - $values = array_values( array_unique( $module->getMessages() ) ); - if ( $keys !== $values - || wfTimestamp( TS_MW, $row->mr_timestamp ) <= $config->get( 'CacheEpoch' ) - ) { - $retval[$row->mr_resource] = $this->updateModule( $row->mr_resource, $module, $lang ); - } else { - $retval[$row->mr_resource] = $row->mr_blob; - } - } - - return $retval; - } - - /** - * Generate the message blob for a given module in a given language. - * - * @param ResourceLoaderModule $module - * @param string $lang Language code - * @return string JSON object - */ - private function generateMessageBlob( ResourceLoaderModule $module, $lang ) { - $messages = array(); - - foreach ( $module->getMessages() as $key ) { - $messages[$key] = wfMessage( $key )->inLanguage( $lang )->plain(); - } - - return FormatJson::encode( (object)$messages ); - } -} diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index ebe98a3c..2b240c3b 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -617,16 +617,18 @@ class MimeMagic { /** * Guess the MIME type from the file contents. * + * @todo Remove $ext param + * * @param string $file * @param mixed $ext * @return bool|string * @throws MWException */ - private function doGuessMimeType( $file, $ext ) { // TODO: remove $ext param + private function doGuessMimeType( $file, $ext ) { // Read a chunk of the file - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $f = fopen( $file, 'rb' ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$f ) { return 'unknown/unknown'; @@ -693,7 +695,7 @@ class MimeMagic { } /* Look for WebP */ - if ( strncmp( $head, "RIFF", 4 ) == 0 && strncmp( substr( $head, 8, 8 ), "WEBPVP8 ", 8 ) == 0 ) { + if ( strncmp( $head, "RIFF", 4 ) == 0 && strncmp( substr( $head, 8, 7 ), "WEBPVP8", 7 ) == 0 ) { wfDebug( __METHOD__ . ": recognized file as image/webp\n" ); return "image/webp"; } @@ -780,9 +782,9 @@ class MimeMagic { return $this->detectZipType( $head, $tail, $ext ); } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $gis = getimagesize( $file ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $gis && isset( $gis['mime'] ) ) { $mime = $gis['mime']; diff --git a/includes/MovePage.php b/includes/MovePage.php index de7da3f9..2cd9698c 100644 --- a/includes/MovePage.php +++ b/includes/MovePage.php @@ -64,21 +64,9 @@ class MovePage { $status->fatal( 'spamprotectiontext' ); } - # The move is allowed only if (1) the target doesn't exist, or - # (2) the target is a redirect to the source, and has no history - # (so we can undo bad moves right after they're done). - - if ( $this->newTitle->getArticleID() ) { # Target exists; check for validity - if ( !$this->isValidMoveTarget() ) { - $status->fatal( 'articleexists' ); - } - } else { - $tp = $this->newTitle->getTitleProtection(); - if ( $tp !== false ) { - if ( !$user->isAllowed( $tp['permission'] ) ) { - $status->fatal( 'cantmove-titleprotected' ); - } - } + $tp = $this->newTitle->getTitleProtection(); + if ( $tp !== false && !$user->isAllowed( $tp['permission'] ) ) { + $status->fatal( 'cantmove-titleprotected' ); } Hooks::run( 'MovePageCheckPermissions', @@ -125,6 +113,13 @@ class MovePage { $status->fatal( 'badarticleerror' ); } + # The move is allowed only if (1) the target doesn't exist, or + # (2) the target is a redirect to the source, and has no history + # (so we can undo bad moves right after they're done). + if ( $this->newTitle->getArticleID() && !$this->isValidMoveTarget() ) { + $status->fatal( 'articleexists' ); + } + // Content model checks if ( !$wgContentHandlerUseDB && $this->oldTitle->getContentModel() !== $this->newTitle->getContentModel() ) { @@ -310,8 +305,8 @@ class MovePage { __METHOD__, array( 'IGNORE' ) ); - # Update the protection log - $log = new LogPage( 'protect' ); + + // Build comment for log $comment = wfMessage( 'prot_1movedto2', $this->oldTitle->getPrefixedText(), @@ -320,14 +315,6 @@ class MovePage { if ( $reason ) { $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; } - // @todo FIXME: $params? - $logId = $log->addEntry( - 'move_prot', - $this->newTitle, - $comment, - array( $this->oldTitle->getPrefixedText() ), - $user - ); // reread inserted pr_ids for log relation $insertedPrIds = $dbw->select( @@ -340,7 +327,18 @@ class MovePage { foreach ( $insertedPrIds as $prid ) { $logRelationsValues[] = $prid->pr_id; } - $log->addRelations( 'pr_id', $logRelationsValues, $logId ); + + // Update the protection log + $logEntry = new ManualLogEntry( 'protect', 'move_prot' ); + $logEntry->setTarget( $this->newTitle ); + $logEntry->setComment( $comment ); + $logEntry->setPerformer( $user ); + $logEntry->setParameters( array( + '4::oldtitle' => $this->oldTitle->getPrefixedText(), + ) ); + $logEntry->setRelations( array( 'pr_id' => $logRelationsValues ) ); + $logId = $logEntry->insert(); + $logEntry->publish( $logId ); } // Update *_from_namespace fields as needed @@ -421,6 +419,13 @@ class MovePage { $redirectContent = null; } + // Figure out whether the content model is no longer the default + $oldDefault = ContentHandler::getDefaultModelFor( $this->oldTitle ); + $contentModel = $this->oldTitle->getContentModel(); + $newDefault = ContentHandler::getDefaultModelFor( $nt ); + $defaultContentModelChanging = ( $oldDefault !== $newDefault + && $oldDefault === $contentModel ); + // bug 57084: log_page should be the ID of the *moved* page $oldid = $this->oldTitle->getArticleID(); $logTitle = clone $this->oldTitle; @@ -498,6 +503,16 @@ class MovePage { $newpage->doEditUpdates( $nullRevision, $user, array( 'changed' => false, 'moved' => true, 'oldcountable' => $oldcountable ) ); + // If the default content model changes, we need to populate rev_content_model + if ( $defaultContentModelChanging ) { + $dbw->update( + 'revision', + array( 'rev_content_model' => $contentModel ), + array( 'rev_page' => $nt->getArticleID(), 'rev_content_model IS NULL' ), + __METHOD__ + ); + } + if ( !$moveOverRedirect ) { WikiPage::onArticleCreate( $nt ); } diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 7e671878..552e1815 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -20,6 +20,9 @@ * @file */ +use MediaWiki\Logger\LoggerFactory; +use WrappedString\WrappedString; + /** * This class should be covered by a general architecture document which does * not exist as of January 2011. This is one of the Core classes and should @@ -139,9 +142,6 @@ class OutputPage extends ContextSource { /** @var string Inline CSS styles. Use addInlineStyle() sparingly */ protected $mInlineStyles = ''; - /** @todo Unused? */ - private $mLinkColours; - /** * @var string Used by skin template. * Example: $tpl->set( 'displaytitle', $out->mPageLinkTitle ); @@ -162,9 +162,6 @@ class OutputPage extends ContextSource { /** @var array */ protected $mModuleStyles = array(); - /** @var array */ - protected $mModuleMessages = array(); - /** @var ResourceLoader */ protected $mResourceLoader; @@ -305,6 +302,11 @@ class OutputPage extends ContextSource { */ private $mEnableSectionEditLinks = true; + /** + * @var string|null The URL to send in a element with rel=copyright + */ + private $copyrightUrl; + /** * Constructor for OutputPage. This should not be called directly. * Instead a new RequestContext should be created and it will implicitly create @@ -341,6 +343,18 @@ class OutputPage extends ContextSource { return $this->mRedirect; } + /** + * Set the copyright URL to send with the output. + * Empty string to omit, null to reset. + * + * @since 1.26 + * + * @param string|null $url + */ + public function setCopyrightUrl( $url ) { + $this->copyrightUrl = $url; + } + /** * Set the HTTP status code to send with the output. * @@ -594,6 +608,20 @@ class OutputPage extends ContextSource { * @return array Array of module names */ public function getModuleStyles( $filter = false, $position = null ) { + // T97420 + $resourceLoader = $this->getResourceLoader(); + + foreach ( $this->mModuleStyles as $val ) { + $module = $resourceLoader->getModule( $val ); + + if ( $module instanceof ResourceLoaderModule && $module->isPositionDefault() ) { + $warning = __METHOD__ . ': style module should define its position explicitly: ' . + $val . ' ' . get_class( $module ); + wfDebugLog( 'resourceloader', $warning ); + wfLogWarning( $warning ); + } + } + return $this->getModules( $filter, $position, 'mModuleStyles' ); } @@ -613,24 +641,24 @@ class OutputPage extends ContextSource { /** * Get the list of module messages to include on this page * + * @deprecated since 1.26 Obsolete * @param bool $filter * @param string|null $position - * * @return array Array of module names */ public function getModuleMessages( $filter = false, $position = null ) { - return $this->getModules( $filter, $position, 'mModuleMessages' ); + wfDeprecated( __METHOD__, '1.26' ); + return array(); } /** - * Add only messages of one or more modules recognized by the resource loader. - * Module messages added through this function will be loaded by the resource - * loader when the page loads. + * Load messages of one or more ResourceLoader modules. * + * @deprecated since 1.26 Use addModules() instead * @param string|array $modules Module name (string) or array of module names */ public function addModuleMessages( $modules ) { - $this->mModuleMessages = array_merge( $this->mModuleMessages, (array)$modules ); + wfDeprecated( __METHOD__, '1.26' ); } /** @@ -797,9 +825,9 @@ class OutputPage extends ContextSource { # this breaks strtotime(). $clientHeader = preg_replace( '/;.*$/', '', $clientHeader ); - wfSuppressWarnings(); // E_STRICT system time bitching + MediaWiki\suppressWarnings(); // E_STRICT system time bitching $clientHeaderTime = strtotime( $clientHeader ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$clientHeaderTime ) { wfDebug( __METHOD__ . ": unable to parse the client's If-Modified-Since header: $clientHeader\n" ); @@ -826,10 +854,10 @@ class OutputPage extends ContextSource { } # Not modified - # Give a 304 response code and disable body output + # Give a 304 Not Modified response code and disable body output wfDebug( __METHOD__ . ": NOT MODIFIED, $info\n", 'log' ); ini_set( 'zlib.output_compression', 0 ); - $this->getRequest()->response()->header( "HTTP/1.1 304 Not Modified" ); + $this->getRequest()->response()->statusHeader( 304 ); $this->sendCacheControl(); $this->disable(); @@ -1761,7 +1789,6 @@ class OutputPage extends ContextSource { $this->addModules( $parserOutput->getModules() ); $this->addModuleScripts( $parserOutput->getModuleScripts() ); $this->addModuleStyles( $parserOutput->getModuleStyles() ); - $this->addModuleMessages( $parserOutput->getModuleMessages() ); $this->addJsConfigVars( $parserOutput->getJsConfigVars() ); $this->mPreventClickjacking = $this->mPreventClickjacking || $parserOutput->preventClickjacking(); @@ -1788,6 +1815,11 @@ class OutputPage extends ContextSource { } } + // enable OOUI if requested via ParserOutput + if ( $parserOutput->getEnableOOUI() ) { + $this->enableOOUI(); + } + // Link flags are ignored for now, but may in the future be // used to mark individual language links. $linkFlags = array(); @@ -1808,7 +1840,6 @@ class OutputPage extends ContextSource { $this->addModules( $parserOutput->getModules() ); $this->addModuleScripts( $parserOutput->getModuleScripts() ); $this->addModuleStyles( $parserOutput->getModuleStyles() ); - $this->addModuleMessages( $parserOutput->getModuleMessages() ); $this->addJsConfigVars( $parserOutput->getJsConfigVars() ); } @@ -1978,21 +2009,20 @@ class OutputPage extends ContextSource { * Add an HTTP header that will influence on the cache * * @param string $header Header name - * @param array|null $option - * @todo FIXME: Document the $option parameter; it appears to be for - * X-Vary-Options but what format is acceptable? + * @param string[]|null $option Options for X-Vary-Options. Possible options are: + * - "string-contains=$XXX" varies on whether the header value as a string + * contains $XXX as a substring. + * - "list-contains=$XXX" varies on whether the header value as a + * comma-separated list contains $XXX as one of the list items. */ - public function addVaryHeader( $header, $option = null ) { + public function addVaryHeader( $header, array $option = null ) { if ( !array_key_exists( $header, $this->mVaryHeader ) ) { - $this->mVaryHeader[$header] = (array)$option; - } elseif ( is_array( $option ) ) { - if ( is_array( $this->mVaryHeader[$header] ) ) { - $this->mVaryHeader[$header] = array_merge( $this->mVaryHeader[$header], $option ); - } else { - $this->mVaryHeader[$header] = $option; - } + $this->mVaryHeader[$header] = array(); } - $this->mVaryHeader[$header] = array_unique( (array)$this->mVaryHeader[$header] ); + if ( !is_array( $option ) ) { + $option = array(); + } + $this->mVaryHeader[$header] = array_unique( array_merge( $this->mVaryHeader[$header], $option ) ); } /** @@ -2210,8 +2240,7 @@ class OutputPage extends ContextSource { if ( Hooks::run( "BeforePageRedirect", array( $this, &$redirect, &$code ) ) ) { if ( $code == '301' || $code == '303' ) { if ( !$config->get( 'DebugRedirects' ) ) { - $message = HttpStatus::getMessage( $code ); - $response->header( "HTTP/1.1 $code $message" ); + $response->statusHeader( $code ); } $this->mLastModified = wfTimestamp( TS_RFC2822 ); } @@ -2233,10 +2262,7 @@ class OutputPage extends ContextSource { return; } elseif ( $this->mStatusCode ) { - $message = HttpStatus::getMessage( $this->mStatusCode ); - if ( $message ) { - $response->header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $message ); - } + $response->statusHeader( $this->mStatusCode ); } # Buffer output; final headers may depend on later processing @@ -2258,14 +2284,14 @@ class OutputPage extends ContextSource { if ( $this->mArticleBodyOnly ) { echo $this->mBodytext; } else { - $sk = $this->getSkin(); // add skin specific modules $modules = $sk->getDefaultModules(); - // enforce various default modules for all skins + // Enforce various default modules for all skins $coreModules = array( - // keep this list as small as possible + // Keep this list as small as possible + 'site', 'mediawiki.page.startup', 'mediawiki.user', ); @@ -2672,16 +2698,14 @@ class OutputPage extends ContextSource { } $ret .= Html::element( 'title', null, $this->getHTMLTitle() ) . "\n"; + $ret .= $this->getInlineHeadScripts() . "\n"; + $ret .= $this->buildCssLinks() . "\n"; + $ret .= $this->getExternalHeadScripts() . "\n"; foreach ( $this->getHeadLinksArray() as $item ) { $ret .= $item . "\n"; } - // No newline after buildCssLinks since makeResourceLoaderLink did that already - $ret .= $this->buildCssLinks(); - - $ret .= $this->getHeadScripts() . "\n"; - foreach ( $this->mHeadItems as $item ) { $ret .= $item . "\n"; } @@ -2729,29 +2753,31 @@ class OutputPage extends ContextSource { */ public function getResourceLoader() { if ( is_null( $this->mResourceLoader ) ) { - $this->mResourceLoader = new ResourceLoader( $this->getConfig() ); + $this->mResourceLoader = new ResourceLoader( + $this->getConfig(), + LoggerFactory::getInstance( 'resourceloader' ) + ); } return $this->mResourceLoader; } /** - * @todo Document + * Construct neccecary html and loader preset states to load modules on a page. + * + * Use getHtmlFromLoaderLinks() to convert this array to HTML. + * * @param array|string $modules One or more module names * @param string $only ResourceLoaderModule TYPE_ class constant - * @param bool $useESI - * @param array $extraQuery Array with extra query parameters to add to each - * request. array( param => value ). - * @param bool $loadCall If true, output an (asynchronous) mw.loader.load() - * call rather than a " tags to put in "". + * + * @return string HTML fragment + */ + function getInlineHeadScripts() { + $links = array(); + + // Client profile classes for . Allows for easy hiding/showing of UI components. + // Must be done synchronously on every page to avoid flashes of wrong content. + // Note: This class distinguishes MediaWiki-supported JavaScript from the rest. + // The "rest" includes browsers that support JavaScript but not supported by our runtime. + // For the performance benefit of the majority, this is added unconditionally here and is + // then fixed up by the startup module for unsupported browsers. $links[] = Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( - ResourceLoader::makeConfigSetScript( $this->getJSVars() ) - ) + 'document.documentElement.className = document.documentElement.className' + . '.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );' + ); + + // Load config before anything else + $links[] = ResourceLoader::makeInlineScript( + ResourceLoader::makeConfigSetScript( $this->getJSVars() ) ); // Load embeddable private modules before any loader links // This needs to be TYPE_COMBINED so these modules are properly wrapped // in mw.loader.implement() calls and deferred until mw.user is available - $embedScripts = array( 'user.options', 'user.tokens' ); + $embedScripts = array( 'user.options' ); $links[] = $this->makeResourceLoaderLink( $embedScripts, ResourceLoaderModule::TYPE_COMBINED ); - - // Scripts and messages "only" requests marked for top inclusion - // Messages should go first - $links[] = $this->makeResourceLoaderLink( - $this->getModuleMessages( true, 'top' ), - ResourceLoaderModule::TYPE_MESSAGES - ); - $links[] = $this->makeResourceLoaderLink( - $this->getModuleScripts( true, 'top' ), - ResourceLoaderModule::TYPE_SCRIPTS - ); + // Separate user.tokens as otherwise caching will be allowed (T84960) + $links[] = $this->makeResourceLoaderLink( 'user.tokens', ResourceLoaderModule::TYPE_COMBINED ); // Modules requests - let the client calculate dependencies and batch requests as it likes // Only load modules that have marked themselves for loading at the top $modules = $this->getModules( true, 'top' ); if ( $modules ) { - $links[] = Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( - Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) - ) + $links[] = ResourceLoader::makeInlineScript( + Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) ); } - if ( $this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) { - $links[] = $this->getScriptsForBottomQueue( true ); - } + // "Scripts only" modules marked for top inclusion + $links[] = $this->makeResourceLoaderLink( + $this->getModuleScripts( true, 'top' ), + ResourceLoaderModule::TYPE_SCRIPTS + ); return self::getHtmlFromLoaderLinks( $links ); } /** - * JS stuff to put at the 'bottom', which can either be the bottom of the - * "" or the bottom of the "" depending on - * $wgResourceLoaderExperimentalAsyncLoading: modules marked with position - * 'bottom', legacy scripts ($this->mScripts), user preferences, site JS - * and user JS. + * JS stuff to put at the 'bottom', which goes at the bottom of the ``. + * These are modules marked with position 'bottom', legacy scripts ($this->mScripts), + * site JS, and user JS. * - * @param bool $inHead If true, this HTML goes into the "", - * if false it goes into the "". + * @param bool $unused Previously used to let this method change its output based + * on whether it was called by getExternalHeadScripts() or getBottomScripts(). * @return string */ - function getScriptsForBottomQueue( $inHead ) { - // Scripts and messages "only" requests marked for bottom inclusion + function getScriptsForBottomQueue( $unused = null ) { + // Scripts "only" requests marked for bottom inclusion // If we're in the , use load() calls rather than " + ); } /** @@ -1361,11 +1404,13 @@ MESSAGE; * @return string */ public static function makeConfigSetScript( array $configuration ) { - return Xml::encodeJsCall( - 'mw.config.set', - array( $configuration ), - ResourceLoader::inDebugMode() - ); + if ( ResourceLoader::inDebugMode() ) { + return Xml::encodeJsCall( 'mw.config.set', array( $configuration ), true ); + } + + $config = RequestContext::getMain()->getConfig(); + $js = Xml::encodeJsCall( 'mw.config.set', array( $configuration ), false ); + return self::applyFilter( 'minify-js', $js, $config ); } /** @@ -1427,7 +1472,7 @@ MESSAGE; * @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) + * @return string URL to load.php. May be protocol-relative if $wgLoadScript is, too. */ public function createLoaderURL( $source, ResourceLoaderContext $context, $extraQuery = array() @@ -1435,14 +1480,12 @@ MESSAGE; $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 ); + return wfAppendQuery( $script, $query ); } /** * Build a load.php URL - * @deprecated since 1.24, use createLoaderURL instead + * @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 @@ -1453,7 +1496,7 @@ MESSAGE; * @param bool $printable Printable mode * @param bool $handheld Handheld mode * @param array $extraQuery Extra query parameters to add - * @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative) + * @return string URL to load.php. May be protocol-relative if $wgLoadScript is, too. */ public static function makeLoaderURL( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null, $printable = false, @@ -1465,9 +1508,7 @@ MESSAGE; $only, $printable, $handheld, $extraQuery ); - // Prevent the IE6 extension check from being triggered (bug 28840) - // by appending a character that's invalid in Windows extensions ('*') - return wfExpandUrl( wfAppendQuery( $wgLoadScript, $query ) . '&*', PROTO_RELATIVE ); + return wfAppendQuery( $wgLoadScript, $query ); } /** @@ -1562,27 +1603,23 @@ MESSAGE; * @param Config $config * @throws MWException * @since 1.22 - * @return lessc + * @return Less_Parser */ 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. - if ( !class_exists( 'lessc' ) ) { - throw new MWException( 'MediaWiki requires the lessphp compiler' ); - } - if ( !function_exists( 'ctype_digit' ) ) { - throw new MWException( 'lessc requires the Ctype extension' ); + if ( !class_exists( 'Less_Parser' ) ) { + throw new MWException( 'MediaWiki requires the less.php parser' ); } - $less = new lessc(); - $less->setPreserveComments( true ); - $less->setVariables( self::getLessVars( $config ) ); - $less->setImportDir( $config->get( 'ResourceLoaderLESSImportPaths' ) ); - foreach ( $config->get( 'ResourceLoaderLESSFunctions' ) as $name => $func ) { - $less->registerFunction( $name, $func ); - } - return $less; + $parser = new Less_Parser; + $parser->ModifyVars( self::getLessVars( $config ) ); + $parser->SetImportDirs( array_fill_keys( $config->get( 'ResourceLoaderLESSImportPaths' ), '' ) ); + $parser->SetOption( 'relativeUrls', false ); + $parser->SetCacheDir( $config->get( 'CacheDirectory' ) ?: wfTempDir() ); + + return $parser; } /** diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php index a6a7d347..2e1752a6 100644 --- a/includes/resourceloader/ResourceLoaderContext.php +++ b/includes/resourceloader/ResourceLoaderContext.php @@ -22,6 +22,8 @@ * @author Roan Kattouw */ +use MediaWiki\Logger\LoggerFactory; + /** * Object passed around to modules which contains information about the state * of a specific loader request @@ -57,24 +59,26 @@ class ResourceLoaderContext { $this->resourceLoader = $resourceLoader; $this->request = $request; - // Interpret request // List of modules $modules = $request->getVal( 'modules' ); $this->modules = $modules ? self::expandModuleNames( $modules ) : array(); + // Various parameters - $this->skin = $request->getVal( 'skin' ); $this->user = $request->getVal( 'user' ); $this->debug = $request->getFuzzyBool( - 'debug', $resourceLoader->getConfig()->get( 'ResourceLoaderDebug' ) + 'debug', + $resourceLoader->getConfig()->get( 'ResourceLoaderDebug' ) ); - $this->only = $request->getVal( 'only' ); - $this->version = $request->getVal( 'version' ); + $this->only = $request->getVal( 'only', null ); + $this->version = $request->getVal( 'version', null ); $this->raw = $request->getFuzzyBool( 'raw' ); + // Image requests $this->image = $request->getVal( 'image' ); $this->variant = $request->getVal( 'variant' ); $this->format = $request->getVal( 'format' ); + $this->skin = $request->getVal( 'skin' ); $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] ) ) { @@ -123,7 +127,8 @@ class ResourceLoaderContext { */ public static function newDummyContext() { return new self( new ResourceLoader( - ConfigFactory::getDefaultInstance()->makeConfig( 'main' ) + ConfigFactory::getDefaultInstance()->makeConfig( 'main' ), + LoggerFactory::getInstance( 'resourceloader' ) ), new FauxRequest( array() ) ); } @@ -154,7 +159,7 @@ class ResourceLoaderContext { public function getLanguage() { if ( $this->language === null ) { // Must be a valid language code after this point (bug 62849) - $this->language = RequestContext::sanitizeLangCode( $this->request->getVal( 'lang' ) ); + $this->language = RequestContext::sanitizeLangCode( $this->getRequest()->getVal( 'lang' ) ); } return $this->language; } @@ -164,7 +169,7 @@ class ResourceLoaderContext { */ public function getDirection() { if ( $this->direction === null ) { - $this->direction = $this->request->getVal( 'dir' ); + $this->direction = $this->getRequest()->getVal( 'dir' ); if ( !$this->direction ) { // Determine directionality based on user language (bug 6100) $this->direction = Language::factory( $this->getLanguage() )->getDir(); @@ -174,7 +179,7 @@ class ResourceLoaderContext { } /** - * @return string|null + * @return string */ public function getSkin() { return $this->skin; @@ -227,6 +232,8 @@ class ResourceLoaderContext { } /** + * @see ResourceLoaderModule::getVersionHash + * @see OutputPage::makeResourceLoaderLink * @return string|null */ public function getVersion() { @@ -285,7 +292,7 @@ class ResourceLoaderContext { return $this->imageObj; } - $image = $module->getImage( $this->image ); + $image = $module->getImage( $this->image, $this ); if ( !$image ) { return $this->imageObj; } @@ -300,21 +307,21 @@ class ResourceLoaderContext { * @return bool */ public function shouldIncludeScripts() { - return is_null( $this->getOnly() ) || $this->getOnly() === 'scripts'; + return $this->getOnly() === null || $this->getOnly() === 'scripts'; } /** * @return bool */ public function shouldIncludeStyles() { - return is_null( $this->getOnly() ) || $this->getOnly() === 'styles'; + return $this->getOnly() === null || $this->getOnly() === 'styles'; } /** * @return bool */ public function shouldIncludeMessages() { - return is_null( $this->getOnly() ) || $this->getOnly() === 'messages'; + return $this->getOnly() === null; } /** diff --git a/includes/resourceloader/ResourceLoaderEditToolbarModule.php b/includes/resourceloader/ResourceLoaderEditToolbarModule.php index d79174cd..da729fdc 100644 --- a/includes/resourceloader/ResourceLoaderEditToolbarModule.php +++ b/includes/resourceloader/ResourceLoaderEditToolbarModule.php @@ -56,7 +56,7 @@ class ResourceLoaderEditToolbarModule extends ResourceLoaderFileModule { // 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 + // less.php tries to be helpful and parse our variables as LESS source code foreach ( $vars as $key => &$value ) { $value = self::cssSerializeString( $value ); } @@ -65,25 +65,10 @@ class ResourceLoaderEditToolbarModule extends ResourceLoaderFileModule { } /** - * @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 + * @return bool */ - public function getModifiedHash( ResourceLoaderContext $context ) { - return md5( - parent::getModifiedHash( $context ) . - serialize( $this->getLessVars( $context ) ) - ); + public function enableModuleContentVersion() { + return true; } /** @@ -93,11 +78,11 @@ class ResourceLoaderEditToolbarModule extends ResourceLoaderFileModule { * * @throws MWException * @param ResourceLoaderContext $context - * @return lessc + * @return Less_Parser */ protected function getLessCompiler( ResourceLoaderContext $context = null ) { - $compiler = parent::getLessCompiler(); - $compiler->setVariables( $this->getLessVars( $context ) ); - return $compiler; + $parser = parent::getLessCompiler(); + $parser->ModifyVars( $this->getLessVars( $context ) ); + return $parser; } } diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index 671098e1..7fbc1cb4 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -143,15 +143,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { */ protected $hasGeneratedStyles = false; - /** - * @var array Cache for mtime - * @par Usage: - * @code - * array( [hash] => [mtime], [hash] => [mtime], ... ) - * @endcode - */ - protected $modifiedTime = array(); - /** * @var array Place where readStyleFile() tracks file dependencies * @par Usage: @@ -161,6 +152,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { */ protected $localFileRefs = array(); + /** + * @var array Place where readStyleFile() tracks file dependencies for non-existent files. + * Used in tests to detect missing dependencies. + */ + protected $missingLocalFileRefs = array(); + /* Methods */ /** @@ -281,8 +278,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $this->{$member} = $option; break; // Single strings - case 'group': case 'position': + $this->isPositionDefined = true; + case 'group': case 'skipFunction': $this->{$member} = (string)$option; break; @@ -486,10 +484,10 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Gets list of names of modules this module depends on. - * + * @param ResourceLoaderContext context * @return array List of module names */ - public function getDependencies() { + public function getDependencies( ResourceLoaderContext $context = null ) { return $this->dependencies; } @@ -522,24 +520,28 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { } /** - * Get the last modified timestamp of this module. + * Disable module content versioning. * - * Last modified timestamps are calculated from the highest last modified - * timestamp of this module's constituent files as well as the files it - * depends on. This function is context-sensitive, only performing - * calculations on files relevant to the given language, skin and debug - * mode. + * This class uses getDefinitionSummary() instead, to avoid filesystem overhead + * involved with building the full module content inside a startup request. * - * @param ResourceLoaderContext $context Context in which to calculate - * the modified time - * @return int UNIX timestamp - * @see ResourceLoaderModule::getFileDependencies + * @return bool */ - public function getModifiedTime( ResourceLoaderContext $context ) { - if ( isset( $this->modifiedTime[$context->getHash()] ) ) { - return $this->modifiedTime[$context->getHash()]; - } + public function enableModuleContentVersion() { + return false; + } + /** + * Helper method to gather file hashes for getDefinitionSummary. + * + * This function is context-sensitive, only computing hashes of files relevant to the + * given language, skin, etc. + * + * @see ResourceLoaderModule::getFileDependencies + * @param ResourceLoaderContext $context + * @return array + */ + protected function getFileHashes( ResourceLoaderContext $context ) { $files = array(); // Flatten style files into $files @@ -578,22 +580,10 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { // entry point Less file we already know about. $files = array_values( array_unique( $files ) ); - // 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; - return $this->modifiedTime[$context->getHash()]; - } - - $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) ); - - $this->modifiedTime[$context->getHash()] = max( - $filesMtime, - $this->getMsgBlobMtime( $context->getLanguage() ), - $this->getDefinitionMtime( $context ) - ); - - return $this->modifiedTime[$context->getHash()]; + // Don't include keys or file paths here, only the hashes. Including that would needlessly + // cause global cache invalidation when files move or if e.g. the MediaWiki path changes. + // Any significant ordering is already detected by the definition summary. + return array_map( array( __CLASS__, 'safeFileHash' ), $files ); } /** @@ -604,7 +594,17 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { */ public function getDefinitionSummary( ResourceLoaderContext $context ) { $summary = parent::getDefinitionSummary( $context ); + + $options = array(); foreach ( array( + // The following properties are omitted because they don't affect the module reponse: + // - localBasePath (Per T104950; Changes when absolute directory name changes. If + // this affects 'scripts' and other file paths, getFileHashes accounts for that.) + // - remoteBasePath (Per T104950) + // - dependencies (provided via startup module) + // - targets + // - group (provided via startup module) + // - position (only used by OutputPage) 'scripts', 'debugScripts', 'loaderScripts', @@ -612,25 +612,23 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { 'languageScripts', 'skinScripts', 'skinStyles', - 'dependencies', 'messages', - 'targets', 'templates', - 'group', - 'position', 'skipFunction', - 'localBasePath', - 'remoteBasePath', 'debugRaw', 'raw', ) as $member ) { - $summary[$member] = $this->{$member}; + $options[$member] = $this->{$member}; }; + + $summary[] = array( + 'options' => $options, + 'fileHashes' => $this->getFileHashes( $context ), + 'msgBlobMtime' => $this->getMsgBlobMtime( $context->getLanguage() ), + ); return $summary; } - /* Protected Methods */ - /** * @param string|ResourceLoaderFilePath $path * @return string @@ -925,10 +923,14 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $localDir = dirname( $localPath ); $remoteDir = dirname( $remotePath ); // Get and register local file references - $this->localFileRefs = array_merge( - $this->localFileRefs, - CSSMin::getLocalFileReferences( $style, $localDir ) - ); + $localFileRefs = CSSMin::getAllLocalFileReferences( $style, $localDir ); + foreach ( $localFileRefs as $file ) { + if ( file_exists( $file ) ) { + $this->localFileRefs[] = $file; + } else { + $this->missingLocalFileRefs[] = $file; + } + } return CSSMin::remap( $style, $localDir, $remoteDir, true ); @@ -958,17 +960,17 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * Keeps track of all used files and adds them to localFileRefs. * * @since 1.22 - * @throws Exception If lessc encounters a parse error + * @throws Exception If less.php encounters a parse error * @param string $fileName File path of LESS source - * @param lessc $compiler Compiler to use, if not default + * @param Less_Parser $parser Compiler to use, if not default * @return string CSS source */ protected function compileLessFile( $fileName, $compiler = null ) { if ( !$compiler ) { $compiler = $this->getLessCompiler(); } - $result = $compiler->compileFile( $fileName ); - $this->localFileRefs += array_keys( $compiler->allParsedFiles() ); + $result = $compiler->parseFile( $fileName )->getCss(); + $this->localFileRefs += array_keys( $compiler->AllParsedFiles() ); return $result; } @@ -980,7 +982,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * @param ResourceLoaderContext $context * @throws MWException * @since 1.24 - * @return lessc + * @return Less_Parser */ protected function getLessCompiler( ResourceLoaderContext $context = null ) { return ResourceLoader::getLessCompiler( $this->getConfig() ); diff --git a/includes/resourceloader/ResourceLoaderForeignApiModule.php b/includes/resourceloader/ResourceLoaderForeignApiModule.php new file mode 100644 index 00000000..7ed08317 --- /dev/null +++ b/includes/resourceloader/ResourceLoaderForeignApiModule.php @@ -0,0 +1,33 @@ +dependencies; + Hooks::run( 'ResourceLoaderForeignApiModules', array( &$dependencies, $context ) ); + return $dependencies; + } +} diff --git a/includes/resourceloader/ResourceLoaderImage.php b/includes/resourceloader/ResourceLoaderImage.php index 12d1e827..2338c902 100644 --- a/includes/resourceloader/ResourceLoaderImage.php +++ b/includes/resourceloader/ResourceLoaderImage.php @@ -54,15 +54,16 @@ class ResourceLoaderImage { $this->variants = $variants; // Expand shorthands: - // array( "en,de,fr" => "foo.svg" ) → array( "en" => "foo.svg", "de" => "foo.svg", "fr" => "foo.svg" ) + // array( "en,de,fr" => "foo.svg" ) + // → array( "en" => "foo.svg", "de" => "foo.svg", "fr" => "foo.svg" ) if ( is_array( $this->descriptor ) && isset( $this->descriptor['lang'] ) ) { foreach ( array_keys( $this->descriptor['lang'] ) as $langList ) { if ( strpos( $langList, ',' ) !== false ) { $this->descriptor['lang'] += array_fill_keys( explode( ',', $langList ), - $this->descriptor['lang'][ $langList ] + $this->descriptor['lang'][$langList] ); - unset( $this->descriptor['lang'][ $langList ] ); + unset( $this->descriptor['lang'][$langList] ); } } } @@ -75,11 +76,15 @@ class ResourceLoaderImage { } ); $extensions = array_unique( $extensions ); if ( count( $extensions ) !== 1 ) { - throw new InvalidArgumentException( "File type for different image files of '$name' not the same" ); + throw new InvalidArgumentException( + "File type for different image files of '$name' not the same" + ); } $ext = $extensions[0]; if ( !isset( self::$fileTypes[$ext] ) ) { - throw new InvalidArgumentException( "Invalid file type for image files of '$name' (valid: svg, png, gif, jpg)" ); + throw new InvalidArgumentException( + "Invalid file type for image files of '$name' (valid: svg, png, gif, jpg)" + ); } $this->extension = $ext; } @@ -117,14 +122,14 @@ class ResourceLoaderImage { * @param ResourceLoaderContext $context Any context * @return string */ - protected function getPath( ResourceLoaderContext $context ) { + public function getPath( ResourceLoaderContext $context ) { $desc = $this->descriptor; if ( is_string( $desc ) ) { return $this->basePath . '/' . $desc; - } elseif ( isset( $desc['lang'][ $context->getLanguage() ] ) ) { - return $this->basePath . '/' . $desc['lang'][ $context->getLanguage() ]; - } elseif ( isset( $desc[ $context->getDirection() ] ) ) { - return $this->basePath . '/' . $desc[ $context->getDirection() ]; + } elseif ( isset( $desc['lang'][$context->getLanguage()] ) ) { + return $this->basePath . '/' . $desc['lang'][$context->getLanguage()]; + } elseif ( isset( $desc[$context->getDirection()] ) ) { + return $this->basePath . '/' . $desc[$context->getDirection()]; } else { return $this->basePath . '/' . $desc['default']; } diff --git a/includes/resourceloader/ResourceLoaderImageModule.php b/includes/resourceloader/ResourceLoaderImageModule.php index bf6a7dd2..8de87f2e 100644 --- a/includes/resourceloader/ResourceLoaderImageModule.php +++ b/includes/resourceloader/ResourceLoaderImageModule.php @@ -28,6 +28,8 @@ */ class ResourceLoaderImageModule extends ResourceLoaderModule { + protected $definition = null; + /** * Local base path, see __construct() * @var string @@ -43,6 +45,9 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { protected $selectorWithVariant = '.{prefix}-{name}-{variant}'; protected $targets = array( 'desktop', 'mobile' ); + /** @var string Position on the page to load this module at */ + protected $position = 'bottom'; + /** * Constructs a new module from an options array. * @@ -57,6 +62,8 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { * array( * // Base path to prepend to all local paths in $options. Defaults to $IP * 'localBasePath' => [base path], + * // Path to JSON file that contains any of the settings below + * 'data' => [file path string] * // CSS class prefix to use in all style rules * 'prefix' => [CSS class prefix], * // Alternatively: Format of CSS selector to use in all style rules @@ -66,21 +73,29 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { * 'selectorWithVariant' => [CSS selector template, variables: {prefix} {name} {variant}], * // List of variants that may be used for the image files * 'variants' => array( + * [theme name] => array( * [variant name] => array( * 'color' => [color string, e.g. '#ffff00'], * 'global' => [boolean, if true, this variant is available * for all images of this type], * ), + * ... + * ), * ... * ), * // List of image files and their options * 'images' => array( - * [file path string], - * [file path string] => array( - * 'name' => [image name string, defaults to file name], + * [theme name] => array( + * [icon name] => array( + * 'file' => [file path string or array whose values are file path strings + * and whose keys are 'default', 'ltr', 'rtl', a single + * language code like 'en', or a list of language codes like + * 'en,de,ar'], * 'variants' => [array of variant name strings, variants * available for this image], * ), + * ... + * ), * ... * ), * ) @@ -90,6 +105,26 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { public function __construct( $options = array(), $localBasePath = null ) { $this->localBasePath = self::extractLocalBasePath( $options, $localBasePath ); + $this->definition = $options; + } + + /** + * Parse definition and external JSON data, if referenced. + */ + protected function loadFromDefinition() { + if ( $this->definition === null ) { + return; + } + + $options = $this->definition; + $this->definition = null; + + if ( isset( $options['data'] ) ) { + $dataPath = $this->localBasePath . '/' . $options['data']; + $data = json_decode( file_get_contents( $dataPath ), true ); + $options = array_merge( $data, $options ); + } + // Accepted combinations: // * prefix // * selector @@ -99,20 +134,30 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { $prefix = isset( $options['prefix'] ) && $options['prefix']; $selector = isset( $options['selector'] ) && $options['selector']; - $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] ) && $options['selectorWithoutVariant']; - $selectorWithVariant = isset( $options['selectorWithVariant'] ) && $options['selectorWithVariant']; + $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] ) + && $options['selectorWithoutVariant']; + $selectorWithVariant = isset( $options['selectorWithVariant'] ) + && $options['selectorWithVariant']; if ( $selectorWithoutVariant && !$selectorWithVariant ) { - throw new InvalidArgumentException( "Given 'selectorWithoutVariant' but no 'selectorWithVariant'." ); + throw new InvalidArgumentException( + "Given 'selectorWithoutVariant' but no 'selectorWithVariant'." + ); } if ( $selectorWithVariant && !$selectorWithoutVariant ) { - throw new InvalidArgumentException( "Given 'selectorWithVariant' but no 'selectorWithoutVariant'." ); + throw new InvalidArgumentException( + "Given 'selectorWithVariant' but no 'selectorWithoutVariant'." + ); } if ( $selector && $selectorWithVariant ) { - throw new InvalidArgumentException( "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given." ); + throw new InvalidArgumentException( + "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given." + ); } if ( !$prefix && !$selector && !$selectorWithVariant ) { - throw new InvalidArgumentException( "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given." ); + throw new InvalidArgumentException( + "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given." + ); } foreach ( $options as $member => $option ) { @@ -124,9 +169,22 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { "Invalid list error. '$option' given, array expected." ); } + if ( !isset( $option['default'] ) ) { + // Backwards compatibility + $option = array( 'default' => $option ); + } + foreach ( $option as $skin => $data ) { + if ( !is_array( $option ) ) { + throw new InvalidArgumentException( + "Invalid list error. '$option' given, array expected." + ); + } + } $this->{$member} = $option; break; + case 'position': + $this->isPositionDefined = true; case 'prefix': case 'selectorWithoutVariant': case 'selectorWithVariant': @@ -144,6 +202,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { * @return string */ public function getPrefix() { + $this->loadFromDefinition(); return $this->prefix; } @@ -152,6 +211,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { * @return string */ public function getSelectors() { + $this->loadFromDefinition(); return array( 'selectorWithoutVariant' => $this->selectorWithoutVariant, 'selectorWithVariant' => $this->selectorWithVariant, @@ -161,31 +221,43 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { /** * Get a ResourceLoaderImage object for given image. * @param string $name Image name + * @param ResourceLoaderContext $context * @return ResourceLoaderImage|null */ - public function getImage( $name ) { - $images = $this->getImages(); + public function getImage( $name, ResourceLoaderContext $context ) { + $this->loadFromDefinition(); + $images = $this->getImages( $context ); return isset( $images[$name] ) ? $images[$name] : null; } /** * Get ResourceLoaderImage objects for all images. + * @param ResourceLoaderContext $context * @return ResourceLoaderImage[] Array keyed by image name */ - public function getImages() { + public function getImages( ResourceLoaderContext $context ) { + $skin = $context->getSkin(); if ( !isset( $this->imageObjects ) ) { + $this->loadFromDefinition(); $this->imageObjects = array(); - - foreach ( $this->images as $name => $options ) { + } + if ( !isset( $this->imageObjects[$skin] ) ) { + $this->imageObjects[$skin] = array(); + if ( !isset( $this->images[$skin] ) ) { + $this->images[$skin] = isset( $this->images['default'] ) ? + $this->images['default'] : + array(); + } + foreach ( $this->images[$skin] as $name => $options ) { $fileDescriptor = is_string( $options ) ? $options : $options['file']; $allowedVariants = array_merge( is_array( $options ) && isset( $options['variants'] ) ? $options['variants'] : array(), - $this->getGlobalVariants() + $this->getGlobalVariants( $context ) ); - if ( isset( $this->variants ) ) { + if ( isset( $this->variants[$skin] ) ) { $variantConfig = array_intersect_key( - $this->variants, + $this->variants[$skin], array_fill_keys( $allowedVariants, true ) ); } else { @@ -199,32 +271,40 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { $this->localBasePath, $variantConfig ); - $this->imageObjects[ $image->getName() ] = $image; + $this->imageObjects[$skin][$image->getName()] = $image; } } - return $this->imageObjects; + return $this->imageObjects[$skin]; } /** * Get list of variants in this module that are 'global', i.e., available * for every image regardless of image options. + * @param ResourceLoaderContext $context * @return string[] */ - public function getGlobalVariants() { + public function getGlobalVariants( ResourceLoaderContext $context ) { + $skin = $context->getSkin(); if ( !isset( $this->globalVariants ) ) { + $this->loadFromDefinition(); $this->globalVariants = array(); - - if ( isset( $this->variants ) ) { - foreach ( $this->variants as $name => $config ) { - if ( isset( $config['global'] ) && $config['global'] ) { - $this->globalVariants[] = $name; - } + } + if ( !isset( $this->globalVariants[$skin] ) ) { + $this->globalVariants[$skin] = array(); + if ( !isset( $this->variants[$skin] ) ) { + $this->variants[$skin] = isset( $this->variants['default'] ) ? + $this->variants['default'] : + array(); + } + foreach ( $this->variants[$skin] as $name => $config ) { + if ( isset( $config['global'] ) && $config['global'] ) { + $this->globalVariants[$skin][] = $name; } } } - return $this->globalVariants; + return $this->globalVariants[$skin]; } /** @@ -232,12 +312,14 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { * @return array */ public function getStyles( ResourceLoaderContext $context ) { + $this->loadFromDefinition(); + // Build CSS rules $rules = array(); $script = $context->getResourceLoader()->getLoadScript( $this->getSource() ); $selectors = $this->getSelectors(); - foreach ( $this->getImages() as $name => $image ) { + foreach ( $this->getImages( $context ) as $name => $image ) { $declarations = $this->getCssDeclarations( $image->getDataUri( $context, null, 'original' ), $image->getUrl( $context, $script, null, 'rasterized' ) @@ -303,6 +385,48 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { return false; } + /** + * Get the definition summary for this module. + * + * @param ResourceLoaderContext $context + * @return array + */ + public function getDefinitionSummary( ResourceLoaderContext $context ) { + $this->loadFromDefinition(); + $summary = parent::getDefinitionSummary( $context ); + foreach ( array( + 'localBasePath', + 'images', + 'variants', + 'prefix', + 'selectorWithoutVariant', + 'selectorWithVariant', + ) as $member ) { + $summary[$member] = $this->{$member}; + }; + return $summary; + } + + /** + * Get the last modified timestamp of this module. + * + * @param ResourceLoaderContext $context Context in which to calculate + * the modified time + * @return int UNIX timestamp + */ + public function getModifiedTime( ResourceLoaderContext $context ) { + $this->loadFromDefinition(); + $files = array(); + foreach ( $this->getImages( $context ) as $name => $image ) { + $files[] = $image->getPath( $context ); + } + + $files = array_values( array_unique( $files ) ); + $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) ); + + return $filesMtime; + } + /** * Extract a local base path from module definition information. * @@ -324,4 +448,17 @@ class ResourceLoaderImageModule extends ResourceLoaderModule { return $localBasePath; } + + /** + * @return string + */ + public function getPosition() { + $this->loadFromDefinition(); + return $this->position; + } + + public function isPositionDefault() { + $this->loadFromDefinition(); + return parent::isPositionDefault(); + } } diff --git a/includes/resourceloader/ResourceLoaderJqueryMsgModule.php b/includes/resourceloader/ResourceLoaderJqueryMsgModule.php new file mode 100644 index 00000000..f9dfbdc2 --- /dev/null +++ b/includes/resourceloader/ResourceLoaderJqueryMsgModule.php @@ -0,0 +1,66 @@ +getHashMtime( $context ) ); + public function enableModuleContentVersion() { + return true; } /** * @param ResourceLoaderContext $context - * @return string Hash - */ - public function getModifiedHash( ResourceLoaderContext $context ) { - return md5( serialize( $this->getData( $context ) ) ); - } - - /** * @return array */ - public function getDependencies() { + public function getDependencies( ResourceLoaderContext $context = null ) { return array( 'mediawiki.language.init' ); } } diff --git a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php index 55b1f4b1..081c728c 100644 --- a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php +++ b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php @@ -32,7 +32,6 @@ class ResourceLoaderLanguageNamesModule extends ResourceLoaderModule { protected $targets = array( 'desktop', 'mobile' ); - /** * @param ResourceLoaderContext $context * @return array @@ -60,24 +59,19 @@ class ResourceLoaderLanguageNamesModule extends ResourceLoaderModule { ); } - public function getDependencies() { - return array( 'mediawiki.language.init' ); - } - /** * @param ResourceLoaderContext $context - * @return int UNIX timestamp + * @return array */ - public function getModifiedTime( ResourceLoaderContext $context ) { - return max( 1, $this->getHashMtime( $context ) ); + public function getDependencies( ResourceLoaderContext $context = null ) { + return array( 'mediawiki.language.init' ); } /** - * @param ResourceLoaderContext $context - * @return string Hash + * @return bool */ - public function getModifiedHash( ResourceLoaderContext $context ) { - return md5( serialize( $this->getData( $context ) ) ); + public function enableModuleContentVersion() { + return true; } } diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php index ed16521b..1d3ffb55 100644 --- a/includes/resourceloader/ResourceLoaderModule.php +++ b/includes/resourceloader/ResourceLoaderModule.php @@ -29,7 +29,6 @@ abstract class ResourceLoaderModule { # Type of resource const TYPE_SCRIPTS = 'scripts'; const TYPE_STYLES = 'styles'; - const TYPE_MESSAGES = 'messages'; const TYPE_COMBINED = 'combined'; # sitewide core module like a skin file or jQuery component @@ -63,6 +62,14 @@ abstract class ResourceLoaderModule { protected $fileDeps = array(); // In-object cache for message blob mtime protected $msgBlobMtime = array(); + // In-object cache for version hash + protected $versionHash = array(); + // In-object cache for module content + protected $contents = array(); + + // Whether the position returned by getPosition() is defined in the module configuration + // and not a default value + protected $isPositionDefined = false; /** * @var Config @@ -284,6 +291,19 @@ abstract class ResourceLoaderModule { return 'bottom'; } + /** + * Whether the position returned by getPosition() is a default value or comes from the module + * definition. This method is meant to be short-lived, and is only useful until classes added + * via addModuleStyles with a default value define an explicit position. See getModuleStyles() + * in OutputPage for the related migration warning. + * + * @return bool + * @since 1.26 + */ + public function isPositionDefault() { + return !$this->isPositionDefined; + } + /** * Whether this module's JS expects to work without the client-side ResourceLoader module. * Returning true from this function will prevent mw.loader.state() call from being @@ -313,9 +333,14 @@ abstract class ResourceLoaderModule { * * To add dependencies dynamically on the client side, use a custom * loader script, see getLoaderScript() + * + * Note: It is expected that $context will be made non-optional in the near + * future. + * + * @param ResourceLoaderContext $context * @return array List of module names as strings */ - public function getDependencies() { + public function getDependencies( ResourceLoaderContext $context = null ) { // Stub, override expected return array(); } @@ -361,16 +386,21 @@ abstract class ResourceLoaderModule { } $dbr = wfGetDB( DB_SLAVE ); - $deps = $dbr->selectField( 'module_deps', 'md_deps', array( + $deps = $dbr->selectField( 'module_deps', + 'md_deps', + array( 'md_module' => $this->getName(), 'md_skin' => $skin, - ), __METHOD__ + ), + __METHOD__ ); + if ( !is_null( $deps ) ) { $this->fileDeps[$skin] = (array)FormatJson::decode( $deps, true ); } else { $this->fileDeps[$skin] = array(); } + return $this->fileDeps[$skin]; } @@ -385,8 +415,7 @@ abstract class ResourceLoaderModule { } /** - * Get the last modification timestamp of the message blob for this - * module in a given language. + * Get the last modification timestamp of the messages in this module for a given language. * @param string $lang Language code * @return int UNIX timestamp */ @@ -397,10 +426,13 @@ abstract class ResourceLoaderModule { } $dbr = wfGetDB( DB_SLAVE ); - $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array( + $msgBlobMtime = $dbr->selectField( 'msg_resource', + 'mr_timestamp', + array( 'mr_resource' => $this->getName(), 'mr_lang' => $lang - ), __METHOD__ + ), + __METHOD__ ); // If no blob was found, but the module does have messages, that means we need // to regenerate it. Return NOW @@ -422,143 +454,317 @@ abstract class ResourceLoaderModule { $this->msgBlobMtime[$lang] = $mtime; } - /* Abstract Methods */ - /** - * Get this module's last modification timestamp for a given - * combination of language, skin and debug mode flag. This is typically - * the highest of each of the relevant components' modification - * timestamps. Whenever anything happens that changes the module's - * contents for these parameters, the mtime should increase. - * - * NOTE: The mtime of the module's messages is NOT automatically included. - * If you want this to happen, you'll need to call getMsgBlobMtime() - * yourself and take its result into consideration. - * - * NOTE: The mtime of the module's hash is NOT automatically included. - * If your module provides a getModifiedHash() method, you'll need to call getHashMtime() - * yourself and take its result into consideration. + * Get an array of this module's resources. Ready for serving to the web. * - * @param ResourceLoaderContext $context Context object - * @return int UNIX timestamp - */ - public function getModifiedTime( ResourceLoaderContext $context ) { - return 1; + * @since 1.26 + * @param ResourceLoaderContext $context + * @return array + */ + public function getModuleContent( ResourceLoaderContext $context ) { + $contextHash = $context->getHash(); + // Cache this expensive operation. This calls builds the scripts, styles, and messages + // content which typically involves filesystem and/or database access. + if ( !array_key_exists( $contextHash, $this->contents ) ) { + $this->contents[$contextHash] = $this->buildContent( $context ); + } + return $this->contents[$contextHash]; } /** - * Helper method for calculating when the module's hash (if it has one) changed. + * Bundle all resources attached to this module into an array. * + * @since 1.26 * @param ResourceLoaderContext $context - * @return int UNIX timestamp - */ - public function getHashMtime( ResourceLoaderContext $context ) { - $hash = $this->getModifiedHash( $context ); - if ( !is_string( $hash ) ) { - return 1; + * @return array + */ + final protected function buildContent( ResourceLoaderContext $context ) { + $rl = $context->getResourceLoader(); + $stats = RequestContext::getMain()->getStats(); + $statStart = microtime( true ); + + // Only include properties that are relevant to this context (e.g. only=scripts) + // and that are non-empty (e.g. don't include "templates" for modules without + // templates). This helps prevent invalidating cache for all modules when new + // optional properties are introduced. + $content = array(); + + // Scripts + if ( $context->shouldIncludeScripts() ) { + // If we are in debug mode, we'll want to return an array of URLs if possible + // However, we can't do this if the module doesn't support it + // We also can't do this if there is an only= parameter, because we have to give + // the module a way to return a load.php URL without causing an infinite loop + if ( $context->getDebug() && !$context->getOnly() && $this->supportsURLLoading() ) { + $scripts = $this->getScriptURLsForDebug( $context ); + } else { + $scripts = $this->getScript( $context ); + // 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"; + } + } + $content['scripts'] = $scripts; + } + + // Styles + if ( $context->shouldIncludeStyles() ) { + $styles = array(); + // Don't create empty stylesheets like array( '' => '' ) for modules + // that don't *have* any stylesheets (bug 38024). + $stylePairs = $this->getStyles( $context ); + 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() && $this->supportsURLLoading() ) { + $styles = array( + 'url' => $this->getStyleURLsForDebug( $context ) + ); + } else { + // Minify CSS before embedding in mw.loader.implement call + // (unless in debug mode) + if ( !$context->getDebug() ) { + foreach ( $stylePairs as $media => $style ) { + // Can be either a string or an array of strings. + if ( is_array( $style ) ) { + $stylePairs[$media] = array(); + foreach ( $style as $cssText ) { + if ( is_string( $cssText ) ) { + $stylePairs[$media][] = + $rl->filter( 'minify-css', $cssText ); + } + } + } elseif ( is_string( $style ) ) { + $stylePairs[$media] = $rl->filter( 'minify-css', $style ); + } + } + } + // Wrap styles into @media groups as needed and flatten into a numerical array + $styles = array( + 'css' => $rl->makeCombinedStyles( $stylePairs ) + ); + } + } + $content['styles'] = $styles; + } + + // Messages + $blobs = $rl->getMessageBlobStore()->get( + $rl, + array( $this->getName() => $this ), + $context->getLanguage() + ); + if ( isset( $blobs[$this->getName()] ) ) { + $content['messagesBlob'] = $blobs[$this->getName()]; } - // 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. - $cache = wfGetCache( CACHE_ANYTHING ); - $key = wfMemcKey( 'resourceloader', 'hashmtime', $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. - return $data; + $templates = $this->getTemplates(); + if ( $templates ) { + $content['templates'] = $templates; } - $timestamp = time(); - $cache->set( $key, $timestamp ); - return $timestamp; + $statTiming = microtime( true ) - $statStart; + $statName = strtr( $this->getName(), '.', '_' ); + $stats->timing( "resourceloader_build.all", 1000 * $statTiming ); + $stats->timing( "resourceloader_build.$statName", 1000 * $statTiming ); + + return $content; } /** - * Get the hash for whatever this module may contain. + * Get a string identifying the current version of this module in a given context. + * + * Whenever anything happens that changes the module's response (e.g. scripts, styles, and + * messages) this value must change. This value is used to store module responses in cache. + * (Both client-side and server-side.) * - * This is the method subclasses should implement if they want to make - * use of getHashMTime() inside getModifiedTime(). + * It is not recommended to override this directly. Use getDefinitionSummary() instead. + * If overridden, one must call the parent getVersionHash(), append data and re-hash. * + * This method should be quick because it is frequently run by ResourceLoaderStartUpModule to + * propagate changes to the client and effectively invalidate cache. + * + * For backward-compatibility, the following optional data providers are automatically included: + * + * - getModifiedTime() + * - getModifiedHash() + * + * @since 1.26 * @param ResourceLoaderContext $context - * @return string|null Hash - */ - public function getModifiedHash( ResourceLoaderContext $context ) { - return null; + * @return string Hash (should use ResourceLoader::makeHash) + */ + public function getVersionHash( ResourceLoaderContext $context ) { + // The startup module produces a manifest with versions representing the entire module. + // Typically, the request for the startup module itself has only=scripts. That must apply + // only to the startup module content, and not to the module version computed here. + $context = new DerivativeResourceLoaderContext( $context ); + $context->setModules( array() ); + // Version hash must cover all resources, regardless of startup request itself. + $context->setOnly( null ); + // Compute version hash based on content, not debug urls. + $context->setDebug( false ); + + // Cache this somewhat expensive operation. Especially because some classes + // (e.g. startup module) iterate more than once over all modules to get versions. + $contextHash = $context->getHash(); + if ( !array_key_exists( $contextHash, $this->versionHash ) ) { + + if ( $this->enableModuleContentVersion() ) { + // Detect changes directly + $str = json_encode( $this->getModuleContent( $context ) ); + } else { + // Infer changes based on definition and other metrics + $summary = $this->getDefinitionSummary( $context ); + if ( !isset( $summary['_cacheEpoch'] ) ) { + throw new LogicException( 'getDefinitionSummary must call parent method' ); + } + $str = json_encode( $summary ); + + $mtime = $this->getModifiedTime( $context ); + if ( $mtime !== null ) { + // Support: MediaWiki 1.25 and earlier + $str .= strval( $mtime ); + } + + $mhash = $this->getModifiedHash( $context ); + if ( $mhash !== null ) { + // Support: MediaWiki 1.25 and earlier + $str .= strval( $mhash ); + } + } + + $this->versionHash[$contextHash] = ResourceLoader::makeHash( $str ); + } + return $this->versionHash[$contextHash]; } /** - * Helper method for calculating when this module's definition summary was last changed. + * Whether to generate version hash based on module content. * - * @since 1.23 + * If a module requires database or file system access to build the module + * content, consider disabling this in favour of manually tracking relevant + * aspects in getDefinitionSummary(). See getVersionHash() for how this is used. * - * @param ResourceLoaderContext $context - * @return int UNIX timestamp + * @return bool */ - public function getDefinitionMtime( ResourceLoaderContext $context ) { - $summary = $this->getDefinitionSummary( $context ); - if ( $summary === null ) { - return 1; - } - - $hash = md5( json_encode( $summary ) ); - $cache = wfGetCache( CACHE_ANYTHING ); - $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. - return $data; - } - - wfDebugLog( 'resourceloader', __METHOD__ . ": New definition for module " - . "{$this->getName()} in context \"{$context->getHash()}\"" ); - - $timestamp = time(); - $cache->set( $key, $timestamp ); - return $timestamp; + public function enableModuleContentVersion() { + return false; } /** * Get the definition summary for this module. * - * This is the method subclasses should implement if they want to make - * use of getDefinitionMTime() inside getModifiedTime(). + * This is the method subclasses are recommended to use to track values in their + * version hash. Call this in getVersionHash() and pass it to e.g. json_encode. + * + * Subclasses must call the parent getDefinitionSummary() and build on that. + * It is recommended that each subclass appends its own new array. This prevents + * clashes or accidental overwrites of existing keys and gives each subclass + * its own scope for simple array keys. + * + * @code + * $summary = parent::getDefinitionSummary( $context ); + * $summary[] = array( + * 'foo' => 123, + * 'bar' => 'quux', + * ); + * return $summary; + * @endcode * * 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). + * module's definition. * - * Avoid including things that are insiginificant (e.g. order of message - * keys is insignificant and should be sorted to avoid unnecessary cache - * invalidation). + * Be careful not to normalise too much. Especially preserve the order of things + * that carry significance in getScript and getStyles (T39812). * - * Avoid including things already considered by other methods inside your - * getModifiedTime(), such as file mtime timestamps. + * Avoid including things that are insiginificant (e.g. order of message keys is + * insignificant and should be sorted to avoid unnecessary cache invalidation). * - * 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. + * This data structure must exclusively contain arrays and scalars as values (avoid + * object instances) to allow simple serialisation using json_encode. * - * @since 1.23 + * If modules have a hash or timestamp from another source, that may be incuded as-is. * + * A number of utility methods are available to help you gather data. These are not + * called by default and must be included by the subclass' getDefinitionSummary(). + * + * - getMsgBlobMtime() + * + * @since 1.23 * @param ResourceLoaderContext $context * @return array|null */ public function getDefinitionSummary( ResourceLoaderContext $context ) { return array( - 'class' => get_class( $this ), + '_class' => get_class( $this ), + '_cacheEpoch' => $this->getConfig()->get( 'CacheEpoch' ), ); } + /** + * Get this module's last modification timestamp for a given context. + * + * @deprecated since 1.26 Use getDefinitionSummary() instead + * @param ResourceLoaderContext $context Context object + * @return int|null UNIX timestamp + */ + public function getModifiedTime( ResourceLoaderContext $context ) { + return null; + } + + /** + * Helper method for providing a version hash to getVersionHash(). + * + * @deprecated since 1.26 Use getDefinitionSummary() instead + * @param ResourceLoaderContext $context + * @return string|null Hash + */ + public function getModifiedHash( ResourceLoaderContext $context ) { + return null; + } + + /** + * Back-compat dummy for old subclass implementations of getModifiedTime(). + * + * This method used to use ObjectCache to track when a hash was first seen. That principle + * stems from a time that ResourceLoader could only identify module versions by timestamp. + * That is no longer the case. Use getDefinitionSummary() directly. + * + * @deprecated since 1.26 Superseded by getVersionHash() + * @param ResourceLoaderContext $context + * @return int UNIX timestamp + */ + public function getHashMtime( ResourceLoaderContext $context ) { + if ( !is_string( $this->getModifiedHash( $context ) ) ) { + return 1; + } + // Dummy that is > 1 + return 2; + } + + /** + * Back-compat dummy for old subclass implementations of getModifiedTime(). + * + * @since 1.23 + * @deprecated since 1.26 Superseded by getVersionHash() + * @param ResourceLoaderContext $context + * @return int UNIX timestamp + */ + public function getDefinitionMtime( ResourceLoaderContext $context ) { + if ( $this->getDefinitionSummary( $context ) === null ) { + return 1; + } + // Dummy that is > 1 + return 2; + } + /** * 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 @@ -587,8 +793,13 @@ abstract class ResourceLoaderModule { protected function validateScriptFile( $fileName, $contents ) { 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 ) ); + // Use CACHE_ANYTHING since parsing JS is much slower than a DB query + $key = wfMemcKey( + 'resourceloader', + 'jsparse', + self::$parseCacheVersion, + md5( $contents ) + ); $cache = wfGetCache( CACHE_ANYTHING ); $cacheEntry = $cache->get( $key ); if ( is_string( $cacheEntry ) ) { @@ -602,7 +813,8 @@ abstract class ResourceLoaderModule { } catch ( Exception $e ) { // We'll save this to cache to avoid having to validate broken JS over and over... $err = $e->getMessage(); - $result = "throw new Error(" . Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");"; + $result = "mw.log.error(" . + Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");"; } $cache->set( $key, $result ); @@ -623,16 +835,57 @@ abstract class ResourceLoaderModule { } /** - * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist - * but returns 1 instead. - * @param string $filename File name + * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist. + * Defaults to 1. + * + * @param string $filePath File path * @return int UNIX timestamp */ - protected static function safeFilemtime( $filename ) { - wfSuppressWarnings(); - $mtime = filemtime( $filename ) ?: 1; - wfRestoreWarnings(); - + protected static function safeFilemtime( $filePath ) { + MediaWiki\suppressWarnings(); + $mtime = filemtime( $filePath ) ?: 1; + MediaWiki\restoreWarnings(); return $mtime; } + + /** + * Compute a non-cryptographic string hash of a file's contents. + * If the file does not exist or cannot be read, returns an empty string. + * + * @since 1.26 Uses MD4 instead of SHA1. + * @param string $filePath File path + * @return string Hash + */ + protected static function safeFileHash( $filePath ) { + static $cache; + + if ( !$cache ) { + $cache = ObjectCache::newAccelerator( CACHE_NONE ); + } + + MediaWiki\suppressWarnings(); + $mtime = filemtime( $filePath ); + MediaWiki\restoreWarnings(); + if ( !$mtime ) { + return ''; + } + + $cacheKey = wfGlobalCacheKey( 'resourceloader', __METHOD__, $filePath ); + $cachedHash = $cache->get( $cacheKey ); + if ( isset( $cachedHash['mtime'] ) && $cachedHash['mtime'] === $mtime ) { + return $cachedHash['hash']; + } + + MediaWiki\suppressWarnings(); + $contents = file_get_contents( $filePath ); + MediaWiki\restoreWarnings(); + if ( !$contents ) { + return ''; + } + + $hash = hash( 'md4', $contents ); + $cache->set( $cacheKey, array( 'mtime' => $mtime, 'hash' => $hash ), 60 * 60 * 24 ); + + return $hash; + } } diff --git a/includes/resourceloader/ResourceLoaderOOUIImageModule.php b/includes/resourceloader/ResourceLoaderOOUIImageModule.php new file mode 100644 index 00000000..8493f9fd --- /dev/null +++ b/includes/resourceloader/ResourceLoaderOOUIImageModule.php @@ -0,0 +1,86 @@ +definition === null ) { + return; + } + + // Core default themes + $themes = array( 'default' => 'mediawiki' ); + $themes += ExtensionRegistry::getInstance()->getAttribute( 'SkinOOUIThemes' ); + + $name = $this->definition['name']; + $rootPath = $this->definition['rootPath']; + + $definition = array(); + foreach ( $themes as $skin => $theme ) { + // TODO Allow extensions to specify this path somehow + $dataPath = $this->localBasePath . '/' . $rootPath . '/' . $theme . '/' . $name . '.json'; + + if ( file_exists( $dataPath ) ) { + $data = json_decode( file_get_contents( $dataPath ), true ); + $fixPath = function ( &$path ) use ( $rootPath, $theme ) { + // TODO Allow extensions to specify this path somehow + $path = $rootPath . '/' . $theme . '/' . $path; + }; + array_walk( $data['images'], function ( &$value ) use ( $fixPath ) { + if ( is_string( $value['file'] ) ) { + $fixPath( $value['file'] ); + } elseif ( is_array( $value['file'] ) ) { + array_walk_recursive( $value['file'], $fixPath ); + } + } ); + } else { + $data = array(); + } + + foreach ( $data as $key => $value ) { + switch ( $key ) { + case 'images': + case 'variants': + $definition[$key][$skin] = $data[$key]; + break; + + default: + if ( !isset( $definition[$key] ) ) { + $definition[$key] = $data[$key]; + } elseif ( $definition[$key] !== $data[$key] ) { + throw new Exception( + "Mismatched OOUI theme definitions are not supported: trying to load $key of $theme theme" + ); + } + break; + } + } + } + + // Fields from definition silently override keys from JSON files + $this->definition += $definition; + + parent::loadFromDefinition(); + } +} diff --git a/includes/resourceloader/ResourceLoaderRawFileModule.php b/includes/resourceloader/ResourceLoaderRawFileModule.php new file mode 100644 index 00000000..d9005fa5 --- /dev/null +++ b/includes/resourceloader/ResourceLoaderRawFileModule.php @@ -0,0 +1,52 @@ +getHashMtime( $context ) ); - } - /** * @param $context ResourceLoaderContext * @return string: Hash diff --git a/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php b/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php index 5c917091..8170cb1c 100644 --- a/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php +++ b/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php @@ -35,7 +35,8 @@ class ResourceLoaderSpecialCharacterDataModule extends ResourceLoaderModule { * @return array */ protected function getData() { - return json_decode( file_get_contents( $this->path ) ); + global $IP; + return json_decode( file_get_contents( "$IP/{$this->path}" ) ); } /** @@ -53,25 +54,17 @@ class ResourceLoaderSpecialCharacterDataModule extends ResourceLoaderModule { } /** - * @param ResourceLoaderContext $context - * @return int UNIX timestamp + * @return bool */ - public function getModifiedTime( ResourceLoaderContext $context ) { - return static::safeFilemtime( $this->path ); + public function enableModuleContentVersion() { + return true; } /** * @param ResourceLoaderContext $context - * @return string Hash - */ - public function getModifiedHash( ResourceLoaderContext $context ) { - return md5( serialize( $this->getData() ) ); - } - - /** * @return array */ - public function getDependencies() { + public function getDependencies( ResourceLoaderContext $context = null ) { return array( 'mediawiki.language' ); } diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index b2fbae9c..87d8ee20 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -24,14 +24,10 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { - /* Protected Members */ - - protected $modifiedTime = array(); + // Cache for getConfigSettings() as it's called by multiple methods protected $configVars = array(); protected $targets = array( 'desktop', 'mobile' ); - /* Protected Methods */ - /** * @param ResourceLoaderContext $context * @return array @@ -92,6 +88,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgContentNamespaces' => MWNamespace::getContentNamespaces(), 'wgSiteName' => $conf->get( 'Sitename' ), 'wgDBname' => $conf->get( 'DBname' ), + 'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ), 'wgAvailableSkins' => Skin::getSkinNames(), 'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ), // MediaWiki sets cookies to have this prefix by default @@ -104,6 +101,9 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ), 'wgResourceLoaderStorageVersion' => $conf->get( 'ResourceLoaderStorageVersion' ), 'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ), + 'wgResourceLoaderLegacyModules' => self::getLegacyModules(), + 'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ), + 'wgEnableUploads' => $conf->get( 'EnableUploads' ), ); Hooks::run( 'ResourceLoaderGetConfigVars', array( &$vars ) ); @@ -159,7 +159,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { * data send to the client. * * @param array &$registryData Modules keyed by name with properties: - * - number 'version' + * - string 'version' * - array 'dependencies' * - string|null 'group' * - string 'source' @@ -191,6 +191,9 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { $resourceLoader = $context->getResourceLoader(); $target = $context->getRequest()->getVal( 'target', 'desktop' ); + // Bypass target filter if this request is from a unit test context. To prevent misuse in + // production, this is only allowed if testing is enabled server-side. + $byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test'; $out = ''; $registryData = array(); @@ -199,7 +202,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { foreach ( $resourceLoader->getModuleNames() as $name ) { $module = $resourceLoader->getModule( $name ); $moduleTargets = $module->getTargets(); - if ( !in_array( $target, $moduleTargets ) ) { + if ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) ) { continue; } @@ -210,31 +213,27 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { continue; } - // Coerce module timestamp to UNIX timestamp. - // getModifiedTime() is supposed to return a UNIX timestamp, but custom implementations - // might forget. TODO: Maybe emit warning? - $moduleMtime = wfTimestamp( TS_UNIX, $module->getModifiedTime( $context ) ); + $versionHash = $module->getVersionHash( $context ); + if ( strlen( $versionHash ) !== 8 ) { + // Module implementation either broken or deviated from ResourceLoader::makeHash + // Asserted by tests/phpunit/structure/ResourcesTest. + $versionHash = ResourceLoader::makeHash( $versionHash ); + } $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 + // There will potentially be lots of these little strings in the registrations // manifest, we don't want to blow up the startup module with - // "/* cache key: ... */" all over it in non-debug mode. + // "/* cache key: ... */" all over it. /* cacheReport = */ false ); } - $mtime = max( - $moduleMtime, - wfTimestamp( TS_UNIX, $this->getConfig()->get( 'CacheEpoch' ) ) - ); - $registryData[$name] = array( - // Convert to numbers as wfTimestamp always returns a string, even for TS_UNIX - 'version' => (int) $mtime, - 'dependencies' => $module->getDependencies(), + 'version' => $versionHash, + 'dependencies' => $module->getDependencies( $context ), 'group' => $module->getGroup(), 'source' => $module->getSource(), 'loader' => $module->getLoaderScript(), @@ -263,7 +262,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { continue; } - // Call mw.loader.register(name, timestamp, dependencies, group, source, skip) + // Call mw.loader.register(name, version, dependencies, group, source, skip) $registrations[] = array( $name, $data['version'], @@ -276,13 +275,11 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { } // Register modules - $out .= ResourceLoader::makeLoaderRegisterScript( $registrations ); + $out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $registrations ); return $out; } - /* Methods */ - /** * @return bool */ @@ -299,6 +296,20 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { return array( 'jquery', 'mediawiki' ); } + public static function getLegacyModules() { + global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil; + + $legacyModules = array(); + if ( $wgIncludeLegacyJavaScript ) { + $legacyModules[] = 'mediawiki.legacy.wikibits'; + } + if ( $wgPreloadJavaScriptMwUtil ) { + $legacyModules[] = 'mediawiki.util'; + } + + return $legacyModules; + } + /** * Get the load URL of the startup modules. * @@ -309,24 +320,16 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { * @return string */ public static function getStartupModulesUrl( ResourceLoaderContext $context ) { + $rl = $context->getResourceLoader(); $moduleNames = self::getStartupModules(); - // Get the latest version - $loader = $context->getResourceLoader(); - $version = 1; - 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 ) + 'version' => $rl->getCombinedVersion( $context, $moduleNames ), ); // Ensure uniform query order ksort( $query ); @@ -339,40 +342,25 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { */ public function getScript( ResourceLoaderContext $context ) { global $IP; + if ( $context->getOnly() !== 'scripts' ) { + return '/* Requires only=script */'; + } $out = file_get_contents( "$IP/resources/src/startup.js" ); - if ( $context->getOnly() === 'scripts' ) { - // Startup function - $configuration = $this->getConfigSettings( $context ); - $registrations = $this->getModuleRegistrations( $context ); + $pairs = array_map( function ( $value ) { + $value = FormatJson::encode( $value, ResourceLoader::inDebugMode(), FormatJson::ALL_OK ); // Fix indentation - $registrations = str_replace( "\n", "\n\t", trim( $registrations ) ); - $mwMapJsCall = Xml::encodeJsCall( - 'mw.Map', - array( $this->getConfig()->get( 'LegacyJavaScriptGlobals' ) ) - ); - $mwConfigSetJsCall = Xml::encodeJsCall( - 'mw.config.set', - array( $configuration ), - ResourceLoader::inDebugMode() - ); - - $out .= "var startUp = function () {\n" . - "\tmw.config = new " . - $mwMapJsCall . "\n" . - "\t$registrations\n" . - "\t" . $mwConfigSetJsCall . - "};\n"; - - // Conditional script injection - $scriptTag = Html::linkedScript( self::getStartupModulesUrl( $context ) ); - $out .= "if ( isCompatible() ) {\n" . - "\t" . Xml::encodeJsCall( 'document.write', array( $scriptTag ) ) . - "\n}"; - } - - return $out; + $value = str_replace( "\n", "\n\t", $value ); + return $value; + }, array( + '$VARS.wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ), + '$VARS.configuration' => $this->getConfigSettings( $context ), + '$VARS.baseModulesUri' => self::getStartupModulesUrl( $context ), + ) ); + $pairs['$CODE.registrations()'] = str_replace( "\n", "\n\t", trim( $this->getModuleRegistrations( $context ) ) ); + + return strtr( $out, $pairs ); } /** @@ -383,59 +371,48 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { } /** + * Get the definition summary for this module. + * * @param ResourceLoaderContext $context - * @return array|mixed + * @return array */ - public function getModifiedTime( ResourceLoaderContext $context ) { + public function getDefinitionSummary( ResourceLoaderContext $context ) { global $IP; + $summary = parent::getDefinitionSummary( $context ); + $summary[] = array( + // Detect changes to variables exposed in mw.config (T30899). + 'vars' => $this->getConfigSettings( $context ), + // Changes how getScript() creates mw.Map for mw.config + 'wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ), + // Detect changes to the module registrations + 'moduleHashes' => $this->getAllModuleHashes( $context ), - $hash = $context->getHash(); - if ( isset( $this->modifiedTime[$hash] ) ) { - return $this->modifiedTime[$hash]; - } - - // Call preloadModuleInfo() on ALL modules as we're about - // to call getModifiedTime() on all of them - $loader = $context->getResourceLoader(); - $loader->preloadModuleInfo( $loader->getModuleNames(), $context ); - - $time = max( - wfTimestamp( TS_UNIX, $this->getConfig()->get( 'CacheEpoch' ) ), - filemtime( "$IP/resources/src/startup.js" ), - $this->getHashMtime( $context ) + 'fileMtimes' => array( + filemtime( "$IP/resources/src/startup.js" ), + ), ); - - // ATTENTION!: Because of the line below, this is not going to cause - // infinite recursion - think carefully before making changes to this - // code! - // Pre-populate modifiedTime with something because the loop over - // all modules below includes the startup module (this module). - $this->modifiedTime[$hash] = 1; - - foreach ( $loader->getModuleNames() as $name ) { - $module = $loader->getModule( $name ); - $time = max( $time, $module->getModifiedTime( $context ) ); - } - - $this->modifiedTime[$hash] = $time; - return $this->modifiedTime[$hash]; + return $summary; } /** - * Hash of all dynamic data embedded in getScript(). - * - * Detect changes to mw.config settings embedded in #getScript (bug 28899). + * Helper method for getDefinitionSummary(). * * @param ResourceLoaderContext $context - * @return string Hash + * @return string SHA-1 */ - public function getModifiedHash( ResourceLoaderContext $context ) { - $data = array( - 'vars' => $this->getConfigSettings( $context ), - 'wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ), - ); - - return md5( serialize( $data ) ); + protected function getAllModuleHashes( ResourceLoaderContext $context ) { + $rl = $context->getResourceLoader(); + // Preload for getCombinedVersion() + $rl->preloadModuleInfo( $rl->getModuleNames(), $context ); + + // ATTENTION: Because of the line below, this is not going to cause infinite recursion. + // Think carefully before making changes to this code! + // Pre-populate versionHash with something because the loop over all modules below includes + // the startup module (this module). + // See ResourceLoaderModule::getVersionHash() for usage of this cache. + $this->versionHash[$context->getHash()] = null; + + return $rl->getCombinedVersion( $context, $rl->getModuleNames() ); } /** diff --git a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php index 472ceb26..65d770e2 100644 --- a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php +++ b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php @@ -27,25 +27,13 @@ */ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule { - /* Protected Members */ - - protected $modifiedTime = array(); - protected $origin = self::ORIGIN_CORE_INDIVIDUAL; - /* Methods */ - /** - * @param ResourceLoaderContext $context - * @return array|int|mixed + * @return bool */ - public function getModifiedTime( ResourceLoaderContext $context ) { - $hash = $context->getHash(); - if ( !isset( $this->modifiedTime[$hash] ) ) { - $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $context->getUserObj()->getTouched() ); - } - - return $this->modifiedTime[$hash]; + public function enableModuleContentVersion() { + return true; } /** diff --git a/includes/resourceloader/ResourceLoaderUserDefaultsModule.php b/includes/resourceloader/ResourceLoaderUserDefaultsModule.php index 5f4bc16b..eba61edc 100644 --- a/includes/resourceloader/ResourceLoaderUserDefaultsModule.php +++ b/includes/resourceloader/ResourceLoaderUserDefaultsModule.php @@ -26,26 +26,13 @@ */ class ResourceLoaderUserDefaultsModule extends ResourceLoaderModule { - /* Protected Members */ - protected $targets = array( 'desktop', 'mobile' ); - /* Methods */ - - /** - * @param ResourceLoaderContext $context - * @return string Hash - */ - public function getModifiedHash( ResourceLoaderContext $context ) { - return md5( serialize( User::getDefaultOptions() ) ); - } - /** - * @param ResourceLoaderContext $context - * @return int + * @return bool */ - public function getModifiedTime( ResourceLoaderContext $context ) { - return $this->getHashMtime( $context ); + public function enableModuleContentVersion() { + return true; } /** diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php index 84c1906d..0847109c 100644 --- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php +++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php @@ -27,34 +27,23 @@ */ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { - /* Protected Members */ - - protected $modifiedTime = array(); - protected $origin = self::ORIGIN_CORE_INDIVIDUAL; protected $targets = array( 'desktop', 'mobile' ); - /* Methods */ - /** + * @param ResourceLoaderContext $context * @return array List of module names as strings */ - public function getDependencies() { + public function getDependencies( ResourceLoaderContext $context = null ) { return array( 'user.defaults' ); } /** - * @param ResourceLoaderContext $context - * @return int + * @return bool */ - public function getModifiedTime( ResourceLoaderContext $context ) { - $hash = $context->getHash(); - if ( !isset( $this->modifiedTime[$hash] ) ) { - $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $context->getUserObj()->getTouched() ); - } - - return $this->modifiedTime[$hash]; + public function enableModuleContentVersion() { + return true; } /** diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index 7b44cc67..0023de27 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -26,15 +26,30 @@ * Abstraction for resource loader modules which pull from wiki pages * * This can only be used for wiki pages in the MediaWiki and User namespaces, - * because of its dependence on the functionality of - * Title::isCssJsSubpage. + * because of its dependence on the functionality of Title::isCssJsSubpage. + * + * This module supports being used as a placeholder for a module on a remote wiki. + * To do so, getDB() must be overloaded to return a foreign database object that + * allows local wikis to query page metadata. + * + * Safe for calls on local wikis are: + * - Option getters: + * - getGroup() + * - getPosition() + * - getPages() + * - Basic methods that strictly involve the foreign database + * - getDB() + * - isKnownEmpty() + * - getTitleInfo() */ class ResourceLoaderWikiModule extends ResourceLoaderModule { + /** @var string Position on the page to load this module at */ + protected $position = 'bottom'; // Origin defaults to users with sitewide authority protected $origin = self::ORIGIN_USER_SITEWIDE; - // In-object cache for title info + // In-process cache for title info protected $titleInfo = array(); // List of page names that contain CSS @@ -50,14 +65,21 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { * @param array $options For back-compat, this can be omitted in favour of overwriting getPages. */ public function __construct( array $options = null ) { - if ( isset( $options['styles'] ) ) { - $this->styles = $options['styles']; + if ( is_null( $options ) ) { + return; } - if ( isset( $options['scripts'] ) ) { - $this->scripts = $options['scripts']; - } - if ( isset( $options['group'] ) ) { - $this->group = $options['group']; + + foreach ( $options as $member => $option ) { + switch ( $member ) { + case 'position': + $this->isPositionDefined = true; + // Don't break since we need the member set as well + case 'styles': + case 'scripts': + case 'group': + $this->{$member} = $option; + break; + } } } @@ -107,13 +129,13 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { } /** - * Get the Database object used in getTitleMTimes(). Defaults to the local slave DB - * but subclasses may want to override this to return a remote DB object, or to return - * null if getTitleMTimes() shouldn't access the DB at all. + * Get the Database object used in getTitleInfo(). + * + * Defaults to the local slave DB. Subclasses may want to override this to return a foreign + * database object, or null if getTitleInfo() shouldn't access the database. * - * NOTE: This ONLY works for getTitleMTimes() and getModifiedTime(), NOT FOR ANYTHING ELSE. - * In particular, it doesn't work for getting the content of JS and CSS pages. That functionality - * will use the local DB irrespective of the return value of this method. + * NOTE: This ONLY works for getTitleInfo() and isKnownEmpty(), NOT FOR ANYTHING ELSE. + * In particular, it doesn't work for getContent() or getScript() etc. * * @return IDatabase|null */ @@ -122,10 +144,15 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { } /** - * @param Title $title + * @param string $title * @return null|string */ - protected function getContent( $title ) { + protected function getContent( $titleText ) { + $title = Title::newFromText( $titleText ); + if ( !$title ) { + return null; + } + $handler = ContentHandler::getForTitle( $title ); if ( $handler->isSupportedFormat( CONTENT_FORMAT_CSS ) ) { $format = CONTENT_FORMAT_CSS; @@ -160,11 +187,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { if ( $options['type'] !== 'script' ) { continue; } - $title = Title::newFromText( $titleText ); - if ( !$title || $title->isRedirect() ) { - continue; - } - $script = $this->getContent( $title ); + $script = $this->getContent( $titleText ); if ( strval( $script ) !== '' ) { $script = $this->validateScriptFile( $titleText, $script ); $scripts .= ResourceLoader::makeComment( $titleText ) . $script . "\n"; @@ -183,12 +206,8 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { if ( $options['type'] !== 'style' ) { continue; } - $title = Title::newFromText( $titleText ); - if ( !$title || $title->isRedirect() ) { - continue; - } $media = isset( $options['media'] ) ? $options['media'] : 'all'; - $style = $this->getContent( $title ); + $style = $this->getContent( $titleText ); if ( strval( $style ) === '' ) { continue; } @@ -206,37 +225,31 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { } /** - * @param ResourceLoaderContext $context - * @return int + * Disable module content versioning. + * + * This class does not support generating content outside of a module + * request due to foreign database support. + * + * See getDefinitionSummary() for meta-data versioning. + * + * @return bool */ - public function getModifiedTime( ResourceLoaderContext $context ) { - $modifiedTime = 1; - $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() ), - $this->getDefinitionMtime( $context ) - ); - return $modifiedTime; + public function enableModuleContentVersion() { + return false; } /** - * Get the definition summary for this module. - * * @param ResourceLoaderContext $context * @return array */ public function getDefinitionSummary( ResourceLoaderContext $context ) { - return array( - 'class' => get_class( $this ), + $summary = parent::getDefinitionSummary( $context ); + $summary[] = array( 'pages' => $this->getPages( $context ), + // Includes SHA1 of content + 'titleInfo' => $this->getTitleInfo( $context ), ); + return $summary; } /** @@ -244,33 +257,29 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule { * @return bool */ public function isKnownEmpty( ResourceLoaderContext $context ) { - $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 - //