summaryrefslogtreecommitdiff
path: root/www/wiki/includes
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2019-07-31 14:50:50 -0300
committerYaco <franco@reevo.org>2019-07-31 14:50:50 -0300
commit3848848fc3bc2db035c824f1453635949505d76e (patch)
tree71fd898ebb220e7ba034cf2bc1bf708fdd0d6219 /www/wiki/includes
parent2dfe0b926fe5c6c4f27ad1f9bc1c1377cb091111 (diff)
ACTUALIZA MW a 1.31.3, SMW a 3.0.2 y extensiones menores
Diffstat (limited to 'www/wiki/includes')
-rw-r--r--www/wiki/includes/ActorMigration.php383
-rw-r--r--www/wiki/includes/AutoLoader.php44
-rw-r--r--www/wiki/includes/Block.php153
-rw-r--r--www/wiki/includes/CategoriesRdf.php39
-rw-r--r--www/wiki/includes/Category.php6
-rw-r--r--www/wiki/includes/CategoryFinder.php25
-rw-r--r--www/wiki/includes/CategoryViewer.php8
-rw-r--r--www/wiki/includes/CommentStore.php228
-rw-r--r--www/wiki/includes/CommentStoreComment.php2
-rw-r--r--www/wiki/includes/DefaultSettings.php484
-rw-r--r--www/wiki/includes/Defines.php2
-rw-r--r--www/wiki/includes/DeprecatedGlobal.php5
-rw-r--r--www/wiki/includes/DevelopmentSettings.php57
-rw-r--r--www/wiki/includes/EditPage.php571
-rw-r--r--www/wiki/includes/Feed.php211
-rw-r--r--www/wiki/includes/FeedUtils.php20
-rw-r--r--www/wiki/includes/FileDeleteForm.php16
-rw-r--r--www/wiki/includes/ForkController.php2
-rw-r--r--www/wiki/includes/GitInfo.php42
-rw-r--r--www/wiki/includes/GlobalFunctions.php279
-rw-r--r--www/wiki/includes/HistoryBlob.php60
-rw-r--r--www/wiki/includes/Html.php53
-rw-r--r--www/wiki/includes/HttpFunctions.php1122
-rw-r--r--www/wiki/includes/Licenses.php210
-rw-r--r--www/wiki/includes/Linker.php77
-rw-r--r--www/wiki/includes/MWGrants.php4
-rw-r--r--www/wiki/includes/MWNamespace.php62
-rw-r--r--www/wiki/includes/MagicWord.php43
-rw-r--r--www/wiki/includes/MagicWordArray.php4
-rw-r--r--www/wiki/includes/MediaWiki.php124
-rw-r--r--www/wiki/includes/MediaWikiServices.php134
-rw-r--r--www/wiki/includes/MergeHistory.php9
-rw-r--r--www/wiki/includes/Message.php48
-rw-r--r--www/wiki/includes/MimeMagic.php1
-rw-r--r--www/wiki/includes/MovePage.php9
-rw-r--r--www/wiki/includes/NoLocalSettings.php6
-rw-r--r--www/wiki/includes/OutputHandler.php306
-rw-r--r--www/wiki/includes/OutputPage.php240
-rw-r--r--www/wiki/includes/PHPVersionCheck.php36
-rw-r--r--www/wiki/includes/PHPVersionError.php26
-rw-r--r--www/wiki/includes/Pingback.php29
-rw-r--r--www/wiki/includes/PreConfigSetup.php54
-rw-r--r--www/wiki/includes/Preferences.php1515
-rw-r--r--www/wiki/includes/ProtectionForm.php16
-rw-r--r--www/wiki/includes/ProxyLookup.php2
-rw-r--r--www/wiki/includes/Revision.php1644
-rw-r--r--www/wiki/includes/RevisionList.php29
-rw-r--r--www/wiki/includes/ServiceWiring.php177
-rw-r--r--www/wiki/includes/Services/ServiceContainer.php222
-rw-r--r--www/wiki/includes/Setup.php147
-rw-r--r--www/wiki/includes/SiteConfiguration.php20
-rw-r--r--www/wiki/includes/SiteStats.php368
-rw-r--r--www/wiki/includes/SiteStatsInit.php202
-rw-r--r--www/wiki/includes/Status.php4
-rw-r--r--www/wiki/includes/Storage/BlobAccessException.php34
-rw-r--r--www/wiki/includes/Storage/BlobStore.php119
-rw-r--r--www/wiki/includes/Storage/BlobStoreFactory.php105
-rw-r--r--www/wiki/includes/Storage/IncompleteRevisionException.php32
-rw-r--r--www/wiki/includes/Storage/MutableRevisionRecord.php328
-rw-r--r--www/wiki/includes/Storage/MutableRevisionSlots.php137
-rw-r--r--www/wiki/includes/Storage/NameTableAccessException.php (renamed from www/wiki/includes/cache/ObjectFileCache.php)39
-rw-r--r--www/wiki/includes/Storage/NameTableStore.php366
-rw-r--r--www/wiki/includes/Storage/RevisionAccessException.php34
-rw-r--r--www/wiki/includes/Storage/RevisionArchiveRecord.php170
-rw-r--r--www/wiki/includes/Storage/RevisionFactory.php94
-rw-r--r--www/wiki/includes/Storage/RevisionLookup.php120
-rw-r--r--www/wiki/includes/Storage/RevisionRecord.php492
-rw-r--r--www/wiki/includes/Storage/RevisionSlots.php202
-rw-r--r--www/wiki/includes/Storage/RevisionStore.php2017
-rw-r--r--www/wiki/includes/Storage/RevisionStoreRecord.php210
-rw-r--r--www/wiki/includes/Storage/SlotRecord.php568
-rw-r--r--www/wiki/includes/Storage/SqlBlobStore.php600
-rw-r--r--www/wiki/includes/Storage/SuppressedDataException.php33
-rw-r--r--www/wiki/includes/StreamFile.php2
-rw-r--r--www/wiki/includes/StubObject.php1
-rw-r--r--www/wiki/includes/Title.php290
-rw-r--r--www/wiki/includes/TitleArray.php6
-rw-r--r--www/wiki/includes/TitleArrayFromResult.php6
-rw-r--r--www/wiki/includes/WatchedItem.php200
-rw-r--r--www/wiki/includes/WebRequest.php19
-rw-r--r--www/wiki/includes/WebStart.php74
-rw-r--r--www/wiki/includes/WikiMap.php33
-rw-r--r--www/wiki/includes/Xml.php9
-rw-r--r--www/wiki/includes/actions/CreditsAction.php3
-rw-r--r--www/wiki/includes/actions/HistoryAction.php38
-rw-r--r--www/wiki/includes/actions/InfoAction.php64
-rw-r--r--www/wiki/includes/actions/MarkpatrolledAction.php4
-rw-r--r--www/wiki/includes/actions/RawAction.php92
-rw-r--r--www/wiki/includes/actions/RevisiondeleteAction.php65
-rw-r--r--www/wiki/includes/actions/WatchAction.php2
-rw-r--r--www/wiki/includes/api/ApiBase.php146
-rw-r--r--www/wiki/includes/api/ApiBlock.php23
-rw-r--r--www/wiki/includes/api/ApiCSPReport.php2
-rw-r--r--www/wiki/includes/api/ApiCheckToken.php2
-rw-r--r--www/wiki/includes/api/ApiClearHasMsg.php2
-rw-r--r--www/wiki/includes/api/ApiComparePages.php46
-rw-r--r--www/wiki/includes/api/ApiDelete.php9
-rw-r--r--www/wiki/includes/api/ApiDisabled.php4
-rw-r--r--www/wiki/includes/api/ApiEditPage.php31
-rw-r--r--www/wiki/includes/api/ApiEmailUser.php6
-rw-r--r--www/wiki/includes/api/ApiErrorFormatter.php8
-rw-r--r--www/wiki/includes/api/ApiExpandTemplates.php4
-rw-r--r--www/wiki/includes/api/ApiFeedContributions.php4
-rw-r--r--www/wiki/includes/api/ApiFeedRecentChanges.php10
-rw-r--r--www/wiki/includes/api/ApiFeedWatchlist.php9
-rw-r--r--www/wiki/includes/api/ApiFileRevert.php4
-rw-r--r--www/wiki/includes/api/ApiFormatBase.php37
-rw-r--r--www/wiki/includes/api/ApiFormatFeedWrapper.php4
-rw-r--r--www/wiki/includes/api/ApiFormatJson.php10
-rw-r--r--www/wiki/includes/api/ApiFormatNone.php7
-rw-r--r--www/wiki/includes/api/ApiFormatPhp.php12
-rw-r--r--www/wiki/includes/api/ApiFormatRaw.php4
-rw-r--r--www/wiki/includes/api/ApiFormatXml.php4
-rw-r--r--www/wiki/includes/api/ApiHelp.php13
-rw-r--r--www/wiki/includes/api/ApiHelpParamValueMessage.php4
-rw-r--r--www/wiki/includes/api/ApiImageRotate.php5
-rw-r--r--www/wiki/includes/api/ApiImport.php15
-rw-r--r--www/wiki/includes/api/ApiLogin.php4
-rw-r--r--www/wiki/includes/api/ApiLogout.php18
-rw-r--r--www/wiki/includes/api/ApiMain.php180
-rw-r--r--www/wiki/includes/api/ApiMergeHistory.php4
-rw-r--r--www/wiki/includes/api/ApiMessage.php7
-rw-r--r--www/wiki/includes/api/ApiModuleManager.php10
-rw-r--r--www/wiki/includes/api/ApiMove.php17
-rw-r--r--www/wiki/includes/api/ApiOpenSearch.php5
-rw-r--r--www/wiki/includes/api/ApiOptions.php13
-rw-r--r--www/wiki/includes/api/ApiPageSet.php8
-rw-r--r--www/wiki/includes/api/ApiParamInfo.php10
-rw-r--r--www/wiki/includes/api/ApiParse.php44
-rw-r--r--www/wiki/includes/api/ApiPatrol.php2
-rw-r--r--www/wiki/includes/api/ApiProtect.php4
-rw-r--r--www/wiki/includes/api/ApiPurge.php11
-rw-r--r--www/wiki/includes/api/ApiQuery.php126
-rw-r--r--www/wiki/includes/api/ApiQueryAllCategories.php4
-rw-r--r--www/wiki/includes/api/ApiQueryAllDeletedRevisions.php31
-rw-r--r--www/wiki/includes/api/ApiQueryAllImages.php35
-rw-r--r--www/wiki/includes/api/ApiQueryAllLinks.php4
-rw-r--r--www/wiki/includes/api/ApiQueryAllMessages.php4
-rw-r--r--www/wiki/includes/api/ApiQueryAllPages.php8
-rw-r--r--www/wiki/includes/api/ApiQueryAllRevisions.php45
-rw-r--r--www/wiki/includes/api/ApiQueryAllUsers.php35
-rw-r--r--www/wiki/includes/api/ApiQueryBacklinks.php12
-rw-r--r--www/wiki/includes/api/ApiQueryBacklinksprop.php6
-rw-r--r--www/wiki/includes/api/ApiQueryBase.php18
-rw-r--r--www/wiki/includes/api/ApiQueryBlocks.php18
-rw-r--r--www/wiki/includes/api/ApiQueryCategories.php10
-rw-r--r--www/wiki/includes/api/ApiQueryCategoryInfo.php4
-rw-r--r--www/wiki/includes/api/ApiQueryCategoryMembers.php6
-rw-r--r--www/wiki/includes/api/ApiQueryContributors.php67
-rw-r--r--www/wiki/includes/api/ApiQueryDeletedRevisions.php31
-rw-r--r--www/wiki/includes/api/ApiQueryDeletedrevs.php52
-rw-r--r--www/wiki/includes/api/ApiQueryDisabled.php4
-rw-r--r--www/wiki/includes/api/ApiQueryDuplicateFiles.php4
-rw-r--r--www/wiki/includes/api/ApiQueryExtLinksUsage.php6
-rw-r--r--www/wiki/includes/api/ApiQueryExternalLinks.php4
-rw-r--r--www/wiki/includes/api/ApiQueryFilearchive.php29
-rw-r--r--www/wiki/includes/api/ApiQueryGeneratorBase.php4
-rw-r--r--www/wiki/includes/api/ApiQueryIWBacklinks.php4
-rw-r--r--www/wiki/includes/api/ApiQueryIWLinks.php2
-rw-r--r--www/wiki/includes/api/ApiQueryImageInfo.php56
-rw-r--r--www/wiki/includes/api/ApiQueryImages.php10
-rw-r--r--www/wiki/includes/api/ApiQueryInfo.php67
-rw-r--r--www/wiki/includes/api/ApiQueryLangBacklinks.php4
-rw-r--r--www/wiki/includes/api/ApiQueryLangLinks.php4
-rw-r--r--www/wiki/includes/api/ApiQueryLinks.php33
-rw-r--r--www/wiki/includes/api/ApiQueryLogEvents.php36
-rw-r--r--www/wiki/includes/api/ApiQueryPagePropNames.php2
-rw-r--r--www/wiki/includes/api/ApiQueryPageProps.php4
-rw-r--r--www/wiki/includes/api/ApiQueryPagesWithProp.php2
-rw-r--r--www/wiki/includes/api/ApiQueryProtectedTitles.php12
-rw-r--r--www/wiki/includes/api/ApiQueryQueryPage.php4
-rw-r--r--www/wiki/includes/api/ApiQueryRandom.php4
-rw-r--r--www/wiki/includes/api/ApiQueryRecentChanges.php101
-rw-r--r--www/wiki/includes/api/ApiQueryRevisions.php68
-rw-r--r--www/wiki/includes/api/ApiQueryRevisionsBase.php4
-rw-r--r--www/wiki/includes/api/ApiQuerySearch.php15
-rw-r--r--www/wiki/includes/api/ApiQuerySiteinfo.php12
-rw-r--r--www/wiki/includes/api/ApiQueryTags.php7
-rw-r--r--www/wiki/includes/api/ApiQueryTokens.php2
-rw-r--r--www/wiki/includes/api/ApiQueryUserContributions.php574
-rw-r--r--www/wiki/includes/api/ApiQueryUserInfo.php4
-rw-r--r--www/wiki/includes/api/ApiQueryUsers.php17
-rw-r--r--www/wiki/includes/api/ApiQueryWatchlist.php35
-rw-r--r--www/wiki/includes/api/ApiQueryWatchlistRaw.php4
-rw-r--r--www/wiki/includes/api/ApiRevisionDelete.php7
-rw-r--r--www/wiki/includes/api/ApiRollback.php6
-rw-r--r--www/wiki/includes/api/ApiRsd.php2
-rw-r--r--www/wiki/includes/api/ApiSerializable.php2
-rw-r--r--www/wiki/includes/api/ApiSetNotificationTimestamp.php6
-rw-r--r--www/wiki/includes/api/ApiSetPageLanguage.php6
-rw-r--r--www/wiki/includes/api/ApiStashEdit.php34
-rw-r--r--www/wiki/includes/api/ApiTag.php4
-rw-r--r--www/wiki/includes/api/ApiTokens.php8
-rw-r--r--www/wiki/includes/api/ApiUnblock.php4
-rw-r--r--www/wiki/includes/api/ApiUndelete.php4
-rw-r--r--www/wiki/includes/api/ApiUpload.php18
-rw-r--r--www/wiki/includes/api/ApiUsageException.php3
-rw-r--r--www/wiki/includes/api/ApiUserrights.php21
-rw-r--r--www/wiki/includes/api/ApiWatch.php4
-rw-r--r--www/wiki/includes/api/i18n/ar.json9
-rw-r--r--www/wiki/includes/api/i18n/ba.json2
-rw-r--r--www/wiki/includes/api/i18n/cs.json11
-rw-r--r--www/wiki/includes/api/i18n/de.json36
-rw-r--r--www/wiki/includes/api/i18n/en.json31
-rw-r--r--www/wiki/includes/api/i18n/es.json52
-rw-r--r--www/wiki/includes/api/i18n/eu.json20
-rw-r--r--www/wiki/includes/api/i18n/fa.json2
-rw-r--r--www/wiki/includes/api/i18n/fr.json57
-rw-r--r--www/wiki/includes/api/i18n/gl.json15
-rw-r--r--www/wiki/includes/api/i18n/he.json20
-rw-r--r--www/wiki/includes/api/i18n/hu.json9
-rw-r--r--www/wiki/includes/api/i18n/it.json8
-rw-r--r--www/wiki/includes/api/i18n/ja.json29
-rw-r--r--www/wiki/includes/api/i18n/ko.json79
-rw-r--r--www/wiki/includes/api/i18n/ksh.json2
-rw-r--r--www/wiki/includes/api/i18n/lb.json43
-rw-r--r--www/wiki/includes/api/i18n/lt.json10
-rw-r--r--www/wiki/includes/api/i18n/mk.json26
-rw-r--r--www/wiki/includes/api/i18n/nb.json455
-rw-r--r--www/wiki/includes/api/i18n/nl.json390
-rw-r--r--www/wiki/includes/api/i18n/pl.json17
-rw-r--r--www/wiki/includes/api/i18n/pt-br.json28
-rw-r--r--www/wiki/includes/api/i18n/pt.json232
-rw-r--r--www/wiki/includes/api/i18n/qqq.json20
-rw-r--r--www/wiki/includes/api/i18n/ru.json95
-rw-r--r--www/wiki/includes/api/i18n/sv.json34
-rw-r--r--www/wiki/includes/api/i18n/uk.json16
-rw-r--r--www/wiki/includes/api/i18n/zh-hans.json39
-rw-r--r--www/wiki/includes/api/i18n/zh-hant.json24
-rw-r--r--www/wiki/includes/auth/AuthManager.php15
-rw-r--r--www/wiki/includes/auth/AuthManagerAuthPlugin.php8
-rw-r--r--www/wiki/includes/auth/AuthPluginPrimaryAuthenticationProvider.php2
-rw-r--r--www/wiki/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php12
-rw-r--r--www/wiki/includes/auth/LegacyHookPreAuthenticationProvider.php15
-rw-r--r--www/wiki/includes/cache/BacklinkCache.php58
-rw-r--r--www/wiki/includes/cache/CacheDependency.php11
-rw-r--r--www/wiki/includes/cache/CacheHelper.php2
-rw-r--r--www/wiki/includes/cache/FileCacheBase.php4
-rw-r--r--www/wiki/includes/cache/GenderCache.php2
-rw-r--r--www/wiki/includes/cache/MessageBlobStore.php8
-rw-r--r--www/wiki/includes/cache/MessageCache.php81
-rw-r--r--www/wiki/includes/cache/UserCache.php19
-rw-r--r--www/wiki/includes/cache/localisation/LCStoreStaticArray.php22
-rw-r--r--www/wiki/includes/cache/localisation/LocalisationCache.php37
-rw-r--r--www/wiki/includes/changes/CategoryMembershipChange.php2
-rw-r--r--www/wiki/includes/changes/ChangesFeed.php5
-rw-r--r--www/wiki/includes/changes/ChangesList.php5
-rw-r--r--www/wiki/includes/changes/ChangesListBooleanFilter.php3
-rw-r--r--www/wiki/includes/changes/ChangesListBooleanFilterGroup.php28
-rw-r--r--www/wiki/includes/changes/ChangesListFilter.php25
-rw-r--r--www/wiki/includes/changes/ChangesListFilterGroup.php47
-rw-r--r--www/wiki/includes/changes/ChangesListStringOptionsFilterGroup.php51
-rw-r--r--www/wiki/includes/changes/EnhancedChangesList.php2
-rw-r--r--www/wiki/includes/changes/RecentChange.php149
-rw-r--r--www/wiki/includes/changetags/ChangeTags.php113
-rw-r--r--www/wiki/includes/changetags/ChangeTagsList.php4
-rw-r--r--www/wiki/includes/changetags/ChangeTagsLogItem.php6
-rw-r--r--www/wiki/includes/changetags/ChangeTagsLogList.php3
-rw-r--r--www/wiki/includes/changetags/ChangeTagsRevisionList.php13
-rw-r--r--www/wiki/includes/clientpool/RedisConnectionPool.php581
-rw-r--r--www/wiki/includes/clientpool/SquidPurgeClient.php80
-rw-r--r--www/wiki/includes/clientpool/SquidPurgeClientPool.php4
-rw-r--r--www/wiki/includes/collation/AbkhazUppercaseCollation.php93
-rw-r--r--www/wiki/includes/collation/Collation.php4
-rw-r--r--www/wiki/includes/collation/CustomUppercaseCollation.php41
-rw-r--r--www/wiki/includes/collation/IcuCollation.php17
-rw-r--r--www/wiki/includes/collation/NorthernSamiUppercaseCollation.php83
-rw-r--r--www/wiki/includes/collation/NumericUppercaseCollation.php2
-rw-r--r--www/wiki/includes/compat/ObjectFactory.php (renamed from www/wiki/includes/compat/IPSetCompat.php)7
-rw-r--r--www/wiki/includes/compat/Timestamp.php6
-rw-r--r--www/wiki/includes/compat/normal/UtfNormalUtil.php5
-rw-r--r--www/wiki/includes/composer/ComposerHookHandler.php2
-rw-r--r--www/wiki/includes/composer/ComposerPackageModifier.php2
-rw-r--r--www/wiki/includes/composer/ComposerVersionNormalizer.php2
-rw-r--r--www/wiki/includes/config/ConfigFactory.php9
-rw-r--r--www/wiki/includes/config/EtcdConfig.php60
-rw-r--r--www/wiki/includes/content/AbstractContent.php6
-rw-r--r--www/wiki/includes/content/Content.php4
-rw-r--r--www/wiki/includes/content/ContentHandler.php225
-rw-r--r--www/wiki/includes/content/JsonContent.php4
-rw-r--r--www/wiki/includes/content/TextContent.php7
-rw-r--r--www/wiki/includes/content/TextContentHandler.php2
-rw-r--r--www/wiki/includes/content/WikiTextStructure.php9
-rw-r--r--www/wiki/includes/content/WikitextContent.php32
-rw-r--r--www/wiki/includes/context/ContextSource.php21
-rw-r--r--www/wiki/includes/context/DerivativeContext.php91
-rw-r--r--www/wiki/includes/context/IContextSource.php19
-rw-r--r--www/wiki/includes/context/MutableContext.php49
-rw-r--r--www/wiki/includes/context/RequestContext.php90
-rw-r--r--www/wiki/includes/dao/DBAccessBase.php10
-rw-r--r--www/wiki/includes/db/ChronologyProtector.php209
-rw-r--r--www/wiki/includes/db/CloneDatabase.php7
-rw-r--r--www/wiki/includes/db/DBConnRef.php544
-rw-r--r--www/wiki/includes/db/Database.php3325
-rw-r--r--www/wiki/includes/db/DatabaseError.php472
-rw-r--r--www/wiki/includes/db/DatabaseMssql.php1558
-rw-r--r--www/wiki/includes/db/DatabaseMysql.php210
-rw-r--r--www/wiki/includes/db/DatabaseMysqlBase.php1512
-rw-r--r--www/wiki/includes/db/DatabaseMysqli.php332
-rw-r--r--www/wiki/includes/db/DatabaseOracle.php180
-rw-r--r--www/wiki/includes/db/DatabasePostgres.php1626
-rw-r--r--www/wiki/includes/db/DatabaseSqlite.php1085
-rw-r--r--www/wiki/includes/db/DatabaseUtility.php347
-rw-r--r--www/wiki/includes/db/IDatabase.php1596
-rw-r--r--www/wiki/includes/db/MWLBFactory.php66
-rw-r--r--www/wiki/includes/db/loadbalancer/LBFactory.php481
-rw-r--r--www/wiki/includes/db/loadbalancer/LBFactoryFake.php49
-rw-r--r--www/wiki/includes/db/loadbalancer/LBFactoryMulti.php424
-rw-r--r--www/wiki/includes/db/loadbalancer/LBFactorySimple.php167
-rw-r--r--www/wiki/includes/db/loadbalancer/LBFactorySingle.php127
-rw-r--r--www/wiki/includes/db/loadbalancer/LoadBalancer.php1407
-rw-r--r--www/wiki/includes/db/loadbalancer/LoadMonitor.php78
-rw-r--r--www/wiki/includes/db/loadbalancer/LoadMonitorMySQL.php148
-rw-r--r--www/wiki/includes/debug/MWDebug.php4
-rw-r--r--www/wiki/includes/debug/logger/LegacyLogger.php4
-rw-r--r--www/wiki/includes/debug/logger/LegacySpi.php2
-rw-r--r--www/wiki/includes/debug/logger/LoggerFactory.php2
-rw-r--r--www/wiki/includes/debug/logger/MonologSpi.php24
-rw-r--r--www/wiki/includes/debug/logger/NullSpi.php2
-rw-r--r--www/wiki/includes/debug/logger/monolog/WikiProcessor.php2
-rw-r--r--www/wiki/includes/deferred/CallableUpdate.php24
-rw-r--r--www/wiki/includes/deferred/CdnCacheUpdate.php1
-rw-r--r--www/wiki/includes/deferred/DataUpdate.php27
-rw-r--r--www/wiki/includes/deferred/DeferredUpdates.php6
-rw-r--r--www/wiki/includes/deferred/HTMLCacheUpdate.php16
-rw-r--r--www/wiki/includes/deferred/LinksUpdate.php29
-rw-r--r--www/wiki/includes/deferred/MWCallableUpdate.php12
-rw-r--r--www/wiki/includes/deferred/MergeableUpdate.php2
-rw-r--r--www/wiki/includes/deferred/SiteStatsUpdate.php83
-rw-r--r--www/wiki/includes/deferred/TransactionRoundDefiningUpdate.php30
-rw-r--r--www/wiki/includes/diff/DifferenceEngine.php210
-rw-r--r--www/wiki/includes/diff/TableDiffFormatter.php1
-rw-r--r--www/wiki/includes/diff/WikiDiff3.php621
-rw-r--r--www/wiki/includes/edit/PreparedEdit.php23
-rw-r--r--www/wiki/includes/editpage/TextConflictHelper.php257
-rw-r--r--www/wiki/includes/editpage/TextboxBuilder.php137
-rw-r--r--www/wiki/includes/exception/CannotCreateActorException.php (renamed from www/wiki/includes/compat/MemcachedClientCompat.php)15
-rw-r--r--www/wiki/includes/exception/LocalizedException.php2
-rw-r--r--www/wiki/includes/exception/MWException.php8
-rw-r--r--www/wiki/includes/exception/MWExceptionHandler.php56
-rw-r--r--www/wiki/includes/exception/MWExceptionRenderer.php64
-rw-r--r--www/wiki/includes/exception/TimestampException.php7
-rw-r--r--www/wiki/includes/export/BaseDump.php219
-rw-r--r--www/wiki/includes/export/DumpNamespaceFilter.php4
-rw-r--r--www/wiki/includes/export/ExportProgressFilter.php47
-rw-r--r--www/wiki/includes/export/WikiExporter.php77
-rw-r--r--www/wiki/includes/export/XmlDumpWriter.php4
-rw-r--r--www/wiki/includes/externalstore/ExternalStore.php69
-rw-r--r--www/wiki/includes/externalstore/ExternalStoreDB.php50
-rw-r--r--www/wiki/includes/externalstore/ExternalStoreFactory.php42
-rw-r--r--www/wiki/includes/externalstore/ExternalStoreHttp.php21
-rw-r--r--www/wiki/includes/externalstore/ExternalStoreMedium.php14
-rw-r--r--www/wiki/includes/externalstore/ExternalStoreMwstore.php15
-rw-r--r--www/wiki/includes/filebackend/FSFile.php280
-rw-r--r--www/wiki/includes/filebackend/FSFileBackend.php975
-rw-r--r--www/wiki/includes/filebackend/FileBackend.php1545
-rw-r--r--www/wiki/includes/filebackend/FileBackendGroup.php17
-rw-r--r--www/wiki/includes/filebackend/FileBackendMultiWrite.php761
-rw-r--r--www/wiki/includes/filebackend/FileBackendStore.php1971
-rw-r--r--www/wiki/includes/filebackend/FileOp.php848
-rw-r--r--www/wiki/includes/filebackend/FileOpBatch.php202
-rw-r--r--www/wiki/includes/filebackend/MemoryFileBackend.php278
-rw-r--r--www/wiki/includes/filebackend/SwiftFileBackend.php1910
-rw-r--r--www/wiki/includes/filebackend/TempFSFile.php157
-rw-r--r--www/wiki/includes/filebackend/filejournal/DBFileJournal.php4
-rw-r--r--www/wiki/includes/filebackend/filejournal/FileJournal.php251
-rw-r--r--www/wiki/includes/filebackend/lockmanager/DBLockManager.php433
-rw-r--r--www/wiki/includes/filebackend/lockmanager/FSLockManager.php248
-rw-r--r--www/wiki/includes/filebackend/lockmanager/LockManager.php258
-rw-r--r--www/wiki/includes/filebackend/lockmanager/LockManagerGroup.php2
-rw-r--r--www/wiki/includes/filebackend/lockmanager/MemcLockManager.php384
-rw-r--r--www/wiki/includes/filebackend/lockmanager/QuorumLockManager.php248
-rw-r--r--www/wiki/includes/filebackend/lockmanager/RedisLockManager.php272
-rw-r--r--www/wiki/includes/filebackend/lockmanager/ScopedLock.php105
-rw-r--r--www/wiki/includes/filerepo/FSRepo.php80
-rw-r--r--www/wiki/includes/filerepo/FileBackendDBRepoWrapper.php8
-rw-r--r--www/wiki/includes/filerepo/FileRepo.php65
-rw-r--r--www/wiki/includes/filerepo/ForeignAPIRepo.php18
-rw-r--r--www/wiki/includes/filerepo/ForeignDBRepo.php4
-rw-r--r--www/wiki/includes/filerepo/ForeignDBViaLBRepo.php4
-rw-r--r--www/wiki/includes/filerepo/LocalRepo.php57
-rw-r--r--www/wiki/includes/filerepo/RepoGroup.php6
-rw-r--r--www/wiki/includes/filerepo/file/ArchivedFile.php106
-rw-r--r--www/wiki/includes/filerepo/file/File.php49
-rw-r--r--www/wiki/includes/filerepo/file/ForeignAPIFile.php20
-rw-r--r--www/wiki/includes/filerepo/file/ForeignDBFile.php2
-rw-r--r--www/wiki/includes/filerepo/file/LocalFile.php538
-rw-r--r--www/wiki/includes/filerepo/file/OldLocalFile.php131
-rw-r--r--www/wiki/includes/filerepo/file/UnregisteredLocalFile.php2
-rw-r--r--www/wiki/includes/gallery/ImageGalleryBase.php41
-rw-r--r--www/wiki/includes/gallery/PackedOverlayImageGallery.php7
-rw-r--r--www/wiki/includes/gallery/TraditionalImageGallery.php42
-rw-r--r--www/wiki/includes/htmlform/HTMLApiField.php23
-rw-r--r--www/wiki/includes/htmlform/HTMLAutoCompleteSelectField.php177
-rw-r--r--www/wiki/includes/htmlform/HTMLButtonField.php132
-rw-r--r--www/wiki/includes/htmlform/HTMLCheckField.php131
-rw-r--r--www/wiki/includes/htmlform/HTMLCheckMatrix.php275
-rw-r--r--www/wiki/includes/htmlform/HTMLComboboxField.php59
-rw-r--r--www/wiki/includes/htmlform/HTMLEditTools.php51
-rw-r--r--www/wiki/includes/htmlform/HTMLFloatField.php46
-rw-r--r--www/wiki/includes/htmlform/HTMLForm.php107
-rw-r--r--www/wiki/includes/htmlform/HTMLFormElement.php8
-rw-r--r--www/wiki/includes/htmlform/HTMLFormField.php6
-rw-r--r--www/wiki/includes/htmlform/HTMLFormFieldCloner.php380
-rw-r--r--www/wiki/includes/htmlform/HTMLFormFieldWithButton.php75
-rw-r--r--www/wiki/includes/htmlform/HTMLHiddenField.php66
-rw-r--r--www/wiki/includes/htmlform/HTMLInfoField.php64
-rw-r--r--www/wiki/includes/htmlform/HTMLIntField.php26
-rw-r--r--www/wiki/includes/htmlform/HTMLMultiSelectField.php151
-rw-r--r--www/wiki/includes/htmlform/HTMLRadioField.php91
-rw-r--r--www/wiki/includes/htmlform/HTMLSelectAndOtherField.php134
-rw-r--r--www/wiki/includes/htmlform/HTMLSelectField.php68
-rw-r--r--www/wiki/includes/htmlform/HTMLSelectLimitField.php35
-rw-r--r--www/wiki/includes/htmlform/HTMLSelectNamespace.php36
-rw-r--r--www/wiki/includes/htmlform/HTMLSelectNamespaceWithButton.php17
-rw-r--r--www/wiki/includes/htmlform/HTMLSelectOrOtherField.php90
-rw-r--r--www/wiki/includes/htmlform/HTMLSubmitField.php19
-rw-r--r--www/wiki/includes/htmlform/HTMLTagFilter.php31
-rw-r--r--www/wiki/includes/htmlform/HTMLTextAreaField.php103
-rw-r--r--www/wiki/includes/htmlform/HTMLTextField.php177
-rw-r--r--www/wiki/includes/htmlform/HTMLTextFieldWithButton.php17
-rw-r--r--www/wiki/includes/htmlform/HTMLTitleTextField.php99
-rw-r--r--www/wiki/includes/htmlform/HTMLUserTextField.php56
-rw-r--r--www/wiki/includes/htmlform/OOUIHTMLForm.php76
-rw-r--r--www/wiki/includes/htmlform/VFormHTMLForm.php5
-rw-r--r--www/wiki/includes/htmlform/fields/HTMLCheckMatrix.php3
-rw-r--r--www/wiki/includes/htmlform/fields/HTMLEditTools.php3
-rw-r--r--www/wiki/includes/htmlform/fields/HTMLMultiSelectField.php84
-rw-r--r--www/wiki/includes/htmlform/fields/HTMLRadioField.php6
-rw-r--r--www/wiki/includes/htmlform/fields/HTMLRestrictionsField.php8
-rw-r--r--www/wiki/includes/htmlform/fields/HTMLSelectAndOtherField.php19
-rw-r--r--www/wiki/includes/htmlform/fields/HTMLSelectOrOtherField.php5
-rw-r--r--www/wiki/includes/htmlform/fields/HTMLSizeFilterField.php23
-rw-r--r--www/wiki/includes/htmlform/fields/HTMLTextAreaField.php47
-rw-r--r--www/wiki/includes/htmlform/fields/HTMLTextField.php2
-rw-r--r--www/wiki/includes/htmlform/fields/HTMLUserTextField.php64
-rw-r--r--www/wiki/includes/htmlform/fields/HTMLUsersMultiselectField.php8
-rw-r--r--www/wiki/includes/http/CurlHttpRequest.php15
-rw-r--r--www/wiki/includes/http/HttpRequestFactory.php82
-rw-r--r--www/wiki/includes/http/MWHttpRequest.php50
-rw-r--r--www/wiki/includes/http/PhpHttpRequest.php17
-rw-r--r--www/wiki/includes/import/ImportStreamSource.php4
-rw-r--r--www/wiki/includes/import/ImportableOldRevision.php68
-rw-r--r--www/wiki/includes/import/ImportableOldRevisionImporter.php145
-rw-r--r--www/wiki/includes/import/ImportableUploadRevision.php68
-rw-r--r--www/wiki/includes/import/ImportableUploadRevisionImporter.php155
-rw-r--r--www/wiki/includes/import/OldRevisionImporter.php17
-rw-r--r--www/wiki/includes/import/UploadRevisionImporter.php18
-rw-r--r--www/wiki/includes/import/WikiImporter.php51
-rw-r--r--www/wiki/includes/import/WikiRevision.php238
-rw-r--r--www/wiki/includes/installer/CliInstaller.php11
-rw-r--r--www/wiki/includes/installer/DatabaseInstaller.php6
-rw-r--r--www/wiki/includes/installer/DatabaseUpdater.php111
-rw-r--r--www/wiki/includes/installer/InstallDocFormatter.php2
-rw-r--r--www/wiki/includes/installer/Installer.php276
-rw-r--r--www/wiki/includes/installer/InstallerOverrides.php6
-rw-r--r--www/wiki/includes/installer/LocalSettingsGenerator.php9
-rw-r--r--www/wiki/includes/installer/MssqlUpdater.php23
-rw-r--r--www/wiki/includes/installer/MysqlInstaller.php23
-rw-r--r--www/wiki/includes/installer/MysqlUpdater.php77
-rw-r--r--www/wiki/includes/installer/OracleInstaller.php4
-rw-r--r--www/wiki/includes/installer/OracleUpdater.php21
-rw-r--r--www/wiki/includes/installer/PhpBugTests.php4
-rw-r--r--www/wiki/includes/installer/PostgresInstaller.php6
-rw-r--r--www/wiki/includes/installer/PostgresUpdater.php122
-rw-r--r--www/wiki/includes/installer/SqliteInstaller.php6
-rw-r--r--www/wiki/includes/installer/SqliteUpdater.php19
-rw-r--r--www/wiki/includes/installer/WebInstaller.php32
-rw-r--r--www/wiki/includes/installer/WebInstallerName.php2
-rw-r--r--www/wiki/includes/installer/WebInstallerOptions.php87
-rw-r--r--www/wiki/includes/installer/WebInstallerOutput.php2
-rw-r--r--www/wiki/includes/installer/i18n/af.json1
-rw-r--r--www/wiki/includes/installer/i18n/ar.json6
-rw-r--r--www/wiki/includes/installer/i18n/ast.json63
-rw-r--r--www/wiki/includes/installer/i18n/ba.json13
-rw-r--r--www/wiki/includes/installer/i18n/be-tarask.json18
-rw-r--r--www/wiki/includes/installer/i18n/bg.json28
-rw-r--r--www/wiki/includes/installer/i18n/bn.json3
-rw-r--r--www/wiki/includes/installer/i18n/br.json11
-rw-r--r--www/wiki/includes/installer/i18n/bs.json3
-rw-r--r--www/wiki/includes/installer/i18n/bto.json3
-rw-r--r--www/wiki/includes/installer/i18n/ca.json9
-rw-r--r--www/wiki/includes/installer/i18n/ce.json4
-rw-r--r--www/wiki/includes/installer/i18n/ckb.json5
-rw-r--r--www/wiki/includes/installer/i18n/cs.json21
-rw-r--r--www/wiki/includes/installer/i18n/csb.json3
-rw-r--r--www/wiki/includes/installer/i18n/de-ch.json6
-rw-r--r--www/wiki/includes/installer/i18n/de.json20
-rw-r--r--www/wiki/includes/installer/i18n/diq.json2
-rw-r--r--www/wiki/includes/installer/i18n/dty.json6
-rw-r--r--www/wiki/includes/installer/i18n/el.json104
-rw-r--r--www/wiki/includes/installer/i18n/eml.json2
-rw-r--r--www/wiki/includes/installer/i18n/en-gb.json8
-rw-r--r--www/wiki/includes/installer/i18n/en.json27
-rw-r--r--www/wiki/includes/installer/i18n/eo.json3
-rw-r--r--www/wiki/includes/installer/i18n/es-formal.json14
-rw-r--r--www/wiki/includes/installer/i18n/es.json48
-rw-r--r--www/wiki/includes/installer/i18n/eu.json197
-rw-r--r--www/wiki/includes/installer/i18n/fa.json18
-rw-r--r--www/wiki/includes/installer/i18n/fi.json46
-rw-r--r--www/wiki/includes/installer/i18n/fo.json4
-rw-r--r--www/wiki/includes/installer/i18n/fr.json49
-rw-r--r--www/wiki/includes/installer/i18n/frc.json2
-rw-r--r--www/wiki/includes/installer/i18n/frp.json3
-rw-r--r--www/wiki/includes/installer/i18n/gl.json14
-rw-r--r--www/wiki/includes/installer/i18n/gor.json1
-rw-r--r--www/wiki/includes/installer/i18n/gsw.json9
-rw-r--r--www/wiki/includes/installer/i18n/he.json18
-rw-r--r--www/wiki/includes/installer/i18n/hi.json3
-rw-r--r--www/wiki/includes/installer/i18n/hrx.json13
-rw-r--r--www/wiki/includes/installer/i18n/hsb.json5
-rw-r--r--www/wiki/includes/installer/i18n/hu-formal.json2
-rw-r--r--www/wiki/includes/installer/i18n/hu.json47
-rw-r--r--www/wiki/includes/installer/i18n/ia.json24
-rw-r--r--www/wiki/includes/installer/i18n/id.json24
-rw-r--r--www/wiki/includes/installer/i18n/is.json5
-rw-r--r--www/wiki/includes/installer/i18n/it.json15
-rw-r--r--www/wiki/includes/installer/i18n/ja.json41
-rw-r--r--www/wiki/includes/installer/i18n/ka.json3
-rw-r--r--www/wiki/includes/installer/i18n/ko.json18
-rw-r--r--www/wiki/includes/installer/i18n/ksh.json13
-rw-r--r--www/wiki/includes/installer/i18n/ku-latn.json5
-rw-r--r--www/wiki/includes/installer/i18n/lb.json3
-rw-r--r--www/wiki/includes/installer/i18n/lij.json13
-rw-r--r--www/wiki/includes/installer/i18n/lki.json6
-rw-r--r--www/wiki/includes/installer/i18n/lt.json3
-rw-r--r--www/wiki/includes/installer/i18n/mg.json6
-rw-r--r--www/wiki/includes/installer/i18n/mk.json20
-rw-r--r--www/wiki/includes/installer/i18n/mr.json7
-rw-r--r--www/wiki/includes/installer/i18n/ms.json9
-rw-r--r--www/wiki/includes/installer/i18n/mzn.json5
-rw-r--r--www/wiki/includes/installer/i18n/nan.json17
-rw-r--r--www/wiki/includes/installer/i18n/nap.json13
-rw-r--r--www/wiki/includes/installer/i18n/nb.json20
-rw-r--r--www/wiki/includes/installer/i18n/nl-informal.json21
-rw-r--r--www/wiki/includes/installer/i18n/nl.json30
-rw-r--r--www/wiki/includes/installer/i18n/oc.json5
-rw-r--r--www/wiki/includes/installer/i18n/pl.json25
-rw-r--r--www/wiki/includes/installer/i18n/pms.json13
-rw-r--r--www/wiki/includes/installer/i18n/ps.json3
-rw-r--r--www/wiki/includes/installer/i18n/pt-br.json18
-rw-r--r--www/wiki/includes/installer/i18n/pt.json44
-rw-r--r--www/wiki/includes/installer/i18n/qqq.json14
-rw-r--r--www/wiki/includes/installer/i18n/ro.json3
-rw-r--r--www/wiki/includes/installer/i18n/ru.json23
-rw-r--r--www/wiki/includes/installer/i18n/sco.json13
-rw-r--r--www/wiki/includes/installer/i18n/sl.json10
-rw-r--r--www/wiki/includes/installer/i18n/sr-ec.json77
-rw-r--r--www/wiki/includes/installer/i18n/sv.json18
-rw-r--r--www/wiki/includes/installer/i18n/te.json7
-rw-r--r--www/wiki/includes/installer/i18n/th.json13
-rw-r--r--www/wiki/includes/installer/i18n/tl.json49
-rw-r--r--www/wiki/includes/installer/i18n/tokipona.json8
-rw-r--r--www/wiki/includes/installer/i18n/tr.json29
-rw-r--r--www/wiki/includes/installer/i18n/tt-cyrl.json3
-rw-r--r--www/wiki/includes/installer/i18n/uk.json21
-rw-r--r--www/wiki/includes/installer/i18n/vi.json13
-rw-r--r--www/wiki/includes/installer/i18n/war.json4
-rw-r--r--www/wiki/includes/installer/i18n/yi.json4
-rw-r--r--www/wiki/includes/installer/i18n/zh-hans.json20
-rw-r--r--www/wiki/includes/installer/i18n/zh-hant.json37
-rw-r--r--www/wiki/includes/interwiki/ClassicInterwikiLookup.php11
-rw-r--r--www/wiki/includes/interwiki/Interwiki.php2
-rw-r--r--www/wiki/includes/interwiki/NullInterwikiLookup.php57
-rw-r--r--www/wiki/includes/jobqueue/Job.php16
-rw-r--r--www/wiki/includes/jobqueue/JobQueue.php2
-rw-r--r--www/wiki/includes/jobqueue/JobQueueDB.php4
-rw-r--r--www/wiki/includes/jobqueue/JobQueueFederated.php5
-rw-r--r--www/wiki/includes/jobqueue/JobQueueGroup.php55
-rw-r--r--www/wiki/includes/jobqueue/JobQueueSecondTestQueue.php10
-rw-r--r--www/wiki/includes/jobqueue/JobRunner.php13
-rw-r--r--www/wiki/includes/jobqueue/JobSpecification.php1
-rw-r--r--www/wiki/includes/jobqueue/aggregator/JobQueueAggregator.php3
-rw-r--r--www/wiki/includes/jobqueue/jobs/ActivityUpdateJob.php15
-rw-r--r--www/wiki/includes/jobqueue/jobs/CategoryMembershipChangeJob.php44
-rw-r--r--www/wiki/includes/jobqueue/jobs/ClearUserWatchlistJob.php118
-rw-r--r--www/wiki/includes/jobqueue/jobs/ClearWatchlistNotificationsJob.php79
-rw-r--r--www/wiki/includes/jobqueue/jobs/DeleteLinksJob.php3
-rw-r--r--www/wiki/includes/jobqueue/jobs/EnqueueJob.php9
-rw-r--r--www/wiki/includes/jobqueue/jobs/HTMLCacheUpdateJob.php34
-rw-r--r--www/wiki/includes/jobqueue/jobs/RecentChangesUpdateJob.php234
-rw-r--r--www/wiki/includes/jobqueue/jobs/RefreshLinksJob.php10
-rw-r--r--www/wiki/includes/jobqueue/jobs/UserGroupExpiryJob.php39
-rw-r--r--www/wiki/includes/json/FormatJson.php2
-rw-r--r--www/wiki/includes/libs/CSSMin.php44
-rw-r--r--www/wiki/includes/libs/CryptRand.php61
-rw-r--r--www/wiki/includes/libs/DeferredStringifier.php1
-rw-r--r--www/wiki/includes/libs/HashRing.php26
-rw-r--r--www/wiki/includes/libs/HtmlArmor.php2
-rw-r--r--www/wiki/includes/libs/IEContentAnalyzer.php851
-rw-r--r--www/wiki/includes/libs/IEUrlExtension.php2
-rw-r--r--www/wiki/includes/libs/IP.php2
-rw-r--r--www/wiki/includes/libs/JavaScriptMinifier.php364
-rw-r--r--www/wiki/includes/libs/MWMessagePack.php232
-rw-r--r--www/wiki/includes/libs/MapCacheLRU.php49
-rw-r--r--www/wiki/includes/libs/MultiHttpClient.php18
-rw-r--r--www/wiki/includes/libs/ObjectFactory.php198
-rw-r--r--www/wiki/includes/libs/ProcessCacheLRU.php9
-rw-r--r--www/wiki/includes/libs/SamplingStatsdClient.php139
-rw-r--r--www/wiki/includes/libs/ScopedCallback.php77
-rw-r--r--www/wiki/includes/libs/StatusValue.php2
-rw-r--r--www/wiki/includes/libs/StringUtils.php14
-rw-r--r--www/wiki/includes/libs/Timing.php4
-rw-r--r--www/wiki/includes/libs/XhprofData.php6
-rw-r--r--www/wiki/includes/libs/XmlTypeCheck.php508
-rw-r--r--www/wiki/includes/libs/filebackend/FSFileBackend.php8
-rw-r--r--www/wiki/includes/libs/filebackend/FileBackend.php4
-rw-r--r--www/wiki/includes/libs/filebackend/FileBackendMultiWrite.php3
-rw-r--r--www/wiki/includes/libs/filebackend/FileBackendStore.php24
-rw-r--r--www/wiki/includes/libs/filebackend/HTTPFileStreamer.php4
-rw-r--r--www/wiki/includes/libs/filebackend/MemoryFileBackend.php4
-rw-r--r--www/wiki/includes/libs/filebackend/SwiftFileBackend.php126
-rw-r--r--www/wiki/includes/libs/filebackend/fileop/StoreFileOp.php4
-rw-r--r--www/wiki/includes/libs/filebackend/fsfile/FSFile.php8
-rw-r--r--www/wiki/includes/libs/filebackend/fsfile/TempFSFile.php12
-rw-r--r--www/wiki/includes/libs/http/HttpAcceptNegotiator.php9
-rw-r--r--www/wiki/includes/libs/http/HttpAcceptParser.php2
-rw-r--r--www/wiki/includes/libs/jsminplus.php2
-rw-r--r--www/wiki/includes/libs/lockmanager/FSLockManager.php4
-rw-r--r--www/wiki/includes/libs/lockmanager/LockManager.php2
-rw-r--r--www/wiki/includes/libs/lockmanager/MemcLockManager.php4
-rw-r--r--www/wiki/includes/libs/lockmanager/ScopedLock.php2
-rw-r--r--www/wiki/includes/libs/mime/MimeAnalyzer.php8
-rw-r--r--www/wiki/includes/libs/mime/XmlTypeCheck.php38
-rw-r--r--www/wiki/includes/libs/mime/mime.info1
-rw-r--r--www/wiki/includes/libs/mime/mime.types1
-rw-r--r--www/wiki/includes/libs/objectcache/BagOStuff.php43
-rw-r--r--www/wiki/includes/libs/objectcache/CachedBagOStuff.php4
-rw-r--r--www/wiki/includes/libs/objectcache/HashBagOStuff.php2
-rw-r--r--www/wiki/includes/libs/objectcache/IExpiringStore.php1
-rw-r--r--www/wiki/includes/libs/objectcache/MemcachedBagOStuff.php2
-rw-r--r--www/wiki/includes/libs/objectcache/MemcachedClient.php8
-rw-r--r--www/wiki/includes/libs/objectcache/MemcachedPhpBagOStuff.php6
-rw-r--r--www/wiki/includes/libs/objectcache/MultiWriteBagOStuff.php5
-rw-r--r--www/wiki/includes/libs/objectcache/RedisBagOStuff.php5
-rw-r--r--www/wiki/includes/libs/objectcache/ReplicatedBagOStuff.php1
-rw-r--r--www/wiki/includes/libs/objectcache/WANObjectCache.php691
-rw-r--r--www/wiki/includes/libs/objectcache/XCacheBagOStuff.php68
-rw-r--r--www/wiki/includes/libs/rdbms/ChronologyProtector.php117
-rw-r--r--www/wiki/includes/libs/rdbms/TransactionProfiler.php12
-rw-r--r--www/wiki/includes/libs/rdbms/connectionmanager/ConnectionManager.php20
-rw-r--r--www/wiki/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php20
-rw-r--r--www/wiki/includes/libs/rdbms/database/AtomicSectionIdentifier.php (renamed from www/wiki/includes/compat/RunningStatCompat.php)9
-rw-r--r--www/wiki/includes/libs/rdbms/database/DBConnRef.php61
-rw-r--r--www/wiki/includes/libs/rdbms/database/Database.php2045
-rw-r--r--www/wiki/includes/libs/rdbms/database/DatabaseMssql.php276
-rw-r--r--www/wiki/includes/libs/rdbms/database/DatabaseMysql.php210
-rw-r--r--www/wiki/includes/libs/rdbms/database/DatabaseMysqlBase.php426
-rw-r--r--www/wiki/includes/libs/rdbms/database/DatabaseMysqli.php46
-rw-r--r--www/wiki/includes/libs/rdbms/database/DatabasePostgres.php713
-rw-r--r--www/wiki/includes/libs/rdbms/database/DatabaseSqlite.php191
-rw-r--r--www/wiki/includes/libs/rdbms/database/IDatabase.php479
-rw-r--r--www/wiki/includes/libs/rdbms/database/IMaintainableDatabase.php36
-rw-r--r--www/wiki/includes/libs/rdbms/database/MaintainableDBConnRef.php12
-rw-r--r--www/wiki/includes/libs/rdbms/database/position/DBMasterPos.php4
-rw-r--r--www/wiki/includes/libs/rdbms/database/position/MySQLMasterPos.php293
-rw-r--r--www/wiki/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php18
-rw-r--r--www/wiki/includes/libs/rdbms/database/utils/SavepointPostgres.php1
-rw-r--r--www/wiki/includes/libs/rdbms/encasing/Blob.php6
-rw-r--r--www/wiki/includes/libs/rdbms/encasing/MssqlBlob.php12
-rw-r--r--www/wiki/includes/libs/rdbms/encasing/Subquery.php44
-rw-r--r--www/wiki/includes/libs/rdbms/exception/DBError.php9
-rw-r--r--www/wiki/includes/libs/rdbms/exception/DBExpectedError.php25
-rw-r--r--www/wiki/includes/libs/rdbms/exception/DBQueryError.php27
-rw-r--r--www/wiki/includes/libs/rdbms/exception/DBQueryTimeoutError.php (renamed from www/wiki/includes/compat/CdbCompat.php)33
-rw-r--r--www/wiki/includes/libs/rdbms/exception/DBTransactionStateError.php (renamed from www/wiki/includes/HtmlFormatter.php)13
-rw-r--r--www/wiki/includes/libs/rdbms/field/PostgresField.php48
-rw-r--r--www/wiki/includes/libs/rdbms/lbfactory/ILBFactory.php53
-rw-r--r--www/wiki/includes/libs/rdbms/lbfactory/LBFactory.php69
-rw-r--r--www/wiki/includes/libs/rdbms/lbfactory/LBFactoryMulti.php10
-rw-r--r--www/wiki/includes/libs/rdbms/lbfactory/LBFactorySimple.php7
-rw-r--r--www/wiki/includes/libs/rdbms/lbfactory/LBFactorySingle.php4
-rw-r--r--www/wiki/includes/libs/rdbms/loadbalancer/ILoadBalancer.php86
-rw-r--r--www/wiki/includes/libs/rdbms/loadbalancer/LoadBalancer.php589
-rw-r--r--www/wiki/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php4
-rw-r--r--www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitor.php52
-rw-r--r--www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php4
-rw-r--r--www/wiki/includes/libs/stats/BufferingStatsdDataFactory.php21
-rw-r--r--www/wiki/includes/libs/stats/IBufferingStatsdDataFactory.php18
-rw-r--r--www/wiki/includes/libs/stats/NullStatsdDataFactory.php22
-rw-r--r--www/wiki/includes/libs/virtualrest/SwiftVirtualRESTService.php2
-rw-r--r--www/wiki/includes/libs/xmp/XMP.php147
-rw-r--r--www/wiki/includes/libs/xmp/XMPValidate.php6
-rwxr-xr-xwww/wiki/includes/limit.sh122
-rw-r--r--www/wiki/includes/linkeddata/PageDataRequestHandler.php20
-rw-r--r--www/wiki/includes/linker/LinkRenderer.php1
-rw-r--r--www/wiki/includes/linker/LinkRendererFactory.php1
-rw-r--r--www/wiki/includes/linker/LinkTarget.php12
-rw-r--r--www/wiki/includes/logging/BlockLogFormatter.php4
-rw-r--r--www/wiki/includes/logging/ContentModelLogFormatter.php4
-rw-r--r--www/wiki/includes/logging/DeleteLogFormatter.php4
-rw-r--r--www/wiki/includes/logging/LogEntry.php100
-rw-r--r--www/wiki/includes/logging/LogEventsList.php38
-rw-r--r--www/wiki/includes/logging/LogFormatter.php32
-rw-r--r--www/wiki/includes/logging/LogPage.php7
-rw-r--r--www/wiki/includes/logging/LogPager.php84
-rw-r--r--www/wiki/includes/logging/MergeLogFormatter.php4
-rw-r--r--www/wiki/includes/logging/MoveLogFormatter.php4
-rw-r--r--www/wiki/includes/logging/PatrolLog.php7
-rw-r--r--www/wiki/includes/logging/PatrolLogFormatter.php4
-rw-r--r--www/wiki/includes/logging/ProtectLogFormatter.php3
-rw-r--r--www/wiki/includes/logging/WikitextLogFormatter.php (renamed from www/wiki/includes/utils/iterators/NotRecursiveIterator.php)24
-rw-r--r--www/wiki/includes/mail/EmailNotification.php2
-rw-r--r--www/wiki/includes/mail/MailAddress.php6
-rw-r--r--www/wiki/includes/mail/UserMailer.php23
-rw-r--r--www/wiki/includes/media/Bitmap.php28
-rw-r--r--www/wiki/includes/media/BitmapMetadataHandler.php8
-rw-r--r--www/wiki/includes/media/Bitmap_ClientOnly.php3
-rw-r--r--www/wiki/includes/media/DjVu.php10
-rw-r--r--www/wiki/includes/media/DjVuImage.php4
-rw-r--r--www/wiki/includes/media/Exif.php24
-rw-r--r--www/wiki/includes/media/ExifBitmap.php8
-rw-r--r--www/wiki/includes/media/FormatMetadata.php21
-rw-r--r--www/wiki/includes/media/GIF.php12
-rw-r--r--www/wiki/includes/media/GIFMetadataExtractor.php4
-rw-r--r--www/wiki/includes/media/IPTC.php18
-rw-r--r--www/wiki/includes/media/ImageHandler.php4
-rw-r--r--www/wiki/includes/media/JpegMetadataExtractor.php8
-rw-r--r--www/wiki/includes/media/MediaHandler.php14
-rw-r--r--www/wiki/includes/media/MediaTransformOutput.php4
-rw-r--r--www/wiki/includes/media/PNG.php12
-rw-r--r--www/wiki/includes/media/PNGMetadataExtractor.php16
-rw-r--r--www/wiki/includes/media/SVG.php58
-rw-r--r--www/wiki/includes/media/SVGMetadataExtractor.php6
-rw-r--r--www/wiki/includes/media/TransformationalImageHandler.php2
-rw-r--r--www/wiki/includes/media/WebP.php14
-rw-r--r--www/wiki/includes/media/XCF.php29
-rw-r--r--www/wiki/includes/media/XMP.php1383
-rw-r--r--www/wiki/includes/media/XMPInfo.php1168
-rw-r--r--www/wiki/includes/media/XMPValidate.php398
-rw-r--r--www/wiki/includes/mime.info119
-rw-r--r--www/wiki/includes/mime.types186
-rw-r--r--www/wiki/includes/objectcache/MemcachedPeclBagOStuff.php241
-rw-r--r--www/wiki/includes/objectcache/ObjectCache.php12
-rw-r--r--www/wiki/includes/objectcache/RedisBagOStuff.php412
-rw-r--r--www/wiki/includes/objectcache/SqlBagOStuff.php15
-rw-r--r--www/wiki/includes/page/Article.php93
-rw-r--r--www/wiki/includes/page/CategoryPage.php2
-rw-r--r--www/wiki/includes/page/ImagePage.php137
-rw-r--r--www/wiki/includes/page/PageArchive.php144
-rw-r--r--www/wiki/includes/page/WikiFilePage.php4
-rw-r--r--www/wiki/includes/page/WikiPage.php411
-rw-r--r--www/wiki/includes/pager/IndexPager.php18
-rw-r--r--www/wiki/includes/parser/BlockLevelPass.php249
-rw-r--r--www/wiki/includes/parser/CoreParserFunctions.php17
-rw-r--r--www/wiki/includes/parser/DateFormatter.php5
-rw-r--r--www/wiki/includes/parser/LinkHolderArray.php20
-rw-r--r--www/wiki/includes/parser/MWTidy.php21
-rw-r--r--www/wiki/includes/parser/Parser.php633
-rw-r--r--www/wiki/includes/parser/ParserCache.php5
-rw-r--r--www/wiki/includes/parser/ParserOptions.php44
-rw-r--r--www/wiki/includes/parser/ParserOutput.php128
-rw-r--r--www/wiki/includes/parser/Preprocessor_DOM.php131
-rw-r--r--www/wiki/includes/parser/Preprocessor_Hash.php179
-rw-r--r--www/wiki/includes/parser/RemexStripTagHandler.php40
-rw-r--r--www/wiki/includes/parser/Sanitizer.php (renamed from www/wiki/includes/Sanitizer.php)83
-rw-r--r--www/wiki/includes/parser/StripState.php173
-rw-r--r--www/wiki/includes/password/PasswordFactory.php2
-rw-r--r--www/wiki/includes/password/PasswordPolicyChecks.php2
-rw-r--r--www/wiki/includes/poolcounter/PoolCounter.php3
-rw-r--r--www/wiki/includes/poolcounter/PoolCounterRedis.php4
-rw-r--r--www/wiki/includes/poolcounter/PoolWorkArticleView.php2
-rw-r--r--www/wiki/includes/preferences/DefaultPreferencesFactory.php1741
-rw-r--r--www/wiki/includes/preferences/PreferencesFactory.php83
-rw-r--r--www/wiki/includes/profiler/Profiler.php9
-rw-r--r--www/wiki/includes/profiler/ProfilerFunctions.php56
-rw-r--r--www/wiki/includes/profiler/ProfilerSectionOnly.php2
-rw-r--r--www/wiki/includes/profiler/ProfilerXhprof.php4
-rw-r--r--www/wiki/includes/profiler/TransactionProfiler.php314
-rw-r--r--www/wiki/includes/profiler/output/ProfilerOutputDb.php83
-rw-r--r--www/wiki/includes/profiler/output/ProfilerOutputText.php2
-rw-r--r--www/wiki/includes/rcfeed/IRCColourfulRCFeedFormatter.php2
-rw-r--r--www/wiki/includes/registration/CoreVersionChecker.php68
-rw-r--r--www/wiki/includes/registration/ExtensionDependencyError.php81
-rw-r--r--www/wiki/includes/registration/ExtensionJsonValidator.php17
-rw-r--r--www/wiki/includes/registration/ExtensionProcessor.php73
-rw-r--r--www/wiki/includes/registration/ExtensionRegistry.php73
-rw-r--r--www/wiki/includes/registration/VersionChecker.php49
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoader.php65
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderClientHtml.php41
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderContext.php18
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderFileModule.php3
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderFilePath.php3
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderImage.php24
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderImageModule.php4
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderLanguageDataModule.php1
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderModule.php68
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderOOUIImageModule.php7
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderSkinModule.php90
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderStartUpModule.php23
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php86
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderUserGroupsModule.php70
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderUserOptionsModule.php12
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderUserTokensModule.php13
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderWikiModule.php10
-rw-r--r--www/wiki/includes/revisiondelete/RevDelArchiveItem.php4
-rw-r--r--www/wiki/includes/revisiondelete/RevDelArchiveList.php7
-rw-r--r--www/wiki/includes/revisiondelete/RevDelArchivedFileItem.php4
-rw-r--r--www/wiki/includes/revisiondelete/RevDelArchivedFileList.php8
-rw-r--r--www/wiki/includes/revisiondelete/RevDelFileItem.php4
-rw-r--r--www/wiki/includes/revisiondelete/RevDelFileList.php8
-rw-r--r--www/wiki/includes/revisiondelete/RevDelItem.php2
-rw-r--r--www/wiki/includes/revisiondelete/RevDelList.php52
-rw-r--r--www/wiki/includes/revisiondelete/RevDelLogItem.php11
-rw-r--r--www/wiki/includes/revisiondelete/RevDelLogList.php11
-rw-r--r--www/wiki/includes/revisiondelete/RevDelRevisionItem.php6
-rw-r--r--www/wiki/includes/revisiondelete/RevDelRevisionList.php17
-rw-r--r--www/wiki/includes/revisiondelete/RevisionDeleteUser.php150
-rw-r--r--www/wiki/includes/revisiondelete/RevisionDeleter.php10
-rw-r--r--www/wiki/includes/search/SearchEngine.php27
-rw-r--r--www/wiki/includes/search/SearchEngineFactory.php12
-rw-r--r--www/wiki/includes/search/SearchExactMatchRescorer.php8
-rw-r--r--www/wiki/includes/search/SearchMySQL.php12
-rw-r--r--www/wiki/includes/search/SearchSqlite.php4
-rw-r--r--www/wiki/includes/session/PHPSessionHandler.php38
-rw-r--r--www/wiki/includes/session/Session.php8
-rw-r--r--www/wiki/includes/session/SessionBackend.php6
-rw-r--r--www/wiki/includes/session/SessionManager.php3
-rw-r--r--www/wiki/includes/shell/Command.php266
-rw-r--r--www/wiki/includes/shell/CommandFactory.php55
-rw-r--r--www/wiki/includes/shell/FirejailCommand.php160
-rw-r--r--www/wiki/includes/shell/Result.php17
-rw-r--r--www/wiki/includes/shell/Shell.php98
-rw-r--r--www/wiki/includes/shell/firejail.profile7
-rw-r--r--www/wiki/includes/site/MediaWikiPageNameNormalizer.php7
-rw-r--r--www/wiki/includes/site/MediaWikiSite.php7
-rw-r--r--www/wiki/includes/site/Site.php8
-rw-r--r--www/wiki/includes/site/SiteList.php2
-rw-r--r--www/wiki/includes/skins/BaseTemplate.php100
-rw-r--r--www/wiki/includes/skins/MediaWikiI18N.php13
-rw-r--r--www/wiki/includes/skins/QuickTemplate.php33
-rw-r--r--www/wiki/includes/skins/Skin.php81
-rw-r--r--www/wiki/includes/skins/SkinApi.php4
-rw-r--r--www/wiki/includes/skins/SkinApiTemplate.php2
-rw-r--r--www/wiki/includes/skins/SkinFallback.php2
-rw-r--r--www/wiki/includes/skins/SkinFallbackTemplate.php12
-rw-r--r--www/wiki/includes/skins/SkinTemplate.php77
-rw-r--r--www/wiki/includes/sparql/SparqlClient.php220
-rw-r--r--www/wiki/includes/sparql/SparqlException.php30
-rw-r--r--www/wiki/includes/specialpage/AuthManagerSpecialPage.php2
-rw-r--r--www/wiki/includes/specialpage/ChangesListSpecialPage.php673
-rw-r--r--www/wiki/includes/specialpage/FormSpecialPage.php43
-rw-r--r--www/wiki/includes/specialpage/ImageQueryPage.php4
-rw-r--r--www/wiki/includes/specialpage/LoginSignupSpecialPage.php4
-rw-r--r--www/wiki/includes/specialpage/PageQueryPage.php4
-rw-r--r--www/wiki/includes/specialpage/QueryPage.php87
-rw-r--r--www/wiki/includes/specialpage/SpecialPage.php57
-rw-r--r--www/wiki/includes/specialpage/SpecialPageFactory.php256
-rw-r--r--www/wiki/includes/specialpage/WantedQueryPage.php4
-rw-r--r--www/wiki/includes/specials/SpecialApiSandbox.php1
-rw-r--r--www/wiki/includes/specials/SpecialBlock.php22
-rw-r--r--www/wiki/includes/specials/SpecialBotPasswords.php2
-rw-r--r--www/wiki/includes/specials/SpecialBrokenRedirects.php4
-rw-r--r--www/wiki/includes/specials/SpecialChangeEmail.php16
-rw-r--r--www/wiki/includes/specials/SpecialContributions.php68
-rw-r--r--www/wiki/includes/specials/SpecialDeletedContributions.php5
-rw-r--r--www/wiki/includes/specials/SpecialDiff.php2
-rw-r--r--www/wiki/includes/specials/SpecialDoubleRedirects.php4
-rw-r--r--www/wiki/includes/specials/SpecialEditTags.php31
-rw-r--r--www/wiki/includes/specials/SpecialEditWatchlist.php49
-rw-r--r--www/wiki/includes/specials/SpecialEmailuser.php29
-rw-r--r--www/wiki/includes/specials/SpecialExpandTemplates.php9
-rw-r--r--www/wiki/includes/specials/SpecialExport.php10
-rw-r--r--www/wiki/includes/specials/SpecialFileDuplicateSearch.php8
-rw-r--r--www/wiki/includes/specials/SpecialImport.php42
-rw-r--r--www/wiki/includes/specials/SpecialJavaScriptTest.php9
-rw-r--r--www/wiki/includes/specials/SpecialLinkSearch.php4
-rw-r--r--www/wiki/includes/specials/SpecialListDuplicatedFiles.php4
-rw-r--r--www/wiki/includes/specials/SpecialListgrouprights.php20
-rw-r--r--www/wiki/includes/specials/SpecialListredirects.php4
-rw-r--r--www/wiki/includes/specials/SpecialLockdb.php4
-rw-r--r--www/wiki/includes/specials/SpecialLog.php35
-rw-r--r--www/wiki/includes/specials/SpecialMIMEsearch.php6
-rw-r--r--www/wiki/includes/specials/SpecialMediaStatistics.php9
-rw-r--r--www/wiki/includes/specials/SpecialMostcategories.php4
-rw-r--r--www/wiki/includes/specials/SpecialMostinterwikis.php4
-rw-r--r--www/wiki/includes/specials/SpecialMostlinked.php4
-rw-r--r--www/wiki/includes/specials/SpecialMostlinkedcategories.php4
-rw-r--r--www/wiki/includes/specials/SpecialMostlinkedtemplates.php4
-rw-r--r--www/wiki/includes/specials/SpecialMovepage.php23
-rw-r--r--www/wiki/includes/specials/SpecialMyLanguage.php33
-rw-r--r--www/wiki/includes/specials/SpecialNewpages.php13
-rw-r--r--www/wiki/includes/specials/SpecialPageData.php23
-rw-r--r--www/wiki/includes/specials/SpecialPasswordReset.php5
-rw-r--r--www/wiki/includes/specials/SpecialPreferences.php11
-rw-r--r--www/wiki/includes/specials/SpecialProtectedpages.php186
-rw-r--r--www/wiki/includes/specials/SpecialProtectedtitles.php71
-rw-r--r--www/wiki/includes/specials/SpecialRecentchanges.php210
-rw-r--r--www/wiki/includes/specials/SpecialRecentchangeslinked.php31
-rw-r--r--www/wiki/includes/specials/SpecialRedirect.php1
-rw-r--r--www/wiki/includes/specials/SpecialResetTokens.php2
-rw-r--r--www/wiki/includes/specials/SpecialRevisiondelete.php28
-rw-r--r--www/wiki/includes/specials/SpecialRunJobs.php63
-rw-r--r--www/wiki/includes/specials/SpecialSearch.php13
-rw-r--r--www/wiki/includes/specials/SpecialShortpages.php4
-rw-r--r--www/wiki/includes/specials/SpecialTags.php2
-rw-r--r--www/wiki/includes/specials/SpecialUnblock.php4
-rw-r--r--www/wiki/includes/specials/SpecialUncategorizedcategories.php2
-rw-r--r--www/wiki/includes/specials/SpecialUndelete.php58
-rw-r--r--www/wiki/includes/specials/SpecialUnlockdb.php4
-rw-r--r--www/wiki/includes/specials/SpecialUnwatchedpages.php4
-rw-r--r--www/wiki/includes/specials/SpecialUpload.php34
-rw-r--r--www/wiki/includes/specials/SpecialUploadStash.php63
-rw-r--r--www/wiki/includes/specials/SpecialUserLogout.php22
-rw-r--r--www/wiki/includes/specials/SpecialUserrights.php37
-rw-r--r--www/wiki/includes/specials/SpecialVersion.php5
-rw-r--r--www/wiki/includes/specials/SpecialWatchlist.php214
-rw-r--r--www/wiki/includes/specials/SpecialWhatlinkshere.php4
-rw-r--r--www/wiki/includes/specials/formfields/Licenses.php81
-rw-r--r--www/wiki/includes/specials/forms/UploadForm.php6
-rw-r--r--www/wiki/includes/specials/helpers/License.php25
-rw-r--r--www/wiki/includes/specials/pagers/ActiveUsersPager.php15
-rw-r--r--www/wiki/includes/specials/pagers/BlockListPager.php21
-rw-r--r--www/wiki/includes/specials/pagers/ContribsPager.php165
-rw-r--r--www/wiki/includes/specials/pagers/DeletedContribsPager.php40
-rw-r--r--www/wiki/includes/specials/pagers/ImageListPager.php39
-rw-r--r--www/wiki/includes/specials/pagers/MergeHistoryPager.php9
-rw-r--r--www/wiki/includes/specials/pagers/NewFilesPager.php37
-rw-r--r--www/wiki/includes/specials/pagers/NewPagesPager.php28
-rw-r--r--www/wiki/includes/specials/pagers/ProtectedPagesPager.php23
-rw-r--r--www/wiki/includes/specials/pagers/UsersPager.php20
-rw-r--r--www/wiki/includes/templates/AtomHeader.mustache8
-rw-r--r--www/wiki/includes/templates/AtomItem.mustache10
-rw-r--r--www/wiki/includes/templates/NoLocalSettings.mustache6
-rw-r--r--www/wiki/includes/templates/RSSHeader.mustache8
-rw-r--r--www/wiki/includes/templates/RSSItem.mustache9
-rw-r--r--www/wiki/includes/tidy/Balancer.php1922
-rw-r--r--www/wiki/includes/tidy/RemexCompatMunger.php37
-rw-r--r--www/wiki/includes/tidy/RemexMungerData.php39
-rw-r--r--www/wiki/includes/tidy/TidyDriverBase.php12
-rw-r--r--www/wiki/includes/title/ForeignTitleFactory.php1
-rw-r--r--www/wiki/includes/title/ImportTitleFactory.php1
-rw-r--r--www/wiki/includes/title/MediaWikiPageLinkRenderer.php134
-rw-r--r--www/wiki/includes/title/MediaWikiTitleCodec.php5
-rw-r--r--www/wiki/includes/title/NaiveForeignTitleFactory.php1
-rw-r--r--www/wiki/includes/title/NaiveImportTitleFactory.php1
-rw-r--r--www/wiki/includes/title/NamespaceAwareForeignTitleFactory.php1
-rw-r--r--www/wiki/includes/title/NamespaceImportTitleFactory.php1
-rw-r--r--www/wiki/includes/title/PageLinkRenderer.php69
-rw-r--r--www/wiki/includes/title/SubpageImportTitleFactory.php1
-rw-r--r--www/wiki/includes/title/TitleFormatter.php3
-rw-r--r--www/wiki/includes/title/TitleParser.php1
-rw-r--r--www/wiki/includes/title/TitleValue.php15
-rw-r--r--www/wiki/includes/upload/UploadBase.php34
-rw-r--r--www/wiki/includes/upload/UploadStash.php143
-rw-r--r--www/wiki/includes/user/BotPassword.php6
-rw-r--r--www/wiki/includes/user/CentralIdLookup.php1
-rw-r--r--www/wiki/includes/user/ExternalUserNames.php133
-rw-r--r--www/wiki/includes/user/PasswordReset.php9
-rw-r--r--www/wiki/includes/user/User.php766
-rw-r--r--www/wiki/includes/user/UserArray.php24
-rw-r--r--www/wiki/includes/user/UserArrayFromResult.php6
-rw-r--r--www/wiki/includes/user/UserGroupMembership.php77
-rw-r--r--www/wiki/includes/user/UserIdentity.php (renamed from www/wiki/includes/profiler/ProfileSection.php)44
-rw-r--r--www/wiki/includes/user/UserIdentityValue.php85
-rw-r--r--www/wiki/includes/utils/AutoloadGenerator.php209
-rw-r--r--www/wiki/includes/utils/AvroValidator.php250
-rw-r--r--www/wiki/includes/utils/BatchRowWriter.php3
-rw-r--r--www/wiki/includes/utils/ExecutableFinder.php115
-rw-r--r--www/wiki/includes/utils/FileContentsHasher.php6
-rw-r--r--www/wiki/includes/utils/IP.php791
-rw-r--r--www/wiki/includes/utils/MWCryptHash.php115
-rw-r--r--www/wiki/includes/utils/MWFileProps.php4
-rw-r--r--www/wiki/includes/utils/MWGrants.php214
-rw-r--r--www/wiki/includes/utils/UIDGenerator.php8
-rw-r--r--www/wiki/includes/utils/iterators/IteratorDecorator.php50
-rw-r--r--www/wiki/includes/watcheditem/NoWriteWatchedItemStore.php145
-rw-r--r--www/wiki/includes/watcheditem/WatchedItem.php85
-rw-r--r--www/wiki/includes/watcheditem/WatchedItemQueryService.php (renamed from www/wiki/includes/WatchedItemQueryService.php)91
-rw-r--r--www/wiki/includes/watcheditem/WatchedItemQueryServiceExtension.php (renamed from www/wiki/includes/WatchedItemQueryServiceExtension.php)0
-rw-r--r--www/wiki/includes/watcheditem/WatchedItemStore.php (renamed from www/wiki/includes/WatchedItemStore.php)272
-rw-r--r--www/wiki/includes/watcheditem/WatchedItemStoreInterface.php318
-rw-r--r--www/wiki/includes/widget/ComplexNamespaceInputWidget.php40
-rw-r--r--www/wiki/includes/widget/ComplexTitleInputWidget.php19
-rw-r--r--www/wiki/includes/widget/DateInputWidget.php68
-rw-r--r--www/wiki/includes/widget/DateTimeInputWidget.php19
-rw-r--r--www/wiki/includes/widget/NamespaceInputWidget.php16
-rw-r--r--www/wiki/includes/widget/SearchInputWidget.php26
-rw-r--r--www/wiki/includes/widget/SelectWithInputWidget.php22
-rw-r--r--www/wiki/includes/widget/SizeFilterWidget.php75
-rw-r--r--www/wiki/includes/widget/TitleInputWidget.php28
-rw-r--r--www/wiki/includes/widget/UserInputWidget.php16
-rw-r--r--www/wiki/includes/widget/UsersMultiselectWidget.php22
-rw-r--r--www/wiki/includes/widget/search/BasicSearchResultSetWidget.php7
-rw-r--r--www/wiki/includes/widget/search/FullSearchResultWidget.php9
-rw-r--r--www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php2
-rw-r--r--www/wiki/includes/widget/search/SearchFormWidget.php34
-rw-r--r--www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php23
-rw-r--r--www/wiki/includes/widget/search/SimpleSearchResultWidget.php5
989 files changed, 34892 insertions, 57856 deletions
diff --git a/www/wiki/includes/ActorMigration.php b/www/wiki/includes/ActorMigration.php
new file mode 100644
index 00000000..161c7a92
--- /dev/null
+++ b/www/wiki/includes/ActorMigration.php
@@ -0,0 +1,383 @@
+<?php
+/**
+ * Methods to help with the actor table migration
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use MediaWiki\MediaWikiServices;
+use MediaWiki\User\UserIdentity;
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * This class handles the logic for the actor table migration.
+ *
+ * This is not intended to be a long-term part of MediaWiki; it will be
+ * deprecated and removed along with $wgActorTableSchemaMigrationStage.
+ *
+ * @since 1.31
+ */
+class ActorMigration {
+
+ /**
+ * Define fields that use temporary tables for transitional purposes
+ * @var array Keys are '$key', values are arrays with four fields:
+ * - table: Temporary table name
+ * - pk: Temporary table column referring to the main table's primary key
+ * - field: Temporary table column referring actor.actor_id
+ * - joinPK: Main table's primary key
+ */
+ private static $tempTables = [
+ 'rev_user' => [
+ 'table' => 'revision_actor_temp',
+ 'pk' => 'revactor_rev',
+ 'field' => 'revactor_actor',
+ 'joinPK' => 'rev_id',
+ 'extra' => [
+ 'revactor_timestamp' => 'rev_timestamp',
+ 'revactor_page' => 'rev_page',
+ ],
+ ],
+ ];
+
+ /**
+ * Fields that formerly used $tempTables
+ * @var array Key is '$key', value is the MediaWiki version in which it was
+ * removed from $tempTables.
+ */
+ private static $formerTempTables = [];
+
+ /**
+ * Define fields that use non-standard mapping
+ * @var array Keys are the user id column name, values are arrays with two
+ * elements (the user text column name and the actor id column name)
+ */
+ private static $specialFields = [
+ 'ipb_by' => [ 'ipb_by_text', 'ipb_by_actor' ],
+ ];
+
+ /** @var array|null Cache for `self::getJoin()` */
+ private $joinCache = null;
+
+ /** @var int One of the MIGRATION_* constants */
+ private $stage;
+
+ /** @private */
+ public function __construct( $stage ) {
+ $this->stage = $stage;
+ }
+
+ /**
+ * Static constructor
+ * @return ActorMigration
+ */
+ public static function newMigration() {
+ return MediaWikiServices::getInstance()->getActorMigration();
+ }
+
+ /**
+ * Return an SQL condition to test if a user field is anonymous
+ * @param string $field Field name or SQL fragment
+ * @return string
+ */
+ public function isAnon( $field ) {
+ return $this->stage === MIGRATION_NEW ? "$field IS NULL" : "$field = 0";
+ }
+
+ /**
+ * Return an SQL condition to test if a user field is non-anonymous
+ * @param string $field Field name or SQL fragment
+ * @return string
+ */
+ public function isNotAnon( $field ) {
+ return $this->stage === MIGRATION_NEW ? "$field IS NOT NULL" : "$field != 0";
+ }
+
+ /**
+ * @param string $key A key such as "rev_user" identifying the actor
+ * field being fetched.
+ * @return string[] [ $text, $actor ]
+ */
+ private static function getFieldNames( $key ) {
+ if ( isset( self::$specialFields[$key] ) ) {
+ return self::$specialFields[$key];
+ }
+
+ return [ $key . '_text', substr( $key, 0, -5 ) . '_actor' ];
+ }
+
+ /**
+ * Get SELECT fields and joins for the actor key
+ *
+ * @param string $key A key such as "rev_user" identifying the actor
+ * field being fetched.
+ * @return array With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ * All tables, fields, and joins are aliased, so `+` is safe to use.
+ */
+ public function getJoin( $key ) {
+ if ( !isset( $this->joinCache[$key] ) ) {
+ $tables = [];
+ $fields = [];
+ $joins = [];
+
+ list( $text, $actor ) = self::getFieldNames( $key );
+
+ if ( $this->stage === MIGRATION_OLD ) {
+ $fields[$key] = $key;
+ $fields[$text] = $text;
+ $fields[$actor] = 'NULL';
+ } else {
+ $join = $this->stage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN';
+
+ if ( isset( self::$tempTables[$key] ) ) {
+ $t = self::$tempTables[$key];
+ $alias = "temp_$key";
+ $tables[$alias] = $t['table'];
+ $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
+ $joinField = "{$alias}.{$t['field']}";
+ } else {
+ $joinField = $actor;
+ }
+
+ $alias = "actor_$key";
+ $tables[$alias] = 'actor';
+ $joins[$alias] = [ $join, "{$alias}.actor_id = {$joinField}" ];
+
+ if ( $this->stage === MIGRATION_NEW ) {
+ $fields[$key] = "{$alias}.actor_user";
+ $fields[$text] = "{$alias}.actor_name";
+ } else {
+ $fields[$key] = "COALESCE( {$alias}.actor_user, $key )";
+ $fields[$text] = "COALESCE( {$alias}.actor_name, $text )";
+ }
+ $fields[$actor] = $joinField;
+ }
+
+ $this->joinCache[$key] = [
+ 'tables' => $tables,
+ 'fields' => $fields,
+ 'joins' => $joins,
+ ];
+ }
+
+ return $this->joinCache[$key];
+ }
+
+ /**
+ * Get UPDATE fields for the actor
+ *
+ * @param IDatabase $dbw Database to use for creating an actor ID, if necessary
+ * @param string $key A key such as "rev_user" identifying the actor
+ * field being fetched.
+ * @param UserIdentity $user User to set in the update
+ * @return array to merge into `$values` to `IDatabase->update()` or `$a` to `IDatabase->insert()`
+ */
+ public function getInsertValues( IDatabase $dbw, $key, UserIdentity $user ) {
+ if ( isset( self::$tempTables[$key] ) ) {
+ throw new InvalidArgumentException( "Must use getInsertValuesWithTempTable() for $key" );
+ }
+
+ list( $text, $actor ) = self::getFieldNames( $key );
+ $ret = [];
+ if ( $this->stage <= MIGRATION_WRITE_BOTH ) {
+ $ret[$key] = $user->getId();
+ $ret[$text] = $user->getName();
+ }
+ if ( $this->stage >= MIGRATION_WRITE_BOTH ) {
+ // We need to be able to assign an actor ID if none exists
+ if ( !$user instanceof User && !$user->getActorId() ) {
+ $user = User::newFromAnyId( $user->getId(), $user->getName(), null );
+ }
+ $ret[$actor] = $user->getActorId( $dbw );
+ }
+ return $ret;
+ }
+
+ /**
+ * Get UPDATE fields for the actor
+ *
+ * @param IDatabase $dbw Database to use for creating an actor ID, if necessary
+ * @param string $key A key such as "rev_user" identifying the actor
+ * field being fetched.
+ * @param UserIdentity $user User to set in the update
+ * @return array with two values:
+ * - array to merge into `$values` to `IDatabase->update()` or `$a` to `IDatabase->insert()`
+ * - callback to call with the the primary key for the main table insert
+ * and extra fields needed for the temp table.
+ */
+ public function getInsertValuesWithTempTable( IDatabase $dbw, $key, UserIdentity $user ) {
+ if ( isset( self::$formerTempTables[$key] ) ) {
+ wfDeprecated( __METHOD__ . " for $key", self::$formerTempTables[$key] );
+ } elseif ( !isset( self::$tempTables[$key] ) ) {
+ throw new InvalidArgumentException( "Must use getInsertValues() for $key" );
+ }
+
+ list( $text, $actor ) = self::getFieldNames( $key );
+ $ret = [];
+ $callback = null;
+ if ( $this->stage <= MIGRATION_WRITE_BOTH ) {
+ $ret[$key] = $user->getId();
+ $ret[$text] = $user->getName();
+ }
+ if ( $this->stage >= MIGRATION_WRITE_BOTH ) {
+ // We need to be able to assign an actor ID if none exists
+ if ( !$user instanceof User && !$user->getActorId() ) {
+ $user = User::newFromAnyId( $user->getId(), $user->getName(), null );
+ }
+ $id = $user->getActorId( $dbw );
+
+ if ( isset( self::$tempTables[$key] ) ) {
+ $func = __METHOD__;
+ $callback = function ( $pk, array $extra ) use ( $dbw, $key, $id, $func ) {
+ $t = self::$tempTables[$key];
+ $set = [ $t['field'] => $id ];
+ foreach ( $t['extra'] as $to => $from ) {
+ if ( !array_key_exists( $from, $extra ) ) {
+ throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" );
+ }
+ $set[$to] = $extra[$from];
+ }
+ $dbw->upsert(
+ $t['table'],
+ [ $t['pk'] => $pk ] + $set,
+ [ $t['pk'] ],
+ $set,
+ $func
+ );
+ };
+ } else {
+ $ret[$actor] = $id;
+ $callback = function ( $pk, array $extra ) {
+ };
+ }
+ } elseif ( isset( self::$tempTables[$key] ) ) {
+ $func = __METHOD__;
+ $callback = function ( $pk, array $extra ) use ( $key, $func ) {
+ $t = self::$tempTables[$key];
+ foreach ( $t['extra'] as $to => $from ) {
+ if ( !array_key_exists( $from, $extra ) ) {
+ throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" );
+ }
+ }
+ };
+ } else {
+ $callback = function ( $pk, array $extra ) {
+ };
+ }
+ return [ $ret, $callback ];
+ }
+
+ /**
+ * Get WHERE condition for the actor
+ *
+ * @param IDatabase $db Database to use for quoting and list-making
+ * @param string $key A key such as "rev_user" identifying the actor
+ * field being fetched.
+ * @param UserIdentity|UserIdentity[] $users Users to test for
+ * @param bool $useId If false, don't try to query by the user ID.
+ * Intended for use with rc_user since it has an index on
+ * (rc_user_text,rc_timestamp) but not (rc_user,rc_timestamp).
+ * @return array With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - conds: (string) to include in the `$cond` to `IDatabase->select()`
+ * - orconds: (array[]) array of alternatives in case a union of multiple
+ * queries would be more efficient than a query with OR. May have keys
+ * 'actor', 'userid', 'username'.
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ * All tables and joins are aliased, so `+` is safe to use.
+ */
+ public function getWhere( IDatabase $db, $key, $users, $useId = true ) {
+ $tables = [];
+ $conds = [];
+ $joins = [];
+
+ if ( $users instanceof UserIdentity ) {
+ $users = [ $users ];
+ }
+
+ // Get information about all the passed users
+ $ids = [];
+ $names = [];
+ $actors = [];
+ foreach ( $users as $user ) {
+ if ( $useId && $user->getId() ) {
+ $ids[] = $user->getId();
+ } else {
+ $names[] = $user->getName();
+ }
+ $actorId = $user->getActorId();
+ if ( $actorId ) {
+ $actors[] = $actorId;
+ }
+ }
+
+ list( $text, $actor ) = self::getFieldNames( $key );
+
+ // Combine data into conditions to be ORed together
+ $actorNotEmpty = [];
+ if ( $this->stage === MIGRATION_OLD ) {
+ $actors = [];
+ $actorEmpty = [];
+ } elseif ( isset( self::$tempTables[$key] ) ) {
+ $t = self::$tempTables[$key];
+ $alias = "temp_$key";
+ $tables[$alias] = $t['table'];
+ $joins[$alias] = [
+ $this->stage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
+ "{$alias}.{$t['pk']} = {$t['joinPK']}"
+ ];
+ $joinField = "{$alias}.{$t['field']}";
+ $actorEmpty = [ $joinField => null ];
+ if ( $this->stage !== MIGRATION_NEW ) {
+ // Otherwise the resulting test can evaluate to NULL, and
+ // NOT(NULL) is NULL rather than true.
+ $actorNotEmpty = [ "$joinField IS NOT NULL" ];
+ }
+ } else {
+ $joinField = $actor;
+ $actorEmpty = [ $joinField => 0 ];
+ }
+
+ if ( $actors ) {
+ $conds['actor'] = $db->makeList(
+ $actorNotEmpty + [ $joinField => $actors ], IDatabase::LIST_AND
+ );
+ }
+ if ( $this->stage < MIGRATION_NEW && $ids ) {
+ $conds['userid'] = $db->makeList(
+ $actorEmpty + [ $key => $ids ], IDatabase::LIST_AND
+ );
+ }
+ if ( $this->stage < MIGRATION_NEW && $names ) {
+ $conds['username'] = $db->makeList(
+ $actorEmpty + [ $text => $names ], IDatabase::LIST_AND
+ );
+ }
+
+ return [
+ 'tables' => $tables,
+ 'conds' => $conds ? $db->makeList( array_values( $conds ), IDatabase::LIST_OR ) : '1=0',
+ 'orconds' => $conds,
+ 'joins' => $joins,
+ ];
+ }
+
+}
diff --git a/www/wiki/includes/AutoLoader.php b/www/wiki/includes/AutoLoader.php
index 8dc7d409..6e770565 100644
--- a/www/wiki/includes/AutoLoader.php
+++ b/www/wiki/includes/AutoLoader.php
@@ -31,6 +31,12 @@ class AutoLoader {
static protected $autoloadLocalClassesLower = null;
/**
+ * @private Only public for ExtensionRegistry
+ * @var string[] Namespace (ends with \) => Path (ends with /)
+ */
+ static public $psr4Namespaces = [];
+
+ /**
* autoload - take a class name and attempt to load it
*
* @param string $className Name of class we're looking for.
@@ -67,6 +73,28 @@ class AutoLoader {
}
}
+ if ( !$filename && strpos( $className, '\\' ) !== false ) {
+ // This class is namespaced, so try looking at the namespace map
+ $prefix = $className;
+ while ( false !== $pos = strrpos( $prefix, '\\' ) ) {
+ // Check to see if this namespace prefix is in the map
+ $prefix = substr( $className, 0, $pos + 1 );
+ if ( isset( self::$psr4Namespaces[$prefix] ) ) {
+ $relativeClass = substr( $className, $pos + 1 );
+ // Build the expected filename, and see if it exists
+ $file = self::$psr4Namespaces[$prefix] . '/' .
+ str_replace( '\\', '/', $relativeClass ) . '.php';
+ if ( file_exists( $file ) ) {
+ $filename = $file;
+ break;
+ }
+ }
+
+ // Remove trailing separator for next iteration
+ $prefix = rtrim( $prefix, '\\' );
+ }
+ }
+
if ( !$filename ) {
// Class not found; let the next autoloader try to find it
return;
@@ -88,6 +116,22 @@ class AutoLoader {
static function resetAutoloadLocalClassesLower() {
self::$autoloadLocalClassesLower = null;
}
+
+ /**
+ * Get a mapping of namespace => file path
+ * The namespaces should follow the PSR-4 standard for autoloading
+ *
+ * @see <http://www.php-fig.org/psr/psr-4/>
+ * @private Only public for usage in AutoloadGenerator
+ * @since 1.31
+ * @return string[]
+ */
+ public static function getAutoloadNamespaces() {
+ return [
+ 'MediaWiki\\Linker\\' => __DIR__ .'/linker/'
+ ];
+ }
}
+AutoLoader::$psr4Namespaces = AutoLoader::getAutoloadNamespaces();
spl_autoload_register( [ 'AutoLoader', 'autoload' ] );
diff --git a/www/wiki/includes/Block.php b/www/wiki/includes/Block.php
index 5a4c43e6..c38c929f 100644
--- a/www/wiki/includes/Block.php
+++ b/www/wiki/includes/Block.php
@@ -183,11 +183,14 @@ class Block {
*/
public static function newFromID( $id ) {
$dbr = wfGetDB( DB_REPLICA );
+ $blockQuery = self::getQueryInfo();
$res = $dbr->selectRow(
- 'ipblocks',
- self::selectFields(),
+ $blockQuery['tables'],
+ $blockQuery['fields'],
[ 'ipb_id' => $id ],
- __METHOD__
+ __METHOD__,
+ [],
+ $blockQuery['joins']
);
if ( $res ) {
return self::newFromRow( $res );
@@ -199,16 +202,29 @@ class Block {
/**
* Return the list of ipblocks fields that should be selected to create
* a new block.
- * @todo Deprecate this in favor of a method that returns tables and joins
- * as well, and use CommentStore::getJoin().
+ * @deprecated since 1.31, use self::getQueryInfo() instead.
* @return array
*/
public static function selectFields() {
+ global $wgActorTableSchemaMigrationStage;
+
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+ // If code is using this instead of self::getQueryInfo(), there's a
+ // decent chance it's going to try to directly access
+ // $row->ipb_by or $row->ipb_by_text and we can't give it
+ // useful values here once those aren't being written anymore.
+ throw new BadMethodCallException(
+ 'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+ );
+ }
+
+ wfDeprecated( __METHOD__, '1.31' );
return [
'ipb_id',
'ipb_address',
'ipb_by',
'ipb_by_text',
+ 'ipb_by_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'ipb_by_actor' : 'NULL',
'ipb_timestamp',
'ipb_auto',
'ipb_anon_only',
@@ -219,7 +235,39 @@ class Block {
'ipb_block_email',
'ipb_allow_usertalk',
'ipb_parent_block_id',
- ] + CommentStore::newKey( 'ipb_reason' )->getFields();
+ ] + CommentStore::getStore()->getFields( 'ipb_reason' );
+ }
+
+ /**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new block object.
+ * @since 1.31
+ * @return array With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ */
+ public static function getQueryInfo() {
+ $commentQuery = CommentStore::getStore()->getJoin( 'ipb_reason' );
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' );
+ return [
+ 'tables' => [ 'ipblocks' ] + $commentQuery['tables'] + $actorQuery['tables'],
+ 'fields' => [
+ 'ipb_id',
+ 'ipb_address',
+ 'ipb_timestamp',
+ 'ipb_auto',
+ 'ipb_anon_only',
+ 'ipb_create_account',
+ 'ipb_enable_autoblock',
+ 'ipb_expiry',
+ 'ipb_deleted',
+ 'ipb_block_email',
+ 'ipb_allow_usertalk',
+ 'ipb_parent_block_id',
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
+ 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
+ ];
}
/**
@@ -295,7 +343,10 @@ class Block {
}
}
- $res = $db->select( 'ipblocks', self::selectFields(), $conds, __METHOD__ );
+ $blockQuery = self::getQueryInfo();
+ $res = $db->select(
+ $blockQuery['tables'], $blockQuery['fields'], $conds, __METHOD__, [], $blockQuery['joins']
+ );
# This result could contain a block on the user, a block on the IP, and a russian-doll
# set of rangeblocks. We want to choose the most specific one, so keep a leader board.
@@ -406,11 +457,9 @@ class Block {
*/
protected function initFromRow( $row ) {
$this->setTarget( $row->ipb_address );
- if ( $row->ipb_by ) { // local user
- $this->setBlocker( User::newFromId( $row->ipb_by ) );
- } else { // foreign user
- $this->setBlocker( $row->ipb_by_text );
- }
+ $this->setBlocker( User::newFromAnyId(
+ $row->ipb_by, $row->ipb_by_text, isset( $row->ipb_by_actor ) ? $row->ipb_by_actor : null
+ ) );
$this->mTimestamp = wfTimestamp( TS_MW, $row->ipb_timestamp );
$this->mAuto = $row->ipb_auto;
@@ -421,9 +470,9 @@ class Block {
// I wish I didn't have to do this
$db = wfGetDB( DB_REPLICA );
$this->mExpiry = $db->decodeExpiry( $row->ipb_expiry );
- $this->mReason = CommentStore::newKey( 'ipb_reason' )
- // Legacy because $row probably came from self::selectFields()
- ->getCommentLegacy( $db, $row )->text;
+ $this->mReason = CommentStore::getStore()
+ // Legacy because $row may have come from self::selectFields()
+ ->getCommentLegacy( $db, 'ipb_reason', $row )->text;
$this->isHardblock( !$row->ipb_anon_only );
$this->isAutoblocking( $row->ipb_enable_autoblock );
@@ -480,6 +529,9 @@ class Block {
if ( $this->getSystemBlockType() !== null ) {
throw new MWException( 'Cannot insert a system block into the database' );
}
+ if ( !$this->getBlocker() || $this->getBlocker()->getName() === '' ) {
+ throw new MWException( 'Cannot insert a block without a blocker set' );
+ }
wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" );
@@ -487,10 +539,7 @@ class Block {
$dbw = wfGetDB( DB_MASTER );
}
- # Periodic purge via commit hooks
- if ( mt_rand( 0, 9 ) == 0 ) {
- self::purgeExpired();
- }
+ self::purgeExpired();
$row = $this->getDatabaseArray( $dbw );
@@ -601,8 +650,6 @@ class Block {
$a = [
'ipb_address' => (string)$this->target,
'ipb_user' => $uid,
- 'ipb_by' => $this->getBy(),
- 'ipb_by_text' => $this->getByName(),
'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ),
'ipb_auto' => $this->mAuto,
'ipb_anon_only' => !$this->isHardblock(),
@@ -615,7 +662,8 @@ class Block {
'ipb_block_email' => $this->prevents( 'sendemail' ),
'ipb_allow_usertalk' => !$this->prevents( 'editownusertalk' ),
'ipb_parent_block_id' => $this->mParentBlockId
- ] + CommentStore::newKey( 'ipb_reason' )->insert( $dbw, $this->mReason );
+ ] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->mReason )
+ + ActorMigration::newMigration()->getInsertValues( $dbw, 'ipb_by', $this->getBlocker() );
return $a;
}
@@ -626,12 +674,11 @@ class Block {
*/
protected function getAutoblockUpdateArray( IDatabase $dbw ) {
return [
- 'ipb_by' => $this->getBy(),
- 'ipb_by_text' => $this->getByName(),
'ipb_create_account' => $this->prevents( 'createaccount' ),
'ipb_deleted' => (int)$this->mHideName, // typecast required for SQLite
'ipb_allow_usertalk' => !$this->prevents( 'editownusertalk' ),
- ] + CommentStore::newKey( 'ipb_reason' )->insert( $dbw, $this->mReason );
+ ] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->mReason )
+ + ActorMigration::newMigration()->getInsertValues( $dbw, 'ipb_by', $this->getBlocker() );
}
/**
@@ -671,16 +718,27 @@ class Block {
return;
}
+ $target = $block->getTarget();
+ if ( is_string( $target ) ) {
+ $target = User::newFromName( $target, false );
+ }
+
$dbr = wfGetDB( DB_REPLICA );
+ $rcQuery = ActorMigration::newMigration()->getWhere( $dbr, 'rc_user', $target, false );
$options = [ 'ORDER BY' => 'rc_timestamp DESC' ];
- $conds = [ 'rc_user_text' => (string)$block->getTarget() ];
// Just the last IP used.
$options['LIMIT'] = 1;
- $res = $dbr->select( 'recentchanges', [ 'rc_ip' ], $conds,
- __METHOD__, $options );
+ $res = $dbr->select(
+ [ 'recentchanges' ] + $rcQuery['tables'],
+ [ 'rc_ip' ],
+ $rcQuery['conds'],
+ __METHOD__,
+ $options,
+ $rcQuery['joins']
+ );
if ( !$res->numRows() ) {
# No results, don't autoblock anything
@@ -709,7 +767,7 @@ class Block {
// than getting the msg raw and explode()'ing it.
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
$lines = $cache->getWithSetCallback(
- $cache->makeKey( 'ipb', 'autoblock', 'whitelist' ),
+ $cache->makeKey( 'ip-autoblock', 'whitelist' ),
$cache::TTL_DAY,
function ( $curValue, &$ttl, array &$setOpts ) {
$setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
@@ -1076,15 +1134,18 @@ class Block {
return;
}
- DeferredUpdates::addUpdate( new AtomicSectionUpdate(
+ DeferredUpdates::addUpdate( new AutoCommitUpdate(
wfGetDB( DB_MASTER ),
__METHOD__,
function ( IDatabase $dbw, $fname ) {
- $dbw->delete(
- 'ipblocks',
+ $ids = $dbw->selectFieldValues( 'ipblocks',
+ 'ipb_id',
[ 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
$fname
);
+ if ( $ids ) {
+ $dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], $fname );
+ }
}
) );
}
@@ -1187,14 +1248,14 @@ class Block {
if ( !$isAnon ) {
$conds = [ $conds, 'ipb_anon_only' => 0 ];
}
- $selectFields = array_merge(
- [ 'ipb_range_start', 'ipb_range_end' ],
- self::selectFields()
- );
- $rows = $db->select( 'ipblocks',
- $selectFields,
+ $blockQuery = self::getQueryInfo();
+ $rows = $db->select(
+ $blockQuery['tables'],
+ array_merge( [ 'ipb_range_start', 'ipb_range_end' ], $blockQuery['fields'] ),
$conds,
- __METHOD__
+ __METHOD__,
+ [],
+ $blockQuery['joins']
);
$blocks = [];
@@ -1432,7 +1493,7 @@ class Block {
/**
* Get the user who implemented this block
- * @return User|string Local User object or string for a foreign user
+ * @return User User object. May name a foreign user.
*/
public function getBlocker() {
return $this->blocker;
@@ -1440,9 +1501,19 @@ class Block {
/**
* Set the user who implemented (or will implement) this block
- * @param User|string $user Local User object or username string for foreign users
+ * @param User|string $user Local User object or username string
*/
public function setBlocker( $user ) {
+ if ( is_string( $user ) ) {
+ $user = User::newFromName( $user, false );
+ }
+
+ if ( $user->isAnon() && User::isUsableName( $user->getName() ) ) {
+ throw new InvalidArgumentException(
+ 'Blocker must be a local user or a name that cannot be a local user'
+ );
+ }
+
$this->blocker = $user;
}
diff --git a/www/wiki/includes/CategoriesRdf.php b/www/wiki/includes/CategoriesRdf.php
index e19dc2aa..fc296d4c 100644
--- a/www/wiki/includes/CategoriesRdf.php
+++ b/www/wiki/includes/CategoriesRdf.php
@@ -37,7 +37,13 @@ class CategoriesRdf {
/**
* Current version of the dump format.
*/
- const FORMAT_VERSION = "1.0";
+ const FORMAT_VERSION = "1.1";
+ /**
+ * Special page for Dump identification.
+ * Used as head URI for each wiki's category dump, e.g.:
+ * https://en.wikipedia.org/wiki/Special:CategoryDump
+ */
+ const SPECIAL_DUMP = 'Special:CategoryDump';
/**
* @var RdfWriter
*/
@@ -74,22 +80,49 @@ class CategoriesRdf {
/**
* Write out the data for single category.
* @param string $categoryName Category name
+ * @param bool $isHidden Hidden category?
+ * @param int $pages Page count (note this includes only Wiki articles, not subcats or files)
+ * @param int $subcategories Subcategory count
*/
- public function writeCategoryData( $categoryName ) {
+ public function writeCategoryData( $categoryName, $isHidden, $pages, $subcategories ) {
$title = Title::makeTitle( NS_CATEGORY, $categoryName );
$this->rdfWriter->about( $this->titleToUrl( $title ) )
->say( 'a' )
->is( self::ONTOLOGY_PREFIX, 'Category' );
+ if ( $isHidden ) {
+ $this->rdfWriter->is( self::ONTOLOGY_PREFIX, 'HiddenCategory' );
+ }
$titletext = $title->getText();
$this->rdfWriter->say( 'rdfs', 'label' )->value( $titletext );
+ $this->rdfWriter->say( self::ONTOLOGY_PREFIX, 'pages' )->value( $pages );
+ $this->rdfWriter->say( self::ONTOLOGY_PREFIX, 'subcategories' )->value( $subcategories );
+ // TODO: do we want files too here? Easy to add, but don't have use case so far.
+ }
+
+ /**
+ * Make URL from title label
+ * @param string $titleLabel Short label (without namespace) of the category
+ * @return string URL for the category
+ */
+ public function labelToUrl( $titleLabel ) {
+ return $this->titleToUrl( Title::makeTitle( NS_CATEGORY, $titleLabel ) );
}
/**
* Convert Title to link to target page.
* @param Title $title
- * @return string
+ * @return string URL for the category
*/
private function titleToUrl( Title $title ) {
return $title->getFullURL( '', false, PROTO_CANONICAL );
}
+
+ /**
+ * Get URI of the dump for this particular wiki.
+ * @return false|string
+ */
+ public function getDumpURI() {
+ return $this->titleToUrl( Title::makeTitle( NS_MAIN, self::SPECIAL_DUMP ) );
+ }
+
}
diff --git a/www/wiki/includes/Category.php b/www/wiki/includes/Category.php
index 629962d2..6104b8a6 100644
--- a/www/wiki/includes/Category.php
+++ b/www/wiki/includes/Category.php
@@ -119,9 +119,9 @@ class Category {
/**
* Factory function.
*
- * @param array $name A category name (no "Category:" prefix). It need
+ * @param string $name A category name (no "Category:" prefix). It need
* not be normalized, with spaces replaced by underscores.
- * @return mixed Category, or false on a totally invalid name
+ * @return Category|bool Category, or false on a totally invalid name
*/
public static function newFromName( $name ) {
$cat = new self();
@@ -328,7 +328,7 @@ class Category {
$dbw = wfGetDB( DB_MASTER );
# Avoid excess contention on the same category (T162121)
$name = __METHOD__ . ':' . md5( $this->mName );
- $scopedLock = $dbw->getScopedLockAndFlush( $name, __METHOD__, 1 );
+ $scopedLock = $dbw->getScopedLockAndFlush( $name, __METHOD__, 0 );
if ( !$scopedLock ) {
return false;
}
diff --git a/www/wiki/includes/CategoryFinder.php b/www/wiki/includes/CategoryFinder.php
index 89bf5c73..7446b590 100644
--- a/www/wiki/includes/CategoryFinder.php
+++ b/www/wiki/includes/CategoryFinder.php
@@ -42,6 +42,8 @@ use Wikimedia\Rdbms\IDatabase;
* $a = $cf->run();
* print implode( ',' , $a );
* @endcode
+ *
+ * @deprecated since 1.31
*/
class CategoryFinder {
/** @var int[] The original article IDs passed to the seed function */
@@ -56,6 +58,9 @@ class CategoryFinder {
/** @var array Array of article/category IDs */
protected $next = [];
+ /** @var int Max layer depth **/
+ protected $maxdepth = -1;
+
/** @var array Array of DBKEY category names */
protected $targets = [];
@@ -73,12 +78,17 @@ class CategoryFinder {
* @param array $articleIds Array of article IDs
* @param array $categories FIXME
* @param string $mode FIXME, default 'AND'.
+ * @param int $maxdepth Maximum layer depth. Where:
+ * -1 means deep recursion (default);
+ * 0 means no-parents;
+ * 1 means one parent layer, etc.
* @todo FIXME: $categories/$mode
*/
- public function seed( $articleIds, $categories, $mode = 'AND' ) {
+ public function seed( $articleIds, $categories, $mode = 'AND', $maxdepth = -1 ) {
$this->articles = $articleIds;
$this->next = $articleIds;
$this->mode = $mode;
+ $this->maxdepth = $maxdepth;
# Set the list of target categories; convert them to DBKEY form first
$this->targets = [];
@@ -98,8 +108,17 @@ class CategoryFinder {
*/
public function run() {
$this->dbr = wfGetDB( DB_REPLICA );
- while ( count( $this->next ) > 0 ) {
+
+ $i = 0;
+ $dig = true;
+ while ( count( $this->next ) && $dig ) {
$this->scanNextLayer();
+
+ // Is there any depth limit?
+ if ( $this->maxdepth !== -1 ) {
+ $dig = $i < $this->maxdepth;
+ $i++;
+ }
}
# Now check if this applies to the individual articles
@@ -190,7 +209,7 @@ class CategoryFinder {
$layer = [];
$res = $this->dbr->select(
/* FROM */ 'categorylinks',
- /* SELECT */ '*',
+ /* SELECT */ [ 'cl_to', 'cl_from' ],
/* WHERE */ [ 'cl_from' => $this->next ],
__METHOD__ . '-1'
);
diff --git a/www/wiki/includes/CategoryViewer.php b/www/wiki/includes/CategoryViewer.php
index 9d692d71..f36c7580 100644
--- a/www/wiki/includes/CategoryViewer.php
+++ b/www/wiki/includes/CategoryViewer.php
@@ -629,7 +629,7 @@ class CategoryViewer extends ContextSource {
* @return string HTML
*/
private function pagingLinks( $first, $last, $type = '' ) {
- $prevLink = $this->msg( 'prev-page' )->text();
+ $prevLink = $this->msg( 'prev-page' )->escaped();
$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
if ( $first != '' ) {
@@ -638,13 +638,13 @@ class CategoryViewer extends ContextSource {
unset( $prevQuery["{$type}from"] );
$prevLink = $linkRenderer->makeKnownLink(
$this->addFragmentToTitle( $this->title, $type ),
- $prevLink,
+ new HtmlArmor( $prevLink ),
[],
$prevQuery
);
}
- $nextLink = $this->msg( 'next-page' )->text();
+ $nextLink = $this->msg( 'next-page' )->escaped();
if ( $last != '' ) {
$lastQuery = $this->query;
@@ -652,7 +652,7 @@ class CategoryViewer extends ContextSource {
unset( $lastQuery["{$type}until"] );
$nextLink = $linkRenderer->makeKnownLink(
$this->addFragmentToTitle( $this->title, $type ),
- $nextLink,
+ new HtmlArmor( $nextLink ),
[],
$lastQuery
);
diff --git a/www/wiki/includes/CommentStore.php b/www/wiki/includes/CommentStore.php
index 0d679d37..55f68572 100644
--- a/www/wiki/includes/CommentStore.php
+++ b/www/wiki/includes/CommentStore.php
@@ -20,6 +20,7 @@
* @file
*/
+use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -33,7 +34,7 @@ class CommentStore {
* Maximum length of a comment in UTF-8 characters. Longer comments will be truncated.
* @note This must be at least 255 and not greater than floor( MAX_COMMENT_LENGTH / 4 ).
*/
- const COMMENT_CHARACTER_LIMIT = 1000;
+ const COMMENT_CHARACTER_LIMIT = 500;
/**
* Maximum length of a comment in bytes. Longer comments will be truncated.
@@ -79,40 +80,71 @@ class CommentStore {
*/
protected static $formerTempTables = [];
- /** @var string */
- protected $key;
+ /**
+ * @since 1.30
+ * @deprecated in 1.31
+ * @var string|null
+ */
+ protected $key = null;
/** @var int One of the MIGRATION_* constants */
protected $stage;
- /** @var array|null Cache for `self::getJoin()` */
- protected $joinCache = null;
+ /** @var array[] Cache for `self::getJoin()` */
+ protected $joinCache = [];
/** @var Language Language to use for comment truncation */
protected $lang;
/**
- * @param string $key A key such as "rev_comment" identifying the comment
- * field being fetched.
* @param Language $lang Language to use for comment truncation. Defaults
* to $wgContLang.
+ * @param int $migrationStage One of the MIGRATION_* constants
*/
- public function __construct( $key, Language $lang = null ) {
- global $wgCommentTableSchemaMigrationStage, $wgContLang;
-
- $this->key = $key;
- $this->stage = $wgCommentTableSchemaMigrationStage;
- $this->lang = $lang ?: $wgContLang;
+ public function __construct( Language $lang, $migrationStage ) {
+ $this->stage = $migrationStage;
+ $this->lang = $lang;
}
/**
* Static constructor for easier chaining
+ * @deprecated in 1.31 Should not be constructed with a $key, use CommentStore::getStore
* @param string $key A key such as "rev_comment" identifying the comment
* field being fetched.
* @return CommentStore
*/
public static function newKey( $key ) {
- return new CommentStore( $key );
+ global $wgCommentTableSchemaMigrationStage, $wgContLang;
+ // TODO uncomment once not used in extensions
+ // wfDeprecated( __METHOD__, '1.31' );
+ $store = new CommentStore( $wgContLang, $wgCommentTableSchemaMigrationStage );
+ $store->key = $key;
+ return $store;
+ }
+
+ /**
+ * @since 1.31
+ * @deprecated in 1.31 Use DI to inject a CommentStore instance into your class.
+ * @return CommentStore
+ */
+ public static function getStore() {
+ return MediaWikiServices::getInstance()->getCommentStore();
+ }
+
+ /**
+ * Compat method allowing use of self::newKey until removed.
+ * @param string|null $methodKey
+ * @throw InvalidArgumentException
+ * @return string
+ */
+ private function getKey( $methodKey = null ) {
+ $key = $this->key !== null ? $this->key : $methodKey;
+ if ( $key === null ) {
+ // @codeCoverageIgnoreStart
+ throw new InvalidArgumentException( '$key should not be null' );
+ // @codeCoverageIgnoreEnd
+ }
+ return $key;
}
/**
@@ -123,23 +155,29 @@ class CommentStore {
*
* @note Use of this method may require a subsequent database query to
* actually fetch the comment. If possible, use `self::getJoin()` instead.
+ *
+ * @since 1.30
+ * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat)
+ * @param string $key A key such as "rev_comment" identifying the comment
+ * field being fetched.
* @return string[] to include in the `$vars` to `IDatabase->select()`. All
* fields are aliased, so `+` is safe to use.
*/
- public function getFields() {
+ public function getFields( $key = null ) {
+ $key = $this->getKey( $key );
$fields = [];
if ( $this->stage === MIGRATION_OLD ) {
- $fields["{$this->key}_text"] = $this->key;
- $fields["{$this->key}_data"] = 'NULL';
- $fields["{$this->key}_cid"] = 'NULL';
+ $fields["{$key}_text"] = $key;
+ $fields["{$key}_data"] = 'NULL';
+ $fields["{$key}_cid"] = 'NULL';
} else {
if ( $this->stage < MIGRATION_NEW ) {
- $fields["{$this->key}_old"] = $this->key;
+ $fields["{$key}_old"] = $key;
}
- if ( isset( self::$tempTables[$this->key] ) ) {
- $fields["{$this->key}_pk"] = self::$tempTables[$this->key]['joinPK'];
+ if ( isset( self::$tempTables[$key] ) ) {
+ $fields["{$key}_pk"] = self::$tempTables[$key]['joinPK'];
} else {
- $fields["{$this->key}_id"] = "{$this->key}_id";
+ $fields["{$key}_id"] = "{$key}_id";
}
}
return $fields;
@@ -151,56 +189,61 @@ class CommentStore {
* Each resulting row should be passed to `self::getComment()` to get the
* actual comment.
*
+ * @since 1.30
+ * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat)
+ * @param string $key A key such as "rev_comment" identifying the comment
+ * field being fetched.
* @return array With three keys:
* - tables: (string[]) to include in the `$table` to `IDatabase->select()`
* - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
* All tables, fields, and joins are aliased, so `+` is safe to use.
*/
- public function getJoin() {
- if ( $this->joinCache === null ) {
+ public function getJoin( $key = null ) {
+ $key = $this->getKey( $key );
+ if ( !array_key_exists( $key, $this->joinCache ) ) {
$tables = [];
$fields = [];
$joins = [];
if ( $this->stage === MIGRATION_OLD ) {
- $fields["{$this->key}_text"] = $this->key;
- $fields["{$this->key}_data"] = 'NULL';
- $fields["{$this->key}_cid"] = 'NULL';
+ $fields["{$key}_text"] = $key;
+ $fields["{$key}_data"] = 'NULL';
+ $fields["{$key}_cid"] = 'NULL';
} else {
$join = $this->stage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN';
- if ( isset( self::$tempTables[$this->key] ) ) {
- $t = self::$tempTables[$this->key];
- $alias = "temp_$this->key";
+ if ( isset( self::$tempTables[$key] ) ) {
+ $t = self::$tempTables[$key];
+ $alias = "temp_$key";
$tables[$alias] = $t['table'];
$joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
$joinField = "{$alias}.{$t['field']}";
} else {
- $joinField = "{$this->key}_id";
+ $joinField = "{$key}_id";
}
- $alias = "comment_$this->key";
+ $alias = "comment_$key";
$tables[$alias] = 'comment';
$joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ];
if ( $this->stage === MIGRATION_NEW ) {
- $fields["{$this->key}_text"] = "{$alias}.comment_text";
+ $fields["{$key}_text"] = "{$alias}.comment_text";
} else {
- $fields["{$this->key}_text"] = "COALESCE( {$alias}.comment_text, $this->key )";
+ $fields["{$key}_text"] = "COALESCE( {$alias}.comment_text, $key )";
}
- $fields["{$this->key}_data"] = "{$alias}.comment_data";
- $fields["{$this->key}_cid"] = "{$alias}.comment_id";
+ $fields["{$key}_data"] = "{$alias}.comment_data";
+ $fields["{$key}_cid"] = "{$alias}.comment_id";
}
- $this->joinCache = [
+ $this->joinCache[$key] = [
'tables' => $tables,
'fields' => $fields,
'joins' => $joins,
];
}
- return $this->joinCache;
+ return $this->joinCache[$key];
}
/**
@@ -209,12 +252,13 @@ class CommentStore {
* Shared implementation for getComment() and getCommentLegacy()
*
* @param IDatabase|null $db Database handle for getCommentLegacy(), or null for getComment()
+ * @param string $key A key such as "rev_comment" identifying the comment
+ * field being fetched.
* @param object|array $row
* @param bool $fallback
* @return CommentStoreComment
*/
- private function getCommentInternal( IDatabase $db = null, $row, $fallback = false ) {
- $key = $this->key;
+ private function getCommentInternal( IDatabase $db = null, $key, $row, $fallback = false ) {
$row = (array)$row;
if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
$cid = isset( $row["{$key}_cid"] ) ? $row["{$key}_cid"] : null;
@@ -333,12 +377,27 @@ class CommentStore {
* If you need to fake a comment in a row for some reason, set fields
* `{$key}_text` (string) and `{$key}_data` (JSON string or null).
*
+ * @since 1.30
+ * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat)
+ * @param string $key A key such as "rev_comment" identifying the comment
+ * field being fetched.
* @param object|array $row Result row.
* @param bool $fallback If true, fall back as well as possible instead of throwing an exception.
* @return CommentStoreComment
*/
- public function getComment( $row, $fallback = false ) {
- return $this->getCommentInternal( null, $row, $fallback );
+ public function getComment( $key, $row = null, $fallback = false ) {
+ // Compat for method sig change in 1.31 (introduction of $key)
+ if ( $this->key !== null ) {
+ $fallback = $row;
+ $row = $key;
+ $key = $this->getKey();
+ }
+ if ( $row === null ) {
+ // @codeCoverageIgnoreStart
+ throw new InvalidArgumentException( '$row must not be null' );
+ // @codeCoverageIgnoreEnd
+ }
+ return $this->getCommentInternal( null, $key, $row, $fallback );
}
/**
@@ -351,13 +410,28 @@ class CommentStore {
* If you need to fake a comment in a row for some reason, set fields
* `{$key}_text` (string) and `{$key}_data` (JSON string or null).
*
+ * @since 1.30
+ * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat)
* @param IDatabase $db Database handle to use for lookup
+ * @param string $key A key such as "rev_comment" identifying the comment
+ * field being fetched.
* @param object|array $row Result row.
* @param bool $fallback If true, fall back as well as possible instead of throwing an exception.
* @return CommentStoreComment
*/
- public function getCommentLegacy( IDatabase $db, $row, $fallback = false ) {
- return $this->getCommentInternal( $db, $row, $fallback );
+ public function getCommentLegacy( IDatabase $db, $key, $row = null, $fallback = false ) {
+ // Compat for method sig change in 1.31 (introduction of $key)
+ if ( $this->key !== null ) {
+ $fallback = $row;
+ $row = $key;
+ $key = $this->getKey();
+ }
+ if ( $row === null ) {
+ // @codeCoverageIgnoreStart
+ throw new InvalidArgumentException( '$row must not be null' );
+ // @codeCoverageIgnoreEnd
+ }
+ return $this->getCommentInternal( $db, $key, $row, $fallback );
}
/**
@@ -443,23 +517,25 @@ class CommentStore {
/**
* Implementation for `self::insert()` and `self::insertWithTempTable()`
* @param IDatabase $dbw
+ * @param string $key A key such as "rev_comment" identifying the comment
+ * field being fetched.
* @param string|Message|CommentStoreComment $comment
* @param array|null $data
* @return array [ array $fields, callable $callback ]
*/
- private function insertInternal( IDatabase $dbw, $comment, $data ) {
+ private function insertInternal( IDatabase $dbw, $key, $comment, $data ) {
$fields = [];
$callback = null;
$comment = $this->createComment( $dbw, $comment, $data );
if ( $this->stage <= MIGRATION_WRITE_BOTH ) {
- $fields[$this->key] = $this->lang->truncate( $comment->text, 255 );
+ $fields[$key] = $this->lang->truncate( $comment->text, 255 );
}
if ( $this->stage >= MIGRATION_WRITE_BOTH ) {
- if ( isset( self::$tempTables[$this->key] ) ) {
- $t = self::$tempTables[$this->key];
+ if ( isset( self::$tempTables[$key] ) ) {
+ $t = self::$tempTables[$key];
$func = __METHOD__;
$commentId = $comment->id;
$callback = function ( $id ) use ( $dbw, $commentId, $t, $func ) {
@@ -473,7 +549,7 @@ class CommentStore {
);
};
} else {
- $fields["{$this->key}_id"] = $comment->id;
+ $fields["{$key}_id"] = $comment->id;
}
}
@@ -485,17 +561,34 @@ class CommentStore {
*
* @note It's recommended to include both the call to this method and the
* row insert in the same transaction.
+ *
+ * @since 1.30
+ * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat)
* @param IDatabase $dbw Database handle to insert on
+ * @param string $key A key such as "rev_comment" identifying the comment
+ * field being fetched.
* @param string|Message|CommentStoreComment $comment As for `self::createComment()`
* @param array|null $data As for `self::createComment()`
* @return array Fields for the insert or update
*/
- public function insert( IDatabase $dbw, $comment, $data = null ) {
- if ( isset( self::$tempTables[$this->key] ) ) {
- throw new InvalidArgumentException( "Must use insertWithTempTable() for $this->key" );
+ public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
+ // Compat for method sig change in 1.31 (introduction of $key)
+ if ( $this->key !== null ) {
+ $data = $comment;
+ $comment = $key;
+ $key = $this->key;
+ }
+ if ( $comment === null ) {
+ // @codeCoverageIgnoreStart
+ throw new InvalidArgumentException( '$comment can not be null' );
+ // @codeCoverageIgnoreEnd
+ }
+
+ if ( isset( self::$tempTables[$key] ) ) {
+ throw new InvalidArgumentException( "Must use insertWithTempTable() for $key" );
}
- list( $fields ) = $this->insertInternal( $dbw, $comment, $data );
+ list( $fields ) = $this->insertInternal( $dbw, $key, $comment, $data );
return $fields;
}
@@ -507,7 +600,12 @@ class CommentStore {
*
* @note It's recommended to include both the call to this method and the
* row insert in the same transaction.
+ *
+ * @since 1.30
+ * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat)
* @param IDatabase $dbw Database handle to insert on
+ * @param string $key A key such as "rev_comment" identifying the comment
+ * field being fetched.
* @param string|Message|CommentStoreComment $comment As for `self::createComment()`
* @param array|null $data As for `self::createComment()`
* @return array Two values:
@@ -515,14 +613,26 @@ class CommentStore {
* - callable Function to call when the primary key of the row being
* inserted/updated is known. Pass it that primary key.
*/
- public function insertWithTempTable( IDatabase $dbw, $comment, $data = null ) {
- if ( isset( self::$formerTempTables[$this->key] ) ) {
- wfDeprecated( __METHOD__ . " for $this->key", self::$formerTempTables[$this->key] );
- } elseif ( !isset( self::$tempTables[$this->key] ) ) {
- throw new InvalidArgumentException( "Must use insert() for $this->key" );
+ public function insertWithTempTable( IDatabase $dbw, $key, $comment = null, $data = null ) {
+ // Compat for method sig change in 1.31 (introduction of $key)
+ if ( $this->key !== null ) {
+ $data = $comment;
+ $comment = $key;
+ $key = $this->getKey();
+ }
+ if ( $comment === null ) {
+ // @codeCoverageIgnoreStart
+ throw new InvalidArgumentException( '$comment can not be null' );
+ // @codeCoverageIgnoreEnd
+ }
+
+ if ( isset( self::$formerTempTables[$key] ) ) {
+ wfDeprecated( __METHOD__ . " for $key", self::$formerTempTables[$key] );
+ } elseif ( !isset( self::$tempTables[$key] ) ) {
+ throw new InvalidArgumentException( "Must use insert() for $key" );
}
- list( $fields, $callback ) = $this->insertInternal( $dbw, $comment, $data );
+ list( $fields, $callback ) = $this->insertInternal( $dbw, $key, $comment, $data );
if ( !$callback ) {
$callback = function () {
// Do nothing.
diff --git a/www/wiki/includes/CommentStoreComment.php b/www/wiki/includes/CommentStoreComment.php
index 3920ba08..7ed86d66 100644
--- a/www/wiki/includes/CommentStoreComment.php
+++ b/www/wiki/includes/CommentStoreComment.php
@@ -20,8 +20,6 @@
* @file
*/
-use Wikimedia\Rdbms\IDatabase;
-
/**
* CommentStoreComment represents a comment stored by CommentStore. The fields
* should be considered read-only.
diff --git a/www/wiki/includes/DefaultSettings.php b/www/wiki/includes/DefaultSettings.php
index 4f29dcf6..0fb01731 100644
--- a/www/wiki/includes/DefaultSettings.php
+++ b/www/wiki/includes/DefaultSettings.php
@@ -39,10 +39,6 @@
*/
/**
- * @defgroup Globalsettings Global settings
- */
-
-/**
* @cond file_level_code
* This is not a valid entry point, perform no further processing unless
* MEDIAWIKI is defined
@@ -75,7 +71,7 @@ $wgConfigRegistry = [
* MediaWiki version number
* @since 1.2
*/
-$wgVersion = '1.30.1';
+$wgVersion = '1.31.3';
/**
* Name of the site. It must be changed in LocalSettings.php
@@ -161,19 +157,6 @@ $wgUsePathInfo = ( strpos( PHP_SAPI, 'cgi' ) === false ) &&
( strpos( PHP_SAPI, 'apache2filter' ) === false ) &&
( strpos( PHP_SAPI, 'isapi' ) === false );
-/**
- * The extension to append to script names by default.
- *
- * Some hosting providers used PHP 4 for *.php files, and PHP 5 for *.php5.
- * This variable was provided to support those providers.
- *
- * @since 1.11
- * @deprecated since 1.25; support for '.php5' has been phased out of MediaWiki
- * proper. Backward-compatibility can be maintained by configuring your web
- * server to rewrite URLs. See RELEASE-NOTES for details.
- */
-$wgScriptExtension = '.php';
-
/**@}*/
/************************************************************************//**
@@ -290,6 +273,17 @@ $wgLogo = false;
* ];
* @endcode
*
+ * SVG is also supported but when enabled, it
+ * disables 1.5x and 2x as svg will already
+ * be optimised for screen resolution.
+ *
+ * @par Example:
+ * @code
+ * $wgLogoHD = [
+ * "svg" => "path/to/svg_version.svg",
+ * ];
+ * @endcode
+ *
* @since 1.25
*/
$wgLogoHD = false;
@@ -309,10 +303,20 @@ $wgAppleTouchIcon = false;
/**
* Value for the referrer policy meta tag.
- * One of 'never', 'default', 'origin', 'always'. Setting it to false just
- * prevents the meta tag from being output.
- * See https://www.w3.org/TR/referrer-policy/ for details.
- *
+ * One or more of the values defined in the Referrer Policy specification:
+ * https://w3c.github.io/webappsec-referrer-policy/
+ * ('no-referrer', 'no-referrer-when-downgrade', 'same-origin',
+ * 'origin', 'strict-origin', 'origin-when-cross-origin',
+ * 'strict-origin-when-cross-origin', or 'unsafe-url')
+ * Setting it to false prevents the meta tag from being output
+ * (which results in falling back to the Referrer-Policy header,
+ * or 'no-referrer-when-downgrade' if that's not set either.)
+ * Setting it to an array (supported since 1.31) will create a meta tag for
+ * each value, in the reverse of the order (meaning that the first array element
+ * will be the default and the others used as fallbacks for browsers which do not
+ * understand it).
+ *
+ * @var array|string|bool
* @since 1.25
*/
$wgReferrerPolicy = false;
@@ -486,9 +490,6 @@ $wgImgAuthUrlPathMap = [];
* - 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.
* 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. https://en.wikipedia.org/wiki/$1
* - fetchDescription Fetch the text of the remote file description page. Equivalent to
* $wgFetchCommonsDescriptions.
@@ -987,15 +988,15 @@ $wgParserTestMediaHandlers = [
*/
$wgContentHandlers = [
// the usual case
- CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler',
+ CONTENT_MODEL_WIKITEXT => WikitextContentHandler::class,
// dumb version, no syntax highlighting
- CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler',
+ CONTENT_MODEL_JAVASCRIPT => JavaScriptContentHandler::class,
// simple implementation, for use by extensions, etc.
- CONTENT_MODEL_JSON => 'JsonContentHandler',
+ CONTENT_MODEL_JSON => JsonContentHandler::class,
// dumb version, no syntax highlighting
- CONTENT_MODEL_CSS => 'CssContentHandler',
+ CONTENT_MODEL_CSS => CssContentHandler::class,
// plain text, for use by extensions, etc.
- CONTENT_MODEL_TEXT => 'TextContentHandler',
+ CONTENT_MODEL_TEXT => TextContentHandler::class,
];
/**
@@ -1799,8 +1800,8 @@ $wgDBtype = 'mysql';
/**
* Whether to use SSL in DB connection.
*
- * This setting is only used $wgLBFactoryConf['class'] is set to
- * 'LBFactorySimple' and $wgDBservers is an empty array; otherwise
+ * This setting is only used if $wgLBFactoryConf['class'] is set to
+ * '\Wikimedia\Rdbms\LBFactorySimple' and $wgDBservers is an empty array; otherwise
* the DBO_SSL flag must be set in the 'flags' option of the database
* connection to achieve the same functionality.
*/
@@ -1810,7 +1811,7 @@ $wgDBssl = false;
* Whether to use compression in DB connection.
*
* This setting is only used $wgLBFactoryConf['class'] is set to
- * 'LBFactorySimple' and $wgDBservers is an empty array; otherwise
+ * '\Wikimedia\Rdbms\LBFactorySimple' and $wgDBservers is an empty array; otherwise
* the DBO_COMPRESS flag must be set in the 'flags' option of the database
* connection to achieve the same functionality.
*/
@@ -1923,6 +1924,7 @@ $wgSharedSchema = false;
* - user: DB user
* - password: DB password
* - type: DB type
+ * - driver: DB driver (when there are multiple drivers)
*
* - load: Ratio of DB_REPLICA load, must be >=0, the sum of all loads must be >0.
* If this is zero for any given server, no normal query traffic will be
@@ -1980,7 +1982,7 @@ $wgDBservers = false;
* The LBFactoryMulti class is provided for this purpose, please see
* includes/db/LBFactoryMulti.php for configuration information.
*/
-$wgLBFactoryConf = [ 'class' => 'LBFactorySimple' ];
+$wgLBFactoryConf = [ 'class' => \Wikimedia\Rdbms\LBFactorySimple::class ];
/**
* After a state-changing request is done by a client, this determines
@@ -2030,6 +2032,8 @@ $wgDBerrorLogTZ = false;
* Even correct usage may cause failures with Unicode supplementary
* characters (those not in the Basic Multilingual Plane) unless MySQL
* has enhanced their Unicode support.
+ *
+ * @deprecated since 1.31
*/
$wgDBmysql5 = false;
@@ -2124,7 +2128,7 @@ $wgExternalStores = [];
* ];
* @endcode
*
- * Used by LBFactorySimple, may be ignored if $wgLBFactoryConf is set to
+ * Used by \Wikimedia\Rdbms\LBFactorySimple, may be ignored if $wgLBFactoryConf is set to
* another class.
*/
$wgExternalServers = [];
@@ -2237,7 +2241,7 @@ $wgCacheDirectory = false;
* - CACHE_NONE: Do not cache
* - CACHE_DB: Store cache objects in the DB
* - CACHE_MEMCACHED: MemCached, must specify servers in $wgMemCachedServers
- * - CACHE_ACCEL: APC, APCU, XCache or WinCache
+ * - CACHE_ACCEL: APC, APCU or WinCache
* - (other): A string may be used which identifies a cache
* configuration in $wgObjectCaches.
*
@@ -2292,34 +2296,33 @@ $wgLanguageConverterCacheType = CACHE_ANYTHING;
* given, giving a callable function which will generate a suitable cache object.
*/
$wgObjectCaches = [
- CACHE_NONE => [ 'class' => 'EmptyBagOStuff', 'reportDupes' => false ],
- CACHE_DB => [ 'class' => 'SqlBagOStuff', 'loggroup' => 'SQLBagOStuff' ],
+ CACHE_NONE => [ 'class' => EmptyBagOStuff::class, 'reportDupes' => false ],
+ CACHE_DB => [ 'class' => SqlBagOStuff::class, 'loggroup' => 'SQLBagOStuff' ],
CACHE_ANYTHING => [ 'factory' => 'ObjectCache::newAnything' ],
CACHE_ACCEL => [ 'factory' => 'ObjectCache::getLocalServerInstance' ],
- CACHE_MEMCACHED => [ 'class' => 'MemcachedPhpBagOStuff', 'loggroup' => 'memcached' ],
+ CACHE_MEMCACHED => [ 'class' => MemcachedPhpBagOStuff::class, 'loggroup' => 'memcached' ],
'db-replicated' => [
- 'class' => 'ReplicatedBagOStuff',
+ 'class' => ReplicatedBagOStuff::class,
'readFactory' => [
- 'class' => 'SqlBagOStuff',
+ 'class' => SqlBagOStuff::class,
'args' => [ [ 'slaveOnly' => true ] ]
],
'writeFactory' => [
- 'class' => 'SqlBagOStuff',
+ 'class' => SqlBagOStuff::class,
'args' => [ [ 'slaveOnly' => false ] ]
],
'loggroup' => 'SQLBagOStuff',
'reportDupes' => false
],
- 'apc' => [ 'class' => 'APCBagOStuff', 'reportDupes' => false ],
- 'apcu' => [ 'class' => 'APCUBagOStuff', 'reportDupes' => false ],
- 'xcache' => [ 'class' => 'XCacheBagOStuff', 'reportDupes' => false ],
- 'wincache' => [ 'class' => 'WinCacheBagOStuff', 'reportDupes' => false ],
- 'memcached-php' => [ 'class' => 'MemcachedPhpBagOStuff', 'loggroup' => 'memcached' ],
- 'memcached-pecl' => [ 'class' => 'MemcachedPeclBagOStuff', 'loggroup' => 'memcached' ],
- 'hash' => [ 'class' => 'HashBagOStuff', 'reportDupes' => false ],
+ 'apc' => [ 'class' => APCBagOStuff::class, 'reportDupes' => false ],
+ 'apcu' => [ 'class' => APCUBagOStuff::class, 'reportDupes' => false ],
+ 'wincache' => [ 'class' => WinCacheBagOStuff::class, 'reportDupes' => false ],
+ 'memcached-php' => [ 'class' => MemcachedPhpBagOStuff::class, 'loggroup' => 'memcached' ],
+ 'memcached-pecl' => [ 'class' => MemcachedPeclBagOStuff::class, 'loggroup' => 'memcached' ],
+ 'hash' => [ 'class' => HashBagOStuff::class, 'reportDupes' => false ],
];
/**
@@ -2356,13 +2359,13 @@ $wgMainWANCache = false;
*/
$wgWANObjectCaches = [
CACHE_NONE => [
- 'class' => 'WANObjectCache',
+ 'class' => WANObjectCache::class,
'cacheId' => CACHE_NONE,
'channels' => []
]
/* Example of a simple single data-center cache:
'memcached-php' => [
- 'class' => 'WANObjectCache',
+ 'class' => WANObjectCache::class,
'cacheId' => 'memcached-php',
'channels' => [ 'purge' => 'wancache-main-memcached-purge' ]
]
@@ -2509,7 +2512,7 @@ $wgAdaptiveMessageCache = false;
* Use maintenance/rebuildLocalisationCache.php instead.
*/
$wgLocalisationCacheConf = [
- 'class' => 'LocalisationCache',
+ 'class' => LocalisationCache::class,
'store' => 'detect',
'storeClass' => false,
'storeDirectory' => false,
@@ -2543,6 +2546,8 @@ $wgGitInfoCacheDirectory = false;
* It should be appended in the query string of static CSS and JS includes,
* to ensure that client-side caches do not keep obsolete copies of global
* styles.
+ *
+ * @deprecated since 1.31
*/
$wgStyleVersion = '303';
@@ -3247,12 +3252,6 @@ $wgSiteNotice = '';
$wgSiteSupportPage = '';
/**
- * Validate the overall output using tidy and refuse
- * to display the page if it's not valid.
- */
-$wgValidateAllHtml = false;
-
-/**
* Default skin, for new users and anonymous visitors. Registered users may
* change this to any one of the other available skins in their preferences.
*/
@@ -3292,9 +3291,10 @@ $wgAllowUserJs = false;
$wgAllowUserCss = false;
/**
- * Allow user-preferences implemented in CSS?
- * This allows users to customise the site appearance to a greater
- * degree; disabling it will improve page load times.
+ * Allow style-related user-preferences?
+ *
+ * This controls whether the `editfont` and `underline` preferences
+ * are availabe to users.
*/
$wgAllowUserCssPrefs = true;
@@ -3391,7 +3391,7 @@ $wgExperimentalHtmlIds = false;
*
* @since 1.30
*/
-$wgFragmentMode = [ 'legacy' ];
+$wgFragmentMode = [ 'legacy', 'html5' ];
/**
* Which ID escaping mode should be used for external interwiki links? See documentation
@@ -3673,23 +3673,6 @@ $wgResourceLoaderMaxage = [
$wgResourceLoaderDebug = false;
/**
- * Put each statement on its own line when minifying JavaScript. This makes
- * debugging in non-debug mode a bit easier.
- *
- * @deprecated since 1.27: Always false; no longer configurable.
- */
-$wgResourceLoaderMinifierStatementsOnOwnLine = false;
-
-/**
- * Maximum line length when minifying JavaScript. This is not a hard maximum:
- * the minifier will try not to produce lines longer than this, but may be
- * forced to do so in certain cases.
- *
- * @deprecated since 1.27: Always 1,000; no longer configurable.
- */
-$wgResourceLoaderMinifierMaxLineLength = 1000;
-
-/**
* Whether to ensure the mediawiki.legacy library is loaded before other modules.
*
* @deprecated since 1.26: Always declare dependencies.
@@ -3697,16 +3680,6 @@ $wgResourceLoaderMinifierMaxLineLength = 1000;
$wgIncludeLegacyJavaScript = false;
/**
- * Use jQuery 3 (with jQuery Migrate) instead of jQuery 1.
- *
- * This is a temporary feature flag for the MediaWiki 1.29 development cycle while
- * instabilities with jQuery 3 are being addressed. See T124742.
- *
- * @deprecated since 1.29
- */
-$wgUsejQueryThree = true;
-
-/**
* Whether or not to assign configuration variables to the global window object.
*
* If this is set to false, old code using deprecated variables will no longer
@@ -3781,7 +3754,7 @@ $wgResourceLoaderValidateStaticJS = false;
* @code
* $wgResourceLoaderLESSVars = [
* 'exampleFontSize' => '1em',
- * 'exampleBlue' => '#eee',
+ * 'exampleBlue' => '#36c',
* ];
* @endcode
* @since 1.22
@@ -3790,10 +3763,11 @@ $wgResourceLoaderValidateStaticJS = false;
*/
$wgResourceLoaderLESSVars = [
/**
- * Minimum available screen width at which a device can be considered a tablet/desktop
+ * Minimum available screen width at which a device can be considered a tablet
* The number is currently based on the device width of a Samsung Galaxy S5 mini and is low
* enough to cover iPad (768px). Number is prone to change with new information.
* @since 1.27
+ * @deprecated 1.31 Use mediawiki.ui/variables instead
*/
'deviceWidthTablet' => '720px',
];
@@ -3943,9 +3917,6 @@ $wgNamespaceAliases = [];
* because articles can be created such that they are hard to view or edit.
*
* In some rare cases you may wish to remove + for compatibility with old links.
- *
- * Theoretically 0x80-0x9F of ISO 8859-1 should be disallowed, but
- * this breaks interlanguage links
*/
$wgLegalTitleChars = " %!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+";
@@ -4176,8 +4147,8 @@ $wgInvalidRedirectTargets = [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect' ];
* an extension setup function.
*/
$wgParserConf = [
- 'class' => 'Parser',
- # 'preprocessorClass' => 'Preprocessor_Hash',
+ 'class' => Parser::class,
+ # 'preprocessorClass' => Preprocessor_Hash::class,
];
/**
@@ -4279,8 +4250,9 @@ $wgAllowImageTag = false;
/**
* 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 https://www.w3.org/People/Raggett/tidy/
+ * array to enable an external tool. By default, we now use the RemexHtml
+ * library; historically, Dave Raggett's "HTML Tidy" was typically used.
+ * See https://www.w3.org/People/Raggett/tidy/
*
* If this is null and $wgUseTidy is true, the deprecated configuration
* parameters will be used instead.
@@ -4301,7 +4273,7 @@ $wgAllowImageTag = false;
* - tidyBin: For RaggettExternal, the path to the tidy binary.
* - tidyCommandLine: For RaggettExternal, additional command line options.
*/
-$wgTidyConfig = null;
+$wgTidyConfig = [ 'driver' => 'RemexHtml' ];
/**
* Set this to true to use the deprecated tidy configuration parameters.
@@ -4448,7 +4420,6 @@ $wgEnableMagicLinks = [
*
* This variable can have the following values:
* - 'any': all pages as considered as valid articles
- * - 'comma': the page must contain a comma to be considered valid
* - 'link': the page must contain a [[wiki link]] to be considered valid
*
* See also See https://www.mediawiki.org/wiki/Manual:Article_count
@@ -4481,7 +4452,7 @@ $wgActiveUserDays = 30;
* @since 1.27
*/
$wgCentralIdLookupProviders = [
- 'local' => [ 'class' => 'LocalIdLookup' ],
+ 'local' => [ 'class' => LocalIdLookup::class ],
];
/**
@@ -4769,7 +4740,7 @@ $wgPasswordDefault = 'pbkdf2';
* An advanced example:
* @code
* $wgPasswordConfig['bcrypt-peppered'] = [
- * 'class' => 'EncryptedPassword',
+ * 'class' => EncryptedPassword::class,
* 'underlying' => 'bcrypt',
* 'secrets' => [],
* 'cipher' => MCRYPT_RIJNDAEL_256,
@@ -4782,31 +4753,31 @@ $wgPasswordDefault = 'pbkdf2';
*/
$wgPasswordConfig = [
'A' => [
- 'class' => 'MWOldPassword',
+ 'class' => MWOldPassword::class,
],
'B' => [
- 'class' => 'MWSaltedPassword',
+ 'class' => MWSaltedPassword::class,
],
'pbkdf2-legacyA' => [
- 'class' => 'LayeredParameterizedPassword',
+ 'class' => LayeredParameterizedPassword::class,
'types' => [
'A',
'pbkdf2',
],
],
'pbkdf2-legacyB' => [
- 'class' => 'LayeredParameterizedPassword',
+ 'class' => LayeredParameterizedPassword::class,
'types' => [
'B',
'pbkdf2',
],
],
'bcrypt' => [
- 'class' => 'BcryptPassword',
+ 'class' => BcryptPassword::class,
'cost' => 9,
],
'pbkdf2' => [
- 'class' => 'Pbkdf2Password',
+ 'class' => Pbkdf2Password::class,
'algo' => 'sha512',
'cost' => '30000',
'length' => '64',
@@ -4849,6 +4820,7 @@ $wgReservedUsernames = [
'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:sorbs', // For $wgEnableDnsBlacklist etc.
'msg:spambot_username', // Used by cleanupSpam.php
'msg:autochange-username', // Used by anon category RC entries (parser functions, Lua & purges)
];
@@ -4868,6 +4840,7 @@ $wgDefaultUserOptions = [
'editfont' => 'monospace',
'editondblclick' => 0,
'editsectiononrightclick' => 0,
+ 'email-allow-new-users' => 1,
'enotifminoredits' => 0,
'enotifrevealaddr' => 0,
'enotifusertalkpages' => 1,
@@ -4880,7 +4853,6 @@ $wgDefaultUserOptions = [
'hidepatrolled' => 0,
'hidecategorization' => 1,
'imagesize' => 2,
- 'math' => 1,
'minordefault' => 0,
'newpageshidepatrolled' => 0,
'nickname' => '',
@@ -5136,8 +5108,6 @@ $wgGroupPermissions['*']['edit'] = true;
$wgGroupPermissions['*']['createpage'] = true;
$wgGroupPermissions['*']['createtalk'] = true;
$wgGroupPermissions['*']['writeapi'] = true;
-$wgGroupPermissions['*']['editmyusercss'] = true;
-$wgGroupPermissions['*']['editmyuserjs'] = true;
$wgGroupPermissions['*']['viewmywatchlist'] = true;
$wgGroupPermissions['*']['editmywatchlist'] = true;
$wgGroupPermissions['*']['viewmyprivateinfo'] = true;
@@ -5160,6 +5130,9 @@ $wgGroupPermissions['user']['upload'] = true;
$wgGroupPermissions['user']['reupload'] = true;
$wgGroupPermissions['user']['reupload-shared'] = true;
$wgGroupPermissions['user']['minoredit'] = true;
+$wgGroupPermissions['user']['editmyusercss'] = true;
+$wgGroupPermissions['user']['editmyuserjson'] = true;
+$wgGroupPermissions['user']['editmyuserjs'] = true;
$wgGroupPermissions['user']['purge'] = true;
$wgGroupPermissions['user']['sendemail'] = true;
$wgGroupPermissions['user']['applychangetags'] = true;
@@ -5194,6 +5167,7 @@ $wgGroupPermissions['sysop']['deletedtext'] = true;
$wgGroupPermissions['sysop']['undelete'] = true;
$wgGroupPermissions['sysop']['editinterface'] = true;
$wgGroupPermissions['sysop']['editusercss'] = true;
+$wgGroupPermissions['sysop']['edituserjson'] = true;
$wgGroupPermissions['sysop']['edituserjs'] = true;
$wgGroupPermissions['sysop']['import'] = true;
$wgGroupPermissions['sysop']['importupload'] = true;
@@ -5707,6 +5681,10 @@ $wgRateLimits = [
'newbie' => [ 5, 86400 ],
'user' => [ 20, 86400 ],
],
+ 'changeemail' => [
+ 'ip-all' => [ 10, 3600 ],
+ 'user' => [ 4, 86400 ]
+ ],
// Purging pages
'purge' => [
'ip' => [ 30, 60 ],
@@ -5785,7 +5763,7 @@ $wgPasswordAttemptThrottle = [
];
/**
- * @var Array Map of (grant => right => boolean)
+ * @var array Map of (grant => right => boolean)
* Users authorize consumers (like Apps) to act on their behalf but only with
* a subset of the user's normal account rights (signed off on by the user).
* The possible rights to grant to a consumer are bundled into groups called
@@ -5823,8 +5801,10 @@ $wgGrantPermissions['editpage']['changetags'] = true;
$wgGrantPermissions['editprotected'] = $wgGrantPermissions['editpage'];
$wgGrantPermissions['editprotected']['editprotected'] = true;
+// FIXME: Rename editmycssjs to editmyconfig
$wgGrantPermissions['editmycssjs'] = $wgGrantPermissions['editpage'];
$wgGrantPermissions['editmycssjs']['editmyusercss'] = true;
+$wgGrantPermissions['editmycssjs']['editmyuserjson'] = true;
$wgGrantPermissions['editmycssjs']['editmyuserjs'] = true;
$wgGrantPermissions['editmyoptions']['editmyoptions'] = true;
@@ -5832,6 +5812,7 @@ $wgGrantPermissions['editmyoptions']['editmyoptions'] = true;
$wgGrantPermissions['editinterface'] = $wgGrantPermissions['editpage'];
$wgGrantPermissions['editinterface']['editinterface'] = true;
$wgGrantPermissions['editinterface']['editusercss'] = true;
+$wgGrantPermissions['editinterface']['edituserjson'] = true;
$wgGrantPermissions['editinterface']['edituserjs'] = true;
$wgGrantPermissions['createeditmovepage'] = $wgGrantPermissions['editpage'];
@@ -5887,7 +5868,7 @@ $wgGrantPermissions['createaccount']['createaccount'] = true;
$wgGrantPermissions['privateinfo']['viewmyprivateinfo'] = true;
/**
- * @var Array Map of grants to their UI grouping
+ * @var array Map of grants to their UI grouping
* @since 1.27
*/
$wgGrantPermissionGroups = [
@@ -6116,7 +6097,7 @@ $wgDebugComments = false;
* Write SQL queries to the debug log.
*
* This setting is only used $wgLBFactoryConf['class'] is set to
- * 'LBFactorySimple' and $wgDBservers is an empty array; otherwise
+ * '\Wikimedia\Rdbms\LBFactorySimple' and $wgDBservers is an empty array; otherwise
* the DBO_DEBUG flag must be set in the 'flags' option of the database
* connection to achieve the same functionality.
*/
@@ -6147,8 +6128,8 @@ $wgTrxProfilerLimits = [
'writes' => 0,
'readQueryTime' => 5
],
- // Deferred updates that run after HTTP response is sent
- 'PostSend' => [
+ // Deferred updates that run after HTTP response is sent for GET requests
+ 'PostSend-GET' => [
'readQueryTime' => 5,
'writeQueryTime' => 1,
'maxAffected' => 1000,
@@ -6156,6 +6137,12 @@ $wgTrxProfilerLimits = [
'masterConns' => 0,
'writes' => 0,
],
+ // Deferred updates that run after HTTP response is sent for POST requests
+ 'PostSend-POST' => [
+ 'readQueryTime' => 5,
+ 'writeQueryTime' => 1,
+ 'maxAffected' => 1000
+ ],
// Background job runner
'JobRunner' => [
'readQueryTime' => 30,
@@ -6215,7 +6202,7 @@ $wgDebugLogGroups = [];
*
* @par To completely disable logging:
* @code
- * $wgMWLoggerDefaultSpi = [ 'class' => '\\MediaWiki\\Logger\\NullSpi' ];
+ * $wgMWLoggerDefaultSpi = [ 'class' => \MediaWiki\Logger\NullSpi::class ];
* @endcode
*
* @since 1.25
@@ -6223,7 +6210,7 @@ $wgDebugLogGroups = [];
* @see MwLogger
*/
$wgMWLoggerDefaultSpi = [
- 'class' => '\\MediaWiki\\Logger\\LegacySpi',
+ 'class' => \MediaWiki\Logger\LegacySpi::class,
];
/**
@@ -6280,6 +6267,12 @@ $wgShowDBErrorBacktrace = false;
$wgLogExceptionBacktrace = true;
/**
+ * If true, the MediaWiki error handler passes errors/warnings to the default error handler
+ * after logging them. The setting is ignored when the track_errors php.ini flag is true.
+ */
+$wgPropagateErrors = true;
+
+/**
* Expose backend server host names through the API and various HTML comments
*/
$wgShowHostnames = false;
@@ -6345,7 +6338,9 @@ $wgStatsdMetricPrefix = 'MediaWiki';
* Rates are sampling probabilities (e.g. 0.1 means 1 in 10 events are sampled).
* @since 1.28
*/
-$wgStatsdSamplingRates = [];
+$wgStatsdSamplingRates = [
+ 'wanobjectcache:*' => 0.001
+];
/**
* InfoAction retrieves a list of transclusion links (both to and from).
@@ -6623,6 +6618,13 @@ $wgCommandLineDarkBg = false;
$wgReadOnly = null;
/**
+ * Set this to true to put the wiki watchlists into read-only mode.
+ * @var bool
+ * @since 1.31
+ */
+$wgReadOnlyWatchedItemStore = false;
+
+/**
* If this lock file exists (size > 0), the wiki will be forced into read-only mode.
* Its contents will be shown to users as part of the read-only warning
* message.
@@ -6662,9 +6664,9 @@ $wgGitBin = '/usr/bin/git';
*/
$wgGitRepositoryViewers = [
'https://(?:[a-z0-9_]+@)?gerrit.wikimedia.org/r/(?:p/)?(.*)' =>
- 'https://phabricator.wikimedia.org/r/revision/%R;%H',
+ 'https://gerrit.wikimedia.org/g/%R/+/%H',
'ssh://(?:[a-z0-9_]+@)?gerrit.wikimedia.org:29418/(.*)' =>
- 'https://phabricator.wikimedia.org/r/revision/%R;%H',
+ 'https://gerrit.wikimedia.org/g/%R/+/%H',
];
/** @} */ # End of maintenance }
@@ -6766,7 +6768,7 @@ $wgRCLinkDays = [ 1, 3, 7, 14, 30 ];
* 'omit_bots' => true,
* ];
* @example $wgRCFeeds['example'] = [
- * 'class' => 'ExampleRCFeed',
+ * 'class' => ExampleRCFeed::class,
* ];
* @since 1.22
*/
@@ -6778,8 +6780,8 @@ $wgRCFeeds = [];
* @since 1.22
*/
$wgRCEngines = [
- 'redis' => 'RedisPubSubFeedEngine',
- 'udp' => 'UDPRCFeedEngine',
+ 'redis' => RedisPubSubFeedEngine::class,
+ 'udp' => UDPRCFeedEngine::class,
];
/**
@@ -6798,6 +6800,10 @@ $wgRCWatchCategoryMembership = false;
/**
* Use RC Patrolling to check for vandalism (from recent changes and watchlists)
* New pages and new files are included.
+ *
+ * @note If you disable all patrolling features, you probably also want to
+ * remove 'patrol' from $wgFilterLogTypes so a show/hide link isn't shown on
+ * Special:Log.
*/
$wgUseRCPatrol = true;
@@ -6814,38 +6820,40 @@ $wgUseRCPatrol = true;
$wgStructuredChangeFiltersShowPreference = false;
/**
- * Whether to show the new experimental views (like namespaces, tags, and users) in
- * RecentChanges filters
+ * Whether to enable RCFilters app on Special:Watchlist
*
* Temporary variable during development and will be removed.
*/
-$wgStructuredChangeFiltersEnableExperimentalViews = false;
+$wgStructuredChangeFiltersOnWatchlist = false;
/**
- * Whether to enable RCFilters app on Special:Watchlist
- *
- * Temporary variable during development and will be removed.
+ * Polling rate, in seconds, used by the 'live update' and 'view newest' features
+ * of the RCFilters app on SpecialRecentChanges and Special:Watchlist.
+ * 0 to disable completely.
*/
-$wgStructuredChangeFiltersOnWatchlist = false;
+$wgStructuredChangeFiltersLiveUpdatePollingRate = 3;
/**
* Use new page patrolling to check new pages on Special:Newpages
+ *
+ * @note If you disable all patrolling features, you probably also want to
+ * remove 'patrol' from $wgFilterLogTypes so a show/hide link isn't shown on
+ * Special:Log.
*/
$wgUseNPPatrol = true;
/**
* Use file patrolling to check new files on Special:Newfiles
*
+ * @note If you disable all patrolling features, you probably also want to
+ * remove 'patrol' from $wgFilterLogTypes so a show/hide link isn't shown on
+ * Special:Log.
+ *
* @since 1.27
*/
$wgUseFilePatrol = true;
/**
- * Log autopatrol actions to the log table
- */
-$wgLogAutopatrol = true;
-
-/**
* Provide syndication feeds (RSS, Atom) for, e.g., Recentchanges, Newpages
*/
$wgFeed = true;
@@ -6892,8 +6900,8 @@ $wgOverrideSiteFeed = [];
* $wgOut->isSyndicated() is true.
*/
$wgFeedClasses = [
- 'rss' => 'RSSFeed',
- 'atom' => 'AtomFeed',
+ 'rss' => RSSFeed::class,
+ 'atom' => AtomFeed::class,
];
/**
@@ -6932,17 +6940,37 @@ $wgShowUpdatedMarker = true;
$wgDisableAnonTalk = false;
/**
- * Enable filtering of categories in Recentchanges
- */
-$wgAllowCategorizedRecentChanges = false;
-
-/**
* Allow filtering by change tag in recentchanges, history, etc
* Has no effect if no tags are defined in valid_tag.
*/
$wgUseTagFilter = true;
/**
+ * List of core tags to enable. Available tags are:
+ * - 'mw-contentmodelchange': Edit changes content model of a page
+ * - 'mw-new-redirect': Edit makes new redirect page (new page or by changing content page)
+ * - 'mw-removed-redirect': Edit changes an existing redirect into a non-redirect
+ * - 'mw-changed-redirect-target': Edit changes redirect target
+ * - 'mw-blank': Edit completely blanks the page
+ * - 'mw-replace': Edit removes more than 90% of the content
+ * - 'mw-rollback': Edit is a rollback, made through the rollback link or rollback API
+ * - 'mw-undo': Edit made through an undo link
+ *
+ * @var array
+ * @since 1.31
+ */
+$wgSoftwareTags = [
+ 'mw-contentmodelchange' => true,
+ 'mw-new-redirect' => true,
+ 'mw-removed-redirect' => true,
+ 'mw-changed-redirect-target' => true,
+ 'mw-blank' => true,
+ 'mw-replace' => true,
+ 'mw-rollback' => true,
+ 'mw-undo' => true,
+];
+
+/**
* If set to an integer, pages that are watched by this many users or more
* will not require the unwatchedpages permission to view the number of
* watchers.
@@ -7292,7 +7320,7 @@ $wgAutoloadAttemptLowercase = true;
* 'version' => '1.9.0',
* 'url' => 'https://example.org/example-extension/',
* 'descriptionmsg' => 'exampleextension-desc',
- * 'license-name' => 'GPL-2.0+',
+ * 'license-name' => 'GPL-2.0-or-later',
* ];
* @endcode
*
@@ -7326,7 +7354,7 @@ $wgAutoloadAttemptLowercase = true;
* localizable message (omit in favour of 'descriptionmsg').
*
* - license-name: Short name of the license (used as label for the link), such
- * as "GPL-2.0+" or "MIT" (https://spdx.org/licenses/ for a list of identifiers).
+ * as "GPL-2.0-or-later" or "MIT" (https://spdx.org/licenses/ for a list of identifiers).
*/
$wgExtensionCredits = [];
@@ -7395,23 +7423,26 @@ $wgServiceWiringFiles = [
* or (since 1.30) a callback to use for creating the job object.
*/
$wgJobClasses = [
- 'refreshLinks' => 'RefreshLinksJob',
- 'deleteLinks' => 'DeleteLinksJob',
- 'htmlCacheUpdate' => 'HTMLCacheUpdateJob',
- 'sendMail' => 'EmaillingJob',
- 'enotifNotify' => 'EnotifNotifyJob',
- 'fixDoubleRedirect' => 'DoubleRedirectJob',
- 'AssembleUploadChunks' => 'AssembleUploadChunksJob',
- 'PublishStashedFile' => 'PublishStashedFileJob',
- 'ThumbnailRender' => 'ThumbnailRenderJob',
- 'recentChangesUpdate' => 'RecentChangesUpdateJob',
- 'refreshLinksPrioritized' => 'RefreshLinksJob',
- 'refreshLinksDynamic' => 'RefreshLinksJob',
- 'activityUpdateJob' => 'ActivityUpdateJob',
- 'categoryMembershipChange' => 'CategoryMembershipChangeJob',
- 'cdnPurge' => 'CdnPurgeJob',
- 'enqueue' => 'EnqueueJob', // local queue for multi-DC setups
- 'null' => 'NullJob'
+ 'refreshLinks' => RefreshLinksJob::class,
+ 'deleteLinks' => DeleteLinksJob::class,
+ 'htmlCacheUpdate' => HTMLCacheUpdateJob::class,
+ 'sendMail' => EmaillingJob::class,
+ 'enotifNotify' => EnotifNotifyJob::class,
+ 'fixDoubleRedirect' => DoubleRedirectJob::class,
+ 'AssembleUploadChunks' => AssembleUploadChunksJob::class,
+ 'PublishStashedFile' => PublishStashedFileJob::class,
+ 'ThumbnailRender' => ThumbnailRenderJob::class,
+ 'recentChangesUpdate' => RecentChangesUpdateJob::class,
+ 'refreshLinksPrioritized' => RefreshLinksJob::class,
+ 'refreshLinksDynamic' => RefreshLinksJob::class,
+ 'activityUpdateJob' => ActivityUpdateJob::class,
+ 'categoryMembershipChange' => CategoryMembershipChangeJob::class,
+ 'clearUserWatchlist' => ClearUserWatchlistJob::class,
+ 'cdnPurge' => CdnPurgeJob::class,
+ 'userGroupExpiry' => UserGroupExpiryJob::class,
+ 'clearWatchlistNotifications' => ClearWatchlistNotificationsJob::class,
+ 'enqueue' => EnqueueJob::class, // local queue for multi-DC setups
+ 'null' => NullJob::class,
];
/**
@@ -7460,7 +7491,7 @@ $wgJobSerialCommitThreshold = false;
* These settings should be global to all wikis.
*/
$wgJobTypeConf = [
- 'default' => [ 'class' => 'JobQueueDB', 'order' => 'random', 'claimTTL' => 3600 ],
+ 'default' => [ 'class' => JobQueueDB::class, 'order' => 'random', 'claimTTL' => 3600 ],
];
/**
@@ -7468,7 +7499,7 @@ $wgJobTypeConf = [
* These settings should be global to all wikis.
*/
$wgJobQueueAggregator = [
- 'class' => 'JobQueueAggregatorNull'
+ 'class' => JobQueueAggregatorNull::class
];
/**
@@ -7489,7 +7520,7 @@ $wgJobQueueIncludeInMaxLagFactor = false;
* Expensive Querypages are already updated.
*/
$wgSpecialPageCacheUpdates = [
- 'Statistics' => [ 'SiteStatsUpdate', 'cacheUpdate' ]
+ 'Statistics' => [ SiteStatsUpdate::class, 'cacheUpdate' ]
];
/**
@@ -7679,42 +7710,42 @@ $wgLogActions = [];
* @see LogFormatter
*/
$wgLogActionsHandlers = [
- 'block/block' => 'BlockLogFormatter',
- 'block/reblock' => 'BlockLogFormatter',
- 'block/unblock' => 'BlockLogFormatter',
- 'contentmodel/change' => 'ContentModelLogFormatter',
- 'contentmodel/new' => 'ContentModelLogFormatter',
- 'delete/delete' => 'DeleteLogFormatter',
- 'delete/delete_redir' => 'DeleteLogFormatter',
- 'delete/event' => 'DeleteLogFormatter',
- 'delete/restore' => 'DeleteLogFormatter',
- 'delete/revision' => 'DeleteLogFormatter',
- 'import/interwiki' => 'ImportLogFormatter',
- 'import/upload' => 'ImportLogFormatter',
- 'managetags/activate' => 'LogFormatter',
- 'managetags/create' => 'LogFormatter',
- 'managetags/deactivate' => 'LogFormatter',
- 'managetags/delete' => 'LogFormatter',
- 'merge/merge' => 'MergeLogFormatter',
- 'move/move' => 'MoveLogFormatter',
- 'move/move_redir' => 'MoveLogFormatter',
- 'patrol/patrol' => 'PatrolLogFormatter',
- 'patrol/autopatrol' => 'PatrolLogFormatter',
- 'protect/modify' => 'ProtectLogFormatter',
- 'protect/move_prot' => 'ProtectLogFormatter',
- 'protect/protect' => 'ProtectLogFormatter',
- 'protect/unprotect' => 'ProtectLogFormatter',
- 'rights/autopromote' => 'RightsLogFormatter',
- 'rights/rights' => 'RightsLogFormatter',
- 'suppress/block' => 'BlockLogFormatter',
- 'suppress/delete' => 'DeleteLogFormatter',
- 'suppress/event' => 'DeleteLogFormatter',
- 'suppress/reblock' => 'BlockLogFormatter',
- 'suppress/revision' => 'DeleteLogFormatter',
- 'tag/update' => 'TagLogFormatter',
- 'upload/overwrite' => 'UploadLogFormatter',
- 'upload/revert' => 'UploadLogFormatter',
- 'upload/upload' => 'UploadLogFormatter',
+ 'block/block' => BlockLogFormatter::class,
+ 'block/reblock' => BlockLogFormatter::class,
+ 'block/unblock' => BlockLogFormatter::class,
+ 'contentmodel/change' => ContentModelLogFormatter::class,
+ 'contentmodel/new' => ContentModelLogFormatter::class,
+ 'delete/delete' => DeleteLogFormatter::class,
+ 'delete/delete_redir' => DeleteLogFormatter::class,
+ 'delete/event' => DeleteLogFormatter::class,
+ 'delete/restore' => DeleteLogFormatter::class,
+ 'delete/revision' => DeleteLogFormatter::class,
+ 'import/interwiki' => ImportLogFormatter::class,
+ 'import/upload' => ImportLogFormatter::class,
+ 'managetags/activate' => LogFormatter::class,
+ 'managetags/create' => LogFormatter::class,
+ 'managetags/deactivate' => LogFormatter::class,
+ 'managetags/delete' => LogFormatter::class,
+ 'merge/merge' => MergeLogFormatter::class,
+ 'move/move' => MoveLogFormatter::class,
+ 'move/move_redir' => MoveLogFormatter::class,
+ 'patrol/patrol' => PatrolLogFormatter::class,
+ 'patrol/autopatrol' => PatrolLogFormatter::class,
+ 'protect/modify' => ProtectLogFormatter::class,
+ 'protect/move_prot' => ProtectLogFormatter::class,
+ 'protect/protect' => ProtectLogFormatter::class,
+ 'protect/unprotect' => ProtectLogFormatter::class,
+ 'rights/autopromote' => RightsLogFormatter::class,
+ 'rights/rights' => RightsLogFormatter::class,
+ 'suppress/block' => BlockLogFormatter::class,
+ 'suppress/delete' => DeleteLogFormatter::class,
+ 'suppress/event' => DeleteLogFormatter::class,
+ 'suppress/reblock' => BlockLogFormatter::class,
+ 'suppress/revision' => DeleteLogFormatter::class,
+ 'tag/update' => TagLogFormatter::class,
+ 'upload/overwrite' => UploadLogFormatter::class,
+ 'upload/revert' => UploadLogFormatter::class,
+ 'upload/upload' => UploadLogFormatter::class,
];
/**
@@ -7845,7 +7876,7 @@ $wgActions = [
'credits' => true,
'delete' => true,
'edit' => true,
- 'editchangetags' => 'SpecialPageAction',
+ 'editchangetags' => SpecialPageAction::class,
'history' => true,
'info' => true,
'markpatrolled' => true,
@@ -7854,7 +7885,7 @@ $wgActions = [
'raw' => true,
'render' => true,
'revert' => true,
- 'revisiondelete' => 'SpecialPageAction',
+ 'revisiondelete' => SpecialPageAction::class,
'rollback' => true,
'submit' => true,
'unprotect' => true,
@@ -7949,6 +7980,8 @@ $wgExemptFromUserRobotsControl = null;
* machine-readable data via api.php
*
* See https://www.mediawiki.org/wiki/API
+ *
+ * @deprecated since 1.31
*/
$wgEnableAPI = true;
@@ -7956,6 +7989,8 @@ $wgEnableAPI = true;
* Allow the API to be used to perform write operations
* (page edits, rollback, etc.) when an authorised user
* accesses it
+ *
+ * @deprecated since 1.31
*/
$wgEnableWriteAPI = true;
@@ -7999,12 +8034,12 @@ $wgDebugAPI = false;
* @code
* $wgAPIModules['foo'] = 'ApiFoo';
* $wgAPIModules['bar'] = [
- * 'class' => 'ApiBar',
+ * 'class' => ApiBar::class,
* 'factory' => function( $main, $name ) { ... }
* ];
* $wgAPIModules['xyzzy'] = [
- * 'class' => 'ApiXyzzy',
- * 'factory' => [ 'XyzzyFactory', 'newApiModule' ]
+ * 'class' => ApiXyzzy::class,
+ * 'factory' => [ XyzzyFactory::class, 'newApiModule' ]
* ];
* @endcode
*
@@ -8097,6 +8132,8 @@ $wgAPIUselessQueryPages = [
/**
* Enable AJAX framework
+ *
+ * @deprecated (officially) since MediaWiki 1.31
*/
$wgUseAjax = true;
@@ -8207,7 +8244,7 @@ $wgMaxShellWallClockTime = 180;
$wgShellCgroup = false;
/**
- * Executable path of the PHP cli binary (php/php5). Should be set up on install.
+ * Executable path of the PHP cli binary. Should be set up on install.
*/
$wgPhpCli = '/usr/bin/php';
@@ -8247,6 +8284,22 @@ $wgPhpCli = '/usr/bin/php';
*/
$wgShellLocale = 'C.UTF-8';
+/**
+ * Method to use to restrict shell commands
+ *
+ * Supported options:
+ * - 'autodetect': Autodetect if any restriction methods are available
+ * - 'firejail': Use firejail <https://firejail.wordpress.com/>
+ * - false: Don't use any restrictions
+ *
+ * @note If using firejail with MediaWiki running in a home directory different
+ * from the webserver user, firejail 0.9.44+ is required.
+ *
+ * @since 1.31
+ * @var string|bool
+ */
+$wgShellRestrictionMethod = false;
+
/** @} */ # End shell }
/************************************************************************//**
@@ -8411,7 +8464,7 @@ $wgRedirectOnLogin = null;
* @par Example using local redis instance:
* @code
* $wgPoolCounterConf = [ 'ArticleView' => [
- * 'class' => 'PoolCounterRedis',
+ * 'class' => PoolCounterRedis::class,
* 'timeout' => 15, // wait timeout in seconds
* 'workers' => 1, // maximum number of active threads in each pool
* 'maxqueue' => 5, // maximum number of total threads in each pool
@@ -8423,7 +8476,7 @@ $wgRedirectOnLogin = null;
* @par Example using C daemon from https://www.mediawiki.org/wiki/Extension:PoolCounter:
* @code
* $wgPoolCounterConf = [ 'ArticleView' => [
- * 'class' => 'PoolCounter_Client',
+ * 'class' => PoolCounter_Client::class,
* 'timeout' => 15, // wait timeout in seconds
* 'workers' => 5, // maximum number of active threads in each pool
* 'maxqueue' => 50, // maximum number of total threads in each pool
@@ -8497,7 +8550,7 @@ $wgTextModelsToParse = [
* @since 1.20
*/
$wgSiteTypes = [
- 'mediawiki' => 'MediaWikiSite',
+ 'mediawiki' => MediaWikiSite::class,
];
/**
@@ -8571,7 +8624,7 @@ $wgPageLanguageUseDB = false;
* Auto-mounting example for Parsoid:
*
* $wgVirtualRestConfig['paths']['/parsoid/'] = [
- * 'class' => 'ParsoidVirtualRESTService',
+ * 'class' => ParsoidVirtualRESTService::class,
* 'options' => [
* 'url' => 'http://localhost:8000',
* 'prefix' => 'enwiki',
@@ -8668,7 +8721,7 @@ $wgMaxJobDBWriteDuration = false;
*/
$wgEventRelayerConfig = [
'default' => [
- 'class' => 'EventRelayerNull',
+ 'class' => EventRelayerNull::class,
]
];
@@ -8766,6 +8819,13 @@ $wgInterwikiPrefixDisplayTypes = [];
$wgCommentTableSchemaMigrationStage = MIGRATION_OLD;
/**
+ * Actor table schema migration stage.
+ * @since 1.31
+ * @var int One of the MIGRATION_* constants
+ */
+$wgActorTableSchemaMigrationStage = MIGRATION_OLD;
+
+/**
* For really cool vim folding this needs to be at the end:
* vim: foldmarker=@{,@} foldmethod=marker
* @}
diff --git a/www/wiki/includes/Defines.php b/www/wiki/includes/Defines.php
index ca603e76..087af39d 100644
--- a/www/wiki/includes/Defines.php
+++ b/www/wiki/includes/Defines.php
@@ -103,7 +103,7 @@ define( 'CACHE_ANYTHING', -1 ); // Use anything, as long as it works
define( 'CACHE_NONE', 0 ); // Do not cache
define( 'CACHE_DB', 1 ); // Store cache objects in the DB
define( 'CACHE_MEMCACHED', 2 ); // MemCached, must specify servers in $wgMemCacheServers
-define( 'CACHE_ACCEL', 3 ); // APC, XCache or WinCache
+define( 'CACHE_ACCEL', 3 ); // APC or WinCache
/**@}*/
/**@{
diff --git a/www/wiki/includes/DeprecatedGlobal.php b/www/wiki/includes/DeprecatedGlobal.php
index 60dde401..242cecf1 100644
--- a/www/wiki/includes/DeprecatedGlobal.php
+++ b/www/wiki/includes/DeprecatedGlobal.php
@@ -37,9 +37,7 @@ class DeprecatedGlobal extends StubObject {
$this->version = $version;
}
- // @codingStandardsIgnoreStart
- // PSR2.Methods.MethodDeclaration.Underscore
- // PSR2.Classes.PropertyDeclaration.ScopeMissing
+ // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore,PSR2.Classes.PropertyDeclaration.ScopeMissing
function _newObject() {
/* Put the caller offset for wfDeprecated as 6, as
* that gives the function that uses this object, since:
@@ -56,5 +54,4 @@ class DeprecatedGlobal extends StubObject {
wfDeprecated( '$' . $this->global, $this->version, false, 6 );
return parent::_newObject();
}
- // @codingStandardsIgnoreEnd
}
diff --git a/www/wiki/includes/DevelopmentSettings.php b/www/wiki/includes/DevelopmentSettings.php
new file mode 100644
index 00000000..55f16edb
--- /dev/null
+++ b/www/wiki/includes/DevelopmentSettings.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Extra settings useful for MediaWiki development.
+ *
+ * To enable built-in debug and development settings, add the
+ * following to your LocalSettings.php file.
+ *
+ * require "$IP/includes/DevelopmentSettings.php";
+ *
+ * Alternatively, if running phpunit.php (or another Maintenance script),
+ * you can use the --mwdebug option to automatically load these settings.
+ *
+ * @file
+ */
+
+/**
+ * Debugging: PHP
+ */
+
+// Enable showing of errors
+error_reporting( -1 );
+ini_set( 'display_errors', 1 );
+
+/**
+ * Debugging: MediaWiki
+ */
+global $wgDevelopmentWarnings, $wgShowDBErrorBacktrace, $wgShowExceptionDetails,
+ $wgShowSQLErrors, $wgDebugRawPage,
+ $wgDebugComments, $wgDebugDumpSql, $wgDebugTimestamps,
+ $wgCommandLineMode, $wgDebugLogFile, $wgDBerrorLog, $wgDebugLogGroups;
+
+// Use of wfWarn() should cause tests to fail
+$wgDevelopmentWarnings = true;
+
+// Enable showing of errors
+$wgShowDBErrorBacktrace = true;
+$wgShowExceptionDetails = true;
+$wgShowSQLErrors = true;
+$wgDebugRawPage = true; // T49960
+
+// Enable log files
+$logDir = getenv( 'MW_LOG_DIR' );
+if ( $logDir ) {
+ if ( $wgCommandLineMode ) {
+ $wgDebugLogFile = "$logDir/mw-debug-cli.log";
+ } else {
+ $wgDebugLogFile = "$logDir/mw-debug-www.log";
+ }
+ $wgDBerrorLog = "$logDir/mw-dberror.log";
+ $wgDebugLogGroups['ratelimit'] = "$logDir/mw-ratelimit.log";
+ $wgDebugLogGroups['exception'] = "$logDir/mw-exception.log";
+ $wgDebugLogGroups['error'] = "$logDir/mw-error.log";
+}
+unset( $logDir );
+
+// Disable rate-limiting
+$wgRateLimits = [];
diff --git a/www/wiki/includes/EditPage.php b/www/wiki/includes/EditPage.php
index eeae7b9d..5c37c425 100644
--- a/www/wiki/includes/EditPage.php
+++ b/www/wiki/includes/EditPage.php
@@ -20,6 +20,8 @@
* @file
*/
+use MediaWiki\EditPage\TextboxBuilder;
+use MediaWiki\EditPage\TextConflictHelper;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use Wikimedia\ScopedCallback;
@@ -236,30 +238,6 @@ class EditPage {
/** @var bool */
public $isConflict = false;
- /**
- * @deprecated since 1.30 use Title::isCssJsSubpage()
- * @var bool
- */
- public $isCssJsSubpage = false;
-
- /**
- * @deprecated since 1.30 use Title::isCssSubpage()
- * @var bool
- */
- public $isCssSubpage = false;
-
- /**
- * @deprecated since 1.30 use Title::isJsSubpage()
- * @var bool
- */
- public $isJsSubpage = false;
-
- /**
- * @deprecated since 1.30
- * @var bool
- */
- public $isWrongCaseCssJsPage = false;
-
/** @var bool New page or new section */
public $isNew = false;
@@ -323,7 +301,7 @@ class EditPage {
/** @var bool Has a summary been preset using GET parameter &summary= ? */
public $hasPresetSummary = false;
- /** @var Revision|bool */
+ /** @var Revision|bool|null */
public $mBaseRevision = false;
/** @var bool */
@@ -447,6 +425,18 @@ class EditPage {
private $unicodeCheck;
/**
+ * Factory function to create an edit conflict helper
+ *
+ * @var callable
+ */
+ private $editConflictHelperFactory;
+
+ /**
+ * @var TextConflictHelper|null
+ */
+ private $editConflictHelper;
+
+ /**
* @param Article $article
*/
public function __construct( Article $article ) {
@@ -459,6 +449,7 @@ class EditPage {
$handler = ContentHandler::getForModelID( $this->contentModel );
$this->contentFormat = $handler->getDefaultFormat();
+ $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
}
/**
@@ -519,6 +510,7 @@ class EditPage {
* @deprecated since 1.30
*/
public function isOouiEnabled() {
+ wfDeprecated( __METHOD__, '1.30' );
return true;
}
@@ -644,13 +636,6 @@ class EditPage {
}
$this->isConflict = false;
- // css / js subpages of user pages get a special treatment
- // The following member variables are deprecated since 1.30,
- // the functions should be used instead.
- $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
- $this->isCssSubpage = $this->mTitle->isCssSubpage();
- $this->isJsSubpage = $this->mTitle->isJsSubpage();
- $this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage();
# Show applicable editing introductions
if ( $this->formtype == 'initial' || $this->firsttime ) {
@@ -764,7 +749,7 @@ class EditPage {
/**
* Display a read-only View Source page
- * @param Content $content content object
+ * @param Content $content
* @param string $errorMessage additional wikitext error message to display
*/
protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
@@ -821,8 +806,15 @@ class EditPage {
* @return bool
*/
protected function previewOnOpen() {
- $previewOnOpenNamespaces = $this->context->getConfig()->get( 'PreviewOnOpenNamespaces' );
+ $config = $this->context->getConfig();
+ $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
$request = $this->context->getRequest();
+ if ( $config->get( 'RawHtml' ) ) {
+ // If raw HTML is enabled, disable preview on open
+ // since it has to be posted with a token for
+ // security reasons
+ return false;
+ }
if ( $request->getVal( 'preview' ) == 'yes' ) {
// Explicit override from request
return true;
@@ -854,9 +846,9 @@ class EditPage {
*
* @return bool
*/
- protected function isWrongCaseCssJsPage() {
- if ( $this->mTitle->isCssJsSubpage() ) {
- $name = $this->mTitle->getSkinFromCssJsSubpage();
+ protected function isWrongCaseUserConfigPage() {
+ if ( $this->mTitle->isUserConfigPage() ) {
+ $name = $this->mTitle->getSkinFromConfigSubpage();
$skins = array_merge(
array_keys( Skin::getSkinNames() ),
[ 'common' ]
@@ -1526,8 +1518,7 @@ class EditPage {
return;
}
- $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
- $stats->increment( 'edit.failures.conflict.resolved' );
+ $this->getEditConflictHelper()->incrementResolvedStats();
}
/**
@@ -1695,7 +1686,7 @@ class EditPage {
// being set. This is used by ConfirmEdit to display a captcha
// without any error message cruft.
} else {
- $this->hookError = $status->getWikiText();
+ $this->hookError = $this->formatStatusErrors( $status );
}
// Use the existing $status->value if the hook set it
if ( !$status->value ) {
@@ -1705,7 +1696,7 @@ class EditPage {
} elseif ( !$status->isOK() ) {
# ...or the hook could be expecting us to produce an error
// FIXME this sucks, we should just use the Status object throughout
- $this->hookError = $status->getWikiText();
+ $this->hookError = $this->formatStatusErrors( $status );
$status->fatal( 'hookaborted' );
$status->value = self::AS_HOOK_ERROR_EXPECTED;
return false;
@@ -1715,6 +1706,26 @@ class EditPage {
}
/**
+ * Wrap status errors in an errorbox for increased visibility
+ *
+ * @param Status $status
+ * @return string Wikitext
+ */
+ private function formatStatusErrors( Status $status ) {
+ $errmsg = $status->getWikiText(
+ 'edit-error-short',
+ 'edit-error-long',
+ $this->context->getLanguage()
+ );
+ return <<<ERROR
+<div class="errorbox">
+{$errmsg}
+</div>
+<br clear="all" />
+ERROR;
+ }
+
+ /**
* Return the summary to be used for a new section.
*
* @param string $sectionanchor Set to the section anchor text
@@ -2327,7 +2338,7 @@ class EditPage {
/**
* @note: this method is very poorly named. If the user opened the form with ?oldid=X,
* one might think of X as the "base revision", which is NOT what this returns.
- * @return Revision Current version when the edit was started
+ * @return Revision|null Current version when the edit was started
*/
public function getBaseRevision() {
if ( !$this->mBaseRevision ) {
@@ -2386,6 +2397,7 @@ class EditPage {
$out->addModules( 'mediawiki.action.edit' );
$out->addModuleStyles( 'mediawiki.action.edit.styles' );
+ $out->addModuleStyles( 'mediawiki.editfont.styles' );
$user = $this->context->getUser();
if ( $user->getOption( 'showtoolbar' ) ) {
@@ -2429,12 +2441,22 @@ class EditPage {
$displayTitle = $contextTitle->getPrefixedText();
}
$out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
+
+ $config = $this->context->getConfig();
+
# Transmit the name of the message to JavaScript for live preview
# Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
$out->addJsConfigVars( [
'wgEditMessage' => $msg,
- 'wgAjaxEditStash' => $this->context->getConfig()->get( 'AjaxEditStash' ),
+ 'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ),
] );
+
+ // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
+ // editors, etc.
+ $out->addJsConfigVars(
+ 'wgEditSubmitButtonLabelPublish',
+ $config->get( 'EditSubmitButtonLabelPublish' )
+ );
}
/**
@@ -2451,9 +2473,11 @@ class EditPage {
if ( $namespace == NS_MEDIAWIKI ) {
# Show a warning if editing an interface message
$out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
- # If this is a default message (but not css or js),
+ # If this is a default message (but not css, json, or js),
# show a hint that it is translatable on translatewiki.net
- if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
+ if (
+ !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
+ && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
&& !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
) {
$defaultMessageText = $this->mTitle->getDefaultMessageText();
@@ -2745,7 +2769,8 @@ class EditPage {
if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
$username = $this->lastDelete->user_name;
- $comment = CommentStore::newKey( 'log_comment' )->getComment( $this->lastDelete )->text;
+ $comment = CommentStore::getStore()
+ ->getComment( 'log_comment', $this->lastDelete )->text;
// It is better to not parse the comment at all than to have templates expanded in the middle
// TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
@@ -2810,8 +2835,22 @@ class EditPage {
}
$out->addHTML( $this->editFormTextBeforeContent );
+ if ( $this->isConflict ) {
+ // In an edit conflict, we turn textbox2 into the user's text,
+ // and textbox1 into the stored version
+ $this->textbox2 = $this->textbox1;
+
+ $content = $this->getCurrentContent();
+ $this->textbox1 = $this->toEditText( $content );
+
+ $editConflictHelper = $this->getEditConflictHelper();
+ $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
+ $editConflictHelper->setContentModel( $this->contentModel );
+ $editConflictHelper->setContentFormat( $this->contentFormat );
+ $out->addHTML( $editConflictHelper->getEditFormHtmlBeforeContent() );
+ }
- if ( !$this->mTitle->isCssJsSubpage() && $showToolbar && $user->getOption( 'showtoolbar' ) ) {
+ if ( !$this->mTitle->isUserConfigPage() && $showToolbar && $user->getOption( 'showtoolbar' ) ) {
$out->addHTML( self::getEditToolbar( $this->mTitle ) );
}
@@ -2824,12 +2863,15 @@ class EditPage {
// and fallback to the raw wpTextbox1 since editconflicts can't be
// resolved between page source edits and custom ui edits using the
// custom edit ui.
- $this->textbox2 = $this->textbox1;
-
- $content = $this->getCurrentContent();
- $this->textbox1 = $this->toEditText( $content );
+ $conflictTextBoxAttribs = [];
+ if ( $this->wasDeletedSinceLastEdit() ) {
+ $conflictTextBoxAttribs['style'] = 'display:none;';
+ } elseif ( $this->isOldRev ) {
+ $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
+ }
- $this->showTextbox1();
+ $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
+ $out->addHTML( $editConflictHelper->getEditFormHtmlAfterContent() );
} else {
$this->showContentForm();
}
@@ -3045,29 +3087,38 @@ class EditPage {
);
}
} else {
- if ( $this->mTitle->isCssJsSubpage() ) {
+ if ( $this->mTitle->isUserConfigPage() ) {
# Check the skin exists
- if ( $this->isWrongCaseCssJsPage() ) {
+ if ( $this->isWrongCaseUserConfigPage() ) {
$out->wrapWikiMsg(
- "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
- [ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
+ "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
+ [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
);
}
if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
- $isCssSubpage = $this->mTitle->isCssSubpage();
- $out->wrapWikiMsg( '<div class="mw-usercssjspublic">$1</div>',
- $isCssSubpage ? 'usercssispublic' : 'userjsispublic'
- );
+ $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
+ $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
+ $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
+
+ $warning = $isUserCssConfig
+ ? 'usercssispublic'
+ : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
+
+ $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
+
if ( $this->formtype !== 'preview' ) {
$config = $this->context->getConfig();
- if ( $isCssSubpage && $config->get( 'AllowUserCss' ) ) {
+ if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
$out->wrapWikiMsg(
"<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
[ 'usercssyoucanpreview' ]
);
- }
-
- if ( $this->mTitle->isJsSubpage() && $config->get( 'AllowUserJs' ) ) {
+ } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
+ $out->wrapWikiMsg(
+ "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
+ [ 'userjsonyoucanpreview' ]
+ );
+ } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
$out->wrapWikiMsg(
"<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
[ 'userjsyoucanpreview' ]
@@ -3094,11 +3145,15 @@ class EditPage {
* @return array
*/
private function getSummaryInputAttributes( array $inputAttrs = null ) {
- // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
+ $conf = $this->context->getConfig();
+ $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
+ // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
+ // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
+ // Unicode codepoints (or 255 UTF-8 bytes for old schema).
return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
'id' => 'wpSummary',
'name' => 'wpSummary',
- 'maxlength' => '200',
+ 'maxlength' => $oldCommentSchema ? 200 : CommentStore::COMMENT_CHARACTER_LIMIT,
'tabindex' => 1,
'size' => 60,
'spellcheck' => 'true',
@@ -3106,62 +3161,6 @@ class EditPage {
}
/**
- * Standard summary input and label (wgSummary), abstracted so EditPage
- * subclasses may reorganize the form.
- * Note that you do not need to worry about the label's for=, it will be
- * inferred by the id given to the input. You can remove them both by
- * passing [ 'id' => false ] to $userInputAttrs.
- *
- * @deprecated since 1.30 Use getSummaryInputWidget() instead
- * @param string $summary The value of the summary input
- * @param string $labelText The html to place inside the label
- * @param array $inputAttrs Array of attrs to use on the input
- * @param array $spanLabelAttrs Array of attrs to use on the span inside the label
- * @return array An array in the format [ $label, $input ]
- */
- public function getSummaryInput( $summary = "", $labelText = null,
- $inputAttrs = null, $spanLabelAttrs = null
- ) {
- wfDeprecated( __METHOD__, '1.30' );
- $inputAttrs = $this->getSummaryInputAttributes( $inputAttrs );
- $inputAttrs += Linker::tooltipAndAccesskeyAttribs( 'summary' );
-
- $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
- 'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
- 'id' => "wpSummaryLabel"
- ];
-
- $label = null;
- if ( $labelText ) {
- $label = Xml::tags(
- 'label',
- $inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
- $labelText
- );
- $label = Xml::tags( 'span', $spanLabelAttrs, $label );
- }
-
- $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
-
- return [ $label, $input ];
- }
-
- /**
- * Builds a standard summary input with a label.
- *
- * @deprecated since 1.30 Use getSummaryInputWidget() instead
- * @param string $summary The value of the summary input
- * @param string $labelText The html to place inside the label
- * @param array $inputAttrs Array of attrs to use on the input
- *
- * @return OOUI\FieldLayout OOUI FieldLayout with Label and Input
- */
- function getSummaryInputOOUI( $summary = "", $labelText = null, $inputAttrs = null ) {
- wfDeprecated( __METHOD__, '1.30' );
- $this->getSummaryInputWidget( $summary, $labelText, $inputAttrs );
- }
-
- /**
* Builds a standard summary input with a label.
*
* @param string $summary The value of the summary input
@@ -3306,22 +3305,9 @@ class EditPage {
if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
$attribs = [ 'style' => 'display:none;' ];
} else {
- $classes = []; // Textarea CSS
- if ( $this->mTitle->isProtected( 'edit' ) &&
- MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
- ) {
- # Is the title semi-protected?
- if ( $this->mTitle->isSemiProtected() ) {
- $classes[] = 'mw-textarea-sprotected';
- } else {
- # Then it must be protected based on static groups (regular)
- $classes[] = 'mw-textarea-protected';
- }
- # Is the title cascade-protected?
- if ( $this->mTitle->isCascadeProtected() ) {
- $classes[] = 'mw-textarea-cprotected';
- }
- }
+ $builder = new TextboxBuilder();
+ $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
+
# Is an old revision being edited?
if ( $this->isOldRev ) {
$classes[] = 'mw-textarea-oldrev';
@@ -3333,12 +3319,7 @@ class EditPage {
$attribs += $customAttribs;
}
- if ( count( $classes ) ) {
- if ( isset( $attribs['class'] ) ) {
- $classes[] = $attribs['class'];
- }
- $attribs['class'] = implode( ' ', $classes );
- }
+ $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
}
$this->showTextbox(
@@ -3353,11 +3334,17 @@ class EditPage {
}
protected function showTextbox( $text, $name, $customAttribs = [] ) {
- $wikitext = $this->addNewLineAtEnd( $text );
-
- $attribs = $this->buildTextboxAttribs( $name, $customAttribs, $this->context->getUser() );
+ $builder = new TextboxBuilder();
+ $attribs = $builder->buildTextboxAttribs(
+ $name,
+ $customAttribs,
+ $this->context->getUser(),
+ $this->mTitle
+ );
- $this->context->getOutput()->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
+ $this->context->getOutput()->addHTML(
+ Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
+ );
}
protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
@@ -3576,6 +3563,8 @@ class EditPage {
* @return string HTML
*/
public static function getPreviewLimitReport( $output ) {
+ global $wgLang;
+
if ( !$output || !$output->getLimitReportData() ) {
return '';
}
@@ -3604,7 +3593,9 @@ class EditPage {
if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
$limitReport .= Html::openElement( 'tr' ) .
Html::rawElement( 'th', null, $keyMsg->parse() ) .
- Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
+ Html::rawElement( 'td', null,
+ $wgLang->formatNum( $valueMsg->params( $value )->parse() )
+ ) .
Html::closeElement( 'tr' );
}
}
@@ -3639,14 +3630,9 @@ class EditPage {
$out->addHTML( $this->editFormTextAfterWarn );
$out->addHTML( "<div class='editButtons'>\n" );
- $out->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
+ $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
$cancel = $this->getCancelLink();
- if ( $cancel !== '' ) {
- $cancel .= Html::element( 'span',
- [ 'class' => 'mw-editButtons-pipe-separator' ],
- $this->context->msg( 'pipe-separator' )->text() );
- }
$message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
$edithelpurl = Skin::makeInternalOrExternalUrl( $message );
@@ -3679,34 +3665,12 @@ class EditPage {
if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) {
$this->incrementConflictStats();
- $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
-
- $content1 = $this->toEditContent( $this->textbox1 );
- $content2 = $this->toEditContent( $this->textbox2 );
-
- $handler = ContentHandler::getForModelID( $this->contentModel );
- $de = $handler->createDifferenceEngine( $this->context );
- $de->setContent( $content2, $content1 );
- $de->showDiff(
- $this->context->msg( 'yourtext' )->parse(),
- $this->context->msg( 'storedversion' )->text()
- );
-
- $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
- $this->showTextbox2();
+ $this->getEditConflictHelper()->showEditFormTextAfterFooters();
}
}
protected function incrementConflictStats() {
- $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
- $stats->increment( 'edit.failures.conflict' );
- // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
- if (
- $this->mTitle->getNamespace() >= NS_MAIN &&
- $this->mTitle->getNamespace() <= NS_CATEGORY_TALK
- ) {
- $stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
- }
+ $this->getEditConflictHelper()->incrementConflictStats();
}
/**
@@ -3722,7 +3686,7 @@ class EditPage {
return new OOUI\ButtonWidget( [
'id' => 'mw-editform-cancel',
- 'href' => $this->getContextTitle()->getLinkUrl( $cancelParams ),
+ 'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
'framed' => false,
'infusable' => true,
@@ -3775,31 +3739,31 @@ class EditPage {
*/
protected function getLastDelete() {
$dbr = wfGetDB( DB_REPLICA );
- $commentQuery = CommentStore::newKey( 'log_comment' )->getJoin();
+ $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
$data = $dbr->selectRow(
- [ 'logging', 'user' ] + $commentQuery['tables'],
+ array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
[
'log_type',
'log_action',
'log_timestamp',
- 'log_user',
'log_namespace',
'log_title',
'log_params',
'log_deleted',
'user_name'
- ] + $commentQuery['fields'], [
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
+ [
'log_namespace' => $this->mTitle->getNamespace(),
'log_title' => $this->mTitle->getDBkey(),
'log_type' => 'delete',
'log_action' => 'delete',
- 'user_id=log_user'
],
__METHOD__,
[ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
[
- 'user' => [ 'JOIN', 'user_id=log_user' ],
- ] + $commentQuery['joins']
+ 'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
+ ] + $commentQuery['joins'] + $actorQuery['joins']
);
// Quick paranoid permission checks...
if ( is_object( $data ) ) {
@@ -3877,10 +3841,10 @@ class EditPage {
}
# don't parse non-wikitext pages, show message about preview
- if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
- if ( $this->mTitle->isCssJsSubpage() ) {
+ if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
+ if ( $this->mTitle->isUserConfigPage() ) {
$level = 'user';
- } elseif ( $this->mTitle->isCssOrJsPage() ) {
+ } elseif ( $this->mTitle->isSiteConfigPage() ) {
$level = 'site';
} else {
$level = false;
@@ -3891,6 +3855,11 @@ class EditPage {
if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
$format = false;
}
+ } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
+ $format = 'json';
+ if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
+ $format = false;
+ }
} elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
$format = 'js';
if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
@@ -3901,7 +3870,8 @@ class EditPage {
}
# Used messages to make sure grep find them:
- # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
+ # Messages: usercsspreview, userjsonpreview, userjspreview,
+ # sitecsspreview, sitejsonpreview, sitejspreview
if ( $level && $format ) {
$note = "<div id='mw-{$level}{$format}preview'>" .
$this->context->msg( "{$level}{$format}preview" )->text() .
@@ -3992,10 +3962,12 @@ class EditPage {
$this->mTitle, $pstContent, $user );
$parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
ScopedCallback::consume( $scopedCallback );
- $parserOutput->setEditSectionTokens( false ); // no section edit links
return [
'parserOutput' => $parserOutput,
- 'html' => $parserOutput->getText() ];
+ 'html' => $parserOutput->getText( [
+ 'enableSectionEditLinks' => false
+ ] )
+ ];
}
/**
@@ -4211,80 +4183,6 @@ class EditPage {
}
/**
- * Returns an array of html code of the following checkboxes old style:
- * minor and watch
- *
- * @deprecated since 1.30 Use getCheckboxesWidget() or getCheckboxesDefinition() instead
- * @param int &$tabindex Current tabindex
- * @param array $checked See getCheckboxesDefinition()
- * @return array
- */
- public function getCheckboxes( &$tabindex, $checked ) {
- global $wgUseMediaWikiUIEverywhere;
-
- $checkboxes = [];
- $checkboxesDef = $this->getCheckboxesDefinition( $checked );
-
- // Backwards-compatibility for the EditPageBeforeEditChecks hook
- if ( !$this->isNew ) {
- $checkboxes['minor'] = '';
- }
- $checkboxes['watch'] = '';
-
- foreach ( $checkboxesDef as $name => $options ) {
- $legacyName = isset( $options['legacy-name'] ) ? $options['legacy-name'] : $name;
- $label = $this->context->msg( $options['label-message'] )->parse();
- $attribs = [
- 'tabindex' => ++$tabindex,
- 'id' => $options['id'],
- ];
- $labelAttribs = [
- 'for' => $options['id'],
- ];
- if ( isset( $options['tooltip'] ) ) {
- $attribs['accesskey'] = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
- $labelAttribs['title'] = Linker::titleAttrib( $options['tooltip'], 'withaccess' );
- }
- if ( isset( $options['title-message'] ) ) {
- $labelAttribs['title'] = $this->context->msg( $options['title-message'] )->text();
- }
- if ( isset( $options['label-id'] ) ) {
- $labelAttribs['id'] = $options['label-id'];
- }
- $checkboxHtml =
- Xml::check( $name, $options['default'], $attribs ) .
- '&#160;' .
- Xml::tags( 'label', $labelAttribs, $label );
-
- if ( $wgUseMediaWikiUIEverywhere ) {
- $checkboxHtml = Html::rawElement( 'div', [ 'class' => 'mw-ui-checkbox' ], $checkboxHtml );
- }
-
- $checkboxes[ $legacyName ] = $checkboxHtml;
- }
-
- // Avoid PHP 7.1 warning of passing $this by reference
- $editPage = $this;
- Hooks::run( 'EditPageBeforeEditChecks', [ &$editPage, &$checkboxes, &$tabindex ], '1.29' );
- return $checkboxes;
- }
-
- /**
- * Returns an array of checkboxes for the edit form, including 'minor' and 'watch' checkboxes and
- * any other added by extensions.
- *
- * @deprecated since 1.30 Use getCheckboxesWidget() or getCheckboxesDefinition() instead
- * @param int &$tabindex Current tabindex
- * @param array $checked Array of checkbox => bool, where bool indicates the checked
- * status of the checkbox
- *
- * @return array Associative array of string keys to OOUI\FieldLayout instances
- */
- public function getCheckboxesOOUI( &$tabindex, $checked ) {
- return $this->getCheckboxesWidget( $tabindex, $checked );
- }
-
- /**
* Returns an array of checkboxes for the edit form, including 'minor' and 'watch' checkboxes and
* any other added by extensions.
*
@@ -4388,34 +4286,32 @@ class EditPage {
public function getEditButtons( &$tabindex ) {
$buttons = [];
- $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
+ $labelAsPublish =
+ $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
- $attribs = [
- 'name' => 'wpSave',
- 'tabindex' => ++$tabindex,
- ];
+ $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
+ $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
- $saveConfig = OOUI\Element::configFromHtmlAttributes( $attribs );
$buttons['save'] = new OOUI\ButtonInputWidget( [
+ 'name' => 'wpSave',
+ 'tabIndex' => ++$tabindex,
'id' => 'wpSaveWidget',
'inputId' => 'wpSave',
// Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
'useInputTag' => true,
- 'flags' => [ 'constructive', 'primary' ],
+ 'flags' => [ 'progressive', 'primary' ],
'label' => $buttonLabel,
'infusable' => true,
'type' => 'submit',
- 'title' => Linker::titleAttrib( 'save' ),
- 'accessKey' => Linker::accesskey( 'save' ),
- ] + $saveConfig );
-
- $attribs = [
- 'name' => 'wpPreview',
- 'tabindex' => ++$tabindex,
- ];
+ // Messages used: tooltip-save, tooltip-publish
+ 'title' => Linker::titleAttrib( $buttonTooltip ),
+ // Messages used: accesskey-save, accesskey-publish
+ 'accessKey' => Linker::accesskey( $buttonTooltip ),
+ ] );
- $previewConfig = OOUI\Element::configFromHtmlAttributes( $attribs );
$buttons['preview'] = new OOUI\ButtonInputWidget( [
+ 'name' => 'wpPreview',
+ 'tabIndex' => ++$tabindex,
'id' => 'wpPreviewWidget',
'inputId' => 'wpPreview',
// Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
@@ -4423,17 +4319,15 @@ class EditPage {
'label' => $this->context->msg( 'showpreview' )->text(),
'infusable' => true,
'type' => 'submit',
+ // Message used: tooltip-preview
'title' => Linker::titleAttrib( 'preview' ),
+ // Message used: accesskey-preview
'accessKey' => Linker::accesskey( 'preview' ),
- ] + $previewConfig );
-
- $attribs = [
- 'name' => 'wpDiff',
- 'tabindex' => ++$tabindex,
- ];
+ ] );
- $diffConfig = OOUI\Element::configFromHtmlAttributes( $attribs );
$buttons['diff'] = new OOUI\ButtonInputWidget( [
+ 'name' => 'wpDiff',
+ 'tabIndex' => ++$tabindex,
'id' => 'wpDiffWidget',
'inputId' => 'wpDiff',
// Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
@@ -4441,9 +4335,11 @@ class EditPage {
'label' => $this->context->msg( 'showdiff' )->text(),
'infusable' => true,
'type' => 'submit',
+ // Message used: tooltip-diff
'title' => Linker::titleAttrib( 'diff' ),
+ // Message used: accesskey-diff
'accessKey' => Linker::accesskey( 'diff' ),
- ] + $diffConfig );
+ ] );
// Avoid PHP 7.1 warning of passing $this by reference
$editPage = $this;
@@ -4635,9 +4531,8 @@ class EditPage {
* @since 1.29
*/
protected function addExplainConflictHeader( OutputPage $out ) {
- $out->wrapWikiMsg(
- "<div class='mw-explainconflict'>\n$1\n</div>",
- [ 'explainconflict', $this->context->msg( $this->getSubmitButtonLabel() )->text() ]
+ $out->addHTML(
+ $this->getEditConflictHelper()->getExplainHeader()
);
}
@@ -4649,38 +4544,9 @@ class EditPage {
* @since 1.29
*/
protected function buildTextboxAttribs( $name, array $customAttribs, User $user ) {
- $attribs = $customAttribs + [
- 'accesskey' => ',',
- 'id' => $name,
- 'cols' => 80,
- 'rows' => 25,
- // Avoid PHP notices when appending preferences
- // (appending allows customAttribs['style'] to still work).
- 'style' => ''
- ];
-
- // The following classes can be used here:
- // * mw-editfont-default
- // * mw-editfont-monospace
- // * mw-editfont-sans-serif
- // * mw-editfont-serif
- $class = 'mw-editfont-' . $user->getOption( 'editfont' );
-
- if ( isset( $attribs['class'] ) ) {
- if ( is_string( $attribs['class'] ) ) {
- $attribs['class'] .= ' ' . $class;
- } elseif ( is_array( $attribs['class'] ) ) {
- $attribs['class'][] = $class;
- }
- } else {
- $attribs['class'] = $class;
- }
-
- $pageLang = $this->mTitle->getPageLanguage();
- $attribs['lang'] = $pageLang->getHtmlCode();
- $attribs['dir'] = $pageLang->getDir();
-
- return $attribs;
+ return ( new TextboxBuilder() )->buildTextboxAttribs(
+ $name, $customAttribs, $user, $this->mTitle
+ );
}
/**
@@ -4689,15 +4555,7 @@ class EditPage {
* @since 1.29
*/
protected function addNewLineAtEnd( $wikitext ) {
- if ( strval( $wikitext ) !== '' ) {
- // Ensure there's a newline at the end, otherwise adding lines
- // is awkward.
- // But don't add a newline if the text is empty, or Firefox in XHTML
- // mode will show an extra newline. A bit annoying.
- $wikitext .= "\n";
- return $wikitext;
- }
- return $wikitext;
+ return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
}
/**
@@ -4720,6 +4578,47 @@ class EditPage {
return $wgParser->guessLegacySectionNameFromWikiText( $text );
}
// Meanwhile, real browsers get real anchors
- return $wgParser->guessSectionNameFromWikiText( $text );
+ $name = $wgParser->guessSectionNameFromWikiText( $text );
+ // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded,
+ // otherwise Chrome double-escapes the rest of the URL.
+ return '#' . urlencode( mb_substr( $name, 1 ) );
+ }
+
+ /**
+ * Set a factory function to create an EditConflictHelper
+ *
+ * @param callable $factory Factory function
+ * @since 1.31
+ */
+ public function setEditConflictHelperFactory( callable $factory ) {
+ $this->editConflictHelperFactory = $factory;
+ $this->editConflictHelper = null;
+ }
+
+ /**
+ * @return TextConflictHelper
+ */
+ private function getEditConflictHelper() {
+ if ( !$this->editConflictHelper ) {
+ $this->editConflictHelper = call_user_func(
+ $this->editConflictHelperFactory,
+ $this->getSubmitButtonLabel()
+ );
+ }
+
+ return $this->editConflictHelper;
+ }
+
+ /**
+ * @param string $submitButtonLabel
+ * @return TextConflictHelper
+ */
+ private function newTextConflictHelper( $submitButtonLabel ) {
+ return new TextConflictHelper(
+ $this->getTitle(),
+ $this->getContext()->getOutput(),
+ MediaWikiServices::getInstance()->getStatsdDataFactory(),
+ $submitButtonLabel
+ );
}
}
diff --git a/www/wiki/includes/Feed.php b/www/wiki/includes/Feed.php
index d05fc2d8..86e9bee6 100644
--- a/www/wiki/includes/Feed.php
+++ b/www/wiki/includes/Feed.php
@@ -84,13 +84,23 @@ class FeedItem {
}
/**
- * Get the unique id of this item
- *
+ * Get the unique id of this item; already xml-encoded
+ * @return string
+ */
+ public function getUniqueID() {
+ $id = $this->getUniqueIdUnescaped();
+ if ( $id ) {
+ return $this->xmlEncode( $id );
+ }
+ }
+
+ /**
+ * Get the unique id of this item, without any escaping
* @return string
*/
- public function getUniqueId() {
+ public function getUniqueIdUnescaped() {
if ( $this->uniqueId ) {
- return $this->xmlEncode( wfExpandUrl( $this->uniqueId, PROTO_CURRENT ) );
+ return wfExpandUrl( $this->uniqueId, PROTO_CURRENT );
}
}
@@ -123,6 +133,14 @@ class FeedItem {
return $this->xmlEncode( $this->url );
}
+ /** Get the URL of this item without any escaping
+ *
+ * @return string
+ */
+ public function getUrlUnescaped() {
+ return $this->url;
+ }
+
/**
* Get the description of this item; already xml-encoded
*
@@ -133,13 +151,22 @@ class FeedItem {
}
/**
+ * Get the description of this item without any escaping
+ *
+ * @return string
+ */
+ public function getDescriptionUnescaped() {
+ return $this->description;
+ }
+
+ /**
* Get the language of this item
*
* @return string
*/
public function getLanguage() {
global $wgLanguageCode;
- return wfBCP47( $wgLanguageCode );
+ return LanguageCode::bcp47( $wgLanguageCode );
}
/**
@@ -161,6 +188,15 @@ class FeedItem {
}
/**
+ * Get the author of this item without any escaping
+ *
+ * @return string
+ */
+ public function getAuthorUnescaped() {
+ return $this->author;
+ }
+
+ /**
* Get the comment of this item; already xml-encoded
*
* @return string
@@ -170,6 +206,15 @@ class FeedItem {
}
/**
+ * Get the comment of this item without any escaping
+ *
+ * @return string
+ */
+ public function getCommentsUnescaped() {
+ return $this->comments;
+ }
+
+ /**
* Quickie hack... strip out wikilinks to more legible form from the comment.
*
* @param string $text Wikitext
@@ -187,6 +232,23 @@ class FeedItem {
* @ingroup Feed
*/
abstract class ChannelFeed extends FeedItem {
+
+ /** @var TemplateParser */
+ protected $templateParser;
+
+ /**
+ * @param string|Title $title Feed's title
+ * @param string $description
+ * @param string $url URL uniquely designating the feed.
+ * @param string $date Feed's date
+ * @param string $author Author's user name
+ * @param string $comments
+ */
+ function __construct( $title, $description, $url, $date = '', $author = '', $comments = '' ) {
+ parent::__construct( $title, $description, $url, $date, $author, $comments );
+ $this->templateParser = new TemplateParser();
+ }
+
/**
* Generate Header of the feed
* @par Example:
@@ -232,7 +294,8 @@ abstract class ChannelFeed extends FeedItem {
header( "Content-type: $mimetype; charset=UTF-8" );
// Set a sane filename
- $exts = MimeMagic::singleton()->getExtensionsForType( $mimetype );
+ $exts = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer()
+ ->getExtensionsForType( $mimetype );
$ext = $exts ? strtok( $exts, ' ' ) : 'xml';
header( "Content-Disposition: inline; filename=\"feed.{$ext}\"" );
@@ -278,13 +341,15 @@ abstract class ChannelFeed extends FeedItem {
class RSSFeed extends ChannelFeed {
/**
- * Format a date given a timestamp
+ * Format a date given a timestamp. If a timestamp is not given, nothing is returned
*
- * @param int $ts Timestamp
- * @return string Date string
+ * @param int|null $ts Timestamp
+ * @return string|null Date string
*/
function formatTime( $ts ) {
- return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) );
+ if ( $ts ) {
+ return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) );
+ }
}
/**
@@ -294,15 +359,17 @@ class RSSFeed extends ChannelFeed {
global $wgVersion;
$this->outXmlHeader();
- ?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
- <channel>
- <title><?php print $this->getTitle() ?></title>
- <link><?php print wfExpandUrl( $this->getUrl(), PROTO_CURRENT ) ?></link>
- <description><?php print $this->getDescription() ?></description>
- <language><?php print $this->getLanguage() ?></language>
- <generator>MediaWiki <?php print $wgVersion ?></generator>
- <lastBuildDate><?php print $this->formatTime( wfTimestampNow() ) ?></lastBuildDate>
-<?php
+ // Manually escaping rather than letting Mustache do it because Mustache
+ // uses htmlentities, which does not work with XML
+ $templateParams = [
+ 'title' => $this->getTitle(),
+ 'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ),
+ 'description' => $this->getDescription(),
+ 'language' => $this->xmlEncode( $this->getLanguage() ),
+ 'version' => $this->xmlEncode( $wgVersion ),
+ 'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) )
+ ];
+ print $this->templateParser->processTemplate( 'RSSHeader', $templateParams );
}
/**
@@ -310,28 +377,30 @@ class RSSFeed extends ChannelFeed {
* @param FeedItem $item Item to be output
*/
function outItem( $item ) {
- // @codingStandardsIgnoreStart Ignore long lines and formatting issues.
- ?>
- <item>
- <title><?php print $item->getTitle(); ?></title>
- <link><?php print wfExpandUrl( $item->getUrl(), PROTO_CURRENT ); ?></link>
- <guid<?php if ( !$item->rssIsPermalink ) { print ' isPermaLink="false"'; } ?>><?php print $item->getUniqueId(); ?></guid>
- <description><?php print $item->getDescription() ?></description>
- <?php if ( $item->getDate() ) { ?><pubDate><?php print $this->formatTime( $item->getDate() ); ?></pubDate><?php } ?>
- <?php if ( $item->getAuthor() ) { ?><dc:creator><?php print $item->getAuthor(); ?></dc:creator><?php }?>
- <?php if ( $item->getComments() ) { ?><comments><?php print wfExpandUrl( $item->getComments(), PROTO_CURRENT ); ?></comments><?php }?>
- </item>
-<?php
- // @codingStandardsIgnoreEnd
+ // Manually escaping rather than letting Mustache do it because Mustache
+ // uses htmlentities, which does not work with XML
+ $templateParams = [
+ "title" => $item->getTitle(),
+ "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ),
+ "permalink" => $item->rssIsPermalink,
+ "uniqueID" => $item->getUniqueID(),
+ "description" => $item->getDescription(),
+ "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ),
+ "author" => $item->getAuthor()
+ ];
+ $comments = $item->getCommentsUnescaped();
+ if ( $comments ) {
+ $commentsEscaped = $this->xmlEncode( wfExpandUrl( $comments, PROTO_CURRENT ) );
+ $templateParams["comments"] = $commentsEscaped;
+ }
+ print $this->templateParser->processTemplate( 'RSSItem', $templateParams );
}
/**
* Output an RSS 2.0 footer
*/
function outFooter() {
- ?>
- </channel>
-</rss><?php
+ print "</channel></rss>";
}
}
@@ -342,14 +411,16 @@ class RSSFeed extends ChannelFeed {
*/
class AtomFeed extends ChannelFeed {
/**
- * Format a date given timestamp.
+ * Format a date given timestamp, if one is given.
*
- * @param string|int $timestamp
- * @return string
+ * @param string|int|null $timestamp
+ * @return string|null
*/
function formatTime( $timestamp ) {
- // need to use RFC 822 time format at least for rss2.0
- return gmdate( 'Y-m-d\TH:i:s', wfTimestamp( TS_UNIX, $timestamp ) );
+ if ( $timestamp ) {
+ // need to use RFC 822 time format at least for rss2.0
+ return gmdate( 'Y-m-d\TH:i:s', wfTimestamp( TS_UNIX, $timestamp ) );
+ }
}
/**
@@ -357,20 +428,20 @@ class AtomFeed extends ChannelFeed {
*/
function outHeader() {
global $wgVersion;
-
$this->outXmlHeader();
- // @codingStandardsIgnoreStart Ignore long lines and formatting issues.
- ?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="<?php print $this->getLanguage() ?>">
- <id><?php print $this->getFeedId() ?></id>
- <title><?php print $this->getTitle() ?></title>
- <link rel="self" type="application/atom+xml" href="<?php print wfExpandUrl( $this->getSelfUrl(), PROTO_CURRENT ) ?>"/>
- <link rel="alternate" type="text/html" href="<?php print wfExpandUrl( $this->getUrl(), PROTO_CURRENT ) ?>"/>
- <updated><?php print $this->formatTime( wfTimestampNow() ) ?>Z</updated>
- <subtitle><?php print $this->getDescription() ?></subtitle>
- <generator>MediaWiki <?php print $wgVersion ?></generator>
-
-<?php
- // @codingStandardsIgnoreEnd
+ // Manually escaping rather than letting Mustache do it because Mustache
+ // uses htmlentities, which does not work with XML
+ $templateParams = [
+ 'language' => $this->xmlEncode( $this->getLanguage() ),
+ 'feedID' => $this->getFeedId(),
+ 'title' => $this->getTitle(),
+ 'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ),
+ 'selfUrl' => $this->getSelfUrl(),
+ 'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) ),
+ 'description' => $this->getDescription(),
+ 'version' => $this->xmlEncode( $wgVersion ),
+ ];
+ print $this->templateParser->processTemplate( 'AtomHeader', $templateParams );
}
/**
@@ -400,30 +471,24 @@ class AtomFeed extends ChannelFeed {
*/
function outItem( $item ) {
global $wgMimeType;
- // @codingStandardsIgnoreStart Ignore long lines and formatting issues.
- ?>
- <entry>
- <id><?php print $item->getUniqueId(); ?></id>
- <title><?php print $item->getTitle(); ?></title>
- <link rel="alternate" type="<?php print $wgMimeType ?>" href="<?php print wfExpandUrl( $item->getUrl(), PROTO_CURRENT ); ?>"/>
- <?php if ( $item->getDate() ) { ?>
- <updated><?php print $this->formatTime( $item->getDate() ); ?>Z</updated>
- <?php } ?>
-
- <summary type="html"><?php print $item->getDescription() ?></summary>
- <?php if ( $item->getAuthor() ) { ?><author><name><?php print $item->getAuthor(); ?></name></author><?php }?>
- </entry>
-
-<?php /* @todo FIXME: Need to add comments
- <?php if( $item->getComments() ) { ?><dc:comment><?php print $item->getComments() ?></dc:comment><?php }?>
- */
+ // Manually escaping rather than letting Mustache do it because Mustache
+ // uses htmlentities, which does not work with XML
+ $templateParams = [
+ "uniqueID" => $item->getUniqueID(),
+ "title" => $item->getTitle(),
+ "mimeType" => $this->xmlEncode( $wgMimeType ),
+ "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ),
+ "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ),
+ "description" => $item->getDescription(),
+ "author" => $item->getAuthor()
+ ];
+ print $this->templateParser->processTemplate( 'AtomItem', $templateParams );
}
/**
* Outputs the footer for Atom 1.0 feed (basically '\</feed\>').
*/
- function outFooter() {?>
- </feed><?php
- // @codingStandardsIgnoreEnd
+ function outFooter() {
+ print "</feed>";
}
}
diff --git a/www/wiki/includes/FeedUtils.php b/www/wiki/includes/FeedUtils.php
index 0def6a04..4dde52d0 100644
--- a/www/wiki/includes/FeedUtils.php
+++ b/www/wiki/includes/FeedUtils.php
@@ -89,9 +89,7 @@ class FeedUtils {
$timestamp,
$row->rc_deleted & Revision::DELETED_COMMENT
? wfMessage( 'rev-deleted-comment' )->escaped()
- : CommentStore::newKey( 'rc_comment' )
- // Legacy from RecentChange::selectFields() via ChangesListSpecialPage::doMainQuery()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row )->text,
+ : CommentStore::getStore()->getComment( 'rc_comment', $row )->text,
$actiontext
);
}
@@ -99,7 +97,7 @@ class FeedUtils {
/**
* Really format a diff for the newsfeed
*
- * @param Title $title Title object
+ * @param Title $title
* @param int $oldid Old revision's id
* @param int $newid New revision's id
* @param int $timestamp New revision's timestamp
@@ -236,18 +234,18 @@ class FeedUtils {
*/
public static function applyDiffStyle( $text ) {
$styles = [
- 'diff' => 'background-color: white; color:black;',
- 'diff-otitle' => 'background-color: white; color:black; text-align: center;',
- 'diff-ntitle' => 'background-color: white; color:black; text-align: center;',
- 'diff-addedline' => 'color:black; font-size: 88%; border-style: solid; '
+ 'diff' => 'background-color: #fff; color: #222;',
+ 'diff-otitle' => 'background-color: #fff; color: #222; text-align: center;',
+ 'diff-ntitle' => 'background-color: #fff; color: #222; text-align: center;',
+ 'diff-addedline' => 'color: #222; font-size: 88%; border-style: solid; '
. 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; '
. 'vertical-align: top; white-space: pre-wrap;',
- 'diff-deletedline' => 'color:black; font-size: 88%; border-style: solid; '
+ 'diff-deletedline' => 'color: #222; font-size: 88%; border-style: solid; '
. 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; '
. 'vertical-align: top; white-space: pre-wrap;',
- 'diff-context' => 'background-color: #f9f9f9; color: #333333; font-size: 88%; '
+ 'diff-context' => 'background-color: #f8f9fa; color: #222; font-size: 88%; '
. 'border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; '
- . 'border-color: #e6e6e6; vertical-align: top; white-space: pre-wrap;',
+ . 'border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;',
'diffchange' => 'font-weight: bold; text-decoration: none;',
];
diff --git a/www/wiki/includes/FileDeleteForm.php b/www/wiki/includes/FileDeleteForm.php
index 8c843c44..783de1c0 100644
--- a/www/wiki/includes/FileDeleteForm.php
+++ b/www/wiki/includes/FileDeleteForm.php
@@ -246,6 +246,9 @@ class FileDeleteForm {
private function showForm() {
global $wgOut, $wgUser, $wgRequest;
+ $conf = RequestContext::getMain()->getConfig();
+ $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
+
if ( $wgUser->isAllowed( 'suppressrevision' ) ) {
$suppress = "<tr id=\"wpDeleteSuppressRow\">
<td></td>
@@ -258,6 +261,8 @@ class FileDeleteForm {
$suppress = '';
}
+ $wgOut->addModules( 'mediawiki.action.delete.file' );
+
$checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $wgUser->isWatched( $this->title );
$form = Xml::openElement( 'form', [ 'method' => 'post', 'action' => $this->getAction(),
'id' => 'mw-img-deleteconfirm' ] ) .
@@ -286,8 +291,15 @@ class FileDeleteForm {
Xml::label( wfMessage( 'filedelete-otherreason' )->text(), 'wpReason' ) .
"</td>
<td class='mw-input'>" .
- Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ),
- [ 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ] ) .
+ Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ), [
+ 'type' => 'text',
+ // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
+ // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
+ // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+ 'maxlength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
+ 'tabindex' => '2',
+ 'id' => 'wpReason'
+ ] ) .
"</td>
</tr>
{$suppress}";
diff --git a/www/wiki/includes/ForkController.php b/www/wiki/includes/ForkController.php
index 2dde17be..cc16964e 100644
--- a/www/wiki/includes/ForkController.php
+++ b/www/wiki/includes/ForkController.php
@@ -54,7 +54,7 @@ class ForkController {
const RESTART_ON_ERROR = 1;
public function __construct( $numProcs, $flags = 0 ) {
- if ( PHP_SAPI != 'cli' ) {
+ if ( !wfIsCLI() ) {
throw new MWException( "ForkController cannot be used from the web." );
}
$this->procsToStart = $numProcs;
diff --git a/www/wiki/includes/GitInfo.php b/www/wiki/includes/GitInfo.php
index 4351acc0..363d7b80 100644
--- a/www/wiki/includes/GitInfo.php
+++ b/www/wiki/includes/GitInfo.php
@@ -23,6 +23,8 @@
* @file
*/
+use MediaWiki\Shell\Shell;
+
class GitInfo {
/**
@@ -36,6 +38,11 @@ class GitInfo {
protected $basedir;
/**
+ * Location of the repository
+ */
+ protected $repoDir;
+
+ /**
* Path to JSON cache file for pre-computed git information.
*/
protected $cacheFile;
@@ -56,6 +63,7 @@ class GitInfo {
* @see precomputeValues
*/
public function __construct( $repoDir, $usePrecomputed = true ) {
+ $this->repoDir = $repoDir;
$this->cacheFile = self::getCacheFilePath( $repoDir );
wfDebugLog( 'gitinfo',
"Computed cacheFile={$this->cacheFile} for {$repoDir}"
@@ -191,8 +199,14 @@ class GitInfo {
} else {
// If not a SHA1 it may be a ref:
$refFile = "{$this->basedir}/{$head}";
+ $packedRefs = "{$this->basedir}/packed-refs";
+ $headRegex = preg_quote( $head, '/' );
if ( is_readable( $refFile ) ) {
$sha1 = rtrim( file_get_contents( $refFile ) );
+ } elseif ( is_readable( $packedRefs ) &&
+ preg_match( "/^([0-9A-Fa-f]{40}) $headRegex$/m", file_get_contents( $packedRefs ), $matches )
+ ) {
+ $sha1 = $matches[1];
}
}
$this->cache['headSHA1'] = $sha1;
@@ -213,15 +227,25 @@ class GitInfo {
$date = false;
if ( is_file( $wgGitBin ) &&
is_executable( $wgGitBin ) &&
+ !Shell::isDisabled() &&
$this->getHead() !== false
) {
- $environment = [ "GIT_DIR" => $this->basedir ];
- $cmd = wfEscapeShellArg( $wgGitBin ) .
- " show -s --format=format:%ct HEAD";
- $retc = false;
- $commitDate = wfShellExec( $cmd, $retc, $environment );
- if ( $retc === 0 ) {
- $date = (int)$commitDate;
+ $cmd = [
+ $wgGitBin,
+ 'show',
+ '-s',
+ '--format=format:%ct',
+ 'HEAD',
+ ];
+ $gitDir = realpath( $this->basedir );
+ $result = Shell::command( $cmd )
+ ->environment( [ 'GIT_DIR' => $gitDir ] )
+ ->restrict( Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK )
+ ->whitelistPaths( [ $gitDir, $this->repoDir ] )
+ ->execute();
+
+ if ( $result->getExitCode() === 0 ) {
+ $date = (int)$result->getStdout();
}
}
$this->cache['headCommitDate'] = $date;
@@ -283,9 +307,9 @@ class GitInfo {
$config = "{$this->basedir}/config";
$url = false;
if ( is_readable( $config ) ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$configArray = parse_ini_file( $config, true );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
$remote = false;
// Use the "origin" remote repo if available or any other repo if not.
diff --git a/www/wiki/includes/GlobalFunctions.php b/www/wiki/includes/GlobalFunctions.php
index e80ecf1c..0152209d 100644
--- a/www/wiki/includes/GlobalFunctions.php
+++ b/www/wiki/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\Sender\SocketSender;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\ProcOpenError;
use MediaWiki\Session\SessionManager;
@@ -33,75 +32,6 @@ use MediaWiki\Shell\Shell;
use Wikimedia\ScopedCallback;
use Wikimedia\Rdbms\DBReplicationWaitError;
-// Hide compatibility functions from Doxygen
-/// @cond
-/**
- * Compatibility functions
- *
- * We support PHP 5.5.9 and up.
- * Re-implementations of newer functions or functions in non-standard
- * PHP extensions may be included here.
- */
-
-// hash_equals function only exists in PHP >= 5.6.0
-// https://secure.php.net/hash_equals
-if ( !function_exists( 'hash_equals' ) ) {
- /**
- * Check whether a user-provided string is equal to a fixed-length secret string
- * without revealing bytes of the secret string through timing differences.
- *
- * The usual way to compare strings (PHP's === operator or the underlying memcmp()
- * function in C) is to compare corresponding bytes and stop at the first difference,
- * which would take longer for a partial match than for a complete mismatch. This
- * is not secure when one of the strings (e.g. an HMAC or token) must remain secret
- * and the other may come from an attacker. Statistical analysis of timing measurements
- * over many requests may allow the attacker to guess the string's bytes one at a time
- * (and check his guesses) even if the timing differences are extremely small.
- *
- * When making such a security-sensitive comparison, it is essential that the sequence
- * in which instructions are executed and memory locations are accessed not depend on
- * the secret string's value. HOWEVER, for simplicity, we do not attempt to minimize
- * the inevitable leakage of the string's length. That is generally known anyway as
- * a chararacteristic of the hash function used to compute the secret value.
- *
- * Longer explanation: http://www.emerose.com/timing-attacks-explained
- *
- * @codeCoverageIgnore
- * @param string $known_string Fixed-length secret string to compare against
- * @param string $user_string User-provided string
- * @return bool True if the strings are the same, false otherwise
- */
- function hash_equals( $known_string, $user_string ) {
- // Strict type checking as in PHP's native implementation
- if ( !is_string( $known_string ) ) {
- trigger_error( 'hash_equals(): Expected known_string to be a string, ' .
- gettype( $known_string ) . ' given', E_USER_WARNING );
-
- return false;
- }
-
- if ( !is_string( $user_string ) ) {
- trigger_error( 'hash_equals(): Expected user_string to be a string, ' .
- gettype( $user_string ) . ' given', E_USER_WARNING );
-
- return false;
- }
-
- $known_string_len = strlen( $known_string );
- if ( $known_string_len !== strlen( $user_string ) ) {
- return false;
- }
-
- $result = 0;
- for ( $i = 0; $i < $known_string_len; $i++ ) {
- $result |= ord( $known_string[$i] ) ^ ord( $user_string[$i] );
- }
-
- return ( $result === 0 );
- }
-}
-/// @endcond
-
/**
* Load an extension
*
@@ -195,11 +125,15 @@ function wfArrayDiff2_cmp( $a, $b ) {
} else {
reset( $a );
reset( $b );
- while ( ( list( , $valueA ) = each( $a ) ) && ( list( , $valueB ) = each( $b ) ) ) {
+ while ( key( $a ) !== null && key( $b ) !== null ) {
+ $valueA = current( $a );
+ $valueB = current( $b );
$cmp = strcmp( $valueA, $valueB );
if ( $cmp !== 0 ) {
return $cmp;
}
+ next( $a );
+ next( $b );
}
return 0;
}
@@ -579,7 +513,7 @@ function wfAppendQuery( $url, $query ) {
* like "subdir/foo.html", etc.
*
* @param string $url Either fully-qualified or a local path + query
- * @param string $defaultProto One of the PROTO_* constants. Determines the
+ * @param string|int|null $defaultProto One of the PROTO_* constants. Determines the
* protocol to use if $url or $wgServer is protocol-relative
* @return string|false Fully-qualified URL, current-path-relative URL or false if
* no valid URL can be constructed
@@ -874,9 +808,9 @@ function wfParseUrl( $url ) {
if ( $wasRelative ) {
$url = "http:$url";
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$bits = parse_url( $url );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
// parse_url() returns an array without scheme for some invalid URLs, e.g.
// parse_url("%0Ahttp://example.com") == [ 'host' => '%0Ahttp', 'path' => 'example.com' ]
if ( !$bits || !isset( $bits['scheme'] ) ) {
@@ -1007,7 +941,7 @@ function wfMakeUrlIndexes( $url ) {
/**
* Check whether a given URL has a domain that occurs in a given set of domains
- * @param string $url URL
+ * @param string $url
* @param array $domains Array of domains (strings)
* @return bool True if the host part of $url ends in one of the strings in $domains
*/
@@ -1047,7 +981,7 @@ function wfMatchesDomainList( $url, $domains ) {
*/
function wfDebug( $text, $dest = 'all', array $context = [] ) {
global $wgDebugRawPage, $wgDebugLogPrefix;
- global $wgDebugTimestamps, $wgRequestTime;
+ global $wgDebugTimestamps;
if ( !$wgDebugRawPage && wfIsDebugRawPage() ) {
return;
@@ -1058,7 +992,7 @@ function wfDebug( $text, $dest = 'all', array $context = [] ) {
if ( $wgDebugTimestamps ) {
$context['seconds_elapsed'] = sprintf(
'%6.4f',
- microtime( true ) - $wgRequestTime
+ microtime( true ) - $_SERVER['REQUEST_TIME_FLOAT']
);
$context['memory_used'] = sprintf(
'%5.1fM',
@@ -1227,6 +1161,7 @@ function wfErrorLog( $text, $file, array $context = [] ) {
/**
* @todo document
+ * @todo Move logic to MediaWiki.php
*/
function wfLogProfilingData() {
global $wgDebugLogGroups, $wgDebugRawPage;
@@ -1238,23 +1173,13 @@ function wfLogProfilingData() {
$profiler->setContext( $context );
$profiler->logData();
- $config = $context->getConfig();
- $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
- if ( $config->get( 'StatsdServer' ) && $stats->hasData() ) {
- 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->setSamplingRates( $config->get( 'StatsdSamplingRates' ) );
- $statsdClient->send( $stats->getData() );
- } catch ( Exception $ex ) {
- MWExceptionHandler::logException( $ex );
- }
- }
+ // Send out any buffered statsd metrics as needed
+ MediaWiki::emitBufferedStatsdData(
+ MediaWikiServices::getInstance()->getStatsdDataFactory(),
+ $context->getConfig()
+ );
- # Profiling must actually be enabled...
+ // Profiling must actually be enabled...
if ( $profiler instanceof ProfilerStub ) {
return;
}
@@ -1520,9 +1445,11 @@ function wfHostname() {
* @return string
*/
function wfReportTime() {
- global $wgRequestTime, $wgShowHostnames;
+ global $wgShowHostnames;
- $responseTime = round( ( microtime( true ) - $wgRequestTime ) * 1000 );
+ $elapsed = ( microtime( true ) - $_SERVER['REQUEST_TIME_FLOAT'] );
+ // seconds to milliseconds
+ $responseTime = round( $elapsed * 1000 );
$reportVars = [ 'wgBackendResponseTime' => $responseTime ];
if ( $wgShowHostnames ) {
$reportVars['wgHostname'] = wfHostname();
@@ -1849,7 +1776,7 @@ function wfHttpError( $code, $label, $desc ) {
function wfResetOutputBuffers( $resetGzipEncoding = true ) {
if ( $resetGzipEncoding ) {
// Suppress Content-Encoding and Content-Length
- // headers from 1.10+s wfOutputHandler
+ // headers from OutputHandler::handle.
global $wgDisableOutputCompression;
$wgDisableOutputCompression = true;
}
@@ -2015,19 +1942,19 @@ function wfNegotiateType( $cprefs, $sprefs ) {
/**
* Reference-counted warning suppression
*
- * @deprecated since 1.26, use MediaWiki\suppressWarnings() directly
+ * @deprecated since 1.26, use Wikimedia\suppressWarnings() directly
* @param bool $end
*/
function wfSuppressWarnings( $end = false ) {
- MediaWiki\suppressWarnings( $end );
+ Wikimedia\suppressWarnings( $end );
}
/**
- * @deprecated since 1.26, use MediaWiki\restoreWarnings() directly
+ * @deprecated since 1.26, use Wikimedia\restoreWarnings() directly
* Restore error level to previous value
*/
function wfRestoreWarnings() {
- MediaWiki\suppressWarnings( true );
+ Wikimedia\restoreWarnings();
}
/**
@@ -2095,6 +2022,16 @@ function wfIsHHVM() {
}
/**
+ * Check if we are running from the commandline
+ *
+ * @since 1.31
+ * @return bool
+ */
+function wfIsCLI() {
+ return PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg';
+}
+
+/**
* Tries to get the system directory for temporary files. First
* $wgTmpDirectory is checked, and then the TMPDIR, TMP, and TEMP
* environment variables are then checked in sequence, then
@@ -2146,9 +2083,9 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) {
}
// Turn off the normal warning, we're doing our own below
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ok = mkdir( $dir, $mode, true ); // PHP5 <3
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$ok ) {
// directory may have been created on another request since we last checked
@@ -2221,7 +2158,23 @@ function wfPercent( $nr, $acc = 2, $round = true ) {
* @return bool
*/
function wfIniGetBool( $setting ) {
- $val = strtolower( ini_get( $setting ) );
+ return wfStringToBool( ini_get( $setting ) );
+}
+
+/**
+ * Convert string value to boolean, when the following are interpreted as true:
+ * - on
+ * - true
+ * - yes
+ * - Any number, except 0
+ * All other strings are interpreted as false.
+ *
+ * @param string $val
+ * @return bool
+ * @since 1.31
+ */
+function wfStringToBool( $val ) {
+ $val = strtolower( $val );
// 'on' and 'true' can't have whitespace around them, but '1' can.
return $val == 'on'
|| $val == 'true'
@@ -2255,6 +2208,7 @@ function wfEscapeShellArg( /*...*/ ) {
* @deprecated since 1.30 use MediaWiki\Shell::isDisabled()
*/
function wfShellExecDisabled() {
+ wfDeprecated( __FUNCTION__, '1.30' );
return Shell::isDisabled() ? 'disabled' : false;
}
@@ -2304,6 +2258,8 @@ function wfShellExec( $cmd, &$retval = null, $environ = [],
->limits( $limits )
->includeStderr( $includeStderr )
->profileMethod( $profileMethod )
+ // For b/c
+ ->restrict( Shell::RESTRICT_NONE )
->execute();
} catch ( ProcOpenError $ex ) {
$retval = -1;
@@ -2346,6 +2302,7 @@ function wfShellExecWithStderr( $cmd, &$retval = null, $environ = [], $limits =
* @see $wgShellLocale
*/
function wfInitShellLocale() {
+ wfDeprecated( __FUNCTION__, '1.30' );
}
/**
@@ -2353,6 +2310,8 @@ function wfInitShellLocale() {
* Note that $parameters should be a flat array and an option with an argument
* should consist of two consecutive items in the array (do not use "--option value").
*
+ * @deprecated since 1.31, use Shell::makeScriptCommand()
+ *
* @param string $script MediaWiki cli script path
* @param array $parameters Arguments and options to the script
* @param array $options Associative array of options:
@@ -2382,16 +2341,17 @@ function wfShellWikiCmd( $script, array $parameters = [], array $options = [] )
* @param string $mine
* @param string $yours
* @param string &$result
+ * @param string &$mergeAttemptResult
* @return bool
*/
-function wfMerge( $old, $mine, $yours, &$result ) {
+function wfMerge( $old, $mine, $yours, &$result, &$mergeAttemptResult = null ) {
global $wgDiff3;
# This check may also protect against code injection in
# case of broken installations.
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$haveDiff3 ) {
wfDebug( "diff3 not found\n" );
@@ -2420,13 +2380,18 @@ function wfMerge( $old, $mine, $yours, &$result ) {
$oldtextName, $yourtextName );
$handle = popen( $cmd, 'r' );
- if ( fgets( $handle, 1024 ) ) {
- $conflict = true;
- } else {
- $conflict = false;
- }
+ $mergeAttemptResult = '';
+ do {
+ $data = fread( $handle, 8192 );
+ if ( strlen( $data ) == 0 ) {
+ break;
+ }
+ $mergeAttemptResult .= $data;
+ } while ( true );
pclose( $handle );
+ $conflict = $mergeAttemptResult !== '';
+
# Merge differences
$cmd = Shell::escape( $wgDiff3, '-a', '-e', '--merge', $mytextName,
$oldtextName, $yourtextName );
@@ -2468,9 +2433,9 @@ function wfDiff( $before, $after, $params = '-u' ) {
}
global $wgDiff;
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$haveDiff = $wgDiff && file_exists( $wgDiff );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
# This check may also protect against code injection in
# case of broken installations.
@@ -2549,6 +2514,7 @@ function wfDiff( $before, $after, $params = '-u' ) {
* @throws MWException
*/
function wfUsePHP( $req_ver ) {
+ wfDeprecated( __FUNCTION__, '1.30' );
$php_ver = PHP_VERSION;
if ( version_compare( $php_ver, (string)$req_ver, '<' ) ) {
@@ -2659,29 +2625,6 @@ function wfRelativePath( $path, $from ) {
}
/**
- * Convert an arbitrarily-long digit string from one numeric base
- * to another, optionally zero-padding to a minimum column width.
- *
- * Supports base 2 through 36; digit values 10-36 are represented
- * as lowercase letters a-z. Input is case-insensitive.
- *
- * @deprecated since 1.27 Use Wikimedia\base_convert() directly
- *
- * @param string $input Input number
- * @param int $sourceBase Base of the input number
- * @param int $destBase Desired base of the output
- * @param int $pad Minimum number of digits in the output (pad with zeroes)
- * @param bool $lowercase Whether to output in lowercase or uppercase
- * @param string $engine Either "gmp", "bcmath", or "php"
- * @return string|bool The output number as a string, or false on error
- */
-function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1,
- $lowercase = true, $engine = 'auto'
-) {
- return Wikimedia\base_convert( $input, $sourceBase, $destBase, $pad, $lowercase, $engine );
-}
-
-/**
* Reset the session id
*
* @deprecated since 1.27, use MediaWiki\Session\SessionManager instead
@@ -2725,7 +2668,7 @@ function wfSetupSession( $sessionId = false ) {
if ( session_id() !== $session->getId() ) {
session_id( $session->getId() );
}
- MediaWiki\quietCall( 'session_start' );
+ Wikimedia\quietCall( 'session_start' );
}
/**
@@ -2891,7 +2834,7 @@ function wfGetLBFactory() {
* Find a file.
* Shortcut for RepoGroup::singleton()->findFile()
*
- * @param string $title String or Title object
+ * @param string|Title $title String or Title object
* @param array $options Associative array of options (see RepoGroup::findFile)
* @return File|bool File, or false if the file does not exist
*/
@@ -3012,7 +2955,7 @@ function wfWaitForSlaves(
$ifWritesSince = null, $wiki = false, $cluster = false, $timeout = null
) {
if ( $timeout === null ) {
- $timeout = ( PHP_SAPI === 'cli' ) ? 86400 : 10;
+ $timeout = wfIsCLI() ? 60 : 10;
}
if ( $cluster === '*' ) {
@@ -3023,7 +2966,8 @@ function wfWaitForSlaves(
}
try {
- wfGetLBFactory()->waitForReplication( [
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $lbFactory->waitForReplication( [
'wiki' => $wiki,
'cluster' => $cluster,
'timeout' => $timeout,
@@ -3041,6 +2985,8 @@ function wfWaitForSlaves(
* Count down from $seconds to zero on the terminal, with a one-second pause
* between showing each number. For use in command-line scripts.
*
+ * @deprecated since 1.31, use Maintenance::countDown()
+ *
* @codeCoverageIgnore
* @param int $seconds
*/
@@ -3091,15 +3037,15 @@ function wfMemoryLimit() {
$conflimit = wfShorthandToInteger( $wgMemoryLimit );
if ( $conflimit == -1 ) {
wfDebug( "Removing PHP's memory limit\n" );
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
ini_set( 'memory_limit', $conflimit );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return $conflimit;
} elseif ( $conflimit > $memlimit ) {
wfDebug( "Raising PHP's memory limit to $conflimit bytes\n" );
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
ini_set( 'memory_limit', $conflimit );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return $conflimit;
}
}
@@ -3162,29 +3108,13 @@ function wfShorthandToInteger( $string = '', $default = -1 ) {
* See unit test for examples.
* See mediawiki.language.bcp47 for the JavaScript implementation.
*
+ * @deprecated since 1.31, use LanguageCode::bcp47() directly.
+ *
* @param string $code The language code.
* @return string The language code which complying with BCP 47 standards.
*/
function wfBCP47( $code ) {
- $codeSegment = explode( '-', $code );
- $codeBCP = [];
- foreach ( $codeSegment as $segNo => $seg ) {
- // when previous segment is x, it is a private segment and should be lc
- if ( $segNo > 0 && strtolower( $codeSegment[( $segNo - 1 )] ) == 'x' ) {
- $codeBCP[$segNo] = strtolower( $seg );
- // ISO 3166 country code
- } elseif ( ( strlen( $seg ) == 2 ) && ( $segNo > 0 ) ) {
- $codeBCP[$segNo] = strtoupper( $seg );
- // ISO 15924 script code
- } elseif ( ( strlen( $seg ) == 4 ) && ( $segNo > 0 ) ) {
- $codeBCP[$segNo] = ucfirst( strtolower( $seg ) );
- // Use lowercase for other cases
- } else {
- $codeBCP[$segNo] = strtolower( $seg );
- }
- }
- $langCode = implode( '-', $codeBCP );
- return $langCode;
+ return LanguageCode::bcp47( $code );
}
/**
@@ -3239,6 +3169,7 @@ function wfGetParserCacheStorage() {
* @deprecated since 1.25 - use Hooks::run
*/
function wfRunHooks( $event, array $args = [], $deprecatedVersion = null ) {
+ wfDeprecated( __METHOD__, '1.25' );
return Hooks::run( $event, $args, $deprecatedVersion );
}
@@ -3267,9 +3198,9 @@ function wfUnpack( $format, $data, $length = false ) {
}
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$result = unpack( $format, $data );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $result === false ) {
// If it cannot extract the packed data.
@@ -3493,3 +3424,21 @@ function wfArrayPlus2d( array $baseArray, array $newValues ) {
return $baseArray;
}
+
+/**
+ * Get system resource usage of current request context.
+ * Invokes the getrusage(2) system call, requesting RUSAGE_SELF if on PHP5
+ * or RUSAGE_THREAD if on HHVM. Returns false if getrusage is not available.
+ *
+ * @since 1.24
+ * @return array|bool Resource usage data or false if no data available.
+ */
+function wfGetRusage() {
+ if ( !function_exists( 'getrusage' ) ) {
+ return false;
+ } elseif ( defined( 'HHVM_VERSION' ) && PHP_OS === 'Linux' ) {
+ return getrusage( 2 /* RUSAGE_THREAD */ );
+ } else {
+ return getrusage( 0 /* RUSAGE_SELF */ );
+ }
+}
diff --git a/www/wiki/includes/HistoryBlob.php b/www/wiki/includes/HistoryBlob.php
index 51bd7a9e..1d4f6e4e 100644
--- a/www/wiki/includes/HistoryBlob.php
+++ b/www/wiki/includes/HistoryBlob.php
@@ -519,9 +519,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"
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$diff = xdiff_string_rabdiff( $t1, $t2 ) . '';
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return $diff;
}
@@ -532,9 +532,9 @@ class DiffHistoryBlob implements HistoryBlob {
*/
function patch( $base, $diff ) {
if ( function_exists( 'xdiff_string_bpatch' ) ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$text = xdiff_string_bpatch( $base, $diff ) . '';
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return $text;
}
@@ -560,26 +560,26 @@ class DiffHistoryBlob implements HistoryBlob {
$op = $x['op'];
++$p;
switch ( $op ) {
- case self::XDL_BDOP_INS:
- $x = unpack( 'Csize', substr( $diff, $p, 1 ) );
- $p++;
- $out .= substr( $diff, $p, $x['size'] );
- $p += $x['size'];
- break;
- case self::XDL_BDOP_INSB:
- $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) );
- $p += 4;
- $out .= substr( $diff, $p, $x['csize'] );
- $p += $x['csize'];
- break;
- case self::XDL_BDOP_CPY:
- $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) );
- $p += 8;
- $out .= substr( $base, $x['off'], $x['csize'] );
- break;
- default:
- wfDebug( __METHOD__ . ": invalid op\n" );
- return false;
+ case self::XDL_BDOP_INS:
+ $x = unpack( 'Csize', substr( $diff, $p, 1 ) );
+ $p++;
+ $out .= substr( $diff, $p, $x['size'] );
+ $p += $x['size'];
+ break;
+ case self::XDL_BDOP_INSB:
+ $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) );
+ $p += 4;
+ $out .= substr( $diff, $p, $x['csize'] );
+ $p += $x['csize'];
+ break;
+ case self::XDL_BDOP_CPY:
+ $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) );
+ $p += 8;
+ $out .= substr( $base, $x['off'], $x['csize'] );
+ break;
+ default:
+ wfDebug( __METHOD__ . ": invalid op\n" );
+ return false;
}
}
return $out;
@@ -699,3 +699,15 @@ class DiffHistoryBlob implements HistoryBlob {
}
}
+
+// phpcs:ignore Generic.CodeAnalysis.UnconditionalIfStatement.Found
+if ( false ) {
+ // Blobs generated by MediaWiki < 1.5 on PHP 4 were serialized with the
+ // class name coerced to lowercase. We can improve efficiency by adding
+ // autoload entries for the lowercase variants of these classes (T166759).
+ // The code below is never executed, but it is picked up by the AutoloadGenerator
+ // parser, which scans for class_alias() calls.
+ class_alias( ConcatenatedGzipHistoryBlob::class, 'concatenatedgziphistoryblob' );
+ class_alias( HistoryBlobCurStub::class, 'historyblobcurstub' );
+ class_alias( HistoryBlobStub::class, 'historyblobstub' );
+}
diff --git a/www/wiki/includes/Html.php b/www/wiki/includes/Html.php
index 0988b054..3bcf1313 100644
--- a/www/wiki/includes/Html.php
+++ b/www/wiki/includes/Html.php
@@ -589,9 +589,12 @@ class Html {
*
* @param string $contents CSS
* @param string $media A media type string, like 'screen'
+ * @param array $attribs (since 1.31) Associative array of attributes, e.g., [
+ * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for
+ * further documentation.
* @return string Raw HTML
*/
- public static function inlineStyle( $contents, $media = 'all' ) {
+ public static function inlineStyle( $contents, $media = 'all', $attribs = [] ) {
// Don't escape '>' since that is used
// as direct child selector.
// Remember, in css, there is no "x" for hexadecimal escapes, and
@@ -609,7 +612,7 @@ class Html {
return self::rawElement( 'style', [
'media' => $media,
- ], $contents );
+ ] + $attribs, $contents );
}
/**
@@ -676,6 +679,52 @@ class Html {
}
/**
+ * Return the HTML for a message box.
+ * @since 1.31
+ * @param string $html of contents of box
+ * @param string $className corresponding to box
+ * @param string $heading (optional)
+ * @return string of HTML representing a box.
+ */
+ private static function messageBox( $html, $className, $heading = '' ) {
+ if ( $heading ) {
+ $html = self::element( 'h2', [], $heading ) . $html;
+ }
+ return self::rawElement( 'div', [ 'class' => $className ], $html );
+ }
+
+ /**
+ * Return a warning box.
+ * @since 1.31
+ * @param string $html of contents of box
+ * @return string of HTML representing a warning box.
+ */
+ public static function warningBox( $html ) {
+ return self::messageBox( $html, 'warningbox' );
+ }
+
+ /**
+ * Return an error box.
+ * @since 1.31
+ * @param string $html of contents of error box
+ * @param string $heading (optional)
+ * @return string of HTML representing an error box.
+ */
+ public static function errorBox( $html, $heading = '' ) {
+ return self::messageBox( $html, 'errorbox', $heading );
+ }
+
+ /**
+ * Return a success box.
+ * @since 1.31
+ * @param string $html of contents of box
+ * @return string of HTML representing a success box.
+ */
+ public static function successBox( $html ) {
+ return self::messageBox( $html, 'successbox' );
+ }
+
+ /**
* Convenience function to produce a radio button (input element with type=radio)
*
* @param string $name Name attribute
diff --git a/www/wiki/includes/HttpFunctions.php b/www/wiki/includes/HttpFunctions.php
deleted file mode 100644
index 694bbb5f..00000000
--- a/www/wiki/includes/HttpFunctions.php
+++ /dev/null
@@ -1,1122 +0,0 @@
-<?php
-/**
- * Various HTTP related functions.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup HTTP
- */
-
-/**
- * @defgroup HTTP HTTP
- */
-
-use MediaWiki\Logger\LoggerFactory;
-
-/**
- * Various HTTP related functions
- * @ingroup HTTP
- */
-class Http {
- static public $httpEngine = false;
-
- /**
- * Perform an HTTP request
- *
- * @param string $method HTTP method. Usually GET/POST
- * @param string $url Full URL to act on. If protocol-relative, will be expanded to an http:// URL
- * @param array $options Options to pass to MWHttpRequest object.
- * Possible keys for the array:
- * - timeout Timeout length in seconds
- * - connectTimeout Timeout for connection, in seconds (curl only)
- * - postData An array of key-value pairs or a url-encoded form data
- * - proxy The proxy to use.
- * Otherwise it will use $wgHTTPProxy (if set)
- * Otherwise it will use the environment variable "http_proxy" (if set)
- * - noProxy Don't use any proxy at all. Takes precedence over proxy value(s).
- * - sslVerifyHost Verify hostname against certificate
- * - sslVerifyCert Verify SSL certificate
- * - caInfo Provide CA information
- * - maxRedirects Maximum number of redirects to follow (defaults to 5)
- * - followRedirects Whether to follow redirects (defaults to false).
- * Note: this should only be used when the target URL is trusted,
- * to avoid attacks on intranet services accessible by HTTP.
- * - userAgent A user agent, if you want to override the default
- * MediaWiki/$wgVersion
- * @param string $caller The method making this request, for profiling
- * @return string|bool (bool)false on failure or a string on success
- */
- public static function request( $method, $url, $options = [], $caller = __METHOD__ ) {
- wfDebug( "HTTP: $method: $url\n" );
-
- $options['method'] = strtoupper( $method );
-
- if ( !isset( $options['timeout'] ) ) {
- $options['timeout'] = 'default';
- }
- if ( !isset( $options['connectTimeout'] ) ) {
- $options['connectTimeout'] = 'default';
- }
-
- $req = MWHttpRequest::factory( $url, $options, $caller );
- $status = $req->execute();
-
- if ( $status->isOK() ) {
- return $req->getContent();
- } else {
- $errors = $status->getErrorsByType( 'error' );
- $logger = LoggerFactory::getInstance( 'http' );
- $logger->warning( $status->getWikiText( false, false, 'en' ),
- [ 'error' => $errors, 'caller' => $caller, 'content' => $req->getContent() ] );
- return false;
- }
- }
-
- /**
- * Simple wrapper for Http::request( 'GET' )
- * @see Http::request()
- * @since 1.25 Second parameter $timeout removed. Second parameter
- * is now $options which can be given a 'timeout'
- *
- * @param string $url
- * @param array $options
- * @param string $caller The method making this request, for profiling
- * @return string|bool false on error
- */
- public static function get( $url, $options = [], $caller = __METHOD__ ) {
- $args = func_get_args();
- if ( isset( $args[1] ) && ( is_string( $args[1] ) || is_numeric( $args[1] ) ) ) {
- // Second was used to be the timeout
- // And third parameter used to be $options
- wfWarn( "Second parameter should not be a timeout.", 2 );
- $options = isset( $args[2] ) && is_array( $args[2] ) ?
- $args[2] : [];
- $options['timeout'] = $args[1];
- $caller = __METHOD__;
- }
- return Http::request( 'GET', $url, $options, $caller );
- }
-
- /**
- * Simple wrapper for Http::request( 'POST' )
- * @see Http::request()
- *
- * @param string $url
- * @param array $options
- * @param string $caller The method making this request, for profiling
- * @return string|bool false on error
- */
- public static function post( $url, $options = [], $caller = __METHOD__ ) {
- return Http::request( 'POST', $url, $options, $caller );
- }
-
- /**
- * Check if the URL can be served by localhost
- *
- * @param string $url Full url to check
- * @return bool
- */
- public static function isLocalURL( $url ) {
- global $wgCommandLineMode, $wgLocalVirtualHosts;
-
- if ( $wgCommandLineMode ) {
- return false;
- }
-
- // Extract host part
- $matches = [];
- if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) {
- $host = $matches[1];
- // Split up dotwise
- $domainParts = explode( '.', $host );
- // Check if this domain or any superdomain is listed as a local virtual host
- $domainParts = array_reverse( $domainParts );
-
- $domain = '';
- $countParts = count( $domainParts );
- for ( $i = 0; $i < $countParts; $i++ ) {
- $domainPart = $domainParts[$i];
- if ( $i == 0 ) {
- $domain = $domainPart;
- } else {
- $domain = $domainPart . '.' . $domain;
- }
-
- if ( in_array( $domain, $wgLocalVirtualHosts ) ) {
- return true;
- }
- }
- }
-
- return false;
- }
-
- /**
- * A standard user-agent we can use for external requests.
- * @return string
- */
- public static function userAgent() {
- global $wgVersion;
- return "MediaWiki/$wgVersion";
- }
-
- /**
- * Checks that the given URI is a valid one. Hardcoding the
- * protocols, because we only want protocols that both cURL
- * and php support.
- *
- * file:// should not be allowed here for security purpose (r67684)
- *
- * @todo FIXME this is wildly inaccurate and fails to actually check most stuff
- *
- * @param string $uri URI to check for validity
- * @return bool
- */
- public static function isValidURI( $uri ) {
- return preg_match(
- '/^https?:\/\/[^\/\s]\S*$/D',
- $uri
- );
- }
-
- /**
- * Gets the relevant proxy from $wgHTTPProxy
- *
- * @return mixed The proxy address or an empty string if not set.
- */
- public static function getProxy() {
- global $wgHTTPProxy;
-
- if ( $wgHTTPProxy ) {
- return $wgHTTPProxy;
- }
-
- return "";
- }
-}
-
-/**
- * This wrapper class will call out to curl (if available) or fallback
- * to regular PHP if necessary for handling internal HTTP requests.
- *
- * Renamed from HttpRequest to MWHttpRequest to avoid conflict with
- * PHP's HTTP extension.
- */
-class MWHttpRequest {
- const SUPPORTS_FILE_POSTS = false;
-
- protected $content;
- protected $timeout = 'default';
- protected $headersOnly = null;
- protected $postData = null;
- protected $proxy = null;
- protected $noProxy = false;
- protected $sslVerifyHost = true;
- protected $sslVerifyCert = true;
- protected $caInfo = null;
- protected $method = "GET";
- protected $reqHeaders = [];
- protected $url;
- protected $parsedUrl;
- protected $callback;
- protected $maxRedirects = 5;
- protected $followRedirects = false;
-
- /**
- * @var CookieJar
- */
- protected $cookieJar;
-
- protected $headerList = [];
- protected $respVersion = "0.9";
- protected $respStatus = "200 Ok";
- protected $respHeaders = [];
-
- public $status;
-
- /**
- * @var Profiler
- */
- protected $profiler;
-
- /**
- * @var string
- */
- protected $profileName;
-
- /**
- * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL
- * @param array $options (optional) extra params to pass (see Http::request())
- * @param string $caller The method making this request, for profiling
- * @param Profiler $profiler An instance of the profiler for profiling, or null
- */
- protected function __construct(
- $url, $options = [], $caller = __METHOD__, $profiler = null
- ) {
- global $wgHTTPTimeout, $wgHTTPConnectTimeout;
-
- $this->url = wfExpandUrl( $url, PROTO_HTTP );
- $this->parsedUrl = wfParseUrl( $this->url );
-
- if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) {
- $this->status = Status::newFatal( 'http-invalid-url', $url );
- } else {
- $this->status = Status::newGood( 100 ); // continue
- }
-
- if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) {
- $this->timeout = $options['timeout'];
- } else {
- $this->timeout = $wgHTTPTimeout;
- }
- if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) {
- $this->connectTimeout = $options['connectTimeout'];
- } else {
- $this->connectTimeout = $wgHTTPConnectTimeout;
- }
- if ( isset( $options['userAgent'] ) ) {
- $this->setUserAgent( $options['userAgent'] );
- }
-
- $members = [ "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
- "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ];
-
- foreach ( $members as $o ) {
- if ( isset( $options[$o] ) ) {
- // ensure that MWHttpRequest::method is always
- // uppercased. Bug 36137
- if ( $o == 'method' ) {
- $options[$o] = strtoupper( $options[$o] );
- }
- $this->$o = $options[$o];
- }
- }
-
- if ( $this->noProxy ) {
- $this->proxy = ''; // noProxy takes precedence
- }
-
- // Profile based on what's calling us
- $this->profiler = $profiler;
- $this->profileName = $caller;
- }
-
- /**
- * Simple function to test if we can make any sort of requests at all, using
- * cURL or fopen()
- * @return bool
- */
- public static function canMakeRequests() {
- return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' );
- }
-
- /**
- * Generate a new request object
- * @param string $url Url to use
- * @param array $options (optional) extra params to pass (see Http::request())
- * @param string $caller The method making this request, for profiling
- * @throws MWException
- * @return CurlHttpRequest|PhpHttpRequest
- * @see MWHttpRequest::__construct
- */
- public static function factory( $url, $options = null, $caller = __METHOD__ ) {
- if ( !Http::$httpEngine ) {
- Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
- } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
- throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' .
- ' Http::$httpEngine is set to "curl"' );
- }
-
- switch ( Http::$httpEngine ) {
- case 'curl':
- return new CurlHttpRequest( $url, $options, $caller, Profiler::instance() );
- case 'php':
- if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
- throw new MWException( __METHOD__ . ': allow_url_fopen ' .
- 'needs to be enabled for pure PHP http requests to ' .
- 'work. If possible, curl should be used instead. See ' .
- 'http://php.net/curl.'
- );
- }
- return new PhpHttpRequest( $url, $options, $caller, Profiler::instance() );
- default:
- throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' );
- }
- }
-
- /**
- * Get the body, or content, of the response to the request
- *
- * @return string
- */
- public function getContent() {
- return $this->content;
- }
-
- /**
- * Set the parameters of the request
- *
- * @param array $args
- * @todo overload the args param
- */
- public function setData( $args ) {
- $this->postData = $args;
- }
-
- /**
- * Take care of setting up the proxy (do nothing if "noProxy" is set)
- *
- * @return void
- */
- public function proxySetup() {
- // If there is an explicit proxy set and proxies are not disabled, then use it
- if ( $this->proxy && !$this->noProxy ) {
- return;
- }
-
- // Otherwise, fallback to $wgHTTPProxy if this is not a machine
- // local URL and proxies are not disabled
- if ( Http::isLocalURL( $this->url ) || $this->noProxy ) {
- $this->proxy = '';
- } else {
- $this->proxy = Http::getProxy();
- }
- }
-
- /**
- * Set the user agent
- * @param string $UA
- */
- public function setUserAgent( $UA ) {
- $this->setHeader( 'User-Agent', $UA );
- }
-
- /**
- * Set an arbitrary header
- * @param string $name
- * @param string $value
- */
- public function setHeader( $name, $value ) {
- // I feel like I should normalize the case here...
- $this->reqHeaders[$name] = $value;
- }
-
- /**
- * Get an array of the headers
- * @return array
- */
- public function getHeaderList() {
- $list = [];
-
- if ( $this->cookieJar ) {
- $this->reqHeaders['Cookie'] =
- $this->cookieJar->serializeToHttpRequest(
- $this->parsedUrl['path'],
- $this->parsedUrl['host']
- );
- }
-
- foreach ( $this->reqHeaders as $name => $value ) {
- $list[] = "$name: $value";
- }
-
- return $list;
- }
-
- /**
- * Set a read callback to accept data read from the HTTP request.
- * By default, data is appended to an internal buffer which can be
- * retrieved through $req->getContent().
- *
- * To handle data as it comes in -- especially for large files that
- * would not fit in memory -- you can instead set your own callback,
- * in the form function($resource, $buffer) where the first parameter
- * is the low-level resource being read (implementation specific),
- * and the second parameter is the data buffer.
- *
- * You MUST return the number of bytes handled in the buffer; if fewer
- * bytes are reported handled than were passed to you, the HTTP fetch
- * will be aborted.
- *
- * @param callable $callback
- * @throws MWException
- */
- public function setCallback( $callback ) {
- if ( !is_callable( $callback ) ) {
- throw new MWException( 'Invalid MwHttpRequest callback' );
- }
- $this->callback = $callback;
- }
-
- /**
- * A generic callback to read the body of the response from a remote
- * server.
- *
- * @param resource $fh
- * @param string $content
- * @return int
- */
- public function read( $fh, $content ) {
- $this->content .= $content;
- return strlen( $content );
- }
-
- /**
- * Take care of whatever is necessary to perform the URI request.
- *
- * @return Status
- */
- public function execute() {
-
- $this->content = "";
-
- if ( strtoupper( $this->method ) == "HEAD" ) {
- $this->headersOnly = true;
- }
-
- $this->proxySetup(); // set up any proxy as needed
-
- if ( !$this->callback ) {
- $this->setCallback( [ $this, 'read' ] );
- }
-
- if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
- $this->setUserAgent( Http::userAgent() );
- }
-
- }
-
- /**
- * Parses the headers, including the HTTP status code and any
- * Set-Cookie headers. This function expects the headers to be
- * found in an array in the member variable headerList.
- */
- protected function parseHeader() {
-
- $lastname = "";
-
- foreach ( $this->headerList as $header ) {
- if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
- $this->respVersion = $match[1];
- $this->respStatus = $match[2];
- } elseif ( preg_match( "#^[ \t]#", $header ) ) {
- $last = count( $this->respHeaders[$lastname] ) - 1;
- $this->respHeaders[$lastname][$last] .= "\r\n$header";
- } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
- $this->respHeaders[strtolower( $match[1] )][] = $match[2];
- $lastname = strtolower( $match[1] );
- }
- }
-
- $this->parseCookies();
-
- }
-
- /**
- * Sets HTTPRequest status member to a fatal value with the error
- * message if the returned integer value of the status code was
- * not successful (< 300) or a redirect (>=300 and < 400). (see
- * RFC2616, section 10,
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for a
- * list of status codes.)
- */
- protected function setStatus() {
- if ( !$this->respHeaders ) {
- $this->parseHeader();
- }
-
- if ( (int)$this->respStatus > 399 ) {
- list( $code, $message ) = explode( " ", $this->respStatus, 2 );
- $this->status->fatal( "http-bad-status", $code, $message );
- }
- }
-
- /**
- * Get the integer value of the HTTP status code (e.g. 200 for "200 Ok")
- * (see RFC2616, section 10, http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
- * for a list of status codes.)
- *
- * @return int
- */
- public function getStatus() {
- if ( !$this->respHeaders ) {
- $this->parseHeader();
- }
-
- return (int)$this->respStatus;
- }
-
- /**
- * Returns true if the last status code was a redirect.
- *
- * @return bool
- */
- public function isRedirect() {
- if ( !$this->respHeaders ) {
- $this->parseHeader();
- }
-
- $status = (int)$this->respStatus;
-
- if ( $status >= 300 && $status <= 303 ) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Returns an associative array of response headers after the
- * request has been executed. Because some headers
- * (e.g. Set-Cookie) can appear more than once the, each value of
- * the associative array is an array of the values given.
- *
- * @return array
- */
- public function getResponseHeaders() {
- if ( !$this->respHeaders ) {
- $this->parseHeader();
- }
-
- return $this->respHeaders;
- }
-
- /**
- * Returns the value of the given response header.
- *
- * @param string $header
- * @return string
- */
- public function getResponseHeader( $header ) {
- if ( !$this->respHeaders ) {
- $this->parseHeader();
- }
-
- if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
- $v = $this->respHeaders[strtolower( $header )];
- return $v[count( $v ) - 1];
- }
-
- return null;
- }
-
- /**
- * Tells the MWHttpRequest object to use this pre-loaded CookieJar.
- *
- * @param CookieJar $jar
- */
- public function setCookieJar( $jar ) {
- $this->cookieJar = $jar;
- }
-
- /**
- * Returns the cookie jar in use.
- *
- * @return CookieJar
- */
- public function getCookieJar() {
- if ( !$this->respHeaders ) {
- $this->parseHeader();
- }
-
- return $this->cookieJar;
- }
-
- /**
- * Sets a cookie. Used before a request to set up any individual
- * cookies. Used internally after a request to parse the
- * Set-Cookie headers.
- * @see Cookie::set
- * @param string $name
- * @param mixed $value
- * @param array $attr
- */
- public function setCookie( $name, $value = null, $attr = null ) {
- if ( !$this->cookieJar ) {
- $this->cookieJar = new CookieJar;
- }
-
- $this->cookieJar->setCookie( $name, $value, $attr );
- }
-
- /**
- * Parse the cookies in the response headers and store them in the cookie jar.
- */
- protected function parseCookies() {
-
- if ( !$this->cookieJar ) {
- $this->cookieJar = new CookieJar;
- }
-
- if ( isset( $this->respHeaders['set-cookie'] ) ) {
- $url = parse_url( $this->getFinalUrl() );
- foreach ( $this->respHeaders['set-cookie'] as $cookie ) {
- $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
- }
- }
-
- }
-
- /**
- * Returns the final URL after all redirections.
- *
- * Relative values of the "Location" header are incorrect as
- * stated in RFC, however they do happen and modern browsers
- * support them. This function loops backwards through all
- * locations in order to build the proper absolute URI - Marooned
- * at wikia-inc.com
- *
- * Note that the multiple Location: headers are an artifact of
- * CURL -- they shouldn't actually get returned this way. Rewrite
- * this when bug 29232 is taken care of (high-level redirect
- * handling rewrite).
- *
- * @return string
- */
- public function getFinalUrl() {
- $headers = $this->getResponseHeaders();
-
- // return full url (fix for incorrect but handled relative location)
- if ( isset( $headers['location'] ) ) {
- $locations = $headers['location'];
- $domain = '';
- $foundRelativeURI = false;
- $countLocations = count( $locations );
-
- for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
- $url = parse_url( $locations[$i] );
-
- if ( isset( $url['host'] ) ) {
- $domain = $url['scheme'] . '://' . $url['host'];
- break; // found correct URI (with host)
- } else {
- $foundRelativeURI = true;
- }
- }
-
- if ( $foundRelativeURI ) {
- if ( $domain ) {
- return $domain . $locations[$countLocations - 1];
- } else {
- $url = parse_url( $this->url );
- if ( isset( $url['host'] ) ) {
- return $url['scheme'] . '://' . $url['host'] .
- $locations[$countLocations - 1];
- }
- }
- } else {
- return $locations[$countLocations - 1];
- }
- }
-
- return $this->url;
- }
-
- /**
- * Returns true if the backend can follow redirects. Overridden by the
- * child classes.
- * @return bool
- */
- public function canFollowRedirects() {
- return true;
- }
-}
-
-/**
- * MWHttpRequest implemented using internal curl compiled into PHP
- */
-class CurlHttpRequest extends MWHttpRequest {
- const SUPPORTS_FILE_POSTS = true;
-
- protected $curlOptions = [];
- protected $headerText = "";
-
- /**
- * @param resource $fh
- * @param string $content
- * @return int
- */
- protected function readHeader( $fh, $content ) {
- $this->headerText .= $content;
- return strlen( $content );
- }
-
- public function execute() {
-
- parent::execute();
-
- if ( !$this->status->isOK() ) {
- return $this->status;
- }
-
- $this->curlOptions[CURLOPT_PROXY] = $this->proxy;
- $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout;
-
- // Only supported in curl >= 7.16.2
- if ( defined( 'CURLOPT_CONNECTTIMEOUT_MS' ) ) {
- $this->curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = $this->connectTimeout * 1000;
- }
-
- $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
- $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback;
- $this->curlOptions[CURLOPT_HEADERFUNCTION] = [ $this, "readHeader" ];
- $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects;
- $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression
-
- $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent'];
-
- $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost ? 2 : 0;
- $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert;
-
- if ( $this->caInfo ) {
- $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo;
- }
-
- if ( $this->headersOnly ) {
- $this->curlOptions[CURLOPT_NOBODY] = true;
- $this->curlOptions[CURLOPT_HEADER] = true;
- } elseif ( $this->method == 'POST' ) {
- $this->curlOptions[CURLOPT_POST] = true;
- $postData = $this->postData;
- // Don't interpret POST parameters starting with '@' as file uploads, because this
- // makes it impossible to POST plain values starting with '@' (and causes security
- // issues potentially exposing the contents of local files).
- // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6,
- // but we support lower versions, and the option doesn't exist in HHVM 5.6.99.
- if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) {
- $this->curlOptions[CURLOPT_SAFE_UPLOAD] = true;
- } elseif ( is_array( $postData ) ) {
- // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS
- // is an array, but not if it's a string. So convert $req['body'] to a string
- // for safety.
- $postData = wfArrayToCgi( $postData );
- }
- $this->curlOptions[CURLOPT_POSTFIELDS] = $postData;
-
- // Suppress 'Expect: 100-continue' header, as some servers
- // will reject it with a 417 and Curl won't auto retry
- // with HTTP 1.0 fallback
- $this->reqHeaders['Expect'] = '';
- } else {
- $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method;
- }
-
- $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList();
-
- $curlHandle = curl_init( $this->url );
-
- if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) {
- throw new MWException( "Error setting curl options." );
- }
-
- if ( $this->followRedirects && $this->canFollowRedirects() ) {
- MediaWiki\suppressWarnings();
- if ( !curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) {
- wfDebug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " .
- "Probably open_basedir is set.\n" );
- // Continue the processing. If it were in curl_setopt_array,
- // processing would have halted on its entry
- }
- MediaWiki\restoreWarnings();
- }
-
- if ( $this->profiler ) {
- $profileSection = $this->profiler->scopedProfileIn(
- __METHOD__ . '-' . $this->profileName
- );
- }
-
- $curlRes = curl_exec( $curlHandle );
- if ( curl_errno( $curlHandle ) == CURLE_OPERATION_TIMEOUTED ) {
- $this->status->fatal( 'http-timed-out', $this->url );
- } elseif ( $curlRes === false ) {
- $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) );
- } else {
- $this->headerList = explode( "\r\n", $this->headerText );
- }
-
- curl_close( $curlHandle );
-
- if ( $this->profiler ) {
- $this->profiler->scopedProfileOut( $profileSection );
- }
-
- $this->parseHeader();
- $this->setStatus();
-
- return $this->status;
- }
-
- /**
- * @return bool
- */
- public function canFollowRedirects() {
- $curlVersionInfo = curl_version();
- if ( $curlVersionInfo['version_number'] < 0x071304 ) {
- wfDebug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" );
- return false;
- }
-
- if ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) {
- if ( strval( ini_get( 'open_basedir' ) ) !== '' ) {
- wfDebug( "Cannot follow redirects when open_basedir is set\n" );
- return false;
- }
- }
-
- return true;
- }
-}
-
-class PhpHttpRequest extends MWHttpRequest {
-
- private $fopenErrors = [];
-
- /**
- * @param string $url
- * @return string
- */
- protected function urlToTcp( $url ) {
- $parsedUrl = parse_url( $url );
-
- return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
- }
-
- /**
- * Returns an array with a 'capath' or 'cafile' key
- * that is suitable to be merged into the 'ssl' sub-array of
- * a stream context options array.
- * Uses the 'caInfo' option of the class if it is provided, otherwise uses the system
- * default CA bundle if PHP supports that, or searches a few standard locations.
- * @return array
- * @throws DomainException
- */
- protected function getCertOptions() {
- $certOptions = [];
- $certLocations = [];
- if ( $this->caInfo ) {
- $certLocations = [ 'manual' => $this->caInfo ];
- } elseif ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) {
- // @codingStandardsIgnoreStart Generic.Files.LineLength
- // Default locations, based on
- // https://www.happyassassin.net/2015/01/12/a-note-about-ssltls-trusted-certificate-stores-and-platforms/
- // PHP 5.5 and older doesn't have any defaults, so we try to guess ourselves.
- // PHP 5.6+ gets the CA location from OpenSSL as long as it is not set manually,
- // so we should leave capath/cafile empty there.
- // @codingStandardsIgnoreEnd
- $certLocations = array_filter( [
- getenv( 'SSL_CERT_DIR' ),
- getenv( 'SSL_CERT_PATH' ),
- '/etc/pki/tls/certs/ca-bundle.crt', # Fedora et al
- '/etc/ssl/certs', # Debian et al
- '/etc/pki/tls/certs/ca-bundle.trust.crt',
- '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem',
- '/System/Library/OpenSSL', # OSX
- ] );
- }
-
- foreach ( $certLocations as $key => $cert ) {
- if ( is_dir( $cert ) ) {
- $certOptions['capath'] = $cert;
- break;
- } elseif ( is_file( $cert ) ) {
- $certOptions['cafile'] = $cert;
- break;
- } elseif ( $key === 'manual' ) {
- // fail more loudly if a cert path was manually configured and it is not valid
- throw new DomainException( "Invalid CA info passed: $cert" );
- }
- }
-
- return $certOptions;
- }
-
- /**
- * Custom error handler for dealing with fopen() errors.
- * fopen() tends to fire multiple errors in succession, and the last one
- * is completely useless (something like "fopen: failed to open stream")
- * so normal methods of handling errors programmatically
- * like get_last_error() don't work.
- */
- public function errorHandler( $errno, $errstr ) {
- $n = count( $this->fopenErrors ) + 1;
- $this->fopenErrors += [ "errno$n" => $errno, "errstr$n" => $errstr ];
- }
-
- public function execute() {
-
- parent::execute();
-
- if ( is_array( $this->postData ) ) {
- $this->postData = wfArrayToCgi( $this->postData );
- }
-
- if ( $this->parsedUrl['scheme'] != 'http'
- && $this->parsedUrl['scheme'] != 'https' ) {
- $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] );
- }
-
- $this->reqHeaders['Accept'] = "*/*";
- $this->reqHeaders['Connection'] = 'Close';
- if ( $this->method == 'POST' ) {
- // Required for HTTP 1.0 POSTs
- $this->reqHeaders['Content-Length'] = strlen( $this->postData );
- if ( !isset( $this->reqHeaders['Content-Type'] ) ) {
- $this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded";
- }
- }
-
- // Set up PHP stream context
- $options = [
- 'http' => [
- 'method' => $this->method,
- 'header' => implode( "\r\n", $this->getHeaderList() ),
- 'protocol_version' => '1.1',
- 'max_redirects' => $this->followRedirects ? $this->maxRedirects : 0,
- 'ignore_errors' => true,
- 'timeout' => $this->timeout,
- // Curl options in case curlwrappers are installed
- 'curl_verify_ssl_host' => $this->sslVerifyHost ? 2 : 0,
- 'curl_verify_ssl_peer' => $this->sslVerifyCert,
- ],
- 'ssl' => [
- 'verify_peer' => $this->sslVerifyCert,
- 'SNI_enabled' => true,
- 'ciphers' => 'HIGH:!SSLv2:!SSLv3:-ADH:-kDH:-kECDH:-DSS',
- 'disable_compression' => true,
- ],
- ];
-
- if ( $this->proxy ) {
- $options['http']['proxy'] = $this->urlToTcp( $this->proxy );
- $options['http']['request_fulluri'] = true;
- }
-
- if ( $this->postData ) {
- $options['http']['content'] = $this->postData;
- }
-
- if ( $this->sslVerifyHost ) {
- // PHP 5.6.0 deprecates CN_match, in favour of peer_name which
- // actually checks SubjectAltName properly.
- if ( version_compare( PHP_VERSION, '5.6.0', '>=' ) ) {
- $options['ssl']['peer_name'] = $this->parsedUrl['host'];
- } else {
- $options['ssl']['CN_match'] = $this->parsedUrl['host'];
- }
- }
-
- $options['ssl'] += $this->getCertOptions();
-
- $context = stream_context_create( $options );
-
- $this->headerList = [];
- $reqCount = 0;
- $url = $this->url;
-
- $result = [];
-
- if ( $this->profiler ) {
- $profileSection = $this->profiler->scopedProfileIn(
- __METHOD__ . '-' . $this->profileName
- );
- }
- do {
- $reqCount++;
- $this->fopenErrors = [];
- set_error_handler( [ $this, 'errorHandler' ] );
- $fh = fopen( $url, "r", false, $context );
- restore_error_handler();
-
- if ( !$fh ) {
- // HACK for instant commons.
- // If we are contacting (commons|upload).wikimedia.org
- // try again with CN_match for en.wikipedia.org
- // as php does not handle SubjectAltName properly
- // prior to "peer_name" option in php 5.6
- if ( isset( $options['ssl']['CN_match'] )
- && ( $options['ssl']['CN_match'] === 'commons.wikimedia.org'
- || $options['ssl']['CN_match'] === 'upload.wikimedia.org' )
- ) {
- $options['ssl']['CN_match'] = 'en.wikipedia.org';
- $context = stream_context_create( $options );
- continue;
- }
- break;
- }
-
- $result = stream_get_meta_data( $fh );
- $this->headerList = $result['wrapper_data'];
- $this->parseHeader();
-
- if ( !$this->followRedirects ) {
- break;
- }
-
- # Handle manual redirection
- if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) {
- break;
- }
- # Check security of URL
- $url = $this->getResponseHeader( "Location" );
-
- if ( !Http::isValidURI( $url ) ) {
- wfDebug( __METHOD__ . ": insecure redirection\n" );
- break;
- }
- } while ( true );
- if ( $this->profiler ) {
- $this->profiler->scopedProfileOut( $profileSection );
- }
-
- $this->setStatus();
-
- if ( $fh === false ) {
- if ( $this->fopenErrors ) {
- LoggerFactory::getInstance( 'http' )->warning( __CLASS__
- . ': error opening connection: {errstr1}', $this->fopenErrors );
- }
- $this->status->fatal( 'http-request-error' );
- return $this->status;
- }
-
- if ( $result['timed_out'] ) {
- $this->status->fatal( 'http-timed-out', $this->url );
- return $this->status;
- }
-
- // If everything went OK, or we received some error code
- // get the response body content.
- if ( $this->status->isOK() || (int)$this->respStatus >= 300 ) {
- while ( !feof( $fh ) ) {
- $buf = fread( $fh, 8192 );
-
- if ( $buf === false ) {
- $this->status->fatal( 'http-read-error' );
- break;
- }
-
- if ( strlen( $buf ) ) {
- call_user_func( $this->callback, $fh, $buf );
- }
- }
- }
- fclose( $fh );
-
- return $this->status;
- }
-}
diff --git a/www/wiki/includes/Licenses.php b/www/wiki/includes/Licenses.php
deleted file mode 100644
index da1a8da6..00000000
--- a/www/wiki/includes/Licenses.php
+++ /dev/null
@@ -1,210 +0,0 @@
-<?php
-/**
- * License selector for use on Special:Upload.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup SpecialPage
- * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
- * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-
-/**
- * A License class for use on Special:Upload
- */
-class Licenses extends HTMLFormField {
- /** @var string */
- protected $msg;
-
- /** @var array */
- protected $licenses = [];
-
- /** @var string */
- protected $html;
- /**#@-*/
-
- /**
- * @param array $params
- */
- public function __construct( $params ) {
- parent::__construct( $params );
-
- $this->msg = empty( $params['licenses'] )
- ? wfMessage( 'licenses' )->inContentLanguage()->plain()
- : $params['licenses'];
- $this->selected = null;
-
- $this->makeLicenses();
- }
-
- /**
- * @private
- */
- protected function makeLicenses() {
- $levels = [];
- $lines = explode( "\n", $this->msg );
-
- foreach ( $lines as $line ) {
- if ( strpos( $line, '*' ) !== 0 ) {
- continue;
- } else {
- list( $level, $line ) = $this->trimStars( $line );
-
- if ( strpos( $line, '|' ) !== false ) {
- $obj = new License( $line );
- $this->stackItem( $this->licenses, $levels, $obj );
- } else {
- if ( $level < count( $levels ) ) {
- $levels = array_slice( $levels, 0, $level );
- }
- if ( $level == count( $levels ) ) {
- $levels[$level - 1] = $line;
- } elseif ( $level > count( $levels ) ) {
- $levels[] = $line;
- }
- }
- }
- }
- }
-
- /**
- * @param string $str
- * @return array
- */
- protected function trimStars( $str ) {
- $numStars = strspn( $str, '*' );
- return [ $numStars, ltrim( substr( $str, $numStars ), ' ' ) ];
- }
-
- /**
- * @param array $list
- * @param array $path
- * @param mixed $item
- */
- protected function stackItem( &$list, $path, $item ) {
- $position =& $list;
- if ( $path ) {
- foreach ( $path as $key ) {
- $position =& $position[$key];
- }
- }
- $position[] = $item;
- }
-
- /**
- * @param array $tagset
- * @param int $depth
- */
- protected function makeHtml( $tagset, $depth = 0 ) {
- foreach ( $tagset as $key => $val ) {
- if ( is_array( $val ) ) {
- $this->html .= $this->outputOption(
- $key, '',
- [
- 'disabled' => 'disabled',
- 'style' => 'color: GrayText', // for MSIE
- ],
- $depth
- );
- $this->makeHtml( $val, $depth + 1 );
- } else {
- $this->html .= $this->outputOption(
- $val->text, $val->template,
- [ 'title' => '{{' . $val->template . '}}' ],
- $depth
- );
- }
- }
- }
-
- /**
- * @param string $message
- * @param string $value
- * @param null|array $attribs
- * @param int $depth
- * @return string
- */
- protected function outputOption( $message, $value, $attribs = null, $depth = 0 ) {
- $msgObj = $this->msg( $message );
- $text = $msgObj->exists() ? $msgObj->text() : $message;
- $attribs['value'] = $value;
- if ( $value === $this->selected ) {
- $attribs['selected'] = 'selected';
- }
-
- $val = str_repeat( /* &nbsp */ "\xc2\xa0", $depth * 2 ) . $text;
- return str_repeat( "\t", $depth ) . Xml::element( 'option', $attribs, $val ) . "\n";
- }
-
- /**#@-*/
-
- /**
- * Accessor for $this->licenses
- *
- * @return array
- */
- public function getLicenses() {
- return $this->licenses;
- }
-
- /**
- * Accessor for $this->html
- *
- * @param bool $value
- *
- * @return string
- */
- public function getInputHTML( $value ) {
- $this->selected = $value;
-
- $this->html = $this->outputOption( wfMessage( 'nolicense' )->text(), '',
- (bool)$this->selected ? null : [ 'selected' => 'selected' ] );
- $this->makeHtml( $this->getLicenses() );
-
- $attribs = [
- 'name' => $this->mName,
- 'id' => $this->mID
- ];
- if ( !empty( $this->mParams['disabled'] ) ) {
- $attibs['disabled'] = 'disabled';
- }
-
- return Html::rawElement( 'select', $attribs, $this->html );
- }
-}
-
-/**
- * A License class for use on Special:Upload (represents a single type of license).
- */
-class License {
- /** @var string */
- public $template;
-
- /** @var string */
- public $text;
-
- /**
- * @param string $str License name??
- */
- function __construct( $str ) {
- list( $text, $template ) = explode( '|', strrev( $str ), 2 );
-
- $this->template = strrev( $template );
- $this->text = strrev( $text );
- }
-}
diff --git a/www/wiki/includes/Linker.php b/www/wiki/includes/Linker.php
index 403b10a1..5fc5eb1c 100644
--- a/www/wiki/includes/Linker.php
+++ b/www/wiki/includes/Linker.php
@@ -892,9 +892,13 @@ class Linker {
*/
public static function userLink( $userId, $userName, $altUserName = false ) {
$classes = 'mw-userlink';
+ $page = null;
if ( $userId == 0 ) {
- $page = SpecialPage::getTitleFor( 'Contributions', $userName );
- if ( $altUserName === false ) {
+ $page = ExternalUserNames::getUserLinkTitle( $userName );
+
+ if ( ExternalUserNames::isExternal( $userName ) ) {
+ $classes .= ' mw-extuserlink';
+ } elseif ( $altUserName === false ) {
$altUserName = IP::prettifyIP( $userName );
}
$classes .= ' mw-anonuserlink'; // Separate link class for anons (T45179)
@@ -903,11 +907,12 @@ class Linker {
}
// Wrap the output with <bdi> tags for directionality isolation
- return self::link(
- $page,
- '<bdi>' . htmlspecialchars( $altUserName !== false ? $altUserName : $userName ) . '</bdi>',
- [ 'class' => $classes ]
- );
+ $linkText =
+ '<bdi>' . htmlspecialchars( $altUserName !== false ? $altUserName : $userName ) . '</bdi>';
+
+ return $page
+ ? self::link( $page, $linkText, [ 'class' => $classes ] )
+ : Html::rawElement( 'span', [ 'class' => $classes ], $linkText );
}
/**
@@ -931,6 +936,11 @@ class Linker {
$blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
$addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
+ if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
+ // No tools for an external user
+ return '';
+ }
+
$items = [];
if ( $talkable ) {
$items[] = self::userTalkLink( $userId, $userText );
@@ -1001,7 +1011,7 @@ class Linker {
/**
* @since 1.16.3
- * @param int $userId Userid
+ * @param int $userId
* @param string $userText User name in database.
* @return string HTML fragment with block link
*/
@@ -1015,7 +1025,7 @@ class Linker {
}
/**
- * @param int $userId Userid
+ * @param int $userId
* @param string $userText User name in database.
* @return string HTML fragment with e-mail user link
*/
@@ -1170,12 +1180,12 @@ class Linker {
$section = str_replace( '[[', '', $section );
$section = str_replace( ']]', '', $section );
- $section = Sanitizer::normalizeSectionNameWhitespace( $section ); # T24784
+ $section = substr( Parser::guessSectionNameFromStrippedText( $section ), 1 );
if ( $local ) {
- $sectionTitle = Title::newFromText( '#' . $section );
+ $sectionTitle = Title::makeTitleSafe( NS_MAIN, '', $section );
} else {
$sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
- $title->getDBkey(), Sanitizer::decodeCharReferences( $section ) );
+ $title->getDBkey(), $section );
}
if ( $sectionTitle ) {
$link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' );
@@ -1573,7 +1583,12 @@ class Linker {
$title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
return '<div id="toc" class="toc">'
- . '<div class="toctitle"><h2>' . $title . "</h2></div>\n"
+ . Html::openElement( 'div', [
+ 'class' => 'toctitle',
+ 'lang' => $lang->getHtmlCode(),
+ 'dir' => $lang->getDir(),
+ ] )
+ . '<h2>' . $title . "</h2></div>\n"
. $toc
. "</ul>\n</div>\n";
}
@@ -1737,9 +1752,10 @@ class Linker {
$dbr = wfGetDB( DB_REPLICA );
// Up to the value of $wgShowRollbackEditCount revisions are counted
+ $revQuery = Revision::getQueryInfo();
$res = $dbr->select(
- 'revision',
- [ 'rev_user_text', 'rev_deleted' ],
+ $revQuery['tables'],
+ [ 'rev_user_text' => $revQuery['fields']['rev_user_text'], 'rev_deleted' ],
// $rev->getPage() returns null sometimes
[ 'rev_page' => $rev->getTitle()->getArticleID() ],
__METHOD__,
@@ -1747,7 +1763,8 @@ class Linker {
'USE INDEX' => [ 'revision' => 'page_timestamp' ],
'ORDER BY' => 'rev_timestamp DESC',
'LIMIT' => $wgShowRollbackEditCount + 1
- ]
+ ],
+ $revQuery['joins']
);
$editCount = 0;
@@ -1941,8 +1958,9 @@ class Linker {
*
* @since 1.16.3 $msgParams added in 1.27
* @param string $name Id of the element, minus prefixes.
- * @param string|null $options Null or the string 'withaccess' to add an access-
- * key hint
+ * @param string|array|null $options Null, string or array with some of the following options:
+ * - 'withaccess' to add an access-key hint
+ * - 'nonexisting' to add an accessibility hint that page does not exist
* @param array $msgParams Parameters to pass to the message
*
* @return string Contents of the title attribute (which you must HTML-
@@ -1962,7 +1980,12 @@ class Linker {
}
}
- if ( $options == 'withaccess' ) {
+ $options = (array)$options;
+
+ if ( in_array( 'nonexisting', $options ) ) {
+ $tooltip = wfMessage( 'red-link-title', $tooltip ?: '' )->text();
+ }
+ if ( in_array( 'withaccess', $options ) ) {
$accesskey = self::accesskey( $name );
if ( $accesskey !== false ) {
// Should be build the same as in jquery.accessKeyLabel.js
@@ -2097,20 +2120,28 @@ class Linker {
return Xml::tags( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
}
- /* Deprecated methods */
-
/**
* Returns the attributes for the tooltip and access key.
*
* @since 1.16.3. $msgParams introduced in 1.27
* @param string $name
* @param array $msgParams Params for constructing the message
+ * @param string|array|null $options Options to be passed to titleAttrib.
+ *
+ * @see Linker::titleAttrib for what options could be passed to $options.
*
* @return array
*/
- public static function tooltipAndAccesskeyAttribs( $name, array $msgParams = [] ) {
+ public static function tooltipAndAccesskeyAttribs(
+ $name,
+ array $msgParams = [],
+ $options = null
+ ) {
+ $options = (array)$options;
+ $options[] = 'withaccess';
+
$attribs = [
- 'title' => self::titleAttrib( $name, 'withaccess', $msgParams ),
+ 'title' => self::titleAttrib( $name, $options, $msgParams ),
'accesskey' => self::accesskey( $name )
];
if ( $attribs['title'] === false ) {
diff --git a/www/wiki/includes/MWGrants.php b/www/wiki/includes/MWGrants.php
index c7c54fd5..ba22590c 100644
--- a/www/wiki/includes/MWGrants.php
+++ b/www/wiki/includes/MWGrants.php
@@ -1,7 +1,5 @@
<?php
/**
- * Functions and constants to deal with grants
- *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
@@ -16,6 +14,8 @@
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
*/
use MediaWiki\MediaWikiServices;
diff --git a/www/wiki/includes/MWNamespace.php b/www/wiki/includes/MWNamespace.php
index 97dba26b..f2f98ba2 100644
--- a/www/wiki/includes/MWNamespace.php
+++ b/www/wiki/includes/MWNamespace.php
@@ -38,6 +38,15 @@ class MWNamespace {
*/
private static $alwaysCapitalizedNamespaces = [ NS_SPECIAL, NS_USER, NS_MEDIAWIKI ];
+ /** @var string[]|null Canonical namespaces cache */
+ private static $canonicalNamespaces = null;
+
+ /** @var array|false Canonical namespaces index cache */
+ private static $namespaceIndexes = false;
+
+ /** @var int[]|null Valid namespaces cache */
+ private static $validNamespaces = null;
+
/**
* Throw an exception when trying to get the subject or talk page
* for a given namespace where it does not make sense.
@@ -58,6 +67,19 @@ class MWNamespace {
}
/**
+ * Clear internal caches
+ *
+ * For use in unit testing when namespace configuration is changed.
+ *
+ * @since 1.31
+ */
+ public static function clearCaches() {
+ self::$canonicalNamespaces = null;
+ self::$namespaceIndexes = false;
+ self::$validNamespaces = null;
+ }
+
+ /**
* Can pages in the given namespace be moved?
*
* @param int $index Namespace index
@@ -200,23 +222,28 @@ class MWNamespace {
* (English) names.
*
* @param bool $rebuild Rebuild namespace list (default = false). Used for testing.
+ * Deprecated since 1.31, use self::clearCaches() instead.
*
* @return array
* @since 1.17
*/
public static function getCanonicalNamespaces( $rebuild = false ) {
- static $namespaces = null;
- if ( $namespaces === null || $rebuild ) {
+ if ( $rebuild ) {
+ self::clearCaches();
+ }
+
+ if ( self::$canonicalNamespaces === null ) {
global $wgExtraNamespaces, $wgCanonicalNamespaceNames;
- $namespaces = [ NS_MAIN => '' ] + $wgCanonicalNamespaceNames;
+ self::$canonicalNamespaces = [ NS_MAIN => '' ] + $wgCanonicalNamespaceNames;
// Add extension namespaces
- $namespaces += ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' );
+ self::$canonicalNamespaces +=
+ ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' );
if ( is_array( $wgExtraNamespaces ) ) {
- $namespaces += $wgExtraNamespaces;
+ self::$canonicalNamespaces += $wgExtraNamespaces;
}
- Hooks::run( 'CanonicalNamespaces', [ &$namespaces ] );
+ Hooks::run( 'CanonicalNamespaces', [ &self::$canonicalNamespaces ] );
}
- return $namespaces;
+ return self::$canonicalNamespaces;
}
/**
@@ -242,15 +269,14 @@ class MWNamespace {
* @return int
*/
public static function getCanonicalIndex( $name ) {
- static $xNamespaces = false;
- if ( $xNamespaces === false ) {
- $xNamespaces = [];
+ if ( self::$namespaceIndexes === false ) {
+ self::$namespaceIndexes = [];
foreach ( self::getCanonicalNamespaces() as $i => $text ) {
- $xNamespaces[strtolower( $text )] = $i;
+ self::$namespaceIndexes[strtolower( $text )] = $i;
}
}
- if ( array_key_exists( $name, $xNamespaces ) ) {
- return $xNamespaces[$name];
+ if ( array_key_exists( $name, self::$namespaceIndexes ) ) {
+ return self::$namespaceIndexes[$name];
} else {
return null;
}
@@ -262,19 +288,17 @@ class MWNamespace {
* @return array
*/
public static function getValidNamespaces() {
- static $mValidNamespaces = null;
-
- if ( is_null( $mValidNamespaces ) ) {
+ if ( is_null( self::$validNamespaces ) ) {
foreach ( array_keys( self::getCanonicalNamespaces() ) as $ns ) {
if ( $ns >= 0 ) {
- $mValidNamespaces[] = $ns;
+ self::$validNamespaces[] = $ns;
}
}
// T109137: sort numerically
- sort( $mValidNamespaces, SORT_NUMERIC );
+ sort( self::$validNamespaces, SORT_NUMERIC );
}
- return $mValidNamespaces;
+ return self::$validNamespaces;
}
/**
diff --git a/www/wiki/includes/MagicWord.php b/www/wiki/includes/MagicWord.php
index 6e7799a3..93c8a71c 100644
--- a/www/wiki/includes/MagicWord.php
+++ b/www/wiki/includes/MagicWord.php
@@ -59,10 +59,10 @@
class MagicWord {
/**#@-*/
- /** @var int */
+ /** @var string */
public $mId;
- /** @var array */
+ /** @var string[] */
public $mSynonyms;
/** @var bool */
@@ -92,7 +92,10 @@ class MagicWord {
/** @var bool */
private $mFound = false;
+ /** @var bool */
public static $mVariableIDsInitialised = false;
+
+ /** @var string[] */
public static $mVariableIDs = [
'!',
'currentmonth',
@@ -174,7 +177,9 @@ class MagicWord {
'cascadingsources',
];
- /* Array of caching hints for ParserCache */
+ /** Array of caching hints for ParserCache
+ * @var array [ string => int ]
+ */
public static $mCacheTTLs = [
'currentmonth' => 86400,
'currentmonth1' => 86400,
@@ -216,6 +221,7 @@ class MagicWord {
'numberingroup' => 3600,
];
+ /** @var string[] */
public static $mDoubleUnderscoreIDs = [
'notoc',
'nogallery',
@@ -232,17 +238,30 @@ class MagicWord {
'nocontentconvert',
];
+ /** @var string[] */
public static $mSubstIDs = [
'subst',
'safesubst',
];
+ /** @var array [ string => MagicWord ] */
public static $mObjects = [];
+
+ /** @var MagicWordArray */
public static $mDoubleUnderscoreArray = null;
/**#@-*/
- public function __construct( $id = 0, $syn = [], $cs = false ) {
+ /**
+ * Create a new MagicWord object
+ *
+ * Use factory instead: MagicWord::get
+ *
+ * @param string $id The internal name of the magic word
+ * @param string[]|string $syn synonyms for the magic word
+ * @param bool $cs If magic word is case sensitive
+ */
+ public function __construct( $id = null, $syn = [], $cs = false ) {
$this->mId = $id;
$this->mSynonyms = (array)$syn;
$this->mCaseSensitive = $cs;
@@ -251,7 +270,7 @@ class MagicWord {
/**
* Factory: creates an object representing an ID
*
- * @param int $id
+ * @param string $id The internal name of the magic word
*
* @return MagicWord
*/
@@ -267,7 +286,7 @@ class MagicWord {
/**
* Get an array of parser variable IDs
*
- * @return array
+ * @return string[]
*/
public static function getVariableIDs() {
if ( !self::$mVariableIDsInitialised ) {
@@ -280,7 +299,7 @@ class MagicWord {
/**
* Get an array of parser substitution modifier IDs
- * @return array
+ * @return string[]
*/
public static function getSubstIDs() {
return self::$mSubstIDs;
@@ -289,7 +308,7 @@ class MagicWord {
/**
* Allow external reads of TTL array
*
- * @param int $id
+ * @param string $id
* @return int
*/
public static function getCacheTTL( $id ) {
@@ -324,7 +343,7 @@ class MagicWord {
/**
* Initialises this object with an ID
*
- * @param int $id
+ * @param string $id
* @throws MWException
*/
public function load( $id ) {
@@ -630,7 +649,7 @@ class MagicWord {
}
/**
- * @return array
+ * @return string[]
*/
public function getSynonyms() {
return $this->mSynonyms;
@@ -650,7 +669,7 @@ class MagicWord {
* Adds all the synonyms of this MagicWord to an array, to allow quick
* lookup in a list of magic words
*
- * @param array &$array
+ * @param string[] &$array
* @param string $value
*/
public function addToArray( &$array, $value ) {
@@ -668,7 +687,7 @@ class MagicWord {
}
/**
- * @return int
+ * @return string
*/
public function getId() {
return $this->mId;
diff --git a/www/wiki/includes/MagicWordArray.php b/www/wiki/includes/MagicWordArray.php
index 5856e21b..4010ec75 100644
--- a/www/wiki/includes/MagicWordArray.php
+++ b/www/wiki/includes/MagicWordArray.php
@@ -203,7 +203,9 @@ class MagicWordArray {
*/
public function parseMatch( $m ) {
reset( $m );
- while ( list( $key, $value ) = each( $m ) ) {
+ while ( ( $key = key( $m ) ) !== null ) {
+ $value = current( $m );
+ next( $m );
if ( $key === 0 || $value === '' ) {
continue;
}
diff --git a/www/wiki/includes/MediaWiki.php b/www/wiki/includes/MediaWiki.php
index 0f40c192..b3abe7cf 100644
--- a/www/wiki/includes/MediaWiki.php
+++ b/www/wiki/includes/MediaWiki.php
@@ -26,6 +26,7 @@ use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\ChronologyProtector;
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\DBConnectionError;
+use Liuggio\StatsdClient\Sender\SocketSender;
/**
* The MediaWiki class is the helper class for the index.php entry point.
@@ -367,7 +368,7 @@ class MediaWiki {
}
throw new HttpError( 500, $message );
}
- $output->setSquidMaxage( 1200 );
+ $output->setCdnMaxage( 1200 );
$output->redirect( $targetUrl, '301' );
return true;
}
@@ -548,6 +549,9 @@ class MediaWiki {
}
MWExceptionHandler::handleException( $e );
+ } catch ( Error $e ) {
+ // Type errors and such: at least handle it now and clean up the LBFactory state
+ MWExceptionHandler::handleException( $e );
}
$this->doPostOutputShutdown( 'normal' );
@@ -602,50 +606,54 @@ class MediaWiki {
DeferredUpdates::doUpdates( 'enqueue', DeferredUpdates::PRESEND );
wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
- // Decide when clients block on ChronologyProtector DB position writes
- $urlDomainDistance = (
- $request->wasPosted() &&
- $output->getRedirect() &&
- $lbFactory->hasOrMadeRecentMasterChanges( INF )
- ) ? self::getUrlDomainDistance( $output->getRedirect() ) : false;
+ // Should the client return, their request should observe the new ChronologyProtector
+ // DB positions. This request might be on a foreign wiki domain, so synchronously update
+ // the DB positions in all datacenters to be safe. If this output is not a redirect,
+ // then OutputPage::output() will be relatively slow, meaning that running it in
+ // $postCommitWork should help mask the latency of those updates.
+ $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
+ $strategy = 'cookie+sync';
$allowHeaders = !( $output->isDisabled() || headers_sent() );
- if ( $urlDomainDistance === 'local' || $urlDomainDistance === 'remote' ) {
- // OutputPage::output() will be fast; $postCommitWork will not be useful for
- // masking the latency of syncing DB positions accross all datacenters synchronously.
- // Instead, make use of the RTT time of the client follow redirects.
- $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
- $cpPosTime = microtime( true );
- // Client's next request should see 1+ positions with this DBMasterPos::asOf() time
- if ( $urlDomainDistance === 'local' && $allowHeaders ) {
- // Client will stay on this domain, so set an unobtrusive cookie
- $expires = time() + ChronologyProtector::POSITION_TTL;
- $options = [ 'prefix' => '' ];
- $request->response()->setCookie( 'cpPosTime', $cpPosTime, $expires, $options );
- } else {
- // Cookies may not work across wiki domains, so use a URL parameter
- $safeUrl = $lbFactory->appendPreShutdownTimeAsQuery(
- $output->getRedirect(),
- $cpPosTime
- );
- $output->redirect( $safeUrl );
+ if ( $output->getRedirect() && $lbFactory->hasOrMadeRecentMasterChanges( INF ) ) {
+ // OutputPage::output() will be fast, so $postCommitWork is useless for masking
+ // the latency of synchronously updating the DB positions in all datacenters.
+ // Try to make use of the time the client spends following redirects instead.
+ $domainDistance = self::getUrlDomainDistance( $output->getRedirect() );
+ if ( $domainDistance === 'local' && $allowHeaders ) {
+ $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
+ $strategy = 'cookie'; // use same-domain cookie and keep the URL uncluttered
+ } elseif ( $domainDistance === 'remote' ) {
+ $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
+ $strategy = 'cookie+url'; // cross-domain cookie might not work
}
- } else {
- // OutputPage::output() is fairly slow; run it in $postCommitWork to mask
- // the latency of syncing DB positions accross all datacenters synchronously
- $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
- if ( $lbFactory->hasOrMadeRecentMasterChanges( INF ) && $allowHeaders ) {
- $cpPosTime = microtime( true );
- // Set a cookie in case the DB position store cannot sync accross datacenters.
- // This will at least cover the common case of the user staying on the domain.
+ }
+
+ // Record ChronologyProtector positions for DBs affected in this request at this point
+ $cpIndex = null;
+ $lbFactory->shutdown( $flags, $postCommitWork, $cpIndex );
+ wfDebug( __METHOD__ . ': LBFactory shutdown completed' );
+
+ if ( $cpIndex > 0 ) {
+ if ( $allowHeaders ) {
$expires = time() + ChronologyProtector::POSITION_TTL;
$options = [ 'prefix' => '' ];
- $request->response()->setCookie( 'cpPosTime', $cpPosTime, $expires, $options );
+ $request->response()->setCookie( 'cpPosIndex', $cpIndex, $expires, $options );
+ }
+
+ if ( $strategy === 'cookie+url' ) {
+ if ( $output->getRedirect() ) { // sanity
+ $safeUrl = $lbFactory->appendShutdownCPIndexAsQuery(
+ $output->getRedirect(),
+ $cpIndex
+ );
+ $output->redirect( $safeUrl );
+ } else {
+ $e = new LogicException( "No redirect; cannot append cpPosIndex parameter." );
+ MWExceptionHandler::logException( $e );
+ }
}
}
- // Record ChronologyProtector positions for DBs affected in this request at this point
- $lbFactory->shutdown( $flags, $postCommitWork );
- wfDebug( __METHOD__ . ': LBFactory shutdown completed' );
// Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that handles this
// POST request (e.g. the "master" data center). Also have the user briefly bypass CDN so
@@ -726,10 +734,12 @@ class MediaWiki {
if ( function_exists( 'register_postsend_function' ) ) {
// https://github.com/facebook/hhvm/issues/1230
register_postsend_function( $callback );
+ /** @noinspection PhpUnusedLocalVariableInspection */
$blocksHttpClient = false;
} else {
if ( function_exists( 'fastcgi_finish_request' ) ) {
fastcgi_finish_request();
+ /** @noinspection PhpUnusedLocalVariableInspection */
$blocksHttpClient = false;
} else {
// Either all DB and deferred updates should happen or none.
@@ -851,7 +861,7 @@ class MediaWiki {
$this->performRequest();
// GUI-ify and stash the page output in MediaWiki::doPreOutputCommit() while
- // ChronologyProtector synchronizes DB positions or slaves accross all datacenters.
+ // ChronologyProtector synchronizes DB positions or replicas accross all datacenters.
$buffer = null;
$outputWork = function () use ( $output, &$buffer ) {
if ( $buffer === null ) {
@@ -884,7 +894,9 @@ class MediaWiki {
$trxProfiler = Profiler::instance()->getTransactionProfiler();
$trxProfiler->resetExpectations();
$trxProfiler->setExpectations(
- $this->config->get( 'TrxProfilerLimits' )['PostSend'],
+ $this->context->getRequest()->hasSafeMethod()
+ ? $this->config->get( 'TrxProfilerLimits' )['PostSend-GET']
+ : $this->config->get( 'TrxProfilerLimits' )['PostSend-POST'],
__METHOD__
);
@@ -911,6 +923,34 @@ class MediaWiki {
}
/**
+ * Send out any buffered statsd data according to sampling rules
+ *
+ * @param IBufferingStatsdDataFactory $stats
+ * @param Config $config
+ * @throws ConfigException
+ * @since 1.31
+ */
+ public static function emitBufferedStatsdData(
+ IBufferingStatsdDataFactory $stats, Config $config
+ ) {
+ if ( $config->get( 'StatsdServer' ) && $stats->hasData() ) {
+ 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->setSamplingRates( $config->get( 'StatsdSamplingRates' ) );
+ $statsdClient->send( $stats->getData() );
+
+ $stats->clearData(); // empty buffer for the next round
+ } catch ( Exception $ex ) {
+ MWExceptionHandler::logException( $ex );
+ }
+ }
+ }
+
+ /**
* Potentially open a socket and sent an HTTP request back to the server
* to run a specified number of jobs. This registers a callback to cleanup
* the socket once it's done.
@@ -991,7 +1031,7 @@ class MediaWiki {
$port = $info['port'];
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$sock = $host ? fsockopen(
$host,
$port,
@@ -1000,7 +1040,7 @@ class MediaWiki {
// If it takes more than 100ms to connect to ourselves there is a problem...
0.100
) : false;
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
$invokedWithSuccess = true;
if ( $sock ) {
diff --git a/www/wiki/includes/MediaWikiServices.php b/www/wiki/includes/MediaWikiServices.php
index 0d010b49..ac986835 100644
--- a/www/wiki/includes/MediaWikiServices.php
+++ b/www/wiki/includes/MediaWikiServices.php
@@ -1,6 +1,8 @@
<?php
namespace MediaWiki;
+use ActorMigration;
+use CommentStore;
use Config;
use ConfigFactory;
use CryptHKDF;
@@ -10,7 +12,15 @@ use GenderCache;
use GlobalVarConfig;
use Hooks;
use IBufferingStatsdDataFactory;
+use MediaWiki\Http\HttpRequestFactory;
+use MediaWiki\Preferences\PreferencesFactory;
use MediaWiki\Shell\CommandFactory;
+use MediaWiki\Storage\BlobStore;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\NameTableStore;
+use MediaWiki\Storage\RevisionFactory;
+use MediaWiki\Storage\RevisionLookup;
+use MediaWiki\Storage\RevisionStore;
use Wikimedia\Rdbms\LBFactory;
use LinkCache;
use Wikimedia\Rdbms\LoadBalancer;
@@ -31,7 +41,7 @@ use SearchEngineConfig;
use SearchEngineFactory;
use SiteLookup;
use SiteStore;
-use WatchedItemStore;
+use WatchedItemStoreInterface;
use WatchedItemQueryService;
use SkinFactory;
use TitleFormatter;
@@ -513,7 +523,7 @@ class MediaWikiServices extends ServiceContainer {
/**
* @since 1.28
- * @return WatchedItemStore
+ * @return WatchedItemStoreInterface
*/
public function getWatchedItemStore() {
return $this->getService( 'WatchedItemStore' );
@@ -683,6 +693,30 @@ class MediaWikiServices extends ServiceContainer {
}
/**
+ * @since 1.31
+ * @return \UploadRevisionImporter
+ */
+ public function getWikiRevisionUploadImporter() {
+ return $this->getService( 'UploadRevisionImporter' );
+ }
+
+ /**
+ * @since 1.31
+ * @return \OldRevisionImporter
+ */
+ public function getWikiRevisionOldRevisionImporter() {
+ return $this->getService( 'OldRevisionImporter' );
+ }
+
+ /**
+ * @since 1.31
+ * @return \OldRevisionImporter
+ */
+ public function getWikiRevisionOldRevisionImporterNoUpdates() {
+ return $this->getService( 'WikiRevisionOldRevisionImporterNoUpdates' );
+ }
+
+ /**
* @since 1.30
* @return CommandFactory
*/
@@ -690,6 +724,102 @@ class MediaWikiServices extends ServiceContainer {
return $this->getService( 'ShellCommandFactory' );
}
+ /**
+ * @since 1.31
+ * @return \ExternalStoreFactory
+ */
+ public function getExternalStoreFactory() {
+ return $this->getService( 'ExternalStoreFactory' );
+ }
+
+ /**
+ * @since 1.31
+ * @return BlobStoreFactory
+ */
+ public function getBlobStoreFactory() {
+ return $this->getService( 'BlobStoreFactory' );
+ }
+
+ /**
+ * @since 1.31
+ * @return BlobStore
+ */
+ public function getBlobStore() {
+ return $this->getService( '_SqlBlobStore' );
+ }
+
+ /**
+ * @since 1.31
+ * @return RevisionStore
+ */
+ public function getRevisionStore() {
+ return $this->getService( 'RevisionStore' );
+ }
+
+ /**
+ * @since 1.31
+ * @return RevisionLookup
+ */
+ public function getRevisionLookup() {
+ return $this->getService( 'RevisionLookup' );
+ }
+
+ /**
+ * @since 1.31
+ * @return RevisionFactory
+ */
+ public function getRevisionFactory() {
+ return $this->getService( 'RevisionFactory' );
+ }
+
+ /**
+ * @since 1.31
+ * @return NameTableStore
+ */
+ public function getContentModelStore() {
+ return $this->getService( 'ContentModelStore' );
+ }
+
+ /**
+ * @since 1.31
+ * @return NameTableStore
+ */
+ public function getSlotRoleStore() {
+ return $this->getService( 'SlotRoleStore' );
+ }
+
+ /**
+ * @since 1.31
+ * @return PreferencesFactory
+ */
+ public function getPreferencesFactory() {
+ return $this->getService( 'PreferencesFactory' );
+ }
+
+ /**
+ * @since 1.31
+ * @return HttpRequestFactory
+ */
+ public function getHttpRequestFactory() {
+ return $this->getService( 'HttpRequestFactory' );
+ }
+
+ /**
+ * @since 1.31
+ * @return CommentStore
+ */
+ public function getCommentStore() {
+ return $this->getService( 'CommentStore' );
+ }
+
+ /**
+ * @since 1.31
+ * @return ActorMigration
+ */
+ public function getActorMigration() {
+ return $this->getService( 'ActorMigration' );
+ }
+
///////////////////////////////////////////////////////////////////////////
// NOTE: When adding a service getter here, don't forget to add a test
// case for it in MediaWikiServicesTest::provideGetters() and in
diff --git a/www/wiki/includes/MergeHistory.php b/www/wiki/includes/MergeHistory.php
index 9d638696..0e9bb467 100644
--- a/www/wiki/includes/MergeHistory.php
+++ b/www/wiki/includes/MergeHistory.php
@@ -1,10 +1,6 @@
<?php
/**
- *
- *
- * Created on Dec 29, 2015
- *
* Copyright © 2015 Geoffrey Mon <geofbot@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
@@ -24,6 +20,7 @@
*
* @file
*/
+use MediaWiki\MediaWikiServices;
use Wikimedia\Timestamp\TimestampException;
use Wikimedia\Rdbms\IDatabase;
@@ -335,6 +332,10 @@ class MergeHistory {
}
$this->dest->invalidateCache(); // update histories
+ // Duplicate watchers of the old article to the new article on history merge
+ $store = MediaWikiServices::getInstance()->getWatchedItemStore();
+ $store->duplicateAllAssociatedEntries( $this->source, $this->dest );
+
// Update our logs
$logEntry = new ManualLogEntry( 'merge', 'merge' );
$logEntry->setPerformer( $user );
diff --git a/www/wiki/includes/Message.php b/www/wiki/includes/Message.php
index 677efb5d..7d05f41e 100644
--- a/www/wiki/includes/Message.php
+++ b/www/wiki/includes/Message.php
@@ -1105,7 +1105,7 @@ class Message implements MessageSpecifier, Serializable {
public static function listParam( array $list, $type = 'text' ) {
if ( !isset( self::$listTypeMap[$type] ) ) {
throw new InvalidArgumentException(
- "Invalid type '$type'. Known types are: " . join( ', ', array_keys( self::$listTypeMap ) )
+ "Invalid type '$type'. Known types are: " . implode( ', ', array_keys( self::$listTypeMap ) )
);
}
return [ 'list' => $list, 'type' => $type ];
@@ -1185,11 +1185,17 @@ class Message implements MessageSpecifier, Serializable {
} elseif ( isset( $param['list'] ) ) {
return $this->formatListParam( $param['list'], $param['type'], $format );
} else {
- $warning = 'Invalid parameter for message "' . $this->getKey() . '": ' .
- htmlspecialchars( serialize( $param ) );
- trigger_error( $warning, E_USER_WARNING );
- $e = new Exception;
- wfDebugLog( 'Bug58676', $warning . "\n" . $e->getTraceAsString() );
+ if ( !is_scalar( $param ) ) {
+ $param = serialize( $param );
+ }
+ \MediaWiki\Logger\LoggerFactory::getInstance( 'Bug58676' )->warning(
+ 'Invalid parameter for message "{msgkey}": {param}',
+ [
+ 'exception' => new Exception,
+ 'msgkey' => $this->getKey(),
+ 'param' => htmlspecialchars( $param ),
+ ]
+ );
return [ 'before', '[INVALID]' ];
}
@@ -1238,7 +1244,16 @@ class Message implements MessageSpecifier, Serializable {
$this->getLanguage()
);
- return $out instanceof ParserOutput ? $out->getText() : $out;
+ return $out instanceof ParserOutput
+ ? $out->getText( [
+ 'enableSectionEditLinks' => false,
+ // Wrapping messages in an extra <div> is probably not expected. If
+ // they're outside the content area they probably shouldn't be
+ // targeted by CSS that's targeting the parser output, and if
+ // they're inside they already are from the outer div.
+ 'unwrap' => true,
+ ] )
+ : $out;
}
/**
@@ -1300,16 +1315,15 @@ class Message implements MessageSpecifier, Serializable {
*/
protected function formatPlaintext( $plaintext, $format ) {
switch ( $format ) {
- case self::FORMAT_TEXT:
- case self::FORMAT_PLAIN:
- return $plaintext;
-
- case self::FORMAT_PARSE:
- case self::FORMAT_BLOCK_PARSE:
- case self::FORMAT_ESCAPED:
- default:
- return htmlspecialchars( $plaintext, ENT_QUOTES );
-
+ case self::FORMAT_TEXT:
+ case self::FORMAT_PLAIN:
+ return $plaintext;
+
+ case self::FORMAT_PARSE:
+ case self::FORMAT_BLOCK_PARSE:
+ case self::FORMAT_ESCAPED:
+ default:
+ return htmlspecialchars( $plaintext, ENT_QUOTES );
}
}
diff --git a/www/wiki/includes/MimeMagic.php b/www/wiki/includes/MimeMagic.php
index a2a44bb8..6152d226 100644
--- a/www/wiki/includes/MimeMagic.php
+++ b/www/wiki/includes/MimeMagic.php
@@ -31,6 +31,7 @@ class MimeMagic extends MimeAnalyzer {
* @deprecated since 1.28 get a MimeAnalyzer instance from MediaWikiServices
*/
public static function singleton() {
+ wfDeprecated( __METHOD__, '1.28' );
// XXX: We know that the MimeAnalyzer is currently an instance of MimeMagic
$instance = MediaWikiServices::getInstance()->getMimeAnalyzer();
Assert::postcondition(
diff --git a/www/wiki/includes/MovePage.php b/www/wiki/includes/MovePage.php
index 2f6bca75..1e9570d2 100644
--- a/www/wiki/includes/MovePage.php
+++ b/www/wiki/includes/MovePage.php
@@ -310,7 +310,7 @@ class MovePage {
# Protect the redirect title as the title used to be...
$res = $dbw->select(
'page_restrictions',
- '*',
+ [ 'pr_type', 'pr_level', 'pr_cascade', 'pr_user', 'pr_expiry' ],
[ 'pr_page' => $pageid ],
__METHOD__,
'FOR UPDATE'
@@ -599,7 +599,12 @@ class MovePage {
$redirectArticle->doEditUpdates( $redirectRevision, $user, [ 'created' => true ] );
- ChangeTags::addTags( $changeTags, null, $redirectRevId, null );
+ // make a copy because of log entry below
+ $redirectTags = $changeTags;
+ if ( in_array( 'mw-new-redirect', ChangeTags::getSoftwareTags() ) ) {
+ $redirectTags[] = 'mw-new-redirect';
+ }
+ ChangeTags::addTags( $redirectTags, null, $redirectRevId, null );
}
}
diff --git a/www/wiki/includes/NoLocalSettings.php b/www/wiki/includes/NoLocalSettings.php
index 50950ef3..46e9630f 100644
--- a/www/wiki/includes/NoLocalSettings.php
+++ b/www/wiki/includes/NoLocalSettings.php
@@ -22,13 +22,11 @@
# T32219 : can not use pathinfo() on URLs since slashes do not match
$matches = [];
-$ext = 'php';
$path = '/';
foreach ( array_filter( explode( '/', $_SERVER['PHP_SELF'] ) ) as $part ) {
- if ( !preg_match( '/\.(php5?)$/', $part, $matches ) ) {
+ if ( !preg_match( '/\.(php)$/', $part, $matches ) ) {
$path .= "$part/";
} else {
- $ext = $matches[1] == 'php5' ? 'php5' : 'php';
break;
}
}
@@ -50,12 +48,12 @@ $templateParser = new TemplateParser();
# Render error page if no LocalSettings file can be found
try {
+ global $wgVersion;
echo $templateParser->processTemplate(
'NoLocalSettings',
[
'wgVersion' => ( isset( $wgVersion ) ? $wgVersion : 'VERSION' ),
'path' => $path,
- 'ext' => $ext,
'localSettingsExists' => file_exists( MW_CONFIG_FILE ),
'installerStarted' => $installerStarted
]
diff --git a/www/wiki/includes/OutputHandler.php b/www/wiki/includes/OutputHandler.php
index 2dc37320..16c37841 100644
--- a/www/wiki/includes/OutputHandler.php
+++ b/www/wiki/includes/OutputHandler.php
@@ -20,219 +20,143 @@
* @file
*/
+namespace MediaWiki;
+
/**
- * Standard output handler for use with ob_start
- *
- * @param string $s
- *
- * @return string
+ * @since 1.31
*/
-function wfOutputHandler( $s ) {
- global $wgDisableOutputCompression, $wgValidateAllHtml, $wgMangleFlashPolicy;
- if ( $wgMangleFlashPolicy ) {
- $s = wfMangleFlashPolicy( $s );
- }
- if ( $wgValidateAllHtml ) {
- $headers = headers_list();
- $isHTML = false;
- foreach ( $headers as $header ) {
- $parts = explode( ':', $header, 2 );
- if ( count( $parts ) !== 2 ) {
- continue;
+class OutputHandler {
+ /**
+ * Standard output handler for use with ob_start.
+ *
+ * @param string $s Web response output
+ * @return string
+ */
+ public static function handle( $s ) {
+ global $wgDisableOutputCompression, $wgMangleFlashPolicy;
+ if ( $wgMangleFlashPolicy ) {
+ $s = self::mangleFlashPolicy( $s );
+ }
+ if ( !$wgDisableOutputCompression && !ini_get( 'zlib.output_compression' ) ) {
+ if ( !defined( 'MW_NO_OUTPUT_COMPRESSION' ) ) {
+ $s = self::handleGzip( $s );
}
- $name = strtolower( trim( $parts[0] ) );
- $value = trim( $parts[1] );
- if ( $name == 'content-type' && ( strpos( $value, 'text/html' ) === 0
- || strpos( $value, 'application/xhtml+xml' ) === 0 )
- ) {
- $isHTML = true;
- break;
+ if ( !ini_get( 'output_handler' ) ) {
+ self::emitContentLength( strlen( $s ) );
}
}
- if ( $isHTML ) {
- $s = wfHtmlValidationHandler( $s );
- }
- }
- if ( !$wgDisableOutputCompression && !ini_get( 'zlib.output_compression' ) ) {
- if ( !defined( 'MW_NO_OUTPUT_COMPRESSION' ) ) {
- $s = wfGzipHandler( $s );
- }
- if ( !ini_get( 'output_handler' ) ) {
- wfDoContentLength( strlen( $s ) );
- }
- }
- return $s;
-}
-
-/**
- * Get the "file extension" that some client apps will estimate from
- * the currently-requested URL.
- * This isn't on WebRequest because we need it when things aren't initialized
- * @private
- *
- * @return string
- */
-function wfRequestExtension() {
- /// @todo FIXME: this sort of dupes some code in WebRequest::getRequestUrl()
- if ( isset( $_SERVER['REQUEST_URI'] ) ) {
- // Strip the query string...
- list( $path ) = explode( '?', $_SERVER['REQUEST_URI'], 2 );
- } elseif ( isset( $_SERVER['SCRIPT_NAME'] ) ) {
- // Probably IIS. QUERY_STRING appears separately.
- $path = $_SERVER['SCRIPT_NAME'];
- } else {
- // Can't get the path from the server? :(
- return '';
- }
-
- $period = strrpos( $path, '.' );
- if ( $period !== false ) {
- return strtolower( substr( $path, $period ) );
- }
- return '';
-}
-
-/**
- * Handler that compresses data with gzip if allowed by the Accept header.
- * Unlike ob_gzhandler, it works for HEAD requests too.
- *
- * @param string $s
- *
- * @return string
- */
-function wfGzipHandler( $s ) {
- if ( !function_exists( 'gzencode' ) ) {
- wfDebug( __FUNCTION__ . "() skipping compression (gzencode unavailable)\n" );
- return $s;
- }
- if ( headers_sent() ) {
- wfDebug( __FUNCTION__ . "() skipping compression (headers already sent)\n" );
return $s;
}
- $ext = wfRequestExtension();
- if ( $ext == '.gz' || $ext == '.tgz' ) {
- // Don't do gzip compression if the URL path ends in .gz or .tgz
- // This confuses Safari and triggers a download of the page,
- // even though it's pretty clearly labeled as viewable HTML.
- // Bad Safari! Bad!
- return $s;
- }
+ /**
+ * Get the "file extension" that some client apps will estimate from
+ * the currently-requested URL.
+ *
+ * This isn't a WebRequest method, because we need it before the class loads.
+ * @todo As of 2018, this actually runs after autoloader in Setup.php, so
+ * WebRequest seems like a good place for this.
+ *
+ * @return string
+ */
+ private static function findUriExtension() {
+ /// @todo FIXME: this sort of dupes some code in WebRequest::getRequestUrl()
+ if ( isset( $_SERVER['REQUEST_URI'] ) ) {
+ // Strip the query string...
+ list( $path ) = explode( '?', $_SERVER['REQUEST_URI'], 2 );
+ } elseif ( isset( $_SERVER['SCRIPT_NAME'] ) ) {
+ // Probably IIS. QUERY_STRING appears separately.
+ $path = $_SERVER['SCRIPT_NAME'];
+ } else {
+ // Can't get the path from the server? :(
+ return '';
+ }
- if ( wfClientAcceptsGzip() ) {
- wfDebug( __FUNCTION__ . "() is compressing output\n" );
- header( 'Content-Encoding: gzip' );
- $s = gzencode( $s, 6 );
+ $period = strrpos( $path, '.' );
+ if ( $period !== false ) {
+ return strtolower( substr( $path, $period ) );
+ }
+ return '';
}
- // Set vary header if it hasn't been set already
- $headers = headers_list();
- $foundVary = false;
- foreach ( $headers as $header ) {
- $headerName = strtolower( substr( $header, 0, 5 ) );
- if ( $headerName == 'vary:' ) {
- $foundVary = true;
- break;
+ /**
+ * Handler that compresses data with gzip if allowed by the Accept header.
+ *
+ * Unlike ob_gzhandler, it works for HEAD requests too.
+ *
+ * @param string $s Web response output
+ * @return string
+ */
+ private static function handleGzip( $s ) {
+ if ( !function_exists( 'gzencode' ) ) {
+ wfDebug( __METHOD__ . "() skipping compression (gzencode unavailable)\n" );
+ return $s;
}
- }
- if ( !$foundVary ) {
- header( 'Vary: Accept-Encoding' );
- global $wgUseKeyHeader;
- if ( $wgUseKeyHeader ) {
- header( 'Key: Accept-Encoding;match=gzip' );
+ if ( headers_sent() ) {
+ wfDebug( __METHOD__ . "() skipping compression (headers already sent)\n" );
+ return $s;
}
- }
- return $s;
-}
-/**
- * Mangle flash policy tags which open up the site to XSS attacks.
- *
- * @param string $s
- *
- * @return string
- */
-function wfMangleFlashPolicy( $s ) {
- # Avoid weird excessive memory usage in PCRE on big articles
- if ( preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $s ) ) {
- return preg_replace( '/\<(\s*)(cross-domain-policy(?=\s|\>))/i', '<$1NOT-$2', $s );
- } else {
- return $s;
- }
-}
+ $ext = self::findUriExtension();
+ if ( $ext == '.gz' || $ext == '.tgz' ) {
+ // Don't do gzip compression if the URL path ends in .gz or .tgz
+ // This confuses Safari and triggers a download of the page,
+ // even though it's pretty clearly labeled as viewable HTML.
+ // Bad Safari! Bad!
+ return $s;
+ }
-/**
- * Add a Content-Length header if possible. This makes it cooperate with CDN better.
- *
- * @param int $length
- */
-function wfDoContentLength( $length ) {
- if ( !headers_sent()
- && isset( $_SERVER['SERVER_PROTOCOL'] )
- && $_SERVER['SERVER_PROTOCOL'] == 'HTTP/1.0'
- ) {
- header( "Content-Length: $length" );
- }
-}
+ if ( wfClientAcceptsGzip() ) {
+ wfDebug( __METHOD__ . "() is compressing output\n" );
+ header( 'Content-Encoding: gzip' );
+ $s = gzencode( $s, 6 );
+ }
-/**
- * Replace the output with an error if the HTML is not valid
- *
- * @param string $s
- *
- * @return string
- */
-function wfHtmlValidationHandler( $s ) {
- $errors = '';
- if ( MWTidy::checkErrors( $s, $errors ) ) {
+ // Set vary header if it hasn't been set already
+ $headers = headers_list();
+ $foundVary = false;
+ foreach ( $headers as $header ) {
+ $headerName = strtolower( substr( $header, 0, 5 ) );
+ if ( $headerName == 'vary:' ) {
+ $foundVary = true;
+ break;
+ }
+ }
+ if ( !$foundVary ) {
+ header( 'Vary: Accept-Encoding' );
+ global $wgUseKeyHeader;
+ if ( $wgUseKeyHeader ) {
+ header( 'Key: Accept-Encoding;match=gzip' );
+ }
+ }
return $s;
}
- header( 'Cache-Control: no-cache' );
-
- $out = Html::element( 'h1', null, 'HTML validation error' );
- $out .= Html::openElement( 'ul' );
-
- $error = strtok( $errors, "\n" );
- $badLines = [];
- while ( $error !== false ) {
- if ( preg_match( '/^line (\d+)/', $error, $m ) ) {
- $lineNum = intval( $m[1] );
- $badLines[$lineNum] = true;
- $out .= Html::rawElement( 'li', null,
- Html::element( 'a', [ 'href' => "#line-{$lineNum}" ], $error ) ) . "\n";
+ /**
+ * Mangle flash policy tags which open up the site to XSS attacks.
+ *
+ * @param string $s Web response output
+ * @return string
+ */
+ private static function mangleFlashPolicy( $s ) {
+ # Avoid weird excessive memory usage in PCRE on big articles
+ if ( preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $s ) ) {
+ return preg_replace( '/\<(\s*)(cross-domain-policy(?=\s|\>))/i', '<$1NOT-$2', $s );
+ } else {
+ return $s;
}
- $error = strtok( "\n" );
}
- $out .= Html::closeElement( 'ul' );
- $out .= Html::element( 'pre', null, $errors );
- $out .= Html::openElement( 'ol' ) . "\n";
- $line = strtok( $s, "\n" );
- $i = 1;
- while ( $line !== false ) {
- $attrs = [];
- if ( isset( $badLines[$i] ) ) {
- $attrs['class'] = 'highlight';
- $attrs['id'] = "line-$i";
+ /**
+ * Add a Content-Length header if possible. This makes it cooperate with CDN better.
+ *
+ * @param int $length
+ */
+ private static function emitContentLength( $length ) {
+ if ( !headers_sent()
+ && isset( $_SERVER['SERVER_PROTOCOL'] )
+ && $_SERVER['SERVER_PROTOCOL'] == 'HTTP/1.0'
+ ) {
+ header( "Content-Length: $length" );
}
- $out .= Html::element( 'li', $attrs, $line ) . "\n";
- $line = strtok( "\n" );
- $i++;
}
- $out .= Html::closeElement( 'ol' );
-
- $style = <<<CSS
-.highlight { background-color: #ffc }
-li { white-space: pre }
-CSS;
-
- $out = Html::htmlHeader( [ 'lang' => 'en', 'dir' => 'ltr' ] ) .
- Html::rawElement( 'head', null,
- Html::element( 'title', null, 'HTML validation error' ) .
- Html::inlineStyle( $style ) ) .
- Html::rawElement( 'body', null, $out ) .
- Html::closeElement( 'html' );
-
- return $out;
}
diff --git a/www/wiki/includes/OutputPage.php b/www/wiki/includes/OutputPage.php
index 52161466..99dd4a7c 100644
--- a/www/wiki/includes/OutputPage.php
+++ b/www/wiki/includes/OutputPage.php
@@ -23,8 +23,9 @@
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use MediaWiki\Session\SessionManager;
-use WrappedString\WrappedString;
-use WrappedString\WrappedStringList;
+use Wikimedia\RelPath;
+use Wikimedia\WrappedString;
+use Wikimedia\WrappedStringList;
/**
* This class should be covered by a general architecture document which does
@@ -52,12 +53,6 @@ class OutputPage extends ContextSource {
protected $mCanonicalUrl = false;
/**
- * @var array Additional stylesheets. Looks like this is for extensions.
- * Might be replaced by ResourceLoader.
- */
- protected $mExtStyles = [];
-
- /**
* @var string Should be private - has getter and setter. Contains
* the HTML title */
public $mPagetitle = '';
@@ -293,11 +288,6 @@ class OutputPage extends ContextSource {
private $mEnableTOC = false;
/**
- * @var bool Whether parser output should contain section edit links
- */
- private $mEnableSectionEditLinks = true;
-
- /**
* @var string|null The URL to send in a <link> element with rel=license
*/
private $copyrightUrl;
@@ -314,21 +304,16 @@ class OutputPage extends ContextSource {
* Constructor for OutputPage. This should not be called directly.
* Instead a new RequestContext should be created and it will implicitly create
* a OutputPage tied to that context.
- * @param IContextSource|null $context
+ * @param IContextSource $context
*/
- function __construct( IContextSource $context = null ) {
- if ( $context === null ) {
- # Extensions should use `new RequestContext` instead of `new OutputPage` now.
- wfDeprecated( __METHOD__, '1.18' );
- } else {
- $this->setContext( $context );
- }
+ function __construct( IContextSource $context ) {
+ $this->setContext( $context );
}
/**
* Redirect to $url rather than displaying the normal page
*
- * @param string $url URL
+ * @param string $url
* @param string $responsecode HTTP status code
*/
public function redirect( $url, $responsecode = '302' ) {
@@ -371,8 +356,8 @@ class OutputPage extends ContextSource {
* Add a new "<meta>" tag
* To add an http-equiv meta tag, precede the name with "http:"
*
- * @param string $name Tag name
- * @param string $val Tag value
+ * @param string $name Name of the meta tag
+ * @param string $val Value of the meta tag
*/
function addMeta( $name, $val ) {
array_push( $this->mMetatags, [ $name, $val ] );
@@ -469,31 +454,6 @@ class OutputPage extends ContextSource {
}
/**
- * Register and add a stylesheet from an extension directory.
- *
- * @deprecated since 1.27 use addModuleStyles() or addStyle() instead
- * @param string $url Path to sheet. Provide either a full url (beginning
- * with 'http', etc) or a relative path from the document root
- * (beginning with '/'). Otherwise it behaves identically to
- * addStyle() and draws from the /skins folder.
- */
- public function addExtensionStyle( $url ) {
- wfDeprecated( __METHOD__, '1.27' );
- array_push( $this->mExtStyles, $url );
- }
-
- /**
- * Get all styles added by extensions
- *
- * @deprecated since 1.27
- * @return array
- */
- function getExtStyle() {
- wfDeprecated( __METHOD__, '1.27' );
- return $this->mExtStyles;
- }
-
- /**
* Add a JavaScript file out of skins/common, or a given relative path.
* Internal use only. Use OutputPage::addModules() if possible.
*
@@ -528,7 +488,7 @@ class OutputPage extends ContextSource {
* Filter an array of modules to remove insufficiently trustworthy members, and modules
* which are no longer registered (eg a page is cached before an extension is disabled)
* @param array $modules
- * @param string|null $position If not null, only return modules with this position
+ * @param string|null $position Unused
* @param string $type
* @return array
*/
@@ -541,7 +501,6 @@ class OutputPage extends ContextSource {
$module = $resourceLoader->getModule( $val );
if ( $module instanceof ResourceLoaderModule
&& $module->getOrigin() <= $this->getAllowedModules( $type )
- && ( is_null( $position ) || $module->getPosition() == $position )
) {
if ( $this->mTarget && !in_array( $this->mTarget, $module->getTargets() ) ) {
$this->warnModuleTargetFilter( $module->getName() );
@@ -572,7 +531,7 @@ class OutputPage extends ContextSource {
* Get the list of modules to include on this page
*
* @param bool $filter Whether to filter out insufficiently trustworthy modules
- * @param string|null $position If not null, only return modules with this position
+ * @param string|null $position Unused
* @param string $param
* @param string $type
* @return array Array of module names
@@ -582,7 +541,7 @@ class OutputPage extends ContextSource {
) {
$modules = array_values( array_unique( $this->$param ) );
return $filter
- ? $this->filterModules( $modules, $position, $type )
+ ? $this->filterModules( $modules, null, $type )
: $modules;
}
@@ -601,11 +560,11 @@ class OutputPage extends ContextSource {
* Get the list of module JS to include on this page
*
* @param bool $filter
- * @param string|null $position
+ * @param string|null $position Unused
* @return array Array of module names
*/
public function getModuleScripts( $filter = false, $position = null ) {
- return $this->getModules( $filter, $position, 'mModuleScripts',
+ return $this->getModules( $filter, null, 'mModuleScripts',
ResourceLoaderModule::TYPE_SCRIPTS
);
}
@@ -625,11 +584,11 @@ class OutputPage extends ContextSource {
* Get the list of module CSS to include on this page
*
* @param bool $filter
- * @param string|null $position
+ * @param string|null $position Unused
* @return array Array of module names
*/
public function getModuleStyles( $filter = false, $position = null ) {
- return $this->getModules( $filter, $position, 'mModuleStyles',
+ return $this->getModules( $filter, null, 'mModuleStyles',
ResourceLoaderModule::TYPE_STYLES
);
}
@@ -719,13 +678,6 @@ class OutputPage extends ContextSource {
}
/**
- * @deprecated since 1.28 Obsolete - wgUseETag experiment was removed.
- * @param string $tag
- */
- public function setETag( $tag ) {
- }
-
- /**
* Set whether the output should only contain the body of the article,
* without any skin, sidebar, etc.
* Used e.g. when calling with "action=render".
@@ -819,9 +771,9 @@ class OutputPage extends ContextSource {
# this breaks strtotime().
$clientHeader = preg_replace( '/;.*$/', '', $clientHeader );
- MediaWiki\suppressWarnings(); // E_STRICT system time bitching
+ Wikimedia\suppressWarnings(); // E_STRICT system time bitching
$clientHeaderTime = strtotime( $clientHeader );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$clientHeaderTime ) {
wfDebug( __METHOD__
. ": unable to parse the client's If-Modified-Since header: $clientHeader\n" );
@@ -1578,15 +1530,18 @@ class OutputPage extends ContextSource {
* Get/set the ParserOptions object to use for wikitext parsing
*
* @param ParserOptions|null $options Either the ParserOption to use or null to only get the
- * current ParserOption object
+ * current ParserOption object. This parameter is deprecated since 1.31.
* @return ParserOptions
*/
public function parserOptions( $options = null ) {
+ if ( $options !== null ) {
+ wfDeprecated( __METHOD__ . ' with non-null $options', '1.31' );
+ }
+
if ( $options !== null && !empty( $options->isBogus ) ) {
// Someone is trying to set a bogus pre-$wgUser PO. Check if it has
// been changed somehow, and keep it if so.
$anonPO = ParserOptions::newFromAnon();
- $anonPO->setEditSection( false );
$anonPO->setAllowUnsafeRawHtml( false );
if ( !$options->matches( $anonPO ) ) {
wfLogWarning( __METHOD__ . ': Setting a changed bogus ParserOptions: ' . wfGetAllCallers( 5 ) );
@@ -1600,7 +1555,6 @@ class OutputPage extends ContextSource {
// ParserOptions for it. And don't cache this ParserOptions
// either.
$po = ParserOptions::newFromAnon();
- $po->setEditSection( false );
$po->setAllowUnsafeRawHtml( false );
$po->isBogus = true;
if ( $options !== null ) {
@@ -1610,7 +1564,6 @@ class OutputPage extends ContextSource {
}
$this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
- $this->mParserOptions->setEditSection( false );
$this->mParserOptions->setAllowUnsafeRawHtml( false );
}
@@ -1784,7 +1737,9 @@ class OutputPage extends ContextSource {
$popts->setTidy( $oldTidy );
- $this->addParserOutput( $parserOutput );
+ $this->addParserOutput( $parserOutput, [
+ 'enableSectionEditLinks' => false,
+ ] );
}
/**
@@ -1852,13 +1807,13 @@ class OutputPage extends ContextSource {
// Avoid PHP 7.1 warning of passing $this by reference
$outputPage = $this;
Hooks::run( 'LanguageLinks', [ $this->getTitle(), &$this->mLanguageLinks, &$linkFlags ] );
- Hooks::run( 'OutputPageParserOutput', [ &$outputPage, $parserOutput ] );
+ Hooks::runWithoutAbort( 'OutputPageParserOutput', [ &$outputPage, $parserOutput ] );
// This check must be after 'OutputPageParserOutput' runs in addParserOutputMetadata
// so that extensions may modify ParserOutput to toggle TOC.
// This cannot be moved to addParserOutputText because that is not
// called by EditPage for Preview.
- if ( $parserOutput->getTOCEnabled() && $parserOutput->getTOCHTML() ) {
+ if ( $parserOutput->getTOCHTML() ) {
$this->mEnableTOC = true;
}
}
@@ -1869,9 +1824,10 @@ class OutputPage extends ContextSource {
*
* @since 1.24
* @param ParserOutput $parserOutput
+ * @param array $poOptions Options to ParserOutput::getText()
*/
- public function addParserOutputContent( $parserOutput ) {
- $this->addParserOutputText( $parserOutput );
+ public function addParserOutputContent( $parserOutput, $poOptions = [] ) {
+ $this->addParserOutputText( $parserOutput, $poOptions );
$this->addModules( $parserOutput->getModules() );
$this->addModuleScripts( $parserOutput->getModuleScripts() );
@@ -1885,12 +1841,13 @@ class OutputPage extends ContextSource {
*
* @since 1.24
* @param ParserOutput $parserOutput
+ * @param array $poOptions Options to ParserOutput::getText()
*/
- public function addParserOutputText( $parserOutput ) {
- $text = $parserOutput->getText();
+ public function addParserOutputText( $parserOutput, $poOptions = [] ) {
+ $text = $parserOutput->getText( $poOptions );
// Avoid PHP 7.1 warning of passing $this by reference
$outputPage = $this;
- Hooks::run( 'OutputPageBeforeHTML', [ &$outputPage, &$text ] );
+ Hooks::runWithoutAbort( 'OutputPageBeforeHTML', [ &$outputPage, &$text ] );
$this->addHTML( $text );
}
@@ -1898,16 +1855,11 @@ class OutputPage extends ContextSource {
* Add everything from a ParserOutput object.
*
* @param ParserOutput $parserOutput
+ * @param array $poOptions Options to ParserOutput::getText()
*/
- function addParserOutput( $parserOutput ) {
+ function addParserOutput( $parserOutput, $poOptions = [] ) {
$this->addParserOutputMetadata( $parserOutput );
-
- // Touch section edit links only if not previously disabled
- if ( $parserOutput->getEditSectionTokens() ) {
- $parserOutput->setEditSectionTokens( $this->mEnableSectionEditLinks );
- }
-
- $this->addParserOutputText( $parserOutput );
+ $this->addParserOutputText( $parserOutput, $poOptions );
}
/**
@@ -1958,7 +1910,9 @@ class OutputPage extends ContextSource {
$popts->setTargetLanguage( $oldLang );
}
- return $parserOutput->getText();
+ return $parserOutput->getText( [
+ 'enableSectionEditLinks' => false,
+ ] );
}
/**
@@ -1977,14 +1931,6 @@ class OutputPage extends ContextSource {
}
/**
- * @param int $maxage
- * @deprecated since 1.27 Use setCdnMaxage() instead
- */
- public function setSquidMaxage( $maxage ) {
- $this->setCdnMaxage( $maxage );
- }
-
- /**
* Set the value of the "s-maxage" part of the "Cache-control" HTTP header
*
* @param int $maxage Maximum cache time on the CDN, in seconds.
@@ -2200,7 +2146,7 @@ class OutputPage extends ContextSource {
// IE and some other browsers use BCP 47 standards in
// their Accept-Language header, like "zh-CN" or "zh-Hant".
// We should handle these too.
- $variantBCP47 = wfBCP47( $variant );
+ $variantBCP47 = LanguageCode::bcp47( $variant );
if ( $variantBCP47 !== $variant ) {
$aloption[] = 'substr=' . $variantBCP47;
}
@@ -2436,7 +2382,7 @@ class OutputPage extends ContextSource {
$outputPage = $this;
// Hook that allows last minute changes to the output page, e.g.
// adding of CSS or Javascript by extensions.
- Hooks::run( 'BeforePageDisplay', [ &$outputPage, &$sk ] );
+ Hooks::runWithoutAbort( 'BeforePageDisplay', [ &$outputPage, &$sk ] );
try {
$sk->outputPage();
@@ -2448,7 +2394,7 @@ class OutputPage extends ContextSource {
try {
// This hook allows last minute changes to final overall output by modifying output buffer
- Hooks::run( 'AfterFinalPageOutput', [ $this ] );
+ Hooks::runWithoutAbort( 'AfterFinalPageOutput', [ $this ] );
} catch ( Exception $e ) {
ob_end_clean(); // bug T129657
throw $e;
@@ -2648,36 +2594,6 @@ class OutputPage extends ContextSource {
}
/**
- * Display a page stating that the Wiki is in read-only mode.
- * Should only be called after wfReadOnly() has returned true.
- *
- * Historically, this function was used to show the source of the page that the user
- * was trying to edit and _also_ permissions error messages. The relevant code was
- * moved into EditPage in 1.19 (r102024 / d83c2a431c2a) and removed here in 1.25.
- *
- * @deprecated since 1.25; throw the exception directly
- * @throws ReadOnlyError
- */
- public function readOnlyPage() {
- if ( func_num_args() > 0 ) {
- throw new MWException( __METHOD__ . ' no longer accepts arguments since 1.25.' );
- }
-
- throw new ReadOnlyError;
- }
-
- /**
- * Turn off regular page output and return an error response
- * for when rate limiting has triggered.
- *
- * @deprecated since 1.25; throw the exception directly
- */
- public function rateLimited() {
- wfDeprecated( __METHOD__, '1.25' );
- throw new ThrottledError;
- }
-
- /**
* Show a warning about replica DB lag
*
* If the lag is higher than $wgSlaveLagCritical seconds,
@@ -2872,7 +2788,9 @@ class OutputPage extends ContextSource {
$this->rlUserModuleState = $exemptStates['user'] = $userState;
}
- $rlClient = new ResourceLoaderClientHtml( $context, $this->getTarget() );
+ $rlClient = new ResourceLoaderClientHtml( $context, [
+ 'target' => $this->getTarget(),
+ ] );
$rlClient->setConfig( $this->getJSVars() );
$rlClient->setModules( $this->getModules( /*filter*/ true ) );
$rlClient->setModuleStyles( $moduleStyles );
@@ -2923,15 +2841,14 @@ class OutputPage extends ContextSource {
$pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) );
$pieces = array_merge( $pieces, array_values( $this->mHeadItems ) );
- $min = ResourceLoader::inDebugMode() ? '' : '.min';
// Use an IE conditional comment to serve the script only to old IE
$pieces[] = '<!--[if lt IE 9]>' .
- Html::element( 'script', [
- 'src' => self::transformResourcePath(
- $this->getConfig(),
- "/resources/lib/html5shiv/html5shiv{$min}.js"
- ),
- ] ) .
+ ResourceLoaderClientHtml::makeLoad(
+ ResourceLoaderContext::newDummyContext(),
+ [ 'html5shiv' ],
+ ResourceLoaderModule::TYPE_SCRIPTS,
+ [ 'sync' => true ]
+ ) .
'<![endif]-->';
$pieces[] = Html::closeElement( 'head' );
@@ -3030,20 +2947,20 @@ class OutputPage extends ContextSource {
private function isUserJsPreview() {
return $this->getConfig()->get( 'AllowUserJs' )
&& $this->getTitle()
- && $this->getTitle()->isJsSubpage()
+ && $this->getTitle()->isUserJsConfigPage()
&& $this->userCanPreview();
}
protected function isUserCssPreview() {
return $this->getConfig()->get( 'AllowUserCss' )
&& $this->getTitle()
- && $this->getTitle()->isCssSubpage()
+ && $this->getTitle()->isUserCssConfigPage()
&& $this->userCanPreview();
}
/**
- * JS stuff to put at the bottom of the `<body>`. These are modules with position 'bottom',
- * legacy scripts ($this->mScripts), and user JS.
+ * JS stuff to put at the bottom of the `<body>`.
+ * These are legacy scripts ($this->mScripts), and user JS.
*
* @return string|WrappedStringList HTML
*/
@@ -3232,6 +3149,8 @@ class OutputPage extends ContextSource {
&& ( $relevantTitle->exists() || $relevantTitle->quickUserCan( 'create', $user ) );
foreach ( $title->getRestrictionTypes() as $type ) {
+ // Following keys are set in $vars:
+ // wgRestrictionCreate, wgRestrictionEdit, wgRestrictionMove, wgRestrictionUpload
$vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
}
@@ -3287,7 +3206,10 @@ class OutputPage extends ContextSource {
}
$title = $this->getTitle();
- if ( !$title->isJsSubpage() && !$title->isCssSubpage() ) {
+ if (
+ !$title->isUserJsConfigPage()
+ && !$title->isUserCssConfigPage()
+ ) {
return false;
}
if ( !$title->isSubpageOf( $user->getUserPage() ) ) {
@@ -3320,10 +3242,14 @@ class OutputPage extends ContextSource {
] );
if ( $config->get( 'ReferrerPolicy' ) !== false ) {
- $tags['meta-referrer'] = Html::element( 'meta', [
- 'name' => 'referrer',
- 'content' => $config->get( 'ReferrerPolicy' )
- ] );
+ // Per https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
+ // fallbacks should come before the primary value so we need to reverse the array.
+ foreach ( array_reverse( (array)$config->get( 'ReferrerPolicy' ) ) as $i => $policy ) {
+ $tags["meta-referrer-$i"] = Html::element( 'meta', [
+ 'name' => 'referrer',
+ 'content' => $policy,
+ ] );
+ }
}
$p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
@@ -3438,7 +3364,7 @@ class OutputPage extends ContextSource {
foreach ( $variants as $variant ) {
$tags["variant-$variant"] = Html::element( 'link', [
'rel' => 'alternate',
- 'hreflang' => wfBCP47( $variant ),
+ 'hreflang' => LanguageCode::bcp47( $variant ),
'href' => $this->getTitle()->getLocalURL(
[ 'variant' => $variant ] )
]
@@ -3696,12 +3622,6 @@ class OutputPage extends ContextSource {
public function buildCssLinksArray() {
$links = [];
- // Add any extension CSS
- foreach ( $this->mExtStyles as $url ) {
- $this->addStyle( $url );
- }
- $this->mExtStyles = [];
-
foreach ( $this->styles as $file => $options ) {
$link = $this->styleLink( $file, $options );
if ( $link ) {
@@ -3801,7 +3721,7 @@ class OutputPage extends ContextSource {
$remotePathPrefix = $remotePath = $uploadPath;
}
- $path = RelPath\getRelativePath( $path, $remotePath );
+ $path = RelPath::getRelativePath( $path, $remotePath );
return self::transformFilePath( $remotePathPrefix, $localDir, $path );
}
@@ -3958,17 +3878,20 @@ class OutputPage extends ContextSource {
* Enables/disables section edit links, doesn't override __NOEDITSECTION__
* @param bool $flag
* @since 1.23
+ * @deprecated since 1.31, use $poOptions to addParserOutput() instead.
*/
public function enableSectionEditLinks( $flag = true ) {
- $this->mEnableSectionEditLinks = $flag;
+ wfDeprecated( __METHOD__, '1.31' );
}
/**
* @return bool
* @since 1.23
+ * @deprecated since 1.31, use $poOptions to addParserOutput() instead.
*/
public function sectionEditLinksEnabled() {
- return $this->mEnableSectionEditLinks;
+ wfDeprecated( __METHOD__, '1.31' );
+ return true;
}
/**
@@ -4027,6 +3950,13 @@ class OutputPage extends ContextSource {
return;
}
+ if ( isset( $logo['svg'] ) ) {
+ // No media queries required if we only have a 1x and svg variant
+ // because all preload-capable browsers support SVGs
+ $this->addLinkHeader( '<' . $logo['svg'] . '>;rel=preload;as=image' );
+ return;
+ }
+
foreach ( $logo as $dppx => $src ) {
// Keys are in this format: "1.5x"
$dppx = substr( $dppx, 0, -1 );
diff --git a/www/wiki/includes/PHPVersionCheck.php b/www/wiki/includes/PHPVersionCheck.php
index 66b7158b..609b75be 100644
--- a/www/wiki/includes/PHPVersionCheck.php
+++ b/www/wiki/includes/PHPVersionCheck.php
@@ -1,7 +1,4 @@
<?php
-// @codingStandardsIgnoreFile Generic.Arrays.DisallowLongArraySyntax
-// @codingStandardsIgnoreFile Generic.Files.LineLength
-// @codingStandardsIgnoreFile MediaWiki.Usage.DirUsage.FunctionFound
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,6 +18,7 @@
* @file
*/
+// phpcs:disable Generic.Arrays.DisallowLongArraySyntax,PSR2.Classes.PropertyDeclaration,MediaWiki.Usage.DirUsage
/**
* Check PHP Version, as well as for composer dependencies in entry points,
* and display something vaguely comprehensible in the event of a totally
@@ -29,10 +27,10 @@
*/
class PHPVersionCheck {
/* @var string The number of the MediaWiki version used */
- var $mwVersion = '1.30';
+ var $mwVersion = '1.31';
var $functionsExtensionsMapping = array(
'mb_substr' => 'mbstring',
- 'utf8_encode' => 'xml',
+ 'xml_parser_create' => 'xml',
'ctype_digit' => 'ctype',
'json_decode' => 'json',
'iconv' => 'iconv',
@@ -56,7 +54,6 @@ class PHPVersionCheck {
* - api.php
* - mw-config/index.php
* - cli
- * @return $this
*/
function setEntryPoint( $entryPoint ) {
$this->entryPoint = $entryPoint;
@@ -87,8 +84,8 @@ class PHPVersionCheck {
'implementation' => 'HHVM',
'version' => defined( 'HHVM_VERSION' ) ? HHVM_VERSION : 'undefined',
'vendor' => 'Facebook',
- 'upstreamSupported' => '3.6.5',
- 'minSupported' => '3.6.5',
+ 'upstreamSupported' => '3.18.5',
+ 'minSupported' => '3.18.5',
'upgradeURL' => 'https://docs.hhvm.com/hhvm/installation/introduction',
);
}
@@ -96,16 +93,14 @@ class PHPVersionCheck {
'implementation' => 'PHP',
'version' => PHP_VERSION,
'vendor' => 'the PHP Group',
- 'upstreamSupported' => '5.5.0',
- 'minSupported' => '5.5.9',
+ 'upstreamSupported' => '5.6.0',
+ 'minSupported' => '7.0.13',
'upgradeURL' => 'https://secure.php.net/downloads.php',
);
}
/**
* Displays an error, if the installed php version does not meet the minimum requirement.
- *
- * @return $this
*/
function checkRequiredPHPVersion() {
$phpInfo = $this->getPHPInfo();
@@ -120,26 +115,29 @@ class PHPVersionCheck {
. "{$otherInfo['minSupported']}, you are using {$phpInfo['implementation']} "
. "{$phpInfo['version']}.";
- $longText = "Error: You might be using an older {$phpInfo['implementation']} version. \n"
+ $longText = "Error: You might be using an older {$phpInfo['implementation']} version "
+ . "({$phpInfo['implementation']} {$phpInfo['version']}). \n"
. "MediaWiki $this->mwVersion needs {$phpInfo['implementation']}"
. " $minimumVersion or higher or {$otherInfo['implementation']} version "
. "{$otherInfo['minSupported']}.\n\nCheck if you have a"
- . " newer php executable with a different name, such as php5.\n\n";
+ . " newer php executable with a different name.\n\n";
+ // phpcs:disable Generic.Files.LineLength
$longHtml = <<<HTML
Please consider <a href="{$phpInfo['upgradeURL']}">upgrading your copy of
{$phpInfo['implementation']}</a>.
- {$phpInfo['implementation']} versions less than {$phpInfo['upstreamSupported']} are no
+ {$phpInfo['implementation']} versions less than {$phpInfo['upstreamSupported']} are no
longer supported by {$phpInfo['vendor']} and will not receive
security or bugfix updates.
</p>
<p>
If for some reason you are unable to upgrade your {$phpInfo['implementation']} version,
- you will need to <a href="https://www.mediawiki.org/wiki/Download">download</a> an
+ you will need to <a href="https://www.mediawiki.org/wiki/Download">download</a> an
older version of MediaWiki from our website.
See our <a href="https://www.mediawiki.org/wiki/Compatibility#PHP">compatibility page</a>
for details of which versions are compatible with prior versions of {$phpInfo['implementation']}.
HTML;
+ // phpcs:enable Generic.Files.LineLength
$this->triggerError(
"Supported {$phpInfo['implementation']} versions",
$shortText,
@@ -151,8 +149,6 @@ HTML;
/**
* Displays an error, if the vendor/autoload.php file could not be found.
- *
- * @return $this
*/
function checkVendorExistence() {
if ( !file_exists( dirname( __FILE__ ) . '/../vendor/autoload.php' ) ) {
@@ -164,12 +160,14 @@ HTML;
. "https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries\n"
. "for help on installing the required components.";
+ // phpcs:disable Generic.Files.LineLength
$longHtml = <<<HTML
MediaWiki now also has some external dependencies that need to be installed via
composer or from a separate git repo. Please see
<a href="https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries">mediawiki.org</a>
for help on installing the required components.
HTML;
+ // phpcs:enable Generic.Files.LineLength
$this->triggerError( 'External dependencies', $shortText, $longText, $longHtml );
}
@@ -177,8 +175,6 @@ HTML;
/**
* Displays an error, if a PHP extension does not exist.
- *
- * @return $this
*/
function checkExtensionExistence() {
$missingExtensions = array();
diff --git a/www/wiki/includes/PHPVersionError.php b/www/wiki/includes/PHPVersionError.php
deleted file mode 100644
index 9fbcf895..00000000
--- a/www/wiki/includes/PHPVersionError.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-/**
- * Backwards compatibility. The PHP version error function is now
- * included in PHPVersionCheck.php.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @deprecated 1.25
- * @file
- */
-// @codingStandardsIgnoreStart MediaWiki.Usage.DirUsage.FunctionFound
-require_once dirname( __FILE__ ) . '/PHPVersionCheck.php';
-// @codingStandardsIgnoreEnd
diff --git a/www/wiki/includes/Pingback.php b/www/wiki/includes/Pingback.php
index c3393bcc..bf2123fa 100644
--- a/www/wiki/includes/Pingback.php
+++ b/www/wiki/includes/Pingback.php
@@ -68,14 +68,25 @@ class Pingback {
}
/**
- * Has a pingback already been sent for this MediaWiki version?
+ * Has a pingback been sent in the last month for this MediaWiki version?
* @return bool
*/
private function checkIfSent() {
$dbr = wfGetDB( DB_REPLICA );
- $sent = $dbr->selectField(
- 'updatelog', '1', [ 'ul_key' => $this->key ], __METHOD__ );
- return $sent !== false;
+ $timestamp = $dbr->selectField(
+ 'updatelog',
+ 'ul_value',
+ [ 'ul_key' => $this->key ],
+ __METHOD__
+ );
+ if ( $timestamp === false ) {
+ return false;
+ }
+ // send heartbeat ping if last ping was over a month ago
+ if ( time() - (int)$timestamp > 60 * 60 * 24 * 30 ) {
+ return false;
+ }
+ return true;
}
/**
@@ -84,8 +95,14 @@ class Pingback {
*/
private function markSent() {
$dbw = wfGetDB( DB_MASTER );
- return $dbw->insert(
- 'updatelog', [ 'ul_key' => $this->key ], __METHOD__, 'IGNORE' );
+ $timestamp = time();
+ return $dbw->upsert(
+ 'updatelog',
+ [ 'ul_key' => $this->key, 'ul_value' => $timestamp ],
+ [ 'ul_key' ],
+ [ 'ul_value' => $timestamp ],
+ __METHOD__
+ );
}
/**
diff --git a/www/wiki/includes/PreConfigSetup.php b/www/wiki/includes/PreConfigSetup.php
deleted file mode 100644
index bda78865..00000000
--- a/www/wiki/includes/PreConfigSetup.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-/**
- * File-scope setup actions, loaded before LocalSettings.php, shared by
- * WebStart.php and doMaintenance.php
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- *
- * @file
- */
-
-if ( !defined( 'MEDIAWIKI' ) ) {
- // Not an entry point
- exit( 1 );
-}
-
-// Grab profiling functions
-require_once "$IP/includes/profiler/ProfilerFunctions.php";
-
-// Start the autoloader, so that extensions can derive classes from core files
-require_once "$IP/includes/AutoLoader.php";
-
-// Load up some global defines.
-require_once "$IP/includes/Defines.php";
-
-// Start the profiler
-$wgProfiler = [];
-if ( file_exists( "$IP/StartProfiler.php" ) ) {
- require "$IP/StartProfiler.php";
-}
-
-// Load default settings
-require_once "$IP/includes/DefaultSettings.php";
-
-// Load global functions
-require_once "$IP/includes/GlobalFunctions.php";
-
-// Load composer's autoloader if present
-if ( is_readable( "$IP/vendor/autoload.php" ) ) {
- require_once "$IP/vendor/autoload.php";
-}
diff --git a/www/wiki/includes/Preferences.php b/www/wiki/includes/Preferences.php
index a7e6684b..26e28baf 100644
--- a/www/wiki/includes/Preferences.php
+++ b/www/wiki/includes/Preferences.php
@@ -17,58 +17,39 @@
*
* @file
*/
+
use MediaWiki\Auth\AuthManager;
-use MediaWiki\Auth\PasswordAuthenticationRequest;
use MediaWiki\MediaWikiServices;
+use MediaWiki\Preferences\DefaultPreferencesFactory;
/**
- * We're now using the HTMLForm object with some customisation to generate the
- * Preferences form. This object handles generic submission, CSRF protection,
- * layout and other logic in a reusable manner. We subclass it as a PreferencesForm
- * to make some minor customisations.
- *
- * In order to generate the form, the HTMLForm object needs an array structure
- * detailing the form fields available, and that's what this class is for. Each
- * element of the array is a basic property-list, including the type of field,
- * the label it is to be given in the form, callbacks for validation and
- * 'filtering', and other pertinent information. Note that the 'default' field
- * is named for generic forms, and does not represent the preference's default
- * (which is stored in $wgDefaultUserOptions), but the default for the form
- * field, which should be whatever the user has set for that preference. There
- * is no need to override it unless you have some special storage logic (for
- * instance, those not presently stored as options, but which are best set from
- * the user preferences view).
- *
- * Field types are implemented as subclasses of the generic HTMLFormField
- * object, and typically implement at least getInputHTML, which generates the
- * HTML for the input field to be placed in the table.
+ * This class has been replaced by the PreferencesFactory service.
*
- * Once fields have been retrieved and validated, submission logic is handed
- * over to the tryUISubmit static method of this class.
+ * @deprecated since 1.31 use the PreferencesFactory service instead.
*/
class Preferences {
- /** @var array */
- protected static $defaultPreferences = null;
- /** @var array */
- protected static $saveFilters = [
- 'timecorrection' => [ 'Preferences', 'filterTimezoneInput' ],
- 'rclimit' => [ 'Preferences', 'filterIntval' ],
- 'wllimit' => [ 'Preferences', 'filterIntval' ],
- 'searchlimit' => [ 'Preferences', 'filterIntval' ],
- ];
-
- // Stuff that shouldn't be saved as a preference.
- private static $saveBlacklist = [
- 'realname',
- 'emailaddress',
- ];
+ /**
+ * A shim to maintain backwards-compatibility of this class, basically replicating the
+ * default behaviour of the PreferencesFactory service but not permitting overriding.
+ * @return DefaultPreferencesFactory
+ */
+ protected static function getDefaultPreferencesFactory() {
+ global $wgContLang;
+ $authManager = AuthManager::singleton();
+ $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+ $config = MediaWikiServices::getInstance()->getMainConfig();
+ $preferencesFactory = new DefaultPreferencesFactory(
+ $config, $wgContLang, $authManager, $linkRenderer
+ );
+ return $preferencesFactory;
+ }
/**
* @return array
*/
- static function getSaveBlacklist() {
- return self::$saveBlacklist;
+ public static function getSaveBlacklist() {
+ throw new Exception( __METHOD__ . '() is deprecated and does nothing' );
}
/**
@@ -77,29 +58,9 @@ class Preferences {
* @param IContextSource $context
* @return array|null
*/
- static function getPreferences( $user, IContextSource $context ) {
- if ( self::$defaultPreferences ) {
- return self::$defaultPreferences;
- }
-
- $defaultPreferences = [];
-
- self::profilePreferences( $user, $context, $defaultPreferences );
- self::skinPreferences( $user, $context, $defaultPreferences );
- self::datetimePreferences( $user, $context, $defaultPreferences );
- self::filesPreferences( $user, $context, $defaultPreferences );
- self::renderingPreferences( $user, $context, $defaultPreferences );
- self::editingPreferences( $user, $context, $defaultPreferences );
- self::rcPreferences( $user, $context, $defaultPreferences );
- self::watchlistPreferences( $user, $context, $defaultPreferences );
- self::searchPreferences( $user, $context, $defaultPreferences );
- self::miscPreferences( $user, $context, $defaultPreferences );
-
- Hooks::run( 'GetPreferences', [ $user, &$defaultPreferences ] );
-
- self::loadPreferenceValues( $user, $context, $defaultPreferences );
- self::$defaultPreferences = $defaultPreferences;
- return $defaultPreferences;
+ public static function getPreferences( $user, IContextSource $context ) {
+ $preferencesFactory = self::getDefaultPreferencesFactory();
+ return $preferencesFactory->getFormDescriptor( $user, $context );
}
/**
@@ -110,46 +71,8 @@ class Preferences {
* @param array &$defaultPreferences Array to load values for
* @return array|null
*/
- static function loadPreferenceValues( $user, $context, &$defaultPreferences ) {
- # # Remove preferences that wikis don't want to use
- foreach ( $context->getConfig()->get( 'HiddenPrefs' ) as $pref ) {
- if ( isset( $defaultPreferences[$pref] ) ) {
- unset( $defaultPreferences[$pref] );
- }
- }
-
- # # Make sure that form fields have their parent set. See T43337.
- $dummyForm = new HTMLForm( [], $context );
-
- $disable = !$user->isAllowed( 'editmyoptions' );
-
- $defaultOptions = User::getDefaultOptions();
- # # Prod in defaults from the user
- foreach ( $defaultPreferences as $name => &$info ) {
- $prefFromUser = self::getOptionFromUser( $name, $info, $user );
- if ( $disable && !in_array( $name, self::$saveBlacklist ) ) {
- $info['disabled'] = 'disabled';
- }
- $field = HTMLForm::loadInputFromParameters( $name, $info, $dummyForm ); // For validation
- $globalDefault = isset( $defaultOptions[$name] )
- ? $defaultOptions[$name]
- : null;
-
- // If it validates, set it as the default
- if ( isset( $info['default'] ) ) {
- // Already set, no problem
- continue;
- } elseif ( !is_null( $prefFromUser ) && // Make sure we're not just pulling nothing
- $field->validate( $prefFromUser, $user->getOptions() ) === true ) {
- $info['default'] = $prefFromUser;
- } elseif ( $field->validate( $globalDefault, $user->getOptions() ) === true ) {
- $info['default'] = $globalDefault;
- } else {
- throw new MWException( "Global default '$globalDefault' is invalid for field $name" );
- }
- }
-
- return $defaultPreferences;
+ public static function loadPreferenceValues( $user, $context, &$defaultPreferences ) {
+ throw new Exception( __METHOD__ . '() is deprecated and does nothing' );
}
/**
@@ -160,41 +83,8 @@ class Preferences {
* @param User $user
* @return array|string
*/
- static function getOptionFromUser( $name, $info, $user ) {
- $val = $user->getOption( $name );
-
- // Handling for multiselect preferences
- if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
- ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
- $options = HTMLFormField::flattenOptions( $info['options'] );
- $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
- $val = [];
-
- foreach ( $options as $value ) {
- if ( $user->getOption( "$prefix$value" ) ) {
- $val[] = $value;
- }
- }
- }
-
- // Handling for checkmatrix preferences
- if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
- ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
- $columns = HTMLFormField::flattenOptions( $info['columns'] );
- $rows = HTMLFormField::flattenOptions( $info['rows'] );
- $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
- $val = [];
-
- foreach ( $columns as $column ) {
- foreach ( $rows as $row ) {
- if ( $user->getOption( "$prefix$column-$row" ) ) {
- $val[] = "$column-$row";
- }
- }
- }
- }
-
- return $val;
+ public static function getOptionFromUser( $name, $info, $user ) {
+ throw new Exception( __METHOD__ . '() is deprecated and does nothing' );
}
/**
@@ -203,411 +93,11 @@ class Preferences {
* @param array &$defaultPreferences
* @return void
*/
- static function profilePreferences( $user, IContextSource $context, &$defaultPreferences ) {
- global $wgContLang, $wgParser;
-
- $authManager = AuthManager::singleton();
- $config = $context->getConfig();
- // retrieving user name for GENDER and misc.
- $userName = $user->getName();
-
- # # User info #####################################
- // Information panel
- $defaultPreferences['username'] = [
- 'type' => 'info',
- 'label-message' => [ 'username', $userName ],
- 'default' => $userName,
- 'section' => 'personal/info',
- ];
-
- $lang = $context->getLanguage();
-
- # Get groups to which the user belongs
- $userEffectiveGroups = $user->getEffectiveGroups();
- $userGroupMemberships = $user->getGroupMemberships();
- $userGroups = $userMembers = $userTempGroups = $userTempMembers = [];
- foreach ( $userEffectiveGroups as $ueg ) {
- if ( $ueg == '*' ) {
- // Skip the default * group, seems useless here
- continue;
- }
-
- if ( isset( $userGroupMemberships[$ueg] ) ) {
- $groupStringOrObject = $userGroupMemberships[$ueg];
- } else {
- $groupStringOrObject = $ueg;
- }
-
- $userG = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html' );
- $userM = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html',
- $userName );
-
- // Store expiring groups separately, so we can place them before non-expiring
- // groups in the list. This is to avoid the ambiguity of something like
- // "administrator, bureaucrat (until X date)" -- users might wonder whether the
- // expiry date applies to both groups, or just the last one
- if ( $groupStringOrObject instanceof UserGroupMembership &&
- $groupStringOrObject->getExpiry()
- ) {
- $userTempGroups[] = $userG;
- $userTempMembers[] = $userM;
- } else {
- $userGroups[] = $userG;
- $userMembers[] = $userM;
- }
- }
- sort( $userGroups );
- sort( $userMembers );
- sort( $userTempGroups );
- sort( $userTempMembers );
- $userGroups = array_merge( $userTempGroups, $userGroups );
- $userMembers = array_merge( $userTempMembers, $userMembers );
-
- $defaultPreferences['usergroups'] = [
- 'type' => 'info',
- 'label' => $context->msg( 'prefs-memberingroups' )->numParams(
- count( $userGroups ) )->params( $userName )->parse(),
- 'default' => $context->msg( 'prefs-memberingroups-type' )
- ->rawParams( $lang->commaList( $userGroups ), $lang->commaList( $userMembers ) )
- ->escaped(),
- 'raw' => true,
- 'section' => 'personal/info',
- ];
-
- $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
-
- $editCount = $linkRenderer->makeLink( SpecialPage::getTitleFor( "Contributions", $userName ),
- $lang->formatNum( $user->getEditCount() ) );
-
- $defaultPreferences['editcount'] = [
- 'type' => 'info',
- 'raw' => true,
- 'label-message' => 'prefs-edits',
- 'default' => $editCount,
- 'section' => 'personal/info',
- ];
-
- if ( $user->getRegistration() ) {
- $displayUser = $context->getUser();
- $userRegistration = $user->getRegistration();
- $defaultPreferences['registrationdate'] = [
- 'type' => 'info',
- 'label-message' => 'prefs-registration',
- 'default' => $context->msg(
- 'prefs-registration-date-time',
- $lang->userTimeAndDate( $userRegistration, $displayUser ),
- $lang->userDate( $userRegistration, $displayUser ),
- $lang->userTime( $userRegistration, $displayUser )
- )->parse(),
- 'section' => 'personal/info',
- ];
- }
-
- $canViewPrivateInfo = $user->isAllowed( 'viewmyprivateinfo' );
- $canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' );
-
- // Actually changeable stuff
- $defaultPreferences['realname'] = [
- // (not really "private", but still shouldn't be edited without permission)
- 'type' => $canEditPrivateInfo && $authManager->allowsPropertyChange( 'realname' )
- ? 'text' : 'info',
- 'default' => $user->getRealName(),
- 'section' => 'personal/info',
- 'label-message' => 'yourrealname',
- 'help-message' => 'prefs-help-realname',
- ];
-
- if ( $canEditPrivateInfo && $authManager->allowsAuthenticationDataChange(
- new PasswordAuthenticationRequest(), false )->isGood()
- ) {
- $link = $linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ),
- $context->msg( 'prefs-resetpass' )->text(), [],
- [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
-
- $defaultPreferences['password'] = [
- 'type' => 'info',
- 'raw' => true,
- 'default' => $link,
- 'label-message' => 'yourpassword',
- 'section' => 'personal/info',
- ];
- }
- // Only show prefershttps if secure login is turned on
- if ( $config->get( 'SecureLogin' ) && wfCanIPUseHTTPS( $context->getRequest()->getIP() ) ) {
- $defaultPreferences['prefershttps'] = [
- 'type' => 'toggle',
- 'label-message' => 'tog-prefershttps',
- 'help-message' => 'prefs-help-prefershttps',
- 'section' => 'personal/info'
- ];
- }
-
- // Language
- $languages = Language::fetchLanguageNames( null, 'mw' );
- $languageCode = $config->get( 'LanguageCode' );
- if ( !array_key_exists( $languageCode, $languages ) ) {
- $languages[$languageCode] = $languageCode;
- }
- ksort( $languages );
-
- $options = [];
- foreach ( $languages as $code => $name ) {
- $display = wfBCP47( $code ) . ' - ' . $name;
- $options[$display] = $code;
- }
- $defaultPreferences['language'] = [
- 'type' => 'select',
- 'section' => 'personal/i18n',
- 'options' => $options,
- 'label-message' => 'yourlanguage',
- ];
-
- $defaultPreferences['gender'] = [
- 'type' => 'radio',
- 'section' => 'personal/i18n',
- 'options' => [
- $context->msg( 'parentheses' )
- ->params( $context->msg( 'gender-unknown' )->plain() )
- ->escaped() => 'unknown',
- $context->msg( 'gender-female' )->escaped() => 'female',
- $context->msg( 'gender-male' )->escaped() => 'male',
- ],
- 'label-message' => 'yourgender',
- 'help-message' => 'prefs-help-gender',
- ];
-
- // see if there are multiple language variants to choose from
- if ( !$config->get( 'DisableLangConversion' ) ) {
- foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
- if ( $langCode == $wgContLang->getCode() ) {
- $variants = $wgContLang->getVariants();
-
- if ( count( $variants ) <= 1 ) {
- continue;
- }
-
- $variantArray = [];
- foreach ( $variants as $v ) {
- $v = str_replace( '_', '-', strtolower( $v ) );
- $variantArray[$v] = $lang->getVariantname( $v, false );
- }
-
- $options = [];
- foreach ( $variantArray as $code => $name ) {
- $display = wfBCP47( $code ) . ' - ' . $name;
- $options[$display] = $code;
- }
-
- $defaultPreferences['variant'] = [
- 'label-message' => 'yourvariant',
- 'type' => 'select',
- 'options' => $options,
- 'section' => 'personal/i18n',
- 'help-message' => 'prefs-help-variant',
- ];
- } else {
- $defaultPreferences["variant-$langCode"] = [
- 'type' => 'api',
- ];
- }
- }
- }
-
- // Stuff from Language::getExtraUserToggles()
- // FIXME is this dead code? $extraUserToggles doesn't seem to be defined for any language
- $toggles = $wgContLang->getExtraUserToggles();
-
- foreach ( $toggles as $toggle ) {
- $defaultPreferences[$toggle] = [
- 'type' => 'toggle',
- 'section' => 'personal/i18n',
- 'label-message' => "tog-$toggle",
- ];
- }
-
- // show a preview of the old signature first
- $oldsigWikiText = $wgParser->preSaveTransform(
- '~~~',
- $context->getTitle(),
- $user,
- ParserOptions::newFromContext( $context )
- );
- $oldsigHTML = $context->getOutput()->parseInline( $oldsigWikiText, true, true );
- $defaultPreferences['oldsig'] = [
- 'type' => 'info',
- 'raw' => true,
- 'label-message' => 'tog-oldsig',
- 'default' => $oldsigHTML,
- 'section' => 'personal/signature',
- ];
- $defaultPreferences['nickname'] = [
- 'type' => $authManager->allowsPropertyChange( 'nickname' ) ? 'text' : 'info',
- 'maxlength' => $config->get( 'MaxSigChars' ),
- 'label-message' => 'yournick',
- 'validation-callback' => [ 'Preferences', 'validateSignature' ],
- 'section' => 'personal/signature',
- 'filter-callback' => [ 'Preferences', 'cleanSignature' ],
- ];
- $defaultPreferences['fancysig'] = [
- 'type' => 'toggle',
- 'label-message' => 'tog-fancysig',
- // show general help about signature at the bottom of the section
- 'help-message' => 'prefs-help-signature',
- 'section' => 'personal/signature'
- ];
-
- # # Email stuff
-
- if ( $config->get( 'EnableEmail' ) ) {
- if ( $canViewPrivateInfo ) {
- $helpMessages[] = $config->get( 'EmailConfirmToEdit' )
- ? 'prefs-help-email-required'
- : 'prefs-help-email';
-
- if ( $config->get( 'EnableUserEmail' ) ) {
- // additional messages when users can send email to each other
- $helpMessages[] = 'prefs-help-email-others';
- }
-
- $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
- if ( $canEditPrivateInfo && $authManager->allowsPropertyChange( 'emailaddress' ) ) {
- $link = $linkRenderer->makeLink(
- SpecialPage::getTitleFor( 'ChangeEmail' ),
- $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
- [],
- [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
-
- $emailAddress .= $emailAddress == '' ? $link : (
- $context->msg( 'word-separator' )->escaped()
- . $context->msg( 'parentheses' )->rawParams( $link )->escaped()
- );
- }
-
- $defaultPreferences['emailaddress'] = [
- 'type' => 'info',
- 'raw' => true,
- 'default' => $emailAddress,
- 'label-message' => 'youremail',
- 'section' => 'personal/email',
- 'help-messages' => $helpMessages,
- # 'cssclass' chosen below
- ];
- }
-
- $disableEmailPrefs = false;
-
- if ( $config->get( 'EmailAuthentication' ) ) {
- $emailauthenticationclass = 'mw-email-not-authenticated';
- if ( $user->getEmail() ) {
- if ( $user->getEmailAuthenticationTimestamp() ) {
- // date and time are separate parameters to facilitate localisation.
- // $time is kept for backward compat reasons.
- // 'emailauthenticated' is also used in SpecialConfirmemail.php
- $displayUser = $context->getUser();
- $emailTimestamp = $user->getEmailAuthenticationTimestamp();
- $time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
- $d = $lang->userDate( $emailTimestamp, $displayUser );
- $t = $lang->userTime( $emailTimestamp, $displayUser );
- $emailauthenticated = $context->msg( 'emailauthenticated',
- $time, $d, $t )->parse() . '<br />';
- $disableEmailPrefs = false;
- $emailauthenticationclass = 'mw-email-authenticated';
- } else {
- $disableEmailPrefs = true;
- $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
- $linkRenderer->makeKnownLink(
- SpecialPage::getTitleFor( 'Confirmemail' ),
- $context->msg( 'emailconfirmlink' )->text()
- ) . '<br />';
- $emailauthenticationclass = "mw-email-not-authenticated";
- }
- } else {
- $disableEmailPrefs = true;
- $emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
- $emailauthenticationclass = 'mw-email-none';
- }
-
- if ( $canViewPrivateInfo ) {
- $defaultPreferences['emailauthentication'] = [
- 'type' => 'info',
- 'raw' => true,
- 'section' => 'personal/email',
- 'label-message' => 'prefs-emailconfirm-label',
- 'default' => $emailauthenticated,
- # Apply the same CSS class used on the input to the message:
- 'cssclass' => $emailauthenticationclass,
- ];
- }
- }
-
- if ( $config->get( 'EnableUserEmail' ) && $user->isAllowed( 'sendemail' ) ) {
- $defaultPreferences['disablemail'] = [
- 'type' => 'toggle',
- 'invert' => true,
- 'section' => 'personal/email',
- 'label-message' => 'allowemail',
- 'disabled' => $disableEmailPrefs,
- ];
- $defaultPreferences['ccmeonemails'] = [
- 'type' => 'toggle',
- 'section' => 'personal/email',
- 'label-message' => 'tog-ccmeonemails',
- 'disabled' => $disableEmailPrefs,
- ];
-
- if ( $config->get( 'EnableUserEmailBlacklist' )
- && !$disableEmailPrefs
- && !(bool)$user->getOption( 'disablemail' )
- ) {
- $lookup = CentralIdLookup::factory();
- $ids = $user->getOption( 'email-blacklist', [] );
- $names = $ids ? $lookup->namesFromCentralIds( $ids, $user ) : [];
-
- $defaultPreferences['email-blacklist'] = [
- 'type' => 'usersmultiselect',
- 'label-message' => 'email-blacklist-label',
- 'section' => 'personal/email',
- 'default' => implode( "\n", $names ),
- ];
- }
- }
-
- if ( $config->get( 'EnotifWatchlist' ) ) {
- $defaultPreferences['enotifwatchlistpages'] = [
- 'type' => 'toggle',
- 'section' => 'personal/email',
- 'label-message' => 'tog-enotifwatchlistpages',
- 'disabled' => $disableEmailPrefs,
- ];
- }
- if ( $config->get( 'EnotifUserTalk' ) ) {
- $defaultPreferences['enotifusertalkpages'] = [
- 'type' => 'toggle',
- 'section' => 'personal/email',
- 'label-message' => 'tog-enotifusertalkpages',
- 'disabled' => $disableEmailPrefs,
- ];
- }
- if ( $config->get( 'EnotifUserTalk' ) || $config->get( 'EnotifWatchlist' ) ) {
- if ( $config->get( 'EnotifMinorEdits' ) ) {
- $defaultPreferences['enotifminoredits'] = [
- 'type' => 'toggle',
- 'section' => 'personal/email',
- 'label-message' => 'tog-enotifminoredits',
- 'disabled' => $disableEmailPrefs,
- ];
- }
-
- if ( $config->get( 'EnotifRevealEditorAddress' ) ) {
- $defaultPreferences['enotifrevealaddr'] = [
- 'type' => 'toggle',
- 'section' => 'personal/email',
- 'label-message' => 'tog-enotifrevealaddr',
- 'disabled' => $disableEmailPrefs,
- ];
- }
- }
- }
+ public static function profilePreferences(
+ $user, IContextSource $context, &$defaultPreferences
+ ) {
+ wfDeprecated( __METHOD__, '1.31' );
+ $defaultPreferences = self::getPreferences( $user, $context );
}
/**
@@ -616,48 +106,9 @@ class Preferences {
* @param array &$defaultPreferences
* @return void
*/
- static function skinPreferences( $user, IContextSource $context, &$defaultPreferences ) {
- # # Skin #####################################
-
- // Skin selector, if there is at least one valid skin
- $skinOptions = self::generateSkinOptions( $user, $context );
- if ( $skinOptions ) {
- $defaultPreferences['skin'] = [
- 'type' => 'radio',
- 'options' => $skinOptions,
- 'section' => 'rendering/skin',
- ];
- }
-
- $config = $context->getConfig();
- $allowUserCss = $config->get( 'AllowUserCss' );
- $allowUserJs = $config->get( 'AllowUserJs' );
- # Create links to user CSS/JS pages for all skins
- # This code is basically copied from generateSkinOptions(). It'd
- # be nice to somehow merge this back in there to avoid redundancy.
- if ( $allowUserCss || $allowUserJs ) {
- $linkTools = [];
- $userName = $user->getName();
-
- $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
- if ( $allowUserCss ) {
- $cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
- $linkTools[] = $linkRenderer->makeLink( $cssPage, $context->msg( 'prefs-custom-css' )->text() );
- }
-
- if ( $allowUserJs ) {
- $jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
- $linkTools[] = $linkRenderer->makeLink( $jsPage, $context->msg( 'prefs-custom-js' )->text() );
- }
-
- $defaultPreferences['commoncssjs'] = [
- 'type' => 'info',
- 'raw' => true,
- 'default' => $context->getLanguage()->pipeList( $linkTools ),
- 'label-message' => 'prefs-common-css-js',
- 'section' => 'rendering/skin',
- ];
- }
+ public static function skinPreferences( $user, IContextSource $context, &$defaultPreferences ) {
+ wfDeprecated( __METHOD__, '1.31' );
+ $defaultPreferences = self::getPreferences( $user, $context );
}
/**
@@ -665,20 +116,11 @@ class Preferences {
* @param IContextSource $context
* @param array &$defaultPreferences
*/
- static function filesPreferences( $user, IContextSource $context, &$defaultPreferences ) {
- # # Files #####################################
- $defaultPreferences['imagesize'] = [
- 'type' => 'select',
- 'options' => self::getImageSizes( $context ),
- 'label-message' => 'imagemaxsize',
- 'section' => 'rendering/files',
- ];
- $defaultPreferences['thumbsize'] = [
- 'type' => 'select',
- 'options' => self::getThumbSizes( $context ),
- 'label-message' => 'thumbsize',
- 'section' => 'rendering/files',
- ];
+ public static function filesPreferences(
+ $user, IContextSource $context, &$defaultPreferences
+ ) {
+ wfDeprecated( __METHOD__, '1.31' );
+ $defaultPreferences = self::getPreferences( $user, $context );
}
/**
@@ -687,75 +129,11 @@ class Preferences {
* @param array &$defaultPreferences
* @return void
*/
- static function datetimePreferences( $user, IContextSource $context, &$defaultPreferences ) {
- # # Date and time #####################################
- $dateOptions = self::getDateOptions( $context );
- if ( $dateOptions ) {
- $defaultPreferences['date'] = [
- 'type' => 'radio',
- 'options' => $dateOptions,
- 'section' => 'rendering/dateformat',
- ];
- }
-
- // Info
- $now = wfTimestampNow();
- $lang = $context->getLanguage();
- $nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
- $lang->userTime( $now, $user ) );
- $nowserver = $lang->userTime( $now, $user,
- [ 'format' => false, 'timecorrection' => false ] ) .
- Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
-
- $defaultPreferences['nowserver'] = [
- 'type' => 'info',
- 'raw' => 1,
- 'label-message' => 'servertime',
- 'default' => $nowserver,
- 'section' => 'rendering/timeoffset',
- ];
-
- $defaultPreferences['nowlocal'] = [
- 'type' => 'info',
- 'raw' => 1,
- 'label-message' => 'localtime',
- 'default' => $nowlocal,
- 'section' => 'rendering/timeoffset',
- ];
-
- // Grab existing pref.
- $tzOffset = $user->getOption( 'timecorrection' );
- $tz = explode( '|', $tzOffset, 3 );
-
- $tzOptions = self::getTimezoneOptions( $context );
-
- $tzSetting = $tzOffset;
- if ( count( $tz ) > 1 && $tz[0] == 'ZoneInfo' &&
- !in_array( $tzOffset, HTMLFormField::flattenOptions( $tzOptions ) )
- ) {
- // Timezone offset can vary with DST
- try {
- $userTZ = new DateTimeZone( $tz[2] );
- $minDiff = floor( $userTZ->getOffset( new DateTime( 'now' ) ) / 60 );
- $tzSetting = "ZoneInfo|$minDiff|{$tz[2]}";
- } catch ( Exception $e ) {
- // User has an invalid time zone set. Fall back to just using the offset
- $tz[0] = 'Offset';
- }
- }
- if ( count( $tz ) > 1 && $tz[0] == 'Offset' ) {
- $minDiff = $tz[1];
- $tzSetting = sprintf( '%+03d:%02d', floor( $minDiff / 60 ), abs( $minDiff ) % 60 );
- }
-
- $defaultPreferences['timecorrection'] = [
- 'class' => 'HTMLSelectOrOtherField',
- 'label-message' => 'timezonelegend',
- 'options' => $tzOptions,
- 'default' => $tzSetting,
- 'size' => 20,
- 'section' => 'rendering/timeoffset',
- ];
+ public static function datetimePreferences(
+ $user, IContextSource $context, &$defaultPreferences
+ ) {
+ wfDeprecated( __METHOD__, '1.31' );
+ $defaultPreferences = self::getPreferences( $user, $context );
}
/**
@@ -763,61 +141,11 @@ class Preferences {
* @param IContextSource $context
* @param array &$defaultPreferences
*/
- static function renderingPreferences( $user, IContextSource $context, &$defaultPreferences ) {
- # # Diffs ####################################
- $defaultPreferences['diffonly'] = [
- 'type' => 'toggle',
- 'section' => 'rendering/diffs',
- 'label-message' => 'tog-diffonly',
- ];
- $defaultPreferences['norollbackdiff'] = [
- 'type' => 'toggle',
- 'section' => 'rendering/diffs',
- 'label-message' => 'tog-norollbackdiff',
- ];
-
- # # Page Rendering ##############################
- if ( $context->getConfig()->get( 'AllowUserCssPrefs' ) ) {
- $defaultPreferences['underline'] = [
- 'type' => 'select',
- 'options' => [
- $context->msg( 'underline-never' )->text() => 0,
- $context->msg( 'underline-always' )->text() => 1,
- $context->msg( 'underline-default' )->text() => 2,
- ],
- 'label-message' => 'tog-underline',
- 'section' => 'rendering/advancedrendering',
- ];
- }
-
- $stubThresholdValues = [ 50, 100, 500, 1000, 2000, 5000, 10000 ];
- $stubThresholdOptions = [ $context->msg( 'stub-threshold-disabled' )->text() => 0 ];
- foreach ( $stubThresholdValues as $value ) {
- $stubThresholdOptions[$context->msg( 'size-bytes', $value )->text()] = $value;
- }
-
- $defaultPreferences['stubthreshold'] = [
- 'type' => 'select',
- 'section' => 'rendering/advancedrendering',
- 'options' => $stubThresholdOptions,
- // This is not a raw HTML message; label-raw is needed for the manual <a></a>
- 'label-raw' => $context->msg( 'stub-threshold' )->rawParams(
- '<a href="#" class="stub">' .
- $context->msg( 'stub-threshold-sample-link' )->parse() .
- '</a>' )->parse(),
- ];
-
- $defaultPreferences['showhiddencats'] = [
- 'type' => 'toggle',
- 'section' => 'rendering/advancedrendering',
- 'label-message' => 'tog-showhiddencats'
- ];
-
- $defaultPreferences['numberheadings'] = [
- 'type' => 'toggle',
- 'section' => 'rendering/advancedrendering',
- 'label-message' => 'tog-numberheadings',
- ];
+ public static function renderingPreferences(
+ $user, IContextSource $context, &$defaultPreferences
+ ) {
+ wfDeprecated( __METHOD__, '1.31' );
+ $defaultPreferences = self::getPreferences( $user, $context );
}
/**
@@ -825,72 +153,11 @@ class Preferences {
* @param IContextSource $context
* @param array &$defaultPreferences
*/
- static function editingPreferences( $user, IContextSource $context, &$defaultPreferences ) {
- # # Editing #####################################
- $defaultPreferences['editsectiononrightclick'] = [
- 'type' => 'toggle',
- 'section' => 'editing/advancedediting',
- 'label-message' => 'tog-editsectiononrightclick',
- ];
- $defaultPreferences['editondblclick'] = [
- 'type' => 'toggle',
- 'section' => 'editing/advancedediting',
- 'label-message' => 'tog-editondblclick',
- ];
-
- if ( $context->getConfig()->get( 'AllowUserCssPrefs' ) ) {
- $defaultPreferences['editfont'] = [
- 'type' => 'select',
- 'section' => 'editing/editor',
- 'label-message' => 'editfont-style',
- 'options' => [
- $context->msg( 'editfont-monospace' )->text() => 'monospace',
- $context->msg( 'editfont-sansserif' )->text() => 'sans-serif',
- $context->msg( 'editfont-serif' )->text() => 'serif',
- $context->msg( 'editfont-default' )->text() => 'default',
- ]
- ];
- }
-
- if ( $user->isAllowed( 'minoredit' ) ) {
- $defaultPreferences['minordefault'] = [
- 'type' => 'toggle',
- 'section' => 'editing/editor',
- 'label-message' => 'tog-minordefault',
- ];
- }
-
- $defaultPreferences['forceeditsummary'] = [
- 'type' => 'toggle',
- 'section' => 'editing/editor',
- 'label-message' => 'tog-forceeditsummary',
- ];
- $defaultPreferences['useeditwarning'] = [
- 'type' => 'toggle',
- 'section' => 'editing/editor',
- 'label-message' => 'tog-useeditwarning',
- ];
- $defaultPreferences['showtoolbar'] = [
- 'type' => 'toggle',
- 'section' => 'editing/editor',
- 'label-message' => 'tog-showtoolbar',
- ];
-
- $defaultPreferences['previewonfirst'] = [
- 'type' => 'toggle',
- 'section' => 'editing/preview',
- 'label-message' => 'tog-previewonfirst',
- ];
- $defaultPreferences['previewontop'] = [
- 'type' => 'toggle',
- 'section' => 'editing/preview',
- 'label-message' => 'tog-previewontop',
- ];
- $defaultPreferences['uselivepreview'] = [
- 'type' => 'toggle',
- 'section' => 'editing/preview',
- 'label-message' => 'tog-uselivepreview',
- ];
+ public static function editingPreferences(
+ $user, IContextSource $context, &$defaultPreferences
+ ) {
+ wfDeprecated( __METHOD__, '1.31' );
+ $defaultPreferences = self::getPreferences( $user, $context );
}
/**
@@ -898,87 +165,9 @@ class Preferences {
* @param IContextSource $context
* @param array &$defaultPreferences
*/
- static function rcPreferences( $user, IContextSource $context, &$defaultPreferences ) {
- $config = $context->getConfig();
- $rcMaxAge = $config->get( 'RCMaxAge' );
- # # RecentChanges #####################################
- $defaultPreferences['rcdays'] = [
- 'type' => 'float',
- 'label-message' => 'recentchangesdays',
- 'section' => 'rc/displayrc',
- 'min' => 1,
- 'max' => ceil( $rcMaxAge / ( 3600 * 24 ) ),
- 'help' => $context->msg( 'recentchangesdays-max' )->numParams(
- ceil( $rcMaxAge / ( 3600 * 24 ) ) )->escaped()
- ];
- $defaultPreferences['rclimit'] = [
- 'type' => 'int',
- 'min' => 0,
- 'max' => 1000,
- 'label-message' => 'recentchangescount',
- 'help-message' => 'prefs-help-recentchangescount',
- 'section' => 'rc/displayrc',
- ];
- $defaultPreferences['usenewrc'] = [
- 'type' => 'toggle',
- 'label-message' => 'tog-usenewrc',
- 'section' => 'rc/advancedrc',
- ];
- $defaultPreferences['hideminor'] = [
- 'type' => 'toggle',
- 'label-message' => 'tog-hideminor',
- 'section' => 'rc/advancedrc',
- ];
- $defaultPreferences['rcfilters-saved-queries'] = [
- 'type' => 'api',
- ];
- $defaultPreferences['rcfilters-wl-saved-queries'] = [
- 'type' => 'api',
- ];
- $defaultPreferences['rcfilters-rclimit'] = [
- 'type' => 'api',
- ];
-
- if ( $config->get( 'RCWatchCategoryMembership' ) ) {
- $defaultPreferences['hidecategorization'] = [
- 'type' => 'toggle',
- 'label-message' => 'tog-hidecategorization',
- 'section' => 'rc/advancedrc',
- ];
- }
-
- if ( $user->useRCPatrol() ) {
- $defaultPreferences['hidepatrolled'] = [
- 'type' => 'toggle',
- 'section' => 'rc/advancedrc',
- 'label-message' => 'tog-hidepatrolled',
- ];
- }
-
- if ( $user->useNPPatrol() ) {
- $defaultPreferences['newpageshidepatrolled'] = [
- 'type' => 'toggle',
- 'section' => 'rc/advancedrc',
- 'label-message' => 'tog-newpageshidepatrolled',
- ];
- }
-
- if ( $config->get( 'RCShowWatchingUsers' ) ) {
- $defaultPreferences['shownumberswatching'] = [
- 'type' => 'toggle',
- 'section' => 'rc/advancedrc',
- 'label-message' => 'tog-shownumberswatching',
- ];
- }
-
- if ( $config->get( 'StructuredChangeFiltersShowPreference' ) ) {
- $defaultPreferences['rcenhancedfilters-disable'] = [
- 'type' => 'toggle',
- 'section' => 'rc/opt-out',
- 'label-message' => 'rcfilters-preference-label',
- 'help-message' => 'rcfilters-preference-help',
- ];
- }
+ public static function rcPreferences( $user, IContextSource $context, &$defaultPreferences ) {
+ wfDeprecated( __METHOD__, '1.31' );
+ $defaultPreferences = self::getPreferences( $user, $context );
}
/**
@@ -986,154 +175,11 @@ class Preferences {
* @param IContextSource $context
* @param array &$defaultPreferences
*/
- static function watchlistPreferences( $user, IContextSource $context, &$defaultPreferences ) {
- $config = $context->getConfig();
- $watchlistdaysMax = ceil( $config->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
-
- # # Watchlist #####################################
- if ( $user->isAllowed( 'editmywatchlist' ) ) {
- $editWatchlistLinks = [];
- $editWatchlistModes = [
- 'edit' => [ 'EditWatchlist', false ],
- 'raw' => [ 'EditWatchlist', 'raw' ],
- 'clear' => [ 'EditWatchlist', 'clear' ],
- ];
- $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
- foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) {
- // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
- $editWatchlistLinks[] = $linkRenderer->makeKnownLink(
- SpecialPage::getTitleFor( $mode[0], $mode[1] ),
- new HtmlArmor( $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() )
- );
- }
-
- $defaultPreferences['editwatchlist'] = [
- 'type' => 'info',
- 'raw' => true,
- 'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ),
- 'label-message' => 'prefs-editwatchlist-label',
- 'section' => 'watchlist/editwatchlist',
- ];
- }
-
- $defaultPreferences['watchlistdays'] = [
- 'type' => 'float',
- 'min' => 0,
- 'max' => $watchlistdaysMax,
- 'section' => 'watchlist/displaywatchlist',
- 'help' => $context->msg( 'prefs-watchlist-days-max' )->numParams(
- $watchlistdaysMax )->escaped(),
- 'label-message' => 'prefs-watchlist-days',
- ];
- $defaultPreferences['wllimit'] = [
- 'type' => 'int',
- 'min' => 0,
- 'max' => 1000,
- 'label-message' => 'prefs-watchlist-edits',
- 'help' => $context->msg( 'prefs-watchlist-edits-max' )->escaped(),
- 'section' => 'watchlist/displaywatchlist',
- ];
- $defaultPreferences['extendwatchlist'] = [
- 'type' => 'toggle',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => 'tog-extendwatchlist',
- ];
- $defaultPreferences['watchlisthideminor'] = [
- 'type' => 'toggle',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => 'tog-watchlisthideminor',
- ];
- $defaultPreferences['watchlisthidebots'] = [
- 'type' => 'toggle',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => 'tog-watchlisthidebots',
- ];
- $defaultPreferences['watchlisthideown'] = [
- 'type' => 'toggle',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => 'tog-watchlisthideown',
- ];
- $defaultPreferences['watchlisthideanons'] = [
- 'type' => 'toggle',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => 'tog-watchlisthideanons',
- ];
- $defaultPreferences['watchlisthideliu'] = [
- 'type' => 'toggle',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => 'tog-watchlisthideliu',
- ];
- $defaultPreferences['watchlistreloadautomatically'] = [
- 'type' => 'toggle',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => 'tog-watchlistreloadautomatically',
- ];
- $defaultPreferences['watchlistunwatchlinks'] = [
- 'type' => 'toggle',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => 'tog-watchlistunwatchlinks',
- ];
-
- if ( $config->get( 'RCWatchCategoryMembership' ) ) {
- $defaultPreferences['watchlisthidecategorization'] = [
- 'type' => 'toggle',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => 'tog-watchlisthidecategorization',
- ];
- }
-
- if ( $user->useRCPatrol() ) {
- $defaultPreferences['watchlisthidepatrolled'] = [
- 'type' => 'toggle',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => 'tog-watchlisthidepatrolled',
- ];
- }
-
- $watchTypes = [
- 'edit' => 'watchdefault',
- 'move' => 'watchmoves',
- 'delete' => 'watchdeletion'
- ];
-
- // Kinda hacky
- if ( $user->isAllowed( 'createpage' ) || $user->isAllowed( 'createtalk' ) ) {
- $watchTypes['read'] = 'watchcreations';
- }
-
- if ( $user->isAllowed( 'rollback' ) ) {
- $watchTypes['rollback'] = 'watchrollback';
- }
-
- if ( $user->isAllowed( 'upload' ) ) {
- $watchTypes['upload'] = 'watchuploads';
- }
-
- foreach ( $watchTypes as $action => $pref ) {
- if ( $user->isAllowed( $action ) ) {
- // Messages:
- // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
- // tog-watchrollback
- $defaultPreferences[$pref] = [
- 'type' => 'toggle',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => "tog-$pref",
- ];
- }
- }
-
- if ( $config->get( 'EnableAPI' ) ) {
- $defaultPreferences['watchlisttoken'] = [
- 'type' => 'api',
- ];
- $defaultPreferences['watchlisttoken-info'] = [
- 'type' => 'info',
- 'section' => 'watchlist/tokenwatchlist',
- 'label-message' => 'prefs-watchlist-token',
- 'default' => $user->getTokenFromOption( 'watchlisttoken' ),
- 'help-message' => 'prefs-help-watchlist-token2',
- ];
- }
+ public static function watchlistPreferences(
+ $user, IContextSource $context, &$defaultPreferences
+ ) {
+ wfDeprecated( __METHOD__, '1.31' );
+ $defaultPreferences = self::getPreferences( $user, $context );
}
/**
@@ -1141,12 +187,11 @@ class Preferences {
* @param IContextSource $context
* @param array &$defaultPreferences
*/
- static function searchPreferences( $user, IContextSource $context, &$defaultPreferences ) {
- foreach ( MWNamespace::getValidNamespaces() as $n ) {
- $defaultPreferences['searchNs' . $n] = [
- 'type' => 'api',
- ];
- }
+ public static function searchPreferences(
+ $user, IContextSource $context, &$defaultPreferences
+ ) {
+ wfDeprecated( __METHOD__, '1.31' );
+ $defaultPreferences = self::getPreferences( $user, $context );
}
/**
@@ -1155,78 +200,17 @@ class Preferences {
* @param IContextSource $context
* @param array &$defaultPreferences
*/
- static function miscPreferences( $user, IContextSource $context, &$defaultPreferences ) {
+ public static function miscPreferences( $user, IContextSource $context, &$defaultPreferences ) {
}
/**
- * @param User $user The User object
+ * @param User $user
* @param IContextSource $context
* @return array Text/links to display as key; $skinkey as value
*/
- static function generateSkinOptions( $user, IContextSource $context ) {
- $ret = [];
-
- $mptitle = Title::newMainPage();
- $previewtext = $context->msg( 'skin-preview' )->escaped();
-
- $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
-
- # Only show skins that aren't disabled in $wgSkipSkins
- $validSkinNames = Skin::getAllowedSkins();
-
- # Sort by UI skin name. First though need to update validSkinNames as sometimes
- # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI).
- foreach ( $validSkinNames as $skinkey => &$skinname ) {
- $msg = $context->msg( "skinname-{$skinkey}" );
- if ( $msg->exists() ) {
- $skinname = htmlspecialchars( $msg->text() );
- }
- }
- asort( $validSkinNames );
-
- $config = $context->getConfig();
- $defaultSkin = $config->get( 'DefaultSkin' );
- $allowUserCss = $config->get( 'AllowUserCss' );
- $allowUserJs = $config->get( 'AllowUserJs' );
-
- $foundDefault = false;
- foreach ( $validSkinNames as $skinkey => $sn ) {
- $linkTools = [];
-
- # Mark the default skin
- if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
- $linkTools[] = $context->msg( 'default' )->escaped();
- $foundDefault = true;
- }
-
- # Create preview link
- $mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
- $linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
-
- # Create links to user CSS/JS pages
- if ( $allowUserCss ) {
- $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
- $linkTools[] = $linkRenderer->makeLink( $cssPage, $context->msg( 'prefs-custom-css' )->text() );
- }
-
- if ( $allowUserJs ) {
- $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
- $linkTools[] = $linkRenderer->makeLink( $jsPage, $context->msg( 'prefs-custom-js' )->text() );
- }
-
- $display = $sn . ' ' . $context->msg( 'parentheses' )
- ->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
- ->escaped();
- $ret[$display] = $skinkey;
- }
-
- if ( !$foundDefault ) {
- // If the default skin is not available, things are going to break horribly because the
- // default value for skin selector will not be a valid value. Let's just not show it then.
- return [];
- }
-
- return $ret;
+ public static function generateSkinOptions( $user, IContextSource $context ) {
+ wfDeprecated( __METHOD__, '1.31' );
+ return self::getPreferences( $user, $context );
}
/**
@@ -1234,66 +218,23 @@ class Preferences {
* @return array
*/
static function getDateOptions( IContextSource $context ) {
- $lang = $context->getLanguage();
- $dateopts = $lang->getDatePreferences();
-
- $ret = [];
-
- if ( $dateopts ) {
- if ( !in_array( 'default', $dateopts ) ) {
- $dateopts[] = 'default'; // Make sure default is always valid T21237
- }
-
- // FIXME KLUGE: site default might not be valid for user language
- global $wgDefaultUserOptions;
- if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
- $wgDefaultUserOptions['date'] = 'default';
- }
-
- $epoch = wfTimestampNow();
- foreach ( $dateopts as $key ) {
- if ( $key == 'default' ) {
- $formatted = $context->msg( 'datedefault' )->escaped();
- } else {
- $formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
- }
- $ret[$formatted] = $key;
- }
- }
- return $ret;
+ throw new Exception( __METHOD__ . '() is deprecated and does nothing' );
}
/**
* @param IContextSource $context
* @return array
*/
- static function getImageSizes( IContextSource $context ) {
- $ret = [];
- $pixels = $context->msg( 'unit-pixel' )->text();
-
- foreach ( $context->getConfig()->get( 'ImageLimits' ) as $index => $limits ) {
- // Note: A left-to-right marker (\u200e) is inserted, see T144386
- $display = "{$limits[0]}" . json_decode( '"\u200e"' ) . "×{$limits[1]}" . $pixels;
- $ret[$display] = $index;
- }
-
- return $ret;
+ public static function getImageSizes( IContextSource $context ) {
+ throw new Exception( __METHOD__ . '() is deprecated and does nothing' );
}
/**
* @param IContextSource $context
* @return array
*/
- static function getThumbSizes( IContextSource $context ) {
- $ret = [];
- $pixels = $context->msg( 'unit-pixel' )->text();
-
- foreach ( $context->getConfig()->get( 'ThumbLimits' ) as $index => $size ) {
- $display = $size . $pixels;
- $ret[$display] = $index;
- }
-
- return $ret;
+ public static function getThumbSizes( IContextSource $context ) {
+ throw new Exception( __METHOD__ . '() is deprecated and does nothing' );
}
/**
@@ -1302,24 +243,8 @@ class Preferences {
* @param HTMLForm $form
* @return bool|string
*/
- static function validateSignature( $signature, $alldata, $form ) {
- global $wgParser;
- $maxSigChars = $form->getConfig()->get( 'MaxSigChars' );
- if ( mb_strlen( $signature ) > $maxSigChars ) {
- return Xml::element( 'span', [ 'class' => 'error' ],
- $form->msg( 'badsiglength' )->numParams( $maxSigChars )->text() );
- } elseif ( isset( $alldata['fancysig'] ) &&
- $alldata['fancysig'] &&
- $wgParser->validateSig( $signature ) === false
- ) {
- return Xml::element(
- 'span',
- [ 'class' => 'error' ],
- $form->msg( 'badsig' )->text()
- );
- } else {
- return true;
- }
+ public static function validateSignature( $signature, $alldata, $form ) {
+ throw new Exception( __METHOD__ . '() is deprecated and does nothing' );
}
/**
@@ -1328,16 +253,8 @@ class Preferences {
* @param HTMLForm $form
* @return string
*/
- static function cleanSignature( $signature, $alldata, $form ) {
- if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
- global $wgParser;
- $signature = $wgParser->cleanSig( $signature );
- } else {
- // When no fancy sig used, make sure ~{3,5} get removed.
- $signature = Parser::cleanSigInSig( $signature );
- }
-
- return $signature;
+ public static function cleanSignature( $signature, $alldata, $form ) {
+ throw new Exception( __METHOD__ . '() is deprecated and does nothing now' );
}
/**
@@ -1345,86 +262,24 @@ class Preferences {
* @param IContextSource $context
* @param string $formClass
* @param array $remove Array of items to remove
- * @return PreferencesForm|HtmlForm
+ * @return PreferencesForm|HTMLForm
*/
- static function getFormObject(
+ public static function getFormObject(
$user,
IContextSource $context,
- $formClass = 'PreferencesForm',
+ $formClass = PreferencesForm::class,
array $remove = []
) {
- $formDescriptor = self::getPreferences( $user, $context );
- if ( count( $remove ) ) {
- $removeKeys = array_flip( $remove );
- $formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
- }
-
- // Remove type=api preferences. They are not intended for rendering in the form.
- foreach ( $formDescriptor as $name => $info ) {
- if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
- unset( $formDescriptor[$name] );
- }
- }
-
- /**
- * @var $htmlForm PreferencesForm
- */
- $htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
-
- $htmlForm->setModifiedUser( $user );
- $htmlForm->setId( 'mw-prefs-form' );
- $htmlForm->setAutocomplete( 'off' );
- $htmlForm->setSubmitText( $context->msg( 'saveprefs' )->text() );
- # Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
- $htmlForm->setSubmitTooltip( 'preferences-save' );
- $htmlForm->setSubmitID( 'prefcontrol' );
- $htmlForm->setSubmitCallback( [ 'Preferences', 'tryFormSubmit' ] );
-
- return $htmlForm;
+ $preferencesFactory = self::getDefaultPreferencesFactory();
+ return $preferencesFactory->getForm( $user, $context, $formClass, $remove );
}
/**
* @param IContextSource $context
* @return array
*/
- static function getTimezoneOptions( IContextSource $context ) {
- $opt = [];
-
- $localTZoffset = $context->getConfig()->get( 'LocalTZoffset' );
- $timeZoneList = self::getTimeZoneList( $context->getLanguage() );
-
- $timestamp = MWTimestamp::getLocalInstance();
- // Check that the LocalTZoffset is the same as the local time zone offset
- if ( $localTZoffset == $timestamp->format( 'Z' ) / 60 ) {
- $timezoneName = $timestamp->getTimezone()->getName();
- // Localize timezone
- if ( isset( $timeZoneList[$timezoneName] ) ) {
- $timezoneName = $timeZoneList[$timezoneName]['name'];
- }
- $server_tz_msg = $context->msg(
- 'timezoneuseserverdefault',
- $timezoneName
- )->text();
- } else {
- $tzstring = sprintf(
- '%+03d:%02d',
- floor( $localTZoffset / 60 ),
- abs( $localTZoffset ) % 60
- );
- $server_tz_msg = $context->msg( 'timezoneuseserverdefault', $tzstring )->text();
- }
- $opt[$server_tz_msg] = "System|$localTZoffset";
- $opt[$context->msg( 'timezoneuseoffset' )->text()] = 'other';
- $opt[$context->msg( 'guesstimezone' )->text()] = 'guess';
-
- foreach ( $timeZoneList as $timeZoneInfo ) {
- $region = $timeZoneInfo['region'];
- if ( !isset( $opt[$region] ) ) {
- $opt[$region] = [];
- }
- $opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
- }
- return $opt;
+ public static function getTimezoneOptions( IContextSource $context ) {
+ throw new Exception( __METHOD__ . '() is deprecated and does nothing' );
}
/**
@@ -1432,8 +287,8 @@ class Preferences {
* @param array $alldata
* @return int
*/
- static function filterIntval( $value, $alldata ) {
- return intval( $value );
+ public static function filterIntval( $value, $alldata ) {
+ throw new Exception( __METHOD__ . '() is deprecated and does nothing' );
}
/**
@@ -1441,119 +296,22 @@ class Preferences {
* @param array $alldata
* @return string
*/
- static function filterTimezoneInput( $tz, $alldata ) {
- $data = explode( '|', $tz, 3 );
- switch ( $data[0] ) {
- case 'ZoneInfo':
- $valid = false;
-
- if ( count( $data ) === 3 ) {
- // Make sure this timezone exists
- try {
- new DateTimeZone( $data[2] );
- // If the constructor didn't throw, we know it's valid
- $valid = true;
- } catch ( Exception $e ) {
- // Not a valid timezone
- }
- }
-
- if ( !$valid ) {
- // If the supplied timezone doesn't exist, fall back to the encoded offset
- return 'Offset|' . intval( $tz[1] );
- }
- return $tz;
- case 'System':
- return $tz;
- default:
- $data = explode( ':', $tz, 2 );
- if ( count( $data ) == 2 ) {
- $data[0] = intval( $data[0] );
- $data[1] = intval( $data[1] );
- $minDiff = abs( $data[0] ) * 60 + $data[1];
- if ( $data[0] < 0 ) {
- $minDiff = - $minDiff;
- }
- } else {
- $minDiff = intval( $data[0] ) * 60;
- }
-
- # Max is +14:00 and min is -12:00, see:
- # https://en.wikipedia.org/wiki/Timezone
- $minDiff = min( $minDiff, 840 ); # 14:00
- $minDiff = max( $minDiff, -720 ); # -12:00
- return 'Offset|' . $minDiff;
- }
+ public static function filterTimezoneInput( $tz, $alldata ) {
+ throw new Exception( __METHOD__ . '() is deprecated and does nothing' );
}
/**
* Handle the form submission if everything validated properly
*
+ * @deprecated since 1.31, use PreferencesFactory
+ *
* @param array $formData
* @param PreferencesForm $form
* @return bool|Status|string
*/
- static function tryFormSubmit( $formData, $form ) {
- $user = $form->getModifiedUser();
- $hiddenPrefs = $form->getConfig()->get( 'HiddenPrefs' );
- $result = true;
-
- if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
- return Status::newFatal( 'mypreferencesprotected' );
- }
-
- // Filter input
- foreach ( array_keys( $formData ) as $name ) {
- if ( isset( self::$saveFilters[$name] ) ) {
- $formData[$name] =
- call_user_func( self::$saveFilters[$name], $formData[$name], $formData );
- }
- }
-
- // Fortunately, the realname field is MUCH simpler
- // (not really "private", but still shouldn't be edited without permission)
-
- if ( !in_array( 'realname', $hiddenPrefs )
- && $user->isAllowed( 'editmyprivateinfo' )
- && array_key_exists( 'realname', $formData )
- ) {
- $realName = $formData['realname'];
- $user->setRealName( $realName );
- }
-
- if ( $user->isAllowed( 'editmyoptions' ) ) {
- $oldUserOptions = $user->getOptions();
-
- foreach ( self::$saveBlacklist as $b ) {
- unset( $formData[$b] );
- }
-
- # If users have saved a value for a preference which has subsequently been disabled
- # via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
- # is subsequently re-enabled
- foreach ( $hiddenPrefs as $pref ) {
- # If the user has not set a non-default value here, the default will be returned
- # and subsequently discarded
- $formData[$pref] = $user->getOption( $pref, null, true );
- }
-
- // Keep old preferences from interfering due to back-compat code, etc.
- $user->resetOptions( 'unused', $form->getContext() );
-
- foreach ( $formData as $key => $value ) {
- $user->setOption( $key, $value );
- }
-
- Hooks::run(
- 'PreferencesFormPreSave',
- [ $formData, $form, $user, &$result, $oldUserOptions ]
- );
- }
-
- MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'updateExternalDB', [ $user ] );
- $user->saveSettings();
-
- return $result;
+ public static function tryFormSubmit( $formData, $form ) {
+ $preferencesFactory = self::getDefaultPreferencesFactory();
+ return $preferencesFactory->legacySaveFormData( $formData, $form );
}
/**
@@ -1562,27 +320,8 @@ class Preferences {
* @return Status
*/
public static function tryUISubmit( $formData, $form ) {
- $res = self::tryFormSubmit( $formData, $form );
-
- if ( $res ) {
- $urlOptions = [];
-
- if ( $res === 'eauth' ) {
- $urlOptions['eauth'] = 1;
- }
-
- $urlOptions += $form->getExtraSuccessRedirectParameters();
-
- $url = $form->getTitle()->getFullURL( $urlOptions );
-
- $context = $form->getContext();
- // Set session data for the success message
- $context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
-
- $context->getOutput()->redirect( $url );
- }
-
- return Status::newGood();
+ $preferencesFactory = self::getDefaultPreferencesFactory();
+ return $preferencesFactory->legacySubmitForm( $formData, $form );
}
/**
@@ -1594,56 +333,6 @@ class Preferences {
* @since 1.26
*/
public static function getTimeZoneList( Language $language ) {
- $identifiers = DateTimeZone::listIdentifiers();
- if ( $identifiers === false ) {
- return [];
- }
- sort( $identifiers );
-
- $tzRegions = [
- 'Africa' => wfMessage( 'timezoneregion-africa' )->inLanguage( $language )->text(),
- 'America' => wfMessage( 'timezoneregion-america' )->inLanguage( $language )->text(),
- 'Antarctica' => wfMessage( 'timezoneregion-antarctica' )->inLanguage( $language )->text(),
- 'Arctic' => wfMessage( 'timezoneregion-arctic' )->inLanguage( $language )->text(),
- 'Asia' => wfMessage( 'timezoneregion-asia' )->inLanguage( $language )->text(),
- 'Atlantic' => wfMessage( 'timezoneregion-atlantic' )->inLanguage( $language )->text(),
- 'Australia' => wfMessage( 'timezoneregion-australia' )->inLanguage( $language )->text(),
- 'Europe' => wfMessage( 'timezoneregion-europe' )->inLanguage( $language )->text(),
- 'Indian' => wfMessage( 'timezoneregion-indian' )->inLanguage( $language )->text(),
- 'Pacific' => wfMessage( 'timezoneregion-pacific' )->inLanguage( $language )->text(),
- ];
- asort( $tzRegions );
-
- $timeZoneList = [];
-
- $now = new DateTime();
-
- foreach ( $identifiers as $identifier ) {
- $parts = explode( '/', $identifier, 2 );
-
- // DateTimeZone::listIdentifiers() returns a number of
- // backwards-compatibility entries. This filters them out of the
- // list presented to the user.
- if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
- continue;
- }
-
- // Localize region
- $parts[0] = $tzRegions[$parts[0]];
-
- $dateTimeZone = new DateTimeZone( $identifier );
- $minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
-
- $display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
- $value = "ZoneInfo|$minDiff|$identifier";
-
- $timeZoneList[$identifier] = [
- 'name' => $display,
- 'timecorrection' => $value,
- 'region' => $parts[0],
- ];
- }
-
- return $timeZoneList;
+ throw new Exception( __METHOD__ . '() is deprecated and does nothing' );
}
}
diff --git a/www/wiki/includes/ProtectionForm.php b/www/wiki/includes/ProtectionForm.php
index 53608e84..51c29233 100644
--- a/www/wiki/includes/ProtectionForm.php
+++ b/www/wiki/includes/ProtectionForm.php
@@ -349,7 +349,9 @@ class ProtectionForm {
$user = $context->getUser();
$output = $context->getOutput();
$lang = $context->getLanguage();
- $cascadingRestrictionLevels = $context->getConfig()->get( 'CascadingRestrictionLevels' );
+ $conf = $context->getConfig();
+ $cascadingRestrictionLevels = $conf->get( 'CascadingRestrictionLevels' );
+ $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
$out = '';
if ( !$this->disabled ) {
$output->addModules( 'mediawiki.legacy.protect' );
@@ -494,6 +496,13 @@ class ProtectionForm {
$this->mReasonSelection,
'mwProtect-reason', 4 );
+ // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
+ // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
+ // Unicode codepoints (or 180 UTF-8 bytes for old schema).
+ // Subtract arbitrary 75 to leave some space for the autogenerated null edit's summary
+ // and other texts chosen by dropdown menus on this page.
+ $maxlength = $oldCommentSchema ? 180 : CommentStore::COMMENT_CHARACTER_LIMIT - 75;
+
$out .= Xml::openElement( 'table', [ 'id' => 'mw-protect-table3' ] ) .
Xml::openElement( 'tbody' );
$out .= "
@@ -511,10 +520,7 @@ class ProtectionForm {
</td>
<td class='mw-input'>" .
Xml::input( 'mwProtect-reason', 60, $this->mReason, [ 'type' => 'text',
- 'id' => 'mwProtect-reason', 'maxlength' => 180 ] ) .
- // Limited maxlength as the database trims at 255 bytes and other texts
- // chosen by dropdown menus on this page are also included in this database field.
- // The byte limit of 180 bytes is enforced in javascript
+ 'id' => 'mwProtect-reason', 'maxlength' => $maxlength ] ) .
"</td>
</tr>";
# Disallow watching is user is not logged in
diff --git a/www/wiki/includes/ProxyLookup.php b/www/wiki/includes/ProxyLookup.php
index 3a3243a5..246ae95a 100644
--- a/www/wiki/includes/ProxyLookup.php
+++ b/www/wiki/includes/ProxyLookup.php
@@ -19,7 +19,7 @@
* @file
*/
-use IPSet\IPSet;
+use Wikimedia\IPSet;
/**
* @since 1.28
diff --git a/www/wiki/includes/Revision.php b/www/wiki/includes/Revision.php
index bcfbe638..c1fa4faf 100644
--- a/www/wiki/includes/Revision.php
+++ b/www/wiki/includes/Revision.php
@@ -20,7 +20,15 @@
* @file
*/
-use Wikimedia\Rdbms\Database;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\RevisionAccessException;
+use MediaWiki\Storage\RevisionFactory;
+use MediaWiki\Storage\RevisionLookup;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\RevisionStoreRecord;
+use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Storage\SqlBlobStore;
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
@@ -28,78 +36,68 @@ use Wikimedia\Rdbms\ResultWrapper;
use Wikimedia\Rdbms\FakeResultWrapper;
/**
- * @todo document
+ * @deprecated since 1.31, use RevisionRecord, RevisionStore, and BlobStore instead.
*/
class Revision implements IDBAccessObject {
- /** @var int|null */
- protected $mId;
- /** @var int|null */
- protected $mPage;
- /** @var string */
- protected $mUserText;
- /** @var string */
- protected $mOrigUserText;
- /** @var int */
- protected $mUser;
- /** @var bool */
- protected $mMinorEdit;
- /** @var string */
- protected $mTimestamp;
- /** @var int */
- protected $mDeleted;
- /** @var int */
- protected $mSize;
- /** @var string */
- protected $mSha1;
- /** @var int */
- protected $mParentId;
- /** @var string */
- protected $mComment;
- /** @var string */
- protected $mText;
- /** @var int */
- protected $mTextId;
- /** @var int */
- protected $mUnpatrolled;
-
- /** @var stdClass|null */
- protected $mTextRow;
-
- /** @var null|Title */
- protected $mTitle;
- /** @var bool */
- protected $mCurrent;
- /** @var string */
- protected $mContentModel;
- /** @var string */
- protected $mContentFormat;
-
- /** @var Content|null|bool */
- protected $mContent;
- /** @var null|ContentHandler */
- protected $mContentHandler;
-
- /** @var int */
- protected $mQueryFlags = 0;
- /** @var bool Used for cached values to reload user text and rev_deleted */
- protected $mRefreshMutableFields = false;
- /** @var string Wiki ID; false means the current wiki */
- protected $mWiki = false;
+
+ /** @var RevisionRecord */
+ protected $mRecord;
// Revision deletion constants
- const DELETED_TEXT = 1;
- const DELETED_COMMENT = 2;
- const DELETED_USER = 4;
- const DELETED_RESTRICTED = 8;
- const SUPPRESSED_USER = 12; // convenience
- const SUPPRESSED_ALL = 15; // convenience
+ const DELETED_TEXT = RevisionRecord::DELETED_TEXT;
+ const DELETED_COMMENT = RevisionRecord::DELETED_COMMENT;
+ const DELETED_USER = RevisionRecord::DELETED_USER;
+ const DELETED_RESTRICTED = RevisionRecord::DELETED_RESTRICTED;
+ const SUPPRESSED_USER = RevisionRecord::SUPPRESSED_USER;
+ const SUPPRESSED_ALL = RevisionRecord::SUPPRESSED_ALL;
// Audience options for accessors
- const FOR_PUBLIC = 1;
- const FOR_THIS_USER = 2;
- const RAW = 3;
+ const FOR_PUBLIC = RevisionRecord::FOR_PUBLIC;
+ const FOR_THIS_USER = RevisionRecord::FOR_THIS_USER;
+ const RAW = RevisionRecord::RAW;
+
+ const TEXT_CACHE_GROUP = SqlBlobStore::TEXT_CACHE_GROUP;
- const TEXT_CACHE_GROUP = 'revisiontext:10'; // process cache name and max key count
+ /**
+ * @return RevisionStore
+ */
+ protected static function getRevisionStore() {
+ return MediaWikiServices::getInstance()->getRevisionStore();
+ }
+
+ /**
+ * @return RevisionLookup
+ */
+ protected static function getRevisionLookup() {
+ return MediaWikiServices::getInstance()->getRevisionLookup();
+ }
+
+ /**
+ * @return RevisionFactory
+ */
+ protected static function getRevisionFactory() {
+ return MediaWikiServices::getInstance()->getRevisionFactory();
+ }
+
+ /**
+ * @param bool|string $wiki The ID of the target wiki database. Use false for the local wiki.
+ *
+ * @return SqlBlobStore
+ */
+ protected static function getBlobStore( $wiki = false ) {
+ $store = MediaWikiServices::getInstance()
+ ->getBlobStoreFactory()
+ ->newSqlBlobStore( $wiki );
+
+ if ( !$store instanceof SqlBlobStore ) {
+ throw new RuntimeException(
+ 'The backwards compatibility code in Revision currently requires the BlobStore '
+ . 'service to be an SqlBlobStore instance, but it is a ' . get_class( $store )
+ );
+ }
+
+ return $store;
+ }
/**
* Load a page revision from a given revision ID number.
@@ -114,7 +112,8 @@ class Revision implements IDBAccessObject {
* @return Revision|null
*/
public static function newFromId( $id, $flags = 0 ) {
- return self::newFromConds( [ 'rev_id' => intval( $id ) ], $flags );
+ $rec = self::getRevisionLookup()->getRevisionById( $id, $flags );
+ return $rec === null ? null : new Revision( $rec, $flags );
}
/**
@@ -132,20 +131,8 @@ class Revision implements IDBAccessObject {
* @return Revision|null
*/
public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) {
- $conds = [
- 'page_namespace' => $linkTarget->getNamespace(),
- 'page_title' => $linkTarget->getDBkey()
- ];
- if ( $id ) {
- // Use the specified ID
- $conds['rev_id'] = $id;
- return self::newFromConds( $conds, $flags );
- } else {
- // Use a join to get the latest revision
- $conds[] = 'rev_id=page_latest';
- $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
- return self::loadFromConds( $db, $conds, $flags );
- }
+ $rec = self::getRevisionLookup()->getRevisionByTitle( $linkTarget, $id, $flags );
+ return $rec === null ? null : new Revision( $rec, $flags );
}
/**
@@ -163,22 +150,13 @@ class Revision implements IDBAccessObject {
* @return Revision|null
*/
public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
- $conds = [ 'page_id' => $pageId ];
- if ( $revId ) {
- $conds['rev_id'] = $revId;
- return self::newFromConds( $conds, $flags );
- } else {
- // Use a join to get the latest revision
- $conds[] = 'rev_id = page_latest';
- $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
- return self::loadFromConds( $db, $conds, $flags );
- }
+ $rec = self::getRevisionLookup()->getRevisionByPageId( $pageId, $revId, $flags );
+ return $rec === null ? null : new Revision( $rec, $flags );
}
/**
* Make a fake revision object from an archive table row. This is queried
* for permissions or even inserted (as in Special:Undelete)
- * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
*
* @param object $row
* @param array $overrides
@@ -187,68 +165,80 @@ class Revision implements IDBAccessObject {
* @return Revision
*/
public static function newFromArchiveRow( $row, $overrides = [] ) {
- global $wgContentHandlerUseDB;
-
- $attribs = $overrides + [
- 'page' => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
- 'id' => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
- 'comment' => CommentStore::newKey( 'ar_comment' )
- // Legacy because $row probably came from self::selectArchiveFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text,
- 'user' => $row->ar_user,
- 'user_text' => $row->ar_user_text,
- 'timestamp' => $row->ar_timestamp,
- 'minor_edit' => $row->ar_minor_edit,
- 'text_id' => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
- 'deleted' => $row->ar_deleted,
- 'len' => $row->ar_len,
- 'sha1' => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
- 'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
- 'content_format' => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
- ];
-
- if ( !$wgContentHandlerUseDB ) {
- unset( $attribs['content_model'] );
- unset( $attribs['content_format'] );
- }
+ /**
+ * MCR Migration: https://phabricator.wikimedia.org/T183564
+ * This method used to overwrite attributes, then passed to Revision::__construct
+ * RevisionStore::newRevisionFromArchiveRow instead overrides row field names
+ * So do a conversion here.
+ */
+ if ( array_key_exists( 'page', $overrides ) ) {
+ $overrides['page_id'] = $overrides['page'];
+ unset( $overrides['page'] );
+ }
+
+ /**
+ * We require a Title for both the Revision object and the RevisionRecord.
+ * Below is duplicated logic from RevisionStore::newRevisionFromArchiveRow
+ * to fetch a title in order pass it into the Revision object.
+ */
+ $title = null;
+ if ( isset( $overrides['title'] ) ) {
+ if ( !( $overrides['title'] instanceof Title ) ) {
+ throw new MWException( 'title field override must contain a Title object.' );
+ }
- if ( !isset( $attribs['title'] )
- && isset( $row->ar_namespace )
- && isset( $row->ar_title )
- ) {
- $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+ $title = $overrides['title'];
}
-
- if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
- // Pre-1.5 ar_text row
- $attribs['text'] = self::getRevisionText( $row, 'ar_' );
- if ( $attribs['text'] === false ) {
- throw new MWException( 'Unable to load text from archive row (possibly T24624)' );
+ if ( $title !== null ) {
+ if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
+ $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+ } else {
+ throw new InvalidArgumentException(
+ 'A Title or ar_namespace and ar_title must be given'
+ );
}
}
- return new self( $attribs );
+
+ $rec = self::getRevisionFactory()->newRevisionFromArchiveRow( $row, 0, $title, $overrides );
+ return new Revision( $rec, self::READ_NORMAL, $title );
}
/**
* @since 1.19
*
- * @param object $row
+ * MCR migration note: replaced by RevisionStore::newRevisionFromRow(). Note that
+ * newFromRow() also accepts arrays, while newRevisionFromRow() does not. Instead,
+ * a MutableRevisionRecord should be constructed directly.
+ * RevisionStore::newMutableRevisionFromArray() can be used as a temporary replacement,
+ * but should be avoided.
+ *
+ * @param object|array $row
* @return Revision
*/
public static function newFromRow( $row ) {
- return new self( $row );
+ if ( is_array( $row ) ) {
+ $rec = self::getRevisionFactory()->newMutableRevisionFromArray( $row );
+ } else {
+ $rec = self::getRevisionFactory()->newRevisionFromRow( $row );
+ }
+
+ return new Revision( $rec );
}
/**
* Load a page revision from a given revision ID number.
* Returns null if no such revision can be found.
*
+ * @deprecated since 1.31, use RevisionStore::getRevisionById() instead.
+ *
* @param IDatabase $db
* @param int $id
* @return Revision|null
*/
public static function loadFromId( $db, $id ) {
- return self::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] );
+ wfDeprecated( __METHOD__, '1.31' ); // no known callers
+ $rec = self::getRevisionStore()->loadRevisionFromId( $db, $id );
+ return $rec === null ? null : new Revision( $rec );
}
/**
@@ -256,19 +246,16 @@ class Revision implements IDBAccessObject {
* that's attached to a given page. If not attached
* to that page, will return null.
*
+ * @deprecated since 1.31, use RevisionStore::getRevisionByPageId() instead.
+ *
* @param IDatabase $db
* @param int $pageid
* @param int $id
* @return Revision|null
*/
public static function loadFromPageId( $db, $pageid, $id = 0 ) {
- $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
- if ( $id ) {
- $conds['rev_id'] = intval( $id );
- } else {
- $conds[] = 'rev_id=page_latest';
- }
- return self::loadFromConds( $db, $conds );
+ $rec = self::getRevisionStore()->loadRevisionFromPageId( $db, $pageid, $id );
+ return $rec === null ? null : new Revision( $rec );
}
/**
@@ -276,24 +263,16 @@ class Revision implements IDBAccessObject {
* that's attached to a given page. If not attached
* to that page, will return null.
*
+ * @deprecated since 1.31, use RevisionStore::getRevisionByTitle() instead.
+ *
* @param IDatabase $db
* @param Title $title
* @param int $id
* @return Revision|null
*/
public static function loadFromTitle( $db, $title, $id = 0 ) {
- if ( $id ) {
- $matchId = intval( $id );
- } else {
- $matchId = 'page_latest';
- }
- return self::loadFromConds( $db,
- [
- "rev_id=$matchId",
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey()
- ]
- );
+ $rec = self::getRevisionStore()->loadRevisionFromTitle( $db, $title, $id );
+ return $rec === null ? null : new Revision( $rec );
}
/**
@@ -301,73 +280,17 @@ class Revision implements IDBAccessObject {
* WARNING: Timestamps may in some circumstances not be unique,
* so this isn't the best key to use.
*
+ * @deprecated since 1.31, use RevisionStore::getRevisionByTimestamp()
+ * or RevisionStore::loadRevisionFromTimestamp() instead.
+ *
* @param IDatabase $db
* @param Title $title
* @param string $timestamp
* @return Revision|null
*/
public static function loadFromTimestamp( $db, $title, $timestamp ) {
- return self::loadFromConds( $db,
- [
- 'rev_timestamp' => $db->timestamp( $timestamp ),
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey()
- ]
- );
- }
-
- /**
- * Given a set of conditions, fetch a revision
- *
- * This method is used then a revision ID is qualified and
- * will incorporate some basic replica DB/master fallback logic
- *
- * @param array $conditions
- * @param int $flags (optional)
- * @return Revision|null
- */
- private static function newFromConds( $conditions, $flags = 0 ) {
- $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
-
- $rev = self::loadFromConds( $db, $conditions, $flags );
- // Make sure new pending/committed revision are visibile later on
- // within web requests to certain avoid bugs like T93866 and T94407.
- if ( !$rev
- && !( $flags & self::READ_LATEST )
- && wfGetLB()->getServerCount() > 1
- && wfGetLB()->hasOrMadeRecentMasterChanges()
- ) {
- $flags = self::READ_LATEST;
- $db = wfGetDB( DB_MASTER );
- $rev = self::loadFromConds( $db, $conditions, $flags );
- }
-
- if ( $rev ) {
- $rev->mQueryFlags = $flags;
- }
-
- return $rev;
- }
-
- /**
- * Given a set of conditions, fetch a revision from
- * the given database connection.
- *
- * @param IDatabase $db
- * @param array $conditions
- * @param int $flags (optional)
- * @return Revision|null
- */
- private static function loadFromConds( $db, $conditions, $flags = 0 ) {
- $row = self::fetchFromConds( $db, $conditions, $flags );
- if ( $row ) {
- $rev = new Revision( $row );
- $rev->mWiki = $db->getDomainID();
-
- return $rev;
- }
-
- return null;
+ $rec = self::getRevisionStore()->loadRevisionFromTimestamp( $db, $title, $timestamp );
+ return $rec === null ? null : new Revision( $rec );
}
/**
@@ -377,58 +300,33 @@ class Revision implements IDBAccessObject {
*
* @param LinkTarget $title
* @return ResultWrapper
- * @deprecated Since 1.28
+ * @deprecated Since 1.28, no callers in core nor in known extensions. No-op since 1.31.
*/
public static function fetchRevision( LinkTarget $title ) {
- $row = self::fetchFromConds(
- wfGetDB( DB_REPLICA ),
- [
- 'rev_id=page_latest',
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey()
- ]
- );
-
- return new FakeResultWrapper( $row ? [ $row ] : [] );
- }
-
- /**
- * Given a set of conditions, return a ResultWrapper
- * which will return matching database rows with the
- * fields necessary to build Revision objects.
- *
- * @param IDatabase $db
- * @param array $conditions
- * @param int $flags (optional)
- * @return stdClass
- */
- private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
- $fields = array_merge(
- self::selectFields(),
- self::selectPageFields(),
- self::selectUserFields()
- );
- $options = [];
- if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
- $options[] = 'FOR UPDATE';
- }
- return $db->selectRow(
- [ 'revision', 'page', 'user' ],
- $fields,
- $conditions,
- __METHOD__,
- $options,
- [ 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() ]
- );
+ wfDeprecated( __METHOD__, '1.31' );
+ return new FakeResultWrapper( [] );
}
/**
* Return the value of a select() JOIN conds array for the user table.
* This will get user table rows for logged-in users.
* @since 1.19
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead.
* @return array
*/
public static function userJoinCond() {
+ global $wgActorTableSchemaMigrationStage;
+
+ wfDeprecated( __METHOD__, '1.31' );
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+ // If code is using this instead of self::getQueryInfo(), there's
+ // no way the join it's trying to do can work once the old fields
+ // aren't being written anymore.
+ throw new BadMethodCallException(
+ 'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+ );
+ }
+
return [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
}
@@ -436,21 +334,34 @@ class Revision implements IDBAccessObject {
* Return the value of a select() page conds array for the page table.
* This will assure that the revision(s) are not orphaned from live pages.
* @since 1.19
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead.
* @return array
*/
public static function pageJoinCond() {
+ wfDeprecated( __METHOD__, '1.31' );
return [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
}
/**
* Return the list of revision fields that should be selected to create
* a new revision.
- * @todo Deprecate this in favor of a method that returns tables and joins
- * as well, and use CommentStore::getJoin().
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
* @return array
*/
public static function selectFields() {
- global $wgContentHandlerUseDB;
+ global $wgContentHandlerUseDB, $wgActorTableSchemaMigrationStage;
+
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+ // If code is using this instead of self::getQueryInfo(), there's a
+ // decent chance it's going to try to directly access
+ // $row->rev_user or $row->rev_user_text and we can't give it
+ // useful values here once those aren't being written anymore.
+ throw new BadMethodCallException(
+ 'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+ );
+ }
+
+ wfDeprecated( __METHOD__, '1.31' );
$fields = [
'rev_id',
@@ -459,6 +370,7 @@ class Revision implements IDBAccessObject {
'rev_timestamp',
'rev_user_text',
'rev_user',
+ 'rev_actor' => 'NULL',
'rev_minor_edit',
'rev_deleted',
'rev_len',
@@ -466,7 +378,7 @@ class Revision implements IDBAccessObject {
'rev_sha1',
];
- $fields += CommentStore::newKey( 'rev_comment' )->getFields();
+ $fields += CommentStore::getStore()->getFields( 'rev_comment' );
if ( $wgContentHandlerUseDB ) {
$fields[] = 'rev_content_format';
@@ -479,21 +391,33 @@ class Revision implements IDBAccessObject {
/**
* Return the list of revision fields that should be selected to create
* a new revision from an archive row.
- * @todo Deprecate this in favor of a method that returns tables and joins
- * as well, and use CommentStore::getJoin().
+ * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
* @return array
*/
public static function selectArchiveFields() {
- global $wgContentHandlerUseDB;
+ global $wgContentHandlerUseDB, $wgActorTableSchemaMigrationStage;
+
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+ // If code is using this instead of self::getQueryInfo(), there's a
+ // decent chance it's going to try to directly access
+ // $row->ar_user or $row->ar_user_text and we can't give it
+ // useful values here once those aren't being written anymore.
+ throw new BadMethodCallException(
+ 'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+ );
+ }
+
+ wfDeprecated( __METHOD__, '1.31' );
+
$fields = [
'ar_id',
'ar_page_id',
'ar_rev_id',
- 'ar_text',
'ar_text_id',
'ar_timestamp',
'ar_user_text',
'ar_user',
+ 'ar_actor' => 'NULL',
'ar_minor_edit',
'ar_deleted',
'ar_len',
@@ -501,7 +425,7 @@ class Revision implements IDBAccessObject {
'ar_sha1',
];
- $fields += CommentStore::newKey( 'ar_comment' )->getFields();
+ $fields += CommentStore::getStore()->getFields( 'ar_comment' );
if ( $wgContentHandlerUseDB ) {
$fields[] = 'ar_content_format';
@@ -513,9 +437,11 @@ class Revision implements IDBAccessObject {
/**
* Return the list of text fields that should be selected to read the
* revision text
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'text' ] ) instead.
* @return array
*/
public static function selectTextFields() {
+ wfDeprecated( __METHOD__, '1.31' );
return [
'old_text',
'old_flags'
@@ -524,9 +450,11 @@ class Revision implements IDBAccessObject {
/**
* Return the list of page fields that should be selected from page table
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead.
* @return array
*/
public static function selectPageFields() {
+ wfDeprecated( __METHOD__, '1.31' );
return [
'page_namespace',
'page_title',
@@ -539,200 +467,146 @@ class Revision implements IDBAccessObject {
/**
* Return the list of user fields that should be selected from user table
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead.
* @return array
*/
public static function selectUserFields() {
+ wfDeprecated( __METHOD__, '1.31' );
return [ 'user_name' ];
}
/**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new revision object.
+ * @since 1.31
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
+ * @param array $options Any combination of the following strings
+ * - 'page': Join with the page table, and select fields to identify the page
+ * - 'user': Join with the user table, and select the user name
+ * - 'text': Join with the text table, and select fields to load page text
+ * @return array With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ */
+ public static function getQueryInfo( $options = [] ) {
+ return self::getRevisionStore()->getQueryInfo( $options );
+ }
+
+ /**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new archived revision object.
+ * @since 1.31
+ * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
+ * @return array With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ */
+ public static function getArchiveQueryInfo() {
+ return self::getRevisionStore()->getArchiveQueryInfo();
+ }
+
+ /**
* Do a batched query to get the parent revision lengths
+ *
+ * @deprecated in 1.31, use RevisionStore::getRevisionSizes instead.
+ *
* @param IDatabase $db
* @param array $revIds
* @return array
*/
public static function getParentLengths( $db, array $revIds ) {
- $revLens = [];
- if ( !$revIds ) {
- return $revLens; // empty
- }
- $res = $db->select( 'revision',
- [ 'rev_id', 'rev_len' ],
- [ 'rev_id' => $revIds ],
- __METHOD__ );
- foreach ( $res as $row ) {
- $revLens[$row->rev_id] = $row->rev_len;
- }
- return $revLens;
+ return self::getRevisionStore()->listRevisionSizes( $db, $revIds );
}
/**
- * @param object|array $row Either a database row or an array
- * @throws MWException
+ * @param object|array|RevisionRecord $row Either a database row or an array
+ * @param int $queryFlags
+ * @param Title|null $title
+ *
* @access private
*/
- function __construct( $row ) {
- if ( is_object( $row ) ) {
- $this->mId = intval( $row->rev_id );
- $this->mPage = intval( $row->rev_page );
- $this->mTextId = intval( $row->rev_text_id );
- $this->mComment = CommentStore::newKey( 'rev_comment' )
- // Legacy because $row probably came from self::selectFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text;
- $this->mUser = intval( $row->rev_user );
- $this->mMinorEdit = intval( $row->rev_minor_edit );
- $this->mTimestamp = $row->rev_timestamp;
- $this->mDeleted = intval( $row->rev_deleted );
-
- if ( !isset( $row->rev_parent_id ) ) {
- $this->mParentId = null;
- } else {
- $this->mParentId = intval( $row->rev_parent_id );
- }
+ function __construct( $row, $queryFlags = 0, Title $title = null ) {
+ global $wgUser;
- if ( !isset( $row->rev_len ) ) {
- $this->mSize = null;
- } else {
- $this->mSize = intval( $row->rev_len );
- }
-
- if ( !isset( $row->rev_sha1 ) ) {
- $this->mSha1 = null;
- } else {
- $this->mSha1 = $row->rev_sha1;
- }
-
- if ( isset( $row->page_latest ) ) {
- $this->mCurrent = ( $row->rev_id == $row->page_latest );
- $this->mTitle = Title::newFromRow( $row );
- } else {
- $this->mCurrent = false;
- $this->mTitle = null;
- }
-
- if ( !isset( $row->rev_content_model ) ) {
- $this->mContentModel = null; # determine on demand if needed
- } else {
- $this->mContentModel = strval( $row->rev_content_model );
- }
-
- if ( !isset( $row->rev_content_format ) ) {
- $this->mContentFormat = null; # determine on demand if needed
- } else {
- $this->mContentFormat = strval( $row->rev_content_format );
- }
-
- // Lazy extraction...
- $this->mText = null;
- if ( isset( $row->old_text ) ) {
- $this->mTextRow = $row;
- } else {
- // 'text' table row entry will be lazy-loaded
- $this->mTextRow = null;
- }
-
- // Use user_name for users and rev_user_text for IPs...
- $this->mUserText = null; // lazy load if left null
- if ( $this->mUser == 0 ) {
- $this->mUserText = $row->rev_user_text; // IP user
- } elseif ( isset( $row->user_name ) ) {
- $this->mUserText = $row->user_name; // logged-in user
- }
- $this->mOrigUserText = $row->rev_user_text;
+ if ( $row instanceof RevisionRecord ) {
+ $this->mRecord = $row;
} elseif ( is_array( $row ) ) {
- // Build a new revision to be saved...
- global $wgUser; // ugh
-
- # if we have a content object, use it to set the model and type
- if ( !empty( $row['content'] ) ) {
- // @todo when is that set? test with external store setup! check out insertOn() [dk]
- if ( !empty( $row['text_id'] ) ) {
- throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
- "can't serialize content object" );
- }
-
- $row['content_model'] = $row['content']->getModel();
- # note: mContentFormat is initializes later accordingly
- # note: content is serialized later in this method!
- # also set text to null?
+ // If no user is specified, fall back to using the global user object, to stay
+ // compatible with pre-1.31 behavior.
+ if ( !isset( $row['user'] ) && !isset( $row['user_text'] ) ) {
+ $row['user'] = $wgUser;
}
- $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
- $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
- $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
- $this->mUserText = isset( $row['user_text'] )
- ? strval( $row['user_text'] ) : $wgUser->getName();
- $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
- $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
- $this->mTimestamp = isset( $row['timestamp'] )
- ? strval( $row['timestamp'] ) : wfTimestampNow();
- $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
- $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
- $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
- $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
-
- $this->mContentModel = isset( $row['content_model'] )
- ? strval( $row['content_model'] ) : null;
- $this->mContentFormat = isset( $row['content_format'] )
- ? strval( $row['content_format'] ) : null;
-
- // Enforce spacing trimming on supplied text
- $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
- $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
- $this->mTextRow = null;
-
- $this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
-
- // if we have a Content object, override mText and mContentModel
- if ( !empty( $row['content'] ) ) {
- if ( !( $row['content'] instanceof Content ) ) {
- throw new MWException( '`content` field must contain a Content object.' );
- }
+ $this->mRecord = self::getRevisionFactory()->newMutableRevisionFromArray(
+ $row,
+ $queryFlags,
+ $this->ensureTitle( $row, $queryFlags, $title )
+ );
+ } elseif ( is_object( $row ) ) {
+ $this->mRecord = self::getRevisionFactory()->newRevisionFromRow(
+ $row,
+ $queryFlags,
+ $this->ensureTitle( $row, $queryFlags, $title )
+ );
+ } else {
+ throw new InvalidArgumentException(
+ '$row must be a row object, an associative array, or a RevisionRecord'
+ );
+ }
+ }
- $handler = $this->getContentHandler();
- $this->mContent = $row['content'];
+ /**
+ * Make sure we have *some* Title object for use by the constructor.
+ * For B/C, the constructor shouldn't fail even for a bad page ID or bad revision ID.
+ *
+ * @param array|object $row
+ * @param int $queryFlags
+ * @param Title|null $title
+ *
+ * @return Title $title if not null, or a Title constructed from information in $row.
+ */
+ private function ensureTitle( $row, $queryFlags, $title = null ) {
+ if ( $title ) {
+ return $title;
+ }
- $this->mContentModel = $this->mContent->getModel();
- $this->mContentHandler = null;
+ if ( is_array( $row ) ) {
+ if ( isset( $row['title'] ) ) {
+ if ( !( $row['title'] instanceof Title ) ) {
+ throw new MWException( 'title field must contain a Title object.' );
+ }
- $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
- } elseif ( $this->mText !== null ) {
- $handler = $this->getContentHandler();
- $this->mContent = $handler->unserializeContent( $this->mText );
+ return $row['title'];
}
- // If we have a Title object, make sure it is consistent with mPage.
- if ( $this->mTitle && $this->mTitle->exists() ) {
- if ( $this->mPage === null ) {
- // if the page ID wasn't known, set it now
- $this->mPage = $this->mTitle->getArticleID();
- } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
- // Got different page IDs. This may be legit (e.g. during undeletion),
- // but it seems worth mentioning it in the log.
- wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
- $this->mTitle->getArticleID() . " provided by the Title object." );
- }
- }
+ $pageId = isset( $row['page'] ) ? $row['page'] : 0;
+ $revId = isset( $row['id'] ) ? $row['id'] : 0;
+ } else {
+ $pageId = isset( $row->rev_page ) ? $row->rev_page : 0;
+ $revId = isset( $row->rev_id ) ? $row->rev_id : 0;
+ }
- $this->mCurrent = false;
+ try {
+ $title = self::getRevisionStore()->getTitle( $pageId, $revId, $queryFlags );
+ } catch ( RevisionAccessException $ex ) {
+ // construct a dummy title!
+ wfLogWarning( __METHOD__ . ': ' . $ex->getMessage() );
- // If we still have no length, see it we have the text to figure it out
- if ( !$this->mSize && $this->mContent !== null ) {
- $this->mSize = $this->mContent->getSize();
- }
+ // NOTE: this Title will only be used inside RevisionRecord
+ $title = Title::makeTitleSafe( NS_SPECIAL, "Badtitle/ID=$pageId" );
+ $title->resetArticleID( $pageId );
+ }
- // Same for sha1
- if ( $this->mSha1 === null ) {
- $this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText );
- }
+ return $title;
+ }
- // force lazy init
- $this->getContentModel();
- $this->getContentFormat();
- } else {
- throw new MWException( 'Revision constructor passed invalid row format.' );
- }
- $this->mUnpatrolled = null;
+ /**
+ * @return RevisionRecord
+ */
+ public function getRevisionRecord() {
+ return $this->mRecord;
}
/**
@@ -741,19 +615,27 @@ class Revision implements IDBAccessObject {
* @return int|null
*/
public function getId() {
- return $this->mId;
+ return $this->mRecord->getId();
}
/**
* Set the revision ID
*
- * This should only be used for proposed revisions that turn out to be null edits
+ * This should only be used for proposed revisions that turn out to be null edits.
+ *
+ * @note Only supported on Revisions that were constructed based on associative arrays,
+ * since they are mutable.
*
* @since 1.19
- * @param int $id
+ * @param int|string $id
+ * @throws MWException
*/
public function setId( $id ) {
- $this->mId = (int)$id;
+ if ( $this->mRecord instanceof MutableRevisionRecord ) {
+ $this->mRecord->setId( intval( $id ) );
+ } else {
+ throw new MWException( __METHOD__ . ' is not supported on this instance' );
+ }
}
/**
@@ -761,32 +643,58 @@ class Revision implements IDBAccessObject {
*
* This should only be used for proposed revisions that turn out to be null edits
*
+ * @note Only supported on Revisions that were constructed based on associative arrays,
+ * since they are mutable.
+ *
* @since 1.28
+ * @deprecated since 1.31, please reuse old Revision object
* @param int $id User ID
* @param string $name User name
+ * @throws MWException
*/
public function setUserIdAndName( $id, $name ) {
- $this->mUser = (int)$id;
- $this->mUserText = $name;
- $this->mOrigUserText = $name;
+ if ( $this->mRecord instanceof MutableRevisionRecord ) {
+ $user = User::newFromAnyId( intval( $id ), $name, null );
+ $this->mRecord->setUser( $user );
+ } else {
+ throw new MWException( __METHOD__ . ' is not supported on this instance' );
+ }
}
/**
- * Get text row ID
+ * @return SlotRecord
+ */
+ private function getMainSlotRaw() {
+ return $this->mRecord->getSlot( 'main', RevisionRecord::RAW );
+ }
+
+ /**
+ * Get the ID of the row of the text table that contains the content of the
+ * revision's main slot, if that content is stored in the text table.
+ *
+ * If the content is stored elsewhere, this returns null.
+ *
+ * @deprecated since 1.31, use RevisionRecord()->getSlot()->getContentAddress() to
+ * get that actual address that can be used with BlobStore::getBlob(); or use
+ * RevisionRecord::hasSameContent() to check if two revisions have the same content.
*
* @return int|null
*/
public function getTextId() {
- return $this->mTextId;
+ $slot = $this->getMainSlotRaw();
+ return $slot->hasAddress()
+ ? self::getBlobStore()->getTextIdFromAddress( $slot->getAddress() )
+ : null;
}
/**
* Get parent revision ID (the original previous page revision)
*
- * @return int|null
+ * @return int|null The ID of the parent revision. 0 indicates that there is no
+ * parent revision. Null indicates that the parent revision is not known.
*/
public function getParentId() {
- return $this->mParentId;
+ return $this->mRecord->getParentId();
}
/**
@@ -795,61 +703,54 @@ class Revision implements IDBAccessObject {
* @return int|null
*/
public function getSize() {
- return $this->mSize;
+ try {
+ return $this->mRecord->getSize();
+ } catch ( RevisionAccessException $ex ) {
+ return null;
+ }
}
/**
- * Returns the base36 sha1 of the text in this revision, or null if unknown.
+ * Returns the base36 sha1 of the content in this revision, or null if unknown.
*
* @return string|null
*/
public function getSha1() {
- return $this->mSha1;
+ try {
+ return $this->mRecord->getSha1();
+ } catch ( RevisionAccessException $ex ) {
+ return null;
+ }
}
/**
- * Returns the title of the page associated with this entry or null.
+ * Returns the title of the page associated with this entry.
+ * Since 1.31, this will never return null.
*
* Will do a query, when title is not set and id is given.
*
- * @return Title|null
+ * @return Title
*/
public function getTitle() {
- if ( $this->mTitle !== null ) {
- return $this->mTitle;
- }
- // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
- if ( $this->mId !== null ) {
- $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
- $row = $dbr->selectRow(
- [ 'page', 'revision' ],
- self::selectPageFields(),
- [ 'page_id=rev_page', 'rev_id' => $this->mId ],
- __METHOD__
- );
- if ( $row ) {
- // @TODO: better foreign title handling
- $this->mTitle = Title::newFromRow( $row );
- }
- }
-
- if ( $this->mWiki === false || $this->mWiki === wfWikiID() ) {
- // Loading by ID is best, though not possible for foreign titles
- if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
- $this->mTitle = Title::newFromID( $this->mPage );
- }
- }
-
- return $this->mTitle;
+ $linkTarget = $this->mRecord->getPageAsLinkTarget();
+ return Title::newFromLinkTarget( $linkTarget );
}
/**
* Set the title of the revision
*
+ * @deprecated: since 1.31, this is now a noop. Pass the Title to the constructor instead.
+ *
* @param Title $title
*/
public function setTitle( $title ) {
- $this->mTitle = $title;
+ if ( !$title->equals( $this->getTitle() ) ) {
+ throw new InvalidArgumentException(
+ $title->getPrefixedText()
+ . ' is not the same as '
+ . $this->mRecord->getPageAsLinkTarget()->__toString()
+ );
+ }
}
/**
@@ -858,7 +759,7 @@ class Revision implements IDBAccessObject {
* @return int|null
*/
public function getPage() {
- return $this->mPage;
+ return $this->mRecord->getPageId();
}
/**
@@ -875,13 +776,14 @@ class Revision implements IDBAccessObject {
* @return int
*/
public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
- return 0;
- } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
- return 0;
- } else {
- return $this->mUser;
+ global $wgUser;
+
+ if ( $audience === self::FOR_THIS_USER && !$user ) {
+ $user = $wgUser;
}
+
+ $user = $this->mRecord->getUser( $audience, $user );
+ return $user ? $user->getId() : 0;
}
/**
@@ -909,23 +811,14 @@ class Revision implements IDBAccessObject {
* @return string
*/
public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
- $this->loadMutableFields();
+ global $wgUser;
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
- return '';
- } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
- return '';
- } else {
- if ( $this->mUserText === null ) {
- $this->mUserText = User::whoIs( $this->mUser ); // load on demand
- if ( $this->mUserText === false ) {
- # This shouldn't happen, but it can if the wiki was recovered
- # via importing revs and there is no user table entry yet.
- $this->mUserText = $this->mOrigUserText;
- }
- }
- return $this->mUserText;
+ if ( $audience === self::FOR_THIS_USER && !$user ) {
+ $user = $wgUser;
}
+
+ $user = $this->mRecord->getUser( $audience, $user );
+ return $user ? $user->getName() : '';
}
/**
@@ -953,13 +846,14 @@ class Revision implements IDBAccessObject {
* @return string
*/
function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
- return '';
- } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
- return '';
- } else {
- return $this->mComment;
+ global $wgUser;
+
+ if ( $audience === self::FOR_THIS_USER && !$user ) {
+ $user = $wgUser;
}
+
+ $comment = $this->mRecord->getComment( $audience, $user );
+ return $comment === null ? null : $comment->text;
}
/**
@@ -977,23 +871,14 @@ class Revision implements IDBAccessObject {
* @return bool
*/
public function isMinor() {
- return (bool)$this->mMinorEdit;
+ return $this->mRecord->isMinor();
}
/**
* @return int Rcid of the unpatrolled row, zero if there isn't one
*/
public function isUnpatrolled() {
- if ( $this->mUnpatrolled !== null ) {
- return $this->mUnpatrolled;
- }
- $rc = $this->getRecentChange();
- if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
- $this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
- } else {
- $this->mUnpatrolled = 0;
- }
- return $this->mUnpatrolled;
+ return self::getRevisionStore()->getRcIdIfUnpatrolled( $this->mRecord );
}
/**
@@ -1006,19 +891,7 @@ class Revision implements IDBAccessObject {
* @return RecentChange|null
*/
public function getRecentChange( $flags = 0 ) {
- $dbr = wfGetDB( DB_REPLICA );
-
- list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
-
- return RecentChange::newFromConds(
- [
- 'rc_user_text' => $this->getUserText( self::RAW ),
- 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
- 'rc_this_oldid' => $this->getId()
- ],
- __METHOD__,
- $dbType
- );
+ return self::getRevisionStore()->getRecentChange( $this->mRecord, $flags );
}
/**
@@ -1027,14 +900,7 @@ class Revision implements IDBAccessObject {
* @return bool
*/
public function isDeleted( $field ) {
- if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
- // Current revisions of pages cannot have the content hidden. Skipping this
- // check is very useful for Parser as it fetches templates using newKnownCurrent().
- // Calling getVisibility() in that case triggers a verification database query.
- return false; // no need to check
- }
-
- return ( $this->getVisibility() & $field ) == $field;
+ return $this->mRecord->isDeleted( $field );
}
/**
@@ -1043,19 +909,17 @@ class Revision implements IDBAccessObject {
* @return int
*/
public function getVisibility() {
- $this->loadMutableFields();
-
- return (int)$this->mDeleted;
+ return $this->mRecord->getVisibility();
}
/**
* Fetch revision content if it's available to the specified audience.
* If the specified audience does not have the ability to view this
- * revision, null will be returned.
+ * revision, or the content could not be loaded, null will be returned.
*
* @param int $audience One of:
* Revision::FOR_PUBLIC to be displayed to all users
- * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::FOR_THIS_USER to be displayed to $user
* Revision::RAW get the text regardless of permissions
* @param User $user User object to check for, only if FOR_THIS_USER is passed
* to the $audience parameter
@@ -1063,12 +927,17 @@ class Revision implements IDBAccessObject {
* @return Content|null
*/
public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
- return null;
- } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
+ global $wgUser;
+
+ if ( $audience === self::FOR_THIS_USER && !$user ) {
+ $user = $wgUser;
+ }
+
+ try {
+ return $this->mRecord->getContent( 'main', $audience, $user );
+ }
+ catch ( RevisionAccessException $e ) {
return null;
- } else {
- return $this->getContentInternal();
}
}
@@ -1076,86 +945,51 @@ class Revision implements IDBAccessObject {
* Get original serialized data (without checking view restrictions)
*
* @since 1.21
+ * @deprecated since 1.31, use BlobStore::getBlob instead.
+ *
* @return string
*/
public function getSerializedData() {
- if ( $this->mText === null ) {
- // Revision is immutable. Load on demand.
- $this->mText = $this->loadText();
- }
-
- return $this->mText;
+ $slot = $this->getMainSlotRaw();
+ return $slot->getContent()->serialize();
}
/**
- * Gets the content object for the revision (or null on failure).
- *
- * Note that for mutable Content objects, each call to this method will return a
- * fresh clone.
- *
- * @since 1.21
- * @return Content|null The Revision's content, or null on failure.
- */
- protected function getContentInternal() {
- if ( $this->mContent === null ) {
- $text = $this->getSerializedData();
-
- if ( $text !== null && $text !== false ) {
- // Unserialize content
- $handler = $this->getContentHandler();
- $format = $this->getContentFormat();
-
- $this->mContent = $handler->unserializeContent( $text, $format );
- }
- }
-
- // NOTE: copy() will return $this for immutable content objects
- return $this->mContent ? $this->mContent->copy() : null;
- }
-
- /**
- * Returns the content model for this revision.
+ * Returns the content model for the main slot of this revision.
*
* If no content model was stored in the database, the default content model for the title is
* used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
* is used as a last resort.
*
+ * @todo: drop this, with MCR, there no longer is a single model associated with a revision.
+ *
* @return string The content model id associated with this revision,
* see the CONTENT_MODEL_XXX constants.
*/
public function getContentModel() {
- if ( !$this->mContentModel ) {
- $title = $this->getTitle();
- if ( $title ) {
- $this->mContentModel = ContentHandler::getDefaultModelFor( $title );
- } else {
- $this->mContentModel = CONTENT_MODEL_WIKITEXT;
- }
-
- assert( !empty( $this->mContentModel ) );
- }
-
- return $this->mContentModel;
+ return $this->getMainSlotRaw()->getModel();
}
/**
- * Returns the content format for this revision.
+ * Returns the content format for the main slot of this revision.
*
* If no content format was stored in the database, the default format for this
* revision's content model is returned.
*
+ * @todo: drop this, the format is irrelevant to the revision!
+ *
* @return string The content format id associated with this revision,
* see the CONTENT_FORMAT_XXX constants.
*/
public function getContentFormat() {
- if ( !$this->mContentFormat ) {
- $handler = $this->getContentHandler();
- $this->mContentFormat = $handler->getDefaultFormat();
+ $format = $this->getMainSlotRaw()->getFormat();
- assert( !empty( $this->mContentFormat ) );
+ if ( $format === null ) {
+ // if no format was stored along with the blob, fall back to default format
+ $format = $this->getContentHandler()->getDefaultFormat();
}
- return $this->mContentFormat;
+ return $format;
}
/**
@@ -1165,33 +999,21 @@ class Revision implements IDBAccessObject {
* @return ContentHandler
*/
public function getContentHandler() {
- if ( !$this->mContentHandler ) {
- $model = $this->getContentModel();
- $this->mContentHandler = ContentHandler::getForModelID( $model );
-
- $format = $this->getContentFormat();
-
- if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
- throw new MWException( "Oops, the content format $format is not supported for "
- . "this content model, $model" );
- }
- }
-
- return $this->mContentHandler;
+ return ContentHandler::getForModelID( $this->getContentModel() );
}
/**
* @return string
*/
public function getTimestamp() {
- return wfTimestamp( TS_MW, $this->mTimestamp );
+ return $this->mRecord->getTimestamp();
}
/**
* @return bool
*/
public function isCurrent() {
- return $this->mCurrent;
+ return ( $this->mRecord instanceof RevisionStoreRecord ) && $this->mRecord->isCurrent();
}
/**
@@ -1200,13 +1022,9 @@ class Revision implements IDBAccessObject {
* @return Revision|null
*/
public function getPrevious() {
- if ( $this->getTitle() ) {
- $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
- if ( $prev ) {
- return self::newFromTitle( $this->getTitle(), $prev );
- }
- }
- return null;
+ $title = $this->getTitle();
+ $rec = self::getRevisionLookup()->getPreviousRevision( $this->mRecord, $title );
+ return $rec === null ? null : new Revision( $rec, self::READ_NORMAL, $title );
}
/**
@@ -1215,38 +1033,9 @@ class Revision implements IDBAccessObject {
* @return Revision|null
*/
public function getNext() {
- if ( $this->getTitle() ) {
- $next = $this->getTitle()->getNextRevisionID( $this->getId() );
- if ( $next ) {
- return self::newFromTitle( $this->getTitle(), $next );
- }
- }
- return null;
- }
-
- /**
- * Get previous revision Id for this page_id
- * This is used to populate rev_parent_id on save
- *
- * @param IDatabase $db
- * @return int
- */
- private function getPreviousRevisionId( $db ) {
- if ( $this->mPage === null ) {
- return 0;
- }
- # Use page_latest if ID is not given
- if ( !$this->mId ) {
- $prevId = $db->selectField( 'page', 'page_latest',
- [ 'page_id' => $this->mPage ],
- __METHOD__ );
- } else {
- $prevId = $db->selectField( 'revision', 'rev_id',
- [ 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ],
- __METHOD__,
- [ 'ORDER BY' => 'rev_id DESC' ] );
- }
- return intval( $prevId );
+ $title = $this->getTitle();
+ $rec = self::getRevisionLookup()->getNextRevision( $this->mRecord, $title );
+ return $rec === null ? null : new Revision( $rec, self::READ_NORMAL, $title );
}
/**
@@ -1279,35 +1068,9 @@ class Revision implements IDBAccessObject {
return false;
}
- // Use external methods for external objects, text in table is URL-only then
- if ( in_array( 'external', $flags ) ) {
- $url = $text;
- $parts = explode( '://', $url, 2 );
- if ( count( $parts ) == 1 || $parts[1] == '' ) {
- return false;
- }
-
- if ( isset( $row->old_id ) && $wiki === false ) {
- // Make use of the wiki-local revision text cache
- $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
- // The cached value should be decompressed, so handle that and return here
- return $cache->getWithSetCallback(
- $cache->makeKey( 'revisiontext', 'textid', $row->old_id ),
- self::getCacheTTL( $cache ),
- function () use ( $url, $wiki, $flags ) {
- // No negative caching per Revision::loadText()
- $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
-
- return self::decompressRevisionText( $text, $flags );
- },
- [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
- );
- } else {
- $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
- }
- }
+ $cacheKey = isset( $row->old_id ) ? ( 'tt:' . $row->old_id ) : null;
- return self::decompressRevisionText( $text, $flags );
+ return self::getBlobStore( $wiki )->expandBlob( $text, $flags, $cacheKey );
}
/**
@@ -1321,28 +1084,7 @@ class Revision implements IDBAccessObject {
* @return string
*/
public static function compressRevisionText( &$text ) {
- global $wgCompressRevisions;
- $flags = [];
-
- # Revisions not marked this way will be converted
- # on load if $wgLegacyCharset is set in the future.
- $flags[] = 'utf-8';
-
- if ( $wgCompressRevisions ) {
- if ( function_exists( 'gzdeflate' ) ) {
- $deflated = gzdeflate( $text );
-
- if ( $deflated === false ) {
- wfLogWarning( __METHOD__ . ': gzdeflate() failed' );
- } else {
- $text = $deflated;
- $flags[] = 'gzip';
- }
- } else {
- wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
- }
- }
- return implode( ',', $flags );
+ return self::getBlobStore()->compressData( $text );
}
/**
@@ -1353,46 +1095,7 @@ class Revision implements IDBAccessObject {
* @return string|bool Decompressed text, or false on failure
*/
public static function decompressRevisionText( $text, $flags ) {
- global $wgLegacyEncoding, $wgContLang;
-
- if ( $text === false ) {
- // Text failed to be fetched; nothing to do
- return false;
- }
-
- if ( in_array( 'gzip', $flags ) ) {
- # Deal with optional compression of archived pages.
- # This can be done periodically via maintenance/compressOld.php, and
- # as pages are saved if $wgCompressRevisions is set.
- $text = gzinflate( $text );
-
- if ( $text === false ) {
- wfLogWarning( __METHOD__ . ': gzinflate() failed' );
- return false;
- }
- }
-
- if ( in_array( 'object', $flags ) ) {
- # Generic compressed storage
- $obj = unserialize( $text );
- if ( !is_object( $obj ) ) {
- // Invalid object
- return false;
- }
- $text = $obj->getText();
- }
-
- if ( $text !== false && $wgLegacyEncoding
- && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
- ) {
- # Old revisions kept around in a legacy encoding?
- # Upconvert on demand.
- # ("utf8" checked for compatibility with some broken
- # conversion scripts 2008-12-30)
- $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
- }
-
- return $text;
+ return self::getBlobStore()->decompressData( $text, $flags );
}
/**
@@ -1404,192 +1107,29 @@ class Revision implements IDBAccessObject {
* @return int The revision ID
*/
public function insertOn( $dbw ) {
- global $wgDefaultExternalStore, $wgContentHandlerUseDB;
-
- // We're inserting a new revision, so we have to use master anyway.
- // If it's a null revision, it may have references to rows that
- // are not in the replica yet (the text row).
- $this->mQueryFlags |= self::READ_LATEST;
-
- // Not allowed to have rev_page equal to 0, false, etc.
- if ( !$this->mPage ) {
- $title = $this->getTitle();
- if ( $title instanceof Title ) {
- $titleText = ' for page ' . $title->getPrefixedText();
- } else {
- $titleText = '';
- }
- throw new MWException( "Cannot insert revision$titleText: page ID must be nonzero" );
- }
-
- $this->checkContentModel();
-
- $data = $this->mText;
- $flags = self::compressRevisionText( $data );
+ global $wgUser;
- # Write to external storage if required
- if ( $wgDefaultExternalStore ) {
- // Store and get the URL
- $data = ExternalStore::insertToDefault( $data );
- if ( !$data ) {
- throw new MWException( "Unable to store text to external storage" );
- }
- if ( $flags ) {
- $flags .= ',';
- }
- $flags .= 'external';
- }
+ // Note that $this->mRecord->getId() will typically return null here, but not always,
+ // e.g. not when restoring a revision.
- # Record the text (or external storage URL) to the text table
- if ( $this->mTextId === null ) {
- $dbw->insert( 'text',
- [
- 'old_text' => $data,
- 'old_flags' => $flags,
- ], __METHOD__
- );
- $this->mTextId = $dbw->insertId();
- }
-
- if ( $this->mComment === null ) {
- $this->mComment = "";
- }
-
- # Record the edit in revisions
- $row = [
- 'rev_page' => $this->mPage,
- 'rev_text_id' => $this->mTextId,
- 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
- 'rev_user' => $this->mUser,
- 'rev_user_text' => $this->mUserText,
- 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
- 'rev_deleted' => $this->mDeleted,
- 'rev_len' => $this->mSize,
- 'rev_parent_id' => $this->mParentId === null
- ? $this->getPreviousRevisionId( $dbw )
- : $this->mParentId,
- 'rev_sha1' => $this->mSha1 === null
- ? self::base36Sha1( $this->mText )
- : $this->mSha1,
- ];
- if ( $this->mId !== null ) {
- $row['rev_id'] = $this->mId;
- }
-
- list( $commentFields, $commentCallback ) =
- CommentStore::newKey( 'rev_comment' )->insertWithTempTable( $dbw, $this->mComment );
- $row += $commentFields;
-
- if ( $wgContentHandlerUseDB ) {
- // NOTE: Store null for the default model and format, to save space.
- // XXX: Makes the DB sensitive to changed defaults.
- // Make this behavior optional? Only in miser mode?
-
- $model = $this->getContentModel();
- $format = $this->getContentFormat();
-
- $title = $this->getTitle();
-
- if ( $title === null ) {
- throw new MWException( "Insufficient information to determine the title of the "
- . "revision's page!" );
+ if ( $this->mRecord->getUser( RevisionRecord::RAW ) === null ) {
+ if ( $this->mRecord instanceof MutableRevisionRecord ) {
+ $this->mRecord->setUser( $wgUser );
+ } else {
+ throw new MWException( 'Cannot insert revision with no associated user.' );
}
-
- $defaultModel = ContentHandler::getDefaultModelFor( $title );
- $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
-
- $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
- $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
}
- $dbw->insert( 'revision', $row, __METHOD__ );
+ $rec = self::getRevisionStore()->insertRevisionOn( $this->mRecord, $dbw );
- if ( $this->mId === null ) {
- // Only if auto-increment was used
- $this->mId = $dbw->insertId();
- }
- $commentCallback( $this->mId );
-
- // Assertion to try to catch T92046
- if ( (int)$this->mId === 0 ) {
- throw new UnexpectedValueException(
- 'After insert, Revision mId is ' . var_export( $this->mId, 1 ) . ': ' .
- var_export( $row, 1 )
- );
- }
-
- // Insert IP revision into ip_changes for use when querying for a range.
- if ( $this->mUser === 0 && IP::isValid( $this->mUserText ) ) {
- $ipcRow = [
- 'ipc_rev_id' => $this->mId,
- 'ipc_rev_timestamp' => $row['rev_timestamp'],
- 'ipc_hex' => IP::toHex( $row['rev_user_text'] ),
- ];
- $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
- }
+ $this->mRecord = $rec;
// Avoid PHP 7.1 warning of passing $this by reference
$revision = $this;
- Hooks::run( 'RevisionInsertComplete', [ &$revision, $data, $flags ] );
-
- return $this->mId;
- }
-
- protected function checkContentModel() {
- global $wgContentHandlerUseDB;
-
- // Note: may return null for revisions that have not yet been inserted
- $title = $this->getTitle();
-
- $model = $this->getContentModel();
- $format = $this->getContentFormat();
- $handler = $this->getContentHandler();
-
- if ( !$handler->isSupportedFormat( $format ) ) {
- $t = $title->getPrefixedDBkey();
-
- throw new MWException( "Can't use format $format with content model $model on $t" );
- }
+ // TODO: hard-deprecate in 1.32 (or even 1.31?)
+ Hooks::run( 'RevisionInsertComplete', [ &$revision, null, null ] );
- if ( !$wgContentHandlerUseDB && $title ) {
- // if $wgContentHandlerUseDB is not set,
- // all revisions must use the default content model and format.
-
- $defaultModel = ContentHandler::getDefaultModelFor( $title );
- $defaultHandler = ContentHandler::getForModelID( $defaultModel );
- $defaultFormat = $defaultHandler->getDefaultFormat();
-
- if ( $this->getContentModel() != $defaultModel ) {
- $t = $title->getPrefixedDBkey();
-
- throw new MWException( "Can't save non-default content model with "
- . "\$wgContentHandlerUseDB disabled: model is $model, "
- . "default for $t is $defaultModel" );
- }
-
- if ( $this->getContentFormat() != $defaultFormat ) {
- $t = $title->getPrefixedDBkey();
-
- throw new MWException( "Can't use non-default content format with "
- . "\$wgContentHandlerUseDB disabled: format is $format, "
- . "default for $t is $defaultFormat" );
- }
- }
-
- $content = $this->getContent( self::RAW );
- $prefixedDBkey = $title->getPrefixedDBkey();
- $revId = $this->mId;
-
- if ( !$content ) {
- throw new MWException(
- "Content of revision $revId ($prefixedDBkey) could not be loaded for validation!"
- );
- }
- if ( !$content->isValid() ) {
- throw new MWException(
- "Content of revision $revId ($prefixedDBkey) is not valid! Content model is $model"
- );
- }
+ return $rec->getId();
}
/**
@@ -1598,103 +1138,7 @@ class Revision implements IDBAccessObject {
* @return string
*/
public static function base36Sha1( $text ) {
- return Wikimedia\base_convert( sha1( $text ), 16, 36, 31 );
- }
-
- /**
- * Get the text cache TTL
- *
- * @param WANObjectCache $cache
- * @return int
- */
- private static function getCacheTTL( WANObjectCache $cache ) {
- global $wgRevisionCacheExpiry;
-
- if ( $cache->getQoS( $cache::ATTR_EMULATION ) <= $cache::QOS_EMULATION_SQL ) {
- // Do not cache RDBMs blobs in...the RDBMs store
- $ttl = $cache::TTL_UNCACHEABLE;
- } else {
- $ttl = $wgRevisionCacheExpiry ?: $cache::TTL_UNCACHEABLE;
- }
-
- return $ttl;
- }
-
- /**
- * Lazy-load the revision's text.
- * Currently hardcoded to the 'text' table storage engine.
- *
- * @return string|bool The revision's text, or false on failure
- */
- private function loadText() {
- $cache = ObjectCache::getMainWANInstance();
-
- // No negative caching; negative hits on text rows may be due to corrupted replica DBs
- return $cache->getWithSetCallback(
- $cache->makeKey( 'revisiontext', 'textid', $this->getTextId() ),
- self::getCacheTTL( $cache ),
- function () {
- return $this->fetchText();
- },
- [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
- );
- }
-
- private function fetchText() {
- $textId = $this->getTextId();
-
- // If we kept data for lazy extraction, use it now...
- if ( $this->mTextRow !== null ) {
- $row = $this->mTextRow;
- $this->mTextRow = null;
- } else {
- $row = null;
- }
-
- // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
- // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
- $flags = $this->mQueryFlags;
- $flags |= DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST )
- ? self::READ_LATEST_IMMUTABLE
- : 0;
-
- list( $index, $options, $fallbackIndex, $fallbackOptions ) =
- DBAccessObjectUtils::getDBOptions( $flags );
-
- if ( !$row ) {
- // Text data is immutable; check replica DBs first.
- $row = wfGetDB( $index )->selectRow(
- 'text',
- [ 'old_text', 'old_flags' ],
- [ 'old_id' => $textId ],
- __METHOD__,
- $options
- );
- }
-
- // Fallback to DB_MASTER in some cases if the row was not found
- if ( !$row && $fallbackIndex !== null ) {
- // Use FOR UPDATE if it was used to fetch this revision. This avoids missing the row
- // due to REPEATABLE-READ. Also fallback to the master if READ_LATEST is provided.
- $row = wfGetDB( $fallbackIndex )->selectRow(
- 'text',
- [ 'old_text', 'old_flags' ],
- [ 'old_id' => $textId ],
- __METHOD__,
- $fallbackOptions
- );
- }
-
- if ( !$row ) {
- wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
- }
-
- $text = self::getRevisionText( $row );
- if ( $row && $text === false ) {
- wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
- }
-
- return is_string( $text ) ? $text : false;
+ return SlotRecord::base36Sha1( $text );
}
/**
@@ -1713,58 +1157,21 @@ class Revision implements IDBAccessObject {
* @return Revision|null Revision or null on error
*/
public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) {
- global $wgContentHandlerUseDB;
-
- $fields = [ 'page_latest', 'page_namespace', 'page_title',
- 'rev_text_id', 'rev_len', 'rev_sha1' ];
-
- if ( $wgContentHandlerUseDB ) {
- $fields[] = 'rev_content_model';
- $fields[] = 'rev_content_format';
+ global $wgUser;
+ if ( !$user ) {
+ $user = $wgUser;
}
- $current = $dbw->selectRow(
- [ 'page', 'revision' ],
- $fields,
- [
- 'page_id' => $pageId,
- 'page_latest=rev_id',
- ],
- __METHOD__,
- [ 'FOR UPDATE' ] // T51581
- );
-
- if ( $current ) {
- if ( !$user ) {
- global $wgUser;
- $user = $wgUser;
- }
-
- $row = [
- 'page' => $pageId,
- 'user_text' => $user->getName(),
- 'user' => $user->getId(),
- 'comment' => $summary,
- 'minor_edit' => $minor,
- 'text_id' => $current->rev_text_id,
- 'parent_id' => $current->page_latest,
- 'len' => $current->rev_len,
- 'sha1' => $current->rev_sha1
- ];
-
- if ( $wgContentHandlerUseDB ) {
- $row['content_model'] = $current->rev_content_model;
- $row['content_format'] = $current->rev_content_format;
- }
-
- $row['title'] = Title::makeTitle( $current->page_namespace, $current->page_title );
+ $comment = CommentStoreComment::newUnsavedComment( $summary, null );
- $revision = new Revision( $row );
- } else {
- $revision = null;
+ $title = Title::newFromID( $pageId, Title::GAID_FOR_UPDATE );
+ if ( $title === null ) {
+ return null;
}
- return $revision;
+ $rec = self::getRevisionStore()->newNullRevision( $dbw, $title, $comment, $minor, $user );
+
+ return new Revision( $rec );
}
/**
@@ -1798,35 +1205,13 @@ class Revision implements IDBAccessObject {
public static function userCanBitfield( $bitfield, $field, User $user = null,
Title $title = null
) {
- if ( $bitfield & $field ) { // aspect is deleted
- if ( $user === null ) {
- global $wgUser;
- $user = $wgUser;
- }
- if ( $bitfield & self::DELETED_RESTRICTED ) {
- $permissions = [ 'suppressrevision', 'viewsuppressed' ];
- } elseif ( $field & self::DELETED_TEXT ) {
- $permissions = [ 'deletedtext' ];
- } else {
- $permissions = [ 'deletedhistory' ];
- }
- $permissionlist = implode( ', ', $permissions );
- if ( $title === null ) {
- wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
- return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
- } else {
- $text = $title->getPrefixedText();
- wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
- foreach ( $permissions as $perm ) {
- if ( $title->userCan( $perm, $user ) ) {
- return true;
- }
- }
- return false;
- }
- } else {
- return true;
+ global $wgUser;
+
+ if ( !$user ) {
+ $user = $wgUser;
}
+
+ return RevisionRecord::userCanBitfield( $bitfield, $field, $user, $title );
}
/**
@@ -1838,18 +1223,7 @@ class Revision implements IDBAccessObject {
* @return string|bool False if not found
*/
static function getTimestampFromId( $title, $id, $flags = 0 ) {
- $db = ( $flags & self::READ_LATEST )
- ? wfGetDB( DB_MASTER )
- : wfGetDB( DB_REPLICA );
- // Casting fix for databases that can't take '' for rev_id
- if ( $id == '' ) {
- $id = 0;
- }
- $conds = [ 'rev_id' => $id ];
- $conds['rev_page'] = $title->getArticleID();
- $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
-
- return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
+ return self::getRevisionStore()->getTimestampFromId( $title, $id, $flags );
}
/**
@@ -1860,12 +1234,7 @@ class Revision implements IDBAccessObject {
* @return int
*/
static function countByPageId( $db, $id ) {
- $row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ],
- [ 'rev_page' => $id ], __METHOD__ );
- if ( $row ) {
- return $row->revCount;
- }
- return 0;
+ return self::getRevisionStore()->countRevisionsByPageId( $db, $id );
}
/**
@@ -1876,11 +1245,7 @@ class Revision implements IDBAccessObject {
* @return int
*/
static function countByTitle( $db, $title ) {
- $id = $title->getArticleID();
- if ( $id ) {
- return self::countByPageId( $db, $id );
- }
- return 0;
+ return self::getRevisionStore()->countRevisionsByTitle( $db, $title );
}
/**
@@ -1900,28 +1265,11 @@ class Revision implements IDBAccessObject {
* @return bool True if the given user was the only one to edit since the given timestamp
*/
public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
- if ( !$userId ) {
- return false;
- }
-
if ( is_int( $db ) ) {
$db = wfGetDB( $db );
}
- $res = $db->select( 'revision',
- 'rev_user',
- [
- 'rev_page' => $pageId,
- 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
- ],
- __METHOD__,
- [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ] );
- foreach ( $res as $row ) {
- if ( $row->rev_user != $userId ) {
- return false;
- }
- }
- return true;
+ return self::getRevisionStore()->userWasLastToEdit( $db, $pageId, $userId, $since );
}
/**
@@ -1929,54 +1277,24 @@ class Revision implements IDBAccessObject {
*
* This method allows for the use of caching, though accessing anything that normally
* requires permission checks (aside from the text) will trigger a small DB lookup.
- * The title will also be lazy loaded, though setTitle() can be used to preload it.
+ * The title will also be loaded if $pageIdOrTitle is an integer ID.
*
- * @param IDatabase $db
- * @param int $pageId Page ID
- * @param int $revId Known current revision of this page
+ * @param IDatabase $db ignored!
+ * @param int|Title $pageIdOrTitle Page ID or Title object
+ * @param int $revId Known current revision of this page. Determined automatically if not given.
* @return Revision|bool Returns false if missing
* @since 1.28
*/
- public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) {
- $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
- return $cache->getWithSetCallback(
- // Page/rev IDs passed in from DB to reflect history merges
- $cache->makeGlobalKey( 'revision', $db->getDomainID(), $pageId, $revId ),
- $cache::TTL_WEEK,
- function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
- $setOpts += Database::getCacheSetOptions( $db );
-
- $rev = Revision::loadFromPageId( $db, $pageId, $revId );
- // Reflect revision deletion and user renames
- if ( $rev ) {
- $rev->mTitle = null; // mutable; lazy-load
- $rev->mRefreshMutableFields = true;
- }
-
- return $rev ?: false; // don't cache negatives
- }
- );
- }
+ public static function newKnownCurrent( IDatabase $db, $pageIdOrTitle, $revId = 0 ) {
+ $title = $pageIdOrTitle instanceof Title
+ ? $pageIdOrTitle
+ : Title::newFromID( $pageIdOrTitle );
- /**
- * For cached revisions, make sure the user name and rev_deleted is up-to-date
- */
- private function loadMutableFields() {
- if ( !$this->mRefreshMutableFields ) {
- return; // not needed
+ if ( !$title ) {
+ return false;
}
- $this->mRefreshMutableFields = false;
- $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
- $row = $dbr->selectRow(
- [ 'revision', 'user' ],
- [ 'rev_deleted', 'user_name' ],
- [ 'rev_id' => $this->mId, 'user_id = rev_user' ],
- __METHOD__
- );
- if ( $row ) { // update values
- $this->mDeleted = (int)$row->rev_deleted;
- $this->mUserText = $row->user_name;
- }
+ $record = self::getRevisionLookup()->getKnownCurrentRevision( $title, $revId );
+ return $record ? new Revision( $record ) : false;
}
}
diff --git a/www/wiki/includes/RevisionList.php b/www/wiki/includes/RevisionList.php
index b0bc60a1..5243cc65 100644
--- a/www/wiki/includes/RevisionList.php
+++ b/www/wiki/includes/RevisionList.php
@@ -204,6 +204,16 @@ abstract class RevisionItemBase {
}
/**
+ * Get the DB field name storing actor ids.
+ * Override this function.
+ * @since 1.31
+ * @return bool
+ */
+ public function getAuthorActorField() {
+ return false;
+ }
+
+ /**
* Get the ID, as it would appear in the ids URL parameter
* @return int
*/
@@ -258,6 +268,16 @@ abstract class RevisionItemBase {
}
/**
+ * Get the author actor ID
+ * @since 1.31
+ * @return string
+ */
+ public function getAuthorActor() {
+ $field = $this->getAuthorActorField();
+ return strval( $this->row->$field );
+ }
+
+ /**
* Returns true if the current user can view the item
*/
abstract public function canView();
@@ -296,15 +316,14 @@ class RevisionList extends RevisionListBase {
if ( $this->ids !== null ) {
$conds['rev_id'] = array_map( 'intval', $this->ids );
}
+ $revQuery = Revision::getQueryInfo( [ 'page', 'user' ] );
return $db->select(
- [ 'revision', 'page', 'user' ],
- array_merge( Revision::selectFields(), Revision::selectUserFields() ),
+ $revQuery['tables'],
+ $revQuery['fields'],
$conds,
__METHOD__,
[ 'ORDER BY' => 'rev_id DESC' ],
- [
- 'page' => Revision::pageJoinCond(),
- 'user' => Revision::userJoinCond() ]
+ $revQuery['joins']
);
}
diff --git a/www/wiki/includes/ServiceWiring.php b/www/wiki/includes/ServiceWiring.php
index 75ce8eca..f97d60df 100644
--- a/www/wiki/includes/ServiceWiring.php
+++ b/www/wiki/includes/ServiceWiring.php
@@ -37,11 +37,18 @@
* MediaWiki code base.
*/
+use MediaWiki\Auth\AuthManager;
use MediaWiki\Interwiki\ClassicInterwikiLookup;
use MediaWiki\Linker\LinkRendererFactory;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
+use MediaWiki\Preferences\DefaultPreferencesFactory;
use MediaWiki\Shell\CommandFactory;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\NameTableStore;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\SqlBlobStore;
+use Wikimedia\ObjectFactory;
return [
'DBLoadBalancerFactory' => function ( MediaWikiServices $services ) {
@@ -54,7 +61,10 @@ return [
);
$class = MWLBFactory::getLBFactoryClass( $lbConf );
- return new $class( $lbConf );
+ $instance = new $class( $lbConf );
+ MWLBFactory::setSchemaAliases( $instance, $mainConfig );
+
+ return $instance;
},
'DBLoadBalancer' => function ( MediaWikiServices $services ) {
@@ -158,14 +168,24 @@ return [
$store = new WatchedItemStore(
$services->getDBLoadBalancer(),
new HashBagOStuff( [ 'maxKeys' => 100 ] ),
- $services->getReadOnlyMode()
+ $services->getReadOnlyMode(),
+ $services->getMainConfig()->get( 'UpdateRowsPerQuery' )
);
$store->setStatsdDataFactory( $services->getStatsdDataFactory() );
+
+ if ( $services->getMainConfig()->get( 'ReadOnlyWatchedItemStore' ) ) {
+ $store = new NoWriteWatchedItemStore( $store );
+ }
+
return $store;
},
'WatchedItemQueryService' => function ( MediaWikiServices $services ) {
- return new WatchedItemQueryService( $services->getDBLoadBalancer() );
+ return new WatchedItemQueryService(
+ $services->getDBLoadBalancer(),
+ $services->getCommentStore(),
+ $services->getActorMigration()
+ );
},
'CryptRand' => function ( MediaWikiServices $services ) {
@@ -266,8 +286,14 @@ return [
$detectorCmd = $mainConfig->get( 'MimeDetectorCommand' );
if ( $detectorCmd ) {
- $params['detectCallback'] = function ( $file ) use ( $detectorCmd ) {
- return wfShellExec( "$detectorCmd " . wfEscapeShellArg( $file ) );
+ $factory = $services->getShellCommandFactory();
+ $params['detectCallback'] = function ( $file ) use ( $detectorCmd, $factory ) {
+ $result = $factory->create()
+ // $wgMimeDetectorCommand can contain commands with parameters
+ ->unsafeParams( $detectorCmd )
+ ->params( $file )
+ ->execute();
+ return $result->getStdout();
};
}
@@ -384,8 +410,6 @@ return [
$id = 'apc';
} elseif ( function_exists( 'apcu_fetch' ) ) {
$id = 'apcu';
- } elseif ( function_exists( 'xcache_get' ) && wfIniGetBool( 'xcache.var_size' ) ) {
- $id = 'xcache';
} elseif ( function_exists( 'wincache_ucache_get' ) ) {
$id = 'wincache';
} else {
@@ -429,6 +453,29 @@ return [
);
},
+ 'UploadRevisionImporter' => function ( MediaWikiServices $services ) {
+ return new ImportableUploadRevisionImporter(
+ $services->getMainConfig()->get( 'EnableUploads' ),
+ LoggerFactory::getInstance( 'UploadRevisionImporter' )
+ );
+ },
+
+ 'OldRevisionImporter' => function ( MediaWikiServices $services ) {
+ return new ImportableOldRevisionImporter(
+ true,
+ LoggerFactory::getInstance( 'OldRevisionImporter' ),
+ $services->getDBLoadBalancer()
+ );
+ },
+
+ 'WikiRevisionOldRevisionImporterNoUpdates' => function ( MediaWikiServices $services ) {
+ return new ImportableOldRevisionImporter(
+ false,
+ LoggerFactory::getInstance( 'OldRevisionImporter' ),
+ $services->getDBLoadBalancer()
+ );
+ },
+
'ShellCommandFactory' => function ( MediaWikiServices $services ) {
$config = $services->getMainConfig();
@@ -439,13 +486,127 @@ return [
'filesize' => $config->get( 'MaxShellFileSize' ),
];
$cgroup = $config->get( 'ShellCgroup' );
+ $restrictionMethod = $config->get( 'ShellRestrictionMethod' );
- $factory = new CommandFactory( $limits, $cgroup );
+ $factory = new CommandFactory( $limits, $cgroup, $restrictionMethod );
$factory->setLogger( LoggerFactory::getInstance( 'exec' ) );
+ $factory->logStderr();
return $factory;
},
+ 'ExternalStoreFactory' => function ( MediaWikiServices $services ) {
+ $config = $services->getMainConfig();
+
+ return new ExternalStoreFactory(
+ $config->get( 'ExternalStores' )
+ );
+ },
+
+ 'RevisionStore' => function ( MediaWikiServices $services ) {
+ /** @var SqlBlobStore $blobStore */
+ $blobStore = $services->getService( '_SqlBlobStore' );
+
+ $store = new RevisionStore(
+ $services->getDBLoadBalancer(),
+ $blobStore,
+ $services->getMainWANObjectCache(),
+ $services->getCommentStore(),
+ $services->getActorMigration()
+ );
+
+ $store->setLogger( LoggerFactory::getInstance( 'RevisionStore' ) );
+
+ $config = $services->getMainConfig();
+ $store->setContentHandlerUseDB( $config->get( 'ContentHandlerUseDB' ) );
+
+ return $store;
+ },
+
+ 'RevisionLookup' => function ( MediaWikiServices $services ) {
+ return $services->getRevisionStore();
+ },
+
+ 'RevisionFactory' => function ( MediaWikiServices $services ) {
+ return $services->getRevisionStore();
+ },
+
+ 'BlobStoreFactory' => function ( MediaWikiServices $services ) {
+ global $wgContLang;
+ return new BlobStoreFactory(
+ $services->getDBLoadBalancer(),
+ $services->getMainWANObjectCache(),
+ $services->getMainConfig(),
+ $wgContLang
+ );
+ },
+
+ 'BlobStore' => function ( MediaWikiServices $services ) {
+ return $services->getService( '_SqlBlobStore' );
+ },
+
+ '_SqlBlobStore' => function ( MediaWikiServices $services ) {
+ return $services->getBlobStoreFactory()->newSqlBlobStore();
+ },
+
+ 'ContentModelStore' => function ( MediaWikiServices $services ) {
+ return new NameTableStore(
+ $services->getDBLoadBalancer(),
+ $services->getMainWANObjectCache(),
+ LoggerFactory::getInstance( 'NameTableSqlStore' ),
+ 'content_models',
+ 'model_id',
+ 'model_name'
+ /**
+ * No strtolower normalization is added to the service as there are examples of
+ * extensions that do not stick to this assumption.
+ * - extensions/examples/DataPages define( 'CONTENT_MODEL_XML_DATA','XML_DATA' );
+ * - extensions/Scribunto define( 'CONTENT_MODEL_SCRIBUNTO', 'Scribunto' );
+ */
+ );
+ },
+
+ 'SlotRoleStore' => function ( MediaWikiServices $services ) {
+ return new NameTableStore(
+ $services->getDBLoadBalancer(),
+ $services->getMainWANObjectCache(),
+ LoggerFactory::getInstance( 'NameTableSqlStore' ),
+ 'slot_roles',
+ 'role_id',
+ 'role_name',
+ 'strtolower'
+ );
+ },
+
+ 'PreferencesFactory' => function ( MediaWikiServices $services ) {
+ global $wgContLang;
+ $authManager = AuthManager::singleton();
+ $linkRenderer = $services->getLinkRendererFactory()->create();
+ $config = $services->getMainConfig();
+ $factory = new DefaultPreferencesFactory( $config, $wgContLang, $authManager, $linkRenderer );
+ $factory->setLogger( LoggerFactory::getInstance( 'preferences' ) );
+
+ return $factory;
+ },
+
+ 'HttpRequestFactory' => function ( MediaWikiServices $services ) {
+ return new \MediaWiki\Http\HttpRequestFactory();
+ },
+
+ 'CommentStore' => function ( MediaWikiServices $services ) {
+ global $wgContLang;
+ return new CommentStore(
+ $wgContLang,
+ $services->getMainConfig()->get( 'CommentTableSchemaMigrationStage' )
+ );
+ },
+
+ 'ActorMigration' => function ( MediaWikiServices $services ) {
+ return new ActorMigration(
+ $services->getMainConfig()->get( 'ActorTableSchemaMigrationStage' )
+ );
+ },
+
///////////////////////////////////////////////////////////////////////////
// NOTE: When adding a service here, don't forget to add a getter function
// in the MediaWikiServices class. The convenience getter should just call
diff --git a/www/wiki/includes/Services/ServiceContainer.php b/www/wiki/includes/Services/ServiceContainer.php
deleted file mode 100644
index e3cda2ee..00000000
--- a/www/wiki/includes/Services/ServiceContainer.php
+++ /dev/null
@@ -1,222 +0,0 @@
-<?php
-namespace MediaWiki\Services;
-
-use InvalidArgumentException;
-use RuntimeException;
-use Wikimedia\Assert\Assert;
-
-/**
- * Generic service container.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- *
- * @since 1.27
- */
-
-/**
- * ServiceContainer provides a generic service to manage named services using
- * lazy instantiation based on instantiator callback functions.
- *
- * Services managed by an instance of ServiceContainer may or may not implement
- * a common interface.
- *
- * @note When using ServiceContainer to manage a set of services, consider
- * creating a wrapper or a subclass that provides access to the services via
- * getter methods with more meaningful names and more specific return type
- * declarations.
- *
- * @see docs/injection.txt for an overview of using dependency injection in the
- * MediaWiki code base.
- */
-class ServiceContainer {
-
- /**
- * @var object[]
- */
- private $services = [];
-
- /**
- * @var callable[]
- */
- private $serviceInstantiators = [];
-
- /**
- * @var array
- */
- private $extraInstantiationParams;
-
- /**
- * @param array $extraInstantiationParams Any additional parameters to be passed to the
- * instantiator function when creating a service. This is typically used to provide
- * access to additional ServiceContainers or Config objects.
- */
- public function __construct( array $extraInstantiationParams = [] ) {
- $this->extraInstantiationParams = $extraInstantiationParams;
- }
-
- /**
- * @param array $wiringFiles A list of PHP files to load wiring information from.
- * Each file is loaded using PHP's include mechanism. Each file is expected to
- * return an associative array that maps service names to instantiator functions.
- */
- public function loadWiringFiles( array $wiringFiles ) {
- foreach ( $wiringFiles as $file ) {
- // the wiring file is required to return an array of instantiators.
- $wiring = require $file;
-
- Assert::postcondition(
- is_array( $wiring ),
- "Wiring file $file is expected to return an array!"
- );
-
- $this->applyWiring( $wiring );
- }
- }
-
- /**
- * Registers multiple services (aka a "wiring").
- *
- * @param array $serviceInstantiators An associative array mapping service names to
- * instantiator functions.
- */
- public function applyWiring( array $serviceInstantiators ) {
- Assert::parameterElementType( 'callable', $serviceInstantiators, '$serviceInstantiators' );
-
- foreach ( $serviceInstantiators as $name => $instantiator ) {
- $this->defineService( $name, $instantiator );
- }
- }
-
- /**
- * Returns true if a service is defined for $name, that is, if a call to getService( $name )
- * would return a service instance.
- *
- * @param string $name
- *
- * @return bool
- */
- public function hasService( $name ) {
- return isset( $this->serviceInstantiators[$name] );
- }
-
- /**
- * @return string[]
- */
- public function getServiceNames() {
- return array_keys( $this->serviceInstantiators );
- }
-
- /**
- * Define a new service. The service must not be known already.
- *
- * @see getService().
- * @see replaceService().
- *
- * @param string $name The name of the service to register, for use with getService().
- * @param callable $instantiator Callback that returns a service instance.
- * Will be called with this MediaWikiServices instance as the only parameter.
- * Any extra instantiation parameters provided to the constructor will be
- * passed as subsequent parameters when invoking the instantiator.
- *
- * @throws RuntimeException if there is already a service registered as $name.
- */
- public function defineService( $name, callable $instantiator ) {
- Assert::parameterType( 'string', $name, '$name' );
-
- if ( $this->hasService( $name ) ) {
- throw new RuntimeException( 'Service already defined: ' . $name );
- }
-
- $this->serviceInstantiators[$name] = $instantiator;
- }
-
- /**
- * Replace an already defined service.
- *
- * @see defineService().
- *
- * @note This causes any previously instantiated instance of the service to be discarded.
- *
- * @param string $name The name of the service to register.
- * @param callable $instantiator Callback function that returns a service instance.
- * Will be called with this MediaWikiServices instance as the only parameter.
- * The instantiator must return a service compatible with the originally defined service.
- * Any extra instantiation parameters provided to the constructor will be
- * passed as subsequent parameters when invoking the instantiator.
- *
- * @throws RuntimeException if $name is not a known service.
- */
- public function redefineService( $name, callable $instantiator ) {
- Assert::parameterType( 'string', $name, '$name' );
-
- if ( !$this->hasService( $name ) ) {
- throw new RuntimeException( 'Service not defined: ' . $name );
- }
-
- if ( isset( $this->services[$name] ) ) {
- throw new RuntimeException( 'Cannot redefine a service that is already in use: ' . $name );
- }
-
- $this->serviceInstantiators[$name] = $instantiator;
- }
-
- /**
- * Returns a service object of the kind associated with $name.
- * Services instances are instantiated lazily, on demand.
- * This method may or may not return the same service instance
- * when called multiple times with the same $name.
- *
- * @note Rather than calling this method directly, it is recommended to provide
- * getters with more meaningful names and more specific return types, using
- * a subclass or wrapper.
- *
- * @see redefineService().
- *
- * @param string $name The service name
- *
- * @throws InvalidArgumentException if $name is not a known service.
- * @return object The service instance
- */
- public function getService( $name ) {
- if ( !isset( $this->services[$name] ) ) {
- $this->services[$name] = $this->createService( $name );
- }
-
- return $this->services[$name];
- }
-
- /**
- * @param string $name
- *
- * @throws InvalidArgumentException if $name is not a known service.
- * @return object
- */
- private function createService( $name ) {
- if ( isset( $this->serviceInstantiators[$name] ) ) {
- $service = call_user_func_array(
- $this->serviceInstantiators[$name],
- array_merge( [ $this ], $this->extraInstantiationParams )
- );
- } else {
- throw new InvalidArgumentException( 'Unknown service: ' . $name );
- }
-
- return $service;
- }
-
-}
diff --git a/www/wiki/includes/Setup.php b/www/wiki/includes/Setup.php
index 68e3d96a..7b7cafcd 100644
--- a/www/wiki/includes/Setup.php
+++ b/www/wiki/includes/Setup.php
@@ -33,6 +33,82 @@ if ( !defined( 'MEDIAWIKI' ) ) {
exit( 1 );
}
+/**
+ * Pre-config setup: Before loading LocalSettings.php
+ */
+
+// Get profiler configuraton
+$wgProfiler = [];
+if ( file_exists( "$IP/StartProfiler.php" ) ) {
+ require "$IP/StartProfiler.php";
+}
+
+// Start the autoloader, so that extensions can derive classes from core files
+require_once "$IP/includes/AutoLoader.php";
+
+// Load up some global defines
+require_once "$IP/includes/Defines.php";
+
+// Load default settings
+require_once "$IP/includes/DefaultSettings.php";
+
+// Load global functions
+require_once "$IP/includes/GlobalFunctions.php";
+
+// Load composer's autoloader if present
+if ( is_readable( "$IP/vendor/autoload.php" ) ) {
+ require_once "$IP/vendor/autoload.php";
+}
+
+// Assert that composer dependencies were successfully loaded
+// Purposely no leading \ due to it breaking HHVM RepoAuthorative mode
+// PHP works fine with both versions
+// See https://github.com/facebook/hhvm/issues/5833
+if ( !interface_exists( 'Psr\Log\LoggerInterface' ) ) {
+ $message = (
+ 'MediaWiki requires the <a href="https://github.com/php-fig/log">PSR-3 logging ' .
+ "library</a> to be present. This library is not embedded directly in MediaWiki's " .
+ "git repository and must be installed separately by the end user.\n\n" .
+ 'Please see <a href="https://www.mediawiki.org/wiki/Download_from_Git' .
+ '#Fetch_external_libraries">mediawiki.org</a> for help on installing ' .
+ 'the required components.'
+ );
+ echo $message;
+ trigger_error( $message, E_USER_ERROR );
+ die( 1 );
+}
+
+// Install a header callback
+MediaWiki\HeaderCallback::register();
+
+/**
+ * Load LocalSettings.php
+ */
+
+if ( defined( 'MW_CONFIG_CALLBACK' ) ) {
+ call_user_func( MW_CONFIG_CALLBACK );
+} else {
+ if ( !defined( 'MW_CONFIG_FILE' ) ) {
+ define( 'MW_CONFIG_FILE', "$IP/LocalSettings.php" );
+ }
+ require_once MW_CONFIG_FILE;
+}
+
+/**
+ * Customization point after all loading (constants, functions, classes,
+ * DefaultSettings, LocalSettings). Specifically, this is before usage of
+ * settings, before instantiation of Profiler (and other singletons), and
+ * before any setup functions or hooks run.
+ */
+
+if ( defined( 'MW_SETUP_CALLBACK' ) ) {
+ call_user_func( MW_SETUP_CALLBACK );
+}
+
+/**
+ * Main setup
+ */
+
$fname = 'Setup.php';
$ps_setup = Profiler::instance()->scopedProfileIn( $fname );
@@ -173,12 +249,12 @@ $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
*/
$wgLockManagers[] = [
'name' => 'fsLockManager',
- 'class' => 'FSLockManager',
+ 'class' => FSLockManager::class,
'lockDirectory' => "{$wgUploadDirectory}/lockdir",
];
$wgLockManagers[] = [
'name' => 'nullLockManager',
- 'class' => 'NullLockManager',
+ 'class' => NullLockManager::class,
];
/**
@@ -200,11 +276,10 @@ $wgGalleryOptions += [
*/
if ( !$wgLocalFileRepo ) {
$wgLocalFileRepo = [
- 'class' => 'LocalRepo',
+ 'class' => LocalRepo::class,
'name' => 'local',
'directory' => $wgUploadDirectory,
'scriptDirUrl' => $wgScriptPath,
- 'scriptExtension' => '.php',
'url' => $wgUploadBaseUrl ? $wgUploadBaseUrl . $wgUploadPath : $wgUploadPath,
'hashLevels' => $wgHashedUploadDirectory ? 2 : 0,
'thumbScriptUrl' => $wgThumbnailScriptPath,
@@ -219,7 +294,7 @@ if ( !$wgLocalFileRepo ) {
if ( $wgUseSharedUploads ) {
if ( $wgSharedUploadDBname ) {
$wgForeignFileRepos[] = [
- 'class' => 'ForeignDBRepo',
+ 'class' => ForeignDBRepo::class,
'name' => 'shared',
'directory' => $wgSharedUploadDirectory,
'url' => $wgSharedUploadPath,
@@ -239,7 +314,7 @@ if ( $wgUseSharedUploads ) {
];
} else {
$wgForeignFileRepos[] = [
- 'class' => 'FileRepo',
+ 'class' => FileRepo::class,
'name' => 'shared',
'directory' => $wgSharedUploadDirectory,
'url' => $wgSharedUploadPath,
@@ -253,7 +328,7 @@ if ( $wgUseSharedUploads ) {
}
if ( $wgUseInstantCommons ) {
$wgForeignFileRepos[] = [
- 'class' => 'ForeignAPIRepo',
+ 'class' => ForeignAPIRepo::class,
'name' => 'wikimediacommons',
'apibase' => 'https://commons.wikimedia.org/w/api.php',
'url' => 'https://upload.wikimedia.org/wikipedia/commons',
@@ -273,7 +348,7 @@ if ( !isset( $wgLocalFileRepo['backend'] ) ) {
$wgLocalFileRepo['backend'] = $wgLocalFileRepo['name'] . '-backend';
}
foreach ( $wgForeignFileRepos as &$repo ) {
- if ( !isset( $repo['directory'] ) && $repo['class'] === 'ForeignAPIRepo' ) {
+ if ( !isset( $repo['directory'] ) && $repo['class'] === ForeignAPIRepo::class ) {
$repo['directory'] = $wgUploadDirectory; // b/c
}
if ( !isset( $repo['backend'] ) ) {
@@ -284,7 +359,8 @@ unset( $repo ); // no global pollution; destroy reference
// Convert this deprecated setting to modern system
if ( $wgExperimentalHtmlIds ) {
- $wgFragmentMode = [ 'html5-legacy', 'legacy' ];
+ wfDeprecated( '$wgExperimentalHtmlIds', '1.30' );
+ $wgFragmentMode = [ 'html5-legacy', 'html5' ];
}
$rcMaxAgeDays = $wgRCMaxAge / ( 3600 * 24 );
@@ -294,9 +370,8 @@ if ( $wgRCFilterByAge ) {
// Note that we allow 1 link higher than the max for things like 56 days but a 60 day link.
sort( $wgRCLinkDays );
- // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
+ // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
for ( $i = 0; $i < count( $wgRCLinkDays ); $i++ ) {
- // @codingStandardsIgnoreEnd
if ( $wgRCLinkDays[$i] >= $rcMaxAgeDays ) {
$wgRCLinkDays = array_slice( $wgRCLinkDays, 0, $i + 1, false );
break;
@@ -452,9 +527,9 @@ $wgJsMimeType = 'text/javascript';
$wgFileExtensions = array_values( array_diff( $wgFileExtensions, $wgFileBlacklist ) );
if ( $wgInvalidateCacheOnLocalSettingsChange ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$wgCacheEpoch = max( $wgCacheEpoch, gmdate( 'YmdHis', filemtime( "$IP/LocalSettings.php" ) ) );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
if ( $wgNewUserLog ) {
@@ -462,16 +537,16 @@ if ( $wgNewUserLog ) {
$wgLogTypes[] = 'newusers';
$wgLogNames['newusers'] = 'newuserlogpage';
$wgLogHeaders['newusers'] = 'newuserlogpagetext';
- $wgLogActionsHandlers['newusers/newusers'] = 'NewUsersLogFormatter';
- $wgLogActionsHandlers['newusers/create'] = 'NewUsersLogFormatter';
- $wgLogActionsHandlers['newusers/create2'] = 'NewUsersLogFormatter';
- $wgLogActionsHandlers['newusers/byemail'] = 'NewUsersLogFormatter';
- $wgLogActionsHandlers['newusers/autocreate'] = 'NewUsersLogFormatter';
+ $wgLogActionsHandlers['newusers/newusers'] = NewUsersLogFormatter::class;
+ $wgLogActionsHandlers['newusers/create'] = NewUsersLogFormatter::class;
+ $wgLogActionsHandlers['newusers/create2'] = NewUsersLogFormatter::class;
+ $wgLogActionsHandlers['newusers/byemail'] = NewUsersLogFormatter::class;
+ $wgLogActionsHandlers['newusers/autocreate'] = NewUsersLogFormatter::class;
}
if ( $wgPageLanguageUseDB ) {
$wgLogTypes[] = 'pagelang';
- $wgLogActionsHandlers['pagelang/pagelang'] = 'PageLangLogFormatter';
+ $wgLogActionsHandlers['pagelang/pagelang'] = PageLangLogFormatter::class;
}
if ( $wgCookieSecure === 'detect' ) {
@@ -622,7 +697,7 @@ if ( $wgMainWANCache === false ) {
// Sites using multiple datacenters can configure a relayer.
$wgMainWANCache = 'mediawiki-main-default';
$wgWANObjectCaches[$wgMainWANCache] = [
- 'class' => 'WANObjectCache',
+ 'class' => WANObjectCache::class,
'cacheId' => $wgMainCacheType,
'channels' => [ 'purge' => 'wancache-main-default-purge' ]
];
@@ -641,9 +716,9 @@ wfMemoryLimit();
* explicitly set. Inspired by phpMyAdmin's treatment of the problem.
*/
if ( is_null( $wgLocaltimezone ) ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$wgLocaltimezone = date_default_timezone_get();
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
date_default_timezone_set( $wgLocaltimezone );
@@ -658,14 +733,24 @@ if ( !$wgDBerrorLogTZ ) {
$wgDBerrorLogTZ = $wgLocaltimezone;
}
-// initialize the request object in $wgRequest
+// Initialize the request object in $wgRequest
$wgRequest = RequestContext::getMain()->getRequest(); // BackCompat
-// Set user IP/agent information for causal consistency purposes
+// Set user IP/agent information for agent session consistency purposes
MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->setRequestInfo( [
'IPAddress' => $wgRequest->getIP(),
'UserAgent' => $wgRequest->getHeader( 'User-Agent' ),
- 'ChronologyProtection' => $wgRequest->getHeader( 'ChronologyProtection' )
+ 'ChronologyProtection' => $wgRequest->getHeader( 'ChronologyProtection' ),
+ // The cpPosIndex cookie has no prefix and is set by MediaWiki::preOutputCommit()
+ 'ChronologyPositionIndex' =>
+ $wgRequest->getInt( 'cpPosIndex', (int)$wgRequest->getCookie( 'cpPosIndex', '' ) )
] );
+// Make sure that object caching does not undermine the ChronologyProtector improvements
+if ( $wgRequest->getCookie( 'UseDC', '' ) === 'master' ) {
+ // The user is pinned to the primary DC, meaning that they made recent changes which should
+ // be reflected in their subsequent web requests. Avoid the use of interim cache keys because
+ // they use a blind TTL and could be stale if an object changes twice in a short time span.
+ MediaWikiServices::getInstance()->getMainWANObjectCache()->useInterimHoldOffCaching( false );
+}
// Useful debug output
if ( $wgCommandLineMode ) {
@@ -784,11 +869,19 @@ if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) {
$session->renew();
if ( MediaWiki\Session\PHPSessionHandler::isEnabled() &&
- ( $session->isPersistent() || $session->shouldRememberUser() )
+ ( $session->isPersistent() || $session->shouldRememberUser() ) &&
+ session_id() !== $session->getId()
) {
// Start the PHP-session for backwards compatibility
+ if ( session_id() !== '' ) {
+ wfDebugLog( 'session', 'PHP session {old_id} was already started, changing to {new_id}', 'all', [
+ 'old_id' => session_id(),
+ 'new_id' => $session->getId(),
+ ] );
+ session_write_close();
+ }
session_id( $session->getId() );
- MediaWiki\quietCall( 'session_start' );
+ session_start();
}
unset( $session );
diff --git a/www/wiki/includes/SiteConfiguration.php b/www/wiki/includes/SiteConfiguration.php
index 7a01a657..6bd179a9 100644
--- a/www/wiki/includes/SiteConfiguration.php
+++ b/www/wiki/includes/SiteConfiguration.php
@@ -20,6 +20,8 @@
* @file
*/
+use MediaWiki\Shell\Shell;
+
/**
* This is a class for holding configuration settings, particularly for
* multi-wiki sites.
@@ -546,19 +548,21 @@ class SiteConfiguration {
} else {
$this->cfgCache[$wiki] = [];
}
- $retVal = 1;
- $cmd = wfShellWikiCmd(
+ $result = Shell::makeScriptCommand(
"$IP/maintenance/getConfiguration.php",
[
'--wiki', $wiki,
'--settings', implode( ' ', $settings ),
- '--format', 'PHP'
+ '--format', 'PHP',
]
- );
- // ulimit5.sh breaks this call
- $data = trim( wfShellExec( $cmd, $retVal, [], [ 'memory' => 0 ] ) );
- if ( $retVal != 0 || !strlen( $data ) ) {
- throw new MWException( "Failed to run getConfiguration.php." );
+ )
+ // limit.sh breaks this call
+ ->limits( [ 'memory' => 0, 'filesize' => 0 ] )
+ ->execute();
+
+ $data = trim( $result->getStdout() );
+ if ( $result->getExitCode() != 0 || !strlen( $data ) ) {
+ throw new MWException( "Failed to run getConfiguration.php: {$result->getStdout()}" );
}
$res = unserialize( $data );
if ( !is_array( $res ) ) {
diff --git a/www/wiki/includes/SiteStats.php b/www/wiki/includes/SiteStats.php
index ce87596a..745c8916 100644
--- a/www/wiki/includes/SiteStats.php
+++ b/www/wiki/includes/SiteStats.php
@@ -23,161 +23,123 @@
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\LoadBalancer;
/**
* Static accessor class for site_stats and related things
*/
class SiteStats {
- /** @var bool|stdClass */
+ /** @var stdClass */
private static $row;
- /** @var bool */
- private static $loaded = false;
-
- /** @var int[] */
- private static $pageCount = [];
-
- static function unload() {
- self::$loaded = false;
- }
-
- static function recache() {
- self::load( true );
- }
-
/**
- * @param bool $recache
+ * Trigger a reload next time a field is accessed
*/
- static function load( $recache = false ) {
- if ( self::$loaded && !$recache ) {
- return;
- }
-
- self::$row = self::loadAndLazyInit();
+ public static function unload() {
+ self::$row = null;
+ }
- # This code is somewhat schema-agnostic, because I'm changing it in a minor release -- TS
- if ( !isset( self::$row->ss_total_pages ) && self::$row->ss_total_pages == -1 ) {
- # Update schema
- $u = new SiteStatsUpdate( 0, 0, 0 );
- $u->doUpdate();
- self::$row = self::doLoad( wfGetDB( DB_REPLICA ) );
+ protected static function load() {
+ if ( self::$row === null ) {
+ self::$row = self::loadAndLazyInit();
}
-
- self::$loaded = true;
}
/**
- * @return bool|stdClass
+ * @return stdClass
*/
- static function loadAndLazyInit() {
- global $wgMiserMode;
+ protected static function loadAndLazyInit() {
+ $config = MediaWikiServices::getInstance()->getMainConfig();
+ $lb = self::getLB();
+ $dbr = $lb->getConnection( DB_REPLICA );
wfDebug( __METHOD__ . ": reading site_stats from replica DB\n" );
- $row = self::doLoad( wfGetDB( DB_REPLICA ) );
-
- if ( !self::isSane( $row ) ) {
- $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
- if ( $lb->hasOrMadeRecentMasterChanges() ) {
- // Might have just been initialized during this request? Underflow?
- wfDebug( __METHOD__ . ": site_stats damaged or missing on replica DB\n" );
- $row = self::doLoad( wfGetDB( DB_MASTER ) );
- }
- }
+ $row = self::doLoadFromDB( $dbr );
- if ( !$wgMiserMode && !self::isSane( $row ) ) {
- // Normally the site_stats table is initialized at install time.
- // Some manual construction scenarios may leave the table empty or
- // broken, however, for instance when importing from a dump into a
- // clean schema with mwdumper.
- wfDebug( __METHOD__ . ": initializing damaged or missing site_stats\n" );
+ if ( !self::isRowSane( $row ) && $lb->hasOrMadeRecentMasterChanges() ) {
+ // Might have just been initialized during this request? Underflow?
+ wfDebug( __METHOD__ . ": site_stats damaged or missing on replica DB\n" );
+ $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
+ }
- SiteStatsInit::doAllAndCommit( wfGetDB( DB_REPLICA ) );
+ if ( !self::isRowSane( $row ) ) {
+ if ( $config->get( 'MiserMode' ) ) {
+ // Start off with all zeroes, assuming that this is a new wiki or any
+ // repopulations where done manually via script.
+ SiteStatsInit::doPlaceholderInit();
+ } else {
+ // Normally the site_stats table is initialized at install time.
+ // Some manual construction scenarios may leave the table empty or
+ // broken, however, for instance when importing from a dump into a
+ // clean schema with mwdumper.
+ wfDebug( __METHOD__ . ": initializing damaged or missing site_stats\n" );
+ SiteStatsInit::doAllAndCommit( $dbr );
+ }
- $row = self::doLoad( wfGetDB( DB_MASTER ) );
+ $row = self::doLoadFromDB( $lb->getConnection( DB_MASTER ) );
}
- if ( !self::isSane( $row ) ) {
+ if ( !self::isRowSane( $row ) ) {
wfDebug( __METHOD__ . ": site_stats persistently nonsensical o_O\n" );
+ // Always return a row-like object
+ $row = self::salvageInsaneRow( $row );
}
return $row;
}
/**
- * @param IDatabase $db
- * @return bool|stdClass
- */
- static function doLoad( $db ) {
- return $db->selectRow( 'site_stats', [
- 'ss_row_id',
- 'ss_total_edits',
- 'ss_good_articles',
- 'ss_total_pages',
- 'ss_users',
- 'ss_active_users',
- 'ss_images',
- ], [], __METHOD__ );
- }
-
- /**
- * Return the total number of page views. Except we don't track those anymore.
- * Stop calling this function, it will be removed some time in the future. It's
- * kept here simply to prevent fatal errors.
- *
- * @deprecated since 1.25
- * @return int
- */
- static function views() {
- wfDeprecated( __METHOD__, '1.25' );
- return 0;
- }
-
- /**
* @return int
*/
- static function edits() {
+ public static function edits() {
self::load();
- return self::$row->ss_total_edits;
+
+ return (int)self::$row->ss_total_edits;
}
/**
* @return int
*/
- static function articles() {
+ public static function articles() {
self::load();
- return self::$row->ss_good_articles;
+
+ return (int)self::$row->ss_good_articles;
}
/**
* @return int
*/
- static function pages() {
+ public static function pages() {
self::load();
- return self::$row->ss_total_pages;
+
+ return (int)self::$row->ss_total_pages;
}
/**
* @return int
*/
- static function users() {
+ public static function users() {
self::load();
- return self::$row->ss_users;
+
+ return (int)self::$row->ss_users;
}
/**
* @return int
*/
- static function activeUsers() {
+ public static function activeUsers() {
self::load();
- return self::$row->ss_active_users;
+
+ return (int)self::$row->ss_active_users;
}
/**
* @return int
*/
- static function images() {
+ public static function images() {
self::load();
- return self::$row->ss_images;
+
+ return (int)self::$row->ss_images;
}
/**
@@ -185,17 +147,17 @@ class SiteStats {
* @param string $group Name of group
* @return int
*/
- static function numberingroup( $group ) {
+ public static function numberingroup( $group ) {
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+
return $cache->getWithSetCallback(
$cache->makeKey( 'SiteStats', 'groupcounts', $group ),
$cache::TTL_HOUR,
function ( $oldValue, &$ttl, array &$setOpts ) use ( $group ) {
- $dbr = wfGetDB( DB_REPLICA );
-
+ $dbr = self::getLB()->getConnection( DB_REPLICA );
$setOpts += Database::getCacheSetOptions( $dbr );
- return $dbr->selectField(
+ return (int)$dbr->selectField(
'user_groups',
'COUNT(*)',
[
@@ -213,8 +175,9 @@ class SiteStats {
* Total number of jobs in the job queue.
* @return int
*/
- static function jobs() {
+ public static function jobs() {
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+
return $cache->getWithSetCallback(
$cache->makeKey( 'SiteStats', 'jobscount' ),
$cache::TTL_MINUTE,
@@ -232,20 +195,54 @@ class SiteStats {
/**
* @param int $ns
- *
* @return int
*/
- static function pagesInNs( $ns ) {
- if ( !isset( self::$pageCount[$ns] ) ) {
- $dbr = wfGetDB( DB_REPLICA );
- self::$pageCount[$ns] = (int)$dbr->selectField(
- 'page',
- 'COUNT(*)',
- [ 'page_namespace' => $ns ],
- __METHOD__
- );
- }
- return self::$pageCount[$ns];
+ public static function pagesInNs( $ns ) {
+ $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+
+ return $cache->getWithSetCallback(
+ $cache->makeKey( 'SiteStats', 'page-in-namespace', $ns ),
+ $cache::TTL_HOUR,
+ function ( $oldValue, &$ttl, array &$setOpts ) use ( $ns ) {
+ $dbr = self::getLB()->getConnection( DB_REPLICA );
+ $setOpts += Database::getCacheSetOptions( $dbr );
+
+ return (int)$dbr->selectField(
+ 'page',
+ 'COUNT(*)',
+ [ 'page_namespace' => $ns ],
+ __METHOD__
+ );
+ },
+ [ 'pcTTL' => $cache::TTL_PROC_LONG ]
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public static function selectFields() {
+ return [
+ 'ss_total_edits',
+ 'ss_good_articles',
+ 'ss_total_pages',
+ 'ss_users',
+ 'ss_active_users',
+ 'ss_images',
+ ];
+ }
+
+ /**
+ * @param IDatabase $db
+ * @return stdClass|bool
+ */
+ private static function doLoadFromDB( IDatabase $db ) {
+ return $db->selectRow(
+ 'site_stats',
+ self::selectFields(),
+ [ 'ss_row_id' => 1 ],
+ __METHOD__
+ );
}
/**
@@ -254,10 +251,9 @@ class SiteStats {
* Checks only fields which are filled by SiteStatsInit::refresh.
*
* @param bool|object $row
- *
* @return bool
*/
- private static function isSane( $row ) {
+ private static function isRowSane( $row ) {
if ( $row === false
|| $row->ss_total_pages < $row->ss_good_articles
|| $row->ss_total_edits < $row->ss_total_pages
@@ -272,152 +268,34 @@ class SiteStats {
'ss_users',
'ss_images',
] as $member ) {
- if ( $row->$member > 2000000000 || $row->$member < 0 ) {
+ if ( $row->$member < 0 ) {
return false;
}
}
- return true;
- }
-}
-
-/**
- * Class designed for counting of stats.
- */
-class SiteStatsInit {
-
- // Database connection
- private $db;
-
- // Various stats
- private $mEdits = null, $mArticles = null, $mPages = null;
- private $mUsers = null, $mFiles = null;
-
- /**
- * @param bool|IDatabase $database
- * - bool: Whether to use the master DB
- * - IDatabase: Database connection to use
- */
- public function __construct( $database = false ) {
- if ( $database instanceof IDatabase ) {
- $this->db = $database;
- } elseif ( $database ) {
- $this->db = wfGetDB( DB_MASTER );
- } else {
- $this->db = wfGetDB( DB_REPLICA, 'vslow' );
- }
- }
- /**
- * Count the total number of edits
- * @return int
- */
- public function edits() {
- $this->mEdits = $this->db->selectField( 'revision', 'COUNT(*)', '', __METHOD__ );
- $this->mEdits += $this->db->selectField( 'archive', 'COUNT(*)', '', __METHOD__ );
- return $this->mEdits;
+ return true;
}
/**
- * Count pages in article space(s)
- * @return int
+ * @param stdClass|bool $row
+ * @return stdClass
*/
- public function articles() {
- global $wgArticleCountMethod;
-
- $tables = [ 'page' ];
- $conds = [
- 'page_namespace' => MWNamespace::getContentNamespaces(),
- 'page_is_redirect' => 0,
- ];
-
- if ( $wgArticleCountMethod == 'link' ) {
- $tables[] = 'pagelinks';
- $conds[] = 'pl_from=page_id';
- } elseif ( $wgArticleCountMethod == 'comma' ) {
- // To make a correct check for this, we would need, for each page,
- // to load the text, maybe uncompress it, maybe decode it and then
- // check if there's one comma.
- // But one thing we are sure is that if the page is empty, it can't
- // contain a comma :)
- $conds[] = 'page_len > 0';
+ private static function salvageInsaneRow( $row ) {
+ $map = $row ? (array)$row : [];
+ // Fill in any missing values with zero
+ $map += array_fill_keys( self::selectFields(), 0 );
+ // Convert negative values to zero
+ foreach ( $map as $field => $value ) {
+ $map[$field] = max( 0, $value );
}
- $this->mArticles = $this->db->selectField( $tables, 'COUNT(DISTINCT page_id)',
- $conds, __METHOD__ );
- return $this->mArticles;
- }
-
- /**
- * Count total pages
- * @return int
- */
- public function pages() {
- $this->mPages = $this->db->selectField( 'page', 'COUNT(*)', '', __METHOD__ );
- return $this->mPages;
- }
-
- /**
- * Count total users
- * @return int
- */
- public function users() {
- $this->mUsers = $this->db->selectField( 'user', 'COUNT(*)', '', __METHOD__ );
- return $this->mUsers;
- }
-
- /**
- * Count total files
- * @return int
- */
- public function files() {
- $this->mFiles = $this->db->selectField( 'image', 'COUNT(*)', '', __METHOD__ );
- return $this->mFiles;
- }
-
- /**
- * Do all updates and commit them. More or less a replacement
- * for the original initStats, but without output.
- *
- * @param IDatabase|bool $database
- * - bool: Whether to use the master DB
- * - IDatabase: Database connection to use
- * @param array $options Array of options, may contain the following values
- * - activeUsers bool: Whether to update the number of active users (default: false)
- */
- public static function doAllAndCommit( $database, array $options = [] ) {
- $options += [ 'update' => false, 'activeUsers' => false ];
-
- // Grab the object and count everything
- $counter = new SiteStatsInit( $database );
-
- $counter->edits();
- $counter->articles();
- $counter->pages();
- $counter->users();
- $counter->files();
-
- $counter->refresh();
-
- // Count active users if need be
- if ( $options['activeUsers'] ) {
- SiteStatsUpdate::cacheUpdate( wfGetDB( DB_MASTER ) );
- }
+ return (object)$row;
}
/**
- * Refresh site_stats
+ * @return LoadBalancer
*/
- public function refresh() {
- $values = [
- 'ss_row_id' => 1,
- 'ss_total_edits' => ( $this->mEdits === null ? $this->edits() : $this->mEdits ),
- 'ss_good_articles' => ( $this->mArticles === null ? $this->articles() : $this->mArticles ),
- 'ss_total_pages' => ( $this->mPages === null ? $this->pages() : $this->mPages ),
- 'ss_users' => ( $this->mUsers === null ? $this->users() : $this->mUsers ),
- 'ss_images' => ( $this->mFiles === null ? $this->files() : $this->mFiles ),
- ];
-
- $dbw = wfGetDB( DB_MASTER );
- $dbw->upsert( 'site_stats', $values, [ 'ss_row_id' ], $values, __METHOD__ );
+ private static function getLB() {
+ return MediaWikiServices::getInstance()->getDBLoadBalancer();
}
}
diff --git a/www/wiki/includes/SiteStatsInit.php b/www/wiki/includes/SiteStatsInit.php
new file mode 100644
index 00000000..8adb2181
--- /dev/null
+++ b/www/wiki/includes/SiteStatsInit.php
@@ -0,0 +1,202 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\MediaWikiServices;
+
+/**
+ * Class designed for counting of stats.
+ */
+class SiteStatsInit {
+ /* @var IDatabase */
+ private $dbr;
+ /** @var int */
+ private $edits;
+ /** @var int */
+ private $articles;
+ /** @var int */
+ private $pages;
+ /** @var int */
+ private $users;
+ /** @var int */
+ private $files;
+
+ /**
+ * @param bool|IDatabase $database
+ * - bool: Whether to use the master DB
+ * - IDatabase: Database connection to use
+ */
+ public function __construct( $database = false ) {
+ if ( $database instanceof IDatabase ) {
+ $this->dbr = $database;
+ } elseif ( $database ) {
+ $this->dbr = self::getDB( DB_MASTER );
+ } else {
+ $this->dbr = self::getDB( DB_REPLICA, 'vslow' );
+ }
+ }
+
+ /**
+ * Count the total number of edits
+ * @return int
+ */
+ public function edits() {
+ $this->edits = $this->dbr->selectField( 'revision', 'COUNT(*)', '', __METHOD__ );
+ $this->edits += $this->dbr->selectField( 'archive', 'COUNT(*)', '', __METHOD__ );
+
+ return $this->edits;
+ }
+
+ /**
+ * Count pages in article space(s)
+ * @return int
+ */
+ public function articles() {
+ $config = MediaWikiServices::getInstance()->getMainConfig();
+
+ $tables = [ 'page' ];
+ $conds = [
+ 'page_namespace' => MWNamespace::getContentNamespaces(),
+ 'page_is_redirect' => 0,
+ ];
+
+ if ( $config->get( 'ArticleCountMethod' ) == 'link' ) {
+ $tables[] = 'pagelinks';
+ $conds[] = 'pl_from=page_id';
+ }
+
+ $this->articles = $this->dbr->selectField(
+ $tables,
+ 'COUNT(DISTINCT page_id)',
+ $conds,
+ __METHOD__
+ );
+
+ return $this->articles;
+ }
+
+ /**
+ * Count total pages
+ * @return int
+ */
+ public function pages() {
+ $this->pages = $this->dbr->selectField( 'page', 'COUNT(*)', '', __METHOD__ );
+
+ return $this->pages;
+ }
+
+ /**
+ * Count total users
+ * @return int
+ */
+ public function users() {
+ $this->users = $this->dbr->selectField( 'user', 'COUNT(*)', '', __METHOD__ );
+
+ return $this->users;
+ }
+
+ /**
+ * Count total files
+ * @return int
+ */
+ public function files() {
+ $this->files = $this->dbr->selectField( 'image', 'COUNT(*)', '', __METHOD__ );
+
+ return $this->files;
+ }
+
+ /**
+ * Do all updates and commit them. More or less a replacement
+ * for the original initStats, but without output.
+ *
+ * @param IDatabase|bool $database
+ * - bool: Whether to use the master DB
+ * - IDatabase: Database connection to use
+ * @param array $options Array of options, may contain the following values
+ * - activeUsers bool: Whether to update the number of active users (default: false)
+ */
+ public static function doAllAndCommit( $database, array $options = [] ) {
+ $options += [ 'update' => false, 'activeUsers' => false ];
+
+ // Grab the object and count everything
+ $counter = new self( $database );
+
+ $counter->edits();
+ $counter->articles();
+ $counter->pages();
+ $counter->users();
+ $counter->files();
+
+ $counter->refresh();
+
+ // Count active users if need be
+ if ( $options['activeUsers'] ) {
+ SiteStatsUpdate::cacheUpdate( self::getDB( DB_MASTER ) );
+ }
+ }
+
+ /**
+ * Insert a dummy row with all zeroes if no row is present
+ */
+ public static function doPlaceholderInit() {
+ $dbw = self::getDB( DB_MASTER );
+ $exists = $dbw->selectField( 'site_stats', '1', [ 'ss_row_id' => 1 ], __METHOD__ );
+ if ( $exists === false ) {
+ $dbw->insert(
+ 'site_stats',
+ [ 'ss_row_id' => 1 ] + array_fill_keys( SiteStats::selectFields(), 0 ),
+ __METHOD__,
+ [ 'IGNORE' ]
+ );
+ }
+ }
+
+ /**
+ * Refresh site_stats
+ */
+ public function refresh() {
+ $values = [
+ 'ss_row_id' => 1,
+ 'ss_total_edits' => $this->edits === null ? $this->edits() : $this->edits,
+ 'ss_good_articles' => $this->articles === null ? $this->articles() : $this->articles,
+ 'ss_total_pages' => $this->pages === null ? $this->pages() : $this->pages,
+ 'ss_users' => $this->users === null ? $this->users() : $this->users,
+ 'ss_images' => $this->files === null ? $this->files() : $this->files,
+ ];
+
+ self::getDB( DB_MASTER )->upsert(
+ 'site_stats',
+ $values,
+ [ 'ss_row_id' ],
+ $values,
+ __METHOD__
+ );
+ }
+
+ /**
+ * @param int $index
+ * @param string[] $groups
+ * @return IDatabase
+ */
+ private static function getDB( $index, $groups = [] ) {
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+
+ return $lb->getConnection( $index, $groups );
+ }
+}
diff --git a/www/wiki/includes/Status.php b/www/wiki/includes/Status.php
index a35af6e8..f17f173e 100644
--- a/www/wiki/includes/Status.php
+++ b/www/wiki/includes/Status.php
@@ -316,7 +316,9 @@ class Status extends StatusValue {
$lang = $this->languageFromParam( $lang );
$text = $this->getWikiText( $shortContext, $longContext, $lang );
$out = MessageCache::singleton()->parse( $text, null, true, true, $lang );
- return $out instanceof ParserOutput ? $out->getText() : $out;
+ return $out instanceof ParserOutput
+ ? $out->getText( [ 'enableSectionEditLinks' => false ] )
+ : $out;
}
/**
diff --git a/www/wiki/includes/Storage/BlobAccessException.php b/www/wiki/includes/Storage/BlobAccessException.php
new file mode 100644
index 00000000..ffc5ecab
--- /dev/null
+++ b/www/wiki/includes/Storage/BlobAccessException.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use RuntimeException;
+
+/**
+ * Exception representing a failure to access a data blob.
+ *
+ * @since 1.31
+ */
+class BlobAccessException extends RuntimeException {
+
+}
diff --git a/www/wiki/includes/Storage/BlobStore.php b/www/wiki/includes/Storage/BlobStore.php
new file mode 100644
index 00000000..8b1112b2
--- /dev/null
+++ b/www/wiki/includes/Storage/BlobStore.php
@@ -0,0 +1,119 @@
+<?php
+/**
+ * Service for loading and storing data blobs.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+/**
+ * Service for loading and storing data blobs.
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ * static methods in Revision.
+ *
+ * @since 1.31
+ */
+interface BlobStore {
+
+ /**
+ * Hint key for use with storeBlob, indicating the general role the block
+ * takes in the application. For instance, it should be "page-content" if
+ * the blob represents a Content object.
+ */
+ const DESIGNATION_HINT = 'designation';
+
+ /**
+ * Hint key for use with storeBlob, indicating the page the blob is associated with.
+ * This may be used for sharding.
+ */
+ const PAGE_HINT = 'page_id';
+
+ /**
+ * Hint key for use with storeBlob, indicating the slot the blob is associated with.
+ * May be relevant for reference counting.
+ */
+ const ROLE_HINT = 'role_name';
+
+ /**
+ * Hint key for use with storeBlob, indicating the revision the blob is associated with.
+ * This may be used for differential storage and reference counting.
+ */
+ const REVISION_HINT = 'rev_id';
+
+ /**
+ * Hint key for use with storeBlob, indicating the parent revision of the revision
+ * the blob is associated with. This may be used for differential storage.
+ */
+ const PARENT_HINT = 'rev_parent_id';
+
+ /**
+ * Hint key for use with storeBlob, providing the SHA1 hash of the blob as passed to the
+ * method. This can be used to avoid re-calculating the hash if it is needed by the BlobStore.
+ */
+ const SHA1_HINT = 'cont_sha1';
+
+ /**
+ * Hint key for use with storeBlob, indicating the model of the content encoded in the
+ * given blob. May be used to implement optimized storage for some well known models.
+ */
+ const MODEL_HINT = 'cont_model';
+
+ /**
+ * Hint key for use with storeBlob, indicating the serialization format used to create
+ * the blob, as a MIME type. May be used for optimized storage in the underlying database.
+ */
+ const FORMAT_HINT = 'cont_format';
+
+ /**
+ * Retrieve a blob, given an address.
+ *
+ * MCR migration note: this replaces Revision::loadText
+ *
+ * @param string $blobAddress The blob address as returned by storeBlob(),
+ * such as "tt:12345" or "ex:DB://s16/456/9876".
+ * @param int $queryFlags See IDBAccessObject.
+ *
+ * @throws BlobAccessException
+ * @return string binary blob data
+ */
+ public function getBlob( $blobAddress, $queryFlags = 0 );
+
+ /**
+ * Stores an arbitrary blob of data and returns an address that can be used with
+ * getBlob() to retrieve the same blob of data,
+ *
+ * @param string $data raw binary data
+ * @param array $hints An array of hints. Implementations may use the hints to optimize storage.
+ * All hints are optional, supported hints depend on the implementation. Hint names by
+ * convention correspond to the names of fields in the database. Callers are encouraged to
+ * provide the well known hints as defined by the XXX_HINT constants.
+ *
+ * @throws BlobAccessException
+ * @return string an address that can be used with getBlob() to retrieve the data.
+ */
+ public function storeBlob( $data, $hints = [] );
+
+ /**
+ * Check if the blob metadata or backing blob data store is read-only
+ *
+ * @return bool
+ */
+ public function isReadOnly();
+}
diff --git a/www/wiki/includes/Storage/BlobStoreFactory.php b/www/wiki/includes/Storage/BlobStoreFactory.php
new file mode 100644
index 00000000..63ca74de
--- /dev/null
+++ b/www/wiki/includes/Storage/BlobStoreFactory.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use Config;
+use Language;
+use WANObjectCache;
+use Wikimedia\Rdbms\LoadBalancer;
+
+/**
+ * Service for instantiating BlobStores
+ *
+ * This can be used to create BlobStore objects for other wikis.
+ *
+ * @since 1.31
+ */
+class BlobStoreFactory {
+
+ /**
+ * @var LoadBalancer
+ */
+ private $loadBalancer;
+
+ /**
+ * @var WANObjectCache
+ */
+ private $cache;
+
+ /**
+ * @var Config
+ */
+ private $config;
+
+ /**
+ * @var Language
+ */
+ private $contLang;
+
+ public function __construct(
+ LoadBalancer $loadBalancer,
+ WANObjectCache $cache,
+ Config $mainConfig,
+ Language $contLang
+ ) {
+ $this->loadBalancer = $loadBalancer;
+ $this->cache = $cache;
+ $this->config = $mainConfig;
+ $this->contLang = $contLang;
+ }
+
+ /**
+ * @since 1.31
+ *
+ * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+ *
+ * @return BlobStore
+ */
+ public function newBlobStore( $wikiId = false ) {
+ return $this->newSqlBlobStore( $wikiId );
+ }
+
+ /**
+ * @internal Please call newBlobStore and use the BlobStore interface.
+ *
+ * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+ *
+ * @return SqlBlobStore
+ */
+ public function newSqlBlobStore( $wikiId = false ) {
+ $store = new SqlBlobStore(
+ $this->loadBalancer,
+ $this->cache,
+ $wikiId
+ );
+
+ $store->setCompressBlobs( $this->config->get( 'CompressRevisions' ) );
+ $store->setCacheExpiry( $this->config->get( 'RevisionCacheExpiry' ) );
+ $store->setUseExternalStore( $this->config->get( 'DefaultExternalStore' ) !== false );
+
+ if ( $this->config->get( 'LegacyEncoding' ) ) {
+ $store->setLegacyEncoding( $this->config->get( 'LegacyEncoding' ), $this->contLang );
+ }
+
+ return $store;
+ }
+
+}
diff --git a/www/wiki/includes/Storage/IncompleteRevisionException.php b/www/wiki/includes/Storage/IncompleteRevisionException.php
new file mode 100644
index 00000000..bf45b012
--- /dev/null
+++ b/www/wiki/includes/Storage/IncompleteRevisionException.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+/**
+ * Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
+ *
+ * @since 1.31
+ */
+class IncompleteRevisionException extends RevisionAccessException {
+
+}
diff --git a/www/wiki/includes/Storage/MutableRevisionRecord.php b/www/wiki/includes/Storage/MutableRevisionRecord.php
new file mode 100644
index 00000000..a259ae0b
--- /dev/null
+++ b/www/wiki/includes/Storage/MutableRevisionRecord.php
@@ -0,0 +1,328 @@
+<?php
+/**
+ * Mutable RevisionRecord implementation, for building new revision entries programmatically.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use CommentStoreComment;
+use Content;
+use InvalidArgumentException;
+use MediaWiki\User\UserIdentity;
+use MWException;
+use Title;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Mutable RevisionRecord implementation, for building new revision entries programmatically.
+ * Provides setters for all fields.
+ *
+ * @since 1.31
+ */
+class MutableRevisionRecord extends RevisionRecord {
+
+ /**
+ * Returns an incomplete MutableRevisionRecord which uses $parent as its
+ * parent revision, and inherits all slots form it. If saved unchanged,
+ * the new revision will act as a null-revision.
+ *
+ * @param RevisionRecord $parent
+ * @param CommentStoreComment $comment
+ * @param UserIdentity $user
+ * @param string $timestamp
+ *
+ * @return MutableRevisionRecord
+ */
+ public static function newFromParentRevision(
+ RevisionRecord $parent,
+ CommentStoreComment $comment,
+ UserIdentity $user,
+ $timestamp
+ ) {
+ // TODO: ideally, we wouldn't need a Title here
+ $title = Title::newFromLinkTarget( $parent->getPageAsLinkTarget() );
+ $rev = new MutableRevisionRecord( $title, $parent->getWikiId() );
+
+ $rev->setComment( $comment );
+ $rev->setUser( $user );
+ $rev->setTimestamp( $timestamp );
+
+ foreach ( $parent->getSlotRoles() as $role ) {
+ $slot = $parent->getSlot( $role, self::RAW );
+ $rev->inheritSlot( $slot );
+ }
+
+ $rev->setPageId( $parent->getPageId() );
+ $rev->setParentId( $parent->getId() );
+
+ return $rev;
+ }
+
+ /**
+ * @note Avoid calling this constructor directly. Use the appropriate methods
+ * in RevisionStore instead.
+ *
+ * @param Title $title The title of the page this Revision is associated with.
+ * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+ * or false for the local site.
+ *
+ * @throws MWException
+ */
+ function __construct( Title $title, $wikiId = false ) {
+ $slots = new MutableRevisionSlots();
+
+ parent::__construct( $title, $slots, $wikiId );
+
+ $this->mSlots = $slots; // redundant, but nice for static analysis
+ }
+
+ /**
+ * @param int $parentId
+ */
+ public function setParentId( $parentId ) {
+ Assert::parameterType( 'integer', $parentId, '$parentId' );
+
+ $this->mParentId = $parentId;
+ }
+
+ /**
+ * Sets the given slot. If a slot with the same role is already present in the revision,
+ * it is replaced.
+ *
+ * @note This can only be used with a fresh "unattached" SlotRecord. Calling code that has a
+ * SlotRecord from another revision should use inheritSlot(). Calling code that has access to
+ * a Content object can use setContent().
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @note Calling this method will cause the revision size and hash to be re-calculated upon
+ * the next call to getSize() and getSha1(), respectively.
+ *
+ * @param SlotRecord $slot
+ */
+ public function setSlot( SlotRecord $slot ) {
+ if ( $slot->hasRevision() && $slot->getRevision() !== $this->getId() ) {
+ throw new InvalidArgumentException(
+ 'The given slot must be an unsaved, unattached one. '
+ . 'This slot is already attached to revision ' . $slot->getRevision() . '. '
+ . 'Use inheritSlot() instead to preserve a slot from a previous revision.'
+ );
+ }
+
+ $this->mSlots->setSlot( $slot );
+ $this->resetAggregateValues();
+ }
+
+ /**
+ * "Inherits" the given slot's content.
+ *
+ * If a slot with the same role is already present in the revision, it is replaced.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @param SlotRecord $parentSlot
+ */
+ public function inheritSlot( SlotRecord $parentSlot ) {
+ $slot = SlotRecord::newInherited( $parentSlot );
+ $this->setSlot( $slot );
+ }
+
+ /**
+ * Sets the content for the slot with the given role.
+ *
+ * If a slot with the same role is already present in the revision, it is replaced.
+ * Calling code that has access to a SlotRecord can use inheritSlot() instead.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @note Calling this method will cause the revision size and hash to be re-calculated upon
+ * the next call to getSize() and getSha1(), respectively.
+ *
+ * @param string $role
+ * @param Content $content
+ */
+ public function setContent( $role, Content $content ) {
+ $this->mSlots->setContent( $role, $content );
+ $this->resetAggregateValues();
+ }
+
+ /**
+ * Removes the slot with the given role from this revision.
+ * This effectively ends the "stream" with that role on the revision's page.
+ * Future revisions will no longer inherit this slot, unless it is added back explicitly.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @note Calling this method will cause the revision size and hash to be re-calculated upon
+ * the next call to getSize() and getSha1(), respectively.
+ *
+ * @param string $role
+ */
+ public function removeSlot( $role ) {
+ $this->mSlots->removeSlot( $role );
+ $this->resetAggregateValues();
+ }
+
+ /**
+ * @param CommentStoreComment $comment
+ */
+ public function setComment( CommentStoreComment $comment ) {
+ $this->mComment = $comment;
+ }
+
+ /**
+ * Set revision hash, for optimization. Prevents getSha1() from re-calculating the hash.
+ *
+ * @note This should only be used if the calling code is sure that the given hash is correct
+ * for the revision's content, and there is no chance of the content being manipulated
+ * later. When in doubt, this method should not be called.
+ *
+ * @param string $sha1 SHA1 hash as a base36 string.
+ */
+ public function setSha1( $sha1 ) {
+ Assert::parameterType( 'string', $sha1, '$sha1' );
+
+ $this->mSha1 = $sha1;
+ }
+
+ /**
+ * Set nominal revision size, for optimization. Prevents getSize() from re-calculating the size.
+ *
+ * @note This should only be used if the calling code is sure that the given size is correct
+ * for the revision's content, and there is no chance of the content being manipulated
+ * later. When in doubt, this method should not be called.
+ *
+ * @param int $size nominal size in bogo-bytes
+ */
+ public function setSize( $size ) {
+ Assert::parameterType( 'integer', $size, '$size' );
+
+ $this->mSize = $size;
+ }
+
+ /**
+ * @param int $visibility
+ */
+ public function setVisibility( $visibility ) {
+ Assert::parameterType( 'integer', $visibility, '$visibility' );
+
+ $this->mDeleted = $visibility;
+ }
+
+ /**
+ * @param string $timestamp A timestamp understood by wfTimestamp
+ */
+ public function setTimestamp( $timestamp ) {
+ Assert::parameterType( 'string', $timestamp, '$timestamp' );
+
+ $this->mTimestamp = wfTimestamp( TS_MW, $timestamp );
+ }
+
+ /**
+ * @param bool $minorEdit
+ */
+ public function setMinorEdit( $minorEdit ) {
+ Assert::parameterType( 'boolean', $minorEdit, '$minorEdit' );
+
+ $this->mMinorEdit = $minorEdit;
+ }
+
+ /**
+ * Set the revision ID.
+ *
+ * MCR migration note: this replaces Revision::setId()
+ *
+ * @warning Use this with care, especially when preparing a revision for insertion
+ * into the database! The revision ID should only be fixed in special cases
+ * like preserving the original ID when restoring a revision.
+ *
+ * @param int $id
+ */
+ public function setId( $id ) {
+ Assert::parameterType( 'integer', $id, '$id' );
+
+ $this->mId = $id;
+ }
+
+ /**
+ * Sets the user identity associated with the revision
+ *
+ * @param UserIdentity $user
+ */
+ public function setUser( UserIdentity $user ) {
+ $this->mUser = $user;
+ }
+
+ /**
+ * @param int $pageId
+ */
+ public function setPageId( $pageId ) {
+ Assert::parameterType( 'integer', $pageId, '$pageId' );
+
+ if ( $this->mTitle->exists() && $pageId !== $this->mTitle->getArticleID() ) {
+ throw new InvalidArgumentException(
+ 'The given Title does not belong to page ID ' . $this->mPageId
+ );
+ }
+
+ $this->mPageId = $pageId;
+ }
+
+ /**
+ * Returns the nominal size of this revision.
+ *
+ * MCR migration note: this replaces Revision::getSize
+ *
+ * @return int The nominal size, may be computed on the fly if not yet known.
+ */
+ public function getSize() {
+ // If not known, re-calculate and remember. Will be reset when slots change.
+ if ( $this->mSize === null ) {
+ $this->mSize = $this->mSlots->computeSize();
+ }
+
+ return $this->mSize;
+ }
+
+ /**
+ * Returns the base36 sha1 of this revision.
+ *
+ * MCR migration note: this replaces Revision::getSha1
+ *
+ * @return string The revision hash, may be computed on the fly if not yet known.
+ */
+ public function getSha1() {
+ // If not known, re-calculate and remember. Will be reset when slots change.
+ if ( $this->mSha1 === null ) {
+ $this->mSha1 = $this->mSlots->computeSha1();
+ }
+
+ return $this->mSha1;
+ }
+
+ /**
+ * Invalidate cached aggregate values such as hash and size.
+ */
+ private function resetAggregateValues() {
+ $this->mSize = null;
+ $this->mSha1 = null;
+ }
+
+}
diff --git a/www/wiki/includes/Storage/MutableRevisionSlots.php b/www/wiki/includes/Storage/MutableRevisionSlots.php
new file mode 100644
index 00000000..2e675c89
--- /dev/null
+++ b/www/wiki/includes/Storage/MutableRevisionSlots.php
@@ -0,0 +1,137 @@
+<?php
+/**
+ * Mutable version of RevisionSlots, for constructing a new revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use Content;
+
+/**
+ * Mutable version of RevisionSlots, for constructing a new revision.
+ *
+ * @since 1.31
+ */
+class MutableRevisionSlots extends RevisionSlots {
+
+ /**
+ * Constructs a MutableRevisionSlots that inherits from the given
+ * list of slots.
+ *
+ * @param SlotRecord[] $slots
+ *
+ * @return MutableRevisionSlots
+ */
+ public static function newFromParentRevisionSlots( array $slots ) {
+ $inherited = [];
+ foreach ( $slots as $slot ) {
+ $role = $slot->getRole();
+ $inherited[$role] = SlotRecord::newInherited( $slot );
+ }
+
+ return new MutableRevisionSlots( $inherited );
+ }
+
+ /**
+ * @param SlotRecord[] $slots An array of SlotRecords.
+ */
+ public function __construct( array $slots = [] ) {
+ parent::__construct( $slots );
+ }
+
+ /**
+ * Sets the given slot.
+ * If a slot with the same role is already present, it is replaced.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @param SlotRecord $slot
+ */
+ public function setSlot( SlotRecord $slot ) {
+ if ( !is_array( $this->slots ) ) {
+ $this->getSlots(); // initialize $this->slots
+ }
+
+ $role = $slot->getRole();
+ $this->slots[$role] = $slot;
+ }
+
+ /**
+ * Sets the content for the slot with the given role.
+ * If a slot with the same role is already present, it is replaced.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @param string $role
+ * @param Content $content
+ */
+ public function setContent( $role, Content $content ) {
+ $slot = SlotRecord::newUnsaved( $role, $content );
+ $this->setSlot( $slot );
+ }
+
+ /**
+ * Remove the slot for the given role, discontinue the corresponding stream.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @param string $role
+ */
+ public function removeSlot( $role ) {
+ if ( !is_array( $this->slots ) ) {
+ $this->getSlots(); // initialize $this->slots
+ }
+
+ unset( $this->slots[$role] );
+ }
+
+ /**
+ * Return all slots that are not inherited.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @return SlotRecord[]
+ */
+ public function getTouchedSlots() {
+ return array_filter(
+ $this->getSlots(),
+ function ( SlotRecord $slot ) {
+ return !$slot->isInherited();
+ }
+ );
+ }
+
+ /**
+ * Return all slots that are inherited.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @return SlotRecord[]
+ */
+ public function getInheritedSlots() {
+ return array_filter(
+ $this->getSlots(),
+ function ( SlotRecord $slot ) {
+ return $slot->isInherited();
+ }
+ );
+ }
+
+}
diff --git a/www/wiki/includes/cache/ObjectFileCache.php b/www/wiki/includes/Storage/NameTableAccessException.php
index c7ef0443..393cb1fa 100644
--- a/www/wiki/includes/cache/ObjectFileCache.php
+++ b/www/wiki/includes/Storage/NameTableAccessException.php
@@ -1,6 +1,6 @@
<?php
/**
- * Object cache in the file system.
+ * Exception representing a failure to look up a row from a name table.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,35 +18,28 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup Cache
*/
+namespace MediaWiki\Storage;
+
+use RuntimeException;
+
/**
- * Object cache in the file system.
+ * Exception representing a failure to look up a row from a name table.
*
- * @ingroup Cache
+ * @since 1.31
*/
-class ObjectFileCache extends FileCacheBase {
- /**
- * Construct an ObjectFileCache from a key and a type
- * @param string $key
- * @param string $type
- * @return ObjectFileCache
- */
- public static function newFromKey( $key, $type ) {
- $cache = new self();
-
- $cache->mKey = (string)$key;
- $cache->mType = (string)$type;
-
- return $cache;
- }
+class NameTableAccessException extends RuntimeException {
/**
- * Get the base file cache directory
- * @return string
+ * @param string $tableName
+ * @param string $accessType
+ * @param string|int $accessValue
+ * @return NameTableAccessException
*/
- protected function cacheDirectory() {
- return $this->baseCacheDirectory() . '/object';
+ public static function newFromDetails( $tableName, $accessType, $accessValue ) {
+ $message = "Failed to access name from ${tableName} using ${accessType} = ${accessValue}";
+ return new self( $message );
}
+
}
diff --git a/www/wiki/includes/Storage/NameTableStore.php b/www/wiki/includes/Storage/NameTableStore.php
new file mode 100644
index 00000000..ebce3da9
--- /dev/null
+++ b/www/wiki/includes/Storage/NameTableStore.php
@@ -0,0 +1,366 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use IExpiringStore;
+use Psr\Log\LoggerInterface;
+use WANObjectCache;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\LoadBalancer;
+
+/**
+ * @author Addshore
+ * @since 1.31
+ */
+class NameTableStore {
+
+ /** @var LoadBalancer */
+ private $loadBalancer;
+
+ /** @var WANObjectCache */
+ private $cache;
+
+ /** @var LoggerInterface */
+ private $logger;
+
+ /** @var string[] */
+ private $tableCache = null;
+
+ /** @var bool|string */
+ private $wikiId = false;
+
+ /** @var int */
+ private $cacheTTL;
+
+ /** @var string */
+ private $table;
+ /** @var string */
+ private $idField;
+ /** @var string */
+ private $nameField;
+ /** @var null|callable */
+ private $normalizationCallback = null;
+
+ /**
+ * @param LoadBalancer $dbLoadBalancer A load balancer for acquiring database connections
+ * @param WANObjectCache $cache A cache manager for caching data
+ * @param LoggerInterface $logger
+ * @param string $table
+ * @param string $idField
+ * @param string $nameField
+ * @param callable $normalizationCallback Normalization to be applied to names before being
+ * saved or queried. This should be a callback that accepts and returns a single string.
+ * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+ */
+ public function __construct(
+ LoadBalancer $dbLoadBalancer,
+ WANObjectCache $cache,
+ LoggerInterface $logger,
+ $table,
+ $idField,
+ $nameField,
+ callable $normalizationCallback = null,
+ $wikiId = false
+ ) {
+ $this->loadBalancer = $dbLoadBalancer;
+ $this->cache = $cache;
+ $this->logger = $logger;
+ $this->table = $table;
+ $this->idField = $idField;
+ $this->nameField = $nameField;
+ $this->normalizationCallback = $normalizationCallback;
+ $this->wikiId = $wikiId;
+ $this->cacheTTL = IExpiringStore::TTL_MONTH;
+ }
+
+ /**
+ * @param int $index A database index, like DB_MASTER or DB_REPLICA
+ * @param int $flags Database connection flags
+ *
+ * @return IDatabase
+ */
+ private function getDBConnection( $index, $flags = 0 ) {
+ return $this->loadBalancer->getConnection( $index, [], $this->wikiId, $flags );
+ }
+
+ private function getCacheKey() {
+ return $this->cache->makeKey( 'NameTableSqlStore', $this->table, $this->wikiId );
+ }
+
+ /**
+ * @param string $name
+ * @return string
+ */
+ private function normalizeName( $name ) {
+ if ( $this->normalizationCallback === null ) {
+ return $name;
+ }
+ return call_user_func( $this->normalizationCallback, $name );
+ }
+
+ /**
+ * Acquire the id of the given name.
+ * This creates a row in the table if it doesn't already exist.
+ *
+ * @param string $name
+ * @throws NameTableAccessException
+ * @return int
+ */
+ public function acquireId( $name ) {
+ Assert::parameterType( 'string', $name, '$name' );
+ $name = $this->normalizeName( $name );
+
+ $table = $this->getTableFromCachesOrReplica();
+ $searchResult = array_search( $name, $table, true );
+ if ( $searchResult === false ) {
+ $id = $this->store( $name );
+ if ( $id === null ) {
+ // RACE: $name was already in the db, probably just inserted, so load from master
+ // Use DBO_TRX to avoid missing inserts due to other threads or REPEATABLE-READs
+ $table = $this->loadTable(
+ $this->getDBConnection( DB_MASTER, LoadBalancer::CONN_TRX_AUTOCOMMIT )
+ );
+ $searchResult = array_search( $name, $table, true );
+ if ( $searchResult === false ) {
+ // Insert failed due to IGNORE flag, but DB_MASTER didn't give us the data
+ $m = "No insert possible but master didn't give us a record for " .
+ "'{$name}' in '{$this->table}'";
+ $this->logger->error( $m );
+ throw new NameTableAccessException( $m );
+ }
+ $this->purgeWANCache(
+ function () {
+ $this->cache->reap( $this->getCacheKey(), INF );
+ }
+ );
+ } else {
+ $table[$id] = $name;
+ $searchResult = $id;
+ // As store returned an ID we know we inserted so delete from WAN cache
+ $this->purgeWANCache(
+ function () {
+ $this->cache->delete( $this->getCacheKey() );
+ }
+ );
+ }
+ $this->tableCache = $table;
+ }
+
+ return $searchResult;
+ }
+
+ /**
+ * Get the id of the given name.
+ * If the name doesn't exist this will throw.
+ * This should be used in cases where we believe the name already exists or want to check for
+ * existence.
+ *
+ * @param string $name
+ * @throws NameTableAccessException The name does not exist
+ * @return int Id
+ */
+ public function getId( $name ) {
+ Assert::parameterType( 'string', $name, '$name' );
+ $name = $this->normalizeName( $name );
+
+ $table = $this->getTableFromCachesOrReplica();
+ $searchResult = array_search( $name, $table, true );
+
+ if ( $searchResult !== false ) {
+ return $searchResult;
+ }
+
+ throw NameTableAccessException::newFromDetails( $this->table, 'name', $name );
+ }
+
+ /**
+ * Get the name of the given id.
+ * If the id doesn't exist this will throw.
+ * This should be used in cases where we believe the id already exists.
+ *
+ * Note: Calls to this method will result in a master select for non existing IDs.
+ *
+ * @param int $id
+ * @throws NameTableAccessException The id does not exist
+ * @return string name
+ */
+ public function getName( $id ) {
+ Assert::parameterType( 'integer', $id, '$id' );
+
+ $table = $this->getTableFromCachesOrReplica();
+ if ( array_key_exists( $id, $table ) ) {
+ return $table[$id];
+ }
+
+ $table = $this->cache->getWithSetCallback(
+ $this->getCacheKey(),
+ $this->cacheTTL,
+ function ( $oldValue, &$ttl, &$setOpts ) use ( $id ) {
+ // Check if cached value is up-to-date enough to have $id
+ if ( is_array( $oldValue ) && array_key_exists( $id, $oldValue ) ) {
+ // Completely leave the cache key alone
+ $ttl = WANObjectCache::TTL_UNCACHEABLE;
+ // Use the old value
+ return $oldValue;
+ }
+ // Regenerate from replica DB, and master DB if needed
+ foreach ( [ DB_REPLICA, DB_MASTER ] as $source ) {
+ // Log a fallback to master
+ if ( $source === DB_MASTER ) {
+ $this->logger->info(
+ __METHOD__ . 'falling back to master select from ' .
+ $this->table . ' with id ' . $id
+ );
+ }
+ $db = $this->getDBConnection( $source );
+ $cacheSetOpts = Database::getCacheSetOptions( $db );
+ $table = $this->loadTable( $db );
+ if ( array_key_exists( $id, $table ) ) {
+ break; // found it
+ }
+ }
+ // Use the value from last source checked
+ $setOpts += $cacheSetOpts;
+
+ return $table;
+ },
+ [ 'minAsOf' => INF ] // force callback run
+ );
+
+ $this->tableCache = $table;
+
+ if ( array_key_exists( $id, $table ) ) {
+ return $table[$id];
+ }
+
+ throw NameTableAccessException::newFromDetails( $this->table, 'id', $id );
+ }
+
+ /**
+ * Get the whole table, in no particular order as a map of ids to names.
+ * This method could be subject to DB or cache lag.
+ *
+ * @return string[] keys are the name ids, values are the names themselves
+ * Example: [ 1 => 'foo', 3 => 'bar' ]
+ */
+ public function getMap() {
+ return $this->getTableFromCachesOrReplica();
+ }
+
+ /**
+ * @return string[]
+ */
+ private function getTableFromCachesOrReplica() {
+ if ( $this->tableCache !== null ) {
+ return $this->tableCache;
+ }
+
+ $table = $this->cache->getWithSetCallback(
+ $this->getCacheKey(),
+ $this->cacheTTL,
+ function ( $oldValue, &$ttl, &$setOpts ) {
+ $dbr = $this->getDBConnection( DB_REPLICA );
+ $setOpts += Database::getCacheSetOptions( $dbr );
+ return $this->loadTable( $dbr );
+ }
+ );
+
+ $this->tableCache = $table;
+
+ return $table;
+ }
+
+ /**
+ * Reap the WANCache entry for this table.
+ *
+ * @param callable $purgeCallback callback to 'purge' the WAN cache
+ */
+ private function purgeWANCache( $purgeCallback ) {
+ // If the LB has no DB changes don't both with onTransactionPreCommitOrIdle
+ if ( !$this->loadBalancer->hasOrMadeRecentMasterChanges() ) {
+ $purgeCallback();
+ return;
+ }
+
+ $this->getDBConnection( DB_MASTER )
+ ->onTransactionPreCommitOrIdle( $purgeCallback, __METHOD__ );
+ }
+
+ /**
+ * Gets the table from the db
+ *
+ * @param IDatabase $db
+ *
+ * @return string[]
+ */
+ private function loadTable( IDatabase $db ) {
+ $result = $db->select(
+ $this->table,
+ [
+ 'id' => $this->idField,
+ 'name' => $this->nameField
+ ],
+ [],
+ __METHOD__,
+ [ 'ORDER BY' => 'id' ]
+ );
+
+ $assocArray = [];
+ foreach ( $result as $row ) {
+ $assocArray[$row->id] = $row->name;
+ }
+
+ return $assocArray;
+ }
+
+ /**
+ * Stores the given name in the DB, returning the ID when an insert occurs.
+ *
+ * @param string $name
+ * @return int|null int if we know the ID, null if we don't
+ */
+ private function store( $name ) {
+ Assert::parameterType( 'string', $name, '$name' );
+ Assert::parameter( $name !== '', '$name', 'should not be an empty string' );
+ // Note: this is only called internally so normalization of $name has already occurred.
+
+ $dbw = $this->getDBConnection( DB_MASTER );
+
+ $dbw->insert(
+ $this->table,
+ [ $this->nameField => $name ],
+ __METHOD__,
+ [ 'IGNORE' ]
+ );
+
+ if ( $dbw->affectedRows() === 0 ) {
+ $this->logger->info(
+ 'Tried to insert name into table ' . $this->table . ', but value already existed.'
+ );
+ return null;
+ }
+
+ return $dbw->insertId();
+ }
+
+}
diff --git a/www/wiki/includes/Storage/RevisionAccessException.php b/www/wiki/includes/Storage/RevisionAccessException.php
new file mode 100644
index 00000000..ee6efc0a
--- /dev/null
+++ b/www/wiki/includes/Storage/RevisionAccessException.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use RuntimeException;
+
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * @since 1.31
+ */
+class RevisionAccessException extends RuntimeException {
+
+}
diff --git a/www/wiki/includes/Storage/RevisionArchiveRecord.php b/www/wiki/includes/Storage/RevisionArchiveRecord.php
new file mode 100644
index 00000000..213ee3cd
--- /dev/null
+++ b/www/wiki/includes/Storage/RevisionArchiveRecord.php
@@ -0,0 +1,170 @@
+<?php
+/**
+ * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use CommentStoreComment;
+use MediaWiki\User\UserIdentity;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+
+/**
+ * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
+ * Most getters on RevisionArchiveRecord will never return null. However, getId() and
+ * getParentId() may indeed return null if this information was not stored when the archive entry
+ * was created.
+ *
+ * @since 1.31
+ */
+class RevisionArchiveRecord extends RevisionRecord {
+
+ /**
+ * @var int
+ */
+ protected $mArchiveId;
+
+ /**
+ * @note Avoid calling this constructor directly. Use the appropriate methods
+ * in RevisionStore instead.
+ *
+ * @param Title $title The title of the page this Revision is associated with.
+ * @param UserIdentity $user
+ * @param CommentStoreComment $comment
+ * @param object $row An archive table row. Use RevisionStore::getArchiveQueryInfo() to build
+ * a query that yields the required fields.
+ * @param RevisionSlots $slots The slots of this revision.
+ * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+ * or false for the local site.
+ */
+ function __construct(
+ Title $title,
+ UserIdentity $user,
+ CommentStoreComment $comment,
+ $row,
+ RevisionSlots $slots,
+ $wikiId = false
+ ) {
+ parent::__construct( $title, $slots, $wikiId );
+ Assert::parameterType( 'object', $row, '$row' );
+
+ $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
+ Assert::parameter( is_string( $timestamp ), '$row->rev_timestamp', 'must be a valid timestamp' );
+
+ $this->mArchiveId = intval( $row->ar_id );
+
+ // NOTE: ar_page_id may be different from $this->mTitle->getArticleID() in some cases,
+ // notably when a partially restored page has been moved, and a new page has been created
+ // with the same title. Archive rows for that title will then have the wrong page id.
+ $this->mPageId = isset( $row->ar_page_id ) ? intval( $row->ar_page_id ) : $title->getArticleID();
+
+ // NOTE: ar_parent_id = 0 indicates that there is no parent revision, while null
+ // indicates that the parent revision is unknown. As per MW 1.31, the database schema
+ // allows ar_parent_id to be NULL.
+ $this->mParentId = isset( $row->ar_parent_id ) ? intval( $row->ar_parent_id ) : null;
+ $this->mId = isset( $row->ar_rev_id ) ? intval( $row->ar_rev_id ) : null;
+ $this->mComment = $comment;
+ $this->mUser = $user;
+ $this->mTimestamp = $timestamp;
+ $this->mMinorEdit = boolval( $row->ar_minor_edit );
+ $this->mDeleted = intval( $row->ar_deleted );
+ $this->mSize = isset( $row->ar_len ) ? intval( $row->ar_len ) : null;
+ $this->mSha1 = !empty( $row->ar_sha1 ) ? $row->ar_sha1 : null;
+ }
+
+ /**
+ * Get archive row ID
+ *
+ * @return int
+ */
+ public function getArchiveId() {
+ return $this->mArchiveId;
+ }
+
+ /**
+ * @return int|null The revision id, or null if the original revision ID
+ * was not recorded in the archive table.
+ */
+ public function getId() {
+ // overwritten just to refine the contract specification.
+ return parent::getId();
+ }
+
+ /**
+ * @throws RevisionAccessException if the size was unknown and could not be calculated.
+ * @return int The nominal revision size, never null. May be computed on the fly.
+ */
+ public function getSize() {
+ // If length is null, calculate and remember it (potentially SLOW!).
+ // This is for compatibility with old database rows that don't have the field set.
+ if ( $this->mSize === null ) {
+ $this->mSize = $this->mSlots->computeSize();
+ }
+
+ return $this->mSize;
+ }
+
+ /**
+ * @throws RevisionAccessException if the hash was unknown and could not be calculated.
+ * @return string The revision hash, never null. May be computed on the fly.
+ */
+ public function getSha1() {
+ // If hash is null, calculate it and remember (potentially SLOW!)
+ // This is for compatibility with old database rows that don't have the field set.
+ if ( $this->mSha1 === null ) {
+ $this->mSha1 = $this->mSlots->computeSha1();
+ }
+
+ return $this->mSha1;
+ }
+
+ /**
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @return UserIdentity The identity of the revision author, null if access is forbidden.
+ */
+ public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
+ // overwritten just to add a guarantee to the contract
+ return parent::getUser( $audience, $user );
+ }
+
+ /**
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @return CommentStoreComment The revision comment, null if access is forbidden.
+ */
+ public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
+ // overwritten just to add a guarantee to the contract
+ return parent::getComment( $audience, $user );
+ }
+
+ /**
+ * @return string never null
+ */
+ public function getTimestamp() {
+ // overwritten just to add a guarantee to the contract
+ return parent::getTimestamp();
+ }
+
+}
diff --git a/www/wiki/includes/Storage/RevisionFactory.php b/www/wiki/includes/Storage/RevisionFactory.php
new file mode 100644
index 00000000..86e8c06f
--- /dev/null
+++ b/www/wiki/includes/Storage/RevisionFactory.php
@@ -0,0 +1,94 @@
+<?php
+/**
+ * Service for constructing revision objects.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use MWException;
+use Title;
+
+/**
+ * Service for constructing revision objects.
+ *
+ * @since 1.31
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ * static methods in Revision.
+ */
+interface RevisionFactory {
+
+ /**
+ * Constructs a new RevisionRecord based on the given associative array following the MW1.29
+ * database convention for the Revision constructor.
+ *
+ * MCR migration note: this replaces Revision::newFromRow
+ *
+ * @deprecated since 1.31. Use a MutableRevisionRecord instead.
+ *
+ * @param array $fields
+ * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
+ * @param Title|null $title
+ *
+ * @return MutableRevisionRecord
+ * @throws MWException
+ */
+ public function newMutableRevisionFromArray( array $fields, $queryFlags = 0, Title $title = null );
+
+ /**
+ * Constructs a RevisionRecord given a database row and content slots.
+ *
+ * MCR migration note: this replaces Revision::newFromRow for rows based on the
+ * revision, slot, and content tables defined for MCR since MW1.31.
+ *
+ * @param object $row A query result row as a raw object.
+ * Use RevisionStore::getQueryInfo() to build a query that yields the required fields.
+ * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
+ * @param Title|null $title
+ *
+ * @return RevisionRecord
+ */
+ public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null );
+
+ /**
+ * Make a fake revision object from an archive table row. This is queried
+ * for permissions or even inserted (as in Special:Undelete)
+ *
+ * MCR migration note: this replaces Revision::newFromArchiveRow
+ *
+ * @param object $row A query result row as a raw object.
+ * Use RevisionStore::getArchiveQueryInfo() to build a query that yields the
+ * required fields.
+ * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
+ * @param Title $title
+ * @param array $overrides An associative array that allows fields in $row to be overwritten.
+ * Keys in this array correspond to field names in $row without the "ar_" prefix, so
+ * $overrides['user'] will override $row->ar_user, etc.
+ *
+ * @return RevisionRecord
+ */
+ public function newRevisionFromArchiveRow(
+ $row,
+ $queryFlags = 0,
+ Title $title = null,
+ array $overrides = []
+ );
+
+}
diff --git a/www/wiki/includes/Storage/RevisionLookup.php b/www/wiki/includes/Storage/RevisionLookup.php
new file mode 100644
index 00000000..45cd1841
--- /dev/null
+++ b/www/wiki/includes/Storage/RevisionLookup.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Service for looking up page revisions.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use IDBAccessObject;
+use MediaWiki\Linker\LinkTarget;
+use Title;
+
+/**
+ * Service for looking up page revisions.
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ * static methods in Revision.
+ *
+ * @since 1.31
+ */
+interface RevisionLookup extends IDBAccessObject {
+
+ /**
+ * Load a page revision from a given revision ID number.
+ * Returns null if no such revision can be found.
+ *
+ * MCR migration note: this replaces Revision::newFromId
+ *
+ * $flags include:
+ *
+ * @param int $id
+ * @param int $flags bit field, see IDBAccessObject::READ_XXX
+ * @return RevisionRecord|null
+ */
+ public function getRevisionById( $id, $flags = 0 );
+
+ /**
+ * Load either the current, or a specified, revision
+ * that's attached to a given link target. If not attached
+ * to that link target, will return null.
+ *
+ * MCR migration note: this replaces Revision::newFromTitle
+ *
+ * @param LinkTarget $linkTarget
+ * @param int $revId (optional)
+ * @param int $flags bit field, see IDBAccessObject::READ_XXX
+ * @return RevisionRecord|null
+ */
+ public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 );
+
+ /**
+ * Load either the current, or a specified, revision
+ * that's attached to a given page ID.
+ * Returns null if no such revision can be found.
+ *
+ * MCR migration note: this replaces Revision::newFromPageId
+ *
+ * @param int $pageId
+ * @param int $revId (optional)
+ * @param int $flags bit field, see IDBAccessObject::READ_XXX
+ * @return RevisionRecord|null
+ */
+ public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 );
+
+ /**
+ * Get previous revision for this title
+ *
+ * MCR migration note: this replaces Revision::getPrevious
+ *
+ * @param RevisionRecord $rev
+ * @param Title $title if known (optional)
+ *
+ * @return RevisionRecord|null
+ */
+ public function getPreviousRevision( RevisionRecord $rev, Title $title = null );
+
+ /**
+ * Get next revision for this title
+ *
+ * MCR migration note: this replaces Revision::getNext
+ *
+ * @param RevisionRecord $rev
+ * @param Title $title if known (optional)
+ *
+ * @return RevisionRecord|null
+ */
+ public function getNextRevision( RevisionRecord $rev, Title $title = null );
+
+ /**
+ * Load a revision based on a known page ID and current revision ID from the DB
+ *
+ * This method allows for the use of caching, though accessing anything that normally
+ * requires permission checks (aside from the text) will trigger a small DB lookup.
+ *
+ * MCR migration note: this replaces Revision::newKnownCurrent
+ *
+ * @param Title $title the associated page title
+ * @param int $revId current revision of this page
+ *
+ * @return RevisionRecord|bool Returns false if missing
+ */
+ public function getKnownCurrentRevision( Title $title, $revId );
+
+}
diff --git a/www/wiki/includes/Storage/RevisionRecord.php b/www/wiki/includes/Storage/RevisionRecord.php
new file mode 100644
index 00000000..6d83e1c1
--- /dev/null
+++ b/www/wiki/includes/Storage/RevisionRecord.php
@@ -0,0 +1,492 @@
+<?php
+/**
+ * Page revision base class.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use CommentStoreComment;
+use Content;
+use InvalidArgumentException;
+use LogicException;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
+use MWException;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Page revision base class.
+ *
+ * RevisionRecords are considered value objects, but they may use callbacks for lazy loading.
+ * Note that while the base class has no setters, subclasses may offer a mutable interface.
+ *
+ * @since 1.31
+ */
+abstract class RevisionRecord {
+
+ // RevisionRecord deletion constants
+ const DELETED_TEXT = 1;
+ const DELETED_COMMENT = 2;
+ const DELETED_USER = 4;
+ const DELETED_RESTRICTED = 8;
+ const SUPPRESSED_USER = 12; // convenience
+ const SUPPRESSED_ALL = 15; // convenience
+
+ // Audience options for accessors
+ const FOR_PUBLIC = 1;
+ const FOR_THIS_USER = 2;
+ const RAW = 3;
+
+ /** @var string Wiki ID; false means the current wiki */
+ protected $mWiki = false;
+ /** @var int|null */
+ protected $mId;
+ /** @var int|null */
+ protected $mPageId;
+ /** @var UserIdentity|null */
+ protected $mUser;
+ /** @var bool */
+ protected $mMinorEdit = false;
+ /** @var string|null */
+ protected $mTimestamp;
+ /** @var int using the DELETED_XXX and SUPPRESSED_XXX flags */
+ protected $mDeleted = 0;
+ /** @var int|null */
+ protected $mSize;
+ /** @var string|null */
+ protected $mSha1;
+ /** @var int|null */
+ protected $mParentId;
+ /** @var CommentStoreComment|null */
+ protected $mComment;
+
+ /** @var Title */
+ protected $mTitle; // TODO: we only need the title for permission checks!
+
+ /** @var RevisionSlots */
+ protected $mSlots;
+
+ /**
+ * @note Avoid calling this constructor directly. Use the appropriate methods
+ * in RevisionStore instead.
+ *
+ * @param Title $title The title of the page this Revision is associated with.
+ * @param RevisionSlots $slots The slots of this revision.
+ * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+ * or false for the local site.
+ *
+ * @throws MWException
+ */
+ function __construct( Title $title, RevisionSlots $slots, $wikiId = false ) {
+ Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+
+ $this->mTitle = $title;
+ $this->mSlots = $slots;
+ $this->mWiki = $wikiId;
+
+ // XXX: this is a sensible default, but we may not have a Title object here in the future.
+ $this->mPageId = $title->getArticleID();
+ }
+
+ /**
+ * Implemented to defy serialization.
+ *
+ * @throws LogicException always
+ */
+ public function __sleep() {
+ throw new LogicException( __CLASS__ . ' is not serializable.' );
+ }
+
+ /**
+ * @param RevisionRecord $rec
+ *
+ * @return bool True if this RevisionRecord is known to have same content as $rec.
+ * False if the content is different (or not known to be the same).
+ */
+ public function hasSameContent( RevisionRecord $rec ) {
+ if ( $rec === $this ) {
+ return true;
+ }
+
+ if ( $this->getId() !== null && $this->getId() === $rec->getId() ) {
+ return true;
+ }
+
+ // check size before hash, since size is quicker to compute
+ if ( $this->getSize() !== $rec->getSize() ) {
+ return false;
+ }
+
+ // instead of checking the hash, we could also check the content addresses of all slots.
+
+ if ( $this->getSha1() === $rec->getSha1() ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the Content of the given slot of this revision.
+ * Call getSlotNames() to get a list of available slots.
+ *
+ * Note that for mutable Content objects, each call to this method will return a
+ * fresh clone.
+ *
+ * MCR migration note: this replaces Revision::getContent
+ *
+ * @param string $role The role name of the desired slot
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @throws RevisionAccessException if the slot does not exist or slot data
+ * could not be lazy-loaded.
+ * @return Content|null The content of the given slot, or null if access is forbidden.
+ */
+ public function getContent( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
+ // XXX: throwing an exception would be nicer, but would a further
+ // departure from the signature of Revision::getContent(), and thus
+ // more complex and error prone refactoring.
+ if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
+ return null;
+ }
+
+ $content = $this->getSlot( $role, $audience, $user )->getContent();
+ return $content->copy();
+ }
+
+ /**
+ * Returns meta-data for the given slot.
+ *
+ * @param string $role The role name of the desired slot
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @throws RevisionAccessException if the slot does not exist or slot data
+ * could not be lazy-loaded.
+ * @return SlotRecord The slot meta-data. If access to the slot content is forbidden,
+ * calling getContent() on the SlotRecord will throw an exception.
+ */
+ public function getSlot( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
+ $slot = $this->mSlots->getSlot( $role );
+
+ if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
+ return SlotRecord::newWithSuppressedContent( $slot );
+ }
+
+ return $slot;
+ }
+
+ /**
+ * Returns whether the given slot is defined in this revision.
+ *
+ * @param string $role The role name of the desired slot
+ *
+ * @return bool
+ */
+ public function hasSlot( $role ) {
+ return $this->mSlots->hasSlot( $role );
+ }
+
+ /**
+ * Returns the slot names (roles) of all slots present in this revision.
+ * getContent() will succeed only for the names returned by this method.
+ *
+ * @return string[]
+ */
+ public function getSlotRoles() {
+ return $this->mSlots->getSlotRoles();
+ }
+
+ /**
+ * Get revision ID. Depending on the concrete subclass, this may return null if
+ * the revision ID is not known (e.g. because the revision does not yet exist
+ * in the database).
+ *
+ * MCR migration note: this replaces Revision::getId
+ *
+ * @return int|null
+ */
+ public function getId() {
+ return $this->mId;
+ }
+
+ /**
+ * Get parent revision ID (the original previous page revision).
+ * If there is no parent revision, this returns 0.
+ * If the parent revision is undefined or unknown, this returns null.
+ *
+ * @note As of MW 1.31, the database schema allows the parent ID to be
+ * NULL to indicate that it is unknown.
+ *
+ * MCR migration note: this replaces Revision::getParentId
+ *
+ * @return int|null
+ */
+ public function getParentId() {
+ return $this->mParentId;
+ }
+
+ /**
+ * Returns the nominal size of this revision, in bogo-bytes.
+ * May be calculated on the fly if not known, which may in the worst
+ * case may involve loading all content.
+ *
+ * MCR migration note: this replaces Revision::getSize
+ *
+ * @throws RevisionAccessException if the size was unknown and could not be calculated.
+ * @return int
+ */
+ abstract public function getSize();
+
+ /**
+ * Returns the base36 sha1 of this revision. This hash is derived from the
+ * hashes of all slots associated with the revision.
+ * May be calculated on the fly if not known, which may in the worst
+ * case may involve loading all content.
+ *
+ * MCR migration note: this replaces Revision::getSha1
+ *
+ * @throws RevisionAccessException if the hash was unknown and could not be calculated.
+ * @return string
+ */
+ abstract public function getSha1();
+
+ /**
+ * Get the page ID. If the page does not yet exist, the page ID is 0.
+ *
+ * MCR migration note: this replaces Revision::getPage
+ *
+ * @return int
+ */
+ public function getPageId() {
+ return $this->mPageId;
+ }
+
+ /**
+ * Get the ID of the wiki this revision belongs to.
+ *
+ * @return string|false The wiki's logical name, of false to indicate the local wiki.
+ */
+ public function getWikiId() {
+ return $this->mWiki;
+ }
+
+ /**
+ * Returns the title of the page this revision is associated with as a LinkTarget object.
+ *
+ * MCR migration note: this replaces Revision::getTitle
+ *
+ * @return LinkTarget
+ */
+ public function getPageAsLinkTarget() {
+ return $this->mTitle;
+ }
+
+ /**
+ * Fetch revision's author's user identity, if it's available to the specified audience.
+ * If the specified audience does not have access to it, null will be
+ * returned. Depending on the concrete subclass, null may also be returned if the user is
+ * not yet specified.
+ *
+ * MCR migration note: this replaces Revision::getUser
+ *
+ * @param int $audience One of:
+ * RevisionRecord::FOR_PUBLIC to be displayed to all users
+ * RevisionRecord::FOR_THIS_USER to be displayed to the given user
+ * RevisionRecord::RAW get the ID regardless of permissions
+ * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
+ * @return UserIdentity|null
+ */
+ public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
+ if ( !$this->audienceCan( self::DELETED_USER, $audience, $user ) ) {
+ return null;
+ } else {
+ return $this->mUser;
+ }
+ }
+
+ /**
+ * Fetch revision comment, if it's available to the specified audience.
+ * If the specified audience does not have access to the comment,
+ * this will return null. Depending on the concrete subclass, null may also be returned
+ * if the comment is not yet specified.
+ *
+ * MCR migration note: this replaces Revision::getComment
+ *
+ * @param int $audience One of:
+ * RevisionRecord::FOR_PUBLIC to be displayed to all users
+ * RevisionRecord::FOR_THIS_USER to be displayed to the given user
+ * RevisionRecord::RAW get the text regardless of permissions
+ * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
+ *
+ * @return CommentStoreComment|null
+ */
+ public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
+ if ( !$this->audienceCan( self::DELETED_COMMENT, $audience, $user ) ) {
+ return null;
+ } else {
+ return $this->mComment;
+ }
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::isMinor
+ *
+ * @return bool
+ */
+ public function isMinor() {
+ return (bool)$this->mMinorEdit;
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::isDeleted
+ *
+ * @param int $field One of DELETED_* bitfield constants
+ *
+ * @return bool
+ */
+ public function isDeleted( $field ) {
+ return ( $this->getVisibility() & $field ) == $field;
+ }
+
+ /**
+ * Get the deletion bitfield of the revision
+ *
+ * MCR migration note: this replaces Revision::getVisibility
+ *
+ * @return int
+ */
+ public function getVisibility() {
+ return (int)$this->mDeleted;
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::getTimestamp.
+ *
+ * May return null if the timestamp was not specified.
+ *
+ * @return string|null
+ */
+ public function getTimestamp() {
+ return $this->mTimestamp;
+ }
+
+ /**
+ * Check that the given audience has access to the given field.
+ *
+ * MCR migration note: this corresponds to Revision::userCan
+ *
+ * @param int $field One of self::DELETED_TEXT,
+ * self::DELETED_COMMENT,
+ * self::DELETED_USER
+ * @param int $audience One of:
+ * RevisionRecord::FOR_PUBLIC to be displayed to all users
+ * RevisionRecord::FOR_THIS_USER to be displayed to the given user
+ * RevisionRecord::RAW get the text regardless of permissions
+ * @param User|null $user User object to check. Required if $audience is FOR_THIS_USER,
+ * ignored otherwise.
+ *
+ * @return bool
+ */
+ protected function audienceCan( $field, $audience, User $user = null ) {
+ if ( $audience == self::FOR_PUBLIC && $this->isDeleted( $field ) ) {
+ return false;
+ } elseif ( $audience == self::FOR_THIS_USER ) {
+ if ( !$user ) {
+ throw new InvalidArgumentException(
+ 'A User object must be given when checking FOR_THIS_USER audience.'
+ );
+ }
+
+ if ( !$this->userCan( $field, $user ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this revision, if it's marked as deleted.
+ *
+ * MCR migration note: this corresponds to Revision::userCan
+ *
+ * @param int $field One of self::DELETED_TEXT,
+ * self::DELETED_COMMENT,
+ * self::DELETED_USER
+ * @param User $user User object to check
+ * @return bool
+ */
+ protected function userCan( $field, User $user ) {
+ // TODO: use callback for permission checks, so we don't need to know a Title object!
+ return self::userCanBitfield( $this->getVisibility(), $field, $user, $this->mTitle );
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this revision, if it's marked as deleted. This is used
+ * by various classes to avoid duplication.
+ *
+ * MCR migration note: this replaces Revision::userCanBitfield
+ *
+ * @param int $bitfield Current field
+ * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
+ * self::DELETED_COMMENT = File::DELETED_COMMENT,
+ * self::DELETED_USER = File::DELETED_USER
+ * @param User $user User object to check
+ * @param Title|null $title A Title object to check for per-page restrictions on,
+ * instead of just plain userrights
+ * @return bool
+ */
+ public static function userCanBitfield( $bitfield, $field, User $user, Title $title = null ) {
+ if ( $bitfield & $field ) { // aspect is deleted
+ if ( $bitfield & self::DELETED_RESTRICTED ) {
+ $permissions = [ 'suppressrevision', 'viewsuppressed' ];
+ } elseif ( $field & self::DELETED_TEXT ) {
+ $permissions = [ 'deletedtext' ];
+ } else {
+ $permissions = [ 'deletedhistory' ];
+ }
+ $permissionlist = implode( ', ', $permissions );
+ if ( $title === null ) {
+ wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
+ return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
+ } else {
+ $text = $title->getPrefixedText();
+ wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
+ foreach ( $permissions as $perm ) {
+ if ( $title->userCan( $perm, $user ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+
+}
diff --git a/www/wiki/includes/Storage/RevisionSlots.php b/www/wiki/includes/Storage/RevisionSlots.php
new file mode 100644
index 00000000..7fa5431d
--- /dev/null
+++ b/www/wiki/includes/Storage/RevisionSlots.php
@@ -0,0 +1,202 @@
+<?php
+/**
+ * Value object representing the set of slots belonging to a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use Content;
+use LogicException;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Value object representing the set of slots belonging to a revision.
+ *
+ * @since 1.31
+ */
+class RevisionSlots {
+
+ /** @var SlotRecord[]|callable */
+ protected $slots;
+
+ /**
+ * @param SlotRecord[]|callable $slots SlotRecords,
+ * or a callback that returns such a structure.
+ */
+ public function __construct( $slots ) {
+ Assert::parameterType( 'array|callable', $slots, '$slots' );
+
+ if ( is_callable( $slots ) ) {
+ $this->slots = $slots;
+ } else {
+ $this->setSlotsInternal( $slots );
+ }
+ }
+
+ /**
+ * @param SlotRecord[] $slots
+ */
+ private function setSlotsInternal( array $slots ) {
+ $this->slots = [];
+
+ // re-key the slot array
+ foreach ( $slots as $slot ) {
+ $role = $slot->getRole();
+ $this->slots[$role] = $slot;
+ }
+ }
+
+ /**
+ * Implemented to defy serialization.
+ *
+ * @throws LogicException always
+ */
+ public function __sleep() {
+ throw new LogicException( __CLASS__ . ' is not serializable.' );
+ }
+
+ /**
+ * Returns the Content of the given slot.
+ * Call getSlotNames() to get a list of available slots.
+ *
+ * Note that for mutable Content objects, each call to this method will return a
+ * fresh clone.
+ *
+ * @param string $role The role name of the desired slot
+ *
+ * @throws RevisionAccessException if the slot does not exist or slot data
+ * could not be lazy-loaded.
+ * @return Content
+ */
+ public function getContent( $role ) {
+ // Return a copy to be safe. Immutable content objects return $this from copy().
+ return $this->getSlot( $role )->getContent()->copy();
+ }
+
+ /**
+ * Returns the SlotRecord of the given slot.
+ * Call getSlotNames() to get a list of available slots.
+ *
+ * @param string $role The role name of the desired slot
+ *
+ * @throws RevisionAccessException if the slot does not exist or slot data
+ * could not be lazy-loaded.
+ * @return SlotRecord
+ */
+ public function getSlot( $role ) {
+ $slots = $this->getSlots();
+
+ if ( isset( $slots[$role] ) ) {
+ return $slots[$role];
+ } else {
+ throw new RevisionAccessException( 'No such slot: ' . $role );
+ }
+ }
+
+ /**
+ * Returns whether the given slot is set.
+ *
+ * @param string $role The role name of the desired slot
+ *
+ * @return bool
+ */
+ public function hasSlot( $role ) {
+ $slots = $this->getSlots();
+
+ return isset( $slots[$role] );
+ }
+
+ /**
+ * Returns the slot names (roles) of all slots present in this revision.
+ * getContent() will succeed only for the names returned by this method.
+ *
+ * @return string[]
+ */
+ public function getSlotRoles() {
+ $slots = $this->getSlots();
+ return array_keys( $slots );
+ }
+
+ /**
+ * Computes the total nominal size of the revision's slots, in bogo-bytes.
+ *
+ * @warn This is potentially expensive! It may cause all slot's content to be loaded
+ * and deserialized.
+ *
+ * @return int
+ */
+ public function computeSize() {
+ return array_reduce( $this->getSlots(), function ( $accu, SlotRecord $slot ) {
+ return $accu + $slot->getSize();
+ }, 0 );
+ }
+
+ /**
+ * Returns an associative array that maps role names to SlotRecords. Each SlotRecord
+ * represents the content meta-data of a slot, together they define the content of
+ * a revision.
+ *
+ * @note This may cause the content meta-data for the revision to be lazy-loaded.
+ *
+ * @return SlotRecord[] revision slot/content rows, keyed by slot role name.
+ */
+ public function getSlots() {
+ if ( is_callable( $this->slots ) ) {
+ $slots = call_user_func( $this->slots );
+
+ Assert::postcondition(
+ is_array( $slots ),
+ 'Slots info callback should return an array of objects'
+ );
+
+ $this->setSlotsInternal( $slots );
+ }
+
+ return $this->slots;
+ }
+
+ /**
+ * Computes the combined hash of the revisions's slots.
+ *
+ * @note For backwards compatibility, the combined hash of a single slot
+ * is that slot's hash. For consistency, the combined hash of an empty set of slots
+ * is the hash of the empty string.
+ *
+ * @warn This is potentially expensive! It may cause all slot's content to be loaded
+ * and deserialized, then re-serialized and hashed.
+ *
+ * @return string
+ */
+ public function computeSha1() {
+ $slots = $this->getSlots();
+ ksort( $slots );
+
+ if ( empty( $slots ) ) {
+ return SlotRecord::base36Sha1( '' );
+ }
+
+ return array_reduce( $slots, function ( $accu, SlotRecord $slot ) {
+ return $accu === null
+ ? $slot->getSha1()
+ : SlotRecord::base36Sha1( $accu . $slot->getSha1() );
+ }, null );
+ }
+
+}
diff --git a/www/wiki/includes/Storage/RevisionStore.php b/www/wiki/includes/Storage/RevisionStore.php
new file mode 100644
index 00000000..13aedbab
--- /dev/null
+++ b/www/wiki/includes/Storage/RevisionStore.php
@@ -0,0 +1,2017 @@
+<?php
+/**
+ * Service for looking up page revisions.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * Attribution notice: when this file was created, much of its content was taken
+ * from the Revision.php file as present in release 1.30. Refer to the history
+ * of that file for original authorship.
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use ActorMigration;
+use CommentStore;
+use CommentStoreComment;
+use Content;
+use ContentHandler;
+use DBAccessObjectUtils;
+use Hooks;
+use IDBAccessObject;
+use InvalidArgumentException;
+use IP;
+use LogicException;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
+use MediaWiki\User\UserIdentityValue;
+use Message;
+use MWException;
+use MWUnknownContentModelException;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use RecentChange;
+use stdClass;
+use Title;
+use User;
+use WANObjectCache;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DBConnRef;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\LoadBalancer;
+
+/**
+ * Service for looking up page revisions.
+ *
+ * @since 1.31
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ * static methods in Revision.
+ */
+class RevisionStore
+ implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
+
+ /**
+ * @var SqlBlobStore
+ */
+ private $blobStore;
+
+ /**
+ * @var bool|string
+ */
+ private $wikiId;
+
+ /**
+ * @var boolean
+ */
+ private $contentHandlerUseDB = true;
+
+ /**
+ * @var LoadBalancer
+ */
+ private $loadBalancer;
+
+ /**
+ * @var WANObjectCache
+ */
+ private $cache;
+
+ /**
+ * @var CommentStore
+ */
+ private $commentStore;
+
+ /**
+ * @var ActorMigration
+ */
+ private $actorMigration;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * @todo $blobStore should be allowed to be any BlobStore!
+ *
+ * @param LoadBalancer $loadBalancer
+ * @param SqlBlobStore $blobStore
+ * @param WANObjectCache $cache
+ * @param CommentStore $commentStore
+ * @param ActorMigration $actorMigration
+ * @param bool|string $wikiId
+ */
+ public function __construct(
+ LoadBalancer $loadBalancer,
+ SqlBlobStore $blobStore,
+ WANObjectCache $cache,
+ CommentStore $commentStore,
+ ActorMigration $actorMigration,
+ $wikiId = false
+ ) {
+ Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+
+ $this->loadBalancer = $loadBalancer;
+ $this->blobStore = $blobStore;
+ $this->cache = $cache;
+ $this->commentStore = $commentStore;
+ $this->actorMigration = $actorMigration;
+ $this->wikiId = $wikiId;
+ $this->logger = new NullLogger();
+ }
+
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ /**
+ * @return bool Whether the store is read-only
+ */
+ public function isReadOnly() {
+ return $this->blobStore->isReadOnly();
+ }
+
+ /**
+ * @return bool
+ */
+ public function getContentHandlerUseDB() {
+ return $this->contentHandlerUseDB;
+ }
+
+ /**
+ * @param bool $contentHandlerUseDB
+ */
+ public function setContentHandlerUseDB( $contentHandlerUseDB ) {
+ $this->contentHandlerUseDB = $contentHandlerUseDB;
+ }
+
+ /**
+ * @return LoadBalancer
+ */
+ private function getDBLoadBalancer() {
+ return $this->loadBalancer;
+ }
+
+ /**
+ * @param int $mode DB_MASTER or DB_REPLICA
+ *
+ * @return IDatabase
+ */
+ private function getDBConnection( $mode ) {
+ $lb = $this->getDBLoadBalancer();
+ return $lb->getConnection( $mode, [], $this->wikiId );
+ }
+
+ /**
+ * @param IDatabase $connection
+ */
+ private function releaseDBConnection( IDatabase $connection ) {
+ $lb = $this->getDBLoadBalancer();
+ $lb->reuseConnection( $connection );
+ }
+
+ /**
+ * @param int $mode DB_MASTER or DB_REPLICA
+ *
+ * @return DBConnRef
+ */
+ private function getDBConnectionRef( $mode ) {
+ $lb = $this->getDBLoadBalancer();
+ return $lb->getConnectionRef( $mode, [], $this->wikiId );
+ }
+
+ /**
+ * Determines the page Title based on the available information.
+ *
+ * MCR migration note: this corresponds to Revision::getTitle
+ *
+ * @note this method should be private, external use should be avoided!
+ *
+ * @param int|null $pageId
+ * @param int|null $revId
+ * @param int $queryFlags
+ *
+ * @return Title
+ * @throws RevisionAccessException
+ */
+ public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
+ if ( !$pageId && !$revId ) {
+ throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
+ }
+
+ // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
+ // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
+ if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
+ $queryFlags = self::READ_NORMAL;
+ }
+
+ $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false );
+ list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
+ $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
+
+ // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
+ if ( $canUseTitleNewFromId ) {
+ // TODO: better foreign title handling (introduce TitleFactory)
+ $title = Title::newFromID( $pageId, $titleFlags );
+ if ( $title ) {
+ return $title;
+ }
+ }
+
+ // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
+ $canUseRevId = ( $revId !== null && $revId > 0 );
+
+ if ( $canUseRevId ) {
+ $dbr = $this->getDBConnectionRef( $dbMode );
+ // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
+ $row = $dbr->selectRow(
+ [ 'revision', 'page' ],
+ [
+ 'page_namespace',
+ 'page_title',
+ 'page_id',
+ 'page_latest',
+ 'page_is_redirect',
+ 'page_len',
+ ],
+ [ 'rev_id' => $revId ],
+ __METHOD__,
+ $dbOptions,
+ [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
+ );
+ if ( $row ) {
+ // TODO: better foreign title handling (introduce TitleFactory)
+ return Title::newFromRow( $row );
+ }
+ }
+
+ // If we still don't have a title, fallback to master if that wasn't already happening.
+ if ( $dbMode !== DB_MASTER ) {
+ $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
+ if ( $title ) {
+ $this->logger->info(
+ __METHOD__ . ' fell back to READ_LATEST and got a Title.',
+ [ 'trace' => wfBacktrace() ]
+ );
+ return $title;
+ }
+ }
+
+ throw new RevisionAccessException(
+ "Could not determine title for page ID $pageId and revision ID $revId"
+ );
+ }
+
+ /**
+ * @param mixed $value
+ * @param string $name
+ *
+ * @throw IncompleteRevisionException if $value is null
+ * @return mixed $value, if $value is not null
+ */
+ private function failOnNull( $value, $name ) {
+ if ( $value === null ) {
+ throw new IncompleteRevisionException(
+ "$name must not be " . var_export( $value, true ) . "!"
+ );
+ }
+
+ return $value;
+ }
+
+ /**
+ * @param mixed $value
+ * @param string $name
+ *
+ * @throw IncompleteRevisionException if $value is empty
+ * @return mixed $value, if $value is not null
+ */
+ private function failOnEmpty( $value, $name ) {
+ if ( $value === null || $value === 0 || $value === '' ) {
+ throw new IncompleteRevisionException(
+ "$name must not be " . var_export( $value, true ) . "!"
+ );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Insert a new revision into the database, returning the new revision record
+ * on success and dies horribly on failure.
+ *
+ * MCR migration note: this replaces Revision::insertOn
+ *
+ * @param RevisionRecord $rev
+ * @param IDatabase $dbw (master connection)
+ *
+ * @throws InvalidArgumentException
+ * @return RevisionRecord the new revision record.
+ */
+ public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
+ // TODO: pass in a DBTransactionContext instead of a database connection.
+ $this->checkDatabaseWikiId( $dbw );
+
+ if ( !$rev->getSlotRoles() ) {
+ throw new InvalidArgumentException( 'At least one slot needs to be defined!' );
+ }
+
+ if ( $rev->getSlotRoles() !== [ 'main' ] ) {
+ throw new InvalidArgumentException( 'Only the main slot is supported for now!' );
+ }
+
+ // TODO: we shouldn't need an actual Title here.
+ $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
+ $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
+
+ $parentId = $rev->getParentId() === null
+ ? $this->getPreviousRevisionId( $dbw, $rev )
+ : $rev->getParentId();
+
+ // Record the text (or external storage URL) to the blob store
+ $slot = $rev->getSlot( 'main', RevisionRecord::RAW );
+
+ $size = $this->failOnNull( $rev->getSize(), 'size field' );
+ $sha1 = $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
+
+ if ( !$slot->hasAddress() ) {
+ $content = $slot->getContent();
+ $format = $content->getDefaultFormat();
+ $model = $content->getModel();
+
+ $this->checkContentModel( $content, $title );
+
+ $data = $content->serialize( $format );
+
+ // Hints allow the blob store to optimize by "leaking" application level information to it.
+ // TODO: with the new MCR storage schema, we rev_id have this before storing the blobs.
+ // When we have it, add rev_id as a hint. Can be used with rev_parent_id for
+ // differential storage or compression of subsequent revisions.
+ $blobHints = [
+ BlobStore::DESIGNATION_HINT => 'page-content', // BlobStore may be used for other things too.
+ BlobStore::PAGE_HINT => $pageId,
+ BlobStore::ROLE_HINT => $slot->getRole(),
+ BlobStore::PARENT_HINT => $parentId,
+ BlobStore::SHA1_HINT => $slot->getSha1(),
+ BlobStore::MODEL_HINT => $model,
+ BlobStore::FORMAT_HINT => $format,
+ ];
+
+ $blobAddress = $this->blobStore->storeBlob( $data, $blobHints );
+ } else {
+ $blobAddress = $slot->getAddress();
+ $model = $slot->getModel();
+ $format = $slot->getFormat();
+ }
+
+ $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
+
+ if ( !$textId ) {
+ throw new LogicException(
+ 'Blob address not supported in 1.29 database schema: ' . $blobAddress
+ );
+ }
+
+ // getTextIdFromAddress() is free to insert something into the text table, so $textId
+ // may be a new value, not anything already contained in $blobAddress.
+ $blobAddress = 'tt:' . $textId;
+
+ $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
+ $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
+ $timestamp = $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
+
+ // Checks.
+ $this->failOnNull( $user->getId(), 'user field' );
+ $this->failOnEmpty( $user->getName(), 'user_text field' );
+
+ # Record the edit in revisions
+ $row = [
+ 'rev_page' => $pageId,
+ 'rev_parent_id' => $parentId,
+ 'rev_text_id' => $textId,
+ 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
+ 'rev_timestamp' => $dbw->timestamp( $timestamp ),
+ 'rev_deleted' => $rev->getVisibility(),
+ 'rev_len' => $size,
+ 'rev_sha1' => $sha1,
+ ];
+
+ if ( $rev->getId() !== null ) {
+ // Needed to restore revisions with their original ID
+ $row['rev_id'] = $rev->getId();
+ }
+
+ list( $commentFields, $commentCallback ) =
+ $this->commentStore->insertWithTempTable( $dbw, 'rev_comment', $comment );
+ $row += $commentFields;
+
+ list( $actorFields, $actorCallback ) =
+ $this->actorMigration->getInsertValuesWithTempTable( $dbw, 'rev_user', $user );
+ $row += $actorFields;
+
+ if ( $this->contentHandlerUseDB ) {
+ // MCR migration note: rev_content_model and rev_content_format will go away
+
+ $defaultModel = ContentHandler::getDefaultModelFor( $title );
+ $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
+
+ $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
+ $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
+ }
+
+ $dbw->insert( 'revision', $row, __METHOD__ );
+
+ if ( !isset( $row['rev_id'] ) ) {
+ // only if auto-increment was used
+ $row['rev_id'] = intval( $dbw->insertId() );
+ }
+ $commentCallback( $row['rev_id'] );
+ $actorCallback( $row['rev_id'], $row );
+
+ // Insert IP revision into ip_changes for use when querying for a range.
+ if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
+ $ipcRow = [
+ 'ipc_rev_id' => $row['rev_id'],
+ 'ipc_rev_timestamp' => $row['rev_timestamp'],
+ 'ipc_hex' => IP::toHex( $user->getName() ),
+ ];
+ $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
+ }
+
+ $newSlot = SlotRecord::newSaved( $row['rev_id'], $textId, $blobAddress, $slot );
+ $slots = new RevisionSlots( [ 'main' => $newSlot ] );
+
+ $rev = new RevisionStoreRecord(
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots,
+ $this->wikiId
+ );
+
+ $newSlot = $rev->getSlot( 'main', RevisionRecord::RAW );
+
+ // sanity checks
+ Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
+ Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
+ Assert::postcondition(
+ $rev->getComment( RevisionRecord::RAW ) !== null,
+ 'revision must have a comment'
+ );
+ Assert::postcondition(
+ $rev->getUser( RevisionRecord::RAW ) !== null,
+ 'revision must have a user'
+ );
+
+ Assert::postcondition( $newSlot !== null, 'revision must have a main slot' );
+ Assert::postcondition(
+ $newSlot->getAddress() !== null,
+ 'main slot must have an addess'
+ );
+
+ Hooks::run( 'RevisionRecordInserted', [ $rev ] );
+
+ return $rev;
+ }
+
+ /**
+ * MCR migration note: this corresponds to Revision::checkContentModel
+ *
+ * @param Content $content
+ * @param Title $title
+ *
+ * @throws MWException
+ * @throws MWUnknownContentModelException
+ */
+ private function checkContentModel( Content $content, Title $title ) {
+ // Note: may return null for revisions that have not yet been inserted
+
+ $model = $content->getModel();
+ $format = $content->getDefaultFormat();
+ $handler = $content->getContentHandler();
+
+ $name = "$title";
+
+ if ( !$handler->isSupportedFormat( $format ) ) {
+ throw new MWException( "Can't use format $format with content model $model on $name" );
+ }
+
+ if ( !$this->contentHandlerUseDB ) {
+ // if $wgContentHandlerUseDB is not set,
+ // all revisions must use the default content model and format.
+
+ $defaultModel = ContentHandler::getDefaultModelFor( $title );
+ $defaultHandler = ContentHandler::getForModelID( $defaultModel );
+ $defaultFormat = $defaultHandler->getDefaultFormat();
+
+ if ( $model != $defaultModel ) {
+ throw new MWException( "Can't save non-default content model with "
+ . "\$wgContentHandlerUseDB disabled: model is $model, "
+ . "default for $name is $defaultModel"
+ );
+ }
+
+ if ( $format != $defaultFormat ) {
+ throw new MWException( "Can't use non-default content format with "
+ . "\$wgContentHandlerUseDB disabled: format is $format, "
+ . "default for $name is $defaultFormat"
+ );
+ }
+ }
+
+ if ( !$content->isValid() ) {
+ throw new MWException(
+ "New content for $name is not valid! Content model is $model"
+ );
+ }
+ }
+
+ /**
+ * Create a new null-revision for insertion into a page's
+ * history. This will not re-save the text, but simply refer
+ * to the text from the previous version.
+ *
+ * Such revisions can for instance identify page rename
+ * operations and other such meta-modifications.
+ *
+ * MCR migration note: this replaces Revision::newNullRevision
+ *
+ * @todo Introduce newFromParentRevision(). newNullRevision can then be based on that
+ * (or go away).
+ *
+ * @param IDatabase $dbw
+ * @param Title $title Title of the page to read from
+ * @param CommentStoreComment $comment RevisionRecord's summary
+ * @param bool $minor Whether the revision should be considered as minor
+ * @param User $user The user to attribute the revision to
+ * @return RevisionRecord|null RevisionRecord or null on error
+ */
+ public function newNullRevision(
+ IDatabase $dbw,
+ Title $title,
+ CommentStoreComment $comment,
+ $minor,
+ User $user
+ ) {
+ $this->checkDatabaseWikiId( $dbw );
+
+ $fields = [ 'page_latest', 'page_namespace', 'page_title',
+ 'rev_id', 'rev_text_id', 'rev_len', 'rev_sha1' ];
+
+ if ( $this->contentHandlerUseDB ) {
+ $fields[] = 'rev_content_model';
+ $fields[] = 'rev_content_format';
+ }
+
+ $current = $dbw->selectRow(
+ [ 'page', 'revision' ],
+ $fields,
+ [
+ 'page_id' => $title->getArticleID(),
+ 'page_latest=rev_id',
+ ],
+ __METHOD__,
+ [ 'FOR UPDATE' ] // T51581
+ );
+
+ if ( $current ) {
+ $fields = [
+ 'page' => $title->getArticleID(),
+ 'user_text' => $user->getName(),
+ 'user' => $user->getId(),
+ 'actor' => $user->getActorId(),
+ 'comment' => $comment,
+ 'minor_edit' => $minor,
+ 'text_id' => $current->rev_text_id,
+ 'parent_id' => $current->page_latest,
+ 'slot_origin' => $current->page_latest,
+ 'len' => $current->rev_len,
+ 'sha1' => $current->rev_sha1
+ ];
+
+ if ( $this->contentHandlerUseDB ) {
+ $fields['content_model'] = $current->rev_content_model;
+ $fields['content_format'] = $current->rev_content_format;
+ }
+
+ $fields['title'] = Title::makeTitle( $current->page_namespace, $current->page_title );
+
+ $mainSlot = $this->emulateMainSlot_1_29( $fields, self::READ_LATEST, $title );
+ $revision = new MutableRevisionRecord( $title, $this->wikiId );
+ $this->initializeMutableRevisionFromArray( $revision, $fields );
+ $revision->setSlot( $mainSlot );
+ } else {
+ $revision = null;
+ }
+
+ return $revision;
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::isUnpatrolled
+ *
+ * @todo This is overly specific, so move or kill this method.
+ *
+ * @param RevisionRecord $rev
+ *
+ * @return int Rcid of the unpatrolled row, zero if there isn't one
+ */
+ public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
+ $rc = $this->getRecentChange( $rev );
+ if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
+ return $rc->getAttribute( 'rc_id' );
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Get the RC object belonging to the current revision, if there's one
+ *
+ * MCR migration note: this replaces Revision::getRecentChange
+ *
+ * @todo move this somewhere else?
+ *
+ * @param RevisionRecord $rev
+ * @param int $flags (optional) $flags include:
+ * IDBAccessObject::READ_LATEST: Select the data from the master
+ *
+ * @return null|RecentChange
+ */
+ public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
+ $dbr = $this->getDBConnection( DB_REPLICA );
+
+ list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
+
+ $userIdentity = $rev->getUser( RevisionRecord::RAW );
+
+ if ( !$userIdentity ) {
+ // If the revision has no user identity, chances are it never went
+ // into the database, and doesn't have an RC entry.
+ return null;
+ }
+
+ // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
+ $actorWhere = $this->actorMigration->getWhere( $dbr, 'rc_user', $rev->getUser(), false );
+ $rc = RecentChange::newFromConds(
+ [
+ $actorWhere['conds'],
+ 'rc_timestamp' => $dbr->timestamp( $rev->getTimestamp() ),
+ 'rc_this_oldid' => $rev->getId()
+ ],
+ __METHOD__,
+ $dbType
+ );
+
+ $this->releaseDBConnection( $dbr );
+
+ // XXX: cache this locally? Glue it to the RevisionRecord?
+ return $rc;
+ }
+
+ /**
+ * Maps fields of the archive row to corresponding revision rows.
+ *
+ * @param object $archiveRow
+ *
+ * @return object a revision row object, corresponding to $archiveRow.
+ */
+ private static function mapArchiveFields( $archiveRow ) {
+ $fieldMap = [
+ // keep with ar prefix:
+ 'ar_id' => 'ar_id',
+
+ // not the same suffix:
+ 'ar_page_id' => 'rev_page',
+ 'ar_rev_id' => 'rev_id',
+
+ // same suffix:
+ 'ar_text_id' => 'rev_text_id',
+ 'ar_timestamp' => 'rev_timestamp',
+ 'ar_user_text' => 'rev_user_text',
+ 'ar_user' => 'rev_user',
+ 'ar_actor' => 'rev_actor',
+ 'ar_minor_edit' => 'rev_minor_edit',
+ 'ar_deleted' => 'rev_deleted',
+ 'ar_len' => 'rev_len',
+ 'ar_parent_id' => 'rev_parent_id',
+ 'ar_sha1' => 'rev_sha1',
+ 'ar_comment' => 'rev_comment',
+ 'ar_comment_cid' => 'rev_comment_cid',
+ 'ar_comment_id' => 'rev_comment_id',
+ 'ar_comment_text' => 'rev_comment_text',
+ 'ar_comment_data' => 'rev_comment_data',
+ 'ar_comment_old' => 'rev_comment_old',
+ 'ar_content_format' => 'rev_content_format',
+ 'ar_content_model' => 'rev_content_model',
+ ];
+
+ $revRow = new stdClass();
+ foreach ( $fieldMap as $arKey => $revKey ) {
+ if ( property_exists( $archiveRow, $arKey ) ) {
+ $revRow->$revKey = $archiveRow->$arKey;
+ }
+ }
+
+ return $revRow;
+ }
+
+ /**
+ * Constructs a RevisionRecord for the revisions main slot, based on the MW1.29 schema.
+ *
+ * @param object|array $row Either a database row or an array
+ * @param int $queryFlags for callbacks
+ * @param Title $title
+ *
+ * @return SlotRecord The main slot, extracted from the MW 1.29 style row.
+ * @throws MWException
+ */
+ private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
+ $mainSlotRow = new stdClass();
+ $mainSlotRow->role_name = 'main';
+ $mainSlotRow->model_name = null;
+ $mainSlotRow->slot_revision_id = null;
+ $mainSlotRow->content_address = null;
+ $mainSlotRow->slot_content_id = null;
+
+ $content = null;
+ $blobData = null;
+ $blobFlags = null;
+
+ if ( is_object( $row ) ) {
+ // archive row
+ if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
+ $row = $this->mapArchiveFields( $row );
+ }
+
+ if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
+ $mainSlotRow->slot_content_id = $row->rev_text_id;
+ $mainSlotRow->content_address = 'tt:' . $row->rev_text_id;
+ }
+
+ // This is used by null-revisions
+ $mainSlotRow->slot_origin = isset( $row->slot_origin )
+ ? intval( $row->slot_origin )
+ : null;
+
+ if ( isset( $row->old_text ) ) {
+ // this happens when the text-table gets joined directly, in the pre-1.30 schema
+ $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
+ // Check against selects that might have not included old_flags
+ if ( !property_exists( $row, 'old_flags' ) ) {
+ throw new InvalidArgumentException( 'old_flags was not set in $row' );
+ }
+ $blobFlags = ( $row->old_flags === null ) ? '' : $row->old_flags;
+ }
+
+ $mainSlotRow->slot_revision_id = intval( $row->rev_id );
+
+ $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
+ $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
+ $mainSlotRow->model_name = isset( $row->rev_content_model )
+ ? strval( $row->rev_content_model )
+ : null;
+ // XXX: in the future, we'll probably always use the default format, and drop content_format
+ $mainSlotRow->format_name = isset( $row->rev_content_format )
+ ? strval( $row->rev_content_format )
+ : null;
+ } elseif ( is_array( $row ) ) {
+ $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
+
+ $mainSlotRow->slot_content_id = isset( $row['text_id'] )
+ ? intval( $row['text_id'] )
+ : null;
+ $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
+ ? intval( $row['slot_origin'] )
+ : null;
+ $mainSlotRow->content_address = isset( $row['text_id'] )
+ ? 'tt:' . intval( $row['text_id'] )
+ : null;
+ $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
+ $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
+
+ $mainSlotRow->model_name = isset( $row['content_model'] )
+ ? strval( $row['content_model'] ) : null; // XXX: must be a string!
+ // XXX: in the future, we'll probably always use the default format, and drop content_format
+ $mainSlotRow->format_name = isset( $row['content_format'] )
+ ? strval( $row['content_format'] ) : null;
+ $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
+ // XXX: If the flags field is not set then $blobFlags should be null so that no
+ // decoding will happen. An empty string will result in default decodings.
+ $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
+
+ // if we have a Content object, override mText and mContentModel
+ if ( !empty( $row['content'] ) ) {
+ if ( !( $row['content'] instanceof Content ) ) {
+ throw new MWException( 'content field must contain a Content object.' );
+ }
+
+ /** @var Content $content */
+ $content = $row['content'];
+ $handler = $content->getContentHandler();
+
+ $mainSlotRow->model_name = $content->getModel();
+
+ // XXX: in the future, we'll probably always use the default format.
+ if ( $mainSlotRow->format_name === null ) {
+ $mainSlotRow->format_name = $handler->getDefaultFormat();
+ }
+ }
+ } else {
+ throw new MWException( 'Revision constructor passed invalid row format.' );
+ }
+
+ // With the old schema, the content changes with every revision,
+ // except for null-revisions.
+ if ( !isset( $mainSlotRow->slot_origin ) ) {
+ $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
+ }
+
+ if ( $mainSlotRow->model_name === null ) {
+ $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
+ // TODO: MCR: consider slot role in getDefaultModelFor()! Use LinkTarget!
+ // TODO: MCR: deprecate $title->getModel().
+ return ContentHandler::getDefaultModelFor( $title );
+ };
+ }
+
+ if ( !$content ) {
+ $content = function ( SlotRecord $slot )
+ use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
+ {
+ return $this->loadSlotContent(
+ $slot,
+ $blobData,
+ $blobFlags,
+ $mainSlotRow->format_name,
+ $queryFlags
+ );
+ };
+ }
+
+ $mainSlotRow->slot_id = $mainSlotRow->slot_revision_id;
+ return new SlotRecord( $mainSlotRow, $content );
+ }
+
+ /**
+ * Loads a Content object based on a slot row.
+ *
+ * This method does not call $slot->getContent(), and may be used as a callback
+ * called by $slot->getContent().
+ *
+ * MCR migration note: this roughly corresponds to Revision::getContentInternal
+ *
+ * @param SlotRecord $slot The SlotRecord to load content for
+ * @param string|null $blobData The content blob, in the form indicated by $blobFlags
+ * @param string|null $blobFlags Flags indicating how $blobData needs to be processed.
+ * Use null if no processing should happen. That is in constrast to the empty string,
+ * which causes the blob to be decoded according to the configured legacy encoding.
+ * @param string|null $blobFormat MIME type indicating how $dataBlob is encoded
+ * @param int $queryFlags
+ *
+ * @throw RevisionAccessException
+ * @return Content
+ */
+ private function loadSlotContent(
+ SlotRecord $slot,
+ $blobData = null,
+ $blobFlags = null,
+ $blobFormat = null,
+ $queryFlags = 0
+ ) {
+ if ( $blobData !== null ) {
+ Assert::parameterType( 'string', $blobData, '$blobData' );
+ Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
+
+ $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
+
+ if ( $blobFlags === null ) {
+ // No blob flags, so use the blob verbatim.
+ $data = $blobData;
+ } else {
+ $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
+ if ( $data === false ) {
+ throw new RevisionAccessException(
+ "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
+ );
+ }
+ }
+
+ } else {
+ $address = $slot->getAddress();
+ try {
+ $data = $this->blobStore->getBlob( $address, $queryFlags );
+ } catch ( BlobAccessException $e ) {
+ throw new RevisionAccessException(
+ "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
+ );
+ }
+ }
+
+ // Unserialize content
+ $handler = ContentHandler::getForModelID( $slot->getModel() );
+
+ $content = $handler->unserializeContent( $data, $blobFormat );
+ return $content;
+ }
+
+ /**
+ * Load a page revision from a given revision ID number.
+ * Returns null if no such revision can be found.
+ *
+ * MCR migration note: this replaces Revision::newFromId
+ *
+ * $flags include:
+ * IDBAccessObject::READ_LATEST: Select the data from the master
+ * IDBAccessObject::READ_LOCKING : Select & lock the data from the master
+ *
+ * @param int $id
+ * @param int $flags (optional)
+ * @return RevisionRecord|null
+ */
+ public function getRevisionById( $id, $flags = 0 ) {
+ return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
+ }
+
+ /**
+ * Load either the current, or a specified, revision
+ * that's attached to a given link target. If not attached
+ * to that link target, will return null.
+ *
+ * MCR migration note: this replaces Revision::newFromTitle
+ *
+ * $flags include:
+ * IDBAccessObject::READ_LATEST: Select the data from the master
+ * IDBAccessObject::READ_LOCKING : Select & lock the data from the master
+ *
+ * @param LinkTarget $linkTarget
+ * @param int $revId (optional)
+ * @param int $flags Bitfield (optional)
+ * @return RevisionRecord|null
+ */
+ public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
+ $conds = [
+ 'page_namespace' => $linkTarget->getNamespace(),
+ 'page_title' => $linkTarget->getDBkey()
+ ];
+ if ( $revId ) {
+ // Use the specified revision ID.
+ // Note that we use newRevisionFromConds here because we want to retry
+ // and fall back to master if the page is not found on a replica.
+ // Since the caller supplied a revision ID, we are pretty sure the revision is
+ // supposed to exist, so we should try hard to find it.
+ $conds['rev_id'] = $revId;
+ return $this->newRevisionFromConds( $conds, $flags );
+ } else {
+ // Use a join to get the latest revision.
+ // Note that we don't use newRevisionFromConds here because we don't want to retry
+ // and fall back to master. The assumption is that we only want to force the fallback
+ // if we are quite sure the revision exists because the caller supplied a revision ID.
+ // If the page isn't found at all on a replica, it probably simply does not exist.
+ $db = $this->getDBConnection( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
+
+ $conds[] = 'rev_id=page_latest';
+ $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
+
+ $this->releaseDBConnection( $db );
+ return $rev;
+ }
+ }
+
+ /**
+ * Load either the current, or a specified, revision
+ * that's attached to a given page ID.
+ * Returns null if no such revision can be found.
+ *
+ * MCR migration note: this replaces Revision::newFromPageId
+ *
+ * $flags include:
+ * IDBAccessObject::READ_LATEST: Select the data from the master (since 1.20)
+ * IDBAccessObject::READ_LOCKING : Select & lock the data from the master
+ *
+ * @param int $pageId
+ * @param int $revId (optional)
+ * @param int $flags Bitfield (optional)
+ * @return RevisionRecord|null
+ */
+ public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
+ $conds = [ 'page_id' => $pageId ];
+ if ( $revId ) {
+ // Use the specified revision ID.
+ // Note that we use newRevisionFromConds here because we want to retry
+ // and fall back to master if the page is not found on a replica.
+ // Since the caller supplied a revision ID, we are pretty sure the revision is
+ // supposed to exist, so we should try hard to find it.
+ $conds['rev_id'] = $revId;
+ return $this->newRevisionFromConds( $conds, $flags );
+ } else {
+ // Use a join to get the latest revision.
+ // Note that we don't use newRevisionFromConds here because we don't want to retry
+ // and fall back to master. The assumption is that we only want to force the fallback
+ // if we are quite sure the revision exists because the caller supplied a revision ID.
+ // If the page isn't found at all on a replica, it probably simply does not exist.
+ $db = $this->getDBConnection( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
+
+ $conds[] = 'rev_id=page_latest';
+ $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
+
+ $this->releaseDBConnection( $db );
+ return $rev;
+ }
+ }
+
+ /**
+ * Load the revision for the given title with the given timestamp.
+ * WARNING: Timestamps may in some circumstances not be unique,
+ * so this isn't the best key to use.
+ *
+ * MCR migration note: this replaces Revision::loadFromTimestamp
+ *
+ * @param Title $title
+ * @param string $timestamp
+ * @return RevisionRecord|null
+ */
+ public function getRevisionByTimestamp( $title, $timestamp ) {
+ $db = $this->getDBConnection( DB_REPLICA );
+ return $this->newRevisionFromConds(
+ [
+ 'rev_timestamp' => $db->timestamp( $timestamp ),
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ ],
+ 0,
+ $title
+ );
+ }
+
+ /**
+ * Make a fake revision object from an archive table row. This is queried
+ * for permissions or even inserted (as in Special:Undelete)
+ *
+ * MCR migration note: this replaces Revision::newFromArchiveRow
+ *
+ * @param object $row
+ * @param int $queryFlags
+ * @param Title|null $title
+ * @param array $overrides associative array with fields of $row to override. This may be
+ * used e.g. to force the parent revision ID or page ID. Keys in the array are fields
+ * names from the archive table without the 'ar_' prefix, i.e. use 'parent_id' to
+ * override ar_parent_id.
+ *
+ * @return RevisionRecord
+ * @throws MWException
+ */
+ public function newRevisionFromArchiveRow(
+ $row,
+ $queryFlags = 0,
+ Title $title = null,
+ array $overrides = []
+ ) {
+ Assert::parameterType( 'object', $row, '$row' );
+
+ // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
+ Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
+
+ if ( !$title && isset( $overrides['title'] ) ) {
+ if ( !( $overrides['title'] instanceof Title ) ) {
+ throw new MWException( 'title field override must contain a Title object.' );
+ }
+
+ $title = $overrides['title'];
+ }
+
+ if ( !isset( $title ) ) {
+ if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
+ $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+ } else {
+ throw new InvalidArgumentException(
+ 'A Title or ar_namespace and ar_title must be given'
+ );
+ }
+ }
+
+ foreach ( $overrides as $key => $value ) {
+ $field = "ar_$key";
+ $row->$field = $value;
+ }
+
+ try {
+ $user = User::newFromAnyId(
+ isset( $row->ar_user ) ? $row->ar_user : null,
+ isset( $row->ar_user_text ) ? $row->ar_user_text : null,
+ isset( $row->ar_actor ) ? $row->ar_actor : null
+ );
+ } catch ( InvalidArgumentException $ex ) {
+ wfWarn( __METHOD__ . ': ' . $ex->getMessage() );
+ $user = new UserIdentityValue( 0, '', 0 );
+ }
+
+ $comment = $this->commentStore
+ // Legacy because $row may have come from self::selectFields()
+ ->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), 'ar_comment', $row, true );
+
+ $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, $title );
+ $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
+
+ return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
+ }
+
+ /**
+ * @see RevisionFactory::newRevisionFromRow_1_29
+ *
+ * MCR migration note: this replaces Revision::newFromRow
+ *
+ * @param object $row
+ * @param int $queryFlags
+ * @param Title|null $title
+ *
+ * @return RevisionRecord
+ * @throws MWException
+ * @throws RevisionAccessException
+ */
+ private function newRevisionFromRow_1_29( $row, $queryFlags = 0, Title $title = null ) {
+ Assert::parameterType( 'object', $row, '$row' );
+
+ if ( !$title ) {
+ $pageId = isset( $row->rev_page ) ? $row->rev_page : 0; // XXX: also check page_id?
+ $revId = isset( $row->rev_id ) ? $row->rev_id : 0;
+
+ $title = $this->getTitle( $pageId, $revId, $queryFlags );
+ }
+
+ if ( !isset( $row->page_latest ) ) {
+ $row->page_latest = $title->getLatestRevID();
+ if ( $row->page_latest === 0 && $title->exists() ) {
+ wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
+ }
+ }
+
+ try {
+ $user = User::newFromAnyId(
+ isset( $row->rev_user ) ? $row->rev_user : null,
+ isset( $row->rev_user_text ) ? $row->rev_user_text : null,
+ isset( $row->rev_actor ) ? $row->rev_actor : null
+ );
+ } catch ( InvalidArgumentException $ex ) {
+ wfWarn( __METHOD__ . ': ' . $ex->getMessage() );
+ $user = new UserIdentityValue( 0, '', 0 );
+ }
+
+ $comment = $this->commentStore
+ // Legacy because $row may have come from self::selectFields()
+ ->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), 'rev_comment', $row, true );
+
+ $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, $title );
+ $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
+
+ return new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
+ }
+
+ /**
+ * @see RevisionFactory::newRevisionFromRow
+ *
+ * MCR migration note: this replaces Revision::newFromRow
+ *
+ * @param object $row
+ * @param int $queryFlags
+ * @param Title|null $title
+ *
+ * @return RevisionRecord
+ */
+ public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null ) {
+ return $this->newRevisionFromRow_1_29( $row, $queryFlags, $title );
+ }
+
+ /**
+ * Constructs a new MutableRevisionRecord based on the given associative array following
+ * the MW1.29 convention for the Revision constructor.
+ *
+ * MCR migration note: this replaces Revision::newFromRow
+ *
+ * @param array $fields
+ * @param int $queryFlags
+ * @param Title|null $title
+ *
+ * @return MutableRevisionRecord
+ * @throws MWException
+ * @throws RevisionAccessException
+ */
+ public function newMutableRevisionFromArray(
+ array $fields,
+ $queryFlags = 0,
+ Title $title = null
+ ) {
+ if ( !$title && isset( $fields['title'] ) ) {
+ if ( !( $fields['title'] instanceof Title ) ) {
+ throw new MWException( 'title field must contain a Title object.' );
+ }
+
+ $title = $fields['title'];
+ }
+
+ if ( !$title ) {
+ $pageId = isset( $fields['page'] ) ? $fields['page'] : 0;
+ $revId = isset( $fields['id'] ) ? $fields['id'] : 0;
+
+ $title = $this->getTitle( $pageId, $revId, $queryFlags );
+ }
+
+ if ( !isset( $fields['page'] ) ) {
+ $fields['page'] = $title->getArticleID( $queryFlags );
+ }
+
+ // if we have a content object, use it to set the model and type
+ if ( !empty( $fields['content'] ) ) {
+ if ( !( $fields['content'] instanceof Content ) ) {
+ throw new MWException( 'content field must contain a Content object.' );
+ }
+
+ if ( !empty( $fields['text_id'] ) ) {
+ throw new MWException(
+ "Text already stored in external store (id {$fields['text_id']}), " .
+ "can't serialize content object"
+ );
+ }
+ }
+
+ if (
+ isset( $fields['comment'] )
+ && !( $fields['comment'] instanceof CommentStoreComment )
+ ) {
+ $commentData = isset( $fields['comment_data'] ) ? $fields['comment_data'] : null;
+
+ if ( $fields['comment'] instanceof Message ) {
+ $fields['comment'] = CommentStoreComment::newUnsavedComment(
+ $fields['comment'],
+ $commentData
+ );
+ } else {
+ $commentText = trim( strval( $fields['comment'] ) );
+ $fields['comment'] = CommentStoreComment::newUnsavedComment(
+ $commentText,
+ $commentData
+ );
+ }
+ }
+
+ $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
+
+ $revision = new MutableRevisionRecord( $title, $this->wikiId );
+ $this->initializeMutableRevisionFromArray( $revision, $fields );
+ $revision->setSlot( $mainSlot );
+
+ return $revision;
+ }
+
+ /**
+ * @param MutableRevisionRecord $record
+ * @param array $fields
+ */
+ private function initializeMutableRevisionFromArray(
+ MutableRevisionRecord $record,
+ array $fields
+ ) {
+ /** @var UserIdentity $user */
+ $user = null;
+
+ if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
+ $user = $fields['user'];
+ } else {
+ try {
+ $user = User::newFromAnyId(
+ isset( $fields['user'] ) ? $fields['user'] : null,
+ isset( $fields['user_text'] ) ? $fields['user_text'] : null,
+ isset( $fields['actor'] ) ? $fields['actor'] : null
+ );
+ } catch ( InvalidArgumentException $ex ) {
+ $user = null;
+ }
+ }
+
+ if ( $user ) {
+ $record->setUser( $user );
+ }
+
+ $timestamp = isset( $fields['timestamp'] )
+ ? strval( $fields['timestamp'] )
+ : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
+
+ $record->setTimestamp( $timestamp );
+
+ if ( isset( $fields['page'] ) ) {
+ $record->setPageId( intval( $fields['page'] ) );
+ }
+
+ if ( isset( $fields['id'] ) ) {
+ $record->setId( intval( $fields['id'] ) );
+ }
+ if ( isset( $fields['parent_id'] ) ) {
+ $record->setParentId( intval( $fields['parent_id'] ) );
+ }
+
+ if ( isset( $fields['sha1'] ) ) {
+ $record->setSha1( $fields['sha1'] );
+ }
+ if ( isset( $fields['size'] ) ) {
+ $record->setSize( intval( $fields['size'] ) );
+ }
+
+ if ( isset( $fields['minor_edit'] ) ) {
+ $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
+ }
+ if ( isset( $fields['deleted'] ) ) {
+ $record->setVisibility( intval( $fields['deleted'] ) );
+ }
+
+ if ( isset( $fields['comment'] ) ) {
+ Assert::parameterType(
+ CommentStoreComment::class,
+ $fields['comment'],
+ '$row[\'comment\']'
+ );
+ $record->setComment( $fields['comment'] );
+ }
+ }
+
+ /**
+ * Load a page revision from a given revision ID number.
+ * Returns null if no such revision can be found.
+ *
+ * MCR migration note: this corresponds to Revision::loadFromId
+ *
+ * @note direct use is deprecated!
+ * @todo remove when unused! there seem to be no callers of Revision::loadFromId
+ *
+ * @param IDatabase $db
+ * @param int $id
+ *
+ * @return RevisionRecord|null
+ */
+ public function loadRevisionFromId( IDatabase $db, $id ) {
+ return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
+ }
+
+ /**
+ * Load either the current, or a specified, revision
+ * that's attached to a given page. If not attached
+ * to that page, will return null.
+ *
+ * MCR migration note: this replaces Revision::loadFromPageId
+ *
+ * @note direct use is deprecated!
+ * @todo remove when unused!
+ *
+ * @param IDatabase $db
+ * @param int $pageid
+ * @param int $id
+ * @return RevisionRecord|null
+ */
+ public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
+ $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
+ if ( $id ) {
+ $conds['rev_id'] = intval( $id );
+ } else {
+ $conds[] = 'rev_id=page_latest';
+ }
+ return $this->loadRevisionFromConds( $db, $conds );
+ }
+
+ /**
+ * Load either the current, or a specified, revision
+ * that's attached to a given page. If not attached
+ * to that page, will return null.
+ *
+ * MCR migration note: this replaces Revision::loadFromTitle
+ *
+ * @note direct use is deprecated!
+ * @todo remove when unused!
+ *
+ * @param IDatabase $db
+ * @param Title $title
+ * @param int $id
+ *
+ * @return RevisionRecord|null
+ */
+ public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
+ if ( $id ) {
+ $matchId = intval( $id );
+ } else {
+ $matchId = 'page_latest';
+ }
+
+ return $this->loadRevisionFromConds(
+ $db,
+ [
+ "rev_id=$matchId",
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ ],
+ 0,
+ $title
+ );
+ }
+
+ /**
+ * Load the revision for the given title with the given timestamp.
+ * WARNING: Timestamps may in some circumstances not be unique,
+ * so this isn't the best key to use.
+ *
+ * MCR migration note: this replaces Revision::loadFromTimestamp
+ *
+ * @note direct use is deprecated! Use getRevisionFromTimestamp instead!
+ * @todo remove when unused!
+ *
+ * @param IDatabase $db
+ * @param Title $title
+ * @param string $timestamp
+ * @return RevisionRecord|null
+ */
+ public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
+ return $this->loadRevisionFromConds( $db,
+ [
+ 'rev_timestamp' => $db->timestamp( $timestamp ),
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ ],
+ 0,
+ $title
+ );
+ }
+
+ /**
+ * Given a set of conditions, fetch a revision
+ *
+ * This method should be used if we are pretty sure the revision exists.
+ * Unless $flags has READ_LATEST set, this method will first try to find the revision
+ * on a replica before hitting the master database.
+ *
+ * MCR migration note: this corresponds to Revision::newFromConds
+ *
+ * @param array $conditions
+ * @param int $flags (optional)
+ * @param Title $title
+ *
+ * @return RevisionRecord|null
+ */
+ private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
+ $db = $this->getDBConnection( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
+ $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
+ $this->releaseDBConnection( $db );
+
+ $lb = $this->getDBLoadBalancer();
+
+ // Make sure new pending/committed revision are visibile later on
+ // within web requests to certain avoid bugs like T93866 and T94407.
+ if ( !$rev
+ && !( $flags & self::READ_LATEST )
+ && $lb->getServerCount() > 1
+ && $lb->hasOrMadeRecentMasterChanges()
+ ) {
+ $flags = self::READ_LATEST;
+ $db = $this->getDBConnection( DB_MASTER );
+ $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
+ $this->releaseDBConnection( $db );
+ }
+
+ return $rev;
+ }
+
+ /**
+ * Given a set of conditions, fetch a revision from
+ * the given database connection.
+ *
+ * MCR migration note: this corresponds to Revision::loadFromConds
+ *
+ * @param IDatabase $db
+ * @param array $conditions
+ * @param int $flags (optional)
+ * @param Title $title
+ *
+ * @return RevisionRecord|null
+ */
+ private function loadRevisionFromConds(
+ IDatabase $db,
+ $conditions,
+ $flags = 0,
+ Title $title = null
+ ) {
+ $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
+ if ( $row ) {
+ $rev = $this->newRevisionFromRow( $row, $flags, $title );
+
+ return $rev;
+ }
+
+ return null;
+ }
+
+ /**
+ * Throws an exception if the given database connection does not belong to the wiki this
+ * RevisionStore is bound to.
+ *
+ * @param IDatabase $db
+ * @throws MWException
+ */
+ private function checkDatabaseWikiId( IDatabase $db ) {
+ $storeWiki = $this->wikiId;
+ $dbWiki = $db->getDomainID();
+
+ if ( $dbWiki === $storeWiki ) {
+ return;
+ }
+
+ // XXX: we really want the default database ID...
+ $storeWiki = $storeWiki ?: wfWikiID();
+ $dbWiki = $dbWiki ?: wfWikiID();
+
+ if ( $dbWiki === $storeWiki ) {
+ return;
+ }
+
+ // HACK: counteract encoding imposed by DatabaseDomain
+ $storeWiki = str_replace( '?h', '-', $storeWiki );
+ $dbWiki = str_replace( '?h', '-', $dbWiki );
+
+ if ( $dbWiki === $storeWiki ) {
+ return;
+ }
+
+ throw new MWException( "RevisionStore for $storeWiki "
+ . "cannot be used with a DB connection for $dbWiki" );
+ }
+
+ /**
+ * Given a set of conditions, return a row with the
+ * fields necessary to build RevisionRecord objects.
+ *
+ * MCR migration note: this corresponds to Revision::fetchFromConds
+ *
+ * @param IDatabase $db
+ * @param array $conditions
+ * @param int $flags (optional)
+ *
+ * @return object|false data row as a raw object
+ */
+ private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
+ $this->checkDatabaseWikiId( $db );
+
+ $revQuery = self::getQueryInfo( [ 'page', 'user' ] );
+ $options = [];
+ if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
+ $options[] = 'FOR UPDATE';
+ }
+ return $db->selectRow(
+ $revQuery['tables'],
+ $revQuery['fields'],
+ $conditions,
+ __METHOD__,
+ $options,
+ $revQuery['joins']
+ );
+ }
+
+ /**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new revision object.
+ *
+ * MCR migration note: this replaces Revision::getQueryInfo
+ *
+ * @since 1.31
+ *
+ * @param array $options Any combination of the following strings
+ * - 'page': Join with the page table, and select fields to identify the page
+ * - 'user': Join with the user table, and select the user name
+ * - 'text': Join with the text table, and select fields to load page text
+ *
+ * @return array With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ */
+ public function getQueryInfo( $options = [] ) {
+ $ret = [
+ 'tables' => [],
+ 'fields' => [],
+ 'joins' => [],
+ ];
+
+ $ret['tables'][] = 'revision';
+ $ret['fields'] = array_merge( $ret['fields'], [
+ 'rev_id',
+ 'rev_page',
+ 'rev_text_id',
+ 'rev_timestamp',
+ 'rev_minor_edit',
+ 'rev_deleted',
+ 'rev_len',
+ 'rev_parent_id',
+ 'rev_sha1',
+ ] );
+
+ $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
+ $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
+ $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
+ $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
+
+ $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
+ $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
+ $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
+ $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
+
+ if ( $this->contentHandlerUseDB ) {
+ $ret['fields'][] = 'rev_content_format';
+ $ret['fields'][] = 'rev_content_model';
+ }
+
+ if ( in_array( 'page', $options, true ) ) {
+ $ret['tables'][] = 'page';
+ $ret['fields'] = array_merge( $ret['fields'], [
+ 'page_namespace',
+ 'page_title',
+ 'page_id',
+ 'page_latest',
+ 'page_is_redirect',
+ 'page_len',
+ ] );
+ $ret['joins']['page'] = [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
+ }
+
+ if ( in_array( 'user', $options, true ) ) {
+ $ret['tables'][] = 'user';
+ $ret['fields'] = array_merge( $ret['fields'], [
+ 'user_name',
+ ] );
+ $u = $actorQuery['fields']['rev_user'];
+ $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
+ }
+
+ if ( in_array( 'text', $options, true ) ) {
+ $ret['tables'][] = 'text';
+ $ret['fields'] = array_merge( $ret['fields'], [
+ 'old_text',
+ 'old_flags'
+ ] );
+ $ret['joins']['text'] = [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ];
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new archived revision object.
+ *
+ * MCR migration note: this replaces Revision::getArchiveQueryInfo
+ *
+ * @since 1.31
+ *
+ * @return array With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ */
+ public function getArchiveQueryInfo() {
+ $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
+ $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
+ $ret = [
+ 'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
+ 'fields' => [
+ 'ar_id',
+ 'ar_page_id',
+ 'ar_namespace',
+ 'ar_title',
+ 'ar_rev_id',
+ 'ar_text_id',
+ 'ar_timestamp',
+ 'ar_minor_edit',
+ 'ar_deleted',
+ 'ar_len',
+ 'ar_parent_id',
+ 'ar_sha1',
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
+ 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
+ ];
+
+ if ( $this->contentHandlerUseDB ) {
+ $ret['fields'][] = 'ar_content_format';
+ $ret['fields'][] = 'ar_content_model';
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Do a batched query for the sizes of a set of revisions.
+ *
+ * MCR migration note: this replaces Revision::getParentLengths
+ *
+ * @param int[] $revIds
+ * @return int[] associative array mapping revision IDs from $revIds to the nominal size
+ * of the corresponding revision.
+ */
+ public function getRevisionSizes( array $revIds ) {
+ return $this->listRevisionSizes( $this->getDBConnection( DB_REPLICA ), $revIds );
+ }
+
+ /**
+ * Do a batched query for the sizes of a set of revisions.
+ *
+ * MCR migration note: this replaces Revision::getParentLengths
+ *
+ * @deprecated use RevisionStore::getRevisionSizes instead.
+ *
+ * @param IDatabase $db
+ * @param int[] $revIds
+ * @return int[] associative array mapping revision IDs from $revIds to the nominal size
+ * of the corresponding revision.
+ */
+ public function listRevisionSizes( IDatabase $db, array $revIds ) {
+ $this->checkDatabaseWikiId( $db );
+
+ $revLens = [];
+ if ( !$revIds ) {
+ return $revLens; // empty
+ }
+
+ $res = $db->select(
+ 'revision',
+ [ 'rev_id', 'rev_len' ],
+ [ 'rev_id' => $revIds ],
+ __METHOD__
+ );
+
+ foreach ( $res as $row ) {
+ $revLens[$row->rev_id] = intval( $row->rev_len );
+ }
+
+ return $revLens;
+ }
+
+ /**
+ * Get previous revision for this title
+ *
+ * MCR migration note: this replaces Revision::getPrevious
+ *
+ * @param RevisionRecord $rev
+ * @param Title $title if known (optional)
+ *
+ * @return RevisionRecord|null
+ */
+ public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) {
+ if ( $title === null ) {
+ $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
+ }
+ $prev = $title->getPreviousRevisionID( $rev->getId() );
+ if ( $prev ) {
+ return $this->getRevisionByTitle( $title, $prev );
+ }
+ return null;
+ }
+
+ /**
+ * Get next revision for this title
+ *
+ * MCR migration note: this replaces Revision::getNext
+ *
+ * @param RevisionRecord $rev
+ * @param Title $title if known (optional)
+ *
+ * @return RevisionRecord|null
+ */
+ public function getNextRevision( RevisionRecord $rev, Title $title = null ) {
+ if ( $title === null ) {
+ $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
+ }
+ $next = $title->getNextRevisionID( $rev->getId() );
+ if ( $next ) {
+ return $this->getRevisionByTitle( $title, $next );
+ }
+ return null;
+ }
+
+ /**
+ * Get previous revision Id for this page_id
+ * This is used to populate rev_parent_id on save
+ *
+ * MCR migration note: this corresponds to Revision::getPreviousRevisionId
+ *
+ * @param IDatabase $db
+ * @param RevisionRecord $rev
+ *
+ * @return int
+ */
+ private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
+ $this->checkDatabaseWikiId( $db );
+
+ if ( $rev->getPageId() === null ) {
+ return 0;
+ }
+ # Use page_latest if ID is not given
+ if ( !$rev->getId() ) {
+ $prevId = $db->selectField(
+ 'page', 'page_latest',
+ [ 'page_id' => $rev->getPageId() ],
+ __METHOD__
+ );
+ } else {
+ $prevId = $db->selectField(
+ 'revision', 'rev_id',
+ [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
+ __METHOD__,
+ [ 'ORDER BY' => 'rev_id DESC' ]
+ );
+ }
+ return intval( $prevId );
+ }
+
+ /**
+ * Get rev_timestamp from rev_id, without loading the rest of the row
+ *
+ * MCR migration note: this replaces Revision::getTimestampFromId
+ *
+ * @param Title $title
+ * @param int $id
+ * @param int $flags
+ * @return string|bool False if not found
+ */
+ public function getTimestampFromId( $title, $id, $flags = 0 ) {
+ $db = $this->getDBConnection(
+ ( $flags & IDBAccessObject::READ_LATEST ) ? DB_MASTER : DB_REPLICA
+ );
+
+ $conds = [ 'rev_id' => $id ];
+ $conds['rev_page'] = $title->getArticleID();
+ $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
+
+ $this->releaseDBConnection( $db );
+ return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
+ }
+
+ /**
+ * Get count of revisions per page...not very efficient
+ *
+ * MCR migration note: this replaces Revision::countByPageId
+ *
+ * @param IDatabase $db
+ * @param int $id Page id
+ * @return int
+ */
+ public function countRevisionsByPageId( IDatabase $db, $id ) {
+ $this->checkDatabaseWikiId( $db );
+
+ $row = $db->selectRow( 'revision',
+ [ 'revCount' => 'COUNT(*)' ],
+ [ 'rev_page' => $id ],
+ __METHOD__
+ );
+ if ( $row ) {
+ return intval( $row->revCount );
+ }
+ return 0;
+ }
+
+ /**
+ * Get count of revisions per page...not very efficient
+ *
+ * MCR migration note: this replaces Revision::countByTitle
+ *
+ * @param IDatabase $db
+ * @param Title $title
+ * @return int
+ */
+ public function countRevisionsByTitle( IDatabase $db, $title ) {
+ $id = $title->getArticleID();
+ if ( $id ) {
+ return $this->countRevisionsByPageId( $db, $id );
+ }
+ return 0;
+ }
+
+ /**
+ * Check if no edits were made by other users since
+ * the time a user started editing the page. Limit to
+ * 50 revisions for the sake of performance.
+ *
+ * MCR migration note: this replaces Revision::userWasLastToEdit
+ *
+ * @deprecated since 1.31; Can possibly be removed, since the self-conflict suppression
+ * logic in EditPage that uses this seems conceptually dubious. Revision::userWasLastToEdit
+ * has been deprecated since 1.24.
+ *
+ * @param IDatabase $db The Database to perform the check on.
+ * @param int $pageId The ID of the page in question
+ * @param int $userId The ID of the user in question
+ * @param string $since Look at edits since this time
+ *
+ * @return bool True if the given user was the only one to edit since the given timestamp
+ */
+ public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
+ $this->checkDatabaseWikiId( $db );
+
+ if ( !$userId ) {
+ return false;
+ }
+
+ $revQuery = self::getQueryInfo();
+ $res = $db->select(
+ $revQuery['tables'],
+ [
+ 'rev_user' => $revQuery['fields']['rev_user'],
+ ],
+ [
+ 'rev_page' => $pageId,
+ 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
+ ],
+ __METHOD__,
+ [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
+ $revQuery['joins']
+ );
+ foreach ( $res as $row ) {
+ if ( $row->rev_user != $userId ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Load a revision based on a known page ID and current revision ID from the DB
+ *
+ * This method allows for the use of caching, though accessing anything that normally
+ * requires permission checks (aside from the text) will trigger a small DB lookup.
+ *
+ * MCR migration note: this replaces Revision::newKnownCurrent
+ *
+ * @param Title $title the associated page title
+ * @param int $revId current revision of this page. Defaults to $title->getLatestRevID().
+ *
+ * @return RevisionRecord|bool Returns false if missing
+ */
+ public function getKnownCurrentRevision( Title $title, $revId ) {
+ $db = $this->getDBConnectionRef( DB_REPLICA );
+
+ $pageId = $title->getArticleID();
+
+ if ( !$pageId ) {
+ return false;
+ }
+
+ if ( !$revId ) {
+ $revId = $title->getLatestRevID();
+ }
+
+ if ( !$revId ) {
+ wfWarn(
+ 'No latest revision known for page ' . $title->getPrefixedDBkey()
+ . ' even though it exists with page ID ' . $pageId
+ );
+ return false;
+ }
+
+ $row = $this->cache->getWithSetCallback(
+ // Page/rev IDs passed in from DB to reflect history merges
+ $this->cache->makeGlobalKey( 'revision-row-1.29', $db->getDomainID(), $pageId, $revId ),
+ WANObjectCache::TTL_WEEK,
+ function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
+ $setOpts += Database::getCacheSetOptions( $db );
+
+ $conds = [
+ 'rev_page' => intval( $pageId ),
+ 'page_id' => intval( $pageId ),
+ 'rev_id' => intval( $revId ),
+ ];
+
+ $row = $this->fetchRevisionRowFromConds( $db, $conds );
+ return $row ?: false; // don't cache negatives
+ }
+ );
+
+ // Reflect revision deletion and user renames
+ if ( $row ) {
+ return $this->newRevisionFromRow( $row, 0, $title );
+ } else {
+ return false;
+ }
+ }
+
+ // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
+
+}
diff --git a/www/wiki/includes/Storage/RevisionStoreRecord.php b/www/wiki/includes/Storage/RevisionStoreRecord.php
new file mode 100644
index 00000000..d092f22e
--- /dev/null
+++ b/www/wiki/includes/Storage/RevisionStoreRecord.php
@@ -0,0 +1,210 @@
+<?php
+/**
+ * A RevisionRecord representing an existing revision persisted in the revision table.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use CommentStoreComment;
+use InvalidArgumentException;
+use MediaWiki\User\UserIdentity;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+
+/**
+ * A RevisionRecord representing an existing revision persisted in the revision table.
+ * RevisionStoreRecord has no optional fields, getters will never return null.
+ *
+ * @since 1.31
+ */
+class RevisionStoreRecord extends RevisionRecord {
+
+ /** @var bool */
+ protected $mCurrent = false;
+
+ /**
+ * @note Avoid calling this constructor directly. Use the appropriate methods
+ * in RevisionStore instead.
+ *
+ * @param Title $title The title of the page this Revision is associated with.
+ * @param UserIdentity $user
+ * @param CommentStoreComment $comment
+ * @param object $row A row from the revision table. Use RevisionStore::getQueryInfo() to build
+ * a query that yields the required fields.
+ * @param RevisionSlots $slots The slots of this revision.
+ * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+ * or false for the local site.
+ */
+ function __construct(
+ Title $title,
+ UserIdentity $user,
+ CommentStoreComment $comment,
+ $row,
+ RevisionSlots $slots,
+ $wikiId = false
+ ) {
+ parent::__construct( $title, $slots, $wikiId );
+ Assert::parameterType( 'object', $row, '$row' );
+
+ $this->mId = intval( $row->rev_id );
+ $this->mPageId = intval( $row->rev_page );
+ $this->mComment = $comment;
+
+ $timestamp = wfTimestamp( TS_MW, $row->rev_timestamp );
+ Assert::parameter( is_string( $timestamp ), '$row->rev_timestamp', 'must be a valid timestamp' );
+
+ $this->mUser = $user;
+ $this->mMinorEdit = boolval( $row->rev_minor_edit );
+ $this->mTimestamp = $timestamp;
+ $this->mDeleted = intval( $row->rev_deleted );
+
+ // NOTE: rev_parent_id = 0 indicates that there is no parent revision, while null
+ // indicates that the parent revision is unknown. As per MW 1.31, the database schema
+ // allows rev_parent_id to be NULL.
+ $this->mParentId = isset( $row->rev_parent_id ) ? intval( $row->rev_parent_id ) : null;
+ $this->mSize = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
+ $this->mSha1 = !empty( $row->rev_sha1 ) ? $row->rev_sha1 : null;
+
+ // NOTE: we must not call $this->mTitle->getLatestRevID() here, since the state of
+ // page_latest may be in limbo during revision creation. In that case, calling
+ // $this->mTitle->getLatestRevID() would cause a bad value to be cached in the Title
+ // object. During page creation, that bad value would be 0.
+ if ( isset( $row->page_latest ) ) {
+ $this->mCurrent = ( $row->rev_id == $row->page_latest );
+ }
+
+ // sanity check
+ if (
+ $this->mPageId && $this->mTitle->exists()
+ && $this->mPageId !== $this->mTitle->getArticleID()
+ ) {
+ throw new InvalidArgumentException(
+ 'The given Title does not belong to page ID ' . $this->mPageId .
+ ' but actually belongs to ' . $this->mTitle->getArticleID()
+ );
+ }
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::isCurrent
+ *
+ * @return bool
+ */
+ public function isCurrent() {
+ return $this->mCurrent;
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::isDeleted
+ *
+ * @param int $field One of DELETED_* bitfield constants
+ *
+ * @return bool
+ */
+ public function isDeleted( $field ) {
+ if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
+ // Current revisions of pages cannot have the content hidden. Skipping this
+ // check is very useful for Parser as it fetches templates using newKnownCurrent().
+ // Calling getVisibility() in that case triggers a verification database query.
+ return false; // no need to check
+ }
+
+ return parent::isDeleted( $field );
+ }
+
+ protected function userCan( $field, User $user ) {
+ if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
+ // Current revisions of pages cannot have the content hidden. Skipping this
+ // check is very useful for Parser as it fetches templates using newKnownCurrent().
+ // Calling getVisibility() in that case triggers a verification database query.
+ return true; // no need to check
+ }
+
+ return parent::userCan( $field, $user );
+ }
+
+ /**
+ * @return int The revision id, never null.
+ */
+ public function getId() {
+ // overwritten just to add a guarantee to the contract
+ return parent::getId();
+ }
+
+ /**
+ * @throws RevisionAccessException if the size was unknown and could not be calculated.
+ * @return string The nominal revision size, never null. May be computed on the fly.
+ */
+ public function getSize() {
+ // If length is null, calculate and remember it (potentially SLOW!).
+ // This is for compatibility with old database rows that don't have the field set.
+ if ( $this->mSize === null ) {
+ $this->mSize = $this->mSlots->computeSize();
+ }
+
+ return $this->mSize;
+ }
+
+ /**
+ * @throws RevisionAccessException if the hash was unknown and could not be calculated.
+ * @return string The revision hash, never null. May be computed on the fly.
+ */
+ public function getSha1() {
+ // If hash is null, calculate it and remember (potentially SLOW!)
+ // This is for compatibility with old database rows that don't have the field set.
+ if ( $this->mSha1 === null ) {
+ $this->mSha1 = $this->mSlots->computeSha1();
+ }
+
+ return $this->mSha1;
+ }
+
+ /**
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @return UserIdentity The identity of the revision author, null if access is forbidden.
+ */
+ public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
+ // overwritten just to add a guarantee to the contract
+ return parent::getUser( $audience, $user );
+ }
+
+ /**
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @return CommentStoreComment The revision comment, null if access is forbidden.
+ */
+ public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
+ // overwritten just to add a guarantee to the contract
+ return parent::getComment( $audience, $user );
+ }
+
+ /**
+ * @return string timestamp, never null
+ */
+ public function getTimestamp() {
+ // overwritten just to add a guarantee to the contract
+ return parent::getTimestamp();
+ }
+
+}
diff --git a/www/wiki/includes/Storage/SlotRecord.php b/www/wiki/includes/Storage/SlotRecord.php
new file mode 100644
index 00000000..50d11005
--- /dev/null
+++ b/www/wiki/includes/Storage/SlotRecord.php
@@ -0,0 +1,568 @@
+<?php
+/**
+ * Value object representing a content slot associated with a page revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use Content;
+use InvalidArgumentException;
+use LogicException;
+use OutOfBoundsException;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Value object representing a content slot associated with a page revision.
+ * SlotRecord provides direct access to a Content object.
+ * That access may be implemented through a callback.
+ *
+ * @since 1.31
+ */
+class SlotRecord {
+
+ /**
+ * @var object database result row, as a raw object
+ */
+ private $row;
+
+ /**
+ * @var Content|callable
+ */
+ private $content;
+
+ /**
+ * Returns a new SlotRecord just like the given $slot, except that calling getContent()
+ * will fail with an exception.
+ *
+ * @param SlotRecord $slot
+ *
+ * @return SlotRecord
+ */
+ public static function newWithSuppressedContent( SlotRecord $slot ) {
+ $row = $slot->row;
+
+ return new SlotRecord( $row, function () {
+ throw new SuppressedDataException( 'Content suppressed!' );
+ } );
+ }
+
+ /**
+ * Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
+ * The slot's content cannot be overwritten.
+ *
+ * @param SlotRecord $slot
+ * @param array $overrides
+ *
+ * @return SlotRecord
+ */
+ private static function newDerived( SlotRecord $slot, array $overrides = [] ) {
+ $row = clone $slot->row;
+ $row->slot_id = null; // never copy the row ID!
+
+ foreach ( $overrides as $key => $value ) {
+ $row->$key = $value;
+ }
+
+ return new SlotRecord( $row, $slot->content );
+ }
+
+ /**
+ * Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord
+ * of a previous revision.
+ *
+ * Note that a SlotRecord constructed this way are intended as prototypes,
+ * to be used wit newSaved(). They are incomplete, so some getters such as
+ * getRevision() will fail.
+ *
+ * @param SlotRecord $slot
+ *
+ * @return SlotRecord
+ */
+ public static function newInherited( SlotRecord $slot ) {
+ // Sanity check - we can't inherit from a Slot that's not attached to a revision.
+ $slot->getRevision();
+ $slot->getOrigin();
+ $slot->getAddress();
+
+ // NOTE: slot_origin and content_address are copied from $slot.
+ return self::newDerived( $slot, [
+ 'slot_revision_id' => null,
+ ] );
+ }
+
+ /**
+ * Constructs a new Slot from a Content object for a new revision.
+ * This is the preferred way to construct a slot for storing Content that
+ * resulted from a user edit. The slot is assumed to be not inherited.
+ *
+ * Note that a SlotRecord constructed this way are intended as prototypes,
+ * to be used wit newSaved(). They are incomplete, so some getters such as
+ * getAddress() will fail.
+ *
+ * @param string $role
+ * @param Content $content
+ *
+ * @return SlotRecord An incomplete proto-slot object, to be used with newSaved() later.
+ */
+ public static function newUnsaved( $role, Content $content ) {
+ Assert::parameterType( 'string', $role, '$role' );
+
+ $row = [
+ 'slot_id' => null, // not yet known
+ 'slot_revision_id' => null, // not yet known
+ 'slot_origin' => null, // not yet known, will be set in newSaved()
+ 'content_size' => null, // compute later
+ 'content_sha1' => null, // compute later
+ 'slot_content_id' => null, // not yet known, will be set in newSaved()
+ 'content_address' => null, // not yet known, will be set in newSaved()
+ 'role_name' => $role,
+ 'model_name' => $content->getModel(),
+ ];
+
+ return new SlotRecord( (object)$row, $content );
+ }
+
+ /**
+ * Constructs a complete SlotRecord for a newly saved revision, based on the incomplete
+ * proto-slot. This adds information that has only become available during saving,
+ * particularly the revision ID and content address.
+ *
+ * @param int $revisionId the revision the slot is to be associated with (field slot_revision_id).
+ * If $protoSlot already has a revision, it must be the same.
+ * @param int $contentId the ID of the row in the content table describing the content
+ * referenced by $contentAddress (field slot_content_id).
+ * If $protoSlot already has a content ID, it must be the same.
+ * @param string $contentAddress the slot's content address (field content_address).
+ * If $protoSlot already has an address, it must be the same.
+ * @param SlotRecord $protoSlot The proto-slot that was provided as input for creating a new
+ * revision. $protoSlot must have a content address if inherited.
+ *
+ * @return SlotRecord If the state of $protoSlot is inappropriate for saving a new revision.
+ */
+ public static function newSaved(
+ $revisionId,
+ $contentId,
+ $contentAddress,
+ SlotRecord $protoSlot
+ ) {
+ Assert::parameterType( 'integer', $revisionId, '$revisionId' );
+ Assert::parameterType( 'integer', $contentId, '$contentId' );
+ Assert::parameterType( 'string', $contentAddress, '$contentAddress' );
+
+ if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
+ throw new LogicException(
+ "Mismatching revision ID $revisionId: "
+ . "The slot already belongs to revision {$protoSlot->getRevision()}. "
+ . "Use SlotRecord::newInherited() to re-use content between revisions."
+ );
+ }
+
+ if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) {
+ throw new LogicException(
+ "Mismatching blob address $contentAddress: "
+ . "The slot already has content at {$protoSlot->getAddress()}."
+ );
+ }
+
+ if ( $protoSlot->hasAddress() && $protoSlot->getContentId() !== $contentId ) {
+ throw new LogicException(
+ "Mismatching content ID $contentId: "
+ . "The slot already has content row {$protoSlot->getContentId()} associated."
+ );
+ }
+
+ if ( $protoSlot->isInherited() ) {
+ if ( !$protoSlot->hasAddress() ) {
+ throw new InvalidArgumentException(
+ "An inherited blob should have a content address!"
+ );
+ }
+ if ( !$protoSlot->hasField( 'slot_origin' ) ) {
+ throw new InvalidArgumentException(
+ "A saved inherited slot should have an origin set!"
+ );
+ }
+ $origin = $protoSlot->getOrigin();
+ } else {
+ $origin = $revisionId;
+ }
+
+ return self::newDerived( $protoSlot, [
+ 'slot_revision_id' => $revisionId,
+ 'slot_content_id' => $contentId,
+ 'slot_origin' => $origin,
+ 'content_address' => $contentAddress,
+ ] );
+ }
+
+ /**
+ * SlotRecord constructor.
+ *
+ * The following fields are supported by the $row parameter:
+ *
+ * $row->blob_data
+ * $row->blob_address
+ *
+ * @param object $row A database row composed of fields of the slot and content tables,
+ * as a raw object. Any field value can be a callback that produces the field value
+ * given this SlotRecord as a parameter. However, plain strings cannot be used as
+ * callbacks here, for security reasons.
+ * @param Content|callable $content The content object associated with the slot, or a
+ * callback that will return that Content object, given this SlotRecord as a parameter.
+ */
+ public function __construct( $row, $content ) {
+ Assert::parameterType( 'object', $row, '$row' );
+ Assert::parameterType( 'Content|callable', $content, '$content' );
+
+ Assert::parameter(
+ property_exists( $row, 'slot_id' ),
+ '$row->slot_id',
+ 'must exist'
+ );
+ Assert::parameter(
+ property_exists( $row, 'slot_revision_id' ),
+ '$row->slot_revision_id',
+ 'must exist'
+ );
+ Assert::parameter(
+ property_exists( $row, 'slot_content_id' ),
+ '$row->slot_content_id',
+ 'must exist'
+ );
+ Assert::parameter(
+ property_exists( $row, 'content_address' ),
+ '$row->content_address',
+ 'must exist'
+ );
+ Assert::parameter(
+ property_exists( $row, 'model_name' ),
+ '$row->model_name',
+ 'must exist'
+ );
+ Assert::parameter(
+ property_exists( $row, 'slot_origin' ),
+ '$row->slot_origin',
+ 'must exist'
+ );
+ Assert::parameter(
+ !property_exists( $row, 'slot_inherited' ),
+ '$row->slot_inherited',
+ 'must not exist'
+ );
+ Assert::parameter(
+ !property_exists( $row, 'slot_revision' ),
+ '$row->slot_revision',
+ 'must not exist'
+ );
+
+ $this->row = $row;
+ $this->content = $content;
+ }
+
+ /**
+ * Implemented to defy serialization.
+ *
+ * @throws LogicException always
+ */
+ public function __sleep() {
+ throw new LogicException( __CLASS__ . ' is not serializable.' );
+ }
+
+ /**
+ * Returns the Content of the given slot.
+ *
+ * @note This is free to load Content from whatever subsystem is necessary,
+ * performing potentially expensive operations and triggering I/O-related
+ * failure modes.
+ *
+ * @note This method does not apply audience filtering.
+ *
+ * @throws SuppressedDataException if access to the content is not allowed according
+ * to the audience check performed by RevisionRecord::getSlot().
+ *
+ * @return Content The slot's content. This is a direct reference to the internal instance,
+ * copy before exposing to application logic!
+ */
+ public function getContent() {
+ if ( $this->content instanceof Content ) {
+ return $this->content;
+ }
+
+ $obj = call_user_func( $this->content, $this );
+
+ Assert::postcondition(
+ $obj instanceof Content,
+ 'Slot content callback should return a Content object'
+ );
+
+ $this->content = $obj;
+
+ return $this->content;
+ }
+
+ /**
+ * Returns the string value of a data field from the database row supplied to the constructor.
+ * If the field was set to a callback, that callback is invoked and the result returned.
+ *
+ * @param string $name
+ *
+ * @throws OutOfBoundsException
+ * @throws IncompleteRevisionException
+ * @return mixed Returns the field's value, never null.
+ */
+ private function getField( $name ) {
+ if ( !isset( $this->row->$name ) ) {
+ // distinguish between unknown and uninitialized fields
+ if ( property_exists( $this->row, $name ) ) {
+ throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
+ } else {
+ throw new OutOfBoundsException( 'No such field: ' . $name );
+ }
+ }
+
+ $value = $this->row->$name;
+
+ // NOTE: allow callbacks, but don't trust plain string callables from the database!
+ if ( !is_string( $value ) && is_callable( $value ) ) {
+ $value = call_user_func( $value, $this );
+ $this->setField( $name, $value );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Returns the string value of a data field from the database row supplied to the constructor.
+ *
+ * @param string $name
+ *
+ * @throws OutOfBoundsException
+ * @throws IncompleteRevisionException
+ * @return string Returns the string value
+ */
+ private function getStringField( $name ) {
+ return strval( $this->getField( $name ) );
+ }
+
+ /**
+ * Returns the int value of a data field from the database row supplied to the constructor.
+ *
+ * @param string $name
+ *
+ * @throws OutOfBoundsException
+ * @throws IncompleteRevisionException
+ * @return int Returns the int value
+ */
+ private function getIntField( $name ) {
+ return intval( $this->getField( $name ) );
+ }
+
+ /**
+ * @param string $name
+ * @return bool whether this record contains the given field
+ */
+ private function hasField( $name ) {
+ return isset( $this->row->$name );
+ }
+
+ /**
+ * Returns the ID of the revision this slot is associated with.
+ *
+ * @return int
+ */
+ public function getRevision() {
+ return $this->getIntField( 'slot_revision_id' );
+ }
+
+ /**
+ * Returns the revision ID of the revision that originated the slot's content.
+ *
+ * @return int
+ */
+ public function getOrigin() {
+ return $this->getIntField( 'slot_origin' );
+ }
+
+ /**
+ * Whether this slot was inherited from an older revision.
+ *
+ * If this SlotRecord is already attached to a revision, this returns true
+ * if the slot's revision of origin is the same as the revision it belongs to.
+ *
+ * If this SlotRecord is not yet attached to a revision, this returns true
+ * if the slot already has an address.
+ *
+ * @return bool
+ */
+ public function isInherited() {
+ if ( $this->hasRevision() ) {
+ return $this->getRevision() !== $this->getOrigin();
+ } else {
+ return $this->hasAddress();
+ }
+ }
+
+ /**
+ * Whether this slot has an address. Slots will have an address if their
+ * content has been stored. While building a new revision,
+ * SlotRecords will not have an address associated.
+ *
+ * @return bool
+ */
+ public function hasAddress() {
+ return $this->hasField( 'content_address' );
+ }
+
+ /**
+ * Whether this slot has revision ID associated. Slots will have a revision ID associated
+ * only if they were loaded as part of an existing revision. While building a new revision,
+ * Slotrecords will not have a revision ID associated.
+ *
+ * @return bool
+ */
+ public function hasRevision() {
+ return $this->hasField( 'slot_revision_id' );
+ }
+
+ /**
+ * Returns the role of the slot.
+ *
+ * @return string
+ */
+ public function getRole() {
+ return $this->getStringField( 'role_name' );
+ }
+
+ /**
+ * Returns the address of this slot's content.
+ * This address can be used with BlobStore to load the Content object.
+ *
+ * @return string
+ */
+ public function getAddress() {
+ return $this->getStringField( 'content_address' );
+ }
+
+ /**
+ * Returns the ID of the content meta data row associated with the slot.
+ * This information should be irrelevant to application logic, it is here to allow
+ * the construction of a full row for the revision table.
+ *
+ * @return int
+ */
+ public function getContentId() {
+ return $this->getIntField( 'slot_content_id' );
+ }
+
+ /**
+ * Returns the content size
+ *
+ * @return int size of the content, in bogo-bytes, as reported by Content::getSize.
+ */
+ public function getSize() {
+ try {
+ $size = $this->getIntField( 'content_size' );
+ } catch ( IncompleteRevisionException $ex ) {
+ $size = $this->getContent()->getSize();
+ $this->setField( 'content_size', $size );
+ }
+
+ return $size;
+ }
+
+ /**
+ * Returns the content size
+ *
+ * @return string hash of the content.
+ */
+ public function getSha1() {
+ try {
+ $sha1 = $this->getStringField( 'content_sha1' );
+ } catch ( IncompleteRevisionException $ex ) {
+ $format = $this->hasField( 'format_name' )
+ ? $this->getStringField( 'format_name' )
+ : null;
+
+ $data = $this->getContent()->serialize( $format );
+ $sha1 = self::base36Sha1( $data );
+ $this->setField( 'content_sha1', $sha1 );
+ }
+
+ return $sha1;
+ }
+
+ /**
+ * Returns the content model. This is the model name that decides
+ * which ContentHandler is appropriate for interpreting the
+ * data of the blob referenced by the address returned by getAddress().
+ *
+ * @return string the content model of the content
+ */
+ public function getModel() {
+ try {
+ $model = $this->getStringField( 'model_name' );
+ } catch ( IncompleteRevisionException $ex ) {
+ $model = $this->getContent()->getModel();
+ $this->setField( 'model_name', $model );
+ }
+
+ return $model;
+ }
+
+ /**
+ * Returns the blob serialization format as a MIME type.
+ *
+ * @note When this method returns null, the caller is expected
+ * to auto-detect the serialization format, or to rely on
+ * the default format associated with the content model.
+ *
+ * @return string|null
+ */
+ public function getFormat() {
+ // XXX: we currently do not plan to store the format for each slot!
+
+ if ( $this->hasField( 'format_name' ) ) {
+ return $this->getStringField( 'format_name' );
+ }
+
+ return null;
+ }
+
+ /**
+ * @param string $name
+ * @param string|int|null $value
+ */
+ private function setField( $name, $value ) {
+ $this->row->$name = $value;
+ }
+
+ /**
+ * Get the base 36 SHA-1 value for a string of text
+ *
+ * MCR migration note: this replaces Revision::base36Sha1
+ *
+ * @param string $blob
+ * @return string
+ */
+ public static function base36Sha1( $blob ) {
+ return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
+ }
+
+}
diff --git a/www/wiki/includes/Storage/SqlBlobStore.php b/www/wiki/includes/Storage/SqlBlobStore.php
new file mode 100644
index 00000000..0ff7c133
--- /dev/null
+++ b/www/wiki/includes/Storage/SqlBlobStore.php
@@ -0,0 +1,600 @@
+<?php
+/**
+ * Service for storing and loading data blobs representing revision content.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * Attribution notice: when this file was created, much of its content was taken
+ * from the Revision.php file as present in release 1.30. Refer to the history
+ * of that file for original authorship.
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use DBAccessObjectUtils;
+use ExternalStore;
+use IDBAccessObject;
+use IExpiringStore;
+use InvalidArgumentException;
+use Language;
+use MWException;
+use WANObjectCache;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\LoadBalancer;
+
+/**
+ * Service for storing and loading Content objects.
+ *
+ * @since 1.31
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ * static methods in Revision.
+ */
+class SqlBlobStore implements IDBAccessObject, BlobStore {
+
+ // Note: the name has been taken unchanged from the Revision class.
+ const TEXT_CACHE_GROUP = 'revisiontext:10';
+
+ /**
+ * @var LoadBalancer
+ */
+ private $dbLoadBalancer;
+
+ /**
+ * @var WANObjectCache
+ */
+ private $cache;
+
+ /**
+ * @var bool|string Wiki ID
+ */
+ private $wikiId;
+
+ /**
+ * @var int
+ */
+ private $cacheExpiry = 604800; // 7 days
+
+ /**
+ * @var bool
+ */
+ private $compressBlobs = false;
+
+ /**
+ * @var bool|string
+ */
+ private $legacyEncoding = false;
+
+ /**
+ * @var Language|null
+ */
+ private $legacyEncodingConversionLang = null;
+
+ /**
+ * @var boolean
+ */
+ private $useExternalStore = false;
+
+ /**
+ * @param LoadBalancer $dbLoadBalancer A load balancer for acquiring database connections
+ * @param WANObjectCache $cache A cache manager for caching blobs
+ * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+ */
+ public function __construct(
+ LoadBalancer $dbLoadBalancer,
+ WANObjectCache $cache,
+ $wikiId = false
+ ) {
+ $this->dbLoadBalancer = $dbLoadBalancer;
+ $this->cache = $cache;
+ $this->wikiId = $wikiId;
+ }
+
+ /**
+ * @return int time for which blobs can be cached, in seconds
+ */
+ public function getCacheExpiry() {
+ return $this->cacheExpiry;
+ }
+
+ /**
+ * @param int $cacheExpiry time for which blobs can be cached, in seconds
+ */
+ public function setCacheExpiry( $cacheExpiry ) {
+ Assert::parameterType( 'integer', $cacheExpiry, '$cacheExpiry' );
+
+ $this->cacheExpiry = $cacheExpiry;
+ }
+
+ /**
+ * @return bool whether blobs should be compressed for storage
+ */
+ public function getCompressBlobs() {
+ return $this->compressBlobs;
+ }
+
+ /**
+ * @param bool $compressBlobs whether blobs should be compressed for storage
+ */
+ public function setCompressBlobs( $compressBlobs ) {
+ $this->compressBlobs = $compressBlobs;
+ }
+
+ /**
+ * @return false|string The legacy encoding to assume for blobs that are not marked as utf8.
+ * False means handling of legacy encoding is disabled, and utf8 assumed.
+ */
+ public function getLegacyEncoding() {
+ return $this->legacyEncoding;
+ }
+
+ /**
+ * @return Language|null The locale to use when decoding from a legacy encoding, or null
+ * if handling of legacy encoding is disabled.
+ */
+ public function getLegacyEncodingConversionLang() {
+ return $this->legacyEncodingConversionLang;
+ }
+
+ /**
+ * @param string $legacyEncoding The legacy encoding to assume for blobs that are
+ * not marked as utf8.
+ * @param Language $language The locale to use when decoding from a legacy encoding.
+ */
+ public function setLegacyEncoding( $legacyEncoding, Language $language ) {
+ Assert::parameterType( 'string', $legacyEncoding, '$legacyEncoding' );
+
+ $this->legacyEncoding = $legacyEncoding;
+ $this->legacyEncodingConversionLang = $language;
+ }
+
+ /**
+ * @return bool Whether to use the ExternalStore mechanism for storing blobs.
+ */
+ public function getUseExternalStore() {
+ return $this->useExternalStore;
+ }
+
+ /**
+ * @param bool $useExternalStore Whether to use the ExternalStore mechanism for storing blobs.
+ */
+ public function setUseExternalStore( $useExternalStore ) {
+ Assert::parameterType( 'boolean', $useExternalStore, '$useExternalStore' );
+
+ $this->useExternalStore = $useExternalStore;
+ }
+
+ /**
+ * @return LoadBalancer
+ */
+ private function getDBLoadBalancer() {
+ return $this->dbLoadBalancer;
+ }
+
+ /**
+ * @param int $index A database index, like DB_MASTER or DB_REPLICA
+ *
+ * @return IDatabase
+ */
+ private function getDBConnection( $index ) {
+ $lb = $this->getDBLoadBalancer();
+ return $lb->getConnection( $index, [], $this->wikiId );
+ }
+
+ /**
+ * Stores an arbitrary blob of data and returns an address that can be used with
+ * getBlob() to retrieve the same blob of data,
+ *
+ * @param string $data
+ * @param array $hints An array of hints.
+ *
+ * @throws BlobAccessException
+ * @return string an address that can be used with getBlob() to retrieve the data.
+ */
+ public function storeBlob( $data, $hints = [] ) {
+ try {
+ $flags = $this->compressData( $data );
+
+ # Write to external storage if required
+ if ( $this->useExternalStore ) {
+ // Store and get the URL
+ $data = ExternalStore::insertToDefault( $data );
+ if ( !$data ) {
+ throw new BlobAccessException( "Failed to store text to external storage" );
+ }
+ if ( $flags ) {
+ $flags .= ',';
+ }
+ $flags .= 'external';
+
+ // TODO: we could also return an address for the external store directly here.
+ // That would mean bypassing the text table entirely when the external store is
+ // used. We'll need to assess expected fallout before doing that.
+ }
+
+ $dbw = $this->getDBConnection( DB_MASTER );
+
+ $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
+ $dbw->insert(
+ 'text',
+ [
+ 'old_id' => $old_id,
+ 'old_text' => $data,
+ 'old_flags' => $flags,
+ ],
+ __METHOD__
+ );
+
+ $textId = $dbw->insertId();
+
+ return 'tt:' . $textId;
+ } catch ( MWException $e ) {
+ throw new BlobAccessException( $e->getMessage(), 0, $e );
+ }
+ }
+
+ /**
+ * Retrieve a blob, given an address.
+ * Currently hardcoded to the 'text' table storage engine.
+ *
+ * MCR migration note: this replaces Revision::loadText
+ *
+ * @param string $blobAddress
+ * @param int $queryFlags
+ *
+ * @throws BlobAccessException
+ * @return string
+ */
+ public function getBlob( $blobAddress, $queryFlags = 0 ) {
+ Assert::parameterType( 'string', $blobAddress, '$blobAddress' );
+
+ // No negative caching; negative hits on text rows may be due to corrupted replica DBs
+ $blob = $this->cache->getWithSetCallback(
+ // TODO: change key, since this is not necessarily revision text!
+ $this->cache->makeKey( 'revisiontext', 'textid', $blobAddress ),
+ $this->getCacheTTL(),
+ function ( $unused, &$ttl, &$setOpts ) use ( $blobAddress, $queryFlags ) {
+ list( $index ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
+ $setOpts += Database::getCacheSetOptions( $this->getDBConnection( $index ) );
+
+ return $this->fetchBlob( $blobAddress, $queryFlags );
+ },
+ [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => IExpiringStore::TTL_PROC_LONG ]
+ );
+
+ if ( $blob === false ) {
+ throw new BlobAccessException( 'Failed to load blob from address ' . $blobAddress );
+ }
+
+ return $blob;
+ }
+
+ /**
+ * MCR migration note: this corresponds to Revision::fetchText
+ *
+ * @param string $blobAddress
+ * @param int $queryFlags
+ *
+ * @throw BlobAccessException
+ * @return string|false
+ */
+ private function fetchBlob( $blobAddress, $queryFlags ) {
+ list( $schema, $id, ) = self::splitBlobAddress( $blobAddress );
+
+ //TODO: MCR: also support 'ex' schema with ExternalStore URLs, plus flags encoded in the URL!
+ if ( $schema === 'tt' ) {
+ $textId = intval( $id );
+ } else {
+ // XXX: change to better exceptions! That makes migration more difficult, though.
+ throw new BlobAccessException( "Unknown blob address schema: $schema" );
+ }
+
+ if ( !$textId || $id !== (string)$textId ) {
+ // XXX: change to better exceptions! That makes migration more difficult, though.
+ throw new BlobAccessException( "Bad blob address: $blobAddress" );
+ }
+
+ // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
+ // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
+ $queryFlags |= DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST )
+ ? self::READ_LATEST_IMMUTABLE
+ : 0;
+
+ list( $index, $options, $fallbackIndex, $fallbackOptions ) =
+ DBAccessObjectUtils::getDBOptions( $queryFlags );
+
+ // Text data is immutable; check replica DBs first.
+ $row = $this->getDBConnection( $index )->selectRow(
+ 'text',
+ [ 'old_text', 'old_flags' ],
+ [ 'old_id' => $textId ],
+ __METHOD__,
+ $options
+ );
+
+ // Fallback to DB_MASTER in some cases if the row was not found, using the appropriate
+ // options, such as FOR UPDATE to avoid missing rows due to REPEATABLE-READ.
+ if ( !$row && $fallbackIndex !== null ) {
+ $row = $this->getDBConnection( $fallbackIndex )->selectRow(
+ 'text',
+ [ 'old_text', 'old_flags' ],
+ [ 'old_id' => $textId ],
+ __METHOD__,
+ $fallbackOptions
+ );
+ }
+
+ if ( !$row ) {
+ wfWarn( __METHOD__ . ": No text row with ID $textId." );
+ return false;
+ }
+
+ $blob = $this->expandBlob( $row->old_text, $row->old_flags, $blobAddress );
+
+ if ( $blob === false ) {
+ wfWarn( __METHOD__ . ": Bad data in text row $textId." );
+ return false;
+ }
+
+ return $blob;
+ }
+
+ /**
+ * Expand a raw data blob according to the flags given.
+ *
+ * MCR migration note: this replaces Revision::getRevisionText
+ *
+ * @note direct use is deprecated, use getBlob() or SlotRecord::getContent() instead.
+ * @todo make this private, there should be no need to use this method outside this class.
+ *
+ * @param string $raw The raw blob data, to be processed according to $flags.
+ * May be the blob itself, or the blob compressed, or just the address
+ * of the actual blob, depending on $flags.
+ * @param string|string[] $flags Blob flags, such as 'external' or 'gzip'.
+ * Note that not including 'utf-8' in $flags will cause the data to be decoded
+ * according to the legacy encoding specified via setLegacyEncoding.
+ * @param string|null $cacheKey May be used for caching if given
+ *
+ * @return false|string The expanded blob or false on failure
+ */
+ public function expandBlob( $raw, $flags, $cacheKey = null ) {
+ if ( is_string( $flags ) ) {
+ $flags = explode( ',', $flags );
+ }
+
+ // Use external methods for external objects, text in table is URL-only then
+ if ( in_array( 'external', $flags ) ) {
+ $url = $raw;
+ $parts = explode( '://', $url, 2 );
+ if ( count( $parts ) == 1 || $parts[1] == '' ) {
+ return false;
+ }
+
+ if ( $cacheKey && $this->wikiId === false ) {
+ // Make use of the wiki-local revision text cache.
+ // The cached value should be decompressed, so handle that and return here.
+ // NOTE: we rely on $this->cache being the right cache for $this->wikiId!
+ return $this->cache->getWithSetCallback(
+ // TODO: change key, since this is not necessarily revision text!
+ $this->cache->makeKey( 'revisiontext', 'textid', $cacheKey ),
+ $this->getCacheTTL(),
+ function () use ( $url, $flags ) {
+ // No negative caching per BlobStore::getBlob()
+ $blob = ExternalStore::fetchFromURL( $url, [ 'wiki' => $this->wikiId ] );
+
+ return $this->decompressData( $blob, $flags );
+ },
+ [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => WANObjectCache::TTL_PROC_LONG ]
+ );
+ } else {
+ $blob = ExternalStore::fetchFromURL( $url, [ 'wiki' => $this->wikiId ] );
+ return $this->decompressData( $blob, $flags );
+ }
+ } else {
+ return $this->decompressData( $raw, $flags );
+ }
+ }
+
+ /**
+ * If $wgCompressRevisions is enabled, we will compress data.
+ * The input string is modified in place.
+ * Return value is the flags field: contains 'gzip' if the
+ * data is compressed, and 'utf-8' if we're saving in UTF-8
+ * mode.
+ *
+ * MCR migration note: this replaces Revision::compressRevisionText
+ *
+ * @note direct use is deprecated!
+ * @todo make this private, there should be no need to use this method outside this class.
+ *
+ * @param mixed &$blob Reference to a text
+ *
+ * @return string
+ */
+ public function compressData( &$blob ) {
+ $blobFlags = [];
+
+ // Revisions not marked as UTF-8 will have legacy decoding applied by decompressData().
+ // XXX: if $this->legacyEncoding is not set, we could skip this. That would however be
+ // risky, since $this->legacyEncoding being set in the future would lead to data corruption.
+ $blobFlags[] = 'utf-8';
+
+ if ( $this->compressBlobs ) {
+ if ( function_exists( 'gzdeflate' ) ) {
+ $deflated = gzdeflate( $blob );
+
+ if ( $deflated === false ) {
+ wfLogWarning( __METHOD__ . ': gzdeflate() failed' );
+ } else {
+ $blob = $deflated;
+ $blobFlags[] = 'gzip';
+ }
+ } else {
+ wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
+ }
+ }
+ return implode( ',', $blobFlags );
+ }
+
+ /**
+ * Re-converts revision text according to its flags.
+ *
+ * MCR migration note: this replaces Revision::decompressRevisionText
+ *
+ * @note direct use is deprecated, use getBlob() or SlotRecord::getContent() instead.
+ * @todo make this private, there should be no need to use this method outside this class.
+ *
+ * @param mixed $blob Reference to a text
+ * @param array $blobFlags Compression flags, such as 'gzip'.
+ * Note that not including 'utf-8' in $blobFlags will cause the data to be decoded
+ * according to the legacy encoding specified via setLegacyEncoding.
+ *
+ * @return string|bool Decompressed text, or false on failure
+ */
+ public function decompressData( $blob, array $blobFlags ) {
+ if ( $blob === false ) {
+ // Text failed to be fetched; nothing to do
+ return false;
+ }
+
+ if ( in_array( 'error', $blobFlags ) ) {
+ // Error row, return false
+ return false;
+ }
+
+ if ( in_array( 'gzip', $blobFlags ) ) {
+ # Deal with optional compression of archived pages.
+ # This can be done periodically via maintenance/compressOld.php, and
+ # as pages are saved if $wgCompressRevisions is set.
+ $blob = gzinflate( $blob );
+
+ if ( $blob === false ) {
+ wfLogWarning( __METHOD__ . ': gzinflate() failed' );
+ return false;
+ }
+ }
+
+ if ( in_array( 'object', $blobFlags ) ) {
+ # Generic compressed storage
+ $obj = unserialize( $blob );
+ if ( !is_object( $obj ) ) {
+ // Invalid object
+ return false;
+ }
+ $blob = $obj->getText();
+ }
+
+ // Needed to support old revisions left over from from the 1.4 / 1.5 migration.
+ if ( $blob !== false && $this->legacyEncoding && $this->legacyEncodingConversionLang
+ && !in_array( 'utf-8', $blobFlags ) && !in_array( 'utf8', $blobFlags )
+ ) {
+ # Old revisions kept around in a legacy encoding?
+ # Upconvert on demand.
+ # ("utf8" checked for compatibility with some broken
+ # conversion scripts 2008-12-30)
+ $blob = $this->legacyEncodingConversionLang->iconv( $this->legacyEncoding, 'UTF-8', $blob );
+ }
+
+ return $blob;
+ }
+
+ /**
+ * Get the text cache TTL
+ *
+ * MCR migration note: this replaces Revision::getCacheTTL
+ *
+ * @return int
+ */
+ private function getCacheTTL() {
+ if ( $this->cache->getQoS( WANObjectCache::ATTR_EMULATION )
+ <= WANObjectCache::QOS_EMULATION_SQL
+ ) {
+ // Do not cache RDBMs blobs in...the RDBMs store
+ $ttl = WANObjectCache::TTL_UNCACHEABLE;
+ } else {
+ $ttl = $this->cacheExpiry ?: WANObjectCache::TTL_UNCACHEABLE;
+ }
+
+ return $ttl;
+ }
+
+ /**
+ * Returns an ID corresponding to the old_id field in the text table, corresponding
+ * to the given $address.
+ *
+ * Currently, $address must start with 'tt:' followed by a decimal integer representing
+ * the old_id; if $address does not start with 'tt:', null is returned. However,
+ * the implementation may change to insert rows into the text table on the fly.
+ *
+ * @note This method exists for use with the text table based storage schema.
+ * It should not be assumed that is will function with all future kinds of content addresses.
+ *
+ * @deprecated since 1.31, so not assume that all blob addresses refer to a row in the text
+ * table. This method should become private once the relevant refactoring in WikiPage is
+ * complete.
+ *
+ * @param string $address
+ *
+ * @return int|null
+ */
+ public function getTextIdFromAddress( $address ) {
+ list( $schema, $id, ) = self::splitBlobAddress( $address );
+
+ if ( $schema !== 'tt' ) {
+ return null;
+ }
+
+ $textId = intval( $id );
+
+ if ( !$textId || $id !== (string)$textId ) {
+ throw new InvalidArgumentException( "Malformed text_id: $id" );
+ }
+
+ return $textId;
+ }
+
+ /**
+ * Splits a blob address into three parts: the schema, the ID, and parameters/flags.
+ *
+ * @param string $address
+ *
+ * @throws InvalidArgumentException
+ * @return array [ $schema, $id, $parameters ], with $parameters being an assoc array.
+ */
+ private static function splitBlobAddress( $address ) {
+ if ( !preg_match( '/^(\w+):(\w+)(\?(.*))?$/', $address, $m ) ) {
+ throw new InvalidArgumentException( "Bad blob address: $address" );
+ }
+
+ $schema = strtolower( $m[1] );
+ $id = $m[2];
+ $parameters = isset( $m[4] ) ? wfCgiToArray( $m[4] ) : [];
+
+ return [ $schema, $id, $parameters ];
+ }
+
+ public function isReadOnly() {
+ if ( $this->useExternalStore && ExternalStore::defaultStoresAreReadOnly() ) {
+ return true;
+ }
+
+ return ( $this->getDBLoadBalancer()->getReadOnlyReason() !== false );
+ }
+}
diff --git a/www/wiki/includes/Storage/SuppressedDataException.php b/www/wiki/includes/Storage/SuppressedDataException.php
new file mode 100644
index 00000000..24f16a64
--- /dev/null
+++ b/www/wiki/includes/Storage/SuppressedDataException.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+/**
+ * Exception raised in response to an audience check when attempting to
+ * access suppressed information without permission.
+ *
+ * @since 1.31
+ */
+class SuppressedDataException extends RevisionAccessException {
+
+}
diff --git a/www/wiki/includes/StreamFile.php b/www/wiki/includes/StreamFile.php
index 71113a86..2ad42e56 100644
--- a/www/wiki/includes/StreamFile.php
+++ b/www/wiki/includes/StreamFile.php
@@ -113,7 +113,7 @@ class StreamFile {
return 'unknown/unknown';
}
- $magic = MimeMagic::singleton();
+ $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
// Use the extension only, rather than magic numbers, to avoid opening
// up vulnerabilities due to uploads of files with allowed extensions
// but disallowed types.
diff --git a/www/wiki/includes/StubObject.php b/www/wiki/includes/StubObject.php
index baf51099..067f11f6 100644
--- a/www/wiki/includes/StubObject.php
+++ b/www/wiki/includes/StubObject.php
@@ -19,6 +19,7 @@
*
* @file
*/
+use Wikimedia\ObjectFactory;
/**
* Class to implement stub globals, which are globals that delay loading the
diff --git a/www/wiki/includes/Title.php b/www/wiki/includes/Title.php
index a4dc8c1f..ec9840a2 100644
--- a/www/wiki/includes/Title.php
+++ b/www/wiki/includes/Title.php
@@ -108,7 +108,12 @@ class Title implements LinkTarget {
/** @var array Array of groups allowed to edit this article */
public $mRestrictions = [];
- /** @var string|bool */
+ /**
+ * @var string|bool Comma-separated set of permission keys
+ * indicating who can move or edit the page from the page table, (pre 1.10) rows.
+ * Edit and move sections are separated by a colon
+ * Example: "edit=autoconfirmed,sysop:move=sysop"
+ */
protected $mOldRestrictions = false;
/** @var bool Cascade restrictions on this page to included templates and images? */
@@ -621,20 +626,6 @@ class Title implements LinkTarget {
}
/**
- * Returns a simple regex that will match on characters and sequences invalid in titles.
- * Note that this doesn't pick up many things that could be wrong with titles, but that
- * replacing this regex with something valid will make many titles valid.
- *
- * @deprecated since 1.25, use MediaWikiTitleCodec::getTitleInvalidRegex() instead
- *
- * @return string Regex string
- */
- static function getTitleInvalidRegex() {
- wfDeprecated( __METHOD__, '1.25' );
- return MediaWikiTitleCodec::getTitleInvalidRegex();
- }
-
- /**
* Utility method for converting a character sequence from bytes to Unicode.
*
* Primary usecase being converting $wgLegalTitleChars to a sequence usable in
@@ -776,6 +767,7 @@ class Title implements LinkTarget {
* @return string Escaped string
*/
static function escapeFragmentForURL( $fragment ) {
+ wfDeprecated( __METHOD__, '1.30' );
# Note that we don't urlencode the fragment. urlencoded Unicode
# fragments appear not to work in IE (at least up to 7) or in at least
# one version of Opera 9.x. The W3C validator, for one, doesn't seem
@@ -1035,12 +1027,11 @@ class Title implements LinkTarget {
*/
public function getNsText() {
if ( $this->isExternal() ) {
- // This probably shouldn't even happen,
- // but for interwiki transclusion it sometimes does.
- // Use the canonical namespaces if possible to try to
- // resolve a foreign namespace.
- if ( MWNamespace::exists( $this->mNamespace ) ) {
- return MWNamespace::getCanonicalName( $this->mNamespace );
+ // This probably shouldn't even happen, except for interwiki transclusion.
+ // If possible, use the canonical name for the foreign namespace.
+ $nsText = MWNamespace::getCanonicalName( $this->mNamespace );
+ if ( $nsText !== false ) {
+ return $nsText;
}
}
@@ -1286,71 +1277,153 @@ class Title implements LinkTarget {
}
/**
- * Could this page contain custom CSS or JavaScript for the global UI.
- * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
- * or CONTENT_MODEL_JAVASCRIPT.
+ * Could this MediaWiki namespace page contain custom CSS, JSON, or JavaScript for the
+ * global UI. This is generally true for pages in the MediaWiki namespace having
+ * CONTENT_MODEL_CSS, CONTENT_MODEL_JSON, or CONTENT_MODEL_JAVASCRIPT.
*
- * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage()
+ * This method does *not* return true for per-user JS/JSON/CSS. Use isUserConfigPage()
* for that!
*
- * Note that this method should not return true for pages that contain and
- * show "inactive" CSS or JS.
+ * Note that this method should not return true for pages that contain and show
+ * "inactive" CSS, JSON, or JS.
*
* @return bool
- * @todo FIXME: Rename to isSiteConfigPage() and remove deprecated hook
+ * @since 1.31
+ */
+ public function isSiteConfigPage() {
+ return (
+ NS_MEDIAWIKI == $this->mNamespace
+ && (
+ $this->hasContentModel( CONTENT_MODEL_CSS )
+ || $this->hasContentModel( CONTENT_MODEL_JSON )
+ || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
+ )
+ );
+ }
+
+ /**
+ * @return bool
+ * @deprecated Since 1.31; use ::isSiteConfigPage() instead (which also checks for JSON pages)
*/
public function isCssOrJsPage() {
- $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
- && ( $this->hasContentModel( CONTENT_MODEL_CSS )
- || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
+ wfDeprecated( __METHOD__, '1.31' );
+ return ( NS_MEDIAWIKI == $this->mNamespace
+ && ( $this->hasContentModel( CONTENT_MODEL_CSS )
+ || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
+ }
- return $isCssOrJsPage;
+ /**
+ * Is this a "config" (.css, .json, or .js) sub-page of a user page?
+ *
+ * @return bool
+ * @since 1.31
+ */
+ public function isUserConfigPage() {
+ return (
+ NS_USER == $this->mNamespace
+ && $this->isSubpage()
+ && (
+ $this->hasContentModel( CONTENT_MODEL_CSS )
+ || $this->hasContentModel( CONTENT_MODEL_JSON )
+ || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
+ )
+ );
}
/**
- * Is this a .css or .js subpage of a user page?
* @return bool
- * @todo FIXME: Rename to isUserConfigPage()
+ * @deprecated Since 1.31; use ::isUserConfigPage() instead (which also checks for JSON pages)
*/
public function isCssJsSubpage() {
+ wfDeprecated( __METHOD__, '1.31' );
return ( NS_USER == $this->mNamespace && $this->isSubpage()
&& ( $this->hasContentModel( CONTENT_MODEL_CSS )
|| $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
}
/**
- * Trim down a .css or .js subpage title to get the corresponding skin name
+ * Trim down a .css, .json, or .js subpage title to get the corresponding skin name
*
- * @return string Containing skin name from .css or .js subpage title
+ * @return string Containing skin name from .css, .json, or .js subpage title
+ * @since 1.31
*/
- public function getSkinFromCssJsSubpage() {
+ public function getSkinFromConfigSubpage() {
$subpage = explode( '/', $this->mTextform );
$subpage = $subpage[count( $subpage ) - 1];
$lastdot = strrpos( $subpage, '.' );
if ( $lastdot === false ) {
- return $subpage; # Never happens: only called for names ending in '.css' or '.js'
+ return $subpage; # Never happens: only called for names ending in '.css'/'.json'/'.js'
}
return substr( $subpage, 0, $lastdot );
}
/**
- * Is this a .css subpage of a user page?
+ * @deprecated Since 1.31; use ::getSkinFromConfigSubpage() instead
+ * @return string Containing skin name from .css, .json, or .js subpage title
+ */
+ public function getSkinFromCssJsSubpage() {
+ wfDeprecated( __METHOD__, '1.31' );
+ return $this->getSkinFromConfigSubpage();
+ }
+
+ /**
+ * Is this a CSS "config" sub-page of a user page?
*
* @return bool
+ * @since 1.31
+ */
+ public function isUserCssConfigPage() {
+ return (
+ NS_USER == $this->mNamespace
+ && $this->isSubpage()
+ && $this->hasContentModel( CONTENT_MODEL_CSS )
+ );
+ }
+
+ /**
+ * @deprecated Since 1.31; use ::isUserCssConfigPage()
+ * @return bool
*/
public function isCssSubpage() {
- return ( NS_USER == $this->mNamespace && $this->isSubpage()
- && $this->hasContentModel( CONTENT_MODEL_CSS ) );
+ wfDeprecated( __METHOD__, '1.31' );
+ return $this->isUserCssConfigPage();
+ }
+
+ /**
+ * Is this a JSON "config" sub-page of a user page?
+ *
+ * @return bool
+ * @since 1.31
+ */
+ public function isUserJsonConfigPage() {
+ return (
+ NS_USER == $this->mNamespace
+ && $this->isSubpage()
+ && $this->hasContentModel( CONTENT_MODEL_JSON )
+ );
}
/**
- * Is this a .js subpage of a user page?
+ * Is this a JS "config" sub-page of a user page?
*
* @return bool
+ * @since 1.31
+ */
+ public function isUserJsConfigPage() {
+ return (
+ NS_USER == $this->mNamespace
+ && $this->isSubpage()
+ && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
+ );
+ }
+
+ /**
+ * @deprecated Since 1.31; use ::isUserJsConfigPage()
+ * @return bool
*/
public function isJsSubpage() {
- return ( NS_USER == $this->mNamespace && $this->isSubpage()
- && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
+ wfDeprecated( __METHOD__, '1.31' );
+ return $this->isUserJsConfigPage();
}
/**
@@ -1463,7 +1536,9 @@ class Title implements LinkTarget {
public function getFragmentForURL() {
if ( !$this->hasFragment() ) {
return '';
- } elseif ( $this->isExternal() && !$this->getTransWikiID() ) {
+ } elseif ( $this->isExternal()
+ && !self::getInterwikiLookup()->fetch( $this->mInterwiki )->isLocal()
+ ) {
return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->getFragment() );
}
return '#' . Sanitizer::escapeIdForLink( $this->getFragment() );
@@ -1761,7 +1836,7 @@ class Title implements LinkTarget {
* @see wfExpandUrl
* @param string|string[] $query
* @param string|string[]|bool $query2
- * @param string $proto Protocol type to use in URL
+ * @param string|int|null $proto Protocol type to use in URL
* @return string The URL
*/
public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
@@ -1804,10 +1879,10 @@ class Title implements LinkTarget {
if ( $this->isExternal() ) {
$target = SpecialPage::getTitleFor(
'GoToInterwiki',
- $this->getPrefixedDBKey()
+ $this->getPrefixedDBkey()
);
}
- return $target->getFullUrl( $query, false, $proto );
+ return $target->getFullURL( $query, false, $proto );
}
/**
@@ -2242,7 +2317,7 @@ class Title implements LinkTarget {
}
/**
- * Check CSS/JS sub-page permissions
+ * Check CSS/JSON/JS sub-page permissions
*
* @param string $action The action to check
* @param User $user User to check
@@ -2252,20 +2327,43 @@ class Title implements LinkTarget {
*
* @return array List of errors
*/
- private function checkCSSandJSPermissions( $action, $user, $errors, $rigor, $short ) {
- # Protect css/js subpages of user pages
+ private function checkUserConfigPermissions( $action, $user, $errors, $rigor, $short ) {
+ # Protect css/json/js subpages of user pages
# XXX: this might be better using restrictions
+
if ( $action != 'patrol' ) {
if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
- if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) {
+ if (
+ $this->isUserCssConfigPage()
+ && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
+ ) {
$errors[] = [ 'mycustomcssprotected', $action ];
- } elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) {
+ } elseif (
+ $this->isUserJsonConfigPage()
+ && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
+ ) {
+ $errors[] = [ 'mycustomjsonprotected', $action ];
+ } elseif (
+ $this->isUserJsConfigPage()
+ && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
+ ) {
$errors[] = [ 'mycustomjsprotected', $action ];
}
} else {
- if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
+ if (
+ $this->isUserCssConfigPage()
+ && !$user->isAllowed( 'editusercss' )
+ ) {
$errors[] = [ 'customcssprotected', $action ];
- } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
+ } elseif (
+ $this->isUserJsonConfigPage()
+ && !$user->isAllowed( 'edituserjson' )
+ ) {
+ $errors[] = [ 'customjsonprotected', $action ];
+ } elseif (
+ $this->isUserJsConfigPage()
+ && !$user->isAllowed( 'edituserjs' )
+ ) {
$errors[] = [ 'customjsprotected', $action ];
}
}
@@ -2322,7 +2420,7 @@ class Title implements LinkTarget {
* @return array List of errors
*/
private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
- if ( $rigor !== 'quick' && !$this->isCssJsSubpage() ) {
+ if ( $rigor !== 'quick' && !$this->isUserConfigPage() ) {
# We /could/ use the protection level on the source page, but it's
# fairly ugly as we have to establish a precedence hierarchy for pages
# included by multiple cascade-protected pages. So just restrict
@@ -2606,7 +2704,7 @@ class Title implements LinkTarget {
'checkReadPermissions',
'checkUserBlock', // for wgBlockDisablesLogin
];
- # Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions
+ # Don't call checkSpecialsAndNSPermissions or checkUserConfigPermissions
# here as it will lead to duplicate error messages. This is okay to do
# since anywhere that checks for create will also check for edit, and
# those checks are called for edit.
@@ -2624,7 +2722,7 @@ class Title implements LinkTarget {
'checkQuickPermissions',
'checkPermissionHooks',
'checkSpecialsAndNSPermissions',
- 'checkCSSandJSPermissions',
+ 'checkUserConfigPermissions',
'checkPageRestrictions',
'checkCascadingSourcesRestrictions',
'checkActionPermissions',
@@ -2730,8 +2828,8 @@ class Title implements LinkTarget {
if ( $this->mTitleProtection === null ) {
$dbr = wfGetDB( DB_REPLICA );
- $commentStore = new CommentStore( 'pt_reason' );
- $commentQuery = $commentStore->getJoin();
+ $commentStore = CommentStore::getStore();
+ $commentQuery = $commentStore->getJoin( 'pt_reason' );
$res = $dbr->select(
[ 'protected_titles' ] + $commentQuery['tables'],
[
@@ -2752,7 +2850,7 @@ class Title implements LinkTarget {
'user' => $row['user'],
'expiry' => $dbr->decodeExpiry( $row['expiry'] ),
'permission' => $row['permission'],
- 'reason' => $commentStore->getComment( $row )->text,
+ 'reason' => $commentStore->getComment( 'pt_reason', $row )->text,
];
} else {
$this->mTitleProtection = false;
@@ -3046,8 +3144,10 @@ class Title implements LinkTarget {
* Public for usage by LiquidThreads.
*
* @param array $rows Array of db result objects
- * @param string $oldFashionedRestrictions Comma-separated list of page
- * restrictions from page table (pre 1.10)
+ * @param string $oldFashionedRestrictions Comma-separated set of permission keys
+ * indicating who can move or edit the page from the page table, (pre 1.10) rows.
+ * Edit and move sections are separated by a colon
+ * Example: "edit=autoconfirmed,sysop:move=sysop"
*/
public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
$dbr = wfGetDB( DB_REPLICA );
@@ -3116,8 +3216,10 @@ class Title implements LinkTarget {
/**
* Load restrictions from the page_restrictions table
*
- * @param string $oldFashionedRestrictions Comma-separated list of page
- * restrictions from page table (pre 1.10)
+ * @param string $oldFashionedRestrictions Comma-separated set of permission keys
+ * indicating who can move or edit the page from the page table, (pre 1.10) rows.
+ * Edit and move sections are separated by a colon
+ * Example: "edit=autoconfirmed,sysop:move=sysop"
*/
public function loadRestrictions( $oldFashionedRestrictions = null ) {
if ( $this->mRestrictionsLoaded ) {
@@ -3630,19 +3732,20 @@ class Title implements LinkTarget {
$blNamespace = "{$prefix}_namespace";
$blTitle = "{$prefix}_title";
+ $pageQuery = WikiPage::getQueryInfo();
$res = $db->select(
- [ $table, 'page' ],
+ [ $table, 'nestpage' => $pageQuery['tables'] ],
array_merge(
[ $blNamespace, $blTitle ],
- WikiPage::selectFields()
+ $pageQuery['fields']
),
[ "{$prefix}_from" => $id ],
__METHOD__,
$options,
- [ 'page' => [
+ [ 'nestpage' => [
'LEFT JOIN',
[ "page_namespace=$blNamespace", "page_title=$blTitle" ]
- ] ]
+ ] ] + $pageQuery['joins']
);
$retVal = [];
@@ -3733,9 +3836,11 @@ class Title implements LinkTarget {
}
// If we are looking at a css/js user subpage, purge the action=raw.
- if ( $this->isJsSubpage() ) {
+ if ( $this->isUserJsConfigPage() ) {
$urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' );
- } elseif ( $this->isCssSubpage() ) {
+ } elseif ( $this->isUserJsonConfigPage() ) {
+ $urls[] = $this->getInternalURL( 'action=raw&ctype=application/json' );
+ } elseif ( $this->isUserCssConfigPage() ) {
$urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' );
}
@@ -4195,13 +4300,15 @@ class Title implements LinkTarget {
$pageId = $this->getArticleID( $flags );
if ( $pageId ) {
$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
- $row = $db->selectRow( 'revision', Revision::selectFields(),
+ $revQuery = Revision::getQueryInfo();
+ $row = $db->selectRow( $revQuery['tables'], $revQuery['fields'],
[ 'rev_page' => $pageId ],
__METHOD__,
[
'ORDER BY' => 'rev_timestamp ASC, rev_id ASC',
- 'IGNORE INDEX' => 'rev_timestamp', // See T159319
- ]
+ 'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
+ ],
+ $revQuery['joins']
);
if ( $row ) {
return new Revision( $row );
@@ -4377,17 +4484,18 @@ class Title implements LinkTarget {
return $authors;
}
$dbr = wfGetDB( DB_REPLICA );
- $res = $dbr->select( 'revision', 'DISTINCT rev_user_text',
+ $revQuery = Revision::getQueryInfo();
+ $authors = $dbr->selectFieldValues(
+ $revQuery['tables'],
+ $revQuery['fields']['rev_user_text'],
[
'rev_page' => $this->getArticleID(),
"rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
"rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
], __METHOD__,
- [ 'LIMIT' => $limit + 1 ] // add one so caller knows it was truncated
+ [ 'DISTINCT', 'LIMIT' => $limit + 1 ], // add one so caller knows it was truncated
+ $revQuery['joins']
);
- foreach ( $res as $row ) {
- $authors[] = $row->rev_user_text;
- }
return $authors;
}
@@ -4488,7 +4596,7 @@ class Title implements LinkTarget {
}
if ( $this->isExternal() ) {
- return true; // any interwiki link might be viewable, for all we know
+ return true; // any interwiki link might be viewable, for all we know
}
switch ( $this->mNamespace ) {
@@ -4621,16 +4729,18 @@ class Title implements LinkTarget {
* on the number of links. Typically called on create and delete.
*/
public function touchLinks() {
- DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks' ) );
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks', 'page-touch' ) );
if ( $this->getNamespace() == NS_CATEGORY ) {
- DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'categorylinks' ) );
+ DeferredUpdates::addUpdate(
+ new HTMLCacheUpdate( $this, 'categorylinks', 'category-touch' )
+ );
}
}
/**
* Get the last touched timestamp
*
- * @param IDatabase $db Optional db
+ * @param IDatabase|null $db
* @return string|false Last-touched timestamp
*/
public function getTouched( $db = null ) {
@@ -4687,14 +4797,12 @@ class Title implements LinkTarget {
*/
public function getNamespaceKey( $prepend = 'nstab-' ) {
global $wgContLang;
- // Gets the subject namespace if this title
- $namespace = MWNamespace::getSubject( $this->getNamespace() );
- // Checks if canonical namespace name exists for namespace
- if ( MWNamespace::exists( $this->getNamespace() ) ) {
- // Uses canonical namespace name
- $namespaceKey = MWNamespace::getCanonicalName( $namespace );
- } else {
- // Uses text of namespace
+ // Gets the subject namespace of this title
+ $subjectNS = MWNamespace::getSubject( $this->getNamespace() );
+ // Prefer canonical namespace name for HTML IDs
+ $namespaceKey = MWNamespace::getCanonicalName( $subjectNS );
+ if ( $namespaceKey === false ) {
+ // Fallback to localised text
$namespaceKey = $this->getSubjectNsText();
}
// Makes namespace key lowercase
diff --git a/www/wiki/includes/TitleArray.php b/www/wiki/includes/TitleArray.php
index bf2344bb..a1eabe5b 100644
--- a/www/wiki/includes/TitleArray.php
+++ b/www/wiki/includes/TitleArray.php
@@ -24,7 +24,7 @@
* @file
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
/**
* The TitleArray class only exists to provide the newFromResult method at pre-
@@ -32,7 +32,7 @@ use Wikimedia\Rdbms\ResultWrapper;
*/
abstract class TitleArray implements Iterator {
/**
- * @param ResultWrapper $res A SQL result including at least page_namespace and
+ * @param IResultWrapper $res A SQL result including at least page_namespace and
* page_title -- also can have page_id, page_len, page_is_redirect,
* page_latest (if those will be used). See Title::newFromRow.
* @return TitleArrayFromResult
@@ -49,7 +49,7 @@ abstract class TitleArray implements Iterator {
}
/**
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
* @return TitleArrayFromResult
*/
protected static function newFromResult_internal( $res ) {
diff --git a/www/wiki/includes/TitleArrayFromResult.php b/www/wiki/includes/TitleArrayFromResult.php
index 189fb405..ee60f7b9 100644
--- a/www/wiki/includes/TitleArrayFromResult.php
+++ b/www/wiki/includes/TitleArrayFromResult.php
@@ -24,10 +24,10 @@
* @file
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
class TitleArrayFromResult extends TitleArray implements Countable {
- /** @var ResultWrapper */
+ /** @var IResultWrapper */
public $res;
public $key;
@@ -41,7 +41,7 @@ class TitleArrayFromResult extends TitleArray implements Countable {
}
/**
- * @param bool|ResultWrapper $row
+ * @param bool|IResultWrapper $row
* @return void
*/
protected function setCurrent( $row ) {
diff --git a/www/wiki/includes/WatchedItem.php b/www/wiki/includes/WatchedItem.php
deleted file mode 100644
index bfd1d613..00000000
--- a/www/wiki/includes/WatchedItem.php
+++ /dev/null
@@ -1,200 +0,0 @@
-<?php
-/**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Watchlist
- */
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Linker\LinkTarget;
-
-/**
- * Representation of a pair of user and title for watchlist entries.
- *
- * @author Tim Starling
- * @author Addshore
- *
- * @ingroup Watchlist
- */
-class WatchedItem {
-
- /**
- * @deprecated since 1.27, see User::IGNORE_USER_RIGHTS
- */
- const IGNORE_USER_RIGHTS = User::IGNORE_USER_RIGHTS;
-
- /**
- * @deprecated since 1.27, see User::CHECK_USER_RIGHTS
- */
- const CHECK_USER_RIGHTS = User::CHECK_USER_RIGHTS;
-
- /**
- * @deprecated Internal class use only
- */
- const DEPRECATED_USAGE_TIMESTAMP = -100;
-
- /**
- * @var bool
- * @deprecated Internal class use only
- */
- public $checkRights = User::CHECK_USER_RIGHTS;
-
- /**
- * @var Title
- * @deprecated Internal class use only
- */
- private $title;
-
- /**
- * @var LinkTarget
- */
- private $linkTarget;
-
- /**
- * @var User
- */
- private $user;
-
- /**
- * @var null|string the value of the wl_notificationtimestamp field
- */
- private $notificationTimestamp;
-
- /**
- * @param User $user
- * @param LinkTarget $linkTarget
- * @param null|string $notificationTimestamp the value of the wl_notificationtimestamp field
- * @param bool|null $checkRights DO NOT USE - used internally for backward compatibility
- */
- public function __construct(
- User $user,
- LinkTarget $linkTarget,
- $notificationTimestamp,
- $checkRights = null
- ) {
- $this->user = $user;
- $this->linkTarget = $linkTarget;
- $this->notificationTimestamp = $notificationTimestamp;
- if ( $checkRights !== null ) {
- $this->checkRights = $checkRights;
- }
- }
-
- /**
- * @return User
- */
- public function getUser() {
- return $this->user;
- }
-
- /**
- * @return LinkTarget
- */
- public function getLinkTarget() {
- return $this->linkTarget;
- }
-
- /**
- * Get the notification timestamp of this entry.
- *
- * @return bool|null|string
- */
- public function getNotificationTimestamp() {
- // Back compat for objects constructed using self::fromUserTitle
- if ( $this->notificationTimestamp === self::DEPRECATED_USAGE_TIMESTAMP ) {
- // wfDeprecated( __METHOD__, '1.27' );
- if ( $this->checkRights && !$this->user->isAllowed( 'viewmywatchlist' ) ) {
- return false;
- }
- $item = MediaWikiServices::getInstance()->getWatchedItemStore()
- ->loadWatchedItem( $this->user, $this->linkTarget );
- if ( $item ) {
- $this->notificationTimestamp = $item->getNotificationTimestamp();
- } else {
- $this->notificationTimestamp = false;
- }
- }
- return $this->notificationTimestamp;
- }
-
- /**
- * Back compat pre 1.27 with the WatchedItemStore introduction
- * @todo remove in 1.28/9
- * -------------------------------------------------
- */
-
- /**
- * @return Title
- * @deprecated Internal class use only
- */
- public function getTitle() {
- if ( !$this->title ) {
- $this->title = Title::newFromLinkTarget( $this->linkTarget );
- }
- return $this->title;
- }
-
- /**
- * @deprecated since 1.27 Use the constructor, WatchedItemStore::getWatchedItem()
- * or WatchedItemStore::loadWatchedItem()
- */
- public static function fromUserTitle( $user, $title, $checkRights = User::CHECK_USER_RIGHTS ) {
- wfDeprecated( __METHOD__, '1.27' );
- return new self( $user, $title, self::DEPRECATED_USAGE_TIMESTAMP, (bool)$checkRights );
- }
-
- /**
- * @deprecated since 1.27 Use User::addWatch()
- * @return bool
- */
- public function addWatch() {
- wfDeprecated( __METHOD__, '1.27' );
- $this->user->addWatch( $this->getTitle(), $this->checkRights );
- return true;
- }
-
- /**
- * @deprecated since 1.27 Use User::removeWatch()
- * @return bool
- */
- public function removeWatch() {
- wfDeprecated( __METHOD__, '1.27' );
- if ( $this->checkRights && !$this->user->isAllowed( 'editmywatchlist' ) ) {
- return false;
- }
- $this->user->removeWatch( $this->getTitle(), $this->checkRights );
- return true;
- }
-
- /**
- * @deprecated since 1.27 Use User::isWatched()
- * @return bool
- */
- public function isWatched() {
- wfDeprecated( __METHOD__, '1.27' );
- return $this->user->isWatched( $this->getTitle(), $this->checkRights );
- }
-
- /**
- * @deprecated since 1.27 Use WatchedItemStore::duplicateAllAssociatedEntries()
- */
- public static function duplicateEntries( Title $oldTitle, Title $newTitle ) {
- wfDeprecated( __METHOD__, '1.27' );
- $store = MediaWikiServices::getInstance()->getWatchedItemStore();
- $store->duplicateAllAssociatedEntries( $oldTitle, $newTitle );
- }
-
-}
diff --git a/www/wiki/includes/WebRequest.php b/www/wiki/includes/WebRequest.php
index 3d5e372c..c6ddf816 100644
--- a/www/wiki/includes/WebRequest.php
+++ b/www/wiki/includes/WebRequest.php
@@ -88,8 +88,7 @@ class WebRequest {
* @codeCoverageIgnore
*/
public function __construct() {
- $this->requestTime = isset( $_SERVER['REQUEST_TIME_FLOAT'] )
- ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime( true );
+ $this->requestTime = $_SERVER['REQUEST_TIME_FLOAT'];
// POST overrides GET data
// We don't use $_REQUEST here to avoid interference from cookies...
@@ -123,9 +122,9 @@ class WebRequest {
if ( !preg_match( '!^https?://!', $url ) ) {
$url = 'http://unused' . $url;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$a = parse_url( $url );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $a ) {
$path = isset( $a['path'] ) ? $a['path'] : '';
@@ -142,7 +141,7 @@ class WebRequest {
$router->add( "$wgScript/$1" );
if ( isset( $_SERVER['SCRIPT_NAME'] )
- && preg_match( '/\.php5?/', $_SERVER['SCRIPT_NAME'] )
+ && preg_match( '/\.php/', $_SERVER['SCRIPT_NAME'] )
) {
# Check for SCRIPT_NAME, we handle index.php explicitly
# But we do have some other .php files such as img_auth.php
@@ -270,6 +269,8 @@ class WebRequest {
* @since 1.27
*/
public static function getRequestId() {
+ // This method is called from various error handlers and should be kept simple.
+
if ( !self::$reqId ) {
self::$reqId = isset( $_SERVER['UNIQUE_ID'] )
? $_SERVER['UNIQUE_ID'] : wfRandomString( 24 );
@@ -407,7 +408,7 @@ class WebRequest {
*
* @since 1.28
* @param string $name
- * @param string|null $default Optional default
+ * @param string|null $default
* @return string|null
*/
public function getRawVal( $name, $default = null ) {
@@ -431,7 +432,7 @@ class WebRequest {
* selected by a drop-down menu). For freeform input, see getText().
*
* @param string $name
- * @param string $default Optional default (or null)
+ * @param string|null $default Optional default (or null)
* @return string|null
*/
public function getVal( $name, $default = null ) {
@@ -781,6 +782,8 @@ class WebRequest {
* @return string
*/
public static function getGlobalRequestURL() {
+ // This method is called on fatal errors; it should not depend on anything complex.
+
if ( isset( $_SERVER['REQUEST_URI'] ) && strlen( $_SERVER['REQUEST_URI'] ) ) {
$base = $_SERVER['REQUEST_URI'];
} elseif ( isset( $_SERVER['HTTP_X_ORIGINAL_URL'] )
@@ -956,7 +959,7 @@ class WebRequest {
public function response() {
/* Lazy initialization of response object for this request */
if ( !is_object( $this->response ) ) {
- $class = ( $this instanceof FauxRequest ) ? 'FauxResponse' : 'WebResponse';
+ $class = ( $this instanceof FauxRequest ) ? FauxResponse::class : WebResponse::class;
$this->response = new $class();
}
return $this->response;
diff --git a/www/wiki/includes/WebStart.php b/www/wiki/includes/WebStart.php
index 8a58e6f0..6f3aa716 100644
--- a/www/wiki/includes/WebStart.php
+++ b/www/wiki/includes/WebStart.php
@@ -41,8 +41,6 @@ header( 'X-Content-Type-Options: nosniff' );
*/
$wgRequestTime = $_SERVER['REQUEST_TIME_FLOAT'];
-unset( $IP );
-
# Valid web server entry point, enable includes.
# Please don't move this line to includes/Defines.php. This line essentially
# defines a valid entry point. If you put it in includes/Defines.php, then
@@ -50,65 +48,41 @@ unset( $IP );
# its purpose.
define( 'MEDIAWIKI', true );
-# Full path to working directory.
-# Makes it possible to for example to have effective exclude path in apc.
-# __DIR__ breaks symlinked includes, but realpath() returns false
-# if we don't have permissions on parent directories.
+# Full path to the installation directory.
$IP = getenv( 'MW_INSTALL_PATH' );
if ( $IP === false ) {
- $IP = realpath( '.' ) ?: dirname( __DIR__ );
-}
-
-require_once "$IP/includes/PreConfigSetup.php";
-
-# Assert that composer dependencies were successfully loaded
-# Purposely no leading \ due to it breaking HHVM RepoAuthorative mode
-# PHP works fine with both versions
-# See https://github.com/facebook/hhvm/issues/5833
-if ( !interface_exists( 'Psr\Log\LoggerInterface' ) ) {
- $message = (
- 'MediaWiki requires the <a href="https://github.com/php-fig/log">PSR-3 logging ' .
- "library</a> to be present. This library is not embedded directly in MediaWiki's " .
- "git repository and must be installed separately by the end user.\n\n" .
- 'Please see <a href="https://www.mediawiki.org/wiki/Download_from_Git' .
- '#Fetch_external_libraries">mediawiki.org</a> for help on installing ' .
- 'the required components.'
- );
- echo $message;
- trigger_error( $message, E_USER_ERROR );
- die( 1 );
+ $IP = dirname( __DIR__ );
}
-# Install a header callback
-MediaWiki\HeaderCallback::register();
-
-if ( defined( 'MW_CONFIG_CALLBACK' ) ) {
- # Use a callback function to configure MediaWiki
- call_user_func( MW_CONFIG_CALLBACK );
-} else {
+// If no LocalSettings file exists, try to display an error page
+// (use a callback because it depends on TemplateParser)
+if ( !defined( 'MW_CONFIG_CALLBACK' ) ) {
if ( !defined( 'MW_CONFIG_FILE' ) ) {
define( 'MW_CONFIG_FILE', "$IP/LocalSettings.php" );
}
-
- # LocalSettings.php is the per site customization file. If it does not exist
- # the wiki installer needs to be launched or the generated file uploaded to
- # the root wiki directory. Give a hint, if it is not readable by the server.
if ( !is_readable( MW_CONFIG_FILE ) ) {
- require_once "$IP/includes/NoLocalSettings.php";
- die();
+ function wfWebStartNoLocalSettings() {
+ # LocalSettings.php is the per-site customization file. If it does not exist
+ # the wiki installer needs to be launched or the generated file uploaded to
+ # the root wiki directory. Give a hint, if it is not readable by the server.
+ global $IP;
+ require_once "$IP/includes/NoLocalSettings.php";
+ die();
+ }
+ define( 'MW_CONFIG_CALLBACK', 'wfWebStartNoLocalSettings' );
}
-
- # Include site settings. $IP may be changed (hopefully before the AutoLoader is invoked)
- require_once MW_CONFIG_FILE;
}
-# Initialise output buffering
-# Check that there is no previous output or previously set up buffers, because
-# that would cause us to potentially mix gzip and non-gzip output, creating a
-# big mess.
-if ( ob_get_level() == 0 ) {
- require_once "$IP/includes/OutputHandler.php";
- ob_start( 'wfOutputHandler' );
+// Custom setup for WebStart entry point
+if ( !defined( 'MW_SETUP_CALLBACK' ) ) {
+ function wfWebStartSetup() {
+ // Initialise output buffering
+ // Check for previously set up buffers, to avoid a mix of gzip and non-gzip output.
+ if ( ob_get_level() == 0 ) {
+ ob_start( 'MediaWiki\\OutputHandler::handle' );
+ }
+ }
+ define( 'MW_SETUP_CALLBACK', 'wfWebStartSetup' );
}
require_once "$IP/includes/Setup.php";
diff --git a/www/wiki/includes/WikiMap.php b/www/wiki/includes/WikiMap.php
index 8bb37b5c..9cc308d2 100644
--- a/www/wiki/includes/WikiMap.php
+++ b/www/wiki/includes/WikiMap.php
@@ -258,4 +258,37 @@ class WikiMap {
? "{$domain->getDatabase()}-{$domain->getTablePrefix()}"
: $domain->getDatabase();
}
+
+ /**
+ * @param DatabaseDomain|string $domain
+ * @return bool Whether $domain has the same DB/prefix as the current wiki
+ * @since 1.33
+ */
+ public static function isCurrentWikiDbDomain( $domain ) {
+ $domain = DatabaseDomain::newFromId( $domain );
+ $curDomain = self::getCurrentWikiDbDomain();
+
+ if ( !in_array( $curDomain->getSchema(), [ null, 'mediawiki' ], true ) ) {
+ // Include the schema if it is set and is not the default placeholder.
+ // This means a site admin may have specifically taylored the schemas.
+ // Domain IDs might use the form <DB>-<project>-<language>, meaning that
+ // the schema portion must be accounted for to disambiguate wikis.
+ return $curDomain->equals( $domain );
+ }
+
+ return (
+ $curDomain->getDatabase() === $domain->getDatabase() &&
+ $curDomain->getTablePrefix() === $domain->getTablePrefix()
+ );
+ }
+
+ /**
+ * @return DatabaseDomain Database domain of the current wiki
+ * @since 1.33
+ */
+ public static function getCurrentWikiDbDomain() {
+ global $wgDBname, $wgDBmwschema, $wgDBprefix;
+ // Avoid invoking LBFactory to avoid any chance of recursion
+ return new DatabaseDomain( $wgDBname, $wgDBmwschema, (string)$wgDBprefix );
+ }
}
diff --git a/www/wiki/includes/Xml.php b/www/wiki/includes/Xml.php
index 00915131..7dcd4a43 100644
--- a/www/wiki/includes/Xml.php
+++ b/www/wiki/includes/Xml.php
@@ -160,8 +160,9 @@ class Xml {
}
/**
- * @param int $year
- * @param int $month
+ * @param int|string $year Use '' or 0 to start with no year preselected.
+ * @param int|string $month A month in the 1..12 range. Use '', 0 or -1 to start with no month
+ * preselected.
* @return string Formatted HTML
*/
public static function dateMenu( $year, $month ) {
@@ -532,8 +533,8 @@ class Xml {
*
* @param string $list Correctly formatted text (newline delimited) to be
* used to generate the options.
- * @param array $params Extra parameters
- * @param string $params['other'] If set, add an option with this as text and a value of 'other'
+ * @param array $params Extra parameters:
+ * - string $params['other'] If set, add an option with this as text and a value of 'other'
* @return array Array keys are textual labels, values are internal values
*/
public static function listDropDownOptions( $list, $params = [] ) {
diff --git a/www/wiki/includes/actions/CreditsAction.php b/www/wiki/includes/actions/CreditsAction.php
index 70254778..ed58686a 100644
--- a/www/wiki/includes/actions/CreditsAction.php
+++ b/www/wiki/includes/actions/CreditsAction.php
@@ -198,6 +198,9 @@ class CreditsAction extends FormlessAction {
protected function link( User $user ) {
if ( $this->canShowRealUserName() && !$user->isAnon() ) {
$real = $user->getRealName();
+ if ( $real === '' ) {
+ $real = $user->getName();
+ }
} else {
$real = $user->getName();
}
diff --git a/www/wiki/includes/actions/HistoryAction.php b/www/wiki/includes/actions/HistoryAction.php
index fe848524..f7ac21b7 100644
--- a/www/wiki/includes/actions/HistoryAction.php
+++ b/www/wiki/includes/actions/HistoryAction.php
@@ -154,7 +154,7 @@ class HistoryAction extends FormlessAction {
# show deletion/move log if there is an entry
LogEventsList::showLogExtract(
$out,
- [ 'delete', 'move' ],
+ [ 'delete', 'move', 'protect' ],
$this->getTitle(),
'',
[ 'lim' => 10,
@@ -258,12 +258,18 @@ class HistoryAction extends FormlessAction {
$page_id = $this->page->getId();
- return $dbr->select( 'revision',
- Revision::selectFields(),
+ $revQuery = Revision::getQueryInfo();
+ return $dbr->select(
+ $revQuery['tables'],
+ $revQuery['fields'],
array_merge( [ 'rev_page' => $page_id ], $offsets ),
__METHOD__,
- [ 'ORDER BY' => "rev_timestamp $dirs",
- 'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit ]
+ [
+ 'ORDER BY' => "rev_timestamp $dirs",
+ 'USE INDEX' => [ 'revision' => 'page_timestamp' ],
+ 'LIMIT' => $limit
+ ],
+ $revQuery['joins']
);
}
@@ -329,8 +335,8 @@ class HistoryAction extends FormlessAction {
* @return FeedItem
*/
function feedItem( $row ) {
- $rev = new Revision( $row );
- $rev->setTitle( $this->getTitle() );
+ $rev = new Revision( $row, 0, $this->getTitle() );
+
$text = FeedUtils::formatDiffRow(
$this->getTitle(),
$this->getTitle()->getPreviousRevisionID( $rev->getId() ),
@@ -385,6 +391,9 @@ class HistoryPager extends ReverseChronologicalPager {
/** @var bool Whether to show the tag editing UI */
protected $showTagEditUI;
+ /** @var string */
+ private $tagFilter;
+
/**
* @param HistoryAction $historyPage
* @param string $year
@@ -415,14 +424,15 @@ class HistoryPager extends ReverseChronologicalPager {
}
function getQueryInfo() {
+ $revQuery = Revision::getQueryInfo( [ 'user' ] );
$queryInfo = [
- 'tables' => [ 'revision', 'user' ],
- 'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ),
+ 'tables' => $revQuery['tables'],
+ 'fields' => $revQuery['fields'],
'conds' => array_merge(
[ 'rev_page' => $this->getWikiPage()->getId() ],
$this->conds ),
'options' => [ 'USE INDEX' => [ 'revision' => 'page_timestamp' ] ],
- 'join_conds' => [ 'user' => Revision::userJoinCond() ],
+ 'join_conds' => $revQuery['joins'],
];
ChangeTags::modifyDisplayQuery(
$queryInfo['tables'],
@@ -601,7 +611,7 @@ class HistoryPager extends ReverseChronologicalPager {
* Creates a submit button
*
* @param string $message Text of the submit button, will be escaped
- * @param array $attributes Attributes
+ * @param array $attributes
* @return string HTML output for the submit button
*/
function submitButton( $message, $attributes = [] ) {
@@ -629,12 +639,10 @@ class HistoryPager extends ReverseChronologicalPager {
*/
function historyLine( $row, $next, $notificationtimestamp = false,
$latest = false, $firstInList = false ) {
- $rev = new Revision( $row );
- $rev->setTitle( $this->getTitle() );
+ $rev = new Revision( $row, 0, $this->getTitle() );
if ( is_object( $next ) ) {
- $prevRev = new Revision( $next );
- $prevRev->setTitle( $this->getTitle() );
+ $prevRev = new Revision( $next, 0, $this->getTitle() );
} else {
$prevRev = null;
}
diff --git a/www/wiki/includes/actions/InfoAction.php b/www/wiki/includes/actions/InfoAction.php
index c5cd89f1..0988f734 100644
--- a/www/wiki/includes/actions/InfoAction.php
+++ b/www/wiki/includes/actions/InfoAction.php
@@ -128,7 +128,7 @@ class InfoAction extends FormlessAction {
// pageinfo-header-basic, pageinfo-header-edits, pageinfo-header-restrictions,
// pageinfo-header-properties, pageinfo-category-info
$content .= $this->makeHeader(
- $this->msg( "pageinfo-${header}" )->escaped(),
+ $this->msg( "pageinfo-${header}" )->text(),
"mw-pageinfo-${header}"
) . "\n";
$table = "\n";
@@ -186,7 +186,7 @@ class InfoAction extends FormlessAction {
* Adds a table to the content that will be added to the output.
*
* @param string $content The content that will be added to the output
- * @param string $table The table
+ * @param string $table
* @return string The content with the table added
*/
protected function addTable( $content, $table ) {
@@ -437,6 +437,19 @@ class InfoAction extends FormlessAction {
];
}
+ // Display image SHA-1 value
+ if ( $title->inNamespace( NS_FILE ) ) {
+ $fileObj = wfFindFile( $title );
+ if ( $fileObj !== false ) {
+ // Convert the base-36 sha1 value obtained from database to base-16
+ $output = Wikimedia\base_convert( $fileObj->getSha1(), 36, 16, 40 );
+ $pageInfo['header-basic'][] = [
+ $this->msg( 'pageinfo-file-hash' ),
+ $output
+ ];
+ }
+ }
+
// Page protection
$pageInfo['header-restrictions'] = [];
@@ -705,6 +718,8 @@ class InfoAction extends FormlessAction {
self::getCacheKey( $cache, $page->getTitle(), $page->getLatest() ),
WANObjectCache::TTL_WEEK,
function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname ) {
+ global $wgActorTableSchemaMigrationStage;
+
$title = $page->getTitle();
$id = $title->getArticleID();
@@ -712,6 +727,29 @@ class InfoAction extends FormlessAction {
$dbrWatchlist = wfGetDB( DB_REPLICA, 'watchlist' );
$setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
+ if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+ $tables = [ 'revision_actor_temp' ];
+ $field = 'revactor_actor';
+ $pageField = 'revactor_page';
+ $tsField = 'revactor_timestamp';
+ $joins = [];
+ } elseif ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
+ $tables = [ 'revision' ];
+ $field = 'rev_user_text';
+ $pageField = 'rev_page';
+ $tsField = 'rev_timestamp';
+ $joins = [];
+ } else {
+ $tables = [ 'revision', 'revision_actor_temp', 'actor' ];
+ $field = 'COALESCE( actor_name, rev_user_text)';
+ $pageField = 'rev_page';
+ $tsField = 'rev_timestamp';
+ $joins = [
+ 'revision_actor_temp' => [ 'LEFT JOIN', 'revactor_rev = rev_id' ],
+ 'actor' => [ 'LEFT JOIN', 'revactor_actor = actor_id' ],
+ ];
+ }
+
$watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
$result = [];
@@ -739,10 +777,12 @@ class InfoAction extends FormlessAction {
$result['authors'] = 0;
} else {
$result['authors'] = (int)$dbr->selectField(
- 'revision',
- 'COUNT(DISTINCT rev_user_text)',
- [ 'rev_page' => $id ],
- $fname
+ $tables,
+ "COUNT(DISTINCT $field)",
+ [ $pageField => $id ],
+ $fname,
+ [],
+ $joins
);
}
@@ -763,13 +803,15 @@ class InfoAction extends FormlessAction {
// Recent number of distinct authors
$result['recent_authors'] = (int)$dbr->selectField(
- 'revision',
- 'COUNT(DISTINCT rev_user_text)',
+ $tables,
+ "COUNT(DISTINCT $field)",
[
- 'rev_page' => $id,
- "rev_timestamp >= " . $dbr->addQuotes( $threshold )
+ $pageField => $id,
+ "$tsField >= " . $dbr->addQuotes( $threshold )
],
- $fname
+ $fname,
+ [],
+ $joins
);
// Subpages (if enabled)
diff --git a/www/wiki/includes/actions/MarkpatrolledAction.php b/www/wiki/includes/actions/MarkpatrolledAction.php
index 66bedb2b..431ea06a 100644
--- a/www/wiki/includes/actions/MarkpatrolledAction.php
+++ b/www/wiki/includes/actions/MarkpatrolledAction.php
@@ -42,6 +42,10 @@ class MarkpatrolledAction extends FormAction {
return 'patrol';
}
+ protected function usesOOUI() {
+ return true;
+ }
+
protected function getRecentChange( $data = null ) {
$rc = null;
// Note: This works both on initial GET url and after submitting the form
diff --git a/www/wiki/includes/actions/RawAction.php b/www/wiki/includes/actions/RawAction.php
index d8c8bc32..3fda401b 100644
--- a/www/wiki/includes/actions/RawAction.php
+++ b/www/wiki/includes/actions/RawAction.php
@@ -26,6 +26,8 @@
* @file
*/
+use MediaWiki\Logger\LoggerFactory;
+
/**
* A simple method to retrieve the plain source of an article,
* using "action=raw" in the GET request string.
@@ -59,20 +61,19 @@ class RawAction extends FormlessAction {
return; // Client cache fresh and headers sent, nothing more to do.
}
- $gen = $request->getVal( 'gen' );
- if ( $gen == 'css' || $gen == 'js' ) {
- $this->gen = true;
- }
-
$contentType = $this->getContentType();
$maxage = $request->getInt( 'maxage', $config->get( 'SquidMaxage' ) );
$smaxage = $request->getIntOrNull( 'smaxage' );
if ( $smaxage === null ) {
- if ( $contentType == 'text/css' || $contentType == 'text/javascript' ) {
- // CSS/JS raw content has its own CDN max age configuration.
- // Note: Title::getCdnUrls() includes action=raw for css/js pages,
- // so if using the canonical url, this will get HTCP purges.
+ if (
+ $contentType == 'text/css' ||
+ $contentType == 'application/json' ||
+ $contentType == 'text/javascript'
+ ) {
+ // CSS/JSON/JS raw content has its own CDN max age configuration.
+ // Note: Title::getCdnUrls() includes action=raw for css/json/js
+ // pages, so if using the canonical url, this will get HTCP purges.
$smaxage = intval( $config->get( 'ForcedRawSMaxage' ) );
} else {
// No CDN cache for anything else
@@ -86,7 +87,6 @@ class RawAction extends FormlessAction {
$response->header( $this->getOutput()->getKeyHeader() );
}
- $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' );
// Output may contain user-specific data;
// vary generated content for open sessions on private wikis
$privateCache = !User::isEveryoneAllowed( 'read' ) &&
@@ -98,6 +98,57 @@ class RawAction extends FormlessAction {
'Cache-Control: ' . $mode . ', s-maxage=' . $smaxage . ', max-age=' . $maxage
);
+ // In the event of user JS, don't allow loading a user JS/CSS/Json
+ // subpage that has no registered user associated with, as
+ // someone could register the account and take control of the
+ // JS/CSS/Json page.
+ $title = $this->getTitle();
+ if ( $title->isUserConfigPage() && $contentType !== 'text/x-wiki' ) {
+ // not using getRootText() as we want this to work
+ // even if subpages are disabled.
+ $rootPage = strtok( $title->getText(), '/' );
+ $userFromTitle = User::newFromName( $rootPage, 'usable' );
+ if ( !$userFromTitle || $userFromTitle->getId() === 0 ) {
+ $log = LoggerFactory::getInstance( "security" );
+ $log->warning(
+ "Unsafe JS/CSS/Json load - {user} loaded {title} with {ctype}",
+ [
+ 'user' => $this->getUser()->getName(),
+ 'title' => $title->getPrefixedDBKey(),
+ 'ctype' => $contentType,
+ ]
+ );
+ $msg = wfMessage( 'unregistered-user-config' );
+ throw new HttpError( 403, $msg );
+ }
+ }
+
+ // Don't allow loading non-protected pages as javascript.
+ // In future we may further restrict this to only CONTENT_MODEL_JAVASCRIPT
+ // in NS_MEDIAWIKI or NS_USER, as well as including other config types,
+ // but for now be more permissive. Allowing protected pages outside of
+ // NS_USER and NS_MEDIAWIKI in particular should be considered a temporary
+ // allowance.
+ if (
+ $contentType === 'text/javascript' &&
+ !$title->isUserJsConfigPage() &&
+ !$title->inNamespace( NS_MEDIAWIKI ) &&
+ !in_array( 'sysop', $title->getRestrictions( 'edit' ) ) &&
+ !in_array( 'editprotected', $title->getRestrictions( 'edit' ) )
+ ) {
+
+ $log = LoggerFactory::getInstance( "security" );
+ $log->info( "Blocked loading unprotected JS {title} for {user}",
+ [
+ 'user' => $this->getUser()->getName(),
+ 'title' => $title->getPrefixedDBKey(),
+ ]
+ );
+ throw new HttpError( 403, wfMessage( 'unprotected-js' ) );
+ }
+
+ $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' );
+
$text = $this->getRawText();
// Don't return a 404 response for CSS or JavaScript;
@@ -166,7 +217,7 @@ class RawAction extends FormlessAction {
}
if ( $content === null || $content === false ) {
- // section not found (or section not supported, e.g. for JS and CSS)
+ // section not found (or section not supported, e.g. for JS, JSON, and CSS)
$text = false;
} else {
$text = $content->getNativeData();
@@ -175,7 +226,7 @@ class RawAction extends FormlessAction {
}
}
- if ( $text !== false && $text !== '' && $request->getVal( 'templates' ) === 'expand' ) {
+ if ( $text !== false && $text !== '' && $request->getRawVal( 'templates' ) === 'expand' ) {
$text = $wgParser->preprocess(
$text,
$title,
@@ -225,10 +276,14 @@ class RawAction extends FormlessAction {
* @return string
*/
public function getContentType() {
- $ctype = $this->getRequest()->getVal( 'ctype' );
+ // Use getRawVal instead of getVal because we only
+ // need to match against known strings, there is no
+ // storing of localised content or other user input.
+ $ctype = $this->getRequest()->getRawVal( 'ctype' );
if ( $ctype == '' ) {
- $gen = $this->getRequest()->getVal( 'gen' );
+ // Legacy compatibilty
+ $gen = $this->getRequest()->getRawVal( 'gen' );
if ( $gen == 'js' ) {
$ctype = 'text/javascript';
} elseif ( $gen == 'css' ) {
@@ -236,7 +291,14 @@ class RawAction extends FormlessAction {
}
}
- $allowedCTypes = [ 'text/x-wiki', 'text/javascript', 'text/css', 'application/x-zope-edit' ];
+ $allowedCTypes = [
+ 'text/x-wiki',
+ 'text/javascript',
+ 'text/css',
+ // FIXME: Should we still allow Zope editing? External editing feature was dropped
+ 'application/x-zope-edit',
+ 'application/json'
+ ];
if ( $ctype == '' || !in_array( $ctype, $allowedCTypes ) ) {
$ctype = 'text/x-wiki';
}
diff --git a/www/wiki/includes/actions/RevisiondeleteAction.php b/www/wiki/includes/actions/RevisiondeleteAction.php
deleted file mode 100644
index 7df42b33..00000000
--- a/www/wiki/includes/actions/RevisiondeleteAction.php
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php
-/**
- * An action that just pass the request to Special:RevisionDelete
- *
- * Copyright © 2011 Alexandre Emsenhuber
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
- *
- * @file
- * @ingroup Actions
- * @author Alexandre Emsenhuber
- */
-
-/**
- * An action that just pass the request to Special:RevisionDelete
- *
- * @ingroup Actions
- * @deprecated since 1.25 This class has been replaced by SpecialPageAction, but
- * you really shouldn't have been using it outside core in the first place
- */
-class RevisiondeleteAction extends FormlessAction {
- public function __construct( Page $page, IContextSource $context = null ) {
- wfDeprecated( 'RevisiondeleteAction class', '1.25' );
- parent::__construct( $page, $context );
- }
-
- public function getName() {
- return 'revisiondelete';
- }
-
- public function requiresUnblock() {
- return false;
- }
-
- public function getDescription() {
- return '';
- }
-
- public function onView() {
- return '';
- }
-
- public function show() {
- $special = SpecialPageFactory::getPage( 'Revisiondelete' );
- $special->setContext( $this->getContext() );
- $special->getContext()->setTitle( $special->getPageTitle() );
- $special->run( '' );
- }
-
- public function doesWrites() {
- return true;
- }
-}
diff --git a/www/wiki/includes/actions/WatchAction.php b/www/wiki/includes/actions/WatchAction.php
index e12a7276..528e0e2e 100644
--- a/www/wiki/includes/actions/WatchAction.php
+++ b/www/wiki/includes/actions/WatchAction.php
@@ -80,8 +80,6 @@ class WatchAction extends FormAction {
$this->getOutput()->addWikiMsg( $msgKey, $this->getTitle()->getPrefixedText() );
}
- /* Static utility methods */
-
/**
* Watch or unwatch a page
* @since 1.22
diff --git a/www/wiki/includes/api/ApiBase.php b/www/wiki/includes/api/ApiBase.php
index bf2b9779..7fafa1f1 100644
--- a/www/wiki/includes/api/ApiBase.php
+++ b/www/wiki/includes/api/ApiBase.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 5, 2006
- *
* Copyright © 2006, 2010 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -155,6 +151,7 @@ abstract class ApiBase extends ContextSource {
* ((string|array|Message)[]) When PARAM_TYPE is an array, this is an array
* mapping those values to $msg for ApiBase::makeMessage(). Any value not
* having a mapping will use apihelp-{$path}-paramvalue-{$param}-{$value}.
+ * Specify an empty array to use the default message key for all values.
* @since 1.25
*/
const PARAM_HELP_MSG_PER_VALUE = 14;
@@ -217,6 +214,18 @@ abstract class ApiBase extends ContextSource {
*/
const PARAM_ISMULTI_LIMIT2 = 22;
+ /**
+ * (integer) Maximum length of a string in bytes (in UTF-8 encoding).
+ * @since 1.31
+ */
+ const PARAM_MAX_BYTES = 23;
+
+ /**
+ * (integer) Maximum length of a string in characters (unicode codepoints).
+ * @since 1.31
+ */
+ const PARAM_MAX_CHARS = 24;
+
/**@}*/
const ALL_DEFAULT_STRING = '*';
@@ -683,7 +692,7 @@ abstract class ApiBase extends ContextSource {
* Set the continuation manager
* @param ApiContinuationManager|null $manager
*/
- public function setContinuationManager( $manager ) {
+ public function setContinuationManager( ApiContinuationManager $manager = null ) {
// Main module has setContinuationManager() method overridden
// Safety - avoid infinite loop:
if ( $this->isMain() ) {
@@ -888,7 +897,7 @@ abstract class ApiBase extends ContextSource {
if ( $badParams ) {
$this->dieWithError(
- [ 'apierror-mustpostparams', join( ', ', $badParams ), count( $badParams ) ]
+ [ 'apierror-mustpostparams', implode( ', ', $badParams ), count( $badParams ) ]
);
}
}
@@ -1017,7 +1026,7 @@ abstract class ApiBase extends ContextSource {
* @param string $paramName Parameter name
* @param array|mixed $paramSettings Default value or an array of settings
* using PARAM_* constants.
- * @param bool $parseLimit Parse limit?
+ * @param bool $parseLimit Whether to parse and validate 'limit' parameters
* @return mixed Parameter value
*/
protected function getParameterFromSettings( $paramName, $paramSettings, $parseLimit ) {
@@ -1120,8 +1129,8 @@ abstract class ApiBase extends ContextSource {
) {
$type = array_merge( $type, $paramSettings[self::PARAM_EXTRA_NAMESPACES] );
}
- // By default, namespace parameters allow ALL_DEFAULT_STRING to be used to specify
- // all namespaces.
+ // Namespace parameters allow ALL_DEFAULT_STRING to be used to
+ // specify all namespaces irrespective of PARAM_ALL.
$allowAll = true;
}
if ( isset( $value ) && $type == 'submodule' ) {
@@ -1143,7 +1152,7 @@ abstract class ApiBase extends ContextSource {
if ( $multi ) {
// This loses the potential $wgContLang->checkTitleEncoding() transformation
// done by WebRequest for $_GET. Let's call that a feature.
- $value = join( "\x1f", $request->normalizeUnicode( explode( "\x1f", $rawValue ) ) );
+ $value = implode( "\x1f", $request->normalizeUnicode( explode( "\x1f", $rawValue ) ) );
} else {
$this->dieWithError( 'apierror-badvalue-notmultivalue', 'badvalue_notmultivalue' );
}
@@ -1173,9 +1182,9 @@ abstract class ApiBase extends ContextSource {
);
}
- // More validation only when choices were not given
- // choices were validated in parseMultiValue()
if ( isset( $value ) ) {
+ // More validation only when choices were not given
+ // choices were validated in parseMultiValue()
if ( !is_array( $type ) ) {
switch ( $type ) {
case 'NULL': // nothing to do
@@ -1285,6 +1294,23 @@ abstract class ApiBase extends ContextSource {
$value = array_unique( $value );
}
+ if ( in_array( $type, [ 'NULL', 'string', 'text', 'password' ], true ) ) {
+ foreach ( (array)$value as $val ) {
+ if ( isset( $paramSettings[self::PARAM_MAX_BYTES] )
+ && strlen( $val ) > $paramSettings[self::PARAM_MAX_BYTES]
+ ) {
+ $this->dieWithError( [ 'apierror-maxbytes', $encParamName,
+ $paramSettings[self::PARAM_MAX_BYTES] ] );
+ }
+ if ( isset( $paramSettings[self::PARAM_MAX_CHARS] )
+ && mb_strlen( $val, 'UTF-8' ) > $paramSettings[self::PARAM_MAX_CHARS]
+ ) {
+ $this->dieWithError( [ 'apierror-maxchars', $encParamName,
+ $paramSettings[self::PARAM_MAX_CHARS] ] );
+ }
+ }
+ }
+
// Set a warning if a deprecated parameter has been passed
if ( $deprecated && $value !== false ) {
$feature = $encParamName;
@@ -1378,7 +1404,7 @@ abstract class ApiBase extends ContextSource {
protected function parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues,
$allSpecifier = null, $limit1 = null, $limit2 = null
) {
- if ( ( trim( $value ) === '' || trim( $value ) === "\x1f" ) && $allowMultiple ) {
+ if ( ( $value === '' || $value === "\x1f" ) && $allowMultiple ) {
return [];
}
$limit1 = $limit1 ?: self::LIMIT_SML1;
@@ -1410,22 +1436,15 @@ abstract class ApiBase extends ContextSource {
return $value;
}
- if ( is_array( $allowedValues ) ) {
- $values = array_map( function ( $v ) {
- return '<kbd>' . wfEscapeWikiText( $v ) . '</kbd>';
- }, $allowedValues );
- $this->dieWithError( [
- 'apierror-multival-only-one-of',
- $valueName,
- Message::listParam( $values ),
- count( $values ),
- ], "multival_$valueName" );
- } else {
- $this->dieWithError( [
- 'apierror-multival-only-one',
- $valueName,
- ], "multival_$valueName" );
- }
+ $values = array_map( function ( $v ) {
+ return '<kbd>' . wfEscapeWikiText( $v ) . '</kbd>';
+ }, $allowedValues );
+ $this->dieWithError( [
+ 'apierror-multival-only-one-of',
+ $valueName,
+ Message::listParam( $values ),
+ count( $values ),
+ ], "multival_$valueName" );
}
if ( is_array( $allowedValues ) ) {
@@ -1511,7 +1530,7 @@ abstract class ApiBase extends ContextSource {
}
/**
- * Validate and normalize of parameters of type 'timestamp'
+ * Validate and normalize parameters of type 'timestamp'
* @param string $value Parameter value
* @param string $encParamName Parameter name
* @return string Validated and normalized parameter
@@ -1533,15 +1552,15 @@ abstract class ApiBase extends ContextSource {
return wfTimestamp( TS_MW );
}
- $unixTimestamp = wfTimestamp( TS_UNIX, $value );
- if ( $unixTimestamp === false ) {
+ $timestamp = wfTimestamp( TS_MW, $value );
+ if ( $timestamp === false ) {
$this->dieWithError(
[ 'apierror-badtimestamp', $encParamName, wfEscapeWikiText( $value ) ],
"badtimestamp_{$encParamName}"
);
}
- return wfTimestamp( TS_MW, $unixTimestamp );
+ return $timestamp;
}
/**
@@ -1583,21 +1602,42 @@ abstract class ApiBase extends ContextSource {
}
/**
- * Validate and normalize of parameters of type 'user'
+ * Validate and normalize parameters of type 'user'
* @param string $value Parameter value
* @param string $encParamName Parameter name
* @return string Validated and normalized parameter
*/
private function validateUser( $value, $encParamName ) {
- $title = Title::makeTitleSafe( NS_USER, $value );
- if ( $title === null || $title->hasFragment() ) {
+ if ( ExternalUserNames::isExternal( $value ) && User::newFromName( $value, false ) ) {
+ return $value;
+ }
+
+ $titleObj = Title::makeTitleSafe( NS_USER, $value );
+
+ if ( $titleObj ) {
+ $value = $titleObj->getText();
+ }
+
+ if (
+ !User::isValidUserName( $value ) &&
+ // We allow ranges as well, for blocks.
+ !IP::isIPAddress( $value ) &&
+ // See comment for User::isIP. We don't just call that function
+ // here because it also returns true for things like
+ // 300.300.300.300 that are neither valid usernames nor valid IP
+ // addresses.
+ !preg_match(
+ '/^' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.xxx$/',
+ $value
+ )
+ ) {
$this->dieWithError(
[ 'apierror-baduser', $encParamName, wfEscapeWikiText( $value ) ],
"baduser_{$encParamName}"
);
}
- return $title->getText();
+ return $value;
}
/**@}*/
@@ -1803,7 +1843,7 @@ abstract class ApiBase extends ContextSource {
$msgs = [ $this->msg( 'api-usage-mailinglist-ref' ) ];
Hooks::run( 'ApiDeprecationHelp', [ &$msgs ] );
if ( count( $msgs ) > 1 ) {
- $key = '$' . join( ' $', range( 1, count( $msgs ) ) );
+ $key = '$' . implode( ' $', range( 1, count( $msgs ) ) );
$msg = ( new RawMessage( $key ) )->params( $msgs );
} else {
$msg = reset( $msgs );
@@ -2435,7 +2475,7 @@ abstract class ApiBase extends ContextSource {
realpath( __DIR__ ) ?: __DIR__ => [
'path' => $IP,
'name' => 'MediaWiki',
- 'license-name' => 'GPL-2.0+',
+ 'license-name' => 'GPL-2.0-or-later',
],
realpath( "$IP/extensions" ) ?: "$IP/extensions" => null,
realpath( $extDir ) ?: $extDir => null,
@@ -2560,16 +2600,6 @@ abstract class ApiBase extends ContextSource {
}
/**
- * @deprecated since 1.25, always returns empty string
- * @param IDatabase|bool $db
- * @return string
- */
- public function getModuleProfileName( $db = false ) {
- wfDeprecated( __METHOD__, '1.25' );
- return '';
- }
-
- /**
* @deprecated since 1.25
*/
public function profileIn() {
@@ -2593,15 +2623,6 @@ abstract class ApiBase extends ContextSource {
}
/**
- * @deprecated since 1.25, always returns 0
- * @return float
- */
- public function getProfileTime() {
- wfDeprecated( __METHOD__, '1.25' );
- return 0;
- }
-
- /**
* @deprecated since 1.25
*/
public function profileDBIn() {
@@ -2616,15 +2637,6 @@ abstract class ApiBase extends ContextSource {
}
/**
- * @deprecated since 1.25, always returns 0
- * @return float
- */
- public function getProfileDBTime() {
- wfDeprecated( __METHOD__, '1.25' );
- return 0;
- }
-
- /**
* Call wfTransactionalTimeLimit() if this request was POSTed
* @since 1.26
*/
diff --git a/www/wiki/includes/api/ApiBlock.php b/www/wiki/includes/api/ApiBlock.php
index 4d37af31..85dd2c7f 100644
--- a/www/wiki/includes/api/ApiBlock.php
+++ b/www/wiki/includes/api/ApiBlock.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 4, 2007
- *
* Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -67,12 +63,12 @@ class ApiBlock extends ApiBase {
$params['user'] = $username;
}
} else {
- $target = User::newFromName( $params['user'] );
+ list( $target, $type ) = SpecialBlock::getTargetAndType( $params['user'] );
// T40633 - if the target is a user (not an IP address), but it
// doesn't exist or is unusable, error.
- if ( $target instanceof User &&
- ( $target->isAnon() /* doesn't exist */ || !User::isUsableName( $target->getName() ) )
+ if ( $type === Block::TYPE_USER &&
+ ( $target->isAnon() /* doesn't exist */ || !User::isUsableName( $params['user'] ) )
) {
$this->dieWithError( [ 'nosuchusershort', $params['user'] ], 'nosuchuser' );
}
@@ -113,6 +109,11 @@ class ApiBlock extends ApiBase {
'Tags' => $params['tags'],
];
+ $status = SpecialBlock::validateTarget( $params['user'], $user );
+ if ( !$status->isOK() ) {
+ $this->dieStatus( $status );
+ }
+
$retval = SpecialBlock::processForm( $data, $this->getContext() );
if ( $retval !== true ) {
$this->dieStatus( $this->errorArrayToStatus( $retval ) );
@@ -128,8 +129,8 @@ class ApiBlock extends ApiBase {
$res['id'] = $block->getId();
} else {
# should be unreachable
- $res['expiry'] = '';
- $res['id'] = '';
+ $res['expiry'] = ''; // @codeCoverageIgnore
+ $res['id'] = ''; // @codeCoverageIgnore
}
$res['reason'] = $params['reason'];
@@ -182,14 +183,14 @@ class ApiBlock extends ApiBase {
}
protected function getExamplesMessages() {
- // @codingStandardsIgnoreStart Generic.Files.LineLength
+ // phpcs:disable Generic.Files.LineLength
return [
'action=block&user=192.0.2.5&expiry=3%20days&reason=First%20strike&token=123ABC'
=> 'apihelp-block-example-ip-simple',
'action=block&user=Vandal&expiry=never&reason=Vandalism&nocreate=&autoblock=&noemail=&token=123ABC'
=> 'apihelp-block-example-user-complex',
];
- // @codingStandardsIgnoreEnd
+ // phpcs:enable
}
public function getHelpUrls() {
diff --git a/www/wiki/includes/api/ApiCSPReport.php b/www/wiki/includes/api/ApiCSPReport.php
index 0df0ca97..af040d15 100644
--- a/www/wiki/includes/api/ApiCSPReport.php
+++ b/www/wiki/includes/api/ApiCSPReport.php
@@ -162,7 +162,7 @@ class ApiCSPReport extends ApiBase {
private function generateLogLine( $flags, $report ) {
$flagText = '';
if ( $flags ) {
- $flagText = '[' . implode( $flags, ', ' ) . ']';
+ $flagText = '[' . implode( ', ', $flags ) . ']';
}
$blockedFile = isset( $report['blocked-uri'] ) ? $report['blocked-uri'] : 'n/a';
diff --git a/www/wiki/includes/api/ApiCheckToken.php b/www/wiki/includes/api/ApiCheckToken.php
index e1be8efa..5d641d83 100644
--- a/www/wiki/includes/api/ApiCheckToken.php
+++ b/www/wiki/includes/api/ApiCheckToken.php
@@ -1,7 +1,5 @@
<?php
/**
- * Created on Jan 29, 2015
- *
* Copyright © 2015 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiClearHasMsg.php b/www/wiki/includes/api/ApiClearHasMsg.php
index 3b246309..12e0b6f2 100644
--- a/www/wiki/includes/api/ApiClearHasMsg.php
+++ b/www/wiki/includes/api/ApiClearHasMsg.php
@@ -1,8 +1,6 @@
<?php
/**
- * Created on August 26, 2014
- *
* Copyright © 2014 Petr Bena (benapetr@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiComparePages.php b/www/wiki/includes/api/ApiComparePages.php
index 953bc10c..93c35d3d 100644
--- a/www/wiki/includes/api/ApiComparePages.php
+++ b/www/wiki/includes/api/ApiComparePages.php
@@ -94,6 +94,26 @@ class ApiComparePages extends ApiBase {
$this->dieWithError( 'apierror-baddiff' );
}
+ // Extract sections, if told to
+ if ( isset( $params['fromsection'] ) ) {
+ $fromContent = $fromContent->getSection( $params['fromsection'] );
+ if ( !$fromContent ) {
+ $this->dieWithError(
+ [ 'apierror-compare-nosuchfromsection', wfEscapeWikiText( $params['fromsection'] ) ],
+ 'nosuchfromsection'
+ );
+ }
+ }
+ if ( isset( $params['tosection'] ) ) {
+ $toContent = $toContent->getSection( $params['tosection'] );
+ if ( !$toContent ) {
+ $this->dieWithError(
+ [ 'apierror-compare-nosuchtosection', wfEscapeWikiText( $params['tosection'] ) ],
+ 'nosuchtosection'
+ );
+ }
+ }
+
// Get the diff
$context = new DerivativeContext( $this->getContext() );
if ( $relRev && $relRev->getTitle() ) {
@@ -147,7 +167,10 @@ class ApiComparePages extends ApiBase {
ApiResult::setContentValue( $vals, 'body', $difftext );
}
- $this->getResult()->addValue( null, $this->getModuleName(), $vals );
+ // Diffs can be really big and there's little point in having
+ // ApiResult truncate it to an empty response since the diff is the
+ // whole reason this module exists. So pass NO_SIZE_CHECK here.
+ $this->getResult()->addValue( null, $this->getModuleName(), $vals, ApiResult::NO_SIZE_CHECK );
}
/**
@@ -175,14 +198,17 @@ class ApiComparePages extends ApiBase {
$rev = Revision::newFromId( $revId );
if ( !$rev ) {
// Titles of deleted revisions aren't secret, per T51088
+ $arQuery = Revision::getArchiveQueryInfo();
$row = $this->getDB()->selectRow(
- 'archive',
+ $arQuery['tables'],
array_merge(
- Revision::selectArchiveFields(),
+ $arQuery['fields'],
[ 'ar_namespace', 'ar_title' ]
),
[ 'ar_rev_id' => $revId ],
- __METHOD__
+ __METHOD__,
+ [],
+ $arQuery['joins']
);
if ( $row ) {
$rev = Revision::newFromArchiveRow( $row );
@@ -285,14 +311,17 @@ class ApiComparePages extends ApiBase {
$rev = Revision::newFromId( $revId );
if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
// Try the 'archive' table
+ $arQuery = Revision::getArchiveQueryInfo();
$row = $this->getDB()->selectRow(
- 'archive',
+ $arQuery['tables'],
array_merge(
- Revision::selectArchiveFields(),
+ $arQuery['fields'],
[ 'ar_namespace', 'ar_title' ]
),
[ 'ar_rev_id' => $revId ],
- __METHOD__
+ __METHOD__,
+ [],
+ $arQuery['joins']
);
if ( $row ) {
$rev = Revision::newFromArchiveRow( $row );
@@ -368,7 +397,7 @@ class ApiComparePages extends ApiBase {
if ( $rev ) {
$title = $rev->getTitle();
if ( isset( $this->props['ids'] ) ) {
- $vals["{$prefix}id"] = $title->getArticleId();
+ $vals["{$prefix}id"] = $title->getArticleID();
$vals["{$prefix}revid"] = $rev->getId();
}
if ( isset( $this->props['title'] ) ) {
@@ -438,6 +467,7 @@ class ApiComparePages extends ApiBase {
'text' => [
ApiBase::PARAM_TYPE => 'text'
],
+ 'section' => null,
'pst' => false,
'contentformat' => [
ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
diff --git a/www/wiki/includes/api/ApiDelete.php b/www/wiki/includes/api/ApiDelete.php
index 7766acd3..a63dee6f 100644
--- a/www/wiki/includes/api/ApiDelete.php
+++ b/www/wiki/includes/api/ApiDelete.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Jun 30, 2007
- *
* Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -59,7 +55,7 @@ class ApiDelete extends ApiBase {
// If change tagging was requested, check that the user is allowed to tag,
// and the tags are valid
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
if ( !$tagStatus->isOK() ) {
$this->dieStatus( $tagStatus );
@@ -120,7 +116,8 @@ class ApiDelete extends ApiBase {
$hasHistory = false;
$reason = $page->getAutoDeleteReason( $hasHistory );
if ( $reason === false ) {
- return Status::newFatal( 'cannotdelete', $title->getPrefixedText() );
+ // Should be reachable only if the page has no revisions
+ return Status::newFatal( 'cannotdelete', $title->getPrefixedText() ); // @codeCoverageIgnore
}
}
diff --git a/www/wiki/includes/api/ApiDisabled.php b/www/wiki/includes/api/ApiDisabled.php
index 684c4254..32111894 100644
--- a/www/wiki/includes/api/ApiDisabled.php
+++ b/www/wiki/includes/api/ApiDisabled.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 25, 2008
- *
* Copyright © 2008 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiEditPage.php b/www/wiki/includes/api/ApiEditPage.php
index 94d6e97b..83f72e54 100644
--- a/www/wiki/includes/api/ApiEditPage.php
+++ b/www/wiki/includes/api/ApiEditPage.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on August 16, 2007
- *
* Copyright © 2007 Iker Labarga "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -137,7 +133,7 @@ class ApiEditPage extends ApiBase {
}
try {
- $content = ContentHandler::makeContent( $text, $this->getTitle() );
+ $content = ContentHandler::makeContent( $text, $titleObj );
} catch ( MWContentSerializationException $ex ) {
$this->dieWithException( $ex, [
'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
@@ -334,7 +330,7 @@ class ApiEditPage extends ApiBase {
}
// Apply change tags
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
if ( $tagStatus->isOK() ) {
$requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
@@ -406,10 +402,17 @@ class ApiEditPage extends ApiBase {
return;
}
if ( !$status->getErrors() ) {
- $status->fatal( 'hookaborted' );
+ // This appears to be unreachable right now, because all
+ // code paths will set an error. Could change, though.
+ $status->fatal( 'hookaborted' ); //@codeCoverageIgnore
}
$this->dieStatus( $status );
+ // These two cases will normally have been caught earlier, and will
+ // only occur if something blocks the user between the earlier
+ // check and the check in EditPage (presumably a hook). It's not
+ // obvious that this is even possible.
+ // @codeCoverageIgnoreStart
case EditPage::AS_BLOCKED_PAGE_FOR_USER:
$this->dieWithError(
'apierror-blocked',
@@ -419,6 +422,7 @@ class ApiEditPage extends ApiBase {
case EditPage::AS_READ_ONLY_PAGE:
$this->dieReadOnly();
+ // @codeCoverageIgnoreEnd
case EditPage::AS_SUCCESS_NEW_ARTICLE:
$r['new'] = true;
@@ -450,7 +454,7 @@ class ApiEditPage extends ApiBase {
$status->fatal( 'apierror-noimageredirect-anon' );
break;
case EditPage::AS_IMAGE_REDIRECT_LOGGED:
- $status->fatal( 'apierror-noimageredirect-logged' );
+ $status->fatal( 'apierror-noimageredirect' );
break;
case EditPage::AS_CONTENT_TOO_BIG:
case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
@@ -472,6 +476,7 @@ class ApiEditPage extends ApiBase {
// Currently shouldn't be needed, but here in case
// hooks use them without setting appropriate
// errors on the status.
+ // @codeCoverageIgnoreStart
case EditPage::AS_SPAM_ERROR:
$status->fatal( 'apierror-spamdetected', $result['spam'] );
break;
@@ -497,10 +502,10 @@ class ApiEditPage extends ApiBase {
wfWarn( __METHOD__ . ": Unknown EditPage code {$status->value} with no message" );
$status->fatal( 'apierror-unknownerror-editpage', $status->value );
break;
+ // @codeCoverageIgnoreEnd
}
}
$this->dieStatus( $status );
- break;
}
$apiResult->addValue( null, $this->getModuleName(), $r );
}
@@ -570,10 +575,14 @@ class ApiEditPage extends ApiBase {
ApiBase::PARAM_TYPE => 'text',
],
'undo' => [
- ApiBase::PARAM_TYPE => 'integer'
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_MIN => 0,
+ ApiBase::PARAM_RANGE_ENFORCE => true,
],
'undoafter' => [
- ApiBase::PARAM_TYPE => 'integer'
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_MIN => 0,
+ ApiBase::PARAM_RANGE_ENFORCE => true,
],
'redirect' => [
ApiBase::PARAM_TYPE => 'boolean',
diff --git a/www/wiki/includes/api/ApiEmailUser.php b/www/wiki/includes/api/ApiEmailUser.php
index edea2661..535a3e80 100644
--- a/www/wiki/includes/api/ApiEmailUser.php
+++ b/www/wiki/includes/api/ApiEmailUser.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on June 1, 2008
- *
* Copyright © 2008 Bryan Tong Minh <Bryan.TongMinh@Gmail.com>
*
* This program is free software; you can redistribute it and/or modify
@@ -71,7 +67,7 @@ class ApiEmailUser extends ApiBase {
}
$result = array_filter( [
- 'result' => $retval->isGood() ? 'Success' : ( $retval->isOk() ? 'Warnings' : 'Failure' ),
+ 'result' => $retval->isGood() ? 'Success' : ( $retval->isOK() ? 'Warnings' : 'Failure' ),
'warnings' => $this->getErrorFormatter()->arrayFromStatus( $retval, 'warning' ),
'errors' => $this->getErrorFormatter()->arrayFromStatus( $retval, 'error' ),
] );
diff --git a/www/wiki/includes/api/ApiErrorFormatter.php b/www/wiki/includes/api/ApiErrorFormatter.php
index 7fb13525..c637752d 100644
--- a/www/wiki/includes/api/ApiErrorFormatter.php
+++ b/www/wiki/includes/api/ApiErrorFormatter.php
@@ -112,7 +112,7 @@ class ApiErrorFormatter {
* Add warnings and errors from a StatusValue object to the result
* @param string|null $modulePath
* @param StatusValue $status
- * @param string[] $types 'warning' and/or 'error'
+ * @param string[]|string $types 'warning' and/or 'error'
*/
public function addMessagesFromStatus(
$modulePath, StatusValue $status, $types = [ 'warning', 'error' ]
@@ -160,6 +160,9 @@ class ApiErrorFormatter {
if ( $exception instanceof ILocalizedException ) {
$msg = $exception->getMessageObject();
$params = [];
+ } elseif ( $exception instanceof MessageSpecifier ) {
+ $msg = Message::newFromSpecifier( $exception );
+ $params = [];
} else {
// Extract code and data from the exception, if applicable
if ( $exception instanceof UsageException ) {
@@ -358,9 +361,8 @@ class ApiErrorFormatter {
* @deprecated Only for backwards compatibility, do not use
* @ingroup API
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class ApiErrorFormatter_BackCompat extends ApiErrorFormatter {
- // @codingStandardsIgnoreEnd
/**
* @param ApiResult $result Into which data will be added
diff --git a/www/wiki/includes/api/ApiExpandTemplates.php b/www/wiki/includes/api/ApiExpandTemplates.php
index 7c86e09d..fe49b259 100644
--- a/www/wiki/includes/api/ApiExpandTemplates.php
+++ b/www/wiki/includes/api/ApiExpandTemplates.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Oct 05, 2007
- *
* Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiFeedContributions.php b/www/wiki/includes/api/ApiFeedContributions.php
index cae1e150..61a90358 100644
--- a/www/wiki/includes/api/ApiFeedContributions.php
+++ b/www/wiki/includes/api/ApiFeedContributions.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on June 06, 2011
- *
* Copyright © 2011 Sam Reed
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiFeedRecentChanges.php b/www/wiki/includes/api/ApiFeedRecentChanges.php
index 2a80dd53..e5dba8fe 100644
--- a/www/wiki/includes/api/ApiFeedRecentChanges.php
+++ b/www/wiki/includes/api/ApiFeedRecentChanges.php
@@ -169,16 +169,6 @@ class ApiFeedRecentChanges extends ApiBase {
'showlinkedto' => false,
];
- if ( $config->get( 'AllowCategorizedRecentChanges' ) ) {
- $ret += [
- 'categories' => [
- ApiBase::PARAM_TYPE => 'string',
- ApiBase::PARAM_ISMULTI => true,
- ],
- 'categories_any' => false,
- ];
- }
-
return $ret;
}
diff --git a/www/wiki/includes/api/ApiFeedWatchlist.php b/www/wiki/includes/api/ApiFeedWatchlist.php
index e3a757f7..393e5362 100644
--- a/www/wiki/includes/api/ApiFeedWatchlist.php
+++ b/www/wiki/includes/api/ApiFeedWatchlist.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Oct 13, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -215,10 +211,7 @@ class ApiFeedWatchlist extends ApiBase {
preg_match( '!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment, $matches )
) {
global $wgParser;
-
- $sectionTitle = $wgParser->stripSectionName( $matches[2] );
- $sectionTitle = Sanitizer::normalizeSectionNameWhitespace( $sectionTitle );
- $titleUrl .= Title::newFromText( '#' . $sectionTitle )->getFragmentForURL();
+ $titleUrl .= $wgParser->guessSectionNameFromWikiText( $matches[ 2 ] );
}
$timestamp = $info['timestamp'];
diff --git a/www/wiki/includes/api/ApiFileRevert.php b/www/wiki/includes/api/ApiFileRevert.php
index 736898ed..b68b9482 100644
--- a/www/wiki/includes/api/ApiFileRevert.php
+++ b/www/wiki/includes/api/ApiFileRevert.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on March 5, 2011
- *
* Copyright © 2011 Bryan Tong Minh <Bryan.TongMinh@Gmail.com>
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiFormatBase.php b/www/wiki/includes/api/ApiFormatBase.php
index c5f2fcfa..234fcfdd 100644
--- a/www/wiki/includes/api/ApiFormatBase.php
+++ b/www/wiki/includes/api/ApiFormatBase.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 19, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -30,7 +26,7 @@
* @ingroup API
*/
abstract class ApiFormatBase extends ApiBase {
- private $mIsHtml, $mFormat, $mUnescapeAmps, $mHelp;
+ private $mIsHtml, $mFormat;
private $mBuffer, $mDisabled = false;
private $mIsWrappedHtml = false;
private $mHttpStatus = false;
@@ -69,8 +65,7 @@ abstract class ApiFormatBase extends ApiBase {
* @note If $this->getIsWrappedHtml() || $this->getIsHtml(), you'll very
* likely want to fall back to this class's version.
* @since 1.27
- * @return string Generally this should be "api-result.$ext", and must be
- * encoded for inclusion in a Content-Disposition header's filename parameter.
+ * @return string Generally this should be "api-result.$ext"
*/
public function getFilename() {
if ( $this->getIsWrappedHtml() ) {
@@ -78,7 +73,8 @@ abstract class ApiFormatBase extends ApiBase {
} elseif ( $this->getIsHtml() ) {
return 'api-result.html';
} else {
- $exts = MimeMagic::singleton()->getExtensionsForType( $this->getMimeType() );
+ $exts = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer()
+ ->getExtensionsForType( $this->getMimeType() );
$ext = $exts ? strtok( $exts, ' ' ) : strtolower( $this->mFormat );
return "api-result.$ext";
}
@@ -215,10 +211,25 @@ abstract class ApiFormatBase extends ApiBase {
// Set a Content-Disposition header so something downloading an API
// response uses a halfway-sensible filename (T128209).
+ $header = 'Content-Disposition: inline';
$filename = $this->getFilename();
- $this->getMain()->getRequest()->response()->header(
- "Content-Disposition: inline; filename=\"{$filename}\""
- );
+ $compatFilename = mb_convert_encoding( $filename, 'ISO-8859-1' );
+ if ( preg_match( '/^[0-9a-zA-Z!#$%&\'*+\-.^_`|~]+$/', $compatFilename ) ) {
+ $header .= '; filename=' . $compatFilename;
+ } else {
+ $header .= '; filename="'
+ . preg_replace( '/([\0-\x1f"\x5c\x7f])/', '\\\\$1', $compatFilename ) . '"';
+ }
+ if ( $compatFilename !== $filename ) {
+ $value = "UTF-8''" . rawurlencode( $filename );
+ // rawurlencode() encodes more characters than RFC 5987 specifies. Unescape the ones it allows.
+ $value = strtr( $value, [
+ '%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&', '%2B' => '+', '%5E' => '^',
+ '%60' => '`', '%7C' => '|',
+ ] );
+ $header .= '; filename*=' . $value;
+ }
+ $this->getMain()->getRequest()->response()->header( $header );
}
/**
@@ -287,7 +298,7 @@ abstract class ApiFormatBase extends ApiBase {
if ( $this->getIsWrappedHtml() ) {
// This is a special output mode mainly intended for ApiSandbox use
- $time = microtime( true ) - $this->getConfig()->get( 'RequestTime' );
+ $time = $this->getMain()->getRequest()->getElapsedTime();
$json = FormatJson::encode(
[
'status' => (int)( $this->mHttpStatus ?: 200 ),
@@ -304,7 +315,7 @@ abstract class ApiFormatBase extends ApiBase {
false, FormatJson::ALL_OK
);
- // T68776: wfMangleFlashPolicy() is needed to avoid a nasty bug in
+ // T68776: OutputHandler::mangleFlashPolicy() avoids a nasty bug in
// Flash, but what it does isn't friendly for the API, so we need to
// work around it.
if ( preg_match( '/\<\s*cross-domain-policy\s*\>/i', $json ) ) {
diff --git a/www/wiki/includes/api/ApiFormatFeedWrapper.php b/www/wiki/includes/api/ApiFormatFeedWrapper.php
index 3ab5ab9e..262eb1f7 100644
--- a/www/wiki/includes/api/ApiFormatFeedWrapper.php
+++ b/www/wiki/includes/api/ApiFormatFeedWrapper.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 19, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiFormatJson.php b/www/wiki/includes/api/ApiFormatJson.php
index e5dafae6..2f63faff 100644
--- a/www/wiki/includes/api/ApiFormatJson.php
+++ b/www/wiki/includes/api/ApiFormatJson.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 19, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -91,7 +87,7 @@ class ApiFormatJson extends ApiFormatBase {
$data = $this->getResult()->getResultData( null, $transform );
$json = FormatJson::encode( $data, $this->getIsHtml(), $opt );
- // T68776: wfMangleFlashPolicy() is needed to avoid a nasty bug in
+ // T68776: OutputHandler::mangleFlashPolicy() avoids a nasty bug in
// Flash, but what it does isn't friendly for the API, so we need to
// work around it.
if ( preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $json ) ) {
@@ -128,8 +124,8 @@ class ApiFormatJson extends ApiFormatBase {
ApiBase::PARAM_HELP_MSG => 'apihelp-json-param-ascii',
],
'formatversion' => [
- ApiBase::PARAM_TYPE => [ 1, 2, 'latest' ],
- ApiBase::PARAM_DFLT => 1,
+ ApiBase::PARAM_TYPE => [ '1', '2', 'latest' ],
+ ApiBase::PARAM_DFLT => '1',
ApiBase::PARAM_HELP_MSG => 'apihelp-json-param-formatversion',
],
];
diff --git a/www/wiki/includes/api/ApiFormatNone.php b/www/wiki/includes/api/ApiFormatNone.php
index dc623ac1..602e3c18 100644
--- a/www/wiki/includes/api/ApiFormatNone.php
+++ b/www/wiki/includes/api/ApiFormatNone.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Oct 22, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -25,7 +21,8 @@
*/
/**
- * API Serialized PHP output formatter
+ * Formatter that outputs nothing, for when you don't care about the response
+ * at all
* @ingroup API
*/
class ApiFormatNone extends ApiFormatBase {
diff --git a/www/wiki/includes/api/ApiFormatPhp.php b/www/wiki/includes/api/ApiFormatPhp.php
index 671f3561..45bdb6d4 100644
--- a/www/wiki/includes/api/ApiFormatPhp.php
+++ b/www/wiki/includes/api/ApiFormatPhp.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Oct 22, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -60,12 +56,12 @@ class ApiFormatPhp extends ApiFormatBase {
}
$text = serialize( $this->getResult()->getResultData( null, $transforms ) );
- // T68776: wfMangleFlashPolicy() is needed to avoid a nasty bug in
+ // T68776: OutputHandler::mangleFlashPolicy() avoids a nasty bug in
// Flash, but what it does isn't friendly for the API. There's nothing
// we can do here that isn't actively broken in some manner, so let's
// just be broken in a useful manner.
if ( $this->getConfig()->get( 'MangleFlashPolicy' ) &&
- in_array( 'wfOutputHandler', ob_list_handlers(), true ) &&
+ in_array( 'MediaWiki\\OutputHandler::handle', ob_list_handlers(), true ) &&
preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $text )
) {
$this->dieWithError( 'apierror-formatphp', 'internalerror' );
@@ -77,8 +73,8 @@ class ApiFormatPhp extends ApiFormatBase {
public function getAllowedParams() {
$ret = parent::getAllowedParams() + [
'formatversion' => [
- ApiBase::PARAM_TYPE => [ 1, 2, 'latest' ],
- ApiBase::PARAM_DFLT => 1,
+ ApiBase::PARAM_TYPE => [ '1', '2', 'latest' ],
+ ApiBase::PARAM_DFLT => '1',
ApiBase::PARAM_HELP_MSG => 'apihelp-php-param-formatversion',
],
];
diff --git a/www/wiki/includes/api/ApiFormatRaw.php b/www/wiki/includes/api/ApiFormatRaw.php
index ebaeb2ce..9ec4a2c3 100644
--- a/www/wiki/includes/api/ApiFormatRaw.php
+++ b/www/wiki/includes/api/ApiFormatRaw.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Feb 2, 2009
- *
* Copyright © 2009 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiFormatXml.php b/www/wiki/includes/api/ApiFormatXml.php
index e4dfda0f..cc1bd820 100644
--- a/www/wiki/includes/api/ApiFormatXml.php
+++ b/www/wiki/includes/api/ApiFormatXml.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 19, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiHelp.php b/www/wiki/includes/api/ApiHelp.php
index ea4f724a..8d248590 100644
--- a/www/wiki/includes/api/ApiHelp.php
+++ b/www/wiki/includes/api/ApiHelp.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Aug 29, 2014
- *
* Copyright © 2014 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
@@ -713,6 +709,15 @@ class ApiHelp extends ApiBase {
}
}
+ if ( isset( $settings[self::PARAM_MAX_BYTES] ) ) {
+ $info[] = $context->msg( 'api-help-param-maxbytes' )
+ ->numParams( $settings[self::PARAM_MAX_BYTES] );
+ }
+ if ( isset( $settings[self::PARAM_MAX_CHARS] ) ) {
+ $info[] = $context->msg( 'api-help-param-maxchars' )
+ ->numParams( $settings[self::PARAM_MAX_CHARS] );
+ }
+
// Add default
$default = isset( $settings[ApiBase::PARAM_DFLT] )
? $settings[ApiBase::PARAM_DFLT]
diff --git a/www/wiki/includes/api/ApiHelpParamValueMessage.php b/www/wiki/includes/api/ApiHelpParamValueMessage.php
index 162b7cd6..7aebf90e 100644
--- a/www/wiki/includes/api/ApiHelpParamValueMessage.php
+++ b/www/wiki/includes/api/ApiHelpParamValueMessage.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Dec 22, 2014
- *
* Copyright © 2014 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiImageRotate.php b/www/wiki/includes/api/ApiImageRotate.php
index 71bda6d7..21e26949 100644
--- a/www/wiki/includes/api/ApiImageRotate.php
+++ b/www/wiki/includes/api/ApiImageRotate.php
@@ -1,8 +1,5 @@
<?php
/**
- *
- * Created on January 3rd, 2013
- *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
@@ -43,7 +40,7 @@ class ApiImageRotate extends ApiBase {
] );
// Check if user can add tags
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$ableToTag = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getUser() );
if ( !$ableToTag->isOK() ) {
$this->dieStatus( $ableToTag );
diff --git a/www/wiki/includes/api/ApiImport.php b/www/wiki/includes/api/ApiImport.php
index b46f0b1e..822711aa 100644
--- a/www/wiki/includes/api/ApiImport.php
+++ b/www/wiki/includes/api/ApiImport.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Feb 4, 2009
- *
* Copyright © 2009 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -53,12 +49,18 @@ class ApiImport extends ApiBase {
$params['fullhistory'],
$params['templates']
);
+ $usernamePrefix = $params['interwikisource'];
} else {
$isUpload = true;
if ( !$user->isAllowed( 'importupload' ) ) {
$this->dieWithError( 'apierror-cantimport-upload' );
}
$source = ImportStreamSource::newFromUpload( 'xml' );
+ $usernamePrefix = (string)$params['interwikiprefix'];
+ if ( $usernamePrefix === '' ) {
+ $encParamName = $this->encodeParamName( 'interwikiprefix' );
+ $this->dieWithError( [ 'apierror-missingparam', $encParamName ] );
+ }
}
if ( !$source->isOK() ) {
$this->dieStatus( $source );
@@ -81,6 +83,7 @@ class ApiImport extends ApiBase {
$this->dieStatus( $statusRootPage );
}
}
+ $importer->setUsernamePrefix( $usernamePrefix, $params['assignknownusers'] );
$reporter = new ApiImportReporter(
$importer,
$isUpload,
@@ -141,6 +144,9 @@ class ApiImport extends ApiBase {
'xml' => [
ApiBase::PARAM_TYPE => 'upload',
],
+ 'interwikiprefix' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ],
'interwikisource' => [
ApiBase::PARAM_TYPE => $this->getAllowedImportSources(),
],
@@ -150,6 +156,7 @@ class ApiImport extends ApiBase {
'namespace' => [
ApiBase::PARAM_TYPE => 'namespace'
],
+ 'assignknownusers' => false,
'rootpage' => null,
'tags' => [
ApiBase::PARAM_TYPE => 'tags',
diff --git a/www/wiki/includes/api/ApiLogin.php b/www/wiki/includes/api/ApiLogin.php
index 1d62f845..14491da1 100644
--- a/www/wiki/includes/api/ApiLogin.php
+++ b/www/wiki/includes/api/ApiLogin.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 19, 2006
- *
* Copyright © 2006-2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com",
* Daniel Cannon (cannon dot danielc at gmail dot com)
*
diff --git a/www/wiki/includes/api/ApiLogout.php b/www/wiki/includes/api/ApiLogout.php
index d56c096c..d549e75f 100644
--- a/www/wiki/includes/api/ApiLogout.php
+++ b/www/wiki/includes/api/ApiLogout.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Jan 4, 2008
- *
* Copyright © 2008 Yuri Astrakhan "<Firstname><Lastname>@gmail.com",
*
* This program is free software; you can redistribute it and/or modify
@@ -63,13 +59,25 @@ class ApiLogout extends ApiBase {
Hooks::run( 'UserLogoutComplete', [ &$user, &$injected_html, $oldName ] );
}
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ protected function getWebUITokenSalt( array $params ) {
+ return 'logoutToken';
+ }
+
public function isReadMode() {
return false;
}
protected function getExamplesMessages() {
return [
- 'action=logout'
+ 'action=logout&token=123ABC'
=> 'apihelp-logout-example-logout',
];
}
diff --git a/www/wiki/includes/api/ApiMain.php b/www/wiki/includes/api/ApiMain.php
index c76c2b20..b7b13c5d 100644
--- a/www/wiki/includes/api/ApiMain.php
+++ b/www/wiki/includes/api/ApiMain.php
@@ -1,7 +1,5 @@
<?php
/**
- * Created on Sep 4, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -57,75 +55,74 @@ class ApiMain extends ApiBase {
* List of available modules: action name => module class
*/
private static $Modules = [
- 'login' => 'ApiLogin',
- 'clientlogin' => 'ApiClientLogin',
- 'logout' => 'ApiLogout',
- 'createaccount' => 'ApiAMCreateAccount',
- 'linkaccount' => 'ApiLinkAccount',
- 'unlinkaccount' => 'ApiRemoveAuthenticationData',
- 'changeauthenticationdata' => 'ApiChangeAuthenticationData',
- 'removeauthenticationdata' => 'ApiRemoveAuthenticationData',
- 'resetpassword' => 'ApiResetPassword',
- 'query' => 'ApiQuery',
- 'expandtemplates' => 'ApiExpandTemplates',
- 'parse' => 'ApiParse',
- 'stashedit' => 'ApiStashEdit',
- 'opensearch' => 'ApiOpenSearch',
- 'feedcontributions' => 'ApiFeedContributions',
- 'feedrecentchanges' => 'ApiFeedRecentChanges',
- 'feedwatchlist' => 'ApiFeedWatchlist',
- 'help' => 'ApiHelp',
- 'paraminfo' => 'ApiParamInfo',
- 'rsd' => 'ApiRsd',
- 'compare' => 'ApiComparePages',
- 'tokens' => 'ApiTokens',
- 'checktoken' => 'ApiCheckToken',
- 'cspreport' => 'ApiCSPReport',
- 'validatepassword' => 'ApiValidatePassword',
+ 'login' => ApiLogin::class,
+ 'clientlogin' => ApiClientLogin::class,
+ 'logout' => ApiLogout::class,
+ 'createaccount' => ApiAMCreateAccount::class,
+ 'linkaccount' => ApiLinkAccount::class,
+ 'unlinkaccount' => ApiRemoveAuthenticationData::class,
+ 'changeauthenticationdata' => ApiChangeAuthenticationData::class,
+ 'removeauthenticationdata' => ApiRemoveAuthenticationData::class,
+ 'resetpassword' => ApiResetPassword::class,
+ 'query' => ApiQuery::class,
+ 'expandtemplates' => ApiExpandTemplates::class,
+ 'parse' => ApiParse::class,
+ 'stashedit' => ApiStashEdit::class,
+ 'opensearch' => ApiOpenSearch::class,
+ 'feedcontributions' => ApiFeedContributions::class,
+ 'feedrecentchanges' => ApiFeedRecentChanges::class,
+ 'feedwatchlist' => ApiFeedWatchlist::class,
+ 'help' => ApiHelp::class,
+ 'paraminfo' => ApiParamInfo::class,
+ 'rsd' => ApiRsd::class,
+ 'compare' => ApiComparePages::class,
+ 'tokens' => ApiTokens::class,
+ 'checktoken' => ApiCheckToken::class,
+ 'cspreport' => ApiCSPReport::class,
+ 'validatepassword' => ApiValidatePassword::class,
// Write modules
- 'purge' => 'ApiPurge',
- 'setnotificationtimestamp' => 'ApiSetNotificationTimestamp',
- 'rollback' => 'ApiRollback',
- 'delete' => 'ApiDelete',
- 'undelete' => 'ApiUndelete',
- 'protect' => 'ApiProtect',
- 'block' => 'ApiBlock',
- 'unblock' => 'ApiUnblock',
- 'move' => 'ApiMove',
- 'edit' => 'ApiEditPage',
- 'upload' => 'ApiUpload',
- 'filerevert' => 'ApiFileRevert',
- 'emailuser' => 'ApiEmailUser',
- 'watch' => 'ApiWatch',
- 'patrol' => 'ApiPatrol',
- 'import' => 'ApiImport',
- 'clearhasmsg' => 'ApiClearHasMsg',
- 'userrights' => 'ApiUserrights',
- 'options' => 'ApiOptions',
- 'imagerotate' => 'ApiImageRotate',
- 'revisiondelete' => 'ApiRevisionDelete',
- 'managetags' => 'ApiManageTags',
- 'tag' => 'ApiTag',
- 'mergehistory' => 'ApiMergeHistory',
- 'setpagelanguage' => 'ApiSetPageLanguage',
+ 'purge' => ApiPurge::class,
+ 'setnotificationtimestamp' => ApiSetNotificationTimestamp::class,
+ 'rollback' => ApiRollback::class,
+ 'delete' => ApiDelete::class,
+ 'undelete' => ApiUndelete::class,
+ 'protect' => ApiProtect::class,
+ 'block' => ApiBlock::class,
+ 'unblock' => ApiUnblock::class,
+ 'move' => ApiMove::class,
+ 'edit' => ApiEditPage::class,
+ 'upload' => ApiUpload::class,
+ 'filerevert' => ApiFileRevert::class,
+ 'emailuser' => ApiEmailUser::class,
+ 'watch' => ApiWatch::class,
+ 'patrol' => ApiPatrol::class,
+ 'import' => ApiImport::class,
+ 'clearhasmsg' => ApiClearHasMsg::class,
+ 'userrights' => ApiUserrights::class,
+ 'options' => ApiOptions::class,
+ 'imagerotate' => ApiImageRotate::class,
+ 'revisiondelete' => ApiRevisionDelete::class,
+ 'managetags' => ApiManageTags::class,
+ 'tag' => ApiTag::class,
+ 'mergehistory' => ApiMergeHistory::class,
+ 'setpagelanguage' => ApiSetPageLanguage::class,
];
/**
* List of available formats: format name => format class
*/
private static $Formats = [
- 'json' => 'ApiFormatJson',
- 'jsonfm' => 'ApiFormatJson',
- 'php' => 'ApiFormatPhp',
- 'phpfm' => 'ApiFormatPhp',
- 'xml' => 'ApiFormatXml',
- 'xmlfm' => 'ApiFormatXml',
- 'rawfm' => 'ApiFormatJson',
- 'none' => 'ApiFormatNone',
+ 'json' => ApiFormatJson::class,
+ 'jsonfm' => ApiFormatJson::class,
+ 'php' => ApiFormatPhp::class,
+ 'phpfm' => ApiFormatPhp::class,
+ 'xml' => ApiFormatXml::class,
+ 'xmlfm' => ApiFormatXml::class,
+ 'rawfm' => ApiFormatJson::class,
+ 'none' => ApiFormatNone::class,
];
- // @codingStandardsIgnoreStart String contenation on "msg" not allowed to break long line
/**
* List of user roles that are specifically relevant to the API.
* [ 'right' => [ 'msg' => 'Some message with a $1',
@@ -142,7 +139,6 @@ class ApiMain extends ApiBase {
'params' => [ ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ]
]
];
- // @codingStandardsIgnoreEnd
/**
* @var ApiFormatBase
@@ -372,19 +368,12 @@ class ApiMain extends ApiBase {
* Set the continuation manager
* @param ApiContinuationManager|null $manager
*/
- public function setContinuationManager( $manager ) {
- if ( $manager !== null ) {
- if ( !$manager instanceof ApiContinuationManager ) {
- throw new InvalidArgumentException( __METHOD__ . ': Was passed ' .
- is_object( $manager ) ? get_class( $manager ) : gettype( $manager )
- );
- }
- if ( $this->mContinuationManager !== null ) {
- throw new UnexpectedValueException(
- __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
- ' when a manager is already set from ' . $this->mContinuationManager->getSource()
- );
- }
+ public function setContinuationManager( ApiContinuationManager $manager = null ) {
+ if ( $manager !== null && $this->mContinuationManager !== null ) {
+ throw new UnexpectedValueException(
+ __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
+ ' when a manager is already set from ' . $this->mContinuationManager->getSource()
+ );
}
$this->mContinuationManager = $manager;
}
@@ -597,7 +586,7 @@ class ApiMain extends ApiBase {
$this->setCacheMode( 'private' );
$response = $this->getRequest()->response();
- $headerStr = 'MediaWiki-API-Error: ' . join( ', ', $errCodes );
+ $headerStr = 'MediaWiki-API-Error: ' . implode( ', ', $errCodes );
$response->header( $headerStr );
// Reset and print just the error message
@@ -1041,7 +1030,7 @@ class ApiMain extends ApiBase {
// None of the rest have any messages for non-error types
} elseif ( $e instanceof UsageException ) {
// User entered incorrect parameters - generate error response
- $data = MediaWiki\quietCall( [ $e, 'getMessageArray' ] );
+ $data = Wikimedia\quietCall( [ $e, 'getMessageArray' ] );
$code = $data['code'];
$info = $data['info'];
unset( $data['code'], $data['info'] );
@@ -1054,13 +1043,14 @@ class ApiMain extends ApiBase {
if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) {
$params = [ 'apierror-databaseerror', WebRequest::getRequestId() ];
} else {
- $params = [
- 'apierror-exceptioncaught',
- WebRequest::getRequestId(),
- $e instanceof ILocalizedException
- ? $e->getMessageObject()
- : wfEscapeWikiText( $e->getMessage() )
- ];
+ if ( $e instanceof ILocalizedException ) {
+ $msg = $e->getMessageObject();
+ } elseif ( $e instanceof MessageSpecifier ) {
+ $msg = Message::newFromSpecifier( $e );
+ } else {
+ $msg = wfEscapeWikiText( $e->getMessage() );
+ }
+ $params = [ 'apierror-exceptioncaught', WebRequest::getRequestId(), $msg ];
}
$messages[] = ApiMessage::create( $params, $code );
}
@@ -1202,9 +1192,12 @@ class ApiMain extends ApiBase {
// Instantiate the module requested by the user
$module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
if ( $module === null ) {
+ // Probably can't happen
+ // @codeCoverageIgnoreStart
$this->dieWithError(
[ 'apierror-unknownaction', wfEscapeWikiText( $this->mAction ) ], 'unknown_action'
);
+ // @codeCoverageIgnoreEnd
}
$moduleParams = $module->extractRequestParams();
@@ -1223,7 +1216,10 @@ class ApiMain extends ApiBase {
}
if ( !isset( $moduleParams['token'] ) ) {
+ // Probably can't happen
+ // @codeCoverageIgnoreStart
$module->dieWithError( [ 'apierror-missingparam', 'token' ] );
+ // @codeCoverageIgnoreEnd
}
$module->requirePostedParameters( [ 'token' ] );
@@ -1400,9 +1396,9 @@ class ApiMain extends ApiBase {
$this->getRequest()->response()->statusHeader( 304 );
// Avoid outputting the compressed representation of a zero-length body
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
ini_set( 'zlib.output_compression', 0 );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
wfClearOutputBuffers();
return false;
@@ -1436,7 +1432,7 @@ class ApiMain extends ApiBase {
}
// Allow extensions to stop execution for arbitrary reasons.
- $message = false;
+ $message = 'hookaborted';
if ( !Hooks::run( 'ApiCheckCanExecute', [ $module, $user, &$message ] ) ) {
$this->dieWithError( $message );
}
@@ -1453,7 +1449,7 @@ class ApiMain extends ApiBase {
if ( $module->isWriteMode()
&& $this->getUser()->isBot()
- && wfGetLB()->getServerCount() > 1
+ && MediaWikiServices::getInstance()->getDBLoadBalancer()->getServerCount() > 1
) {
$this->checkBotReadOnly();
}
@@ -1467,7 +1463,7 @@ class ApiMain extends ApiBase {
$numLagged = 0;
$lagLimit = $this->getConfig()->get( 'APIMaxLagThreshold' );
$laggedServers = [];
- $loadBalancer = wfGetLB();
+ $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
foreach ( $loadBalancer->getLagTimes() as $serverIndex => $lag ) {
if ( $lag > $lagLimit ) {
++$numLagged;
@@ -1476,7 +1472,7 @@ class ApiMain extends ApiBase {
}
// If a majority of replica DBs are too lagged then disallow writes
- $replicaCount = wfGetLB()->getServerCount() - 1;
+ $replicaCount = $loadBalancer->getServerCount() - 1;
if ( $numLagged >= ceil( $replicaCount / 2 ) ) {
$laggedServers = implode( ', ', $laggedServers );
wfDebugLog(
@@ -1723,8 +1719,8 @@ class ApiMain extends ApiBase {
/**
* Get a request value, and register the fact that it was used, for logging.
* @param string $name
- * @param mixed $default
- * @return mixed
+ * @param string|null $default
+ * @return string|null
*/
public function getVal( $name, $default = null ) {
$this->mParamsUsed[$name] = true;
diff --git a/www/wiki/includes/api/ApiMergeHistory.php b/www/wiki/includes/api/ApiMergeHistory.php
index 79e99095..86b44270 100644
--- a/www/wiki/includes/api/ApiMergeHistory.php
+++ b/www/wiki/includes/api/ApiMergeHistory.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Dec 29, 2015
- *
* Copyright © 2015 Geoffrey Mon <geofbot@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiMessage.php b/www/wiki/includes/api/ApiMessage.php
index 9e42d5fd..33471287 100644
--- a/www/wiki/includes/api/ApiMessage.php
+++ b/www/wiki/includes/api/ApiMessage.php
@@ -30,6 +30,13 @@
* (see e.g. Title::getUserPermissionsErrors()) and the API has to make do with that.
*
* @since 1.25
+ * @note This interface exists to work around PHP's inheritance, so ApiMessage
+ * can extend Message and ApiRawMessage can extend RawMessage while still
+ * allowing an instanceof check for a Message object including this
+ * functionality. If for some reason you feel the need to implement this
+ * interface on some other class, that class must also implement all the
+ * public methods the Message class provides (not just those from
+ * MessageSpecifier, which as written is fairly useless).
* @ingroup API
*/
interface IApiMessage extends MessageSpecifier {
diff --git a/www/wiki/includes/api/ApiModuleManager.php b/www/wiki/includes/api/ApiModuleManager.php
index b5e47ac9..e02c8627 100644
--- a/www/wiki/includes/api/ApiModuleManager.php
+++ b/www/wiki/includes/api/ApiModuleManager.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Dec 27, 2012
- *
* Copyright © 2012 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -82,12 +78,12 @@ class ApiModuleManager extends ContextSource {
* @code
* $modules['foo'] = 'ApiFoo';
* $modules['bar'] = [
- * 'class' => 'ApiBar',
+ * 'class' => ApiBar::class,
* 'factory' => function( $main, $name ) { ... }
* ];
* $modules['xyzzy'] = [
- * 'class' => 'ApiXyzzy',
- * 'factory' => [ 'XyzzyFactory', 'newApiModule' ]
+ * 'class' => ApiXyzzy::class,
+ * 'factory' => [ XyzzyFactory::class, 'newApiModule' ]
* ];
* @endcode
*
diff --git a/www/wiki/includes/api/ApiMove.php b/www/wiki/includes/api/ApiMove.php
index e7b28080..f6b6b35d 100644
--- a/www/wiki/includes/api/ApiMove.php
+++ b/www/wiki/includes/api/ApiMove.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Oct 31, 2007
- *
* Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -103,7 +99,6 @@ class ApiMove extends ApiBase {
// a redirect to the new title. This is not safe, but what we did before was
// even worse: we just determined whether a redirect should have been created,
// and reported that it was created if it should have, without any checks.
- // Also note that isRedirect() is unreliable because of T39209.
$r['redirectcreated'] = $fromTitle->exists();
$r['moveoverredirect'] = $toTitleExists;
@@ -156,10 +151,6 @@ class ApiMove extends ApiBase {
$watch = 'preferences';
if ( isset( $params['watchlist'] ) ) {
$watch = $params['watchlist'];
- } elseif ( $params['watch'] ) {
- $watch = 'watch';
- } elseif ( $params['unwatch'] ) {
- $watch = 'unwatch';
}
// Watch pages
@@ -254,14 +245,6 @@ class ApiMove extends ApiBase {
'movetalk' => false,
'movesubpages' => false,
'noredirect' => false,
- 'watch' => [
- ApiBase::PARAM_DFLT => false,
- ApiBase::PARAM_DEPRECATED => true,
- ],
- 'unwatch' => [
- ApiBase::PARAM_DFLT => false,
- ApiBase::PARAM_DEPRECATED => true,
- ],
'watchlist' => [
ApiBase::PARAM_DFLT => 'preferences',
ApiBase::PARAM_TYPE => [
diff --git a/www/wiki/includes/api/ApiOpenSearch.php b/www/wiki/includes/api/ApiOpenSearch.php
index 419fd140..416fc7f6 100644
--- a/www/wiki/includes/api/ApiOpenSearch.php
+++ b/www/wiki/includes/api/ApiOpenSearch.php
@@ -1,7 +1,5 @@
<?php
/**
- * Created on Oct 13, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
* Copyright © 2008 Brion Vibber <brion@wikimedia.org>
* Copyright © 2014 Wikimedia Foundation and contributors
@@ -382,6 +380,9 @@ class ApiOpenSearch extends ApiBase {
}
}
+/**
+ * @ingroup API
+ */
class ApiOpenSearchFormatJson extends ApiFormatJson {
private $warningsAsError = false;
diff --git a/www/wiki/includes/api/ApiOptions.php b/www/wiki/includes/api/ApiOptions.php
index 5b0d86a7..4b769060 100644
--- a/www/wiki/includes/api/ApiOptions.php
+++ b/www/wiki/includes/api/ApiOptions.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Apr 15, 2012
- *
* Copyright © 2012 Szymon Świerkosz beau@adres.pl
*
* This program is free software; you can redistribute it and/or modify
@@ -24,6 +20,8 @@
* @file
*/
+use MediaWiki\MediaWikiServices;
+
/**
* API module that facilitates the changing of user's preferences.
* Requires API write mode to be enabled.
@@ -64,7 +62,7 @@ class ApiOptions extends ApiBase {
}
$changes = [];
- if ( count( $params['change'] ) ) {
+ if ( $params['change'] ) {
foreach ( $params['change'] as $entry ) {
$array = explode( '=', $entry, 2 );
$changes[$array[0]] = isset( $array[1] ) ? $array[1] : null;
@@ -78,7 +76,8 @@ class ApiOptions extends ApiBase {
$this->dieWithError( 'apierror-nochanges' );
}
- $prefs = Preferences::getPreferences( $user, $this->getContext() );
+ $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
+ $prefs = $preferencesFactory->getFormDescriptor( $user, $this->getContext() );
$prefsKinds = $user->getOptionKinds( $this->getContext(), $changes );
$htmlForm = null;
@@ -121,7 +120,7 @@ class ApiOptions extends ApiBase {
$user->setOption( $key, $value );
$changed = true;
} else {
- $this->addWarning( [ 'apiwarn-validationfailed', wfEscapeWikitext( $key ), $validation ] );
+ $this->addWarning( [ 'apiwarn-validationfailed', wfEscapeWikiText( $key ), $validation ] );
}
}
diff --git a/www/wiki/includes/api/ApiPageSet.php b/www/wiki/includes/api/ApiPageSet.php
index cfac761c..48303a57 100644
--- a/www/wiki/includes/api/ApiPageSet.php
+++ b/www/wiki/includes/api/ApiPageSet.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 24, 2006
- *
* Copyright © 2006, 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -753,7 +749,7 @@ class ApiPageSet extends ApiBase {
* $this->getPageTableFields().
*
* @param IDatabase $db
- * @param ResultWrapper $queryResult Query result object
+ * @param ResultWrapper $queryResult
*/
public function populateFromQueryResult( $db, $queryResult ) {
$this->initFromQueryResult( $queryResult );
@@ -1532,7 +1528,7 @@ class ApiPageSet extends ApiBase {
$prefix = $query->getModulePath() . '+';
$mgr = $query->getModuleManager();
foreach ( $mgr->getNamesWithClasses() as $name => $class ) {
- if ( is_subclass_of( $class, 'ApiQueryGeneratorBase' ) ) {
+ if ( is_subclass_of( $class, ApiQueryGeneratorBase::class ) ) {
$gens[$name] = $prefix . $name;
}
}
diff --git a/www/wiki/includes/api/ApiParamInfo.php b/www/wiki/includes/api/ApiParamInfo.php
index 2fa20a96..bfd3d614 100644
--- a/www/wiki/includes/api/ApiParamInfo.php
+++ b/www/wiki/includes/api/ApiParamInfo.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Dec 01, 2007
- *
* Copyright © 2008 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -471,6 +467,12 @@ class ApiParamInfo extends ApiBase {
if ( !empty( $settings[ApiBase::PARAM_RANGE_ENFORCE] ) ) {
$item['enforcerange'] = true;
}
+ if ( isset( $settings[self::PARAM_MAX_BYTES] ) ) {
+ $item['maxbytes'] = $settings[self::PARAM_MAX_BYTES];
+ }
+ if ( isset( $settings[self::PARAM_MAX_CHARS] ) ) {
+ $item['maxchars'] = $settings[self::PARAM_MAX_CHARS];
+ }
if ( !empty( $settings[ApiBase::PARAM_DEPRECATED_VALUES] ) ) {
$deprecatedValues = array_keys( $settings[ApiBase::PARAM_DEPRECATED_VALUES] );
if ( is_array( $item['type'] ) ) {
diff --git a/www/wiki/includes/api/ApiParse.php b/www/wiki/includes/api/ApiParse.php
index 7cbd3537..099d278f 100644
--- a/www/wiki/includes/api/ApiParse.php
+++ b/www/wiki/includes/api/ApiParse.php
@@ -1,7 +1,5 @@
<?php
/**
- * Created on Dec 01, 2007
- *
* Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -245,12 +243,6 @@ class ApiParse extends ApiBase {
if ( $params['onlypst'] ) {
// Build a result and bail out
$result_array = [];
- if ( $this->contentIsDeleted ) {
- $result_array['textdeleted'] = true;
- }
- if ( $this->contentIsSuppressed ) {
- $result_array['textsuppressed'] = true;
- }
$result_array['text'] = $this->pstContent->serialize( $format );
$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
if ( isset( $prop['wikitext'] ) ) {
@@ -288,10 +280,6 @@ class ApiParse extends ApiBase {
$result_array['textsuppressed'] = true;
}
- if ( $params['disabletoc'] ) {
- $p_result->setTOCEnabled( false );
- }
-
if ( isset( $params['useskin'] ) ) {
$factory = MediaWikiServices::getInstance()->getSkinFactory();
$skin = $factory->makeSkin( Skin::normalizeKey( $params['useskin'] ) );
@@ -329,6 +317,8 @@ class ApiParse extends ApiBase {
$context->setOutput( $outputPage );
if ( $skin ) {
+ // Based on OutputPage::headElement()
+ $skin->setupSkinUserCss( $outputPage );
// Based on OutputPage::output()
foreach ( $skin->getDefaultModules() as $group ) {
$outputPage->addModules( $group );
@@ -345,7 +335,12 @@ class ApiParse extends ApiBase {
}
if ( isset( $prop['text'] ) ) {
- $result_array['text'] = $p_result->getText();
+ $result_array['text'] = $p_result->getText( [
+ 'allowTOC' => !$params['disabletoc'],
+ 'enableSectionEditLinks' => !$params['disableeditsection'],
+ 'unwrap' => $params['wrapoutputclass'] === '',
+ 'deduplicateStyles' => !$params['disablestylededuplication'],
+ ] );
$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
}
@@ -399,8 +394,8 @@ class ApiParse extends ApiBase {
}
if ( isset( $prop['displaytitle'] ) ) {
- $result_array['displaytitle'] = $p_result->getDisplayTitle() ?:
- $titleObj->getPrefixedText();
+ $result_array['displaytitle'] = $p_result->getDisplayTitle() !== false
+ ? $p_result->getDisplayTitle() : $titleObj->getPrefixedText();
}
if ( isset( $prop['headitems'] ) ) {
@@ -489,12 +484,7 @@ class ApiParse extends ApiBase {
}
$wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
- $dom = $wgParser->preprocessToDom( $this->content->getNativeData() );
- if ( is_callable( [ $dom, 'saveXML' ] ) ) {
- $xml = $dom->saveXML();
- } else {
- $xml = $dom->__toString();
- }
+ $xml = $wgParser->preprocessToDom( $this->content->getNativeData() )->__toString();
$result_array['parsetree'] = $xml;
$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
}
@@ -535,13 +525,12 @@ class ApiParse extends ApiBase {
$popts->enableLimitReport( !$params['disablepp'] && !$params['disablelimitreport'] );
$popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
$popts->setIsSectionPreview( $params['sectionpreview'] );
- $popts->setEditSection( !$params['disableeditsection'] );
if ( $params['disabletidy'] ) {
$popts->setTidy( false );
}
- $popts->setWrapOutputClass(
- $params['wrapoutputclass'] === '' ? false : $params['wrapoutputclass']
- );
+ if ( $params['wrapoutputclass'] !== '' ) {
+ $popts->setWrapOutputClass( $params['wrapoutputclass'] );
+ }
$reset = null;
$suppressCache = false;
@@ -578,7 +567,7 @@ class ApiParse extends ApiBase {
} else {
$this->content = $page->getContent( Revision::FOR_THIS_USER, $this->getUser() );
if ( !$this->content ) {
- $this->dieWithError( [ 'apierror-missingcontent-pageid', $pageId ] );
+ $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ] );
}
}
$this->contentIsDeleted = $isDeleted;
@@ -602,7 +591,7 @@ class ApiParse extends ApiBase {
$pout = $page->getParserOutput( $popts, $revId, $suppressCache );
}
if ( !$pout ) {
- $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
+ $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] ); // @codeCoverageIgnore
}
return $pout;
@@ -877,6 +866,7 @@ class ApiParse extends ApiBase {
'disablelimitreport' => false,
'disableeditsection' => false,
'disabletidy' => false,
+ 'disablestylededuplication' => false,
'generatexml' => [
ApiBase::PARAM_DFLT => false,
ApiBase::PARAM_HELP_MSG => [
diff --git a/www/wiki/includes/api/ApiPatrol.php b/www/wiki/includes/api/ApiPatrol.php
index 06e8ae28..a20aca4d 100644
--- a/www/wiki/includes/api/ApiPatrol.php
+++ b/www/wiki/includes/api/ApiPatrol.php
@@ -2,8 +2,6 @@
/**
* API for MediaWiki 1.14+
*
- * Created on Sep 2, 2008
- *
* Copyright © 2008 Soxred93 soxred93@gmail.com,
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiProtect.php b/www/wiki/includes/api/ApiProtect.php
index 1be4b103..f67d3b3e 100644
--- a/www/wiki/includes/api/ApiProtect.php
+++ b/www/wiki/includes/api/ApiProtect.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 1, 2007
- *
* Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiPurge.php b/www/wiki/includes/api/ApiPurge.php
index 35f93e07..bb0be684 100644
--- a/www/wiki/includes/api/ApiPurge.php
+++ b/www/wiki/includes/api/ApiPurge.php
@@ -1,12 +1,6 @@
<?php
/**
- * API for MediaWiki 1.14+
- *
- * Created on Sep 2, 2008
- *
- * Copyright © 2008 Chad Horohoe
- *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
@@ -32,7 +26,7 @@ use MediaWiki\MediaWikiServices;
* @ingroup API
*/
class ApiPurge extends ApiBase {
- private $mPageSet;
+ private $mPageSet = null;
/**
* Purges the cache of a page
@@ -93,6 +87,7 @@ class ApiPurge extends ApiBase {
$updates = $content->getSecondaryDataUpdates(
$title, null, $forceRecursiveLinkUpdate, $p_result );
foreach ( $updates as $update ) {
+ $update->setCause( 'api-purge', $this->getUser()->getName() );
DeferredUpdates::addUpdate( $update, DeferredUpdates::PRESEND );
}
@@ -137,7 +132,7 @@ class ApiPurge extends ApiBase {
* @return ApiPageSet
*/
private function getPageSet() {
- if ( !isset( $this->mPageSet ) ) {
+ if ( $this->mPageSet === null ) {
$this->mPageSet = new ApiPageSet( $this );
}
diff --git a/www/wiki/includes/api/ApiQuery.php b/www/wiki/includes/api/ApiQuery.php
index 31bcc7a7..e49024d1 100644
--- a/www/wiki/includes/api/ApiQuery.php
+++ b/www/wiki/includes/api/ApiQuery.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 7, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -44,26 +40,26 @@ class ApiQuery extends ApiBase {
* @var array
*/
private static $QueryPropModules = [
- 'categories' => 'ApiQueryCategories',
- 'categoryinfo' => 'ApiQueryCategoryInfo',
- 'contributors' => 'ApiQueryContributors',
- 'deletedrevisions' => 'ApiQueryDeletedRevisions',
- 'duplicatefiles' => 'ApiQueryDuplicateFiles',
- 'extlinks' => 'ApiQueryExternalLinks',
- 'fileusage' => 'ApiQueryBacklinksprop',
- 'images' => 'ApiQueryImages',
- 'imageinfo' => 'ApiQueryImageInfo',
- 'info' => 'ApiQueryInfo',
- 'links' => 'ApiQueryLinks',
- 'linkshere' => 'ApiQueryBacklinksprop',
- 'iwlinks' => 'ApiQueryIWLinks',
- 'langlinks' => 'ApiQueryLangLinks',
- 'pageprops' => 'ApiQueryPageProps',
- 'redirects' => 'ApiQueryBacklinksprop',
- 'revisions' => 'ApiQueryRevisions',
- 'stashimageinfo' => 'ApiQueryStashImageInfo',
- 'templates' => 'ApiQueryLinks',
- 'transcludedin' => 'ApiQueryBacklinksprop',
+ 'categories' => ApiQueryCategories::class,
+ 'categoryinfo' => ApiQueryCategoryInfo::class,
+ 'contributors' => ApiQueryContributors::class,
+ 'deletedrevisions' => ApiQueryDeletedRevisions::class,
+ 'duplicatefiles' => ApiQueryDuplicateFiles::class,
+ 'extlinks' => ApiQueryExternalLinks::class,
+ 'fileusage' => ApiQueryBacklinksprop::class,
+ 'images' => ApiQueryImages::class,
+ 'imageinfo' => ApiQueryImageInfo::class,
+ 'info' => ApiQueryInfo::class,
+ 'links' => ApiQueryLinks::class,
+ 'linkshere' => ApiQueryBacklinksprop::class,
+ 'iwlinks' => ApiQueryIWLinks::class,
+ 'langlinks' => ApiQueryLangLinks::class,
+ 'pageprops' => ApiQueryPageProps::class,
+ 'redirects' => ApiQueryBacklinksprop::class,
+ 'revisions' => ApiQueryRevisions::class,
+ 'stashimageinfo' => ApiQueryStashImageInfo::class,
+ 'templates' => ApiQueryLinks::class,
+ 'transcludedin' => ApiQueryBacklinksprop::class,
];
/**
@@ -71,41 +67,41 @@ class ApiQuery extends ApiBase {
* @var array
*/
private static $QueryListModules = [
- 'allcategories' => 'ApiQueryAllCategories',
- 'alldeletedrevisions' => 'ApiQueryAllDeletedRevisions',
- 'allfileusages' => 'ApiQueryAllLinks',
- 'allimages' => 'ApiQueryAllImages',
- 'alllinks' => 'ApiQueryAllLinks',
- 'allpages' => 'ApiQueryAllPages',
- 'allredirects' => 'ApiQueryAllLinks',
- 'allrevisions' => 'ApiQueryAllRevisions',
- 'mystashedfiles' => 'ApiQueryMyStashedFiles',
- 'alltransclusions' => 'ApiQueryAllLinks',
- 'allusers' => 'ApiQueryAllUsers',
- 'backlinks' => 'ApiQueryBacklinks',
- 'blocks' => 'ApiQueryBlocks',
- 'categorymembers' => 'ApiQueryCategoryMembers',
- 'deletedrevs' => 'ApiQueryDeletedrevs',
- 'embeddedin' => 'ApiQueryBacklinks',
- 'exturlusage' => 'ApiQueryExtLinksUsage',
- 'filearchive' => 'ApiQueryFilearchive',
- 'imageusage' => 'ApiQueryBacklinks',
- 'iwbacklinks' => 'ApiQueryIWBacklinks',
- 'langbacklinks' => 'ApiQueryLangBacklinks',
- 'logevents' => 'ApiQueryLogEvents',
- 'pageswithprop' => 'ApiQueryPagesWithProp',
- 'pagepropnames' => 'ApiQueryPagePropNames',
- 'prefixsearch' => 'ApiQueryPrefixSearch',
- 'protectedtitles' => 'ApiQueryProtectedTitles',
- 'querypage' => 'ApiQueryQueryPage',
- 'random' => 'ApiQueryRandom',
- 'recentchanges' => 'ApiQueryRecentChanges',
- 'search' => 'ApiQuerySearch',
- 'tags' => 'ApiQueryTags',
- 'usercontribs' => 'ApiQueryContributions',
- 'users' => 'ApiQueryUsers',
- 'watchlist' => 'ApiQueryWatchlist',
- 'watchlistraw' => 'ApiQueryWatchlistRaw',
+ 'allcategories' => ApiQueryAllCategories::class,
+ 'alldeletedrevisions' => ApiQueryAllDeletedRevisions::class,
+ 'allfileusages' => ApiQueryAllLinks::class,
+ 'allimages' => ApiQueryAllImages::class,
+ 'alllinks' => ApiQueryAllLinks::class,
+ 'allpages' => ApiQueryAllPages::class,
+ 'allredirects' => ApiQueryAllLinks::class,
+ 'allrevisions' => ApiQueryAllRevisions::class,
+ 'mystashedfiles' => ApiQueryMyStashedFiles::class,
+ 'alltransclusions' => ApiQueryAllLinks::class,
+ 'allusers' => ApiQueryAllUsers::class,
+ 'backlinks' => ApiQueryBacklinks::class,
+ 'blocks' => ApiQueryBlocks::class,
+ 'categorymembers' => ApiQueryCategoryMembers::class,
+ 'deletedrevs' => ApiQueryDeletedrevs::class,
+ 'embeddedin' => ApiQueryBacklinks::class,
+ 'exturlusage' => ApiQueryExtLinksUsage::class,
+ 'filearchive' => ApiQueryFilearchive::class,
+ 'imageusage' => ApiQueryBacklinks::class,
+ 'iwbacklinks' => ApiQueryIWBacklinks::class,
+ 'langbacklinks' => ApiQueryLangBacklinks::class,
+ 'logevents' => ApiQueryLogEvents::class,
+ 'pageswithprop' => ApiQueryPagesWithProp::class,
+ 'pagepropnames' => ApiQueryPagePropNames::class,
+ 'prefixsearch' => ApiQueryPrefixSearch::class,
+ 'protectedtitles' => ApiQueryProtectedTitles::class,
+ 'querypage' => ApiQueryQueryPage::class,
+ 'random' => ApiQueryRandom::class,
+ 'recentchanges' => ApiQueryRecentChanges::class,
+ 'search' => ApiQuerySearch::class,
+ 'tags' => ApiQueryTags::class,
+ 'usercontribs' => ApiQueryContributions::class,
+ 'users' => ApiQueryUsers::class,
+ 'watchlist' => ApiQueryWatchlist::class,
+ 'watchlistraw' => ApiQueryWatchlistRaw::class,
];
/**
@@ -113,12 +109,12 @@ class ApiQuery extends ApiBase {
* @var array
*/
private static $QueryMetaModules = [
- 'allmessages' => 'ApiQueryAllMessages',
- 'authmanagerinfo' => 'ApiQueryAuthManagerInfo',
- 'siteinfo' => 'ApiQuerySiteinfo',
- 'userinfo' => 'ApiQueryUserInfo',
- 'filerepoinfo' => 'ApiQueryFileRepoInfo',
- 'tokens' => 'ApiQueryTokens',
+ 'allmessages' => ApiQueryAllMessages::class,
+ 'authmanagerinfo' => ApiQueryAuthManagerInfo::class,
+ 'siteinfo' => ApiQuerySiteinfo::class,
+ 'userinfo' => ApiQueryUserInfo::class,
+ 'filerepoinfo' => ApiQueryFileRepoInfo::class,
+ 'tokens' => ApiQueryTokens::class,
];
/**
diff --git a/www/wiki/includes/api/ApiQueryAllCategories.php b/www/wiki/includes/api/ApiQueryAllCategories.php
index aa89158f..4f833e0c 100644
--- a/www/wiki/includes/api/ApiQueryAllCategories.php
+++ b/www/wiki/includes/api/ApiQueryAllCategories.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on December 12, 2007
- *
* Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryAllDeletedRevisions.php b/www/wiki/includes/api/ApiQueryAllDeletedRevisions.php
index b22bb1ff..be129779 100644
--- a/www/wiki/includes/api/ApiQueryAllDeletedRevisions.php
+++ b/www/wiki/includes/api/ApiQueryAllDeletedRevisions.php
@@ -1,7 +1,5 @@
<?php
/**
- * Created on Oct 3, 2014
- *
* Copyright © 2014 Wikimedia Foundation and contributors
*
* Heavily based on ApiQueryDeletedrevs,
@@ -103,13 +101,16 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
}
}
- $this->addTables( 'archive' );
if ( $resultPageSet === null ) {
$this->parseParameters( $params );
- $this->addFields( Revision::selectArchiveFields() );
+ $arQuery = Revision::getArchiveQueryInfo();
+ $this->addTables( $arQuery['tables'] );
+ $this->addJoinConds( $arQuery['joins'] );
+ $this->addFields( $arQuery['fields'] );
$this->addFields( [ 'ar_title', 'ar_namespace' ] );
} else {
$this->limit = $this->getParameter( 'limit' ) ?: 10;
+ $this->addTables( 'archive' );
$this->addFields( [ 'ar_title', 'ar_namespace' ] );
if ( $optimizeGenerateTitles ) {
$this->addOption( 'DISTINCT' );
@@ -135,16 +136,11 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
}
if ( $this->fetchContent ) {
- // Modern MediaWiki has the content for deleted revs in the 'text'
- // table using fields old_text and old_flags. But revisions deleted
- // pre-1.5 store the content in the 'archive' table directly using
- // fields ar_text and ar_flags, and no corresponding 'text' row. So
- // we have to LEFT JOIN and fetch all four fields.
$this->addTables( 'text' );
$this->addJoinConds(
[ 'text' => [ 'LEFT JOIN', [ 'ar_text_id=old_id' ] ] ]
);
- $this->addFields( [ 'ar_text', 'ar_flags', 'old_text', 'old_flags' ] );
+ $this->addFields( [ 'old_text', 'old_flags' ] );
// This also means stricter restrictions
$this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
@@ -223,10 +219,19 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
}
if ( !is_null( $params['user'] ) ) {
- $this->addWhereFld( 'ar_user_text', $params['user'] );
+ // Don't query by user ID here, it might be able to use the ar_usertext_timestamp index.
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( $actorQuery['conds'] );
} elseif ( !is_null( $params['excludeuser'] ) ) {
- $this->addWhere( 'ar_user_text != ' .
- $db->addQuotes( $params['excludeuser'] ) );
+ // Here there's no chance of using ar_usertext_timestamp.
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
}
if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
diff --git a/www/wiki/includes/api/ApiQueryAllImages.php b/www/wiki/includes/api/ApiQueryAllImages.php
index 250bee66..14f1cc4f 100644
--- a/www/wiki/includes/api/ApiQueryAllImages.php
+++ b/www/wiki/includes/api/ApiQueryAllImages.php
@@ -3,8 +3,6 @@
/**
* API for MediaWiki 1.12+
*
- * Created on Mar 16, 2008
- *
* Copyright © 2008 Vasiliev Victor vasilvv@gmail.com,
* based on ApiQueryAllPages.php
*
@@ -87,13 +85,14 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
$db = $this->getDB();
$params = $this->extractRequestParams();
- $userId = !is_null( $params['user'] ) ? User::idFromName( $params['user'] ) : null;
// Table and return fields
- $this->addTables( 'image' );
-
$prop = array_flip( $params['prop'] );
- $this->addFields( LocalFile::selectFields() );
+
+ $fileQuery = LocalFile::getQueryInfo();
+ $this->addTables( $fileQuery['tables'] );
+ $this->addFields( $fileQuery['fields'] );
+ $this->addJoinConds( $fileQuery['joins'] );
$ascendingOrder = true;
if ( $params['dir'] == 'descending' || $params['dir'] == 'older' ) {
@@ -192,19 +191,22 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
// Image filters
if ( !is_null( $params['user'] ) ) {
- if ( $userId ) {
- $this->addWhereFld( 'img_user', $userId );
- } else {
- $this->addWhereFld( 'img_user_text', $params['user'] );
- }
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $db, 'img_user', User::newFromName( $params['user'], false ) );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( $actorQuery['conds'] );
}
if ( $params['filterbots'] != 'all' ) {
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'img_user' );
+ $this->addTables( $actorQuery['tables'] );
$this->addTables( 'user_groups' );
+ $this->addJoinConds( $actorQuery['joins'] );
$this->addJoinConds( [ 'user_groups' => [
'LEFT JOIN',
[
'ug_group' => User::getGroupsWithPermission( 'bot' ),
- 'ug_user = img_user',
+ 'ug_user = ' . $actorQuery['fields']['img_user'],
'ug_expiry IS NULL OR ug_expiry >= ' . $db->addQuotes( $db->timestamp() )
]
] ] );
@@ -273,15 +275,6 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
}
if ( $params['sort'] == 'timestamp' ) {
$this->addOption( 'ORDER BY', 'img_timestamp' . $sortFlag );
- if ( !is_null( $params['user'] ) ) {
- if ( $userId ) {
- $this->addOption( 'USE INDEX', [ 'image' => 'img_user_timestamp' ] );
- } else {
- $this->addOption( 'USE INDEX', [ 'image' => 'img_usertext_timestamp' ] );
- }
- } else {
- $this->addOption( 'USE INDEX', [ 'image' => 'img_timestamp' ] );
- }
} else {
$this->addOption( 'ORDER BY', 'img_name' . $sortFlag );
}
diff --git a/www/wiki/includes/api/ApiQueryAllLinks.php b/www/wiki/includes/api/ApiQueryAllLinks.php
index 9d6bf463..057dbb2a 100644
--- a/www/wiki/includes/api/ApiQueryAllLinks.php
+++ b/www/wiki/includes/api/ApiQueryAllLinks.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on July 7, 2007
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryAllMessages.php b/www/wiki/includes/api/ApiQueryAllMessages.php
index 271d2811..40abcaf2 100644
--- a/www/wiki/includes/api/ApiQueryAllMessages.php
+++ b/www/wiki/includes/api/ApiQueryAllMessages.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Dec 1, 2007
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryAllPages.php b/www/wiki/includes/api/ApiQueryAllPages.php
index 315def04..40909a49 100644
--- a/www/wiki/includes/api/ApiQueryAllPages.php
+++ b/www/wiki/includes/api/ApiQueryAllPages.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 25, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -136,12 +132,12 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
}
// Page protection filtering
- if ( count( $params['prtype'] ) || $params['prexpiry'] != 'all' ) {
+ if ( $params['prtype'] || $params['prexpiry'] != 'all' ) {
$this->addTables( 'page_restrictions' );
$this->addWhere( 'page_id=pr_page' );
$this->addWhere( "pr_expiry > {$db->addQuotes( $db->timestamp() )} OR pr_expiry IS NULL" );
- if ( count( $params['prtype'] ) ) {
+ if ( $params['prtype'] ) {
$this->addWhereFld( 'pr_type', $params['prtype'] );
if ( isset( $params['prlevel'] ) ) {
diff --git a/www/wiki/includes/api/ApiQueryAllRevisions.php b/www/wiki/includes/api/ApiQueryAllRevisions.php
index 8f7d6eb2..3af24597 100644
--- a/www/wiki/includes/api/ApiQueryAllRevisions.php
+++ b/www/wiki/includes/api/ApiQueryAllRevisions.php
@@ -1,7 +1,5 @@
<?php
/**
- * Created on Sep 27, 2015
- *
* Copyright © 2015 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
@@ -63,20 +61,20 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
}
}
- $this->addTables( 'revision' );
if ( $resultPageSet === null ) {
$this->parseParameters( $params );
- $this->addTables( 'page' );
- $this->addJoinConds(
- [ 'page' => [ 'INNER JOIN', [ 'rev_page = page_id' ] ] ]
+ $revQuery = Revision::getQueryInfo(
+ $this->fetchContent ? [ 'page', 'text' ] : [ 'page' ]
);
- $this->addFields( Revision::selectFields() );
- $this->addFields( Revision::selectPageFields() );
+ $this->addTables( $revQuery['tables'] );
+ $this->addFields( $revQuery['fields'] );
+ $this->addJoinConds( $revQuery['joins'] );
// Review this depeneding on the outcome of T113901
$this->addOption( 'STRAIGHT_JOIN' );
} else {
$this->limit = $this->getParameter( 'limit' ) ?: 10;
+ $this->addTables( 'revision' );
$this->addFields( [ 'rev_timestamp', 'rev_id' ] );
if ( $params['generatetitles'] ) {
$this->addFields( [ 'rev_page' ] );
@@ -105,29 +103,18 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
$this->addFields( 'ts_tags' );
}
- if ( $this->fetchContent ) {
- $this->addTables( 'text' );
- $this->addJoinConds(
- [ 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ] ]
- );
- $this->addFields( 'old_id' );
- $this->addFields( Revision::selectTextFields() );
- }
-
if ( $params['user'] !== null ) {
- $id = User::idFromName( $params['user'] );
- if ( $id ) {
- $this->addWhereFld( 'rev_user', $id );
- } else {
- $this->addWhereFld( 'rev_user_text', $params['user'] );
- }
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $db, 'rev_user', User::newFromName( $params['user'], false ) );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( $actorQuery['conds'] );
} elseif ( $params['excludeuser'] !== null ) {
- $id = User::idFromName( $params['excludeuser'] );
- if ( $id ) {
- $this->addWhere( 'rev_user != ' . $id );
- } else {
- $this->addWhere( 'rev_user_text != ' . $db->addQuotes( $params['excludeuser'] ) );
- }
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $db, 'rev_user', User::newFromName( $params['excludeuser'], false ) );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
}
if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
diff --git a/www/wiki/includes/api/ApiQueryAllUsers.php b/www/wiki/includes/api/ApiQueryAllUsers.php
index d594ad44..9652f810 100644
--- a/www/wiki/includes/api/ApiQueryAllUsers.php
+++ b/www/wiki/includes/api/ApiQueryAllUsers.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on July 7, 2007
- *
* Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -45,11 +41,13 @@ class ApiQueryAllUsers extends ApiQueryBase {
}
public function execute() {
+ global $wgActorTableSchemaMigrationStage;
+
$params = $this->extractRequestParams();
$activeUserDays = $this->getConfig()->get( 'ActiveUserDays' );
$db = $this->getDB();
- $commentStore = new CommentStore( 'ipb_reason' );
+ $commentStore = CommentStore::getStore();
$prop = $params['prop'];
if ( !is_null( $prop ) ) {
@@ -182,17 +180,36 @@ class ApiQueryAllUsers extends ApiQueryBase {
] ] );
// Actually count the actions using a subquery (T66505 and T66507)
+ $tables = [ 'recentchanges' ];
+ $joins = [];
+ if ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
+ $userCond = 'rc_user_text = user_name';
+ } else {
+ $tables[] = 'actor';
+ $joins['actor'] = [
+ $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
+ 'rc_actor = actor_id'
+ ];
+ if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+ $userCond = 'actor_user = user_id';
+ } else {
+ $userCond = 'actor_user = user_id OR (rc_actor = 0 AND rc_user_text = user_name)';
+ }
+ }
$timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds );
$this->addFields( [
'recentactions' => '(' . $db->selectSQLText(
- 'recentchanges',
+ $tables,
'COUNT(*)',
[
- 'rc_user_text = user_name',
+ $userCond,
'rc_type != ' . $db->addQuotes( RC_EXTERNAL ), // no wikidata
'rc_log_type IS NULL OR rc_log_type != ' . $db->addQuotes( 'newusers' ),
'rc_timestamp >= ' . $db->addQuotes( $timestamp ),
- ]
+ ],
+ __METHOD__,
+ [],
+ $joins
) . ')'
] );
}
@@ -264,7 +281,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
$data['blockedby'] = $row->ipb_by_text;
$data['blockedbyid'] = (int)$row->ipb_by;
$data['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp );
- $data['blockreason'] = $commentStore->getComment( $row )->text;
+ $data['blockreason'] = $commentStore->getComment( 'ipb_reason', $row )->text;
$data['blockexpiry'] = $row->ipb_expiry;
}
if ( $row->ipb_deleted ) {
diff --git a/www/wiki/includes/api/ApiQueryBacklinks.php b/www/wiki/includes/api/ApiQueryBacklinks.php
index 54be254d..35cb83ac 100644
--- a/www/wiki/includes/api/ApiQueryBacklinks.php
+++ b/www/wiki/includes/api/ApiQueryBacklinks.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Oct 16, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -138,7 +134,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
if ( count( $this->cont ) >= 2 ) {
$op = $this->params['dir'] == 'descending' ? '<' : '>';
- if ( count( $this->params['namespace'] ) > 1 ) {
+ if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
$this->addWhere(
"{$this->bl_from_ns} $op {$this->cont[0]} OR " .
"({$this->bl_from_ns} = {$this->cont[0]} AND " .
@@ -160,7 +156,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$this->addOption( 'LIMIT', $this->params['limit'] + 1 );
$sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
$orderBy = [];
- if ( count( $this->params['namespace'] ) > 1 ) {
+ if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
$orderBy[] = $this->bl_from_ns . $sort;
}
$orderBy[] = $this->bl_from . $sort;
@@ -246,7 +242,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$where = "{$this->bl_from} $op= {$this->cont[5]}";
// Don't bother with namespace, title, or from_namespace if it's
// otherwise constant in the where clause.
- if ( count( $this->params['namespace'] ) > 1 ) {
+ if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
$where = "{$this->bl_from_ns} $op {$this->cont[4]} OR " .
"({$this->bl_from_ns} = {$this->cont[4]} AND ($where))";
}
@@ -278,7 +274,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
if ( count( $allRedirDBkey ) > 1 ) {
$orderBy[] = $this->bl_title . $sort;
}
- if ( count( $this->params['namespace'] ) > 1 ) {
+ if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
$orderBy[] = $this->bl_from_ns . $sort;
}
$orderBy[] = $this->bl_from . $sort;
diff --git a/www/wiki/includes/api/ApiQueryBacklinksprop.php b/www/wiki/includes/api/ApiQueryBacklinksprop.php
index 1db15f87..fb90e25c 100644
--- a/www/wiki/includes/api/ApiQueryBacklinksprop.php
+++ b/www/wiki/includes/api/ApiQueryBacklinksprop.php
@@ -2,8 +2,6 @@
/**
* API module to handle links table back-queries
*
- * Created on Aug 19, 2014
- *
* Copyright © 2014 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
@@ -161,7 +159,9 @@ class ApiQueryBacklinksprop extends ApiQueryGeneratorBase {
}
} else {
$this->addWhereFld( "{$p}_from_namespace", $params['namespace'] );
- if ( !empty( $settings['from_namespace'] ) && count( $params['namespace'] ) > 1 ) {
+ if ( !empty( $settings['from_namespace'] )
+ && $params['namespace'] !== null && count( $params['namespace'] ) > 1
+ ) {
$sortby["{$p}_from_namespace"] = 'int';
}
}
diff --git a/www/wiki/includes/api/ApiQueryBase.php b/www/wiki/includes/api/ApiQueryBase.php
index 6987dfb1..3ad45bbb 100644
--- a/www/wiki/includes/api/ApiQueryBase.php
+++ b/www/wiki/includes/api/ApiQueryBase.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 7, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -259,12 +255,10 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Equivalent to addWhere(array($field => $value))
* @param string $field Field name
- * @param string|string[] $value Value; ignored if null or empty array;
+ * @param string|string[] $value Value; ignored if null or empty array
*/
protected function addWhereFld( $field, $value ) {
- // Use count() to its full documented capabilities to simultaneously
- // test for null, empty array or empty countable object
- if ( count( $value ) ) {
+ if ( $value !== null && !( is_array( $value ) && !$value ) ) {
$this->where[$field] = $value;
}
}
@@ -452,12 +446,14 @@ abstract class ApiQueryBase extends ApiBase {
if ( $showBlockInfo ) {
$this->addFields( [
'ipb_id',
- 'ipb_by',
- 'ipb_by_text',
'ipb_expiry',
'ipb_timestamp'
] );
- $commentQuery = CommentStore::newKey( 'ipb_reason' )->getJoin();
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addFields( $actorQuery['fields'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $commentQuery = CommentStore::getStore()->getJoin( 'ipb_reason' );
$this->addTables( $commentQuery['tables'] );
$this->addFields( $commentQuery['fields'] );
$this->addJoinConds( $commentQuery['joins'] );
diff --git a/www/wiki/includes/api/ApiQueryBlocks.php b/www/wiki/includes/api/ApiQueryBlocks.php
index 698c13c5..08c13e7a 100644
--- a/www/wiki/includes/api/ApiQueryBlocks.php
+++ b/www/wiki/includes/api/ApiQueryBlocks.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 10, 2007
- *
* Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -37,7 +33,7 @@ class ApiQueryBlocks extends ApiQueryBase {
public function execute() {
$db = $this->getDB();
- $commentStore = new CommentStore( 'ipb_reason' );
+ $commentStore = CommentStore::getStore();
$params = $this->extractRequestParams();
$this->requireMaxOneParameter( $params, 'users', 'ip' );
@@ -59,8 +55,12 @@ class ApiQueryBlocks extends ApiQueryBase {
$this->addFields( [ 'ipb_auto', 'ipb_id', 'ipb_timestamp' ] );
$this->addFieldsIf( [ 'ipb_address', 'ipb_user' ], $fld_user || $fld_userid );
- $this->addFieldsIf( 'ipb_by_text', $fld_by );
- $this->addFieldsIf( 'ipb_by', $fld_byid );
+ if ( $fld_by || $fld_byid ) {
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addFields( $actorQuery['fields'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ }
$this->addFieldsIf( 'ipb_expiry', $fld_expiry );
$this->addFieldsIf( [ 'ipb_range_start', 'ipb_range_end' ], $fld_range );
$this->addFieldsIf( [ 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock',
@@ -68,7 +68,7 @@ class ApiQueryBlocks extends ApiQueryBase {
$fld_flags );
if ( $fld_reason ) {
- $commentQuery = $commentStore->getJoin();
+ $commentQuery = $commentStore->getJoin( 'ipb_reason' );
$this->addTables( $commentQuery['tables'] );
$this->addFields( $commentQuery['fields'] );
$this->addJoinConds( $commentQuery['joins'] );
@@ -212,7 +212,7 @@ class ApiQueryBlocks extends ApiQueryBase {
$block['expiry'] = ApiResult::formatExpiry( $row->ipb_expiry );
}
if ( $fld_reason ) {
- $block['reason'] = $commentStore->getComment( $row )->text;
+ $block['reason'] = $commentStore->getComment( 'ipb_reason', $row )->text;
}
if ( $fld_range && !$row->ipb_auto ) {
$block['rangestart'] = IP::formatHex( $row->ipb_range_start );
diff --git a/www/wiki/includes/api/ApiQueryCategories.php b/www/wiki/includes/api/ApiQueryCategories.php
index c4428d57..7b447cb9 100644
--- a/www/wiki/includes/api/ApiQueryCategories.php
+++ b/www/wiki/includes/api/ApiQueryCategories.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on May 13, 2007
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -69,7 +65,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
$this->addTables( 'categorylinks' );
$this->addWhereFld( 'cl_from', array_keys( $this->getPageSet()->getGoodTitles() ) );
- if ( !is_null( $params['categories'] ) ) {
+ if ( $params['categories'] ) {
$cats = [];
foreach ( $params['categories'] as $cat ) {
$title = Title::newFromText( $cat );
@@ -79,6 +75,10 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
$cats[] = $title->getDBkey();
}
}
+ if ( !$cats ) {
+ // No titles so no results
+ return;
+ }
$this->addWhereFld( 'cl_to', $cats );
}
diff --git a/www/wiki/includes/api/ApiQueryCategoryInfo.php b/www/wiki/includes/api/ApiQueryCategoryInfo.php
index 25e9b274..02361a27 100644
--- a/www/wiki/includes/api/ApiQueryCategoryInfo.php
+++ b/www/wiki/includes/api/ApiQueryCategoryInfo.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on May 13, 2007
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryCategoryMembers.php b/www/wiki/includes/api/ApiQueryCategoryMembers.php
index c570ec99..37d07887 100644
--- a/www/wiki/includes/api/ApiQueryCategoryMembers.php
+++ b/www/wiki/includes/api/ApiQueryCategoryMembers.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on June 14, 2007
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -97,7 +93,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
// how to have efficient subcategory access :-) ~~~~ (oh well, domas)
$miser_ns = [];
if ( $this->getConfig()->get( 'MiserMode' ) ) {
- $miser_ns = $params['namespace'];
+ $miser_ns = $params['namespace'] ?: [];
} else {
$this->addWhereFld( 'page_namespace', $params['namespace'] );
}
diff --git a/www/wiki/includes/api/ApiQueryContributors.php b/www/wiki/includes/api/ApiQueryContributors.php
index f802d9ef..d07df5a3 100644
--- a/www/wiki/includes/api/ApiQueryContributors.php
+++ b/www/wiki/includes/api/ApiQueryContributors.php
@@ -2,8 +2,6 @@
/**
* Query the list of contributors to a page
*
- * Created on Nov 14, 2013
- *
* Copyright © 2013 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
@@ -45,6 +43,8 @@ class ApiQueryContributors extends ApiQueryBase {
}
public function execute() {
+ global $wgActorTableSchemaMigrationStage;
+
$db = $this->getDB();
$params = $this->extractRequestParams();
$this->requireMaxOneParameter( $params, 'group', 'excludegroup', 'rights', 'excluderights' );
@@ -75,17 +75,27 @@ class ApiQueryContributors extends ApiQueryBase {
}
$result = $this->getResult();
+ $revQuery = Revision::getQueryInfo();
+
+ // For MIGRATION_NEW, target indexes on the revision_actor_temp table.
+ // Otherwise, revision is fine because it'll have to check all revision rows anyway.
+ $pageField = $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'revactor_page' : 'rev_page';
+ $idField = $wgActorTableSchemaMigrationStage === MIGRATION_NEW
+ ? 'revactor_actor' : $revQuery['fields']['rev_user'];
+ $countField = $wgActorTableSchemaMigrationStage === MIGRATION_NEW
+ ? 'revactor_actor' : $revQuery['fields']['rev_user_text'];
// First, count anons
- $this->addTables( 'revision' );
+ $this->addTables( $revQuery['tables'] );
+ $this->addJoinConds( $revQuery['joins'] );
$this->addFields( [
- 'page' => 'rev_page',
- 'anons' => 'COUNT(DISTINCT rev_user_text)',
+ 'page' => $pageField,
+ 'anons' => "COUNT(DISTINCT $countField)",
] );
- $this->addWhereFld( 'rev_page', $pages );
- $this->addWhere( 'rev_user = 0' );
+ $this->addWhereFld( $pageField, $pages );
+ $this->addWhere( ActorMigration::newMigration()->isAnon( $revQuery['fields']['rev_user'] ) );
$this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' );
- $this->addOption( 'GROUP BY', 'rev_page' );
+ $this->addOption( 'GROUP BY', $pageField );
$res = $this->select( __METHOD__ );
foreach ( $res as $row ) {
$fit = $result->addValue( [ 'query', 'pages', $row->page ],
@@ -105,24 +115,27 @@ class ApiQueryContributors extends ApiQueryBase {
// Next, add logged-in users
$this->resetQueryParams();
- $this->addTables( 'revision' );
+ $this->addTables( $revQuery['tables'] );
+ $this->addJoinConds( $revQuery['joins'] );
$this->addFields( [
- 'page' => 'rev_page',
- 'user' => 'rev_user',
- 'username' => 'MAX(rev_user_text)', // Non-MySQL databases don't like partial group-by
+ 'page' => $pageField,
+ 'id' => $idField,
+ // Non-MySQL databases don't like partial group-by
+ 'userid' => 'MAX(' . $revQuery['fields']['rev_user'] . ')',
+ 'username' => 'MAX(' . $revQuery['fields']['rev_user_text'] . ')',
] );
- $this->addWhereFld( 'rev_page', $pages );
- $this->addWhere( 'rev_user != 0' );
+ $this->addWhereFld( $pageField, $pages );
+ $this->addWhere( ActorMigration::newMigration()->isNotAnon( $revQuery['fields']['rev_user'] ) );
$this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' );
- $this->addOption( 'GROUP BY', 'rev_page, rev_user' );
+ $this->addOption( 'GROUP BY', [ $pageField, $idField ] );
$this->addOption( 'LIMIT', $params['limit'] + 1 );
// Force a sort order to ensure that properties are grouped by page
- // But only if pp_page is not constant in the WHERE clause.
+ // But only if rev_page is not constant in the WHERE clause.
if ( count( $pages ) > 1 ) {
- $this->addOption( 'ORDER BY', 'rev_page, rev_user' );
+ $this->addOption( 'ORDER BY', [ 'page', 'id' ] );
} else {
- $this->addOption( 'ORDER BY', 'rev_user' );
+ $this->addOption( 'ORDER BY', 'id' );
}
$limitGroups = [];
@@ -161,7 +174,7 @@ class ApiQueryContributors extends ApiQueryBase {
$this->addJoinConds( [ 'user_groups' => [
$excludeGroups ? 'LEFT OUTER JOIN' : 'INNER JOIN',
[
- 'ug_user=rev_user',
+ 'ug_user=' . $revQuery['fields']['rev_user'],
'ug_group' => $limitGroups,
'ug_expiry IS NULL OR ug_expiry >= ' . $db->addQuotes( $db->timestamp() )
]
@@ -173,11 +186,11 @@ class ApiQueryContributors extends ApiQueryBase {
$cont = explode( '|', $params['continue'] );
$this->dieContinueUsageIf( count( $cont ) != 2 );
$cont_page = (int)$cont[0];
- $cont_user = (int)$cont[1];
+ $cont_id = (int)$cont[1];
$this->addWhere(
- "rev_page > $cont_page OR " .
- "(rev_page = $cont_page AND " .
- "rev_user >= $cont_user)"
+ "$pageField > $cont_page OR " .
+ "($pageField = $cont_page AND " .
+ "$idField >= $cont_id)"
);
}
@@ -187,18 +200,16 @@ class ApiQueryContributors extends ApiQueryBase {
if ( ++$count > $params['limit'] ) {
// We've reached the one extra which shows that
// there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->user );
-
+ $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->id );
return;
}
$fit = $this->addPageSubItem( $row->page,
- [ 'userid' => (int)$row->user, 'name' => $row->username ],
+ [ 'userid' => (int)$row->userid, 'name' => $row->username ],
'user'
);
if ( !$fit ) {
- $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->user );
-
+ $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->id );
return;
}
}
diff --git a/www/wiki/includes/api/ApiQueryDeletedRevisions.php b/www/wiki/includes/api/ApiQueryDeletedRevisions.php
index 8e4752e8..1a1e8f7a 100644
--- a/www/wiki/includes/api/ApiQueryDeletedRevisions.php
+++ b/www/wiki/includes/api/ApiQueryDeletedRevisions.php
@@ -1,7 +1,5 @@
<?php
/**
- * Created on Oct 3, 2014
- *
* Copyright © 2014 Wikimedia Foundation and contributors
*
* Heavily based on ApiQueryDeletedrevs,
@@ -60,13 +58,16 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
$this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
- $this->addTables( 'archive' );
if ( $resultPageSet === null ) {
$this->parseParameters( $params );
- $this->addFields( Revision::selectArchiveFields() );
+ $arQuery = Revision::getArchiveQueryInfo();
+ $this->addTables( $arQuery['tables'] );
+ $this->addFields( $arQuery['fields'] );
+ $this->addJoinConds( $arQuery['joins'] );
$this->addFields( [ 'ar_title', 'ar_namespace' ] );
} else {
$this->limit = $this->getParameter( 'limit' ) ?: 10;
+ $this->addTables( 'archive' );
$this->addFields( [ 'ar_title', 'ar_namespace', 'ar_timestamp', 'ar_rev_id', 'ar_id' ] );
}
@@ -87,16 +88,11 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
}
if ( $this->fetchContent ) {
- // Modern MediaWiki has the content for deleted revs in the 'text'
- // table using fields old_text and old_flags. But revisions deleted
- // pre-1.5 store the content in the 'archive' table directly using
- // fields ar_text and ar_flags, and no corresponding 'text' row. So
- // we have to LEFT JOIN and fetch all four fields.
$this->addTables( 'text' );
$this->addJoinConds(
[ 'text' => [ 'LEFT JOIN', [ 'ar_text_id=old_id' ] ] ]
);
- $this->addFields( [ 'ar_text', 'ar_flags', 'old_text', 'old_flags' ] );
+ $this->addFields( [ 'old_text', 'old_flags' ] );
// This also means stricter restrictions
$this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
@@ -116,10 +112,19 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
}
if ( !is_null( $params['user'] ) ) {
- $this->addWhereFld( 'ar_user_text', $params['user'] );
+ // Don't query by user ID here, it might be able to use the ar_usertext_timestamp index.
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( $actorQuery['conds'] );
} elseif ( !is_null( $params['excludeuser'] ) ) {
- $this->addWhere( 'ar_user_text != ' .
- $db->addQuotes( $params['excludeuser'] ) );
+ // Here there's no chance of using ar_usertext_timestamp.
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
}
if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
diff --git a/www/wiki/includes/api/ApiQueryDeletedrevs.php b/www/wiki/includes/api/ApiQueryDeletedrevs.php
index 5dd007b4..83d00a93 100644
--- a/www/wiki/includes/api/ApiQueryDeletedrevs.php
+++ b/www/wiki/includes/api/ApiQueryDeletedrevs.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Jul 2, 2007
- *
* Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -44,7 +40,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$user = $this->getUser();
$db = $this->getDB();
- $commentStore = new CommentStore( 'ar_comment' );
+ $commentStore = CommentStore::getStore();
$params = $this->extractRequestParams( false );
$prop = array_flip( $params['prop'] );
$fld_parentid = isset( $prop['parentid'] );
@@ -114,14 +110,18 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$this->addFieldsIf( 'ar_parent_id', $fld_parentid );
$this->addFieldsIf( 'ar_rev_id', $fld_revid );
- $this->addFieldsIf( 'ar_user_text', $fld_user );
- $this->addFieldsIf( 'ar_user', $fld_userid );
+ if ( $fld_user || $fld_userid ) {
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'ar_user' );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addFields( $actorQuery['fields'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ }
$this->addFieldsIf( 'ar_minor_edit', $fld_minor );
$this->addFieldsIf( 'ar_len', $fld_len );
$this->addFieldsIf( 'ar_sha1', $fld_sha1 );
if ( $fld_comment || $fld_parsedcomment ) {
- $commentQuery = $commentStore->getJoin();
+ $commentQuery = $commentStore->getJoin( 'ar_comment' );
$this->addTables( $commentQuery['tables'] );
$this->addFields( $commentQuery['fields'] );
$this->addJoinConds( $commentQuery['joins'] );
@@ -144,17 +144,11 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
}
if ( $fld_content ) {
- // Modern MediaWiki has the content for deleted revs in the 'text'
- // table using fields old_text and old_flags. But revisions deleted
- // pre-1.5 store the content in the 'archive' table directly using
- // fields ar_text and ar_flags, and no corresponding 'text' row. So
- // we have to LEFT JOIN and fetch all four fields, plus ar_text_id
- // to be able to tell the difference.
$this->addTables( 'text' );
$this->addJoinConds(
[ 'text' => [ 'LEFT JOIN', [ 'ar_text_id=old_id' ] ] ]
);
- $this->addFields( [ 'ar_text', 'ar_flags', 'ar_text_id', 'old_text', 'old_flags' ] );
+ $this->addFields( [ 'ar_text_id', 'old_text', 'old_flags' ] );
// This also means stricter restrictions
$this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
@@ -203,10 +197,19 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
}
if ( !is_null( $params['user'] ) ) {
- $this->addWhereFld( 'ar_user_text', $params['user'] );
+ // Don't query by user ID here, it might be able to use the ar_usertext_timestamp index.
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( $actorQuery['conds'] );
} elseif ( !is_null( $params['excludeuser'] ) ) {
- $this->addWhere( 'ar_user_text != ' .
- $db->addQuotes( $params['excludeuser'] ) );
+ // Here there's no chance of using ar_usertext_timestamp.
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
}
if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
@@ -255,10 +258,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
}
$this->addOption( 'LIMIT', $limit + 1 );
- $this->addOption(
- 'USE INDEX',
- [ 'archive' => ( $mode == 'user' ? 'ar_usertext_timestamp' : 'name_title_timestamp' ) ]
- );
if ( $mode == 'all' ) {
if ( $params['unique'] ) {
// @todo Does this work on non-MySQL?
@@ -329,7 +328,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$anyHidden = true;
}
if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_COMMENT, $user ) ) {
- $comment = $commentStore->getComment( $row )->text;
+ $comment = $commentStore->getComment( 'ar_comment', $row )->text;
if ( $fld_comment ) {
$rev['comment'] = $comment;
}
@@ -365,12 +364,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$anyHidden = true;
}
if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_TEXT, $user ) ) {
- if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
- // Pre-1.5 ar_text row (if condition from Revision::newFromArchiveRow)
- ApiResult::setContentValue( $rev, 'text', Revision::getRevisionText( $row, 'ar_' ) );
- } else {
- ApiResult::setContentValue( $rev, 'text', Revision::getRevisionText( $row ) );
- }
+ ApiResult::setContentValue( $rev, 'text', Revision::getRevisionText( $row ) );
}
}
diff --git a/www/wiki/includes/api/ApiQueryDisabled.php b/www/wiki/includes/api/ApiQueryDisabled.php
index a94af695..86100077 100644
--- a/www/wiki/includes/api/ApiQueryDisabled.php
+++ b/www/wiki/includes/api/ApiQueryDisabled.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 25, 2008
- *
* Copyright © 2008 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryDuplicateFiles.php b/www/wiki/includes/api/ApiQueryDuplicateFiles.php
index 0eaeaece..dfe3cf35 100644
--- a/www/wiki/includes/api/ApiQueryDuplicateFiles.php
+++ b/www/wiki/includes/api/ApiQueryDuplicateFiles.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 27, 2008
- *
* Copyright © 2008 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryExtLinksUsage.php b/www/wiki/includes/api/ApiQueryExtLinksUsage.php
index 6c29b603..fc5d8a04 100644
--- a/www/wiki/includes/api/ApiQueryExtLinksUsage.php
+++ b/www/wiki/includes/api/ApiQueryExtLinksUsage.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on July 7, 2007
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -61,7 +57,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
$miser_ns = [];
if ( $this->getConfig()->get( 'MiserMode' ) ) {
- $miser_ns = $params['namespace'];
+ $miser_ns = $params['namespace'] ?: [];
} else {
$this->addWhereFld( 'page_namespace', $params['namespace'] );
}
diff --git a/www/wiki/includes/api/ApiQueryExternalLinks.php b/www/wiki/includes/api/ApiQueryExternalLinks.php
index 71fd6d1b..fd196aab 100644
--- a/www/wiki/includes/api/ApiQueryExternalLinks.php
+++ b/www/wiki/includes/api/ApiQueryExternalLinks.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on May 13, 2007
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryFilearchive.php b/www/wiki/includes/api/ApiQueryFilearchive.php
index 212b6134..ebffb151 100644
--- a/www/wiki/includes/api/ApiQueryFilearchive.php
+++ b/www/wiki/includes/api/ApiQueryFilearchive.php
@@ -2,8 +2,6 @@
/**
* API for MediaWiki 1.12+
*
- * Created on May 10, 2010
- *
* Copyright © 2010 Sam Reed
* Copyright © 2008 Vasiliev Victor vasilvv@gmail.com,
* based on ApiQueryAllPages.php
@@ -43,7 +41,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
$user = $this->getUser();
$db = $this->getDB();
- $commentStore = new CommentStore( 'fa_description' );
+ $commentStore = CommentStore::getStore();
$params = $this->extractRequestParams();
@@ -60,25 +58,10 @@ class ApiQueryFilearchive extends ApiQueryBase {
$fld_bitdepth = isset( $prop['bitdepth'] );
$fld_archivename = isset( $prop['archivename'] );
- $this->addTables( 'filearchive' );
-
- $this->addFields( ArchivedFile::selectFields() );
- $this->addFields( [ 'fa_id', 'fa_name', 'fa_timestamp', 'fa_deleted' ] );
- $this->addFieldsIf( 'fa_sha1', $fld_sha1 );
- $this->addFieldsIf( [ 'fa_user', 'fa_user_text' ], $fld_user );
- $this->addFieldsIf( [ 'fa_height', 'fa_width', 'fa_size' ], $fld_dimensions || $fld_size );
- $this->addFieldsIf( [ 'fa_major_mime', 'fa_minor_mime' ], $fld_mime );
- $this->addFieldsIf( 'fa_media_type', $fld_mediatype );
- $this->addFieldsIf( 'fa_metadata', $fld_metadata );
- $this->addFieldsIf( 'fa_bits', $fld_bitdepth );
- $this->addFieldsIf( 'fa_archive_name', $fld_archivename );
-
- if ( $fld_description ) {
- $commentQuery = $commentStore->getJoin();
- $this->addTables( $commentQuery['tables'] );
- $this->addFields( $commentQuery['fields'] );
- $this->addJoinConds( $commentQuery['joins'] );
- }
+ $fileQuery = ArchivedFile::getQueryInfo();
+ $this->addTables( $fileQuery['tables'] );
+ $this->addFields( $fileQuery['fields'] );
+ $this->addJoinConds( $fileQuery['joins'] );
if ( !is_null( $params['continue'] ) ) {
$cont = explode( '|', $params['continue'] );
@@ -172,7 +155,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
if ( $fld_description &&
Revision::userCanBitfield( $row->fa_deleted, File::DELETED_COMMENT, $user )
) {
- $file['description'] = $commentStore->getComment( $row )->text;
+ $file['description'] = $commentStore->getComment( 'fa_description', $row )->text;
if ( isset( $prop['parseddescription'] ) ) {
$file['parseddescription'] = Linker::formatComment(
$file['description'], $title );
diff --git a/www/wiki/includes/api/ApiQueryGeneratorBase.php b/www/wiki/includes/api/ApiQueryGeneratorBase.php
index 5acd75f7..a7df33a1 100644
--- a/www/wiki/includes/api/ApiQueryGeneratorBase.php
+++ b/www/wiki/includes/api/ApiQueryGeneratorBase.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 7, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryIWBacklinks.php b/www/wiki/includes/api/ApiQueryIWBacklinks.php
index a10ba164..83801c73 100644
--- a/www/wiki/includes/api/ApiQueryIWBacklinks.php
+++ b/www/wiki/includes/api/ApiQueryIWBacklinks.php
@@ -2,8 +2,6 @@
/**
* API for MediaWiki 1.17+
*
- * Created on May 14, 2010
- *
* Copyright © 2010 Sam Reed
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
@@ -133,7 +131,7 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
if ( !is_null( $resultPageSet ) ) {
$pages[] = Title::newFromRow( $row );
} else {
- $entry = [ 'pageid' => $row->page_id ];
+ $entry = [ 'pageid' => (int)$row->page_id ];
$title = Title::makeTitle( $row->page_namespace, $row->page_title );
ApiQueryBase::addTitleInfo( $entry, $title );
diff --git a/www/wiki/includes/api/ApiQueryIWLinks.php b/www/wiki/includes/api/ApiQueryIWLinks.php
index 9313af30..08c8abf1 100644
--- a/www/wiki/includes/api/ApiQueryIWLinks.php
+++ b/www/wiki/includes/api/ApiQueryIWLinks.php
@@ -2,8 +2,6 @@
/**
* API for MediaWiki 1.17+
*
- * Created on May 14, 2010
- *
* Copyright © 2010 Sam Reed
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
diff --git a/www/wiki/includes/api/ApiQueryImageInfo.php b/www/wiki/includes/api/ApiQueryImageInfo.php
index b1df982d..e447f4f4 100644
--- a/www/wiki/includes/api/ApiQueryImageInfo.php
+++ b/www/wiki/includes/api/ApiQueryImageInfo.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on July 6, 2007
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -503,32 +499,36 @@ class ApiQueryImageInfo extends ApiQueryBase {
}
if ( $url ) {
- if ( !is_null( $thumbParams ) ) {
- $mto = $file->transform( $thumbParams );
- self::$transformCount++;
- if ( $mto && !$mto->isError() ) {
- $vals['thumburl'] = wfExpandUrl( $mto->getUrl(), PROTO_CURRENT );
-
- // T25834 - If the URLs are the same, we haven't resized it, so shouldn't give the wanted
- // thumbnail sizes for the thumbnail actual size
- if ( $mto->getUrl() !== $file->getUrl() ) {
- $vals['thumbwidth'] = intval( $mto->getWidth() );
- $vals['thumbheight'] = intval( $mto->getHeight() );
- } else {
- $vals['thumbwidth'] = intval( $file->getWidth() );
- $vals['thumbheight'] = intval( $file->getHeight() );
- }
+ if ( $file->exists() ) {
+ if ( !is_null( $thumbParams ) ) {
+ $mto = $file->transform( $thumbParams );
+ self::$transformCount++;
+ if ( $mto && !$mto->isError() ) {
+ $vals['thumburl'] = wfExpandUrl( $mto->getUrl(), PROTO_CURRENT );
+
+ // T25834 - If the URLs are the same, we haven't resized it, so shouldn't give the wanted
+ // thumbnail sizes for the thumbnail actual size
+ if ( $mto->getUrl() !== $file->getUrl() ) {
+ $vals['thumbwidth'] = intval( $mto->getWidth() );
+ $vals['thumbheight'] = intval( $mto->getHeight() );
+ } else {
+ $vals['thumbwidth'] = intval( $file->getWidth() );
+ $vals['thumbheight'] = intval( $file->getHeight() );
+ }
- if ( isset( $prop['thumbmime'] ) && $file->getHandler() ) {
- list( , $mime ) = $file->getHandler()->getThumbType(
- $mto->getExtension(), $file->getMimeType(), $thumbParams );
- $vals['thumbmime'] = $mime;
+ if ( isset( $prop['thumbmime'] ) && $file->getHandler() ) {
+ list( , $mime ) = $file->getHandler()->getThumbType(
+ $mto->getExtension(), $file->getMimeType(), $thumbParams );
+ $vals['thumbmime'] = $mime;
+ }
+ } elseif ( $mto && $mto->isError() ) {
+ $vals['thumberror'] = $mto->toText();
}
- } elseif ( $mto && $mto->isError() ) {
- $vals['thumberror'] = $mto->toText();
}
+ $vals['url'] = wfExpandUrl( $file->getFullUrl(), PROTO_CURRENT );
+ } else {
+ $vals['filemissing'] = true;
}
- $vals['url'] = wfExpandUrl( $file->getFullUrl(), PROTO_CURRENT );
$vals['descriptionurl'] = wfExpandUrl( $file->getDescriptionUrl(), PROTO_CURRENT );
$shortDescriptionUrl = $file->getDescriptionShortUrl();
@@ -542,9 +542,9 @@ class ApiQueryImageInfo extends ApiQueryBase {
}
if ( $meta ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$metadata = unserialize( $file->getMetadata() );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $metadata && $version !== 'latest' ) {
$metadata = $file->convertMetadataVersion( $metadata, $version );
}
diff --git a/www/wiki/includes/api/ApiQueryImages.php b/www/wiki/includes/api/ApiQueryImages.php
index 0086c58a..06a3ba08 100644
--- a/www/wiki/includes/api/ApiQueryImages.php
+++ b/www/wiki/includes/api/ApiQueryImages.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on May 13, 2007
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -85,7 +81,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
}
$this->addOption( 'LIMIT', $params['limit'] + 1 );
- if ( !is_null( $params['images'] ) ) {
+ if ( $params['images'] ) {
$images = [];
foreach ( $params['images'] as $img ) {
$title = Title::newFromText( $img );
@@ -95,6 +91,10 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
$images[] = $title->getDBkey();
}
}
+ if ( !$images ) {
+ // No titles so no results
+ return;
+ }
$this->addWhereFld( 'il_to', $images );
}
diff --git a/www/wiki/includes/api/ApiQueryInfo.php b/www/wiki/includes/api/ApiQueryInfo.php
index bff19780..1d3c110c 100644
--- a/www/wiki/includes/api/ApiQueryInfo.php
+++ b/www/wiki/includes/api/ApiQueryInfo.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 25, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -38,7 +34,7 @@ class ApiQueryInfo extends ApiQueryBase {
$fld_readable = false, $fld_watched = false,
$fld_watchers = false, $fld_visitingwatchers = false,
$fld_notificationtimestamp = false,
- $fld_preload = false, $fld_displaytitle = false;
+ $fld_preload = false, $fld_displaytitle = false, $fld_varianttitles = false;
private $params;
@@ -53,7 +49,7 @@ class ApiQueryInfo extends ApiQueryBase {
$pageLatest, $pageLength;
private $protections, $restrictionTypes, $watched, $watchers, $visitingwatchers,
- $notificationtimestamps, $talkids, $subjectids, $displaytitles;
+ $notificationtimestamps, $talkids, $subjectids, $displaytitles, $variantTitles;
private $showZeroWatchers = false;
private $tokenFunctions;
@@ -107,15 +103,15 @@ class ApiQueryInfo extends ApiQueryBase {
}
$this->tokenFunctions = [
- 'edit' => [ 'ApiQueryInfo', 'getEditToken' ],
- 'delete' => [ 'ApiQueryInfo', 'getDeleteToken' ],
- 'protect' => [ 'ApiQueryInfo', 'getProtectToken' ],
- 'move' => [ 'ApiQueryInfo', 'getMoveToken' ],
- 'block' => [ 'ApiQueryInfo', 'getBlockToken' ],
- 'unblock' => [ 'ApiQueryInfo', 'getUnblockToken' ],
- 'email' => [ 'ApiQueryInfo', 'getEmailToken' ],
- 'import' => [ 'ApiQueryInfo', 'getImportToken' ],
- 'watch' => [ 'ApiQueryInfo', 'getWatchToken' ],
+ 'edit' => [ self::class, 'getEditToken' ],
+ 'delete' => [ self::class, 'getDeleteToken' ],
+ 'protect' => [ self::class, 'getProtectToken' ],
+ 'move' => [ self::class, 'getMoveToken' ],
+ 'block' => [ self::class, 'getBlockToken' ],
+ 'unblock' => [ self::class, 'getUnblockToken' ],
+ 'email' => [ self::class, 'getEmailToken' ],
+ 'import' => [ self::class, 'getImportToken' ],
+ 'watch' => [ self::class, 'getWatchToken' ],
];
Hooks::run( 'APIQueryInfoTokens', [ &$this->tokenFunctions ] );
@@ -310,6 +306,7 @@ class ApiQueryInfo extends ApiQueryBase {
$this->fld_readable = isset( $prop['readable'] );
$this->fld_preload = isset( $prop['preload'] );
$this->fld_displaytitle = isset( $prop['displaytitle'] );
+ $this->fld_varianttitles = isset( $prop['varianttitles'] );
}
$pageSet = $this->getPageSet();
@@ -318,7 +315,7 @@ class ApiQueryInfo extends ApiQueryBase {
$this->everything = $this->titles + $this->missing;
$result = $this->getResult();
- uasort( $this->everything, [ 'Title', 'compare' ] );
+ uasort( $this->everything, [ Title::class, 'compare' ] );
if ( !is_null( $this->params['continue'] ) ) {
// Throw away any titles we're gonna skip so they don't
// clutter queries
@@ -372,6 +369,10 @@ class ApiQueryInfo extends ApiQueryBase {
$this->getDisplayTitle();
}
+ if ( $this->fld_varianttitles ) {
+ $this->getVariantTitles();
+ }
+
/** @var Title $title */
foreach ( $this->everything as $pageid => $title ) {
$pageInfo = $this->extractPageInfo( $pageid, $title );
@@ -514,6 +515,12 @@ class ApiQueryInfo extends ApiQueryBase {
}
}
+ if ( $this->fld_varianttitles ) {
+ if ( isset( $this->variantTitles[$pageid] ) ) {
+ $pageInfo['varianttitles'] = $this->variantTitles[$pageid];
+ }
+ }
+
if ( $this->params['testactions'] ) {
$limit = $this->getMain()->canApiHighLimits() ? self::LIMIT_SML1 : self::LIMIT_SML2;
if ( $this->countTestedActions >= $limit ) {
@@ -744,6 +751,32 @@ class ApiQueryInfo extends ApiQueryBase {
}
}
+ private function getVariantTitles() {
+ if ( !count( $this->titles ) ) {
+ return;
+ }
+ $this->variantTitles = [];
+ foreach ( $this->titles as $pageId => $t ) {
+ $this->variantTitles[$pageId] = isset( $this->displaytitles[$pageId] )
+ ? $this->getAllVariants( $this->displaytitles[$pageId] )
+ : $this->getAllVariants( $t->getText(), $t->getNamespace() );
+ }
+ }
+
+ private function getAllVariants( $text, $ns = NS_MAIN ) {
+ global $wgContLang;
+ $result = [];
+ foreach ( $wgContLang->getVariants() as $variant ) {
+ $convertTitle = $wgContLang->autoConvert( $text, $variant );
+ if ( $ns !== NS_MAIN ) {
+ $convertNs = $wgContLang->convertNamespace( $ns, $variant );
+ $convertTitle = $convertNs . ':' . $convertTitle;
+ }
+ $result[$variant] = $convertTitle;
+ }
+ return $result;
+ }
+
/**
* Get information about watched status and put it in $this->watched
* and $this->notificationtimestamps
@@ -883,6 +916,7 @@ class ApiQueryInfo extends ApiQueryBase {
'url',
'preload',
'displaytitle',
+ 'varianttitles',
];
if ( array_diff( (array)$params['prop'], $publicProps ) ) {
return 'private';
@@ -916,6 +950,7 @@ class ApiQueryInfo extends ApiQueryBase {
'readable', # private
'preload',
'displaytitle',
+ 'varianttitles',
// If you add more properties here, please consider whether they
// need to be added to getCacheMode()
],
diff --git a/www/wiki/includes/api/ApiQueryLangBacklinks.php b/www/wiki/includes/api/ApiQueryLangBacklinks.php
index fd67d7c4..2e19783c 100644
--- a/www/wiki/includes/api/ApiQueryLangBacklinks.php
+++ b/www/wiki/includes/api/ApiQueryLangBacklinks.php
@@ -2,8 +2,6 @@
/**
* API for MediaWiki 1.17+
*
- * Created on May 14, 2011
- *
* Copyright © 2011 Sam Reed
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
@@ -132,7 +130,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
if ( !is_null( $resultPageSet ) ) {
$pages[] = Title::newFromRow( $row );
} else {
- $entry = [ 'pageid' => $row->page_id ];
+ $entry = [ 'pageid' => (int)$row->page_id ];
$title = Title::makeTitle( $row->page_namespace, $row->page_title );
ApiQueryBase::addTitleInfo( $entry, $title );
diff --git a/www/wiki/includes/api/ApiQueryLangLinks.php b/www/wiki/includes/api/ApiQueryLangLinks.php
index df33d027..2d8e44cd 100644
--- a/www/wiki/includes/api/ApiQueryLangLinks.php
+++ b/www/wiki/includes/api/ApiQueryLangLinks.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on May 13, 2007
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryLinks.php b/www/wiki/includes/api/ApiQueryLinks.php
index 4b340912..67bf619c 100644
--- a/www/wiki/includes/api/ApiQueryLinks.php
+++ b/www/wiki/includes/api/ApiQueryLinks.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on May 12, 2007
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -87,22 +83,34 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
$this->addTables( $this->table );
$this->addWhereFld( $this->prefix . '_from', array_keys( $this->getPageSet()->getGoodTitles() ) );
- $this->addWhereFld( $this->prefix . '_namespace', $params['namespace'] );
- if ( !is_null( $params[$this->titlesParam] ) ) {
+ $multiNS = true;
+ $multiTitle = true;
+ if ( $params[$this->titlesParam] ) {
+ // Filter the titles in PHP so our ORDER BY bug avoidance below works right.
+ $filterNS = $params['namespace'] ? array_flip( $params['namespace'] ) : false;
+
$lb = new LinkBatch;
foreach ( $params[$this->titlesParam] as $t ) {
$title = Title::newFromText( $t );
if ( !$title ) {
$this->addWarning( [ 'apiwarn-invalidtitle', wfEscapeWikiText( $t ) ] );
- } else {
+ } elseif ( !$filterNS || isset( $filterNS[$title->getNamespace()] ) ) {
$lb->addObj( $title );
}
}
$cond = $lb->constructSet( $this->prefix, $this->getDB() );
if ( $cond ) {
$this->addWhere( $cond );
+ $multiNS = count( $lb->data ) !== 1;
+ $multiTitle = count( call_user_func_array( 'array_merge', $lb->data ) ) !== 1;
+ } else {
+ // No titles so no results
+ return;
}
+ } elseif ( $params['namespace'] ) {
+ $this->addWhereFld( $this->prefix . '_namespace', $params['namespace'] );
+ $multiNS = $params['namespace'] === null || count( $params['namespace'] ) !== 1;
}
if ( !is_null( $params['continue'] ) ) {
@@ -131,12 +139,15 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
if ( count( $this->getPageSet()->getGoodTitles() ) != 1 ) {
$order[] = $this->prefix . '_from' . $sort;
}
- if ( count( $params['namespace'] ) != 1 ) {
+ if ( $multiNS ) {
$order[] = $this->prefix . '_namespace' . $sort;
}
-
- $order[] = $this->prefix . '_title' . $sort;
- $this->addOption( 'ORDER BY', $order );
+ if ( $multiTitle ) {
+ $order[] = $this->prefix . '_title' . $sort;
+ }
+ if ( $order ) {
+ $this->addOption( 'ORDER BY', $order );
+ }
$this->addOption( 'LIMIT', $params['limit'] + 1 );
$res = $this->select( __METHOD__ );
diff --git a/www/wiki/includes/api/ApiQueryLogEvents.php b/www/wiki/includes/api/ApiQueryLogEvents.php
index 3066720d..84e12d7d 100644
--- a/www/wiki/includes/api/ApiQueryLogEvents.php
+++ b/www/wiki/includes/api/ApiQueryLogEvents.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Oct 16, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -45,7 +41,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
public function execute() {
$params = $this->extractRequestParams();
$db = $this->getDB();
- $this->commentStore = new CommentStore( 'log_comment' );
+ $this->commentStore = CommentStore::getStore();
$this->requireMaxOneParameter( $params, 'title', 'prefix', 'namespace' );
$prop = array_flip( $params['prop'] );
@@ -66,11 +62,15 @@ class ApiQueryLogEvents extends ApiQueryBase {
$this->addWhere( $hideLogs );
}
- // Order is significant here
- $this->addTables( [ 'logging', 'user', 'page' ] );
+ $actorMigration = ActorMigration::newMigration();
+ $actorQuery = $actorMigration->getJoin( 'log_user' );
+ $this->addTables( 'logging' );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addTables( [ 'user', 'page' ] );
+ $this->addJoinConds( $actorQuery['joins'] );
$this->addJoinConds( [
'user' => [ 'LEFT JOIN',
- 'user_id=log_user' ],
+ 'user_id=' . $actorQuery['fields']['log_user'] ],
'page' => [ 'LEFT JOIN',
[ 'log_namespace=page_namespace',
'log_title=page_title' ] ] ] );
@@ -88,8 +88,8 @@ class ApiQueryLogEvents extends ApiQueryBase {
// join at query time. This leads to different results in various
// scenarios, e.g. deletion, recreation.
$this->addFieldsIf( 'log_page', $this->fld_ids );
- $this->addFieldsIf( [ 'log_user', 'log_user_text', 'user_name' ], $this->fld_user );
- $this->addFieldsIf( 'log_user', $this->fld_userid );
+ $this->addFieldsIf( $actorQuery['fields'] + [ 'user_name' ], $this->fld_user );
+ $this->addFieldsIf( $actorQuery['fields'], $this->fld_userid );
$this->addFieldsIf(
[ 'log_namespace', 'log_title' ],
$this->fld_title || $this->fld_parsedcomment
@@ -97,7 +97,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
$this->addFieldsIf( 'log_params', $this->fld_details );
if ( $this->fld_comment || $this->fld_parsedcomment ) {
- $commentQuery = $this->commentStore->getJoin();
+ $commentQuery = $this->commentStore->getJoin( 'log_comment' );
$this->addTables( $commentQuery['tables'] );
$this->addFields( $commentQuery['fields'] );
$this->addJoinConds( $commentQuery['joins'] );
@@ -170,12 +170,12 @@ class ApiQueryLogEvents extends ApiQueryBase {
$user = $params['user'];
if ( !is_null( $user ) ) {
- $userid = User::idFromName( $user );
- if ( $userid ) {
- $this->addWhereFld( 'log_user', $userid );
- } else {
- $this->addWhereFld( 'log_user_text', $user );
- }
+ // Note the joins in $q are the same as those from ->getJoin() above
+ // so we only need to add 'conds' here.
+ $q = $actorMigration->getWhere(
+ $db, 'log_user', User::newFromName( $params['user'], false )
+ );
+ $this->addWhere( $q['conds'] );
}
$title = $params['title'];
@@ -342,7 +342,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
$anyHidden = true;
}
if ( LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $user ) ) {
- $comment = $this->commentStore->getComment( $row )->text;
+ $comment = $this->commentStore->getComment( 'log_comment', $row )->text;
if ( $this->fld_comment ) {
$vals['comment'] = $comment;
}
diff --git a/www/wiki/includes/api/ApiQueryPagePropNames.php b/www/wiki/includes/api/ApiQueryPagePropNames.php
index 2d56983c..f280ef06 100644
--- a/www/wiki/includes/api/ApiQueryPagePropNames.php
+++ b/www/wiki/includes/api/ApiQueryPagePropNames.php
@@ -1,7 +1,5 @@
<?php
/**
- * Created on January 21, 2013
- *
* Copyright © 2013 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryPageProps.php b/www/wiki/includes/api/ApiQueryPageProps.php
index e49dfbcf..2bee6983 100644
--- a/www/wiki/includes/api/ApiQueryPageProps.php
+++ b/www/wiki/includes/api/ApiQueryPageProps.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Aug 7, 2010
- *
* Copyright © 2010 soxred93, Bryan Tong Minh
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryPagesWithProp.php b/www/wiki/includes/api/ApiQueryPagesWithProp.php
index 97f79b66..06b412f8 100644
--- a/www/wiki/includes/api/ApiQueryPagesWithProp.php
+++ b/www/wiki/includes/api/ApiQueryPagesWithProp.php
@@ -1,7 +1,5 @@
<?php
/**
- * Created on December 31, 2012
- *
* Copyright © 2012 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryProtectedTitles.php b/www/wiki/includes/api/ApiQueryProtectedTitles.php
index b69a2996..f5266850 100644
--- a/www/wiki/includes/api/ApiQueryProtectedTitles.php
+++ b/www/wiki/includes/api/ApiQueryProtectedTitles.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Feb 13, 2009
- *
* Copyright © 2009 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -59,8 +55,8 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
$this->addFieldsIf( 'pt_create_perm', isset( $prop['level'] ) );
if ( isset( $prop['comment'] ) || isset( $prop['parsedcomment'] ) ) {
- $commentStore = new CommentStore( 'pt_reason' );
- $commentQuery = $commentStore->getJoin();
+ $commentStore = CommentStore::getStore();
+ $commentQuery = $commentStore->getJoin( 'pt_reason' );
$this->addTables( $commentQuery['tables'] );
$this->addFields( $commentQuery['fields'] );
$this->addJoinConds( $commentQuery['joins'] );
@@ -134,12 +130,12 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
}
if ( isset( $prop['comment'] ) ) {
- $vals['comment'] = $commentStore->getComment( $row )->text;
+ $vals['comment'] = $commentStore->getComment( 'pt_reason', $row )->text;
}
if ( isset( $prop['parsedcomment'] ) ) {
$vals['parsedcomment'] = Linker::formatComment(
- $commentStore->getComment( $row )->text, $titles
+ $commentStore->getComment( 'pt_reason', $row )->text, $titles
);
}
diff --git a/www/wiki/includes/api/ApiQueryQueryPage.php b/www/wiki/includes/api/ApiQueryQueryPage.php
index 46c22655..a828e117 100644
--- a/www/wiki/includes/api/ApiQueryQueryPage.php
+++ b/www/wiki/includes/api/ApiQueryQueryPage.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Dec 22, 2010
- *
* Copyright © 2010 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryRandom.php b/www/wiki/includes/api/ApiQueryRandom.php
index ce62226f..636191d3 100644
--- a/www/wiki/includes/api/ApiQueryRandom.php
+++ b/www/wiki/includes/api/ApiQueryRandom.php
@@ -1,10 +1,6 @@
<?php
/**
- *
- *
- * Created on Monday, January 28, 2008
- *
* Copyright © 2008 Brent Garber
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryRecentChanges.php b/www/wiki/includes/api/ApiQueryRecentChanges.php
index 63e07487..864b1828 100644
--- a/www/wiki/includes/api/ApiQueryRecentChanges.php
+++ b/www/wiki/includes/api/ApiQueryRecentChanges.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Oct 19, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -65,7 +61,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
}
$this->tokenFunctions = [
- 'patrol' => [ 'ApiQueryRecentChanges', 'getPatrolToken' ]
+ 'patrol' => [ self::class, 'getPatrolToken' ]
];
Hooks::run( 'APIQueryRecentChangesTokens', [ &$this->tokenFunctions ] );
@@ -196,15 +192,15 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
|| ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
|| ( isset( $show['patrolled'] ) && isset( $show['unpatrolled'] ) )
|| ( isset( $show['!patrolled'] ) && isset( $show['unpatrolled'] ) )
+ || ( isset( $show['autopatrolled'] ) && isset( $show['!autopatrolled'] ) )
+ || ( isset( $show['autopatrolled'] ) && isset( $show['unpatrolled'] ) )
+ || ( isset( $show['autopatrolled'] ) && isset( $show['!patrolled'] ) )
) {
$this->dieWithError( 'apierror-show' );
}
// Check permissions
- if ( isset( $show['patrolled'] )
- || isset( $show['!patrolled'] )
- || isset( $show['unpatrolled'] )
- ) {
+ if ( $this->includesPatrollingFlags( $show ) ) {
if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
$this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
}
@@ -215,8 +211,18 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
$this->addWhereIf( 'rc_minor != 0', isset( $show['minor'] ) );
$this->addWhereIf( 'rc_bot = 0', isset( $show['!bot'] ) );
$this->addWhereIf( 'rc_bot != 0', isset( $show['bot'] ) );
- $this->addWhereIf( 'rc_user = 0', isset( $show['anon'] ) );
- $this->addWhereIf( 'rc_user != 0', isset( $show['!anon'] ) );
+ if ( isset( $show['anon'] ) || isset( $show['!anon'] ) ) {
+ $actorMigration = ActorMigration::newMigration();
+ $actorQuery = $actorMigration->getJoin( 'rc_user' );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhereIf(
+ $actorMigration->isAnon( $actorQuery['fields']['rc_user'] ), isset( $show['anon'] )
+ );
+ $this->addWhereIf(
+ $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] ), isset( $show['!anon'] )
+ );
+ }
$this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
$this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
$this->addWhereIf( 'page_is_redirect = 1', isset( $show['redirect'] ) );
@@ -224,13 +230,22 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
if ( isset( $show['unpatrolled'] ) ) {
// See ChangesList::isUnpatrolled
if ( $user->useRCPatrol() ) {
- $this->addWhere( 'rc_patrolled = 0' );
+ $this->addWhere( 'rc_patrolled = ' . RecentChange::PRC_UNPATROLLED );
} elseif ( $user->useNPPatrol() ) {
- $this->addWhere( 'rc_patrolled = 0' );
+ $this->addWhere( 'rc_patrolled = ' . RecentChange::PRC_UNPATROLLED );
$this->addWhereFld( 'rc_type', RC_NEW );
}
}
+ $this->addWhereIf(
+ 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED,
+ isset( $show['!autopatrolled'] )
+ );
+ $this->addWhereIf(
+ 'rc_patrolled = ' . RecentChange::PRC_AUTOPATROLLED,
+ isset( $show['autopatrolled'] )
+ );
+
// Don't throw log entries out the window here
$this->addWhereIf(
'page_is_redirect = 0 OR page_is_redirect IS NULL',
@@ -241,14 +256,21 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
$this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
if ( !is_null( $params['user'] ) ) {
- $this->addWhereFld( 'rc_user_text', $params['user'] );
+ // Don't query by user ID here, it might be able to use the rc_user_text index.
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $this->getDB(), 'rc_user', User::newFromName( $params['user'], false ), false );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( $actorQuery['conds'] );
}
if ( !is_null( $params['excludeuser'] ) ) {
- // We don't use the rc_user_text index here because
- // * it would require us to sort by rc_user_text before rc_timestamp
- // * the != condition doesn't throw out too many rows anyway
- $this->addWhere( 'rc_user_text != ' . $this->getDB()->addQuotes( $params['excludeuser'] ) );
+ // Here there's no chance to use the rc_user_text index, so allow ID to be used.
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $this->getDB(), 'rc_user', User::newFromName( $params['excludeuser'], false ) );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
}
/* Add the fields we're concerned with to our query. */
@@ -276,8 +298,12 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
/* Add fields to our query if they are specified as a needed parameter. */
$this->addFieldsIf( [ 'rc_this_oldid', 'rc_last_oldid' ], $this->fld_ids );
- $this->addFieldsIf( 'rc_user', $this->fld_user || $this->fld_userid );
- $this->addFieldsIf( 'rc_user_text', $this->fld_user );
+ if ( $this->fld_user || $this->fld_userid ) {
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addFields( $actorQuery['fields'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ }
$this->addFieldsIf( [ 'rc_minor', 'rc_type', 'rc_bot' ], $this->fld_flags );
$this->addFieldsIf( [ 'rc_old_len', 'rc_new_len' ], $this->fld_sizes );
$this->addFieldsIf( [ 'rc_patrolled', 'rc_log_type' ], $this->fld_patrolled );
@@ -354,8 +380,8 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
$this->token = $params['token'];
if ( $this->fld_comment || $this->fld_parsedcomment || $this->token ) {
- $this->commentStore = new CommentStore( 'rc_comment' );
- $commentQuery = $this->commentStore->getJoin();
+ $this->commentStore = CommentStore::getStore();
+ $commentQuery = $this->commentStore->getJoin( 'rc_comment' );
$this->addTables( $commentQuery['tables'] );
$this->addFields( $commentQuery['fields'] );
$this->addJoinConds( $commentQuery['joins'] );
@@ -510,7 +536,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
$anyHidden = true;
}
if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_COMMENT, $user ) ) {
- $comment = $this->commentStore->getComment( $row )->text;
+ $comment = $this->commentStore->getComment( 'rc_comment', $row )->text;
if ( $this->fld_comment ) {
$vals['comment'] = $comment;
}
@@ -527,8 +553,9 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
/* Add the patrolled flag */
if ( $this->fld_patrolled ) {
- $vals['patrolled'] = $row->rc_patrolled == 1;
+ $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
$vals['unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
+ $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
}
if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) {
@@ -588,13 +615,23 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
return $vals;
}
+ /**
+ * @param array $flagsArray flipped array (string flags are keys)
+ * @return bool
+ */
+ private function includesPatrollingFlags( array $flagsArray ) {
+ return isset( $flagsArray['patrolled'] ) ||
+ isset( $flagsArray['!patrolled'] ) ||
+ isset( $flagsArray['unpatrolled'] ) ||
+ isset( $flagsArray['autopatrolled'] ) ||
+ isset( $flagsArray['!autopatrolled'] );
+ }
+
public function getCacheMode( $params ) {
- if ( isset( $params['show'] ) ) {
- foreach ( $params['show'] as $show ) {
- if ( $show === 'patrolled' || $show === '!patrolled' ) {
- return 'private';
- }
- }
+ if ( isset( $params['show'] ) &&
+ $this->includesPatrollingFlags( array_flip( $params['show'] ) )
+ ) {
+ return 'private';
}
if ( isset( $params['token'] ) ) {
return 'private';
@@ -677,7 +714,9 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
'!redirect',
'patrolled',
'!patrolled',
- 'unpatrolled'
+ 'unpatrolled',
+ 'autopatrolled',
+ '!autopatrolled',
]
],
'limit' => [
diff --git a/www/wiki/includes/api/ApiQueryRevisions.php b/www/wiki/includes/api/ApiQueryRevisions.php
index 2dfa42a3..5858bc72 100644
--- a/www/wiki/includes/api/ApiQueryRevisions.php
+++ b/www/wiki/includes/api/ApiQueryRevisions.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 7, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -60,7 +56,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
}
$this->tokenFunctions = [
- 'rollback' => [ 'ApiQueryRevisions', 'getRollbackToken' ]
+ 'rollback' => [ self::class, 'getRollbackToken' ]
];
Hooks::run( 'APIQueryRevisionsTokens', [ &$this->tokenFunctions ] );
@@ -129,20 +125,31 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
}
$db = $this->getDB();
- $this->addTables( [ 'revision', 'page' ] );
- $this->addJoinConds(
- [ 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ] ]
- );
if ( $resultPageSet === null ) {
$this->parseParameters( $params );
$this->token = $params['token'];
- $this->addFields( Revision::selectFields() );
+ $opts = [];
if ( $this->token !== null || $pageCount > 0 ) {
- $this->addFields( Revision::selectPageFields() );
+ $opts[] = 'page';
+ }
+ if ( $this->fetchContent ) {
+ $opts[] = 'text';
}
+ if ( $this->fld_user ) {
+ $opts[] = 'user';
+ }
+ $revQuery = Revision::getQueryInfo( $opts );
+ $this->addTables( $revQuery['tables'] );
+ $this->addFields( $revQuery['fields'] );
+ $this->addJoinConds( $revQuery['joins'] );
} else {
$this->limit = $this->getParameter( 'limit' ) ?: 10;
+ // Always join 'page' so orphaned revisions are filtered out
+ $this->addTables( [ 'revision', 'page' ] );
+ $this->addJoinConds(
+ [ 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ] ]
+ );
$this->addFields( [ 'rev_id', 'rev_timestamp', 'rev_page' ] );
}
@@ -162,7 +169,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
$this->addWhereFld( 'ct_tag', $params['tag'] );
}
- if ( $this->fetchContent ) {
+ if ( $resultPageSet === null && $this->fetchContent ) {
// For each page we will request, the user must have read rights for that page
$user = $this->getUser();
$status = Status::newGood();
@@ -178,20 +185,6 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
if ( !$status->isGood() ) {
$this->dieStatus( $status );
}
-
- $this->addTables( 'text' );
- $this->addJoinConds(
- [ 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ] ]
- );
- $this->addFields( 'old_id' );
- $this->addFields( Revision::selectTextFields() );
- }
-
- // add user name, if needed
- if ( $this->fld_user ) {
- $this->addTables( 'user' );
- $this->addJoinConds( [ 'user' => Revision::userJoinCond() ] );
- $this->addFields( Revision::selectUserFields() );
}
if ( $enumRevMode ) {
@@ -293,20 +286,17 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
$this->addWhereFld( 'rev_page', reset( $ids ) );
if ( $params['user'] !== null ) {
- $user = User::newFromName( $params['user'] );
- if ( $user && $user->getId() > 0 ) {
- $this->addWhereFld( 'rev_user', $user->getId() );
- } else {
- $this->addWhereFld( 'rev_user_text', $params['user'] );
- }
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $db, 'rev_user', User::newFromName( $params['user'], false ) );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( $actorQuery['conds'] );
} elseif ( $params['excludeuser'] !== null ) {
- $user = User::newFromName( $params['excludeuser'] );
- if ( $user && $user->getId() > 0 ) {
- $this->addWhere( 'rev_user != ' . $user->getId() );
- } else {
- $this->addWhere( 'rev_user_text != ' .
- $db->addQuotes( $params['excludeuser'] ) );
- }
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $db, 'rev_user', User::newFromName( $params['excludeuser'], false ) );
+ $this->addTables( $actorQuery['tables'] );
+ $this->addJoinConds( $actorQuery['joins'] );
+ $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
}
if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
// Paranoia: avoid brute force searches (T19342)
diff --git a/www/wiki/includes/api/ApiQueryRevisionsBase.php b/www/wiki/includes/api/ApiQueryRevisionsBase.php
index 2ffd0248..f888434d 100644
--- a/www/wiki/includes/api/ApiQueryRevisionsBase.php
+++ b/www/wiki/includes/api/ApiQueryRevisionsBase.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Oct 3, 2014 as a split from ApiQueryRevisions
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQuerySearch.php b/www/wiki/includes/api/ApiQuerySearch.php
index f0c41800..7d46a5fb 100644
--- a/www/wiki/includes/api/ApiQuerySearch.php
+++ b/www/wiki/includes/api/ApiQuerySearch.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on July 30, 2007
- *
* Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -272,6 +268,16 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
if ( isset( $prop['isfilematch'] ) ) {
$vals['isfilematch'] = $result->isFileMatch();
}
+
+ if ( isset( $prop['extensiondata'] ) ) {
+ $extra = $result->getExtensionData();
+ // Add augmented data to the result. The data would be organized as a map:
+ // augmentorName => data
+ if ( $extra ) {
+ $vals['extensiondata'] = ApiResult::addMetadataToResultVars( $extra );
+ }
+ }
+
return $vals;
}
@@ -372,6 +378,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
'categorysnippet',
'score', // deprecated
'hasrelated', // deprecated
+ 'extensiondata',
],
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
diff --git a/www/wiki/includes/api/ApiQuerySiteinfo.php b/www/wiki/includes/api/ApiQuerySiteinfo.php
index 6b896c95..30482732 100644
--- a/www/wiki/includes/api/ApiQuerySiteinfo.php
+++ b/www/wiki/includes/api/ApiQuerySiteinfo.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 25, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -283,6 +279,8 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data['interwikimagic'] = (bool)$config->get( 'InterwikiMagic' );
$data['magiclinks'] = $config->get( 'EnableMagicLinks' );
+ $data['categorycollation'] = $config->get( 'CategoryCollation' );
+
Hooks::run( 'APIQuerySiteInfoGeneralInfo', [ $this, &$data ] );
return $this->getResult()->addValue( 'query', $property, $data );
@@ -447,7 +445,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
protected function appendDbReplLagInfo( $property, $includeAll ) {
$data = [];
- $lb = wfGetLB();
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
$showHostnames = $this->getConfig()->get( 'ShowHostnames' );
if ( $includeAll ) {
if ( !$showHostnames ) {
@@ -467,7 +465,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
'host' => $showHostnames
? $lb->getServerName( $index )
: '',
- 'lag' => intval( $lag )
+ 'lag' => $lag
];
}
@@ -850,7 +848,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
foreach ( $myWgHooks as $name => $subscribers ) {
$arr = [
'name' => $name,
- 'subscribers' => array_map( [ 'SpecialVersion', 'arrayToString' ], $subscribers ),
+ 'subscribers' => array_map( [ SpecialVersion::class, 'arrayToString' ], $subscribers ),
];
ApiResult::setArrayType( $arr['subscribers'], 'array' );
diff --git a/www/wiki/includes/api/ApiQueryTags.php b/www/wiki/includes/api/ApiQueryTags.php
index 1b154fae..ed5fe8a5 100644
--- a/www/wiki/includes/api/ApiQueryTags.php
+++ b/www/wiki/includes/api/ApiQueryTags.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Jul 9, 2009
- *
* Copyright © 2009
*
* This program is free software; you can redistribute it and/or modify
@@ -141,9 +137,8 @@ class ApiQueryTags extends ApiQueryBase {
ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
],
'prop' => [
- ApiBase::PARAM_DFLT => 'name',
+ ApiBase::PARAM_DFLT => '',
ApiBase::PARAM_TYPE => [
- 'name',
'displayname',
'description',
'hitcount',
diff --git a/www/wiki/includes/api/ApiQueryTokens.php b/www/wiki/includes/api/ApiQueryTokens.php
index 0e46fd05..e86fdf35 100644
--- a/www/wiki/includes/api/ApiQueryTokens.php
+++ b/www/wiki/includes/api/ApiQueryTokens.php
@@ -2,8 +2,6 @@
/**
* Module to fetch tokens via action=query&meta=tokens
*
- * Created on August 8, 2014
- *
* Copyright © 2014 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryUserContributions.php b/www/wiki/includes/api/ApiQueryUserContributions.php
index bb0f335b..12f42edc 100644
--- a/www/wiki/includes/api/ApiQueryUserContributions.php
+++ b/www/wiki/includes/api/ApiQueryUserContributions.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Oct 16, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -35,17 +31,18 @@ class ApiQueryContributions extends ApiQueryBase {
parent::__construct( $query, $moduleName, 'uc' );
}
- private $params, $prefixMode, $userprefix, $multiUserMode, $idMode, $usernames, $userids,
- $parentLens, $commentStore;
+ private $params, $multiUserMode, $orderBy, $parentLens;
private $fld_ids = false, $fld_title = false, $fld_timestamp = false,
$fld_comment = false, $fld_parsedcomment = false, $fld_flags = false,
$fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false;
public function execute() {
+ global $wgActorTableSchemaMigrationStage;
+
// Parse some parameters
$this->params = $this->extractRequestParams();
- $this->commentStore = new CommentStore( 'rev_comment' );
+ $this->commentStore = CommentStore::getStore();
$prop = array_flip( $this->params['prop'] );
$this->fld_ids = isset( $prop['ids'] );
@@ -67,36 +64,174 @@ class ApiQueryContributions extends ApiQueryBase {
// TODO: if the query is going only against the revision table, should this be done?
$this->selectNamedDB( 'contributions', DB_REPLICA, 'contributions' );
- $this->requireOnlyOneParameter( $this->params, 'userprefix', 'userids', 'user' );
+ $sort = ( $this->params['dir'] == 'newer' ? '' : ' DESC' );
+ $op = ( $this->params['dir'] == 'older' ? '<' : '>' );
- $this->idMode = false;
+ // Create an Iterator that produces the UserIdentity objects we need, depending
+ // on which of the 'userprefix', 'userids', or 'user' params was
+ // specified.
+ $this->requireOnlyOneParameter( $this->params, 'userprefix', 'userids', 'user' );
if ( isset( $this->params['userprefix'] ) ) {
- $this->prefixMode = true;
$this->multiUserMode = true;
- $this->userprefix = $this->params['userprefix'];
- } elseif ( isset( $this->params['userids'] ) ) {
- $this->userids = [];
+ $this->orderBy = 'name';
+ $fname = __METHOD__;
+
+ // Because 'userprefix' might produce a huge number of users (e.g.
+ // a wiki with users "Test00000001" to "Test99999999"), use a
+ // generator with batched lookup and continuation.
+ $userIter = call_user_func( function () use ( $dbSecondary, $sort, $op, $fname ) {
+ global $wgActorTableSchemaMigrationStage;
+
+ $fromName = false;
+ if ( !is_null( $this->params['continue'] ) ) {
+ $continue = explode( '|', $this->params['continue'] );
+ $this->dieContinueUsageIf( count( $continue ) != 4 );
+ $this->dieContinueUsageIf( $continue[0] !== 'name' );
+ $fromName = $continue[1];
+ }
+ $like = $dbSecondary->buildLike( $this->params['userprefix'], $dbSecondary->anyString() );
+
+ $limit = 501;
+
+ do {
+ $from = $fromName ? "$op= " . $dbSecondary->addQuotes( $fromName ) : false;
+
+ // For the new schema, pull from the actor table. For the
+ // old, pull from rev_user. For migration a FULL [OUTER]
+ // JOIN would be what we want, except MySQL doesn't support
+ // that so we have to UNION instead.
+ if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+ $res = $dbSecondary->select(
+ 'actor',
+ [ 'actor_id', 'user_id' => 'COALESCE(actor_user,0)', 'user_name' => 'actor_name' ],
+ array_merge( [ "actor_name$like" ], $from ? [ "actor_name $from" ] : [] ),
+ $fname,
+ [ 'ORDER BY' => [ "user_name $sort" ], 'LIMIT' => $limit ]
+ );
+ } elseif ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
+ $res = $dbSecondary->select(
+ 'revision',
+ [ 'actor_id' => 'NULL', 'user_id' => 'rev_user', 'user_name' => 'rev_user_text' ],
+ array_merge( [ "rev_user_text$like" ], $from ? [ "rev_user_text $from" ] : [] ),
+ $fname,
+ [ 'DISTINCT', 'ORDER BY' => [ "rev_user_text $sort" ], 'LIMIT' => $limit ]
+ );
+ } else {
+ // There are three queries we have to combine to be sure of getting all results:
+ // - actor table (any rows that have been migrated will have empty rev_user_text)
+ // - revision+actor by user id
+ // - revision+actor by name for anons
+ $options = $dbSecondary->unionSupportsOrderAndLimit()
+ ? [ 'ORDER BY' => [ "user_name $sort" ], 'LIMIT' => $limit ] : [];
+ $subsql = [];
+ $subsql[] = $dbSecondary->selectSQLText(
+ 'actor',
+ [ 'actor_id', 'user_id' => 'COALESCE(actor_user,0)', 'user_name' => 'actor_name' ],
+ array_merge( [ "actor_name$like" ], $from ? [ "actor_name $from" ] : [] ),
+ $fname,
+ $options
+ );
+ $subsql[] = $dbSecondary->selectSQLText(
+ [ 'revision', 'actor' ],
+ [ 'actor_id', 'user_id' => 'rev_user', 'user_name' => 'rev_user_text' ],
+ array_merge(
+ [ "rev_user_text$like", 'rev_user != 0' ],
+ $from ? [ "rev_user_text $from" ] : []
+ ),
+ $fname,
+ array_merge( [ 'DISTINCT' ], $options ),
+ [ 'actor' => [ 'LEFT JOIN', 'rev_user = actor_user' ] ]
+ );
+ $subsql[] = $dbSecondary->selectSQLText(
+ [ 'revision', 'actor' ],
+ [ 'actor_id', 'user_id' => 'rev_user', 'user_name' => 'rev_user_text' ],
+ array_merge(
+ [ "rev_user_text$like", 'rev_user = 0' ],
+ $from ? [ "rev_user_text $from" ] : []
+ ),
+ $fname,
+ array_merge( [ 'DISTINCT' ], $options ),
+ [ 'actor' => [ 'LEFT JOIN', 'rev_user_text = actor_name' ] ]
+ );
+ $sql = $dbSecondary->unionQueries( $subsql, false ) . " ORDER BY user_name $sort";
+ $sql = $dbSecondary->limitResult( $sql, $limit );
+ $res = $dbSecondary->query( $sql, $fname );
+ }
+ $count = 0;
+ $fromName = false;
+ foreach ( $res as $row ) {
+ if ( ++$count >= $limit ) {
+ $fromName = $row->user_name;
+ break;
+ }
+ yield User::newFromRow( $row );
+ }
+ } while ( $fromName !== false );
+ } );
+ // Do the actual sorting client-side, because otherwise
+ // prepareQuery might try to sort by actor and confuse everything.
+ $batchSize = 1;
+ } elseif ( isset( $this->params['userids'] ) ) {
if ( !count( $this->params['userids'] ) ) {
$encParamName = $this->encodeParamName( 'userids' );
$this->dieWithError( [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName" );
}
+ $ids = [];
foreach ( $this->params['userids'] as $uid ) {
if ( $uid <= 0 ) {
$this->dieWithError( [ 'apierror-invaliduserid', $uid ], 'invaliduserid' );
}
+ $ids[] = $uid;
+ }
- $this->userids[] = $uid;
+ $this->orderBy = 'id';
+ $this->multiUserMode = count( $ids ) > 1;
+
+ $from = $fromId = false;
+ if ( $this->multiUserMode && !is_null( $this->params['continue'] ) ) {
+ $continue = explode( '|', $this->params['continue'] );
+ $this->dieContinueUsageIf( count( $continue ) != 4 );
+ $this->dieContinueUsageIf( $continue[0] !== 'id' && $continue[0] !== 'actor' );
+ $fromId = (int)$continue[1];
+ $this->dieContinueUsageIf( $continue[1] !== (string)$fromId );
+ $from = "$op= $fromId";
}
- $this->prefixMode = false;
- $this->multiUserMode = ( count( $this->params['userids'] ) > 1 );
- $this->idMode = true;
+ // For the new schema, just select from the actor table. For the
+ // old and transitional schemas, select from user and left join
+ // actor if it exists.
+ if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+ $res = $dbSecondary->select(
+ 'actor',
+ [ 'actor_id', 'user_id' => 'actor_user', 'user_name' => 'actor_name' ],
+ array_merge( [ 'actor_user' => $ids ], $from ? [ "actor_id $from" ] : [] ),
+ __METHOD__,
+ [ 'ORDER BY' => "user_id $sort" ]
+ );
+ } elseif ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
+ $res = $dbSecondary->select(
+ 'user',
+ [ 'actor_id' => 'NULL', 'user_id' => 'user_id', 'user_name' => 'user_name' ],
+ array_merge( [ 'user_id' => $ids ], $from ? [ "user_id $from" ] : [] ),
+ __METHOD__,
+ [ 'ORDER BY' => "user_id $sort" ]
+ );
+ } else {
+ $res = $dbSecondary->select(
+ [ 'user', 'actor' ],
+ [ 'actor_id', 'user_id', 'user_name' ],
+ array_merge( [ 'user_id' => $ids ], $from ? [ "user_id $from" ] : [] ),
+ __METHOD__,
+ [ 'ORDER BY' => "user_id $sort" ],
+ [ 'actor' => [ 'LEFT JOIN', 'actor_user = user_id' ] ]
+ );
+ }
+ $userIter = UserArray::newFromResult( $res );
+ $batchSize = count( $ids );
} else {
- $anyIPs = false;
- $this->userids = [];
- $this->usernames = [];
+ $names = [];
if ( !count( $this->params['user'] ) ) {
$encParamName = $this->encodeParamName( 'user' );
$this->dieWithError(
@@ -111,9 +246,8 @@ class ApiQueryContributions extends ApiQueryBase {
);
}
- if ( User::isIP( $u ) ) {
- $anyIPs = true;
- $this->usernames[] = $u;
+ if ( User::isIP( $u ) || ExternalUserNames::isExternal( $u ) ) {
+ $names[$u] = null;
} else {
$name = User::getCanonicalName( $u, 'valid' );
if ( $name === false ) {
@@ -122,94 +256,218 @@ class ApiQueryContributions extends ApiQueryBase {
[ 'apierror-baduser', $encParamName, wfEscapeWikiText( $u ) ], "baduser_$encParamName"
);
}
- $this->usernames[] = $name;
+ $names[$name] = null;
}
}
- $this->prefixMode = false;
- $this->multiUserMode = ( count( $this->params['user'] ) > 1 );
- if ( !$anyIPs ) {
- $dbr = $this->getDB();
- $res = $dbr->select( 'user', 'user_id', [ 'user_name' => $this->usernames ], __METHOD__ );
+ $this->orderBy = 'name';
+ $this->multiUserMode = count( $names ) > 1;
+
+ $from = $fromName = false;
+ if ( $this->multiUserMode && !is_null( $this->params['continue'] ) ) {
+ $continue = explode( '|', $this->params['continue'] );
+ $this->dieContinueUsageIf( count( $continue ) != 4 );
+ $this->dieContinueUsageIf( $continue[0] !== 'name' && $continue[0] !== 'actor' );
+ $fromName = $continue[1];
+ $from = "$op= " . $dbSecondary->addQuotes( $fromName );
+ }
+
+ // For the new schema, just select from the actor table. For the
+ // old and transitional schemas, select from user and left join
+ // actor if it exists then merge in any unknown users (IPs and imports).
+ if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+ $res = $dbSecondary->select(
+ 'actor',
+ [ 'actor_id', 'user_id' => 'actor_user', 'user_name' => 'actor_name' ],
+ array_merge( [ 'actor_name' => array_keys( $names ) ], $from ? [ "actor_id $from" ] : [] ),
+ __METHOD__,
+ [ 'ORDER BY' => "actor_name $sort" ]
+ );
+ $userIter = UserArray::newFromResult( $res );
+ } else {
+ if ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
+ $res = $dbSecondary->select(
+ 'user',
+ [ 'actor_id' => 'NULL', 'user_id', 'user_name' ],
+ array_merge( [ 'user_name' => array_keys( $names ) ], $from ? [ "user_name $from" ] : [] ),
+ __METHOD__
+ );
+ } else {
+ $res = $dbSecondary->select(
+ [ 'user', 'actor' ],
+ [ 'actor_id', 'user_id', 'user_name' ],
+ array_merge( [ 'user_name' => array_keys( $names ) ], $from ? [ "user_name $from" ] : [] ),
+ __METHOD__,
+ [],
+ [ 'actor' => [ 'LEFT JOIN', 'actor_user = user_id' ] ]
+ );
+ }
foreach ( $res as $row ) {
- $this->userids[] = $row->user_id;
+ $names[$row->user_name] = $row;
}
- $this->idMode = count( $this->userids ) === count( $this->usernames );
+ call_user_func_array(
+ $this->params['dir'] == 'newer' ? 'ksort' : 'krsort', [ &$names, SORT_STRING ]
+ );
+ $neg = $op === '>' ? -1 : 1;
+ $userIter = call_user_func( function () use ( $names, $fromName, $neg ) {
+ foreach ( $names as $name => $row ) {
+ if ( $fromName === false || $neg * strcmp( $name, $fromName ) <= 0 ) {
+ $user = $row ? User::newFromRow( $row ) : User::newFromName( $name, false );
+ yield $user;
+ }
+ }
+ } );
}
+ $batchSize = count( $names );
}
- $this->prepareQuery();
-
- $hookData = [];
- // Do the actual query.
- $res = $this->select( __METHOD__, [], $hookData );
+ // During migration, force ordering on the client side because we're
+ // having to combine multiple queries that would otherwise have
+ // different sort orders.
+ if ( $wgActorTableSchemaMigrationStage === MIGRATION_WRITE_BOTH ||
+ $wgActorTableSchemaMigrationStage === MIGRATION_WRITE_NEW
+ ) {
+ $batchSize = 1;
+ }
- if ( $this->fld_sizediff ) {
- $revIds = [];
- foreach ( $res as $row ) {
- if ( $row->rev_parent_id ) {
- $revIds[] = $row->rev_parent_id;
- }
- }
- $this->parentLens = Revision::getParentLengths( $dbSecondary, $revIds );
- $res->rewind(); // reset
+ // With the new schema, the DB query will order by actor so update $this->orderBy to match.
+ if ( $batchSize > 1 && $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+ $this->orderBy = 'actor';
}
- // Initialise some variables
$count = 0;
$limit = $this->params['limit'];
+ $userIter->rewind();
+ while ( $userIter->valid() ) {
+ $users = [];
+ while ( count( $users ) < $batchSize && $userIter->valid() ) {
+ $users[] = $userIter->current();
+ $userIter->next();
+ }
- // Fetch each row
- foreach ( $res as $row ) {
- if ( ++$count > $limit ) {
- // We've reached the one extra which shows that there are
- // additional pages to be had. Stop here...
- $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
- break;
+ // Ugh. We have to run the query three times, once for each
+ // possible 'orcond' from ActorMigration, and then merge them all
+ // together in the proper order. And preserving the correct
+ // $hookData for each one.
+ // @todo When ActorMigration is removed, this can go back to a
+ // single prepare and select.
+ $merged = [];
+ foreach ( [ 'actor', 'userid', 'username' ] as $which ) {
+ if ( $this->prepareQuery( $users, $limit - $count, $which ) ) {
+ $hookData = [];
+ $res = $this->select( __METHOD__, [], $hookData );
+ foreach ( $res as $row ) {
+ $merged[] = [ $row, &$hookData ];
+ }
+ }
}
+ $neg = $this->params['dir'] == 'newer' ? 1 : -1;
+ usort( $merged, function ( $a, $b ) use ( $neg, $batchSize ) {
+ if ( $batchSize === 1 ) { // One user, can't be different
+ $ret = 0;
+ } elseif ( $this->orderBy === 'id' ) {
+ $ret = $a[0]->rev_user - $b[0]->rev_user;
+ } elseif ( $this->orderBy === 'name' ) {
+ $ret = strcmp( $a[0]->rev_user_text, $b[0]->rev_user_text );
+ } else {
+ $ret = $a[0]->rev_actor - $b[0]->rev_actor;
+ }
+
+ if ( !$ret ) {
+ $ret = strcmp(
+ wfTimestamp( TS_MW, $a[0]->rev_timestamp ),
+ wfTimestamp( TS_MW, $b[0]->rev_timestamp )
+ );
+ }
+
+ if ( !$ret ) {
+ $ret = $a[0]->rev_id - $b[0]->rev_id;
+ }
+
+ return $neg * $ret;
+ } );
+ $merged = array_slice( $merged, 0, $limit - $count + 1 );
+ // (end "Ugh")
+
+ if ( $this->fld_sizediff ) {
+ $revIds = [];
+ foreach ( $merged as $data ) {
+ if ( $data[0]->rev_parent_id ) {
+ $revIds[] = $data[0]->rev_parent_id;
+ }
+ }
+ $this->parentLens = Revision::getParentLengths( $dbSecondary, $revIds );
+ }
+
+ foreach ( $merged as $data ) {
+ $row = $data[0];
+ $hookData = &$data[1];
+ if ( ++$count > $limit ) {
+ // We've reached the one extra which shows that there are
+ // additional pages to be had. Stop here...
+ $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
+ break 2;
+ }
- $vals = $this->extractRowInfo( $row );
- $fit = $this->processRow( $row, $vals, $hookData ) &&
- $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
- if ( !$fit ) {
- $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
- break;
+ $vals = $this->extractRowInfo( $row );
+ $fit = $this->processRow( $row, $vals, $hookData ) &&
+ $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
+ if ( !$fit ) {
+ $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
+ break 2;
+ }
}
}
- $this->getResult()->addIndexedTagName(
- [ 'query', $this->getModuleName() ],
- 'item'
- );
+ $this->getResult()->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
}
/**
* Prepares the query and returns the limit of rows requested
+ * @param User[] $users
+ * @param int $limit
+ * @param string $which 'actor', 'userid', or 'username'
+ * @return bool
*/
- private function prepareQuery() {
- // We're after the revision table, and the corresponding page
- // row for anything we retrieve. We may also need the
- // recentchanges row and/or tag summary row.
- $user = $this->getUser();
- $tables = [ 'page', 'revision' ]; // Order may change
- $this->addWhere( 'page_id=rev_page' );
+ private function prepareQuery( array $users, $limit, $which ) {
+ global $wgActorTableSchemaMigrationStage;
+
+ $this->resetQueryParams();
+ $db = $this->getDB();
+
+ $revQuery = Revision::getQueryInfo( [ 'page' ] );
+ $this->addTables( $revQuery['tables'] );
+ $this->addJoinConds( $revQuery['joins'] );
+ $this->addFields( $revQuery['fields'] );
+
+ $revWhere = ActorMigration::newMigration()->getWhere( $db, 'rev_user', $users );
+ if ( !isset( $revWhere['orconds'][$which] ) ) {
+ return false;
+ }
+ $this->addWhere( $revWhere['orconds'][$which] );
+
+ if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+ $orderUserField = 'rev_actor';
+ $userField = $this->orderBy === 'actor' ? 'revactor_actor' : 'actor_name';
+ } else {
+ $orderUserField = $this->orderBy === 'id' ? 'rev_user' : 'rev_user_text';
+ $userField = $revQuery['fields'][$orderUserField];
+ }
+ if ( $which === 'actor' ) {
+ $tsField = 'revactor_timestamp';
+ $idField = 'revactor_rev';
+ } else {
+ $tsField = 'rev_timestamp';
+ $idField = 'rev_id';
+ }
// Handle continue parameter
if ( !is_null( $this->params['continue'] ) ) {
$continue = explode( '|', $this->params['continue'] );
- $db = $this->getDB();
if ( $this->multiUserMode ) {
$this->dieContinueUsageIf( count( $continue ) != 4 );
$modeFlag = array_shift( $continue );
- $this->dieContinueUsageIf( !in_array( $modeFlag, [ 'id', 'name' ] ) );
- if ( $this->idMode && $modeFlag === 'name' ) {
- // The users were created since this query started, but we
- // can't go back and change modes now. So just keep on with
- // name mode.
- $this->idMode = false;
- }
- $this->dieContinueUsageIf( ( $modeFlag === 'id' ) !== $this->idMode );
- $userField = $this->idMode ? 'rev_user' : 'rev_user_text';
+ $this->dieContinueUsageIf( $modeFlag !== $this->orderBy );
$encUser = $db->addQuotes( array_shift( $continue ) );
} else {
$this->dieContinueUsageIf( count( $continue ) != 2 );
@@ -222,21 +480,22 @@ class ApiQueryContributions extends ApiQueryBase {
$this->addWhere(
"$userField $op $encUser OR " .
"($userField = $encUser AND " .
- "(rev_timestamp $op $encTS OR " .
- "(rev_timestamp = $encTS AND " .
- "rev_id $op= $encId)))"
+ "($tsField $op $encTS OR " .
+ "($tsField = $encTS AND " .
+ "$idField $op= $encId)))"
);
} else {
$this->addWhere(
- "rev_timestamp $op $encTS OR " .
- "(rev_timestamp = $encTS AND " .
- "rev_id $op= $encId)"
+ "$tsField $op $encTS OR " .
+ "($tsField = $encTS AND " .
+ "$idField $op= $encId)"
);
}
}
// Don't include any revisions where we're not supposed to be able to
// see the username.
+ $user = $this->getUser();
if ( !$user->isAllowed( 'deletedhistory' ) ) {
$bitmask = Revision::DELETED_USER;
} elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
@@ -245,29 +504,20 @@ class ApiQueryContributions extends ApiQueryBase {
$bitmask = 0;
}
if ( $bitmask ) {
- $this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
+ $this->addWhere( $db->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
}
- // We only want pages by the specified users.
- if ( $this->prefixMode ) {
- $this->addWhere( 'rev_user_text' .
- $this->getDB()->buildLike( $this->userprefix, $this->getDB()->anyString() ) );
- } elseif ( $this->idMode ) {
- $this->addWhereFld( 'rev_user', $this->userids );
- } else {
- $this->addWhereFld( 'rev_user_text', $this->usernames );
+ // Add the user field to ORDER BY if there are multiple users
+ if ( count( $users ) > 1 ) {
+ $this->addWhereRange( $orderUserField, $this->params['dir'], null, null );
}
- // ... and in the specified timeframe.
- // Ensure the same sort order for rev_user/rev_user_text and rev_timestamp
- // so our query is indexed
- if ( $this->multiUserMode ) {
- $this->addWhereRange( $this->idMode ? 'rev_user' : 'rev_user_text',
- $this->params['dir'], null, null );
- }
- $this->addTimestampWhereRange( 'rev_timestamp',
+
+ // Then timestamp
+ $this->addTimestampWhereRange( $tsField,
$this->params['dir'], $this->params['start'], $this->params['end'] );
- // Include in ORDER BY for uniqueness
- $this->addWhereRange( 'rev_id', $this->params['dir'], null, null );
+
+ // Then rev_id for a total ordering
+ $this->addWhereRange( $idField, $this->params['dir'], null, null );
$this->addWhereFld( 'page_namespace', $this->params['namespace'] );
@@ -280,6 +530,8 @@ class ApiQueryContributions extends ApiQueryBase {
if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
|| ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
+ || ( isset( $show['autopatrolled'] ) && isset( $show['!autopatrolled'] ) )
+ || ( isset( $show['autopatrolled'] ) && isset( $show['!patrolled'] ) )
|| ( isset( $show['top'] ) && isset( $show['!top'] ) )
|| ( isset( $show['new'] ) && isset( $show['!new'] ) )
) {
@@ -288,77 +540,57 @@ class ApiQueryContributions extends ApiQueryBase {
$this->addWhereIf( 'rev_minor_edit = 0', isset( $show['!minor'] ) );
$this->addWhereIf( 'rev_minor_edit != 0', isset( $show['minor'] ) );
- $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
- $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
- $this->addWhereIf( 'rev_id != page_latest', isset( $show['!top'] ) );
- $this->addWhereIf( 'rev_id = page_latest', isset( $show['top'] ) );
+ $this->addWhereIf(
+ 'rc_patrolled = ' . RecentChange::PRC_UNPATROLLED,
+ isset( $show['!patrolled'] )
+ );
+ $this->addWhereIf(
+ 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED,
+ isset( $show['patrolled'] )
+ );
+ $this->addWhereIf(
+ 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED,
+ isset( $show['!autopatrolled'] )
+ );
+ $this->addWhereIf(
+ 'rc_patrolled = ' . RecentChange::PRC_AUTOPATROLLED,
+ isset( $show['autopatrolled'] )
+ );
+ $this->addWhereIf( $idField . ' != page_latest', isset( $show['!top'] ) );
+ $this->addWhereIf( $idField . ' = page_latest', isset( $show['top'] ) );
$this->addWhereIf( 'rev_parent_id != 0', isset( $show['!new'] ) );
$this->addWhereIf( 'rev_parent_id = 0', isset( $show['new'] ) );
}
- $this->addOption( 'LIMIT', $this->params['limit'] + 1 );
-
- // Mandatory fields: timestamp allows request continuation
- // ns+title checks if the user has access rights for this page
- // user_text is necessary if multiple users were specified
- $this->addFields( [
- 'rev_id',
- 'rev_timestamp',
- 'page_namespace',
- 'page_title',
- 'rev_user',
- 'rev_user_text',
- 'rev_deleted'
- ] );
+ $this->addOption( 'LIMIT', $limit + 1 );
if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
- $this->fld_patrolled
+ isset( $show['autopatrolled'] ) || isset( $show['!autopatrolled'] ) || $this->fld_patrolled
) {
if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
$this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
}
- // Use a redundant join condition on both
- // timestamp and ID so we can use the timestamp
- // index
- $index['recentchanges'] = 'rc_user_text';
- if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) {
- // Put the tables in the right order for
- // STRAIGHT_JOIN
- $tables = [ 'revision', 'recentchanges', 'page' ];
- $this->addOption( 'STRAIGHT_JOIN' );
- $this->addWhere( 'rc_user_text=rev_user_text' );
- $this->addWhere( 'rc_timestamp=rev_timestamp' );
- $this->addWhere( 'rc_this_oldid=rev_id' );
- } else {
- $tables[] = 'recentchanges';
- $this->addJoinConds( [ 'recentchanges' => [
- 'LEFT JOIN', [
- 'rc_user_text=rev_user_text',
- 'rc_timestamp=rev_timestamp',
- 'rc_this_oldid=rev_id' ] ] ] );
- }
+ $isFilterset = isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
+ isset( $show['autopatrolled'] ) || isset( $show['!autopatrolled'] );
+ $this->addTables( 'recentchanges' );
+ $this->addJoinConds( [ 'recentchanges' => [
+ $isFilterset ? 'JOIN' : 'LEFT JOIN',
+ [
+ // This is a crazy hack. recentchanges has no index on rc_this_oldid, so instead of adding
+ // one T19237 did a join using rc_user_text and rc_timestamp instead. Now rc_user_text is
+ // probably unavailable, so just do rc_timestamp.
+ 'rc_timestamp = ' . $tsField,
+ 'rc_this_oldid = ' . $idField,
+ ]
+ ] ] );
}
- $this->addTables( $tables );
- $this->addFieldsIf( 'rev_page', $this->fld_ids );
- $this->addFieldsIf( 'page_latest', $this->fld_flags );
- // $this->addFieldsIf( 'rev_text_id', $this->fld_ids ); // Should this field be exposed?
- $this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff );
- $this->addFieldsIf( 'rev_minor_edit', $this->fld_flags );
- $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff || $this->fld_ids );
$this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
- if ( $this->fld_comment || $this->fld_parsedcomment ) {
- $commentQuery = $this->commentStore->getJoin();
- $this->addTables( $commentQuery['tables'] );
- $this->addFields( $commentQuery['fields'] );
- $this->addJoinConds( $commentQuery['joins'] );
- }
-
if ( $this->fld_tags ) {
$this->addTables( 'tag_summary' );
$this->addJoinConds(
- [ 'tag_summary' => [ 'LEFT JOIN', [ 'rev_id=ts_rev_id' ] ] ]
+ [ 'tag_summary' => [ 'LEFT JOIN', [ $idField . ' = ts_rev_id' ] ] ]
);
$this->addFields( 'ts_tags' );
}
@@ -366,14 +598,12 @@ class ApiQueryContributions extends ApiQueryBase {
if ( isset( $this->params['tag'] ) ) {
$this->addTables( 'change_tag' );
$this->addJoinConds(
- [ 'change_tag' => [ 'INNER JOIN', [ 'rev_id=ct_rev_id' ] ] ]
+ [ 'change_tag' => [ 'INNER JOIN', [ $idField . ' = ct_rev_id' ] ] ]
);
$this->addWhereFld( 'ct_tag', $this->params['tag'] );
}
- if ( isset( $index ) ) {
- $this->addOption( 'USE INDEX', $index );
- }
+ return true;
}
/**
@@ -436,7 +666,7 @@ class ApiQueryContributions extends ApiQueryBase {
);
if ( $userCanView ) {
- $comment = $this->commentStore->getComment( $row )->text;
+ $comment = $this->commentStore->getComment( 'rev_comment', $row )->text;
if ( $this->fld_comment ) {
$vals['comment'] = $comment;
}
@@ -448,7 +678,8 @@ class ApiQueryContributions extends ApiQueryBase {
}
if ( $this->fld_patrolled ) {
- $vals['patrolled'] = (bool)$row->rc_patrolled;
+ $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
+ $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
}
if ( $this->fld_size && !is_null( $row->rev_len ) ) {
@@ -484,10 +715,13 @@ class ApiQueryContributions extends ApiQueryBase {
private function continueStr( $row ) {
if ( $this->multiUserMode ) {
- if ( $this->idMode ) {
- return "id|$row->rev_user|$row->rev_timestamp|$row->rev_id";
- } else {
- return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
+ switch ( $this->orderBy ) {
+ case 'id':
+ return "id|$row->rev_user|$row->rev_timestamp|$row->rev_id";
+ case 'name':
+ return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
+ case 'actor':
+ return "actor|$row->rev_actor|$row->rev_timestamp|$row->rev_id";
}
} else {
return "$row->rev_timestamp|$row->rev_id";
@@ -563,6 +797,8 @@ class ApiQueryContributions extends ApiQueryBase {
'!minor',
'patrolled',
'!patrolled',
+ 'autopatrolled',
+ '!autopatrolled',
'top',
'!top',
'new',
diff --git a/www/wiki/includes/api/ApiQueryUserInfo.php b/www/wiki/includes/api/ApiQueryUserInfo.php
index 036515d6..fa151c98 100644
--- a/www/wiki/includes/api/ApiQueryUserInfo.php
+++ b/www/wiki/includes/api/ApiQueryUserInfo.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on July 30, 2007
- *
* Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiQueryUsers.php b/www/wiki/includes/api/ApiQueryUsers.php
index fbf1f9eb..824c4d52 100644
--- a/www/wiki/includes/api/ApiQueryUsers.php
+++ b/www/wiki/includes/api/ApiQueryUsers.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on July 30, 2007
- *
* Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -77,7 +73,7 @@ class ApiQueryUsers extends ApiQueryBase {
}
$this->tokenFunctions = [
- 'userrights' => [ 'ApiQueryUsers', 'getUserrightsToken' ],
+ 'userrights' => [ self::class, 'getUserrightsToken' ],
];
Hooks::run( 'APIQueryUsersTokens', [ &$this->tokenFunctions ] );
@@ -99,7 +95,7 @@ class ApiQueryUsers extends ApiQueryBase {
public function execute() {
$db = $this->getDB();
- $commentStore = new CommentStore( 'ipb_reason' );
+ $commentStore = CommentStore::getStore();
$params = $this->extractRequestParams();
$this->requireMaxOneParameter( $params, 'userids', 'users' );
@@ -144,8 +140,10 @@ class ApiQueryUsers extends ApiQueryBase {
$result = $this->getResult();
if ( count( $parameters ) ) {
- $this->addTables( 'user' );
- $this->addFields( User::selectFields() );
+ $userQuery = User::getQueryInfo();
+ $this->addTables( $userQuery['tables'] );
+ $this->addFields( $userQuery['fields'] );
+ $this->addJoinConds( $userQuery['joins'] );
if ( $useNames ) {
$this->addWhereFld( 'user_name', $goodNames );
} else {
@@ -237,7 +235,8 @@ class ApiQueryUsers extends ApiQueryBase {
$data[$key]['blockedby'] = $row->ipb_by_text;
$data[$key]['blockedbyid'] = (int)$row->ipb_by;
$data[$key]['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp );
- $data[$key]['blockreason'] = $commentStore->getComment( $row )->text;
+ $data[$key]['blockreason'] = $commentStore->getComment( 'ipb_reason', $row )
+ ->text;
$data[$key]['blockexpiry'] = $row->ipb_expiry;
}
diff --git a/www/wiki/includes/api/ApiQueryWatchlist.php b/www/wiki/includes/api/ApiQueryWatchlist.php
index 1e3b2c73..bb09838e 100644
--- a/www/wiki/includes/api/ApiQueryWatchlist.php
+++ b/www/wiki/includes/api/ApiQueryWatchlist.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 25, 2006
- *
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -53,7 +49,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$fld_flags = false, $fld_timestamp = false, $fld_user = false,
$fld_comment = false, $fld_parsedcomment = false, $fld_sizes = false,
$fld_notificationtimestamp = false, $fld_userid = false,
- $fld_loginfo = false;
+ $fld_loginfo = false, $fld_tags;
/**
* @param ApiPageSet $resultPageSet
@@ -82,6 +78,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$this->fld_patrol = isset( $prop['patrol'] );
$this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
$this->fld_loginfo = isset( $prop['loginfo'] );
+ $this->fld_tags = isset( $prop['tags'] );
if ( $this->fld_patrol ) {
if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
@@ -90,7 +87,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
}
if ( $this->fld_comment || $this->fld_parsedcomment ) {
- $this->commentStore = new CommentStore( 'rc_comment' );
+ $this->commentStore = CommentStore::getStore();
}
}
@@ -236,6 +233,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
}
if ( $this->fld_patrol ) {
$includeFields[] = WatchedItemQueryService::INCLUDE_PATROL_INFO;
+ $includeFields[] = WatchedItemQueryService::INCLUDE_AUTOPATROL_INFO;
}
if ( $this->fld_sizes ) {
$includeFields[] = WatchedItemQueryService::INCLUDE_SIZES;
@@ -243,6 +241,9 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
if ( $this->fld_loginfo ) {
$includeFields[] = WatchedItemQueryService::INCLUDE_LOG_INFO;
}
+ if ( $this->fld_tags ) {
+ $includeFields[] = WatchedItemQueryService::INCLUDE_TAGS;
+ }
return $includeFields;
}
@@ -255,6 +256,10 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
&& isset( $show[WatchedItemQueryService::FILTER_NOT_ANON] ) )
|| ( isset( $show[WatchedItemQueryService::FILTER_PATROLLED] )
&& isset( $show[WatchedItemQueryService::FILTER_NOT_PATROLLED] ) )
+ || ( isset( $show[WatchedItemQueryService::FILTER_AUTOPATROLLED] )
+ && isset( $show[WatchedItemQueryService::FILTER_NOT_AUTOPATROLLED] ) )
+ || ( isset( $show[WatchedItemQueryService::FILTER_AUTOPATROLLED] )
+ && isset( $show[WatchedItemQueryService::FILTER_NOT_PATROLLED] ) )
|| ( isset( $show[WatchedItemQueryService::FILTER_UNREAD] )
&& isset( $show[WatchedItemQueryService::FILTER_NOT_UNREAD] ) );
}
@@ -357,7 +362,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
Revision::DELETED_COMMENT,
$user
) ) {
- $comment = $this->commentStore->getComment( $recentChangeInfo )->text;
+ $comment = $this->commentStore->getComment( 'rc_comment', $recentChangeInfo )->text;
if ( $this->fld_comment ) {
$vals['comment'] = $comment;
}
@@ -370,8 +375,9 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
/* Add the patrolled flag */
if ( $this->fld_patrol ) {
- $vals['patrolled'] = $recentChangeInfo['rc_patrolled'] == 1;
+ $vals['patrolled'] = $recentChangeInfo['rc_patrolled'] != RecentChange::PRC_UNPATROLLED;
$vals['unpatrolled'] = ChangesList::isUnpatrolled( (object)$recentChangeInfo, $user );
+ $vals['autopatrolled'] = $recentChangeInfo['rc_patrolled'] == RecentChange::PRC_AUTOPATROLLED;
}
if ( $this->fld_loginfo && $recentChangeInfo['rc_type'] == RC_LOG ) {
@@ -391,6 +397,16 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
}
}
+ if ( $this->fld_tags ) {
+ if ( $recentChangeInfo['rc_tags'] ) {
+ $tags = explode( ',', $recentChangeInfo['rc_tags'] );
+ ApiResult::setIndexedTagName( $tags, 'tag' );
+ $vals['tags'] = $tags;
+ } else {
+ $vals['tags'] = [];
+ }
+ }
+
if ( $anyHidden && ( $recentChangeInfo['rc_deleted'] & Revision::DELETED_RESTRICTED ) ) {
$vals['suppressed'] = true;
}
@@ -453,6 +469,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'sizes',
'notificationtimestamp',
'loginfo',
+ 'tags',
]
],
'show' => [
@@ -466,6 +483,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
WatchedItemQueryService::FILTER_NOT_ANON,
WatchedItemQueryService::FILTER_PATROLLED,
WatchedItemQueryService::FILTER_NOT_PATROLLED,
+ WatchedItemQueryService::FILTER_AUTOPATROLLED,
+ WatchedItemQueryService::FILTER_NOT_AUTOPATROLLED,
WatchedItemQueryService::FILTER_UNREAD,
WatchedItemQueryService::FILTER_NOT_UNREAD,
]
diff --git a/www/wiki/includes/api/ApiQueryWatchlistRaw.php b/www/wiki/includes/api/ApiQueryWatchlistRaw.php
index b0b1cde9..0adbfdbf 100644
--- a/www/wiki/includes/api/ApiQueryWatchlistRaw.php
+++ b/www/wiki/includes/api/ApiQueryWatchlistRaw.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Oct 4, 2008
- *
* Copyright © 2008 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiRevisionDelete.php b/www/wiki/includes/api/ApiRevisionDelete.php
index 9d71a7db..9a793e2f 100644
--- a/www/wiki/includes/api/ApiRevisionDelete.php
+++ b/www/wiki/includes/api/ApiRevisionDelete.php
@@ -1,7 +1,5 @@
<?php
/**
- * Created on Jun 25, 2013
- *
* Copyright © 2013 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
@@ -47,7 +45,7 @@ class ApiRevisionDelete extends ApiBase {
}
// Check if user can add tags
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$ableToTag = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
if ( !$ableToTag->isOK() ) {
$this->dieStatus( $ableToTag );
@@ -116,11 +114,10 @@ class ApiRevisionDelete extends ApiBase {
}
$list->reloadFromMaster();
- // @codingStandardsIgnoreStart Avoid function calls in a FOR loop test part
+ // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
for ( $item = $list->reset(); $list->current(); $item = $list->next() ) {
$data['items'][$item->getId()] += $item->getApiData( $this->getResult() );
}
- // @codingStandardsIgnoreEnd
$data['items'] = array_values( $data['items'] );
ApiResult::setIndexedTagName( $data['items'], 'i' );
diff --git a/www/wiki/includes/api/ApiRollback.php b/www/wiki/includes/api/ApiRollback.php
index 76b6cc67..d2ff7902 100644
--- a/www/wiki/includes/api/ApiRollback.php
+++ b/www/wiki/includes/api/ApiRollback.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Jun 20, 2007
- *
* Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
@@ -52,7 +48,7 @@ class ApiRollback extends ApiBase {
// If change tagging was requested, check that the user is allowed to tag,
// and the tags are valid
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
if ( !$tagStatus->isOK() ) {
$this->dieStatus( $tagStatus );
diff --git a/www/wiki/includes/api/ApiRsd.php b/www/wiki/includes/api/ApiRsd.php
index fdc62a8e..f20d1c6c 100644
--- a/www/wiki/includes/api/ApiRsd.php
+++ b/www/wiki/includes/api/ApiRsd.php
@@ -3,8 +3,6 @@
/**
* API for MediaWiki 1.17+
*
- * Created on October 26, 2010
- *
* Copyright © 2010 Bryan Tong Minh and Brion Vibber
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiSerializable.php b/www/wiki/includes/api/ApiSerializable.php
index a41f655c..31d00cc8 100644
--- a/www/wiki/includes/api/ApiSerializable.php
+++ b/www/wiki/includes/api/ApiSerializable.php
@@ -1,7 +1,5 @@
<?php
/**
- * Created on Feb 25, 2015
- *
* Copyright © 2015 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiSetNotificationTimestamp.php b/www/wiki/includes/api/ApiSetNotificationTimestamp.php
index b6a0a783..f7dc4a78 100644
--- a/www/wiki/includes/api/ApiSetNotificationTimestamp.php
+++ b/www/wiki/includes/api/ApiSetNotificationTimestamp.php
@@ -3,8 +3,6 @@
/**
* API for MediaWiki 1.14+
*
- * Created on Jun 18, 2012
- *
* Copyright © 2012 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
@@ -32,7 +30,7 @@ use MediaWiki\MediaWikiServices;
*/
class ApiSetNotificationTimestamp extends ApiBase {
- private $mPageSet;
+ private $mPageSet = null;
public function execute() {
$user = $this->getUser();
@@ -189,7 +187,7 @@ class ApiSetNotificationTimestamp extends ApiBase {
* @return ApiPageSet
*/
private function getPageSet() {
- if ( !isset( $this->mPageSet ) ) {
+ if ( $this->mPageSet === null ) {
$this->mPageSet = new ApiPageSet( $this );
}
diff --git a/www/wiki/includes/api/ApiSetPageLanguage.php b/www/wiki/includes/api/ApiSetPageLanguage.php
index 7e3f1acf..40826ae3 100644
--- a/www/wiki/includes/api/ApiSetPageLanguage.php
+++ b/www/wiki/includes/api/ApiSetPageLanguage.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on January 1, 2017
- *
* Copyright © 2017 Justin Du "<justin.d128@gmail.com>"
*
* This program is free software; you can redistribute it and/or modify
@@ -73,7 +69,7 @@ class ApiSetPageLanguage extends ApiBase {
// If change tagging was requested, check that the user is allowed to tag,
// and the tags are valid
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
if ( !$tagStatus->isOK() ) {
$this->dieStatus( $tagStatus );
diff --git a/www/wiki/includes/api/ApiStashEdit.php b/www/wiki/includes/api/ApiStashEdit.php
index 8a9de064..23163c24 100644
--- a/www/wiki/includes/api/ApiStashEdit.php
+++ b/www/wiki/includes/api/ApiStashEdit.php
@@ -181,9 +181,14 @@ class ApiStashEdit extends ApiBase {
$title = $page->getTitle();
$key = self::getStashKey( $title, self::getContentHash( $content ), $user );
- // Use the master DB for fast blocking locks
+ // Use the master DB to allow for fast blocking locks on the "save path" where this
+ // value might actually be used to complete a page edit. If the edit submission request
+ // happens before this edit stash requests finishes, then the submission will block until
+ // the stash request finishes parsing. For the lock acquisition below, there is not much
+ // need to duplicate parsing of the same content/user/summary bundle, so try to avoid
+ // blocking at all here.
$dbw = wfGetDB( DB_MASTER );
- if ( !$dbw->lock( $key, __METHOD__, 1 ) ) {
+ if ( !$dbw->lock( $key, __METHOD__, 0 ) ) {
// De-duplicate requests on the same key
return self::ERROR_BUSY;
}
@@ -209,8 +214,10 @@ class ApiStashEdit extends ApiBase {
Hooks::run( 'ParserOutputStashForEdit',
[ $page, $content, $editInfo->output, $summary, $user ] );
+ $titleStr = (string)$title;
if ( $alreadyCached ) {
- $logger->debug( "Already cached parser output for key '$key' ('$title')." );
+ $logger->debug( "Already cached parser output for key '{cachekey}' ('{title}').",
+ [ 'cachekey' => $key, 'title' => $titleStr ] );
return self::ERROR_NONE;
}
@@ -224,14 +231,17 @@ class ApiStashEdit extends ApiBase {
if ( $stashInfo ) {
$ok = $cache->set( $key, $stashInfo, $ttl );
if ( $ok ) {
- $logger->debug( "Cached parser output for key '$key' ('$title')." );
+ $logger->debug( "Cached parser output for key '{cachekey}' ('{title}').",
+ [ 'cachekey' => $key, 'title' => $titleStr ] );
return self::ERROR_NONE;
} else {
- $logger->error( "Failed to cache parser output for key '$key' ('$title')." );
+ $logger->error( "Failed to cache parser output for key '{cachekey}' ('{title}').",
+ [ 'cachekey' => $key, 'title' => $titleStr ] );
return self::ERROR_CACHE;
}
} else {
- $logger->info( "Uncacheable parser output for key '$key' ('$title') [$code]." );
+ $logger->info( "Uncacheable parser output for key '{cachekey}' ('{title}') [{code}].",
+ [ 'cachekey' => $key, 'title' => $titleStr, 'code' => $code ] );
return self::ERROR_UNCACHEABLE;
}
}
@@ -330,11 +340,15 @@ class ApiStashEdit extends ApiBase {
* @return string|null TS_MW timestamp or null
*/
private static function lastEditTime( User $user ) {
- $time = wfGetDB( DB_REPLICA )->selectField(
- 'recentchanges',
+ $db = wfGetDB( DB_REPLICA );
+ $actorQuery = ActorMigration::newMigration()->getWhere( $db, 'rc_user', $user, false );
+ $time = $db->selectField(
+ [ 'recentchanges' ] + $actorQuery['tables'],
'MAX(rc_timestamp)',
- [ 'rc_user_text' => $user->getName() ],
- __METHOD__
+ [ $actorQuery['conds'] ],
+ __METHOD__,
+ [],
+ $actorQuery['joins']
);
return wfTimestampOrNull( TS_MW, $time );
diff --git a/www/wiki/includes/api/ApiTag.php b/www/wiki/includes/api/ApiTag.php
index 76c67629..c9f6db39 100644
--- a/www/wiki/includes/api/ApiTag.php
+++ b/www/wiki/includes/api/ApiTag.php
@@ -37,9 +37,9 @@ class ApiTag extends ApiBase {
}
// Check if user can add tags
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$ableToTag = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
- if ( !$ableToTag->isOk() ) {
+ if ( !$ableToTag->isOK() ) {
$this->dieStatus( $ableToTag );
}
}
diff --git a/www/wiki/includes/api/ApiTokens.php b/www/wiki/includes/api/ApiTokens.php
index fc2951a9..ff1914c8 100644
--- a/www/wiki/includes/api/ApiTokens.php
+++ b/www/wiki/includes/api/ApiTokens.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Jul 29, 2011
- *
* Copyright © 2011 John Du Hart john@johnduhart.me
*
* This program is free software; you can redistribute it and/or modify
@@ -66,11 +62,11 @@ class ApiTokens extends ApiBase {
if ( $types ) {
return $types;
}
- $types = [ 'patrol' => [ 'ApiQueryRecentChanges', 'getPatrolToken' ] ];
+ $types = [ 'patrol' => [ ApiQueryRecentChanges::class, 'getPatrolToken' ] ];
$names = [ 'edit', 'delete', 'protect', 'move', 'block', 'unblock',
'email', 'import', 'watch', 'options' ];
foreach ( $names as $name ) {
- $types[$name] = [ 'ApiQueryInfo', 'get' . ucfirst( $name ) . 'Token' ];
+ $types[$name] = [ ApiQueryInfo::class, 'get' . ucfirst( $name ) . 'Token' ];
}
Hooks::run( 'ApiTokensGetTokenTypes', [ &$types ] );
diff --git a/www/wiki/includes/api/ApiUnblock.php b/www/wiki/includes/api/ApiUnblock.php
index 887edaae..b748cb32 100644
--- a/www/wiki/includes/api/ApiUnblock.php
+++ b/www/wiki/includes/api/ApiUnblock.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Sep 7, 2007
- *
* Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiUndelete.php b/www/wiki/includes/api/ApiUndelete.php
index 3aa7b608..ee5c3a2f 100644
--- a/www/wiki/includes/api/ApiUndelete.php
+++ b/www/wiki/includes/api/ApiUndelete.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Jul 3, 2007
- *
* Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/ApiUpload.php b/www/wiki/includes/api/ApiUpload.php
index cfe19689..93e432b9 100644
--- a/www/wiki/includes/api/ApiUpload.php
+++ b/www/wiki/includes/api/ApiUpload.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Aug 21, 2008
- *
* Copyright © 2008 - 2010 Bryan Tong Minh <Bryan.TongMinh@Gmail.com>
*
* This program is free software; you can redistribute it and/or modify
@@ -728,26 +724,26 @@ class ApiUpload extends ApiBase {
*/
protected function handleStashException( $e ) {
switch ( get_class( $e ) ) {
- case 'UploadStashFileNotFoundException':
+ case UploadStashFileNotFoundException::class:
$wrap = 'apierror-stashedfilenotfound';
break;
- case 'UploadStashBadPathException':
+ case UploadStashBadPathException::class:
$wrap = 'apierror-stashpathinvalid';
break;
- case 'UploadStashFileException':
+ case UploadStashFileException::class:
$wrap = 'apierror-stashfilestorage';
break;
- case 'UploadStashZeroLengthFileException':
+ case UploadStashZeroLengthFileException::class:
$wrap = 'apierror-stashzerolength';
break;
- case 'UploadStashNotLoggedInException':
+ case UploadStashNotLoggedInException::class:
return StatusValue::newFatal( ApiMessage::create(
[ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin'
) );
- case 'UploadStashWrongOwnerException':
+ case UploadStashWrongOwnerException::class:
$wrap = 'apierror-stashwrongowner';
break;
- case 'UploadStashNoSuchKeyException':
+ case UploadStashNoSuchKeyException::class:
$wrap = 'apierror-stashnosuchfilekey';
break;
default:
diff --git a/www/wiki/includes/api/ApiUsageException.php b/www/wiki/includes/api/ApiUsageException.php
index 47902a75..c200dcba 100644
--- a/www/wiki/includes/api/ApiUsageException.php
+++ b/www/wiki/includes/api/ApiUsageException.php
@@ -16,7 +16,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @defgroup API API
*/
/**
@@ -213,7 +212,7 @@ class ApiUsageException extends UsageException implements ILocalizedException {
* @inheritDoc
*/
public function getMessageObject() {
- return $this->status->getMessage();
+ return Status::wrap( $this->status )->getMessage();
}
/**
diff --git a/www/wiki/includes/api/ApiUserrights.php b/www/wiki/includes/api/ApiUserrights.php
index 2a364d97..47f3bc5a 100644
--- a/www/wiki/includes/api/ApiUserrights.php
+++ b/www/wiki/includes/api/ApiUserrights.php
@@ -58,20 +58,23 @@ class ApiUserrights extends ApiBase {
$params = $this->extractRequestParams();
// Figure out expiry times from the input
- // $params['expiry'] may not be set in subclasses
+ // $params['expiry'] is not set in CentralAuth's ApiGlobalUserRights subclass
if ( isset( $params['expiry'] ) ) {
$expiry = (array)$params['expiry'];
} else {
$expiry = [ 'infinity' ];
}
- if ( count( $expiry ) !== count( $params['add'] ) ) {
+ $add = (array)$params['add'];
+ if ( !$add ) {
+ $expiry = [];
+ } elseif ( count( $expiry ) !== count( $add ) ) {
if ( count( $expiry ) === 1 ) {
- $expiry = array_fill( 0, count( $params['add'] ), $expiry[0] );
+ $expiry = array_fill( 0, count( $add ), $expiry[0] );
} else {
$this->dieWithError( [
'apierror-toofewexpiries',
count( $expiry ),
- count( $params['add'] )
+ count( $add )
] );
}
}
@@ -79,7 +82,7 @@ class ApiUserrights extends ApiBase {
// Validate the expiries
$groupExpiries = [];
foreach ( $expiry as $index => $expiryValue ) {
- $group = $params['add'][$index];
+ $group = $add[$index];
$groupExpiries[$group] = UserrightsPage::expiryToTimestamp( $expiryValue );
if ( $groupExpiries[$group] === false ) {
@@ -97,7 +100,7 @@ class ApiUserrights extends ApiBase {
$tags = $params['tags'];
// Check if user can add tags
- if ( !is_null( $tags ) ) {
+ if ( $tags !== null ) {
$ableToTag = ChangeTags::canAddTagsAccompanyingChange( $tags, $pUser );
if ( !$ableToTag->isOK() ) {
$this->dieStatus( $ableToTag );
@@ -109,8 +112,9 @@ class ApiUserrights extends ApiBase {
$r['user'] = $user->getName();
$r['userid'] = $user->getId();
list( $r['added'], $r['removed'] ) = $form->doSaveUserGroups(
- $user, (array)$params['add'], (array)$params['remove'],
- $params['reason'], $tags, $groupExpiries
+ // Don't pass null to doSaveUserGroups() for array params, cast to empty array
+ $user, (array)$add, (array)$params['remove'],
+ $params['reason'], (array)$tags, $groupExpiries
);
$result = $this->getResult();
@@ -185,6 +189,7 @@ class ApiUserrights extends ApiBase {
ApiBase::PARAM_ISMULTI => true
],
];
+ // CentralAuth's ApiGlobalUserRights subclass can't handle expiries
if ( !$this->getUserRightsPage()->canProcessExpiries() ) {
unset( $a['expiry'] );
}
diff --git a/www/wiki/includes/api/ApiWatch.php b/www/wiki/includes/api/ApiWatch.php
index efe21f11..38be178b 100644
--- a/www/wiki/includes/api/ApiWatch.php
+++ b/www/wiki/includes/api/ApiWatch.php
@@ -1,9 +1,5 @@
<?php
/**
- *
- *
- * Created on Jan 4, 2008
- *
* Copyright © 2008 Yuri Astrakhan "<Firstname><Lastname>@gmail.com",
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/api/i18n/ar.json b/www/wiki/includes/api/i18n/ar.json
index 6d7fea2c..f07c6e2d 100644
--- a/www/wiki/includes/api/i18n/ar.json
+++ b/www/wiki/includes/api/i18n/ar.json
@@ -9,9 +9,12 @@
"Maroen1990",
"محمد أحمد عبد الفتاح",
"ديفيد",
- "ASHmed"
+ "ASHmed",
+ "Yasser Yousssef",
+ "Azouz.anis"
]
},
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentation]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailing list]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API Announcements]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & requests]\n</div>\n<strong>Status:</strong> يجب أن تعمل جميع الميزات المعروضة في هذه الصفحة، إلا أن واجهة برمجة التطبيقات لا تزال قيد التطوير النشط، وقد تتغير في أي وقت. الاشتراك في\n<strong>Erroneous requests:</strong>عند إرسال طلبات خاطئة إلى api, فالـHTTP سيتم إرسال رأس مع المفتاح \"MediaWiki-API-Error\" ومن ثم سيتم تعيين قيمة رأس ورمز الخطأ المرسل مرة أخرى إلى نفس القيمة. لمزيد من المعلومات، راجع [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]].\n<p class=\"mw-apisandbox-link\"><strong>Testing:</strong> لسهولة إختبار طلبات API، انظر [[Special:ApiSandbox]].</p>",
"apihelp-main-param-action": "أي فعل للعمل.",
"apihelp-main-param-format": "صيغة الخرج.",
"apihelp-main-param-assertuser": "التحقق من أن المستخدم الحالي هو المستخدم المسمى.",
@@ -30,7 +33,9 @@
"apihelp-block-param-noemail": "منع المستخدم من إرسال البريد الإلكتروني من خلال الويكي. (يتطلب صلاحية <code>blockemail</code>).",
"apihelp-block-param-hidename": "إخفاء اسم المستخدم من سجل المنع. (يتطلب صلاحية <code>hideuser</code>).",
"apihelp-block-param-allowusertalk": "تسمح للمستخدم بتحرير صفحة النقاش الخاصة (يعتمد على <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).",
+ "apihelp-block-param-reblock": "إذا كان المستخدم محظوراً بالفعل، يستبدل الحظر القائم.",
"apihelp-block-param-watchuser": "مشاهدة صفحة المستخدم ونقاش IP.",
+ "apihelp-block-param-tags": "تغيير الوسوم للتطبيق على الإدخال في سجل الحظر.",
"apihelp-block-example-ip-simple": "منع عنوان IP <kbd>192.0.2.5</kbd> لمدة ثلاثة أيام بسبب >المخالفة الأولى</kbd>.",
"apihelp-block-example-user-complex": "منع المستخدم <kbd>المخرب</kbd> لأجل غير مسمى بسبب <kbd>التخريب</kbd>، ومنع إنشاء حساب جديد وإرسال بريد إلكتروني.",
"apihelp-changeauthenticationdata-summary": "تغيير بيانات المصادقة للمستخدم الحالي.",
@@ -155,8 +160,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "فلتر بالوسم.",
"apihelp-feedrecentchanges-param-target": "أحدث التغييرات في الصفحات الموصولة من هذه الصفحة فقط",
"apihelp-feedrecentchanges-param-showlinkedto": "أظهر التغييرات للصفحات الموصولة للصفحة المعطاة عوضا عن ذلك",
- "apihelp-feedrecentchanges-param-categories": "أظهر التغييرات في الصفحات في كل تصنيف من هذه التصنيفات فقط.",
- "apihelp-feedrecentchanges-param-categories_any": "أظهر التغييرات في الصفحات في أي تصنيف بدلا من ذلك.",
"apihelp-feedrecentchanges-example-simple": " اظهر التغييرات الحديثة",
"apihelp-feedrecentchanges-example-30days": "أظهر التغييرات الأخيرة في 30 يوم.",
"apihelp-feedwatchlist-summary": "إرجاع تغذية قائمة المراقبة.",
diff --git a/www/wiki/includes/api/i18n/ba.json b/www/wiki/includes/api/i18n/ba.json
index da8535d6..99421fed 100644
--- a/www/wiki/includes/api/i18n/ba.json
+++ b/www/wiki/includes/api/i18n/ba.json
@@ -153,8 +153,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Тэг буйынса һөҙгөс",
"apihelp-feedrecentchanges-param-target": "Был биттән һылтанған биттәрҙә һуңғы үҙгәртеүҙәрҙе күрһәтергә",
"apihelp-feedrecentchanges-param-showlinkedto": "Киреһенсә, был биткә һылтанма яһаған биттәрҙәге үҙгәртеүҙәрҙе күрһәтергә",
- "apihelp-feedrecentchanges-param-categories": "Бар категория биттәрендәге үҙгәрештәрҙе генә күрһәтергә",
- "apihelp-feedrecentchanges-param-categories_any": "Был категориянан башҡа теләһә ҡайһы категориялар биттәрендәге үҙгәрештәрҙе генә күрһәтергә",
"apihelp-feedrecentchanges-example-simple": "Һуңғы үҙгәртеүҙәрҙе күрһәтергә.",
"apihelp-feedrecentchanges-example-30days": "30 көн арауығындағы һуңғы үҙгәртеүҙәрҙе күрһәтергә.",
"apihelp-feedwatchlist-summary": "Күҙәтеү каналын ҡайтара",
diff --git a/www/wiki/includes/api/i18n/cs.json b/www/wiki/includes/api/i18n/cs.json
index fcb4af4a..90af65f9 100644
--- a/www/wiki/includes/api/i18n/cs.json
+++ b/www/wiki/includes/api/i18n/cs.json
@@ -11,10 +11,11 @@
"Danny B.",
"LordMsz",
"Dvorapa",
- "Matěj Suchánek"
+ "Matěj Suchánek",
+ "Ilimanaq29"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentace]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-mailová konference]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Oznámení k API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Chyby a požadavky]\n</div>\n<strong>Stav:</strong> Všechny funkce uvedené na této stránce by měly fungovat, ale API se stále aktivně vyvíjí a může se kdykoli změnit. Upozornění na změny získáte přihlášením se k [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-mailové konferenci mediawiki-api-announce].\n\n<strong>Chybné požadavky:</strong> Pokud jsou do API zaslány chybné požadavky, bude vrácena HTTP hlavička s klíčem „MediaWiki-API-Error“ a hodnota této hlavičky a chybový kód budou nastaveny na stejnou hodnotu. Více informací najdete [[mw:Special:MyLanguage/API:Errors_and_warnings|v dokumentaci]].\n\n<strong>Testování:</strong> Pro jednoduché testování požadavků na API zkuste [[Special:ApiSandbox]].",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentace]]\n* [[mw:Special:MyLanguage/API:FAQ|Otázky a odpovědi]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-mailová konference]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Oznámení k API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Chyby a požadavky]\n</div>\n<strong>Stav:</strong> Všechny funkce uvedené na této stránce by měly fungovat, ale API se stále aktivně vyvíjí a může se kdykoli změnit. Upozornění na změny získáte přihlášením se k [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-mailové konferenci mediawiki-api-announce].\n\n<strong>Chybné požadavky:</strong> Pokud jsou do API zaslány chybné požadavky, bude vrácena HTTP hlavička s klíčem „MediaWiki-API-Error“ a hodnota této hlavičky a chybový kód budou nastaveny na stejnou hodnotu. Více informací najdete [[mw:Special:MyLanguage/API:Errors_and_warnings|v dokumentaci]].\n\n<strong>Testování:</strong> Pro jednoduché testování požadavků na API zkuste [[Special:ApiSandbox]].",
"apihelp-main-param-action": "Která akce se má provést.",
"apihelp-main-param-format": "Formát výstupu.",
"apihelp-main-param-maxlag": "Maximální zpoždění lze použít, když je MediaWiki nainstalováno na cluster s replikovanou databází. Abyste se vyhnuli zhoršování už tak špatného replikačního zpoždění, můžete tímto parametrem nechat klienta čekat, dokud replikační zpoždění neklesne pod uvedenou hodnotu. V případě příliš vysokého zpoždění se vrátí chybový kód „<samp>maxlag</samp>“ s hlášením typu „<samp>Waiting for $host: $lag seconds lagged</samp>“.<br />Více informací najdete v [[mw:Special:MyLanguage/Manual:Maxlag_parameter|příručce]].",
@@ -153,7 +154,7 @@
"apihelp-opensearch-summary": "Vyhledávání na wiki pomocí protokolu OpenSearch.",
"apihelp-opensearch-param-search": "Hledaný řetězec.",
"apihelp-opensearch-param-limit": "Maximální počet vrácených výsledků",
- "apihelp-opensearch-param-namespace": "Jmenné prostory pro vyhledávání.",
+ "apihelp-opensearch-param-namespace": "Jmenné prostory pro vyhledávání. Ignorováno, pokud <var>$1search</var> začíná platným jmenným prostorem.",
"apihelp-opensearch-param-suggest": "Pokud je <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> vypnuto, nedělat nic.",
"apihelp-opensearch-param-format": "Formát výstupu.",
"apihelp-opensearch-example-te": "Najít stránky začínající na „<kbd>Te</kbd>“.",
@@ -161,6 +162,7 @@
"apihelp-options-example-reset": "Vrátit všechna nastavení.",
"apihelp-parse-param-summary": "Shrnutí, které se má parsovat.",
"apihelp-parse-paramvalue-prop-displaytitle": "Přidává název parsovaného wikitextu.",
+ "apihelp-parse-param-disablestylededuplication": "Neodstraňovat duplicitní in-line styly ve výstupu parseru.",
"apihelp-parse-param-preview": "Parsovat v režimu náhledu.",
"apihelp-parse-example-page": "Parsovat stránku.",
"apihelp-parse-example-text": "Parsovat wikitext.",
@@ -219,6 +221,7 @@
"apihelp-query+langlinks-summary": "Zobrazit všechny mezijazykové odkazy z daných stránek.",
"apihelp-query+langlinks-param-lang": "Zobrazit pouze jazykové odkazy s tímto kódem jazyka.",
"apihelp-query+linkshere-example-generator": "Získat informace o stránkách, které odkazují na [[Hlavní Stránka|Hlavní stránku]].",
+ "apihelp-query+prefixsearch-param-namespace": "Jmenné prostory pro vyhledávání. Ignorováno, pokud <var>$1search</var> začíná platným jmenným prostorem.",
"apihelp-query+recentchanges-param-excludeuser": "Nezobrazovat změny od tohoto uživatele.",
"apihelp-query+recentchanges-example-simple": "Seznam posledních změn.",
"apihelp-query+redirects-param-limit": "Počet přesměrování, který má být zobrazen.",
@@ -287,6 +290,8 @@
"api-help-permissions-granted-to": "Uděleno {{PLURAL:$1|skupině|skupinám}}: $2",
"api-help-right-apihighlimits": "Používání vyšších limitů v API dotazech (pomalé dotazy: $1, rychlé dotazy: $2). Limity pro pomalé dotazy se vztahují i na vícehodnotové parametry.",
"api-help-open-in-apisandbox": "<small>[otevřít v pískovišti]</small>",
+ "apierror-blocked": "Byla vám zablokována možnost editace.",
+ "apierror-mustbeloggedin": "Abyste mohli $1, musíte být přihlášeni.",
"apierror-nosuchsection-what": "$2 neobsahuje sekci $1.",
"apierror-sectionsnotsupported-what": "$1 nepodporuje sekce.",
"apierror-timeout": "Server neodpověděl v očekávaném čase.",
diff --git a/www/wiki/includes/api/i18n/de.json b/www/wiki/includes/api/i18n/de.json
index d3273db4..b1eee12d 100644
--- a/www/wiki/includes/api/i18n/de.json
+++ b/www/wiki/includes/api/i18n/de.json
@@ -22,7 +22,7 @@
"Tacsipacsi"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentation]]\n* [[mw:Special:MyLanguage/API:FAQ|Häufig gestellte Fragen]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailingliste]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-Ankündigungen]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Fehlerberichte und Anfragen]\n</div>\n<strong>Status:</strong> Alle auf dieser Seite gezeigten Funktionen sollten funktionieren, allerdings ist die API in aktiver Entwicklung und kann sich zu jeder Zeit ändern. Abonniere die [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ MediaWiki-API-Ankündigungs-Mailingliste], um über Aktualisierungen informiert zu werden.\n\n<strong>Fehlerhafte Anfragen:</strong> Wenn fehlerhafte Anfragen an die API gesendet werden, wird ein HTTP-Header mit dem Schlüssel „MediaWiki-API-Error“ gesendet. Der Wert des Headers und der Fehlercode werden auf den gleichen Wert gesetzt. Für weitere Informationen siehe [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Fehler und Warnungen]].\n\n<strong>Testen:</strong> Zum einfachen Testen von API-Anfragen, siehe [[Special:ApiSandbox]].",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentation]]\n* [[mw:Special:MyLanguage/API:FAQ|Häufig gestellte Fragen]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailingliste]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-Ankündigungen]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Fehlerberichte und Anfragen]\n</div>\n<strong>Status:</strong> Die MediaWiki-API ist eine ausgereifte und stabile Schnittstelle, die aktiv unterstützt und verbessert wird. Während wir versuchen, dies zu vermeiden, können wir gelegentlich Breaking Changes erforderlich machen. Abonniere die [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ MediaWiki-API-Ankündigungs-Mailingliste] für Mitteilungen zu Aktualisierungen.\n\n<strong>Fehlerhafte Anfragen:</strong> Wenn fehlerhafte Anfragen an die API gesendet werden, wird ein HTTP-Header mit dem Schlüssel „MediaWiki-API-Error“ gesendet. Der Wert des Headers und der Fehlercode werden auf den gleichen Wert gesetzt. Für weitere Informationen siehe [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Fehler und Warnungen]].\n\n<p class=\"mw-apisandbox-link\"><strong>Testen:</strong> Zum einfachen Testen von API-Anfragen, siehe [[Special:ApiSandbox]].</p>",
"apihelp-main-param-action": "Auszuführende Aktion.",
"apihelp-main-param-format": "Format der Ausgabe.",
"apihelp-main-param-maxlag": "maxlag kann verwendet werden, wenn MediaWiki auf einem datenbankreplizierten Cluster installiert ist. Um weitere Replikationsrückstände zu verhindern, lässt dieser Parameter den Client warten, bis der Replikationsrückstand kleiner als der angegebene Wert (in Sekunden) ist. Bei einem größerem Rückstand wird der Fehlercode <samp>maxlag</samp> zurückgegeben mit einer Nachricht wie <samp>Waiting for $host: $lag seconds lagged</samp>.<br />Siehe [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Handbuch: Maxlag parameter]] für weitere Informationen.",
@@ -72,6 +72,8 @@
"apihelp-compare-param-totitle": "Zweiter zu vergleichender Titel.",
"apihelp-compare-param-toid": "Zweite zu vergleichende Seitennummer.",
"apihelp-compare-param-torev": "Zweite zu vergleichende Version.",
+ "apihelp-compare-paramvalue-prop-diff": "Das Unterschieds-HTML.",
+ "apihelp-compare-paramvalue-prop-diffsize": "Die Größe des Unterschieds-HTML in Bytes.",
"apihelp-compare-paramvalue-prop-title": "Die Seitentitel der Versionen „Von“ und „Nach“.",
"apihelp-compare-example-1": "Unterschied zwischen Version 1 und 2 abrufen",
"apihelp-createaccount-summary": "Erstellt ein neues Benutzerkonto.",
@@ -184,8 +186,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Nach Markierung filtern.",
"apihelp-feedrecentchanges-param-target": "Nur Änderungen an Seiten anzeigen, die von dieser Seite verlinkt sind.",
"apihelp-feedrecentchanges-param-showlinkedto": "Zeige Änderungen an Seiten die von der ausgewählten Seite verlinkt sind.",
- "apihelp-feedrecentchanges-param-categories": "Zeigt nur Änderungen von Seiten in all diesen Kategorien.",
- "apihelp-feedrecentchanges-param-categories_any": "Zeigt stattdessen nur Änderungen auf Seiten in einer dieser Kategorien.",
"apihelp-feedrecentchanges-example-simple": "Letzte Änderungen anzeigen",
"apihelp-feedrecentchanges-example-30days": "Letzte Änderungen für 30 Tage anzeigen",
"apihelp-feedwatchlist-summary": "Gibt einen Beobachtungslisten-Feed zurück.",
@@ -220,6 +220,8 @@
"apihelp-import-extended-description": "Bitte beachte, dass der HTTP-POST-Vorgang als Dateiupload ausgeführt werden muss (z.B. durch multipart/form-data), um eine Datei über den <var>xml</var>-Parameter zu senden.",
"apihelp-import-param-summary": "Importzusammenfassung des Logbucheintrags.",
"apihelp-import-param-xml": "Hochgeladene XML-Datei.",
+ "apihelp-import-param-interwikiprefix": "Für hochgeladene Importe: Auf unbekannte Benutzernamen anzuwendendes Interwiki-Präfix (und bekannte Benutzer, falls <var>$1assignknownusers</var> festgelegt ist).",
+ "apihelp-import-param-assignknownusers": "Weist Bearbeitungen lokalen Benutzern zu, wo der benannte Benutzer lokal vorhanden ist.",
"apihelp-import-param-interwikisource": "Für Interwiki-Importe: Wiki, von dem importiert werden soll.",
"apihelp-import-param-interwikipage": "Für Interwiki-Importe: zu importierende Seite.",
"apihelp-import-param-fullhistory": "Für Interwiki-Importe: importiere die komplette Versionsgeschichte, nicht nur die aktuelle Version.",
@@ -269,7 +271,7 @@
"apihelp-opensearch-summary": "Das Wiki mithilfe des OpenSearch-Protokolls durchsuchen.",
"apihelp-opensearch-param-search": "Such-Zeichenfolge.",
"apihelp-opensearch-param-limit": "Maximale Anzahl zurückzugebender Ergebnisse.",
- "apihelp-opensearch-param-namespace": "Zu durchsuchende Namensräume.",
+ "apihelp-opensearch-param-namespace": "Zu durchsuchende Namensräume. Wird ignoriert, falls <var>$1search</var> mit einem gültigen Namensraumpräfix beginnt.",
"apihelp-opensearch-param-suggest": "Nichts unternehmen, falls <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> falsch ist.",
"apihelp-opensearch-param-redirects": "Wie mit Weiterleitungen umgegangen werden soll:\n;return:Gibt die Weiterleitung selbst zurück.\n;resolve:Gibt die Zielseite zurück. Kann weniger als $1limit Ergebnisse zurückgeben.\nAus Kompatibilitätsgründen ist für $1format=json die Vorgabe \"return\" und \"resolve\" für alle anderen Formate.",
"apihelp-opensearch-param-format": "Das Format der Ausgabe.",
@@ -290,6 +292,7 @@
"apihelp-paraminfo-param-mainmodule": "Auch Informationen über die Hauptmodule (top-level) erhalten. Benutze <kbd>$1modules=main</kbd> stattdessen.",
"apihelp-paraminfo-param-formatmodules": "Liste von Formatmodulnamen (Wert des Parameters <var>format</var>). Stattdessen <var>$1modules</var> verwenden.",
"apihelp-paraminfo-example-1": "Zeige Info für <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>, <kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>, <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd>, und <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>.",
+ "apihelp-parse-summary": "Parst den Inhalt und gibt die Parserausgabe zurück.",
"apihelp-parse-param-title": "Titel der Seite, zu der der Text gehört. Falls ausgelassen, muss <var>$1contentmodel</var> angegeben werden und [[API]] wird als Titel verwendet.",
"apihelp-parse-param-text": "Zu parsender Text. <var>$1title</var> oder <var>$1contentmodel</var> verwenden, um das Inhaltsmodell zu steuern.",
"apihelp-parse-param-revid": "Versionskennung, für <code><nowiki>{{REVISIONID}}</nowiki></code> und ähnliche Variablen.",
@@ -506,6 +509,7 @@
"apihelp-query+allrevisions-example-ns-main": "Liste die ersten 50 Bearbeitungen im Hauptnamensraum auf.",
"apihelp-query+mystashedfiles-param-prop": "Welche Eigenschaften für die Dateien abgerufen werden sollen.",
"apihelp-query+mystashedfiles-paramvalue-prop-size": "Ruft die Dateigröße und Bildabmessungen ab.",
+ "apihelp-query+mystashedfiles-paramvalue-prop-type": "Ruft den MIME- und Medientyp der Datei ab.",
"apihelp-query+mystashedfiles-param-limit": "Wie viele Dateien zurückgegeben werden sollen.",
"apihelp-query+alltransclusions-summary": "Liste alle Transklusionen auf (eingebettete Seiten die &#123;&#123;x&#125;&#125; benutzen), einschließlich nicht vorhandener.",
"apihelp-query+alltransclusions-param-from": "Der Titel der Transklusion bei dem die Auflistung beginnen soll.",
@@ -606,6 +610,7 @@
"apihelp-query+contributors-summary": "Rufe die Liste der angemeldeten Bearbeiter und die Zahl anonymer Bearbeiter einer Seite ab.",
"apihelp-query+contributors-param-limit": "Wie viele Spender zurückgegeben werden sollen.",
"apihelp-query+contributors-example-simple": "Zeige Mitwirkende der Seite <kbd>Main Page</kbd>.",
+ "apihelp-query+deletedrevisions-summary": "Ruft Informationen zur gelöschten Version ab.",
"apihelp-query+deletedrevisions-param-start": "Der Zeitstempel bei dem die Auflistung beginnen soll. Wird bei der Verarbeitung einer Liste von Bearbeitungs-IDs ignoriert.",
"apihelp-query+deletedrevisions-param-end": "Der Zeitstempel bei dem die Auflistung enden soll. Wird bei der Verarbeitung einer List von Bearbeitungs-IDs ignoriert.",
"apihelp-query+deletedrevisions-param-tag": "Listet nur Bearbeitungen auf, die die angegebene Markierung haben.",
@@ -650,6 +655,7 @@
"apihelp-query+extlinks-summary": "Gebe alle externen URLs (nicht Interwiki) der angegebenen Seiten zurück.",
"apihelp-query+extlinks-param-limit": "Wie viele Links zurückgegeben werden sollen.",
"apihelp-query+extlinks-param-query": "Suchbegriff ohne Protokoll. Nützlich um zu prüfen, ob eine bestimmte Seite eine bestimmte externe URL enthält.",
+ "apihelp-query+extlinks-param-expandurl": "Expandiert protokollrelative URLs mit dem kanonischen Protokoll.",
"apihelp-query+extlinks-example-simple": "Rufe eine Liste erxterner Verweise auf <kbd>Main Page</kbd> ab.",
"apihelp-query+exturlusage-summary": "Listet Seiten auf, die die angegebene URL beinhalten.",
"apihelp-query+exturlusage-param-prop": "Welche Informationsteile einbinden:",
@@ -659,7 +665,8 @@
"apihelp-query+exturlusage-param-query": "Suchbegriff ohne Protokoll. Siehe [[Special:LinkSearch]]. Leer lassen, um alle externen Verknüpfungen aufzulisten.",
"apihelp-query+exturlusage-param-namespace": "Die aufzulistenden Seiten-Namensräume.",
"apihelp-query+exturlusage-param-limit": "Wie viele Seiten zurückgegeben werden sollen.",
- "apihelp-query+exturlusage-example-simple": "Zeigt Seiten, die auf <kbd>http://www.mediawiki.org</kbd> verlinken.",
+ "apihelp-query+exturlusage-param-expandurl": "Expandiert protokollrelative URLs mit dem kanonischen Protokoll.",
+ "apihelp-query+exturlusage-example-simple": "Zeigt Seiten, die auf <kbd>https://www.mediawiki.org</kbd> verlinken.",
"apihelp-query+filearchive-summary": "Alle gelöschten Dateien der Reihe nach auflisten.",
"apihelp-query+filearchive-param-from": "Der Bildertitel, bei dem die Auflistung beginnen soll.",
"apihelp-query+filearchive-param-to": "Der Bildertitel, bei dem die Auflistung enden soll.",
@@ -744,7 +751,10 @@
"apihelp-query+info-paramvalue-prop-subjectid": "Die Seitenkennung der Elternseite jeder Diskussionsseite.",
"apihelp-query+info-paramvalue-prop-readable": "Ob der Benutzer diese Seite betrachten darf.",
"apihelp-query+info-paramvalue-prop-displaytitle": "Gibt die Art und Weise an, in der der Seitentitel tatsächlich angezeigt wird.",
+ "apihelp-query+info-paramvalue-prop-varianttitles": "Gibt den Anzeigetitel in allen Varianten der Sprache des Websiteinhalts aus.",
"apihelp-query+info-param-testactions": "Überprüft, ob der aktuelle Benutzer gewisse Aktionen auf der Seite ausführen kann.",
+ "apihelp-query+info-example-simple": "Ruft Informationen über die Seite <kbd>Hauptseite</kbd> ab.",
+ "apihelp-query+iwbacklinks-summary": "Findet alle Seiten, die auf einen angegebenen Interwikilink verlinken.",
"apihelp-query+iwbacklinks-param-prefix": "Präfix für das Interwiki.",
"apihelp-query+iwbacklinks-param-limit": "Wie viele Seiten insgesamt zurückgegeben werden sollen.",
"apihelp-query+iwbacklinks-param-prop": "Zurückzugebende Eigenschaften:",
@@ -759,6 +769,7 @@
"apihelp-query+iwlinks-param-dir": "Die Auflistungsrichtung.",
"apihelp-query+langbacklinks-param-limit": "Wie viele Gesamtseiten zurückgegeben werden sollen.",
"apihelp-query+langbacklinks-param-prop": "Zurückzugebende Eigenschaften:",
+ "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "Ergänzt den Titel des Sprachlinks.",
"apihelp-query+langbacklinks-param-dir": "Die Auflistungsrichtung.",
"apihelp-query+langbacklinks-example-simple": "Ruft Seiten ab, die auf [[:fr:Test]] verlinken.",
"apihelp-query+langlinks-param-limit": "Wie viele Sprachlinks zurückgegeben werden sollen.",
@@ -775,6 +786,7 @@
"apihelp-query+linkshere-param-prop": "Zurückzugebende Eigenschaften:",
"apihelp-query+linkshere-paramvalue-prop-pageid": "Die Seitenkennung jeder Seite.",
"apihelp-query+linkshere-paramvalue-prop-title": "Titel jeder Seite.",
+ "apihelp-query+linkshere-paramvalue-prop-redirect": "Markieren, falls die Seite eine Weiterleitung ist.",
"apihelp-query+linkshere-param-limit": "Wie viel zurückgegeben werden soll.",
"apihelp-query+linkshere-example-simple": "Holt eine Liste von Seiten, die auf [[Main Page]] verlinken.",
"apihelp-query+logevents-summary": "Ruft Ereignisse von Logbüchern ab.",
@@ -783,8 +795,10 @@
"apihelp-query+logevents-paramvalue-prop-title": "Ergänzt den Titel der Seite für das Logbuchereignis.",
"apihelp-query+logevents-paramvalue-prop-type": "Ergänzt den Typ des Logbuchereignisses.",
"apihelp-query+logevents-paramvalue-prop-user": "Ergänzt den verantwortlichen Benutzer für das Logbuchereignis.",
+ "apihelp-query+logevents-paramvalue-prop-timestamp": "Ergänzt den Zeitstempel des Logbucheintrags.",
"apihelp-query+logevents-paramvalue-prop-comment": "Ergänzt den Kommentar des Logbuchereignisses.",
"apihelp-query+logevents-paramvalue-prop-tags": "Listet Markierungen für das Logbuchereignis auf.",
+ "apihelp-query+logevents-param-type": "Filtert nur Logbucheinträge mit diesem Typ heraus.",
"apihelp-query+logevents-param-start": "Der Zeitstempel, bei dem die Aufzählung beginnen soll.",
"apihelp-query+logevents-param-end": "Der Zeitstempel, bei dem die Aufzählung enden soll.",
"apihelp-query+logevents-param-prefix": "Filtert Einträge, die mit diesem Präfix beginnen.",
@@ -794,10 +808,11 @@
"apihelp-query+pageswithprop-param-limit": "Die maximale Anzahl zurückzugebender Seiten.",
"apihelp-query+pageswithprop-param-dir": "In welche Richtung sortiert werden soll.",
"apihelp-query+prefixsearch-param-search": "Such-Zeichenfolge.",
- "apihelp-query+prefixsearch-param-namespace": "Welche Namensräume durchsucht werden sollen.",
+ "apihelp-query+prefixsearch-param-namespace": "Welche Namensräume durchsucht werden sollen. Wird ignoriert, falls <var>$1search</var> mit einem gültigen Namensraumpräfix beginnt.",
"apihelp-query+prefixsearch-param-limit": "Maximale Anzahl zurückzugebender Ergebnisse.",
"apihelp-query+prefixsearch-param-offset": "Anzahl der zu überspringenden Ergebnisse.",
"apihelp-query+prefixsearch-param-profile": "Zu verwendendes Suchprofil.",
+ "apihelp-query+protectedtitles-summary": "Listet alle Titel auf, die vor einer Erstellung geschützt sind.",
"apihelp-query+protectedtitles-param-limit": "Wie viele Seiten insgesamt zurückgegeben werden sollen.",
"apihelp-query+protectedtitles-param-prop": "Zurückzugebende Eigenschaften:",
"apihelp-query+protectedtitles-paramvalue-prop-level": "Ergänzt den Schutzstatus.",
@@ -812,6 +827,7 @@
"apihelp-query+recentchanges-paramvalue-prop-flags": "Ergänzt Markierungen für die Bearbeitung.",
"apihelp-query+recentchanges-paramvalue-prop-timestamp": "Ergänzt den Zeitstempel für die Bearbeitung.",
"apihelp-query+recentchanges-paramvalue-prop-title": "Ergänzt den Seitentitel der Bearbeitung.",
+ "apihelp-query+recentchanges-paramvalue-prop-autopatrolled": "Markiert kontrollierbare Bearbeitungen als automatisch kontrolliert oder nicht.",
"apihelp-query+recentchanges-paramvalue-prop-tags": "Listet Markierungen für den Eintrag auf.",
"apihelp-query+recentchanges-example-simple": "Listet die letzten Änderungen auf.",
"apihelp-query+redirects-param-prop": "Zurückzugebende Eigenschaften:",
@@ -836,6 +852,7 @@
"apihelp-query+search-param-prop": "Eigenschaften zur Rückgabe:",
"apihelp-query+search-param-qiprofile": "Zu verwendendes anfrageunabhängiges Profil (wirkt sich auf den Ranking-Algorithmus aus).",
"apihelp-query+search-paramvalue-prop-wordcount": "Ergänzt den Wortzähler der Seite.",
+ "apihelp-query+search-paramvalue-prop-extensiondata": "Ergänzt zusätzliche von Erweiterungen erzeugte Daten.",
"apihelp-query+search-param-limit": "Wie viele Seiten insgesamt zurückgegeben werden sollen.",
"apihelp-query+search-example-simple": "Nach <kbd>meaning</kbd> suchen.",
"apihelp-query+search-example-text": "Texte nach <kbd>meaning</kbd> durchsuchen.",
@@ -870,6 +887,7 @@
"apihelp-query+usercontribs-paramvalue-prop-size": "Ergänzt die neue Größe der Bearbeitung.",
"apihelp-query+usercontribs-paramvalue-prop-flags": "Ergänzt Markierungen der Bearbeitung.",
"apihelp-query+usercontribs-paramvalue-prop-patrolled": "Markiert kontrollierte Bearbeitungen.",
+ "apihelp-query+usercontribs-paramvalue-prop-autopatrolled": "Markiert automatisch kontrollierte Bearbeitungen.",
"apihelp-query+usercontribs-paramvalue-prop-tags": "Listet die Markierungen für die Bearbeitung auf.",
"apihelp-query+userinfo-paramvalue-prop-blockinfo": "Markiert, ob der aktuelle Benutzer gesperrt ist, von wem und aus welchem Grund.",
"apihelp-query+userinfo-paramvalue-prop-options": "Listet alle Einstellungen auf, die der aktuelle Benutzer festgelegt hat.",
@@ -899,7 +917,9 @@
"apihelp-query+watchlist-paramvalue-prop-parsedcomment": "Ergänzt den geparsten Kommentar der Bearbeitung.",
"apihelp-query+watchlist-paramvalue-prop-timestamp": "Ergänzt den Zeitstempel der Bearbeitung.",
"apihelp-query+watchlist-paramvalue-prop-patrol": "Markiert Bearbeitungen, die kontrolliert sind.",
+ "apihelp-query+watchlist-paramvalue-prop-autopatrol": "Markiert Bearbeitungen, die automatisch kontrolliert sind.",
"apihelp-query+watchlist-paramvalue-prop-sizes": "Ergänzt die alten und neuen Längen der Seite.",
+ "apihelp-query+watchlist-paramvalue-prop-tags": "Listet Markierungen für den Eintrag auf.",
"apihelp-query+watchlist-paramvalue-type-edit": "Normale Seitenbearbeitungen.",
"apihelp-query+watchlist-paramvalue-type-external": "Externe Änderungen.",
"apihelp-query+watchlist-paramvalue-type-new": "Seitenerstellungen.",
@@ -1035,6 +1055,8 @@
"api-help-param-disabled-in-miser-mode": "Deaktiviert aufgrund des [[mw:Special:MyLanguage/Manual:$wgMiserMode|Miser-Modus]].",
"api-help-param-continue": "Falls weitere Ergebnisse verfügbar sind, dies zum Fortfahren verwenden.",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(keine Beschreibung)</span>",
+ "api-help-param-maxbytes": "Kann nicht länger sein als {{PLURAL:$1|ein Byte|$1 Bytes}}.",
+ "api-help-param-maxchars": "Kann nicht länger sein als {{PLURAL:$1|ein|$1}} Zeichen.",
"api-help-examples": "{{PLURAL:$1|Beispiel|Beispiele}}:",
"api-help-permissions": "{{PLURAL:$1|Berechtigung|Berechtigungen}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|Gewährt an}}: $2",
@@ -1055,6 +1077,8 @@
"apierror-invalid-file-key": "Kein gültiger Dateischlüssel.",
"apierror-invalidsection": "Der Parameter <var>section</var> muss eine gültige Abschnittskennung oder <kbd>new</kbd> sein.",
"apierror-invaliduserid": "Die Benutzerkennung <var>$1</var> ist nicht gültig.",
+ "apierror-maxbytes": "Der Parameter <var>$1</var> kann nicht länger sein als {{PLURAL:$2|ein Byte|$2 Bytes}}",
+ "apierror-maxchars": "Der Parameter <var>$1</var> kann nicht länger sein als {{PLURAL:$2|ein|$2}} Zeichen",
"apierror-nosuchsection": "Es gibt keinen Abschnitt $1.",
"apierror-nosuchuserid": "Es gibt keinen Benutzer mit der Kennung $1.",
"apierror-offline": "Aufgrund von Problemen bei der Netzwerkverbindung kannst du nicht weitermachen. Stelle sicher, dass du eine funktionierende Internetverbindung hast und versuche es erneut.",
diff --git a/www/wiki/includes/api/i18n/en.json b/www/wiki/includes/api/i18n/en.json
index dbd54514..6838e545 100644
--- a/www/wiki/includes/api/i18n/en.json
+++ b/www/wiki/includes/api/i18n/en.json
@@ -2,12 +2,13 @@
"@metadata": {
"authors": [
"Anomie",
- "Siebrand"
+ "Siebrand",
+ "Zoranzoki21"
]
},
"apihelp-main-summary": "",
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentation]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailing list]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API Announcements]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & requests]\n</div>\n<strong>Status:</strong> All features shown on this page should be working, but the API is still in active development, and may change at any time. Subscribe to [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce mailing list] for notice of updates.\n\n<strong>Erroneous requests:</strong> When erroneous requests are sent to the API, an HTTP header will be sent with the key \"MediaWiki-API-Error\" and then both the value of the header and the error code sent back will be set to the same value. For more information see [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]].\n\n<strong>Testing:</strong> For ease of testing API requests, see [[Special:ApiSandbox]].",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentation]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailing list]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API Announcements]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & requests]\n</div>\n<strong>Status:</strong> The MediaWiki API is a mature and stable interface that is actively supported and improved. While we try to avoid it, we may occasionally need to make breaking changes; subscribe to [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce mailing list] for notice of updates.\n\n<strong>Erroneous requests:</strong> When erroneous requests are sent to the API, an HTTP header will be sent with the key \"MediaWiki-API-Error\" and then both the value of the header and the error code sent back will be set to the same value. For more information see [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]].\n\n<p class=\"mw-apisandbox-link\"><strong>Testing:</strong> For ease of testing API requests, see [[Special:ApiSandbox]].</p>",
"apihelp-main-param-action": "Which action to perform.",
"apihelp-main-param-format": "The format of the output.",
"apihelp-main-param-maxlag": "Maximum lag can be used when MediaWiki is installed on a database replicated cluster. To save actions causing any more site replication lag, this parameter can make the client wait until the replication lag is less than the specified value. In case of excessive lag, error code <samp>maxlag</samp> is returned with a message like <samp>Waiting for $host: $lag seconds lagged</samp>.<br />See [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: Maxlag parameter]] for more information.",
@@ -64,6 +65,7 @@
"apihelp-compare-param-fromid": "First page ID to compare.",
"apihelp-compare-param-fromrev": "First revision to compare.",
"apihelp-compare-param-fromtext": "Use this text instead of the content of the revision specified by <var>fromtitle</var>, <var>fromid</var> or <var>fromrev</var>.",
+ "apihelp-compare-param-fromsection": "Only use the specified section of the specified 'from' content.",
"apihelp-compare-param-frompst": "Do a pre-save transform on <var>fromtext</var>.",
"apihelp-compare-param-fromcontentmodel": "Content model of <var>fromtext</var>. If not supplied, it will be guessed based on the other parameters.",
"apihelp-compare-param-fromcontentformat": "Content serialization format of <var>fromtext</var>.",
@@ -72,6 +74,7 @@
"apihelp-compare-param-torev": "Second revision to compare.",
"apihelp-compare-param-torelative": "Use a revision relative to the revision determined from <var>fromtitle</var>, <var>fromid</var> or <var>fromrev</var>. All of the other 'to' options will be ignored.",
"apihelp-compare-param-totext": "Use this text instead of the content of the revision specified by <var>totitle</var>, <var>toid</var> or <var>torev</var>.",
+ "apihelp-compare-param-tosection": "Only use the specified section of the specified 'to' content.",
"apihelp-compare-param-topst": "Do a pre-save transform on <var>totext</var>.",
"apihelp-compare-param-tocontentmodel": "Content model of <var>totext</var>. If not supplied, it will be guessed based on the other parameters.",
"apihelp-compare-param-tocontentformat": "Content serialization format of <var>totext</var>.",
@@ -209,8 +212,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Filter by tag.",
"apihelp-feedrecentchanges-param-target": "Show only changes on pages linked from this page.",
"apihelp-feedrecentchanges-param-showlinkedto": "Show changes on pages linked to the selected page instead.",
- "apihelp-feedrecentchanges-param-categories": "Show only changes on pages in all of these categories.",
- "apihelp-feedrecentchanges-param-categories_any": "Show only changes on pages in any of the categories instead.",
"apihelp-feedrecentchanges-example-simple": "Show recent changes.",
"apihelp-feedrecentchanges-example-30days": "Show recent changes for 30 days.",
@@ -250,6 +251,8 @@
"apihelp-import-extended-description": "Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when sending a file for the <var>xml</var> parameter.",
"apihelp-import-param-summary": "Log entry import summary.",
"apihelp-import-param-xml": "Uploaded XML file.",
+ "apihelp-import-param-interwikiprefix": "For uploaded imports: interwiki prefix to apply to unknown user names (and known users if <var>$1assignknownusers</var> is set).",
+ "apihelp-import-param-assignknownusers": "Assign edits to local users where the named user exists locally.",
"apihelp-import-param-interwikisource": "For interwiki imports: wiki to import from.",
"apihelp-import-param-interwikipage": "For interwiki imports: page to import.",
"apihelp-import-param-fullhistory": "For interwiki imports: import the full history, not just the current version.",
@@ -314,7 +317,7 @@
"apihelp-opensearch-summary": "Search the wiki using the OpenSearch protocol.",
"apihelp-opensearch-param-search": "Search string.",
"apihelp-opensearch-param-limit": "Maximum number of results to return.",
- "apihelp-opensearch-param-namespace": "Namespaces to search.",
+ "apihelp-opensearch-param-namespace": "Namespaces to search. Ignored if <var>$1search</var> begins with a valid namespace prefix.",
"apihelp-opensearch-param-suggest": "Do nothing if <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> is false.",
"apihelp-opensearch-param-redirects": "How to handle redirects:\n;return:Return the redirect itself.\n;resolve:Return the target page. May return fewer than $1limit results.\nFor historical reasons, the default is \"return\" for $1format=json and \"resolve\" for other formats.",
"apihelp-opensearch-param-format": "The format of the output.",
@@ -387,6 +390,7 @@
"apihelp-parse-param-disablepp": "Use <var>$1disablelimitreport</var> instead.",
"apihelp-parse-param-disableeditsection": "Omit edit section links from the parser output.",
"apihelp-parse-param-disabletidy": "Do not run HTML cleanup (e.g. tidy) on the parser output.",
+ "apihelp-parse-param-disablestylededuplication": "Do not deduplicate inline stylesheets in the parser output.",
"apihelp-parse-param-generatexml": "Generate XML parse tree (requires content model <code>$1</code>; replaced by <kbd>$2prop=parsetree</kbd>).",
"apihelp-parse-param-preview": "Parse in preview mode.",
"apihelp-parse-param-sectionpreview": "Parse in section preview mode (enables preview mode too).",
@@ -786,7 +790,7 @@
"apihelp-query+exturlusage-param-namespace": "The page namespaces to enumerate.",
"apihelp-query+exturlusage-param-limit": "How many pages to return.",
"apihelp-query+exturlusage-param-expandurl": "Expand protocol-relative URLs with the canonical protocol.",
- "apihelp-query+exturlusage-example-simple": "Show pages linking to <kbd>http://www.mediawiki.org</kbd>.",
+ "apihelp-query+exturlusage-example-simple": "Show pages linking to <kbd>https://www.mediawiki.org</kbd>.",
"apihelp-query+filearchive-summary": "Enumerate all deleted files sequentially.",
"apihelp-query+filearchive-param-from": "The image title to start enumerating from.",
@@ -894,6 +898,7 @@
"apihelp-query+info-paramvalue-prop-readable": "Whether the user can read this page.",
"apihelp-query+info-paramvalue-prop-preload": "Gives the text returned by EditFormPreloadText.",
"apihelp-query+info-paramvalue-prop-displaytitle": "Gives the manner in which the page title is actually displayed.",
+ "apihelp-query+info-paramvalue-prop-varianttitles": "Gives the display title in all variants of the site content language.",
"apihelp-query+info-param-testactions": "Test whether the current user can perform certain actions on the page.",
"apihelp-query+info-param-token": "Use [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] instead.",
"apihelp-query+info-example-simple": "Get information about the page <kbd>Main Page</kbd>.",
@@ -1012,7 +1017,7 @@
"apihelp-query+prefixsearch-summary": "Perform a prefix search for page titles.",
"apihelp-query+prefixsearch-extended-description": "Despite the similarity in names, this module is not intended to be equivalent to [[Special:PrefixIndex]]; for that, see <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> with the <kbd>apprefix</kbd> parameter. The purpose of this module is similar to <kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd>: to take user input and provide the best-matching titles. Depending on the search engine backend, this might include typo correction, redirect avoidance, or other heuristics.",
"apihelp-query+prefixsearch-param-search": "Search string.",
- "apihelp-query+prefixsearch-param-namespace": "Namespaces to search.",
+ "apihelp-query+prefixsearch-param-namespace": "Namespaces to search. Ignored if <var>$1search</var> begins with a valid namespace prefix.",
"apihelp-query+prefixsearch-param-limit": "Maximum number of results to return.",
"apihelp-query+prefixsearch-param-offset": "Number of results to skip.",
"apihelp-query+prefixsearch-example-simple": "Search for page titles beginning with <kbd>meaning</kbd>.",
@@ -1068,6 +1073,7 @@
"apihelp-query+recentchanges-paramvalue-prop-sizes": "Adds the new and old page length in bytes.",
"apihelp-query+recentchanges-paramvalue-prop-redirect": "Tags edit if page is a redirect.",
"apihelp-query+recentchanges-paramvalue-prop-patrolled": "Tags patrollable edits as being patrolled or unpatrolled.",
+ "apihelp-query+recentchanges-paramvalue-prop-autopatrolled": "Tags patrollable edits as being autopatrolled or not.",
"apihelp-query+recentchanges-paramvalue-prop-loginfo": "Adds log information (log ID, log type, etc) to log entries.",
"apihelp-query+recentchanges-paramvalue-prop-tags": "Lists tags for the entry.",
"apihelp-query+recentchanges-paramvalue-prop-sha1": "Adds the content checksum for entries associated with a revision.",
@@ -1151,6 +1157,7 @@
"apihelp-query+search-paramvalue-prop-sectiontitle": "Adds the title of the matching section.",
"apihelp-query+search-paramvalue-prop-categorysnippet": "Adds a parsed snippet of the matching category.",
"apihelp-query+search-paramvalue-prop-isfilematch": "Adds a boolean indicating if the search matched file content.",
+ "apihelp-query+search-paramvalue-prop-extensiondata": "Adds extra data generated by extensions.",
"apihelp-query+search-paramvalue-prop-score": "Ignored.",
"apihelp-query+search-paramvalue-prop-hasrelated": "Ignored.",
"apihelp-query+search-param-limit": "How many total pages to return.",
@@ -1256,6 +1263,7 @@
"apihelp-query+usercontribs-paramvalue-prop-sizediff": "Adds the size delta of the edit against its parent.",
"apihelp-query+usercontribs-paramvalue-prop-flags": "Adds flags of the edit.",
"apihelp-query+usercontribs-paramvalue-prop-patrolled": "Tags patrolled edits.",
+ "apihelp-query+usercontribs-paramvalue-prop-autopatrolled": "Tags autopatrolled edits.",
"apihelp-query+usercontribs-paramvalue-prop-tags": "Lists tags for the edit.",
"apihelp-query+usercontribs-param-show": "Show only items that meet these criteria, e.g. non minor edits only: <kbd>$2show=!minor</kbd>.\n\nIf <kbd>$2show=patrolled</kbd> or <kbd>$2show=!patrolled</kbd> is set, revisions older than <var>[[mw:Special:MyLanguage/Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ($1 {{PLURAL:$1|second|seconds}}) won't be shown.",
"apihelp-query+usercontribs-param-tag": "Only list revisions tagged with this tag.",
@@ -1323,9 +1331,11 @@
"apihelp-query+watchlist-paramvalue-prop-parsedcomment": "Adds parsed comment of the edit.",
"apihelp-query+watchlist-paramvalue-prop-timestamp": "Adds timestamp of the edit.",
"apihelp-query+watchlist-paramvalue-prop-patrol": "Tags edits that are patrolled.",
+ "apihelp-query+watchlist-paramvalue-prop-autopatrol": "Tags edits that are autopatrolled.",
"apihelp-query+watchlist-paramvalue-prop-sizes": "Adds the old and new lengths of the page.",
"apihelp-query+watchlist-paramvalue-prop-notificationtimestamp": "Adds timestamp of when the user was last notified about the edit.",
"apihelp-query+watchlist-paramvalue-prop-loginfo": "Adds log information where appropriate.",
+ "apihelp-query+watchlist-paramvalue-prop-tags": "Lists tags for the entry.",
"apihelp-query+watchlist-param-show": "Show only items that meet these criteria. For example, to see only minor edits done by logged-in users, set $1show=minor|!anon.",
"apihelp-query+watchlist-param-type": "Which types of changes to show:",
"apihelp-query+watchlist-paramvalue-type-edit": "Regular page edits.",
@@ -1605,6 +1615,8 @@
"api-help-param-direction": "In which direction to enumerate:\n;newer:List oldest first. Note: $1start has to be before $1end.\n;older:List newest first (default). Note: $1start has to be later than $1end.",
"api-help-param-continue": "When more results are available, use this to continue.",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(no description)</span>",
+ "api-help-param-maxbytes": "Cannot be longer than $1 {{PLURAL:$1|byte|bytes}}.",
+ "api-help-param-maxchars": "Cannot be longer than $1 {{PLURAL:$1|character|characters}}.",
"api-help-examples": "{{PLURAL:$1|Example|Examples}}:",
"api-help-permissions": "{{PLURAL:$1|Permission|Permissions}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|Granted to}}: $2",
@@ -1670,6 +1682,8 @@
"apierror-chunk-too-small": "Minimum chunk size is $1 {{PLURAL:$1|byte|bytes}} for non-final chunks.",
"apierror-cidrtoobroad": "$1 CIDR ranges broader than /$2 are not accepted.",
"apierror-compare-no-title": "Cannot pre-save transform without a title. Try specifying <var>fromtitle</var> or <var>totitle</var>.",
+ "apierror-compare-nosuchfromsection": "There is no section $1 in the 'from' content.",
+ "apierror-compare-nosuchtosection": "There is no section $1 in the 'to' content.",
"apierror-compare-relative-to-nothing": "No 'from' revision for <var>torelative</var> to be relative to.",
"apierror-contentserializationexception": "Content serialization failed: $1",
"apierror-contenttoobig": "The content you supplied exceeds the article size limit of $1 {{PLURAL:$1|kilobyte|kilobytes}}.",
@@ -1710,6 +1724,8 @@
"apierror-invalidurlparam": "Invalid value for <var>$1urlparam</var> (<kbd>$2=$3</kbd>).",
"apierror-invaliduser": "Invalid username \"$1\".",
"apierror-invaliduserid": "User ID <var>$1</var> is not valid.",
+ "apierror-maxbytes": "Parameter <var>$1</var> cannot be longer than $2 {{PLURAL:$2|byte|bytes}}",
+ "apierror-maxchars": "Parameter <var>$1</var> cannot be longer than $2 {{PLURAL:$2|character|characters}}",
"apierror-maxlag-generic": "Waiting for a database server: $1 {{PLURAL:$1|second|seconds}} lagged.",
"apierror-maxlag": "Waiting for $2: $1 {{PLURAL:$1|second|seconds}} lagged.",
"apierror-mimesearchdisabled": "MIME search is disabled in Miser Mode.",
@@ -1725,7 +1741,6 @@
"apierror-missingtitle-byname": "The page $1 doesn't exist.",
"apierror-moduledisabled": "The <kbd>$1</kbd> module has been disabled.",
"apierror-multival-only-one-of": "{{PLURAL:$3|Only|Only one of}} $2 is allowed for parameter <var>$1</var>.",
- "apierror-multival-only-one": "Only one value is allowed for parameter <var>$1</var>.",
"apierror-multpages": "<var>$1</var> may only be used with a single page.",
"apierror-mustbeloggedin-changeauth": "You must be logged in to change authentication data.",
"apierror-mustbeloggedin-generic": "You must be logged in.",
diff --git a/www/wiki/includes/api/i18n/es.json b/www/wiki/includes/api/i18n/es.json
index af3097bc..0816ed77 100644
--- a/www/wiki/includes/api/i18n/es.json
+++ b/www/wiki/includes/api/i18n/es.json
@@ -29,13 +29,14 @@
"Igv",
"Fortega",
"Luzcaru",
- "Javiersanp"
+ "Javiersanp",
+ "KATRINE1992"
]
},
"apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentation]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailing list]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API Announcements]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & requests]\n</div>\n<strong>Status:</strong> Todas las funciones mostradas en esta página deberían estar funcionando, pero la API aún está en desarrollo activo, y puede cambiar en cualquier momento. Suscribase a [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce mailing list] para aviso de actualizaciones.\n\n<strong>Erroneous requests:</strong> Cuando se envían solicitudes erróneas a la API, se enviará un encabezado HTTP con la clave \"MediaWiki-API-Error\" y, luego, el valor del encabezado y el código de error devuelto se establecerán en el mismo valor. Para más información ver [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]].\n\n<strong>Testing:</strong> Para facilitar la comprobación de las solicitudes de API, consulte [[Special:ApiSandbox]].",
"apihelp-main-param-action": "Qué acción se realizará.",
"apihelp-main-param-format": "El formato de la salida.",
- "apihelp-main-param-maxlag": "El retraso máximo puede utilizarse cuando MediaWiki se instala en un clúster replicado de base de datos. Para guardar las acciones que causan más retardo de replicación de sitio, este parámetro puede hacer que el cliente espere hasta que el retardo de replicación sea menor que el valor especificado. En caso de retraso excesivo, se devuelve el código de error <samp>maxlag</samp> con un mensaje como <samp>Esperando a $host: $lag segundos de retraso</samp>.<br />Consulta [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: parámetro Maxlag]] para más información.",
+ "apihelp-main-param-maxlag": "El retardo máximo puede utilizarse cuando MediaWiki se instala en una agrupación replicada de bases de datos. Para guardar las acciones que causan más retardo de replicación de sitio, este parámetro puede hacer que el cliente espere hasta que el retardo de replicación sea menor que el valor especificado. En caso de un retardo excesivo, se devuelve el código de error <samp>maxlag</samp> con un mensaje como <samp>Esperando a $host: $lag segundos de retardo</samp>.<br />Consulta [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: parámetro Maxlag]] para más información.",
"apihelp-main-param-smaxage": "Establece la cabecera HTTP <code>s-maxage</code> de control de antememoria a esta cantidad de segundos. Los errores nunca se almacenan en la antememoria.",
"apihelp-main-param-maxage": "Establece la cabecera HTTP <code>max-age</code> de control de antememoria a esta cantidad de segundos. Los errores nunca se almacenan en la antememoria.",
"apihelp-main-param-assert": "Comprobar que el usuario haya iniciado sesión si el valor es <kbd>user</kbd> o si tiene el permiso de bot si es <kbd>bot</kbd>.",
@@ -82,9 +83,14 @@
"apihelp-compare-param-fromtitle": "Primer título para comparar",
"apihelp-compare-param-fromid": "ID de la primera página a comparar.",
"apihelp-compare-param-fromrev": "Primera revisión para comparar.",
+ "apihelp-compare-param-fromsection": "Solamente usar la sección especificada del contenido 'from' especificado.",
"apihelp-compare-param-totitle": "Segundo título para comparar.",
"apihelp-compare-param-toid": "Segunda identificador de página para comparar.",
"apihelp-compare-param-torev": "Segunda revisión para comparar.",
+ "apihelp-compare-param-tosection": "Solamente usar la sección especificada del contenido 'to' especificado.",
+ "apihelp-compare-param-prop": "Cuáles fragmentos de información se obtendrán.",
+ "apihelp-compare-paramvalue-prop-diff": "El HTML de las diferencias.",
+ "apihelp-compare-paramvalue-prop-diffsize": "El tamaño del HTML de las diferencias, en bytes.",
"apihelp-compare-example-1": "Crear una diferencia entre las revisiones 1 y 2.",
"apihelp-createaccount-summary": "Crear una nueva cuenta de usuario.",
"apihelp-createaccount-param-preservestate": "Si <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> devolvió true (verdadero) para <samp>hasprimarypreservedstate</samp>, deberían omitirse las peticiones marcadas como <samp>primary-required</samp>. Si devolvió un valor no vacío para <samp>preservedusername</samp>, se debe usar ese nombre de usuario en el parámetro <var>username</var>.",
@@ -155,7 +161,7 @@
"apihelp-expandtemplates-summary": "Expande todas las plantillas en wikitexto.",
"apihelp-expandtemplates-param-title": "Título de la página.",
"apihelp-expandtemplates-param-text": "Sintaxis wiki que se convertirá.",
- "apihelp-expandtemplates-param-revid": "Revisión de ID, para <code><nowiki>{{REVISIONID}}</nowiki></code> y variables similares.",
+ "apihelp-expandtemplates-param-revid": "Id. de revisión, para <code><nowiki>{{REVISIONID}}</nowiki></code> y variables similares.",
"apihelp-expandtemplates-param-prop": "Qué elementos de información se utilizan para llegar.\n\nTenga en cuenta que si no se seleccionan los valores, el resultado contendrá el wikitexto, pero la salida será en un formato obsoleto.",
"apihelp-expandtemplates-paramvalue-prop-wikitext": "El wikitexto expandido.",
"apihelp-expandtemplates-paramvalue-prop-categories": "Cualesquiera categorías presentes en la entrada que no están representadas en salida de wikitexto.",
@@ -169,7 +175,7 @@
"apihelp-expandtemplates-param-includecomments": "Incluir o no los comentarios HTML en la salida.",
"apihelp-expandtemplates-param-generatexml": "Generar un árbol de análisis XML (remplazado por $1prop=parsetree).",
"apihelp-expandtemplates-example-simple": "Expandir el wikitexto <kbd><nowiki>{{Project:Sandbox}}</nowiki></kbd>.",
- "apihelp-feedcontributions-summary": "Devuelve el canal de contribuciones de un usuario.",
+ "apihelp-feedcontributions-summary": "Devuelve el suministro de contribuciones de un usuario.",
"apihelp-feedcontributions-param-feedformat": "El formato del suministro.",
"apihelp-feedcontributions-param-user": "De qué usuarios recibir contribuciones.",
"apihelp-feedcontributions-param-namespace": "Espacio de nombre para filtrar las contribuciones.",
@@ -182,7 +188,7 @@
"apihelp-feedcontributions-param-hideminor": "Ocultar ediciones menores.",
"apihelp-feedcontributions-param-showsizediff": "Mostrar la diferencia de tamaño entre revisiones.",
"apihelp-feedcontributions-example-simple": "Devolver las contribuciones del usuario <kbd>Example</kbd>.",
- "apihelp-feedrecentchanges-summary": "Devuelve un canal de cambios recientes.",
+ "apihelp-feedrecentchanges-summary": "Devuelve un suministro de cambios recientes.",
"apihelp-feedrecentchanges-param-feedformat": "El formato del suministro.",
"apihelp-feedrecentchanges-param-namespace": "Espacio de nombres al cual limitar los resultados.",
"apihelp-feedrecentchanges-param-invert": "Todos los espacios de nombres menos el que está seleccionado.",
@@ -200,15 +206,13 @@
"apihelp-feedrecentchanges-param-tagfilter": "Filtrar por etiquetas.",
"apihelp-feedrecentchanges-param-target": "Mostrar solo los cambios en las páginas enlazadas en esta.",
"apihelp-feedrecentchanges-param-showlinkedto": "Mostrar los cambios en páginas enlazadas con la página seleccionada.",
- "apihelp-feedrecentchanges-param-categories": "Mostrar sólo cambios en las páginas en todas estas categorías.",
- "apihelp-feedrecentchanges-param-categories_any": "Mostrar sólo cambios en las páginas en cualquiera de las categorías en lugar.",
"apihelp-feedrecentchanges-example-simple": "Mostrar los cambios recientes.",
"apihelp-feedrecentchanges-example-30days": "Mostrar los cambios recientes limitados a 30 días.",
- "apihelp-feedwatchlist-summary": "Devuelve el canal de una lista de seguimiento.",
+ "apihelp-feedwatchlist-summary": "Devuelve el suministro de una lista de seguimiento.",
"apihelp-feedwatchlist-param-feedformat": "El formato del suministro.",
"apihelp-feedwatchlist-param-hours": "Listar las páginas modificadas desde estas horas hasta ahora.",
"apihelp-feedwatchlist-param-linktosections": "Enlazar directamente a las secciones cambiadas de ser posible.",
- "apihelp-feedwatchlist-example-default": "Mostrar el canal de la lista de seguimiento.",
+ "apihelp-feedwatchlist-example-default": "Mostrar el suministro de la lista de seguimiento.",
"apihelp-feedwatchlist-example-all6hrs": "Mostrar todos los cambios en páginas vigiladas en las últimas 6 horas.",
"apihelp-filerevert-summary": "Revertir el archivo a una versión anterior.",
"apihelp-filerevert-param-filename": "Nombre de archivo final, sin el prefijo Archivo:",
@@ -236,6 +240,8 @@
"apihelp-import-extended-description": "Tenga en cuenta que el HTTP POST debe hacerse como una carga de archivos (es decir, el uso de multipart/form-data) al enviar un archivo para el parámetro <var>xml</var>.",
"apihelp-import-param-summary": "Resumen de importación de entrada del registro.",
"apihelp-import-param-xml": "Se cargó el archivo XML.",
+ "apihelp-import-param-interwikiprefix": "Para importaciones cargadas: el prefijo de interwiki debe aplicarse a los nombres de usuario desconocidos (y a los conocidos si se define <var>$1assignknownusers</var>).",
+ "apihelp-import-param-assignknownusers": "Asignar ediciones a usuarios locales cuando sus nombres de usuario existan localmente.",
"apihelp-import-param-interwikisource": "Para importaciones interwiki: wiki desde la que importar.",
"apihelp-import-param-interwikipage": "Para importaciones interwiki: página a importar.",
"apihelp-import-param-fullhistory": "Para importaciones interwiki: importar todo el historial, no solo la versión actual.",
@@ -293,7 +299,7 @@
"apihelp-opensearch-summary": "Buscar en el wiki mediante el protocolo OpenSearch.",
"apihelp-opensearch-param-search": "Buscar cadena.",
"apihelp-opensearch-param-limit": "Número máximo de resultados que devolver.",
- "apihelp-opensearch-param-namespace": "Espacio de nombres que buscar.",
+ "apihelp-opensearch-param-namespace": "Espacios de nombres que buscar. Se ignora si <var>$1search</var> comienza por un prefijo de espacio de nombre válido.",
"apihelp-opensearch-param-suggest": "No hacer nada si <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> es falso.",
"apihelp-opensearch-param-redirects": "Cómo manejar las redirecciones:\n;return: Volver a la propia redirección.\n;resolve: Volver a la página de destino. Puede devolver menos de $1limit resultados.\nPor motivos históricos, se utiliza \"return\" para $1format=json y \"resolve\" para otros formatos.",
"apihelp-opensearch-param-format": "El formato de salida.",
@@ -546,12 +552,12 @@
"apihelp-query+allrevisions-param-generatetitles": "Cuando se utilice como generador, genera títulos en lugar de ID de revisión.",
"apihelp-query+allrevisions-example-user": "Listar las últimas 50 contribuciones del usuario <kbd>Example</kbd>.",
"apihelp-query+allrevisions-example-ns-main": "Listar las primeras 50 revisiones en el espacio de nombres principal.",
- "apihelp-query+mystashedfiles-summary": "Obtener una lista de archivos en la corriente de carga de usuarios.",
+ "apihelp-query+mystashedfiles-summary": "Obtener una lista de los archivos en el almacén provisional de cargas del usuario actual.",
"apihelp-query+mystashedfiles-param-prop": "Propiedades a buscar para los archivos.",
"apihelp-query+mystashedfiles-paramvalue-prop-size": "Buscar el tamaño del archivo y las dimensiones de la imagen.",
"apihelp-query+mystashedfiles-paramvalue-prop-type": "Obtener el tipo MIME y tipo multimedia del archivo.",
"apihelp-query+mystashedfiles-param-limit": "Cuántos archivos obtener.",
- "apihelp-query+mystashedfiles-example-simple": "Obtenga la clave de archivo, el tamaño del archivo y el tamaño de los archivos en pixeles en el caché de carga del usuario actual.",
+ "apihelp-query+mystashedfiles-example-simple": "Obtener la clave de archivo, el tamaño del archivo y el tamaño en píxeles de los archivos en el almacén provisional de cargas del usuario actual.",
"apihelp-query+alltransclusions-summary": "Mostrar todas las transclusiones (páginas integradas mediante &#123;&#123;x&#125;&#125;), incluidas las inexistentes.",
"apihelp-query+alltransclusions-param-from": "El título de la transclusión por la que empezar la enumeración.",
"apihelp-query+alltransclusions-param-to": "El título de la transclusión por la que terminar la enumeración.",
@@ -730,7 +736,7 @@
"apihelp-query+exturlusage-param-namespace": "Los espacios de nombres que enumerar.",
"apihelp-query+exturlusage-param-limit": "Cuántas páginas se devolverán.",
"apihelp-query+exturlusage-param-expandurl": "Expandir las URL relativas a un protocolo con el protocolo canónico.",
- "apihelp-query+exturlusage-example-simple": "Mostrar páginas que enlacen con <kbd>http://www.mediawiki.org</kbd>.",
+ "apihelp-query+exturlusage-example-simple": "Mostrar páginas que enlacen con <kbd>https://www.mediawiki.org</kbd>.",
"apihelp-query+filearchive-summary": "Enumerar todos los archivos borrados de forma secuencial.",
"apihelp-query+filearchive-param-from": "El título de imagen para comenzar la enumeración",
"apihelp-query+filearchive-param-to": "El título de imagen para detener la enumeración.",
@@ -938,7 +944,7 @@
"apihelp-query+prefixsearch-summary": "Realice una búsqueda de prefijo de títulos de página.",
"apihelp-query+prefixsearch-extended-description": "A pesar de la similitud en los nombres, este módulo no pretende ser equivalente a [[Special:PrefixIndex]]; para eso, vea <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> con el parámetro <kbd> apprefix</kbd>. El propósito de este módulo es similar a <kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd>: para tomar la entrada del usuario y proporcionar los mejores títulos coincidentes. Dependiendo del motor de búsqueda backend, esto puede incluir la corrección de errores, redirigir la evasión, u otras heurísticas.",
"apihelp-query+prefixsearch-param-search": "Buscar cadena.",
- "apihelp-query+prefixsearch-param-namespace": "Espacio de nombres que buscar.",
+ "apihelp-query+prefixsearch-param-namespace": "Espacios de nombres que buscar. Se ignora si <var>$1search</var> comienza por un prefijo de espacio de nombre válido.",
"apihelp-query+prefixsearch-param-limit": "Número máximo de resultados que devolver.",
"apihelp-query+prefixsearch-param-offset": "Número de resultados que omitir.",
"apihelp-query+prefixsearch-example-simple": "Buscar títulos de páginas que empiecen con <kbd>meaning</kbd>.",
@@ -1063,6 +1069,7 @@
"apihelp-query+search-paramvalue-prop-sectiontitle": "Añade el título de la sección correspondiente.",
"apihelp-query+search-paramvalue-prop-categorysnippet": "Añade un fragmento analizado de la categoría correspondiente.",
"apihelp-query+search-paramvalue-prop-isfilematch": "Añade un booleano que indica si la búsqueda corresponde al contenido del archivo.",
+ "apihelp-query+search-paramvalue-prop-extensiondata": "Añade datos adicionales generados por las extensiones.",
"apihelp-query+search-paramvalue-prop-score": "Ignorado.",
"apihelp-query+search-paramvalue-prop-hasrelated": "Ignorado",
"apihelp-query+search-param-limit": "Cuántas páginas en total se devolverán.",
@@ -1100,6 +1107,7 @@
"apihelp-query+siteinfo-example-simple": "Obtener información del sitio.",
"apihelp-query+siteinfo-example-interwiki": "Obtener una lista de prefijos interwiki locales.",
"apihelp-query+stashimageinfo-summary": "Devuelve información del archivo para archivos escondidos.",
+ "apihelp-query+stashimageinfo-param-filekey": "Clave que identifica una carga anterior que se guardó en el almacén provisional.",
"apihelp-query+stashimageinfo-param-sessionkey": "Alias de $1filekey, para retrocompatibilidad.",
"apihelp-query+stashimageinfo-example-simple": "Devuelve información para un archivo escondido.",
"apihelp-query+stashimageinfo-example-params": "Devuelve las miniaturas de dos archivos escondidos.",
@@ -1208,6 +1216,7 @@
"apihelp-query+watchlist-paramvalue-prop-sizes": "Añade la longitud vieja y la nueva de la página.",
"apihelp-query+watchlist-paramvalue-prop-notificationtimestamp": "Añade fecha y hora de cuando el usuario fue notificado por última vez acerca de la edición.",
"apihelp-query+watchlist-paramvalue-prop-loginfo": "Añade información del registro cuando corresponda.",
+ "apihelp-query+watchlist-paramvalue-prop-tags": "Enumera las etiquetas de la entrada.",
"apihelp-query+watchlist-param-show": "Muestra solo los elementos que cumplan estos criterios. Por ejemplo, para ver solo ediciones menores realizadas por usuarios conectados, introduce $1show=minor|!anon.",
"apihelp-query+watchlist-param-type": "Qué tipos de cambios mostrar:",
"apihelp-query+watchlist-paramvalue-type-edit": "Ediciones comunes a páginas",
@@ -1323,6 +1332,7 @@
"apihelp-upload-param-ignorewarnings": "Ignorar las advertencias.",
"apihelp-upload-param-file": "Contenido del archivo.",
"apihelp-upload-param-url": "URL de la que obtener el archivo.",
+ "apihelp-upload-param-filekey": "Clave que identifica una carga anterior que se guardó en el almacén provisional.",
"apihelp-upload-param-sessionkey": "Idéntico a $1filekey, mantenido por razones de retrocompatibilidad.",
"apihelp-upload-param-filesize": "Tamaño de archivo total de la carga.",
"apihelp-upload-param-offset": "Posición del fragmento en bytes.",
@@ -1387,6 +1397,7 @@
"api-help-title": "Ayuda de la API de MediaWiki",
"api-help-lead": "Esta es una página de documentación autogenerada de la API de MediaWiki.\n\nDocumentación y ejemplos: https://www.mediawiki.org/wiki/API",
"api-help-main-header": "Módulo principal",
+ "api-help-undocumented-module": "No existe ninguna documentación para el módulo $1.",
"api-help-flag-deprecated": "Este módulo está en desuso.",
"api-help-flag-internal": "<strong>Este módulo es interno o inestable.</strong> Su funcionamiento puede cambiar sin previo aviso.",
"api-help-flag-readrights": "Este módulo requiere permisos de lectura.",
@@ -1425,6 +1436,8 @@
"api-help-param-direction": "En qué sentido hacer la enumeración:\n;newer: De más antiguos a más recientes. Nota: $1start debe ser anterior a $1end.\n;older: De más recientes a más antiguos (orden predefinido). Nota: $1start debe ser posterior a $1end.",
"api-help-param-continue": "Cuando haya más resultados disponibles, utiliza esto para continuar.",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(sin descripción)</span>",
+ "api-help-param-maxbytes": "No puede sobrepasar $1 {{PLURAL:$1|byte|bytes}} de longitud.",
+ "api-help-param-maxchars": "No puede sobrepasar $1 {{PLURAL:$1|carácter|caracteres}} de longitud.",
"api-help-examples": "{{PLURAL:$1|Ejemplo|Ejemplos}}:",
"api-help-permissions": "{{PLURAL:$1|Permiso|Permisos}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|Concedido a|Concedidos a}}: $2",
@@ -1475,6 +1488,8 @@
"apierror-cantsend": "No estás conectado, no tienes una dirección de correo electrónico confirmada o no tienes permiso para enviar correo electrónico a otros usuarios, así que no puedes enviar correo electrónico.",
"apierror-cantundelete": "No se ha podido restaurar: puede que las revisiones solicitadas no existan o que ya se hayan restaurado.",
"apierror-changeauth-norequest": "No se ha podido crear la petición de modificación.",
+ "apierror-compare-nosuchfromsection": "No existe una sección $1 en el contenido 'from'.",
+ "apierror-compare-nosuchtosection": "No existe una sección $1 en el contenido 'to'.",
"apierror-contentserializationexception": "La serialización de contenido falló: $1",
"apierror-contenttoobig": "El contenido que has suministrado supera el tamaño máximo de archivo de $1 {{PLURAL:$1|kilobyte|kilobytes}}.",
"apierror-copyuploadbaddomain": "No se permite realizar cargas a partir de este dominio.",
@@ -1511,6 +1526,8 @@
"apierror-invalidurlparam": "Valor no válido para <var>$1urlparam</var> (<kbd>$2=$3</kbd>).",
"apierror-invaliduser": "Nombre de usuario «$1» no válido.",
"apierror-invaliduserid": "El identificador de usuario <var>$1</var> no es válido.",
+ "apierror-maxbytes": "El parámetro <var>$1</var> no puede sobrepasar $2 {{PLURAL:$2|byte|bytes}}",
+ "apierror-maxchars": "El parámetro <var>$1</var> no puede sobrepasar $2 {{PLURAL:$2|carácter|caracteres}} de longitud.",
"apierror-mimesearchdisabled": "La búsqueda MIME está deshabilitada en el modo avaro.",
"apierror-missingcontent-pageid": "Contenido faltante para la página con identificador $1.",
"apierror-missingparam-at-least-one-of": "{{PLURAL:$2|El parámetro|Al menos uno de los parámetros}} $1 es necesario.",
@@ -1522,12 +1539,12 @@
"apierror-missingtitle-byname": "La página $1 no existe.",
"apierror-moduledisabled": "El módulo <kbd>$1</kbd> ha sido deshabilitado.",
"apierror-multival-only-one-of": "Solo {{PLURAL:$3|se permite el valor|se permiten los valores}} $2 para el parámetro <var>$1</var>.",
- "apierror-multival-only-one": "Solo se permite un valor para el parámetro <var>$1</var>.",
"apierror-multpages": "<var>$1</var> no se puede utilizar más que con una sola página.",
"apierror-mustbeloggedin-changeauth": "Debes estar conectado para poder cambiar los datos de autentificación.",
"apierror-mustbeloggedin-generic": "Debes estar conectado.",
"apierror-mustbeloggedin-linkaccounts": "Debes estar conectado para enlazar cuentas.",
"apierror-mustbeloggedin-removeauth": "Debes estar conectado para borrar datos de autentificación.",
+ "apierror-mustbeloggedin-uploadstash": "El almacén provisional de cargas solo está disponible para usuarios que hayan accedido a una cuenta.",
"apierror-mustbeloggedin": "Debes estar conectado para $1.",
"apierror-mustbeposted": "El módulo <kbd>$1</kbd> requiere una petición POST.",
"apierror-mustpostparams": "Se {{PLURAL:$2|encontró el siguiente parámetro|encontraron los siguientes parámetros}} en la cadena de la consulta, pero deben estar en el cuerpo del POST: $1.",
@@ -1563,6 +1580,7 @@
"apierror-promised-nonwrite-api": "La cabecera HTTP <code>Promise-Non-Write-API-Action</code> no se puede enviar a módulos de la API en modo escritura.",
"apierror-protect-invalidaction": "Tipo de protección «$1» no válido.",
"apierror-protect-invalidlevel": "Nivel de protección «$1» no válido.",
+ "apierror-ratelimited": "Has excedido tu límite de frecuencia. Aguarda unos minutos y vuelve a intentarlo.",
"apierror-readapidenied": "Necesitas permiso de lectura para utilizar este módulo.",
"apierror-readonly": "El wiki está actualmente en modo de solo lectura.",
"apierror-reauthenticate": "No te has autentificado recientemente en esta sesión. Por favor, vuelve a autentificarte.",
@@ -1581,7 +1599,10 @@
"apierror-sizediffdisabled": "La diferencia de tamaño está deshabilitada en el modo avaro.",
"apierror-spamdetected": "Tu edición fue rechazada por contener un fragmento de spam: <code>$1</code>.",
"apierror-specialpage-cantexecute": "No tienes permiso para ver los resultados de esta página especial.",
+ "apierror-stashedfilenotfound": "No se pudo encontrar el archivo en el almacén provisional: $1.",
+ "apierror-stashfilestorage": "No se pudo guardar la carga en el almacén provisional: $1",
"apierror-stashwrongowner": "Propietario incorrecto: $1",
+ "apierror-stashzerolength": "El archivo mide cero bytes y no puede guardarse en el almacén provisional: $1.",
"apierror-systemblocked": "Has sido bloqueado automáticamente por el software MediaWiki.",
"apierror-templateexpansion-notwikitext": "La expansión de plantillas solo es compatible con el contenido en wikitexto. $1 usa el modelo de contenido $2.",
"apierror-timeout": "El servidor no respondió en el plazo previsto.",
@@ -1593,6 +1614,7 @@
"apierror-unrecognizedparams": "{{PLURAL:$2|Parámetro no reconocido|Parámetros no reconocidos}}: $1.",
"apierror-unrecognizedvalue": "Valor no reconocido para el parámetro <var>$1</var>: $2.",
"apierror-unsupportedrepo": "El repositorio local de archivos no permite consultar todas las imágenes.",
+ "apierror-upload-inprogress": "Ya está en curso la carga desde el almacén provisional.",
"apierror-urlparamnormal": "No se pudieron normalizar los parámetros de imagen de $1.",
"apierror-writeapidenied": "No tienes permiso para editar este wiki a través de la API.",
"apiwarn-alldeletedrevisions-performance": "Para conseguir un mejor rendimiento a la hora de generar títulos, establece <kbd>$1dir=newer</kbd>.",
diff --git a/www/wiki/includes/api/i18n/eu.json b/www/wiki/includes/api/i18n/eu.json
index 32c5164b..f15e8194 100644
--- a/www/wiki/includes/api/i18n/eu.json
+++ b/www/wiki/includes/api/i18n/eu.json
@@ -5,13 +5,15 @@
"Sator",
"An13sa",
"Gorkaazk",
- "Mikel Ibaiba"
+ "Mikel Ibaiba",
+ "Iñaki LL"
]
},
"apihelp-main-param-action": "Zein ekintza burutuko da.",
"apihelp-main-param-format": "Irteerako formatua.",
"apihelp-main-param-assertuser": "Egiaztatu erabiltzaile hau izendatutakoa dela.",
"apihelp-main-param-requestid": "Hemen emandako edozein balio erantzunean kontuan hartuko da. Eskaerak ezberdintzeko erabili ahalko da.",
+ "apihelp-main-param-servedby": "Sartu eskaera emaitzetan zerbitzariaren izena.",
"apihelp-main-param-curtimestamp": "Emaitzan oraingo denbora-zigilua jarri.",
"apihelp-block-summary": "Blokeatu erabiltzaile bat.",
"apihelp-block-param-userid": "Erabiltzaile IDa blokeatzear. Ezin da honekin batera erabili: <var>$1user</var>.",
@@ -20,7 +22,12 @@
"apihelp-block-param-nocreate": "Saihestu kontuak sortzea.",
"apihelp-block-param-reblock": "Erabiltzailea honezkero blokeatuta badago, lehendik dagoen blokea gainidatzi.",
"apihelp-block-param-watchuser": "Ikusi erabiltzaile edo IP helbidearen erabiltzaileak eta mintzamen orriak.",
+ "apihelp-block-param-tags": "Aldatu etiketak blokeen erregistro sarreran aplikatzeko.",
+ "apihelp-changeauthenticationdata-summary": "Aldatu autentifikazio datuak uneko erabiltzailearentzat.",
+ "apihelp-checktoken-param-type": "Frogatzen ari den token mota.",
"apihelp-checktoken-param-token": "Testatzeko hartuta.",
+ "apihelp-checktoken-param-maxtokenage": "Token-aren gehienezko adin baimendua, segundutan.",
+ "apihelp-clientlogin-summary": "Hasi saioa wikiera fluxu interaktiboa erabiliz.",
"apihelp-compare-summary": "Bi orrien arteko ezberdintasuna jaso.",
"apihelp-compare-param-fromtitle": "Aldaratzeko lehenengo izenburua",
"apihelp-compare-param-fromid": "Aldaratzeko lehenengo orri IDa",
@@ -31,6 +38,7 @@
"apihelp-compare-param-prop": "Hartu beharreko informazio zatiak.",
"apihelp-compare-paramvalue-prop-diff": "HTML diff-a",
"apihelp-compare-paramvalue-prop-diffsize": "HTML diff-aren tamainia, byte-tan",
+ "apihelp-compare-paramvalue-prop-size": "\"nondik\" eta \"nora\" berrikuspenen tamaina.",
"apihelp-compare-example-1": "1. eta 2. berrikusketen arteko \"diff\"-a sortu.",
"apihelp-createaccount-summary": "Erabiltzaile kontu berria sortu.",
"apihelp-createaccount-param-name": "Erabiltzaile izena.",
@@ -52,17 +60,20 @@
"apihelp-edit-summary": "Orrialdeak sortu eta aldatu.",
"apihelp-edit-param-title": "Orri izenburua aldatzeke. Hurrengoarekin batera ezin da erabili: <var>$1pageid</var>.",
"apihelp-edit-param-pageid": "Ezabatzeko orri edo ID orria. Hurrengoarekin batera ezin da erabili: <var>$1title</var>.",
- "apihelp-edit-param-sectiontitle": "Atal berri baten titulua.",
+ "apihelp-edit-param-sectiontitle": "Atal berri baten izenburua.",
"apihelp-edit-param-text": "Orrialdearen edukia.",
"apihelp-edit-param-tags": "Aldatu etiketak berrikusketa eskatzeko.",
"apihelp-edit-param-minor": "Aldaketa txikia.",
"apihelp-edit-param-notminor": "Aldaketa ez-txikiak",
"apihelp-edit-param-bot": "Aldaketa hau errobot aldaketa bezala markatu.",
+ "apihelp-edit-param-recreate": "Bitartean ezabatu den orrialdearen inguruko akatsak gainidaztea.",
"apihelp-edit-param-createonly": "Ez aldatu orria jadanik existitzen bada.",
"apihelp-edit-param-nocreate": "Orria ez bada existitzen akatsa bota.",
"apihelp-edit-param-watch": "Orria erabiltzaile honen ikus-zerrendan sartu.",
"apihelp-edit-param-unwatch": "Erabiltzailearen oraingo ikus-zerrendatik orria kendu.",
+ "apihelp-edit-param-watchlist": "Baldintzarik gabe gehitu edo kendu orria uneko erabiltzaileen jarraipen zerrendatik, erabili hobespenak edo ikuspena ez aldatu.",
"apihelp-edit-param-redirect": "Birbideratzeak automatikoki konpondu.",
+ "apihelp-edit-param-contentformat": "Sarrera-testuan erabilitako edukien serializazio formatua.",
"apihelp-edit-param-contentmodel": "Eduki berriko eduki eredua.",
"apihelp-edit-example-edit": "Orrialde bat aldatu",
"apihelp-emailuser-summary": "Erabiltzaileari e-maila bidali",
@@ -71,9 +82,11 @@
"apihelp-emailuser-param-text": "Mezuaren gorputza.",
"apihelp-emailuser-param-ccme": "Bidal iezadazu mezu elektroniko honen kopia bat.",
"apihelp-emailuser-example-email": "<kbd>WikiSysop</kbd> erabiltzaileari mezu elektronikoa bidali <kbd>Edukia</kbd> testuarekin.",
+ "apihelp-expandtemplates-summary": "Wikitesturako txantiloi guztiak zabaldu.",
"apihelp-expandtemplates-param-title": "Orrialdearen izenburua.",
"apihelp-expandtemplates-param-text": "Bihurtzeko Wikitestua",
"apihelp-expandtemplates-param-revid": "Berrikusketa ID, <code><nowiki>{{REVISIONID}}</nowiki></code> eta antzeko aldagaientzako.",
+ "apihelp-expandtemplates-param-prop": "Lortzeko informazio zatiak.\n\nKontuan izan baliorik ez bada hautatu, emaitzak wikitestua eramango duela, baina irteera formatu zaharkitu batekin.",
"apihelp-expandtemplates-paramvalue-prop-wikitext": "Wikitestu zabaldua.",
"apihelp-expandtemplates-paramvalue-prop-ttl": "Emaitzen cache-ak baliogabetu baino lehen iraun dezaketen denbora.",
"apihelp-feedcontributions-param-feedformat": "Produktuaren formatua.",
@@ -101,7 +114,10 @@
"apihelp-feedwatchlist-param-feedformat": "Produktuaren formatua.",
"apihelp-filerevert-summary": "Artxibo bat bertsio zaharrera bueltatu.",
"apihelp-filerevert-param-comment": "Iruzkina igo.",
+ "apihelp-help-example-main": "Modulu nagusirako laguntza.",
"apihelp-help-example-recursive": "Laguntza guztia orrialde batean.",
+ "apihelp-help-example-help": "Laguntza modulurako laguntza.",
+ "apihelp-help-example-query": "Bi eskaera azpi-moduluentzako laguntza.",
"apihelp-imagerotate-summary": "Irudi bat edo gehiago biratu.",
"apihelp-imagerotate-param-rotation": "Irudia erloju-orratzen norabidean biratzeko graduak.",
"apihelp-import-param-summary": "Inportazioaren laburpena.",
diff --git a/www/wiki/includes/api/i18n/fa.json b/www/wiki/includes/api/i18n/fa.json
index c1376119..a799d00f 100644
--- a/www/wiki/includes/api/i18n/fa.json
+++ b/www/wiki/includes/api/i18n/fa.json
@@ -191,6 +191,7 @@
"apihelp-opensearch-param-suggest": "کاری نکنید اگر <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> false است.",
"apihelp-opensearch-param-format": "فرمت خروجی.",
"apihelp-opensearch-example-te": "یافتن صفحه‌هایی که با <kbd>Te</kbd> آغاز می‌شوند",
+ "apihelp-options-summary": "تغییر ترجیحات کاربر جاری",
"apihelp-options-param-reset": "ترجیحات را به مقادیر پیش فرض سایت بازمی گرداند.",
"apihelp-options-example-reset": "بازنشانی همه تنظیمات.",
"apihelp-paraminfo-param-helpformat": "ساختار راهنمای رشته‌ها",
@@ -311,6 +312,7 @@
"apihelp-query+tags-param-prop": "خصوصیتی که باید گرفته شود:",
"apihelp-query+tags-paramvalue-prop-name": "افزودن نام برچسب.",
"apihelp-query+transcludedin-paramvalue-prop-title": "عنوان هر صفحه.",
+ "apihelp-query+watchlist-paramvalue-prop-tags": "برچسب‌‌های یک مدخل را لیست می‌کند",
"apihelp-query+watchlist-paramvalue-type-log": "مدخل‌های سیاهه.",
"apihelp-stashedit-param-text": "محتوای صفحه.",
"apihelp-stashedit-param-contentmodel": "مدل محتوایی محتوای جدید",
diff --git a/www/wiki/includes/api/i18n/fr.json b/www/wiki/includes/api/i18n/fr.json
index bd9ebcf9..5ce23316 100644
--- a/www/wiki/includes/api/i18n/fr.json
+++ b/www/wiki/includes/api/i18n/fr.json
@@ -28,10 +28,13 @@
"Pols12",
"The RedBurn",
"Umherirrender",
- "Thibaut120094"
+ "Thibaut120094",
+ "KATRINE1992",
+ "Kenjiraw",
+ "Framawiki"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentation]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bogues et demandes]\n</div>\n<strong>État :</strong> Toutes les fonctionnalités affichées sur cette page devraient fonctionner, mais l’API est encore en cours de développement et peut changer à tout moment. Inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion mediawiki-api-announce] pour être informé des mises à jour.\n\n<strong>Requêtes erronées :</strong> Si des requêtes erronées sont envoyées à l’API, un entête HTTP sera renvoyé avec la clé « MediaWiki-API-Error ». La valeur de cet entête et le code d’erreur renvoyé prendront la même valeur. Pour plus d’information, voyez [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]].\n\n<strong>Test :</strong> Pour faciliter le test des requêtes de l’API, voyez [[Special:ApiSandbox]].",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentation]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bogues et demandes]\n</div>\n<strong>État :</strong> L’API MédiaWiki est une interface stable et mature qui est supportée et améliorée de façon active. Bien que nous essayions de l’éviter, nous pouvons avoir parfois besoin de faire des modifications impactantes ; inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion mediawiki-api-announce] pour être informé des mises à jour.\n\n<strong>Requêtes erronées :</strong> Si des requêtes erronées sont envoyées à l’API, un entête HTTP sera renvoyé avec la clé « MediaWiki-API-Error ». La valeur de cet entête et le code d’erreur renvoyé prendront la même valeur. Pour plus d’information, voyez [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]].\n\n<p class=\"mw-apisandbox-link\"><strong>Test :</strong> Pour faciliter le test des requêtes de l’API, voyez [[Special:ApiSandbox]].</p>",
"apihelp-main-param-action": "Quelle action effectuer.",
"apihelp-main-param-format": "Le format de sortie.",
"apihelp-main-param-maxlag": "La latence maximale peut être utilisée quand MédiaWiki est installé sur un cluster de base de données répliqué. Pour éviter des actions provoquant un supplément de latence de réplication de site, ce paramètre peut faire attendre le client jusqu’à ce que la latence de réplication soit inférieure à une valeur spécifiée. En cas de latence excessive, le code d’erreur <samp>maxlag</samp> est renvoyé avec un message tel que <samp>Attente de $host : $lag secondes de délai</samp>.<br />Voyez [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manuel: Maxlag parameter]] pour plus d’information.",
@@ -82,17 +85,19 @@
"apihelp-compare-param-fromid": "ID de la première page à comparer.",
"apihelp-compare-param-fromrev": "Première révision à comparer.",
"apihelp-compare-param-fromtext": "Utiliser ce texte au lieu du contenu de la révision spécifié par <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>.",
+ "apihelp-compare-param-fromsection": "N'utiliser que la section spécifiée du contenu 'from'.",
"apihelp-compare-param-frompst": "Faire une transformation avant enregistrement sur <var>fromtext</var>.",
- "apihelp-compare-param-fromcontentmodel": "Modèle de contenu de <var>fromtext</var>. Si non fourni, il sera deviné d’après les autres paramètres.",
+ "apihelp-compare-param-fromcontentmodel": "Modèle de contenu de <var>fromtext</var>. Si non fourni, il sera déduit d’après les autres paramètres.",
"apihelp-compare-param-fromcontentformat": "Sérialisation du contenu de <var>fromtext</var>.",
"apihelp-compare-param-totitle": "Second titre à comparer.",
"apihelp-compare-param-toid": "ID de la seconde page à comparer.",
"apihelp-compare-param-torev": "Seconde révision à comparer.",
"apihelp-compare-param-torelative": "Utiliser une révision relative à la révision déterminée de <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>. Toutes les autres options 'to' seront ignorées.",
"apihelp-compare-param-totext": "Utiliser ce texte au lieu du contenu de la révision spécifié par <var>totitle</var>, <var>toid</var> ou <var>torev</var>.",
+ "apihelp-compare-param-tosection": "N'utiliser que la section spécifiée du contenu 'to'.",
"apihelp-compare-param-topst": "Faire une transformation avant enregistrement sur <var>totext</var>.",
"apihelp-compare-param-tocontentmodel": "Modèle de contenu de <var>totext</var>. Si non fourni, il sera deviné d’après les autres paramètres.",
- "apihelp-compare-param-tocontentformat": "Sérialisation du contenu de <var>totext</var>.",
+ "apihelp-compare-param-tocontentformat": "Format de sérialisation du contenu de <var>totext</var>.",
"apihelp-compare-param-prop": "Quelles informations obtenir.",
"apihelp-compare-paramvalue-prop-diff": "Le diff HTML.",
"apihelp-compare-paramvalue-prop-diffsize": "La taille du diff HTML en octets.",
@@ -218,8 +223,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Filtrer par balise.",
"apihelp-feedrecentchanges-param-target": "Afficher uniquement les modifications sur les pages liées depuis cette page.",
"apihelp-feedrecentchanges-param-showlinkedto": "Afficher les modifications plutôt sur les pages liées vers la page sélectionnée.",
- "apihelp-feedrecentchanges-param-categories": "Afficher uniquement les modifications sur les pages dans toutes ces catégories",
- "apihelp-feedrecentchanges-param-categories_any": "Afficher plutôt uniquement les modifications sur les pages dans n’importe laquelle de ces catégories.",
"apihelp-feedrecentchanges-example-simple": "Afficher les modifications récentes",
"apihelp-feedrecentchanges-example-30days": "Afficher les modifications récentes sur 30 jours",
"apihelp-feedwatchlist-summary": "Renvoie un flux de liste de suivi.",
@@ -254,6 +257,8 @@
"apihelp-import-extended-description": "Noter que le POST HTTP doit être effectué comme un import de fichier (c’est-à-dire en utilisant multipart/form-data) lors de l’envoi d’un fichier pour le paramètre <var>xml</var>.",
"apihelp-import-param-summary": "Résumé de l’importation de l’entrée de journal.",
"apihelp-import-param-xml": "Fichier XML téléversé.",
+ "apihelp-import-param-interwikiprefix": "Pour les importations téléchargées : le préfixe interwiki à appliquer aux noms d’utilisateur inconnus (et aux utilisateurs connus si <var>$1assignknownusers</var> est positionné).",
+ "apihelp-import-param-assignknownusers": "Affecter les modifications aux utilisateurs locaux quand l’utilisateur nommé existe localement.",
"apihelp-import-param-interwikisource": "Pour les importations interwiki : wiki depuis lequel importer.",
"apihelp-import-param-interwikipage": "Pour les importations interwiki : page à importer.",
"apihelp-import-param-fullhistory": "Pour les importations interwiki : importer tout l’historique, et pas seulement la version courante.",
@@ -311,7 +316,7 @@
"apihelp-opensearch-summary": "Rechercher dans le wiki en utilisant le protocole OpenSearch.",
"apihelp-opensearch-param-search": "Chaîne de caractères cherchée.",
"apihelp-opensearch-param-limit": "Nombre maximal de résultats à renvoyer.",
- "apihelp-opensearch-param-namespace": "Espaces de nom à rechercher.",
+ "apihelp-opensearch-param-namespace": "Espaces de nom à rechercher. Ignoré if <var>$1search</var> commence avec le préfixe d'un espace de noms valide.",
"apihelp-opensearch-param-suggest": "Ne rien faire si <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> vaut faux.",
"apihelp-opensearch-param-redirects": "Comment gérer les redirections :\n;return:Renvoie la redirection elle-même.\n;resolve:Renvoie la page cible. Peut renvoyer moins de $1limit résultats.\nPour des raisons historiques, la valeur par défaut est « return » pour $1format=json et « resolve » pour les autres formats.",
"apihelp-opensearch-param-format": "Le format de sortie.",
@@ -337,9 +342,10 @@
"apihelp-paraminfo-example-1": "Afficher les informations pour <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>, <kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>, <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> et <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>.",
"apihelp-paraminfo-example-2": "Afficher les informations pour tous les sous-modules de <kbd>[[Special:ApiHelp/query|action=query]]</kbd>.",
"apihelp-parse-summary": "Analyse le contenu et renvoie le résultat de l’analyseur.",
- "apihelp-parse-extended-description": "Voyez les différents modules prop de <kbd>[[Special:ApiHelp/query|action=query]]</kbd> pour avoir de l’information sur la version actuelle d’une page.\n\nIl y a plusieurs moyens de spécifier le texte à analyser :\n# Spécifier une page ou une révision, en utilisant <var>$1page</var>, <var>$1pageid</var> ou <var>$1oldid</var>.\n# Spécifier explicitement un contenu, en utilisant <var>$1text</var>, <var>$1title</var> et <var>$1contentmodel</var>\n# Spécifier uniquement un résumé à analyser. <var>$1prop</var> doit recevoir une valeur vide.",
+ "apihelp-parse-extended-description": "Voyez les différents modules prop de <kbd>[[Special:ApiHelp/query|action=query]]</kbd> pour avoir de l’information sur la version actuelle d’une page.\n\nIl y a plusieurs moyens de spécifier le texte à analyser :\n# Spécifier une page ou une révision, en utilisant <var>$1page</var>, <var>$1pageid</var> ou <var>$1oldid</var>.\n# Spécifier explicitement un contenu, en utilisant <var>$1text</var>, <var>$1title</var> et <var>$1contentmodel</var>.\n# Spécifier uniquement un résumé à analyser. <var>$1prop</var> doit recevoir une valeur vide.",
"apihelp-parse-param-title": "Titre de la page à laquelle appartient le texte. Si omis, <var>$1contentmodel</var> doit être spécifié, et [[API]] sera utilisé comme titre.",
"apihelp-parse-param-text": "Texte à analyser. utiliser <var>$1title</var> ou <var>$1contentmodel</var> pour contrôler le modèle de contenu.",
+ "apihelp-parse-param-revid": "ID de révision, pour <code><nowiki>{{REVISIONID}}</nowiki></code> et autres variables semblables.",
"apihelp-parse-param-summary": "Résumé à analyser.",
"apihelp-parse-param-page": "Analyser le contenu de cette page. Impossible à utiliser avec <var>$1text</var> et <var>$1title</var>.",
"apihelp-parse-param-pageid": "Analyser le contenu de cette page. Écrase <var>$1page</var>.",
@@ -380,6 +386,7 @@
"apihelp-parse-param-disablepp": "Utiliser <var>$1disablelimitreport</var> à la place.",
"apihelp-parse-param-disableeditsection": "Omettre les liens de modification de section de la sortie de l’analyseur.",
"apihelp-parse-param-disabletidy": "Ne pas exécuter de nettoyage du code HTML (par exemple, réagencer) sur la sortie de l'analyseur.",
+ "apihelp-parse-param-disablestylededuplication": "Ne pas dupliquer les feuilles de style incluses, dans la sortie de l'analyseur.",
"apihelp-parse-param-generatexml": "Générer un arbre d’analyse XML (nécessite le modèle de contenu <code>$1</code> ; remplacé par <kbd>$2prop=parsetree</kbd>).",
"apihelp-parse-param-preview": "Analyser en mode aperçu.",
"apihelp-parse-param-sectionpreview": "Analyser en mode aperçu de section (active aussi le mode aperçu).",
@@ -565,12 +572,12 @@
"apihelp-query+allrevisions-param-generatetitles": "Utilisé comme générateur, génère des titres plutôt que des IDs de révision.",
"apihelp-query+allrevisions-example-user": "Lister les 50 dernières contributions de l’utilisateur <kbd>Example</kbd>.",
"apihelp-query+allrevisions-example-ns-main": "Lister les 50 premières révisions dans l’espace de noms principal.",
- "apihelp-query+mystashedfiles-summary": "Obtenir une liste des fichiers dans le cache de téléversement de l’utilisateur actuel",
+ "apihelp-query+mystashedfiles-summary": "Obtenir une liste des fichiers du cache de téléversement de l’utilisateur actuel.",
"apihelp-query+mystashedfiles-param-prop": "Quelles propriétés récupérer pour les fichiers.",
"apihelp-query+mystashedfiles-paramvalue-prop-size": "Récupérer la taille du fichier et les dimensions de l’image.",
"apihelp-query+mystashedfiles-paramvalue-prop-type": "Récupérer le type MIME du fichier et son type de média.",
"apihelp-query+mystashedfiles-param-limit": "Combien de fichiers obtenir.",
- "apihelp-query+mystashedfiles-example-simple": "Obtenir la clé du fichier, sa taille, et la taille en pixels des fichiers dans le cache de téléversement de l’utilisateur actuel.",
+ "apihelp-query+mystashedfiles-example-simple": "Obtenir la clé du fichier, sa taille, et la taille en pixels des fichiers du cache de téléversement de l’utilisateur actuel.",
"apihelp-query+alltransclusions-summary": "Lister toutes les transclusions (pages intégrées en utilisant &#123;&#123;x&#125;&#125;), y compris les inexistantes.",
"apihelp-query+alltransclusions-param-from": "Le titre de la transclusion depuis lequel commencer l’énumération.",
"apihelp-query+alltransclusions-param-to": "Le titre de la transclusion auquel arrêter l’énumération.",
@@ -749,7 +756,7 @@
"apihelp-query+exturlusage-param-namespace": "Les espaces de nom à énumérer.",
"apihelp-query+exturlusage-param-limit": "Combien de pages renvoyer.",
"apihelp-query+exturlusage-param-expandurl": "Étendre les URLs relatives au protocole avec le protocole canonique.",
- "apihelp-query+exturlusage-example-simple": "Afficher les pages avec un lien vers <kbd>http://www.mediawiki.org</kbd>.",
+ "apihelp-query+exturlusage-example-simple": "Afficher les pages avec un lien vers <kbd>https://www.mediawiki.org</kbd>.",
"apihelp-query+filearchive-summary": "Énumérer séquentiellement tous les fichiers supprimés.",
"apihelp-query+filearchive-param-from": "Le titre de l’image auquel démarrer l’énumération.",
"apihelp-query+filearchive-param-to": "Le titre de l’image auquel arrêter l’énumération.",
@@ -785,7 +792,7 @@
"apihelp-query+fileusage-param-show": "Afficher uniquement les éléments qui correspondent à ces critères :\n;redirect:Afficher uniquement les redirections.\n;!redirect:Afficher uniquement les non-redirections.",
"apihelp-query+fileusage-example-simple": "Obtenir une liste des pages utilisant [[:File:Example.jpg]]",
"apihelp-query+fileusage-example-generator": "Obtenir l’information sur les pages utilisant [[:File:Example.jpg]]",
- "apihelp-query+imageinfo-summary": "Renvoyer l’information de fichier et l’historique de téléversement.",
+ "apihelp-query+imageinfo-summary": "Renvoie l’information de fichier et l’historique de téléversement.",
"apihelp-query+imageinfo-param-prop": "Quelle information obtenir du fichier :",
"apihelp-query+imageinfo-paramvalue-prop-timestamp": "Ajoute l’horodatage à la version téléversée.",
"apihelp-query+imageinfo-paramvalue-prop-user": "Ajoute l’utilisateur qui a téléversé chaque version du fichier.",
@@ -850,6 +857,7 @@
"apihelp-query+info-paramvalue-prop-readable": "Si l’utilisateur peut lire cette page.",
"apihelp-query+info-paramvalue-prop-preload": "Fournit le texte renvoyé par EditFormPreloadText.",
"apihelp-query+info-paramvalue-prop-displaytitle": "Fournit la manière dont le titre de la page est réellement affiché.",
+ "apihelp-query+info-paramvalue-prop-varianttitles": "Donne le titre affiché dans toutes les variantes de la langue de contenu du site.",
"apihelp-query+info-param-testactions": "Tester si l’utilisateur actuel peut effectuer certaines actions sur la page.",
"apihelp-query+info-param-token": "Utiliser plutôt [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].",
"apihelp-query+info-example-simple": "Obtenir des informations sur la page <kbd>Main Page</kbd>.",
@@ -957,7 +965,7 @@
"apihelp-query+prefixsearch-summary": "Effectuer une recherche de préfixe sur les titres de page.",
"apihelp-query+prefixsearch-extended-description": "Malgré les similarités dans le nom, ce module n’est pas destiné à être l’équivalent de [[Special:PrefixIndex]] ; pour cela, voyez <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> avec le paramètre <kbd>apprefix</kbd>. Le but de ce module est similaire à <kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd> : prendre l’entrée utilisateur et fournir les meilleurs titres s’en approchant. Selon le serveur du moteur de recherche, cela peut inclure corriger des fautes de frappe, éviter des redirections, ou d’autres heuristiques.",
"apihelp-query+prefixsearch-param-search": "Chaîne de recherche.",
- "apihelp-query+prefixsearch-param-namespace": "Espaces de noms à rechercher.",
+ "apihelp-query+prefixsearch-param-namespace": "Espaces de noms à rechercher. Ignoré if <var>$1search</var> commence avec le préfixe d'un espace de noms valide.",
"apihelp-query+prefixsearch-param-limit": "Nombre maximal de résultats à renvoyer.",
"apihelp-query+prefixsearch-param-offset": "Nombre de résultats à sauter.",
"apihelp-query+prefixsearch-example-simple": "Rechercher les titres de page commençant par <kbd>meaning</kbd>.",
@@ -1009,6 +1017,7 @@
"apihelp-query+recentchanges-paramvalue-prop-sizes": "Ajoute l’ancienne et la nouvelle taille de la page en octets.",
"apihelp-query+recentchanges-paramvalue-prop-redirect": "Marque la modification si la page est une redirection.",
"apihelp-query+recentchanges-paramvalue-prop-patrolled": "Marque les modifications à relire comme relues ou pas.",
+ "apihelp-query+recentchanges-paramvalue-prop-autopatrolled": "Marque les modifications patrouillables comme patrouillée automatiquement ou non.",
"apihelp-query+recentchanges-paramvalue-prop-loginfo": "Ajoute les informations du journal (Id du journal, type de trace, etc.) aux entrées du journal.",
"apihelp-query+recentchanges-paramvalue-prop-tags": "Liste les balises de l’entrée.",
"apihelp-query+recentchanges-paramvalue-prop-sha1": "Ajoute la somme de contrôle du contenu pour les entrées associées à une révision.",
@@ -1088,6 +1097,7 @@
"apihelp-query+search-paramvalue-prop-sectiontitle": "Ajoute le titre de la section correspondante.",
"apihelp-query+search-paramvalue-prop-categorysnippet": "Ajoute un extrait analysé de la catégorie correspondante.",
"apihelp-query+search-paramvalue-prop-isfilematch": "Ajoute un booléen indiquant si la recherche correspond au contenu du fichier.",
+ "apihelp-query+search-paramvalue-prop-extensiondata": "Va ajouter des données générées supplémentaires par extension.",
"apihelp-query+search-paramvalue-prop-score": "Ignoré.",
"apihelp-query+search-paramvalue-prop-hasrelated": "Ignoré.",
"apihelp-query+search-param-limit": "Combien de pages renvoyer au total.",
@@ -1186,6 +1196,7 @@
"apihelp-query+usercontribs-paramvalue-prop-sizediff": "Ajoute le delta de taille de la modification par rapport à son parent.",
"apihelp-query+usercontribs-paramvalue-prop-flags": "Ajoute les marques de la modification.",
"apihelp-query+usercontribs-paramvalue-prop-patrolled": "Marque les modifications relues.",
+ "apihelp-query+usercontribs-paramvalue-prop-autopatrolled": "Marque les modifications patrouillées automatiquement.",
"apihelp-query+usercontribs-paramvalue-prop-tags": "Liste les balises de la modification.",
"apihelp-query+usercontribs-param-show": "Afficher uniquement les éléments correspondant à ces critères, par ex. les modifications non mineures uniquement : <kbd>$2show=!minor</kbd>.\n\nSi <kbd>$2show=patrolled</kbd> ou <kbd>$2show=!patrolled</kbd> est positionné, les révisions plus anciennes que <var>[[mw:Special:MyLanguage/Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ($1 {{PLURAL:$1|seconde|secondes}}) ne seront pas affichées.",
"apihelp-query+usercontribs-param-tag": "Lister uniquement les révisions marquées avec cette balise.",
@@ -1214,7 +1225,7 @@
"apihelp-query+userinfo-param-attachedwiki": "Avec <kbd>$1prop=centralids</kbd>, indiquer si l’utilisateur est attaché au wiki identifié par cet ID.",
"apihelp-query+userinfo-example-simple": "Obtenir des informations sur l’utilisateur actuel.",
"apihelp-query+userinfo-example-data": "Obtenir des informations supplémentaires sur l’utilisateur actuel.",
- "apihelp-query+users-summary": "Obtenir des informations sur une liste d’utilisateurs",
+ "apihelp-query+users-summary": "Obtenir des informations sur une liste d’utilisateurs.",
"apihelp-query+users-param-prop": "Quelles informations inclure :",
"apihelp-query+users-paramvalue-prop-blockinfo": "Marque si l’utilisateur est bloqué, par qui, et pour quelle raison.",
"apihelp-query+users-paramvalue-prop-groups": "Liste tous les groupes auxquels appartient chaque utilisateur.",
@@ -1250,9 +1261,11 @@
"apihelp-query+watchlist-paramvalue-prop-parsedcomment": "Ajoute le commentaire analysé de la modification.",
"apihelp-query+watchlist-paramvalue-prop-timestamp": "Ajoute l’horodatage de la modification.",
"apihelp-query+watchlist-paramvalue-prop-patrol": "Marque les modifications relues.",
+ "apihelp-query+watchlist-paramvalue-prop-autopatrol": "Marque les modifications qui sont patrouillées automatiquement.",
"apihelp-query+watchlist-paramvalue-prop-sizes": "Ajoute les tailles ancienne et nouvelle de la page.",
"apihelp-query+watchlist-paramvalue-prop-notificationtimestamp": "Ajoute l’horodatage de la dernière notification de la modification à l’utilisateur.",
"apihelp-query+watchlist-paramvalue-prop-loginfo": "Ajoute l’information de trace le cas échéant.",
+ "apihelp-query+watchlist-paramvalue-prop-tags": "Liste les balises associées à l'entrée.",
"apihelp-query+watchlist-param-show": "Afficher uniquement les éléments qui correspondent à ces critères. Par exemple, pour voir uniquement les modifications mineures faites par des utilisateurs connectés, mettre $1show=minor|!anon.",
"apihelp-query+watchlist-param-type": "Quels types de modification afficher :",
"apihelp-query+watchlist-paramvalue-type-edit": "Modifications normales de page.",
@@ -1319,9 +1332,9 @@
"apihelp-setnotificationtimestamp-param-timestamp": "Horodatage auquel dater la notification.",
"apihelp-setnotificationtimestamp-param-torevid": "Révision pour laquelle fixer l’horodatage de notification (une page uniquement).",
"apihelp-setnotificationtimestamp-param-newerthanrevid": "Révision pour fixer l’horodatage de notification plus récent (une page uniquement).",
- "apihelp-setnotificationtimestamp-example-all": "Réinitialiser l’état de notification pour toute la liste de suivi",
+ "apihelp-setnotificationtimestamp-example-all": "Réinitialiser l’état de notification pour toute la liste de suivi.",
"apihelp-setnotificationtimestamp-example-page": "Réinitialiser l’état de notification pour la <kbd>Page principale<kbd>.",
- "apihelp-setnotificationtimestamp-example-pagetimestamp": "Fixer l’horodatage de notification pour <kbd>Page principale</kbd> afin que toutes les modifications depuis le 1 janvier 2012 soient non vues",
+ "apihelp-setnotificationtimestamp-example-pagetimestamp": "Fixer l’horodatage de notification pour <kbd>Page principale</kbd> afin que toutes les modifications depuis le 1 janvier 2012 soient non vues.",
"apihelp-setnotificationtimestamp-example-allpages": "Réinitialiser l’état de notification sur les pages dans l’espace de noms <kbd>{{ns:user}}</kbd>.",
"apihelp-setpagelanguage-summary": "Modifier la langue d’une page.",
"apihelp-setpagelanguage-extended-description-disabled": "Il n’est pas possible de modifier la langue d’une page sur ce wiki.\n\nActiver <var>[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> pour utiliser cette action.",
@@ -1343,8 +1356,8 @@
"apihelp-stashedit-param-contentformat": "Format de sérialisation de contenu utilisé pour le texte saisi.",
"apihelp-stashedit-param-baserevid": "ID de révision de la révision de base.",
"apihelp-stashedit-param-summary": "Résumé du changement",
- "apihelp-tag-summary": "Ajouter ou enlever des balises de modification aux révisions ou ou aux entrées de journal individuelles.",
- "apihelp-tag-param-rcid": "Un ou plus IDs de modification récente à partir desquels ajouter ou supprimer la balise.",
+ "apihelp-tag-summary": "Ajouter ou enlever des balises de modification aux révisions ou aux entrées de journal individuelles.",
+ "apihelp-tag-param-rcid": "Un ou plusieurs IDs de modification récente à partir desquels ajouter ou supprimer la balise.",
"apihelp-tag-param-revid": "Un ou plusieurs IDs de révision à partir desquels ajouter ou supprimer la balise.",
"apihelp-tag-param-logid": "Un ou plusieurs IDs d’entrée de journal à partir desquels ajouter ou supprimer la balise.",
"apihelp-tag-param-add": "Balises à ajouter. Seules les balises définies manuellement peuvent être ajoutées.",
@@ -1503,6 +1516,8 @@
"api-help-param-direction": "Dans quelle direction énumérer :\n;newer:Lister les plus anciens en premier. Note : $1start doit être avant $1end.\n;older:Lister les nouveaux en premier (par défaut). Note : $1start doit être postérieur à $1end.",
"api-help-param-continue": "Quand plus de résultats sont disponibles, utiliser cela pour continuer.",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(aucune description)</span>",
+ "api-help-param-maxbytes": "Ne peut excéder $1 octet{{PLURAL:$1||s}}.",
+ "api-help-param-maxchars": "Ne peut excéder $1 caractères{{PLURAL:$1||s}}.",
"api-help-examples": "{{PLURAL:$1|Exemple|Exemples}} :",
"api-help-permissions": "{{PLURAL:$1|Droit|Droits}} :",
"api-help-permissions-granted-to": "{{PLURAL:$1|Accordé à}} : $2",
@@ -1565,6 +1580,8 @@
"apierror-chunk-too-small": "La taille minimale d’un segment est de $1 {{PLURAL:$1|octet|octets}} pour les segments hors le dernier.",
"apierror-cidrtoobroad": "Les plages CIDR $1 plus large que /$2 ne sont pas acceptées.",
"apierror-compare-no-title": "Impossible de faire une transformation avant enregistrement sans titre. Essayez de spécifier <var>fromtitle</var> ou <var>totitle</var>.",
+ "apierror-compare-nosuchfromsection": "Il n'y a pas de section $1 dans le contenu 'from'.",
+ "apierror-compare-nosuchtosection": "Il n'y a pas de section $1 dans le contenu 'to'.",
"apierror-compare-relative-to-nothing": "Pas de révision 'depuis' pour <var>torelative</var> à laquelle se rapporter.",
"apierror-contentserializationexception": "Échec de sérialisation du contenu : $1",
"apierror-contenttoobig": "Le contenu que vous avez fourni dépasse la limite de taille d’un article, qui est de $1 {{PLURAL:$1|kilooctet|kilooctets}}.",
@@ -1605,6 +1622,8 @@
"apierror-invalidurlparam": "Valeur non valide pour <var>$1urlparam</var> (<kbd>$2=$3</kbd>).",
"apierror-invaliduser": "Nom d'utilisateur invalide \"$1\".",
"apierror-invaliduserid": "L'ID d'utilisateur <var>$1</var> n'est pas valide.",
+ "apierror-maxbytes": "Le paramètre <var>$1</var> ne peut excéder $2 octets{{PLURAL:$2||s}}",
+ "apierror-maxchars": "Le paramètre <var>$1</var> ne peut excéder $2 catactères{{PLURAL:$2||s}}",
"apierror-maxlag-generic": "Attente d’un serveur de base de données : $1 {{PLURAL:$1|seconde|secondes}} de délai.",
"apierror-maxlag": "Attente de $2 : $1 {{PLURAL:$1|seconed|secondes}} de délai.",
"apierror-mimesearchdisabled": "La recherche MIME est désactivée en mode Misère.",
@@ -1620,7 +1639,6 @@
"apierror-missingtitle-byname": "La page $1 n’existe pas.",
"apierror-moduledisabled": "Le module <kbd>$1</kbd> a été désactivé.",
"apierror-multival-only-one-of": "{{PLURAL:$3|Seul|Seul un des}} $2 est autorisé pour le paramètre <var>$1</var>.",
- "apierror-multival-only-one": "Une seule valeur est autorisée pour le paramètre <var>$1</var>.",
"apierror-multpages": "<var>$1</var> ne peut être utilisé qu’avec une seule page.",
"apierror-mustbeloggedin-changeauth": "Vous devez être connecté pour modifier les données d’authentification.",
"apierror-mustbeloggedin-generic": "Vous devez être connecté.",
@@ -1741,6 +1759,7 @@
"apiwarn-notfile": "« $1 » n'est pas un fichier.",
"apiwarn-nothumb-noimagehandler": "Impossible de créer la vignette car $1 n’a pas de gestionnaire d’image associé.",
"apiwarn-parse-nocontentmodel": "Ni <var>title</var> ni <var>contentmodel</var> n’ont été fournis, $1 est supposé.",
+ "apiwarn-parse-revidwithouttext": "<var>revid</var> utilisé sans <var>text</var>, et les propriétés de la page analysée ont été demandées. Vouliez-vous utiliser <var>oldid</var> au lieu de <var>revid</var> ?",
"apiwarn-parse-titlewithouttext": "<var>title</var> utilisé sans <var>text</var>, et les propriétés de page analysées sont nécessaires. Voulez-vous dire que vous voulez utiliser <var>page</var> à la place de <var>title</var> ?",
"apiwarn-redirectsandrevids": "La résolution de la redirection ne peut pas être utilisée avec le paramètre <var>revids</var>. Toutes les redirections vers lesquelles pointent <var>revids</var> n’ont pas été résolues.",
"apiwarn-tokennotallowed": "L'action « $1 » n'est pas autorisée pour l'utilisateur actuel.",
diff --git a/www/wiki/includes/api/i18n/gl.json b/www/wiki/includes/api/i18n/gl.json
index 8e978b2f..1f2b028b 100644
--- a/www/wiki/includes/api/i18n/gl.json
+++ b/www/wiki/includes/api/i18n/gl.json
@@ -11,10 +11,12 @@
"Amire80",
"Macofe",
"Hamilton Abreu",
- "Umherirrender"
+ "Umherirrender",
+ "Fitoschido",
+ "Athena in Wonderland"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentación]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de discusión]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anuncios da API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Erros e solicitudes]\n</div>\n<strong>Estado:</strong> Tódalas funcionalidades mostradas nesta páxina deberían estar funcionanado, pero a API aínda está desenrolo, e pode ser modificada en calquera momento. Apúntese na [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de discusión mediawiki-api-announce] para estar informado acerca das actualizacións.\n\n<strong>Solicitudes incorrectas:</strong> Cando se envían solicitudes incorrectas á API, envíase unha cabeceira HTTP coa chave \"MediaWiki-API-Error\" e, a seguir, tanto o valor da cabeceira como o código de erro retornado serán definidos co mesmo valor. Para máis información, consulte [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Erros e avisos]].\n\n<strong>Test:</strong> Para facilitar as probas das peticións da API, consulte [[Special:ApiSandbox]].",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentación]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de discusión]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anuncios da API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Erros e solicitudes]\n</div>\n<strong>Estado:</strong> Tódalas funcionalidades mostradas nesta páxina deberían estar funcionando, pero a API aínda está desenrolo, e pode ser modificada en calquera momento. Apúntese na [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de discusión mediawiki-api-announce] para estar informado acerca das actualizacións.\n\n<strong>Solicitudes incorrectas:</strong> Cando se envían solicitudes incorrectas á API, envíase unha cabeceira HTTP coa chave \"MediaWiki-API-Error\" e, a seguir, tanto o valor da cabeceira como o código de erro retornado serán definidos co mesmo valor. Para máis información, consulte [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Erros e avisos]].\n\n<strong>Test:</strong> Para facilitar as probas das peticións da API, consulte [[Special:ApiSandbox]].",
"apihelp-main-param-action": "Que acción se realizará.",
"apihelp-main-param-format": "O formato de saída.",
"apihelp-main-param-maxlag": "O retardo máximo pode usarse cando MediaWiki está instalada nun cluster de base de datos replicadas. Para gardar accións que causen calquera retardo máis de replicación do sitio, este parámetro pode facer que o cliente espere ata que o retardo de replicación sexa menor que o valor especificado. No caso de retardo excesivo, é devolto o código de erro <samp>maxlag</samp> cunha mensaxe como <samp>esperando por $host: $lag segundos de retardo</samp>.<br />Para máis información, ver [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: Maxlag parameter]].",
@@ -187,8 +189,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Filtrar por etiqueta.",
"apihelp-feedrecentchanges-param-target": "Mostrar só os cambios nas páxinas ligadas a esta.",
"apihelp-feedrecentchanges-param-showlinkedto": "Mostrar os cambios nas páxinas ligadas coa páxina seleccionada.",
- "apihelp-feedrecentchanges-param-categories": "Só mostrar cambios en páxinas pertencentes a todas estas categorías.",
- "apihelp-feedrecentchanges-param-categories_any": "Só mostrar cambios en páxinas pertencentes a calquera das categorías.",
"apihelp-feedrecentchanges-example-simple": "Mostrar os cambios recentes",
"apihelp-feedrecentchanges-example-30days": "Mostrar os cambios recentes limitados a 30 días",
"apihelp-feedwatchlist-summary": "Devolve o fluxo dunha lista de vixiancia.",
@@ -718,7 +718,7 @@
"apihelp-query+exturlusage-param-namespace": "Espazo de nomes a enumerar.",
"apihelp-query+exturlusage-param-limit": "Cantas páxinas devolver.",
"apihelp-query+exturlusage-param-expandurl": "Expandir as URLs relativas a un protocolo co protocolo canónico.",
- "apihelp-query+exturlusage-example-simple": "Mostrar páxinas ligando a <kbd>http://www.mediawiki.org</kbd>.",
+ "apihelp-query+exturlusage-example-simple": "Amosar páxinas que ligan con <kbd>https://www.mediawiki.org</kbd>.",
"apihelp-query+filearchive-summary": "Enumerar secuencialmente todos os ficheiros borrados.",
"apihelp-query+filearchive-param-from": "Título da imaxe coa que comezar a enumeración.",
"apihelp-query+filearchive-param-to": "Título da imaxe coa que rematar a enumeración.",
@@ -1457,7 +1457,7 @@
"api-help-param-upload": "Debe ser enviado como un ficheiro importado usando multipart/form-data.",
"api-help-param-multi-separate": "Separe os valores con <kbd>|</kbd> ou [[Special:ApiHelp/main#main/datatypes|outros]].",
"api-help-param-multi-max": "O número máximo de valores é {{PLURAL:$1|$1}} ({{PLURAL:$2|$2}} para os bots).",
- "api-help-param-multi-max-simple": "O número máximo de valores é {{PLURAL:1$|1$}}.",
+ "api-help-param-multi-max-simple": "O número máximo de valores é {{PLURAL:$1|$1}}.",
"api-help-param-multi-all": "Para especificar tódolos valores use <kbd>$1</kbd>.",
"api-help-param-default": "Por defecto: $1",
"api-help-param-default-empty": "Por defecto: <span class=\"apihelp-empty\">(baleiro)</span>",
@@ -1468,6 +1468,8 @@
"api-help-param-direction": "En que dirección enumerar:\n;newer:Lista os máis antigos primeiro. Nota: $1start ten que estar antes que $1end.\n;older:Lista os máis novos primeiro (por defecto). Nota: $1start ten que estar despois que $1end.",
"api-help-param-continue": "Cando estean dispoñibles máis resultados, use isto para continuar.",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(sen descrición)</span>",
+ "api-help-param-maxbytes": "Non pode ser máis longo que $1 {{PLURAL:$1|byte|bytes}}.",
+ "api-help-param-maxchars": "Non pode ser máis longo que $1 {{PLURAL:$1|carácter|caracteres}}.",
"api-help-examples": "{{PLURAL:$1|Exemplo|Exemplos}}:",
"api-help-permissions": "{{PLURAL:$1|Permiso|Permisos}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|Concedida a|Concedidas a}}: $2",
@@ -1580,7 +1582,6 @@
"apierror-missingtitle-byname": "A páxina $1 non existe.",
"apierror-moduledisabled": "O módulo <kbd>$1</kbd> foi deshabilitado.",
"apierror-multival-only-one-of": "Só {{PLURAL:$3|se permite o valor|se permiten os valores}} $2 para o parámetro <var>$1</var>.",
- "apierror-multival-only-one": "Só se permite un valor para o parámetro <var>$1</var>.",
"apierror-multpages": "<var>$1</var> non se pode utilizar máis que con unha soa páxina.",
"apierror-mustbeloggedin-changeauth": "Debe estar conectado para poder cambiar os datos de autentificación.",
"apierror-mustbeloggedin-generic": "Debe estar conectado.",
diff --git a/www/wiki/includes/api/i18n/he.json b/www/wiki/includes/api/i18n/he.json
index f9016c86..aaa6aed0 100644
--- a/www/wiki/includes/api/i18n/he.json
+++ b/www/wiki/includes/api/i18n/he.json
@@ -205,8 +205,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "סינון לפי תגית.",
"apihelp-feedrecentchanges-param-target": "הצגת שינויים שנעשו בדפים המקושרים לדף זה בלבד.",
"apihelp-feedrecentchanges-param-showlinkedto": "להציג את השינויים בדפים שמקושרים לדף שנבחר במקום זה.",
- "apihelp-feedrecentchanges-param-categories": "להציג רק שינויים בדפים בכל הקטגוריות האלו.",
- "apihelp-feedrecentchanges-param-categories_any": "להציג רק שינויים בדפים בכל הקטגוריות במקום.",
"apihelp-feedrecentchanges-example-simple": "הצגת שינויים אחרונים.",
"apihelp-feedrecentchanges-example-30days": "הצגת שינויים אחרונים עבור 30 ימים.",
"apihelp-feedwatchlist-summary": "החזרת הזנת רשימת מעקב.",
@@ -324,9 +322,10 @@
"apihelp-paraminfo-example-1": "הצגת מידע עבור <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>‏, <kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>‏, <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd>‏, ו־<kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>.",
"apihelp-paraminfo-example-2": "הצגת מידע עבור כל התת־מודולים של <kbd>[[Special:ApiHelp/query|action=query]]</kbd>.",
"apihelp-parse-summary": "מפענח את התוכן ומחזיר פלט מפענח.",
- "apihelp-parse-extended-description": "ר' את יחידת ה־prop השיונות של <kbd>[[Special:ApiHelp/query|action=query]]</kbd> כדי לקבל מידע על הגרסה הנוכחית של הדף.\n\nיש מספר דרכים לציין טקסט לפענוח:\n# ציון דף או גרסה באמצעות <var>$1page</var>‏, <var>$1pageid</var>, או <var>$1oldid</var>.\n# ציון התוכן במפורש, באמצעות <var>$1text</var>‏, <var>$1title</var>, ו־<var>$1contentmodel</var>.\n# ציון רק של התקציר לפענוח. ל־<var>$1prop</var> צריך לתת ערך ריק.",
+ "apihelp-parse-extended-description": "ר' את יחידת ה־prop השונות של <kbd>[[Special:ApiHelp/query|action=query]]</kbd> כדי לקבל מידע על הגרסה הנוכחית של הדף.\n\nיש מספר דרכים לציין טקסט לפענוח:\n# ציון דף או גרסה באמצעות <var>$1page</var>‏, <var>$1pageid</var>, או <var>$1oldid</var>.\n# ציון התוכן במפורש, באמצעות <var>$1text</var>‏, <var>$1title</var>, ו־<var>$1contentmodel</var>.\n# ציון רק של התקציר לפענוח. ל־<var>$1prop</var> צריך לתת ערך ריק.",
"apihelp-parse-param-title": "שם הדף שהטקסט שייך אליו. אם זה מושמט, יש לציין את <var>$1contentmodel</var>, ו־[[API]] ישמש ככותרת.",
"apihelp-parse-param-text": "הטקסט לפענוח. יש להשתמש ב־<var>$1title</var> או ב־<var>$1contentmodel</var>.",
+ "apihelp-parse-param-revid": "מזהה גרסה, עבור <code><nowiki>{{REVISIONID}}</nowiki></code> ומשתנים דומים.",
"apihelp-parse-param-summary": "התקציר שצריך לפענח.",
"apihelp-parse-param-page": "פענוח תוכן הדף הזה. לא יכול לשמש יחד עם <var>$1text</var> ו־<var>$1title</var>.",
"apihelp-parse-param-pageid": "לפענח את התוכן של הדף הזה. דורס את <var>$1page</var>.",
@@ -736,7 +735,7 @@
"apihelp-query+exturlusage-param-namespace": "איזה מרחב שם למנות.",
"apihelp-query+exturlusage-param-limit": "כמה דפים להחזיר.",
"apihelp-query+exturlusage-param-expandurl": "הרחבת URL־ים בעלי פרוטוקול יחסי בפרוטוקול קנוני.",
- "apihelp-query+exturlusage-example-simple": "הצגת דפים שמקשרים ל־<kbd>http://www.mediawiki.org</kbd>.",
+ "apihelp-query+exturlusage-example-simple": "הצגת דפים שמקשרים ל־<kbd>https://www.mediawiki.org</kbd>.",
"apihelp-query+filearchive-summary": "למנות את כל הקבצים המחוקים לפי הסדר.",
"apihelp-query+filearchive-param-from": "מאיזו כותרת תמונה להתחיל למנות.",
"apihelp-query+filearchive-param-to": "באיזו כותרת תמונה להפסיק למנות.",
@@ -837,6 +836,7 @@
"apihelp-query+info-paramvalue-prop-readable": "האם המשתמש יכול להציג דף זה.",
"apihelp-query+info-paramvalue-prop-preload": "נותן את הטקסט שמוחזר על־ידי EditFormPreloadText.",
"apihelp-query+info-paramvalue-prop-displaytitle": "נותן את האופן שבה שם הדף באמת מוצג.",
+ "apihelp-query+info-paramvalue-prop-varianttitles": "כותרת התצוגה בכל הגרסאות של שפת התוכן של האתר.",
"apihelp-query+info-param-testactions": "בדיקה האם המשתמש הנוכחי יכול לבצע פעולות מסוימות על הדף.",
"apihelp-query+info-param-token": "להשתמש ב־[[Special:ApiHelp/query+tokens|action=query&meta=tokens]] במקום.",
"apihelp-query+info-example-simple": "קבלת מידע על הדף <kbd>Main Page</kbd>",
@@ -996,6 +996,7 @@
"apihelp-query+recentchanges-paramvalue-prop-sizes": "הוספת אורך הדף החדש והישן בבייטים.",
"apihelp-query+recentchanges-paramvalue-prop-redirect": "מתייג שהדף הוא הפניה.",
"apihelp-query+recentchanges-paramvalue-prop-patrolled": "מתייג עריכה בת־בדיקה בתור בדוקה או בלתי־בדוקה.",
+ "apihelp-query+recentchanges-paramvalue-prop-autopatrolled": "ציון האם עריכות הניתנות לבדיקה נבדקו אוטומטית או לא.",
"apihelp-query+recentchanges-paramvalue-prop-loginfo": "הוספת מידע יומן (זהה יומן, סוג יומן וכו') לעיולי יומן.",
"apihelp-query+recentchanges-paramvalue-prop-tags": "רשימת תגים עבור העיול.",
"apihelp-query+recentchanges-paramvalue-prop-sha1": "הוספת סיכום־ביקורת תוכן לעיולים שמשויכים לגרסה.",
@@ -1173,6 +1174,7 @@
"apihelp-query+usercontribs-paramvalue-prop-sizediff": "הוספת ההפרש של העריכה אל מול ההורה שלה.",
"apihelp-query+usercontribs-paramvalue-prop-flags": "הוספת הדגלים של העריכה.",
"apihelp-query+usercontribs-paramvalue-prop-patrolled": "מתייג עריכות בדוקות.",
+ "apihelp-query+usercontribs-paramvalue-prop-autopatrolled": "תיוג עריכות שנבדקו אוטומטית.",
"apihelp-query+usercontribs-paramvalue-prop-tags": "רשימת תגים עבור עריכות.",
"apihelp-query+usercontribs-param-show": "הצגה רק של פריטים שמתאימים לאמות המידה האלה, למשל רק עריכות לא־משניות.\n\nאם מוגדר <kbd>$2show=patrolled</kbd> או <kbd>$2show=!patrolled</kbd>, גרסאות ישנות מ־<var dir=\"ltr\">[[mw:Special:MyLanguage/Manual:$wgRCMaxAge|$wgRCMaxAge]]</var>‏ ({{PLURAL:$1|שנייה אחת|$1 שניות}}) לא תוצגנה.",
"apihelp-query+usercontribs-param-tag": "לרשום רק גרסאות עם התג הזה.",
@@ -1473,9 +1475,9 @@
"api-help-param-list-can-be-empty": "{{PLURAL:$1|0=חייב להיות ריק|יכול להיות ריק או $2}}",
"api-help-param-limit": "מספר הפרמטרים לא יכול להיות גדול מ־$1.",
"api-help-param-limit2": "המספר המרבי המותר הוא $1 (עבור בוטים – $2).",
- "api-help-param-integer-min": "ה{{PLURAL:$1|1=ערך|2=ערכים}} לא יכולים להיות קטנים מ־$2.",
- "api-help-param-integer-max": "ה{{PLURAL:$1|1=ערך לא יכול להיות גדול|2=ערכים לא יכולים להיות גדולים}} מ־$3.",
- "api-help-param-integer-minmax": "ה{{PLURAL:$1|1=ערך חייב|2=ערכים חייבים}} להיות בין $2 ל־$3.",
+ "api-help-param-integer-min": "{{PLURAL:$1|1=הערך לא יכול להיות קטן|2=הערכים לא יכולים להיות קטנים}} מ־$2.",
+ "api-help-param-integer-max": "{{PLURAL:$1|1=הערך לא יכול להיות גדול|2=הערכים לא יכולים להיות גדולים}} מ־$3.",
+ "api-help-param-integer-minmax": "{{PLURAL:$1|1=הערך חייב|2=הערכים חייבים}} להיות בין $2 ל־$3.",
"api-help-param-upload": "חייב להישלח (posted) בתור העלאת קובץ באמצעות multipart/form-data.",
"api-help-param-multi-separate": "הפרדה בין ערכים נעשית באמצעות <kbd>|</kbd> או [[Special:ApiHelp/main#main/datatypes|תו חלופי]].",
"api-help-param-multi-max": "מספר הערכים המרבי הוא {{PLURAL:$1|$1}} (עבור בוטים – {{PLURAL:$2|$2}}).",
@@ -1490,6 +1492,8 @@
"api-help-param-direction": "באיזה כיוון למנות:\n;newer:לרשום את הישנים ביותר בהתחלה. לתשומת לבך: $1start חייב להיות לפני $1end.\n;older:לרשום את החדשים ביותר בהתחלה (בררת מחדל). לתשומת לבך: $1start חייב להיות אחרי $1end.",
"api-help-param-continue": "כשיש עוד תוצאות, להשתמש בזה בשביל להמשיך.",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(ללא תיאור)</span>",
+ "api-help-param-maxbytes": "לא יכול להיות ארוך {{PLURAL:$1|מבית אחד|מ־$1 בתים}}.",
+ "api-help-param-maxchars": "לא יכול להיות ארוך {{PLURAL:$1|מתו אחד|מ־$1 תווים}}.",
"api-help-examples": "{{PLURAL:$1|דוגמה|דוגמאות}}:",
"api-help-permissions": "{{PLURAL:$1|הרשאה|הרשאות}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|הוענק ל|הוענקו ל}}: $2",
@@ -1607,7 +1611,6 @@
"apierror-missingtitle-byname": "הדף $1 אינו קיים.",
"apierror-moduledisabled": "המודול <kbd>$1</kbd> כובה.",
"apierror-multival-only-one-of": "{{PLURAL:$3|רק הערך|רק אחד מתוך הערכים}} $2 מותר עבור הפרמטר <var>$1</var>.",
- "apierror-multival-only-one": "רק ערך אחד מותר עבור הפרמטר <var>$1</var>.",
"apierror-multpages": "<var>$1</var> יכול לשמש רק בדף בודד.",
"apierror-mustbeloggedin-changeauth": "יש להיכנס לחשבון כדי לשנות נתוני אימות.",
"apierror-mustbeloggedin-generic": "חובה להיכנס.",
@@ -1728,6 +1731,7 @@
"apiwarn-notfile": "\"$1\" אינו קובץ.",
"apiwarn-nothumb-noimagehandler": "לא היה אפשר ליצור תמונה ממוזערת כי לקובץ $1 לא משויך מטפל תמונה.",
"apiwarn-parse-nocontentmodel": "לא ניתן <var>title</var> או <var>contentmodel</var>, נניח שזה $1.",
+ "apiwarn-parse-revidwithouttext": "<var>revid</var> משמש ללא <var>text</var>, והתבקשו מאפייני דף. האם התכוונת להשתמש ב־<var>oldid</var> במקום <var>revid</var>?",
"apiwarn-parse-titlewithouttext": "<var>title</var> שימש ללא <var>text</var>, והתבקשו מאפייני דף מפוענח. האם התכוונת להשתמש ב־<var>page</var> במקום <var>title</var>?",
"apiwarn-redirectsandrevids": "פתרון הפניות לא יכול לשמש יחד עם הפרמטר <var>revids</var>. הפניות ש־<var>revids</var> מצביע אליהן לא נפתרו.",
"apiwarn-tokennotallowed": "הפעולה \"$1\" אינה מותרת למשתמש הנוכחי.",
diff --git a/www/wiki/includes/api/i18n/hu.json b/www/wiki/includes/api/i18n/hu.json
index 92182581..4451f194 100644
--- a/www/wiki/includes/api/i18n/hu.json
+++ b/www/wiki/includes/api/i18n/hu.json
@@ -10,7 +10,7 @@
"Dj"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentáció]]\n* [[mw:Special:MyLanguage/API:FAQ|GYIK]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Levelezőlista]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-bejelentések]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Hibabejelentések és kérések]\n</div>\n<strong>Státusz:</strong> Minden ezen a lapon látható funkciónak működnie kell, de az API jelenleg is aktív fejlesztés alatt áll, és bármikor változhat. Iratkozz fel a [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce levelezőlistára] a frissítések követéséhez.\n\n<strong>Hibás kérések:</strong> Ha az API hibás kérést kap, egy HTTP-fejlécet küld vissza „MediaWiki-API-Error” kulccsal, és a fejléc értéke és a visszaküldött hibakód ugyanarra az értékre lesz állítva. További információért lásd: [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Hibák és figyelmeztetések]].\n\n<strong>Tesztelés:</strong> Az API-kérések könnyebb teszteléséhez használható az [[Special:ApiSandbox|API-homokozó]].",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentáció]]\n* [[mw:Special:MyLanguage/API:FAQ|GYIK]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Levelezőlista]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-bejelentések]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Hibabejelentések és kérések]\n</div>\n<strong>Státusz:</strong> Minden ezen a lapon látható funkciónak működnie kell, de az API jelenleg is aktív fejlesztés alatt áll, és bármikor változhat. Iratkozz fel a [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce levelezőlistára] a frissítések követéséhez.\n\n<strong>Hibás kérések:</strong> Ha az API hibás kérést kap, egy HTTP-fejlécet küld vissza „MediaWiki-API-Error” kulccsal, és a fejléc értéke és a visszaküldött hibakód ugyanarra az értékre lesz állítva. További információért lásd: [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Hibák és figyelmeztetések]].\n\n<p class=\"mw-apisandbox-link\"><strong>Tesztelés:</strong> Az API-kérések könnyebb teszteléséhez használható az [[Special:ApiSandbox|API-homokozó]].</p>",
"apihelp-main-param-action": "Milyen műveletet hajtson végre.",
"apihelp-main-param-format": "A kimenet formátuma.",
"apihelp-main-param-smaxage": "Az <code>s-maxage</code> gyorsítótár-vezérlő HTTP-fejléc beállítása ennyi másodpercre. A hibák soha nincsenek gyorsítótárazva.",
@@ -158,8 +158,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Szűrés címke szerint.",
"apihelp-feedrecentchanges-param-target": "Csak a megadott lapról hivatkozott lapok szerkesztéseinek megjelenítése.",
"apihelp-feedrecentchanges-param-showlinkedto": "Inkább a megadott lap''ra'' hivatkozó lapok szerkesztéseinek megjelenítése.",
- "apihelp-feedrecentchanges-param-categories": "Csak a megadott kategóriák mindegyikében szereplő lapok szerkesztéseinek megjelenítése.",
- "apihelp-feedrecentchanges-param-categories_any": "Inkább a megadott kategóriák bármelyikében szereplő lapok szerkesztéseinek megjelenítése.",
"apihelp-feedrecentchanges-example-simple": "Friss változtatások megjelenítése.",
"apihelp-feedrecentchanges-example-30days": "Az elmúlt 30 nap friss változtatásainak megjelenítése.",
"apihelp-feedwatchlist-summary": "A figyelőlista lekérése hírcsatornaként.",
@@ -263,7 +261,7 @@
"apihelp-paraminfo-example-1": "Információk megjelenítése az <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>, <kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>, <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> és <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> lekérdezésekhez.",
"apihelp-paraminfo-example-2": "Információk megjelenítése az <kbd>[[Special:ApiHelp/query|action=query]]</kbd> összes almoduljához.",
"apihelp-parse-summary": "Tartalom feldolgozása.",
- "apihelp-parse-extended-description": "Lásd az <kbd>[[Special:ApiHelp/query|action=query]]</kbd> számos prop-modulját a információk lekérésére a lap aktuális változatáról.\n\nTöbbféle módon megadható a feldolgozandó szöveg:\n# Egy lap vagy lapváltozat megadásával, a <var>$1page</var>, <var>$1pageid</var> vagy <var>$1oldid</var> paraméterrel.\n# Magának a tartalomnak a megadásával, a <var>$1text</var>, <var>$1title</var> és <var>$1contentmodel</var> paraméterrel.\n# Csak egy összefoglaló feldolgozása. A <var>$1prop</var> paraméternek üresnek kell lennie.",
+ "apihelp-parse-extended-description": "Lásd az <kbd>[[Special:ApiHelp/query|action=query]]</kbd> számos prop-modulját a információk lekérésére a lap aktuális változatáról.\n\nTöbbféle módon megadható a feldolgozandó szöveg:\n# Egy lap vagy lapváltozat megadásával, a <var>$1page</var>, <var>$1pageid</var> vagy <var>$1oldid</var> paraméterrel.\n# Magának a tartalomnak a megadásával, a <var>$1text</var>, <var>$1title</var>, <var>$1revid</var> és <var>$1contentmodel</var> paraméterrel.\n# Csak egy összefoglaló feldolgozása. A <var>$1prop</var> paraméternek üresnek kell lennie.",
"apihelp-parse-param-title": "A lapnak a címe, amihez a szöveg tartozik. Ha nincs megadva, a <var>$1contentmodel</var> paraméter kötelező, és a cím [[API]] lesz.",
"apihelp-parse-param-text": "A feldolgozandó szöveg. Használd a <var>$1title</var> vagy <var>$1contentmodel</var> paramétert a tartalommodell megadásához.",
"apihelp-parse-param-summary": "Feldolgozandó szerkesztési összefoglaló.",
@@ -638,7 +636,7 @@
"apihelp-query+exturlusage-param-protocol": "Az URL protokollja. Ha üres és az <var>$1query</var> paraméter meg van adva, a protokoll <kbd>http</kbd>. Hagyd ezt és az <var>$1query</var> paramétert is üresen az összes külső link listázásához.",
"apihelp-query+exturlusage-param-namespace": "A listázandó névtér.",
"apihelp-query+exturlusage-param-limit": "A visszaadandó lapok száma.",
- "apihelp-query+exturlusage-example-simple": "A <kbd>http://www.mediawiki.org</kbd> URL-re hivatkozó lapok megjelenítése.",
+ "apihelp-query+exturlusage-example-simple": "A <kbd>https://www.mediawiki.org</kbd> URL-re hivatkozó lapok megjelenítése.",
"apihelp-query+filearchive-summary": "Az összes törölt fájl visszaadása.",
"apihelp-query+filearchive-param-from": "A fájlok listázása ettől a címtől.",
"apihelp-query+filearchive-param-to": "A fájlok listázása eddig a címig.",
@@ -1164,5 +1162,6 @@
"api-help-param-integer-max": "Az {{PLURAL:$1|1=érték nem lehet nagyobb|2=értékek nem lehetnek nagyobbak}} mint $3.",
"api-help-param-integer-minmax": "{{PLURAL:$1|1=Az értéknek $2 és $3 között kell lennie.|2=Az értékeknek $2 és $3 között kell lenniük.}}",
"api-help-param-default": "Alapértelmezett: $1",
+ "api-help-examples": "{{PLURAL:$1|Példa|Példák}}:",
"apierror-timeout": "A kiszolgáló nem adott választ a várt időn belül."
}
diff --git a/www/wiki/includes/api/i18n/it.json b/www/wiki/includes/api/i18n/it.json
index fd88c186..38d29015 100644
--- a/www/wiki/includes/api/i18n/it.json
+++ b/www/wiki/includes/api/i18n/it.json
@@ -19,7 +19,7 @@
"Margherita.mignanelli"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentazione]] (in inglese)\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]] (in inglese)\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailing list]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annunci sull'API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bug & richieste]\n</div>\n<strong>Stato:</strong> tutte le funzioni e caratteristiche mostrate su questa pagina dovrebbero funzionare, ma le API sono ancora in fase attiva di sviluppo, e potrebbero cambiare in qualsiasi momento. Iscriviti alla [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la mailing list sugli annunci delle API MediaWiki] per essere informato sugli aggiornamenti.\n\n<strong>Istruzioni sbagliate:</strong> quando vengono impartite alle API delle istruzioni sbagliate, un'intestazione HTTP verrà inviata col messaggio \"MediaWiki-API-Error\" e, sia il valore dell'intestazione, sia il codice d'errore, verranno impostati con lo stesso valore. Per maggiori informazioni leggi [[mw:Special:MyLanguage/API:Errors_and_warnings|API:Errori ed avvertimenti]] (in inglese).\n\n<strong>Test:</strong> per testare facilmente le richieste API, vedi [[Special:ApiSandbox]].",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentazione]] (in inglese)\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]] (in inglese)\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailing list]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annunci sull'API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bug & richieste]\n</div>\n<strong>Stato:</strong> l'API MediaWiki è un'interfaccia matura e stabile che è attivamente supportata e migliorata. Anche se cerchiamo di evitarlo, potremmo dover fare delle modifiche che causano malfunzionamenti; iscriviti alla [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mailing list sugli annunci delle API MediaWiki] per essere informato sugli aggiornamenti.\n\n<strong>Istruzioni sbagliate:</strong> quando vengono impartite alle API delle istruzioni sbagliate, un'intestazione HTTP verrà inviata col messaggio \"MediaWiki-API-Error\" e, sia il valore dell'intestazione, sia il codice d'errore, verranno impostati con lo stesso valore. Per maggiori informazioni leggi [[mw:Special:MyLanguage/API:Errors_and_warnings|API:Errori ed avvertimenti]] (in inglese).\n\n<p class=\"mw-apisandbox-link\"><strong>Test:</strong> per testare facilmente le richieste API, vedi [[Special:ApiSandbox]].</p>",
"apihelp-main-param-action": "Azione da compiere.",
"apihelp-main-param-format": "Formato dell'output.",
"apihelp-main-param-assert": "Verifica che l'utente abbia effettuato l'accesso se si è impostato <kbd>user</kbd>, o che abbia i permessi di bot se si è impostato <kbd>bot</kbd>.",
@@ -142,8 +142,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Filtra per etichetta.",
"apihelp-feedrecentchanges-param-target": "Mostra solo le modifiche alle pagine collegate da questa pagina.",
"apihelp-feedrecentchanges-param-showlinkedto": "Mostra solo le modifiche alle pagine collegate a quella specificata.",
- "apihelp-feedrecentchanges-param-categories": "Mostra solo le variazioni sulle pagine di tutte queste categorie.",
- "apihelp-feedrecentchanges-param-categories_any": "Mostra invece solo le variazioni sulle pagine in una qualunque categoria.",
"apihelp-feedrecentchanges-example-simple": "Mostra le ultime modifiche.",
"apihelp-feedrecentchanges-example-30days": "Mostra le modifiche degli ultimi 30 giorni.",
"apihelp-feedwatchlist-param-feedformat": "Il formato del feed.",
@@ -676,6 +674,8 @@
"api-help-param-token": "Un token \"$1\" recuperato da [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]",
"api-help-param-continue": "Quando più risultati sono disponibili, usa questo per continuare.",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(nessuna descrizione)</span>",
+ "api-help-param-maxbytes": "Non può essere più lungo di $1 {{PLURAL:$1|byte}}.",
+ "api-help-param-maxchars": "Non può essere più lungo di $1 {{PLURAL:$1|carattere|caratteri}}.",
"api-help-examples": "{{PLURAL:$1|Esempio|Esempi}}:",
"api-help-permissions": "{{PLURAL:$1|Permesso|Permessi}}:",
"api-help-open-in-apisandbox": "<small>[apri in una sandbox]</small>",
@@ -687,6 +687,8 @@
"api-help-authmanagerhelper-additional-params": "Questo modulo accetta parametri aggiuntivi a seconda delle richieste di autenticazione disponibili. Utilizza <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> con <kbd>amirequestsfor=$1</kbd> (o una precedente risposta da questo modulo, se applicabile) per determinare le richieste disponibili e i campi usati da queste.",
"apierror-invalidoldimage": "Il parametro <var>oldimage</var> ha un formato non valido.",
"apierror-invaliduserid": "L'ID utente <var>$1</var> non è valido.",
+ "apierror-maxbytes": "Il parametro <var>$1</var> non può essere più lungo di $2 {{PLURAL:$2|byte}}",
+ "apierror-maxchars": "Il parametro <var>$1</var> non può essere più lungo di $2 {{PLURAL:$2|carattere|caratteri}}",
"apierror-nosuchuserid": "Non c'è alcun utente con ID $1.",
"apierror-timeout": "Il server non ha risposto entro il tempo previsto.",
"api-credits-header": "Crediti"
diff --git a/www/wiki/includes/api/i18n/ja.json b/www/wiki/includes/api/i18n/ja.json
index e637be39..6a14bb59 100644
--- a/www/wiki/includes/api/i18n/ja.json
+++ b/www/wiki/includes/api/i18n/ja.json
@@ -11,19 +11,24 @@
"Macofe",
"Suchichi02",
"Kkairri",
- "ネイ"
+ "ネイ",
+ "Omotecho",
+ "Yusuke1109"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|説明文書]]\n* [[mw:API:FAQ|よくある質問]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api メーリングリスト]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API 告知]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R バグの報告とリクエスト]\n</div>\n<strong>状態:</strong> このページに表示されている機能は全て動作するはずですが、この API は未だ活発に開発されており、変更される可能性があります。アップデートの通知を受け取るには、[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce メーリングリスト]に参加してください。\n\n<strong>誤ったリクエスト:</strong> 誤ったリクエストが API に送られた場合、\"MediaWiki-API-Error\" HTTP ヘッダーが送信され、そのヘッダーの値と送り返されるエラーコードは同じ値にセットされます。より詳しい情報は [[mw:API:Errors_and_warnings|API: Errors and warnings]] を参照してください。\n\n<strong>テスト:</strong> API のリクエストのテストは、[[Special:ApiSandbox]]で簡単に行えます。",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|説明文書]]\n* [[mw:Special:MyLanguage/API:FAQ|よくある質問]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api メーリングリスト]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API 告知]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R バグの報告とリクエスト]\n</div>\n<strong>状態:</strong> MediaWiki APIは、積極的にサポートされ、改善された成熟した安定したインターフェースです。避けようとはしていますが、時には壊れた変更が加えられるかもしれません。アップデートの通知を受け取るには、[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce メーリングリスト]に参加してください。\n\n<strong>誤ったリクエスト:</strong> 誤ったリクエストが API に送られた場合、\"MediaWiki-API-Error\" HTTP ヘッダーが送信され、そのヘッダーの値と送り返されるエラーコードは同じ値にセットされます。より詳しい情報は [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]] を参照してください。\n\n<p class=\"mw-apisandbox-link\"><strong>テスト:</strong> API のリクエストのテストは、[[Special:ApiSandbox]]で簡単に行えます。</p>",
"apihelp-main-param-action": "実行する操作です。",
"apihelp-main-param-format": "出力する形式です。",
"apihelp-main-param-smaxage": "<code>s-maxage</code> HTTP キャッシュ コントロール ヘッダー に、この秒数を設定します。エラーがキャッシュされることはありません。",
"apihelp-main-param-maxage": "<code>max-age</code> HTTP キャッシュ コントロール ヘッダー に、この秒数を設定します。エラーがキャッシュされることはありません。",
"apihelp-main-param-assert": "<kbd>user</kbd> を設定した場合は利用者がログイン済みかどうかを、<kbd>bot</kbd> を指定した場合はボット権限があるかどうかを、それぞれ検証します。",
+ "apihelp-main-param-assertuser": "現在のユーザーが指定されたユーザーであることを確認します。",
"apihelp-main-param-requestid": "任意の値を指定でき、その値が結果に含められます。リクエストを識別するために使用できます。",
"apihelp-main-param-servedby": "リクエストを処理したホスト名を結果に含めます。",
"apihelp-main-param-curtimestamp": "現在のタイムスタンプを結果に含めます。",
+ "apihelp-main-param-responselanginfo": "結果に<var>uselang</var>と<var>errorlang</var>に使用される言語を含めます。",
"apihelp-main-param-uselang": "メッセージの翻訳に使用する言語です。<kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> は <kbd>siprop=languages</kbd> を付けると言語コードの一覧を返します。<kbd>user</kbd> を指定することで現在の利用者の個人設定の言語を、<kbd>content</kbd> を指定することでこのウィキの本文の言語を使用することもできます。",
+ "apihelp-main-param-errorsuselocal": "指定された場合、エラーテキストは{{ns:MediaWiki}}名前空間からローカルにカスタマイズされたメッセージを使用します。",
"apihelp-block-summary": "利用者をブロックします。",
"apihelp-block-param-user": "ブロックを解除する利用者名、IPアドレスまたはIPレンジ。<var>$1userid</var>とは同時に使用できません。",
"apihelp-block-param-userid": "ブロックする利用者のID。<var>$1user</var>とは同時に使用できません。",
@@ -34,15 +39,17 @@
"apihelp-block-param-autoblock": "その利用者が最後に使用したIPアドレスと、ブロック後に編集を試みた際のIPアドレスを自動的にブロックします。",
"apihelp-block-param-noemail": "Wikiを通して電子メールを送信することを禁止します。(<code>blockemail</code> 権限が必要です)",
"apihelp-block-param-hidename": "ブロック記録から利用者名を秘匿します。(<code>hideuser</code> 権限が必要です)",
- "apihelp-block-param-allowusertalk": "自身のトークページの編集を許可する (<var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var> に依存)。",
+ "apihelp-block-param-allowusertalk": "自身のトークページの編集を許可する (<var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var> に依存)。",
"apihelp-block-param-reblock": "その利用者がすでにブロックされている場合、ブロックを上書きします。",
"apihelp-block-param-watchuser": "その利用者またはIPアドレスの利用者ページとトークページをウォッチします。",
+ "apihelp-block-param-tags": "ブロック記録の項目に適用する変更タグ。",
"apihelp-block-example-ip-simple": "IPアドレス <kbd>192.0.2.5</kbd> を <kbd>First strike<kbd> という理由で3日ブロックする",
"apihelp-block-example-user-complex": "利用者 <kbd>Vandal</kbd> を <kbd>Vandalism</kbd> という理由で無期限ブロックし、新たなアカウント作成とメールの送信を禁止する。",
"apihelp-changeauthenticationdata-example-password": "現在の利用者のパスワードを <kbd>ExamplePassword</kbd> に変更する。",
"apihelp-checktoken-summary": "<kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> のトークンの妥当性を確認します。",
"apihelp-checktoken-param-type": "調べるトークンの種類。",
"apihelp-checktoken-param-token": "調べるトークン。",
+ "apihelp-checktoken-param-maxtokenage": "トークンの最大有効期限 (秒)。",
"apihelp-checktoken-example-simple": "<kbd>csrf</kbd> トークンの妥当性を調べる。",
"apihelp-clearhasmsg-summary": "現在の利用者の <code>hasmsg</code> フラグを消去します。",
"apihelp-clearhasmsg-example-1": "現在の利用者の <code>hasmsg</code> フラグを消去する。",
@@ -224,7 +231,7 @@
"apihelp-opensearch-summary": "OpenSearch プロトコルを使用してWiki内を検索します。",
"apihelp-opensearch-param-search": "検索文字列。",
"apihelp-opensearch-param-limit": "返す結果の最大数。",
- "apihelp-opensearch-param-namespace": "検索する名前空間。",
+ "apihelp-opensearch-param-namespace": "検索する名前空間。<var>$1search</var>が有効な名前空間接頭辞で始まる場合は無視されます。",
"apihelp-opensearch-param-suggest": "<var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> が false の場合、何もしません。",
"apihelp-opensearch-param-redirects": "転送を処理する方法:\n;return: 転送ページそのものを返します。\n;resolve: 転送先のページを返します。$1limit より返される結果が少なくなるかもしれません。\n歴史的な理由により、$1format=json では \"return\" が、他の形式では \"resolve\" が既定です。",
"apihelp-opensearch-param-format": "出力する形式。",
@@ -242,7 +249,7 @@
"apihelp-paraminfo-example-1": "<kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>, <kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>, <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd>, and <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> に関する情報を表示する。",
"apihelp-parse-param-title": "文字列が属するページのページ名。これを省略する場合、必ず <var>$1contentmodel</var> を指定しなければなりません。また、その場合 [[API]] がページ名として使用されます。",
"apihelp-parse-param-text": "構文解析する文字列。コンテンツ・モデルを制御するためには<var>$1title</var> または <var>$1contentmodel</var> を使用してください。",
- "apihelp-parse-param-summary": "構文解析のための要約",
+ "apihelp-parse-param-summary": "構文解析する要約",
"apihelp-parse-param-page": "このページの内容を構文解析します。<var>$1text</var> および <var>$1title</var> とは同時に使用できません。",
"apihelp-parse-param-pageid": "このページの内容を構文解析する。<var>$1page</var> をオーバーライドします。",
"apihelp-parse-param-redirects": "もし <var>$1page</var> や <var>$1pageid</var> に転送ページが指定された場合、それを解決する。",
@@ -261,7 +268,7 @@
"apihelp-parse-paramvalue-prop-displaytitle": "構文解析されたウィキテキストのタイトルを追加します。",
"apihelp-parse-paramvalue-prop-headitems": "ページの <code>&lt;head&gt;</code> の中に入れてアイテムを提供します。",
"apihelp-parse-paramvalue-prop-headhtml": "ページの解析された <code>&lt;head&gt;</code> を与える。",
- "apihelp-parse-paramvalue-prop-jsconfigvars": "ページに固有のJavaScriptの設定変数を提供します。",
+ "apihelp-parse-paramvalue-prop-jsconfigvars": "ページに固有のJavaScriptの設定変数を提供します。適用するには、<code>mw.config.set()</code>を使用します。",
"apihelp-parse-paramvalue-prop-encodedjsconfigvars": "JSON文字列としてページに固有のJavaScriptの設定変数を提供します。",
"apihelp-parse-paramvalue-prop-indicators": "ページ上で使用されるページのステータスインジケータのHTMLを提供します。",
"apihelp-parse-paramvalue-prop-iwlinks": "構文解析されたウィキテキスト内でウィキ間リンクを提供します。",
@@ -559,7 +566,7 @@
"apihelp-query+exturlusage-param-query": "プロトコルを除いた検索文字列。[[Special:LinkSearch]] も参照してください。すべての外部リンクを一覧表示するには空欄にしてください。",
"apihelp-query+exturlusage-param-namespace": "列挙するページ名前空間。",
"apihelp-query+exturlusage-param-limit": "返すページの数。",
- "apihelp-query+exturlusage-example-simple": "<kbd>http://www.mediawiki.org</kbd> にリンクしているページを一覧表示する。",
+ "apihelp-query+exturlusage-example-simple": "<kbd>https://www.mediawiki.org</kbd> にリンクしているページを一覧表示する。",
"apihelp-query+filearchive-summary": "削除されたファイルをすべて順に列挙します。",
"apihelp-query+filearchive-param-from": "列挙の始点となる画像のページ名。",
"apihelp-query+filearchive-param-to": "列挙の終点となる画像のページ名。",
@@ -691,7 +698,7 @@
"apihelp-query+prefixsearch-summary": "ページ名の先頭一致検索を行います。",
"apihelp-query+prefixsearch-extended-description": "名前が似ていますが、このモジュールは[[Special:PrefixIndex]]と等価であることを意図しません。そのような目的では<kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> を <kbd>apprefix</kbd> パラメーターと共に使用してください。このモジュールの目的は <kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd> と似ています: 利用者から入力を受け取り、最も適合するページ名を提供するというものです。検索エンジンのバックエンドによっては、誤入力の訂正や、転送の回避、その他のヒューリスティクスが適用されることがあります。",
"apihelp-query+prefixsearch-param-search": "検索文字列。",
- "apihelp-query+prefixsearch-param-namespace": "検索する名前空間。",
+ "apihelp-query+prefixsearch-param-namespace": "検索する名前空間。<var>$1search</var>が有効な名前空間接頭辞で始まる場合は無視されます。",
"apihelp-query+prefixsearch-param-limit": "返す結果の最大数。",
"apihelp-query+prefixsearch-example-simple": "<kbd>meaning</kbd> で始まるページ名を検索する。",
"apihelp-query+protectedtitles-summary": "作成保護が掛けられているページを一覧表示します。",
@@ -879,7 +886,7 @@
"apihelp-tokens-param-type": "リクエストするトークンの種類。",
"apihelp-tokens-example-edit": "編集トークンを取得する (既定)。",
"apihelp-unblock-summary": "利用者のブロックを解除します。",
- "apihelp-unblock-param-id": "解除するブロックのID (<kbd>list=blocks</kbd>で取得できます)。<var>$1user</var> とは同時に使用できません。",
+ "apihelp-unblock-param-id": "解除するブロックのID (<kbd>list=blocks</kbd>で取得できます)。<var>$1user</var> または <var>$1userid</var> とは同時に使用できません。",
"apihelp-unblock-param-user": "ブロックを解除する利用者名、IPアドレスまたはIPレンジ。<var>$1id</var>とは同時に使用できません。",
"apihelp-unblock-param-reason": "ブロック解除の理由。",
"apihelp-unblock-param-tags": "ブロック記録の項目に適用する変更タグ。",
@@ -899,7 +906,7 @@
"apihelp-userrights-summary": "利用者の所属グループを変更します。",
"apihelp-userrights-param-user": "利用者名。",
"apihelp-userrights-param-userid": "利用者ID。",
- "apihelp-userrights-param-add": "利用者をこのグループに追加します。",
+ "apihelp-userrights-param-add": "利用者をこのグループに追加するか、既にメンバーの場合は、そのグループのメンバーシップの有効期限を更新します。",
"apihelp-userrights-param-reason": "変更の理由。",
"apihelp-userrights-example-expiry": "利用者 <kbd>SometimeSysop</kbd> を 1ヶ月間 <kbd>sysop</kbd> グループに追加する。",
"apihelp-watch-summary": "現在の利用者のウォッチリストにページを追加/除去します。",
@@ -955,6 +962,8 @@
"api-help-param-limited-in-miser-mode": "<strong>注意:</strong> [[mw:Special:MyLanguage/Manual:$wgMiserMode|miser mode]] により、これを使用すると継続する前に <var>$1limit</var> より返される結果が少なくなることがあります; 極端な場合では、ゼロ件の結果が返ることもあります。",
"api-help-param-direction": "列挙の方向:\n;newer:古いものを先に表示します。注意: $1start は $1end 以前でなければなりません。\n;older:新しいものを先に表示します (既定)。注意: $1start は $1end 以降でなければなりません。",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(説明なし)</span>",
+ "api-help-param-maxbytes": "$1 {{PLURAL:$1|byte|バイト}}以下で入力してください。",
+ "api-help-param-maxchars": "$1 {{PLURAL:$1|character|文字}}以下で入力してください。",
"api-help-examples": "{{PLURAL:$1|例}}:",
"api-help-permissions": "{{PLURAL:$1|権限}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|権限を持つグループ}}: $2",
diff --git a/www/wiki/includes/api/i18n/ko.json b/www/wiki/includes/api/i18n/ko.json
index edcd41b8..354e75cf 100644
--- a/www/wiki/includes/api/i18n/ko.json
+++ b/www/wiki/includes/api/i18n/ko.json
@@ -69,12 +69,33 @@
"apihelp-compare-param-fromtitle": "비교할 첫 이름.",
"apihelp-compare-param-fromid": "비교할 첫 문서 ID.",
"apihelp-compare-param-fromrev": "비교할 첫 판.",
+ "apihelp-compare-param-fromtext": "<var>fromtitle</var>, <var>fromid</var> 또는 <var>fromrev</var>로 지정된 판의 내용 대신 이 텍스트를 사용합니다.",
+ "apihelp-compare-param-fromsection": "지정된 'from' 내용의 지정된 문단만 사용합니다.",
+ "apihelp-compare-param-frompst": "<var>fromtext</var>에 사전 저장 변환을 수행합니다.",
+ "apihelp-compare-param-fromcontentmodel": "<var>fromtext</var>의 콘텐츠 모델입니다. 지정하지 않으면 다른 변수를 참고하여 추정합니다.",
+ "apihelp-compare-param-fromcontentformat": "<var>fromtext</var>의 콘텐츠 직렬화 포맷입니다.",
"apihelp-compare-param-totitle": "비교할 두 번째 제목.",
"apihelp-compare-param-toid": "비교할 두 번째 문서 ID.",
"apihelp-compare-param-torev": "비교할 두 번째 판.",
+ "apihelp-compare-param-torelative": "<var>fromtitle</var>, <var>fromid</var> 또는 <var>fromrev</var>에서 결정된 판과 상대적인 판을 사용합니다. 다른 'to' 옵션들은 모두 무시됩니다.",
+ "apihelp-compare-param-totext": "<var>totitle</var>, <var>toid</var> 또는 <var>torev</var>로 지정된 판의 내용 대신 이 텍스트를 사용합니다.",
+ "apihelp-compare-param-tosection": "지정된 'to' 내용의 지정된 문단만 사용합니다.",
+ "apihelp-compare-param-topst": "<var>totext</var>에 사전 저장 변환을 수행합니다.",
+ "apihelp-compare-param-tocontentmodel": "<var>totext</var>의 콘텐츠 모델입니다. 지정하지 않으면 다른 변수를 참고하여 추정합니다.",
+ "apihelp-compare-param-tocontentformat": "<var>totext</var>의 콘텐츠 직렬화 포맷입니다.",
"apihelp-compare-param-prop": "가져올 정보입니다.",
+ "apihelp-compare-paramvalue-prop-diff": "HTML의 차이입니다.",
+ "apihelp-compare-paramvalue-prop-diffsize": "HTML 차이의 크기(바이트 단위)입니다.",
+ "apihelp-compare-paramvalue-prop-rel": "해당하는 경우 'from' 이전과 'to' 이후 판의 판 ID입니다.",
+ "apihelp-compare-paramvalue-prop-ids": "'from'과 'to' 판의 문서와 판 ID입니다.",
+ "apihelp-compare-paramvalue-prop-title": "'from'과 'to' 판의 문서 제목입니다.",
+ "apihelp-compare-paramvalue-prop-user": "'from'과 'to' 판의 사용자 이름과 ID입니다.",
+ "apihelp-compare-paramvalue-prop-comment": "'from'과 'to' 판의 설명입니다.",
+ "apihelp-compare-paramvalue-prop-parsedcomment": "'from'과 to' 판의 구문 분석된 설명입니다.",
+ "apihelp-compare-paramvalue-prop-size": "'from'과 'to' 판의 크기입니다.",
"apihelp-compare-example-1": "판 1과 2의 차이를 생성합니다.",
"apihelp-createaccount-summary": "새 사용자 계정을 만듭니다.",
+ "apihelp-createaccount-param-preservestate": "<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>가 <samp>hasprimarypreservedstate</samp>에 대해 참을 반환하면 <samp>primary-required</samp>로 표시된 요청은 생략됩니다. <samp>preservedusername</samp>에 대해 비어있지 않은 값이 반환되면 해당 사용자 이름은 <var>username</var> 변수를 위해 사용됩니다.",
"apihelp-createaccount-example-create": "비밀번호 <kbd>ExamplePassword</kbd>로 된 사용자 <kbd>Example</kbd>의 생성 과정을 시작합니다.",
"apihelp-createaccount-param-name": "사용자 이름",
"apihelp-createaccount-param-password": "비밀번호입니다. (<var>$1mailpassword</var>가 설정되어 있으면 무시됩니다)",
@@ -89,18 +110,22 @@
"apihelp-createaccount-example-mail": "사용자 <kbd>testmailuser</kbd>를 만들고 자동 생성된 비밀번호를 이메일로 보냅니다.",
"apihelp-cspreport-summary": "브라우저가 콘텐츠 보안 정책의 위반을 보고하기 위해 사용합니다. 이 모듈은 SCP를 준수하는 웹 브라우저에 의해 자동으로 사용될 때를 제외하고는 사용해서는 안 됩니다.",
"apihelp-cspreport-param-reportonly": "강제적 정책이 아닌, 모니터링 정책에서 나온 보고서인 것으로 표시합니다",
- "apihelp-delete-summary": "문서 삭제",
+ "apihelp-cspreport-param-source": "이 보고서를 작동시킨 CSP 헤더를 생성한 원본입니다",
+ "apihelp-delete-summary": "문서를 삭제합니다.",
"apihelp-delete-param-title": "삭제할 문서의 제목. <var>$1pageid</var>과 함께 사용할 수 없습니다.",
"apihelp-delete-param-pageid": "삭제할 문서의 ID. <var>$1title</var>과 함께 사용할 수 없습니다.",
"apihelp-delete-param-reason": "삭제의 이유. 설정하지 않으면 자동 생성되는 이유를 사용합니다.",
+ "apihelp-delete-param-tags": "삭제 기록의 항목에 적용할 변경 태그입니다.",
"apihelp-delete-param-watch": "문서를 현재 사용자의 주시문서 목록에 추가합니다.",
"apihelp-delete-param-watchlist": "현재 사용자의 주시목록에서 문서를 무조건적으로 추가하거나 제거하거나, 환경 설정을 사용하거나 주시를 변경하지 않습니다.",
"apihelp-delete-param-unwatch": "문서를 현재 사용자의 주시문서 목록에서 제거합니다.",
+ "apihelp-delete-param-oldimage": "[[Special:ApiHelp/query+imageinfo|action=query&prop=imageinfo&iiprop=archivename]]에 지정된 바대로 삭제할 오래된 그림의 이름입니다.",
"apihelp-delete-example-simple": "<kbd>Main Page</kbd>를 삭제합니다.",
"apihelp-delete-example-reason": "<kbd>Preparing for move</kbd> 라는 이유로 <kbd>Main Page</kbd>를 삭제하기.",
"apihelp-disabled-summary": "이 모듈은 해제되었습니다.",
"apihelp-edit-summary": "문서를 만들고 편집합니다.",
"apihelp-edit-param-title": "편집할 문서의 제목. <var>$1pageid</var>과 같이 사용할 수 없습니다.",
+ "apihelp-edit-param-pageid": "편집할 문서의 문서 ID입니다. <var>$1title</var>과 함께 사용할 수 없습니다.",
"apihelp-edit-param-section": "문단 번호입니다. <kbd>0</kbd>은 최상위 문단, <kbd>new</kbd>는 새 문단입니다.",
"apihelp-edit-param-sectiontitle": "새 문단을 위한 제목.",
"apihelp-edit-param-text": "문서 내용.",
@@ -109,6 +134,9 @@
"apihelp-edit-param-minor": "사소한 편집.",
"apihelp-edit-param-notminor": "사소하지 않은 편집.",
"apihelp-edit-param-bot": "이 편집을 봇 편집으로 표시.",
+ "apihelp-edit-param-basetimestamp": "기본 판의 타임스탬프이며, 편집 충돌을 발견하기 위해 사용됩니다. [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]]를 통해 가져올 수 있습니다.",
+ "apihelp-edit-param-starttimestamp": "편집 과정을 시작할 때의 타임스탬프이며 편집 충돌을 발견하기 위해 사용됩니다. 편집 과정을 시작할 때(예: 문서 내용을 편집으로 불러올 때) <var>[[Special:ApiHelp/main|curtimestamp]]</var>를 사용하여 적절한 값을 가져올 수 있습니다.",
+ "apihelp-edit-param-recreate": "중간에 삭제되는 문서에 관한 오류를 모두 무시합니다.",
"apihelp-edit-param-createonly": "이 페이지가 이미 존재하면 편집하지 않습니다.",
"apihelp-edit-param-nocreate": "페이지가 존재하지 않으면 오류를 출력합니다.",
"apihelp-edit-param-watch": "문서를 현재 사용자의 주시문서 목록에 추가합니다.",
@@ -183,6 +211,7 @@
"apihelp-import-param-xml": "업로드한 XML 파일.",
"apihelp-linkaccount-summary": "서드파티 제공자의 계정을 현재 사용자와 연결합니다.",
"apihelp-login-summary": "로그인한 다음 인증 쿠키를 가져옵니다.",
+ "apihelp-login-extended-description": "이 동작은 [[Special:BotPasswords|특수:BotPasswords]]와 함께 사용해야만 합니다. 주 계정 로그인을 위해 사용하는 것은 권장되지 않으며 경고 없이 실패할 수 있습니다. 주 계정에 안전하게 로그인하려면 <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>을 사용하십시오.",
"apihelp-login-param-name": "사용자 이름.",
"apihelp-login-param-password": "비밀번호.",
"apihelp-login-param-domain": "도메인 (선택).",
@@ -191,6 +220,7 @@
"apihelp-login-example-login": "로그인.",
"apihelp-logout-summary": "로그아웃하고 세션 데이터를 지웁니다.",
"apihelp-logout-example-logout": "현재 사용자를 로그아웃합니다.",
+ "apihelp-managetags-summary": "변경 태그에 관한 관리 작업을 수행합니다.",
"apihelp-mergehistory-summary": "문서 역사를 합칩니다.",
"apihelp-mergehistory-param-reason": "문서 병합 이유.",
"apihelp-move-summary": "문서 이동하기.",
@@ -206,13 +236,21 @@
"apihelp-opensearch-summary": "OpenSearch 프로토콜을 이용하여 위키를 검색합니다.",
"apihelp-opensearch-param-search": "문자열 검색",
"apihelp-opensearch-param-limit": "반환할 결과의 최대 수",
- "apihelp-opensearch-param-namespace": "검색할 이름공간.",
+ "apihelp-opensearch-param-namespace": "검색할 이름공간입니다. <var>$1search</var>이 유효한 이름공간 접두사로 시작하면 무시합니다.",
"apihelp-opensearch-param-suggest": "<var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var>이 거짓인 경우 아무 것도 하지 않습니다.",
+ "apihelp-opensearch-param-redirects": "넘겨주기 관리 방법:\n;return:넘겨주기 자체를 반환합니다.\n;resolve:대상 문서를 반환합니다. $1제한 미만의 결과를 반환할 수 있습니다.\n역사적인 이유로 기본값은 $1format=json의 경우 \"return\"이며, 그 밖의 포맷의 경우 \"resolve\"입니다.",
"apihelp-opensearch-param-format": "출력 포맷.",
+ "apihelp-opensearch-param-warningsaserror": "<kbd>format=json</kbd>을 사용 시 경고가 발생할 경우, 이를 무시하지 않고 API 오류를 반환합니다.",
"apihelp-opensearch-example-te": "<kbd>Te</kbd>로 시작하는 문서를 찾기.",
"apihelp-options-summary": "현재 사용자의 환경 설정을 변경합니다.",
- "apihelp-options-param-reset": "사이트 기본으로 설정 초기화",
- "apihelp-options-example-reset": "모든 설정 초기화",
+ "apihelp-options-extended-description": "핵심 확장 기능 기능이나 설치된 확장 기능 중 하나에 등록된 옵션 또는 <code>userjs-</code>(사용자 스크립트에 의해 사용됨)로 시작하는 키를 가진 옵션만 설정할 수 있습니다.",
+ "apihelp-options-param-reset": "사이트 기본값으로 환경 설정을 초기화합니다.",
+ "apihelp-options-param-resetkinds": "<var>$1reset</var> 옵션을 설정할 때 초기화할 옵션의 유형 목록입니다.",
+ "apihelp-options-param-optionname": "<var>$1optionvalue</var>에 의해 지정된 값으로 설정할 옵션의 이름입니다.",
+ "apihelp-options-param-optionvalue": "<var>$1optionname</var>에 의해 지정된 옵션의 값입니다.",
+ "apihelp-options-example-reset": "모든 환경 설정을 초기화합니다.",
+ "apihelp-options-example-change": "<kbd>skin</kbd>과 <kbd>hideminor</kbd> 환경 설정을 변경합니다.",
+ "apihelp-options-example-complex": "모든 환경 설정을 초기화하고 <kbd>skin</kbd>과 <kbd>nickname</kbd>을 설정합니다.",
"apihelp-paraminfo-summary": "API 모듈의 정보를 가져옵니다.",
"apihelp-paraminfo-param-helpformat": "도움말 문자열 포맷.",
"apihelp-parse-summary": "내용의 구문을 분석하고 파서 출력을 반환합니다.",
@@ -235,6 +273,7 @@
"apihelp-parse-paramvalue-prop-iwlinks": "구문 분석된 위키텍스트의 인터위키 링크를 제공합니다.",
"apihelp-parse-paramvalue-prop-wikitext": "구문 분석된 위키텍스트 원문을 제공합니다.",
"apihelp-parse-paramvalue-prop-properties": "구문 분석된 위키텍스트에 정의된 다양한 속성을 제공합니다.",
+ "apihelp-parse-param-pst": "구문 분석 이전에 입력에 대한 사전 저장 변환을 수행합니다. 텍스트로 사용할 때에만 유효합니다.",
"apihelp-parse-param-disablelimitreport": "파서 출력에서 제한 보고서(\"NewPP limit report\")를 제외합니다.",
"apihelp-parse-param-disablepp": "<var>$1disablelimitreport</var>를 대신 사용합니다.",
"apihelp-parse-param-disableeditsection": "파서 출력에서 문단 편집 링크를 제외합니다.",
@@ -305,6 +344,7 @@
"apihelp-query+allredirects-param-limit": "반환할 총 항목 수입니다.",
"apihelp-query+allrevisions-summary": "모든 판 표시.",
"apihelp-query+mystashedfiles-param-limit": "가져올 파일의 갯수.",
+ "apihelp-query+alltransclusions-summary": "존재하지 않는 문서를 포함하여 끼워넣은 모든 문서(&#123;&#123;x&#125;&#125;를 사용하여 끼워넣은 문서)를 나열합니다.",
"apihelp-query+alltransclusions-param-prop": "포함할 정보:",
"apihelp-query+alltransclusions-param-namespace": "열거할 이름공간.",
"apihelp-query+alltransclusions-param-limit": "반환할 총 항목 수입니다.",
@@ -366,6 +406,7 @@
"apihelp-query+exturlusage-paramvalue-prop-url": "문서에 사용된 URL을 추가합니다.",
"apihelp-query+exturlusage-param-namespace": "열거할 문서 이름공간.",
"apihelp-query+exturlusage-param-limit": "반환할 문서 수.",
+ "apihelp-query+exturlusage-example-simple": "<kbd>https://www.mediawiki.org</kbd>를 가리키는 문서를 표시합니다.",
"apihelp-query+filearchive-summary": "삭제된 모든 파일을 순서대로 열거합니다.",
"apihelp-query+filearchive-paramvalue-prop-sha1": "그림에 대한 SHA-1 해시를 추가합니다.",
"apihelp-query+filearchive-paramvalue-prop-user": "그림 판을 올린 사용자를 추가합니다.",
@@ -385,7 +426,9 @@
"apihelp-query+fileusage-param-limit": "반환할 항목 수.",
"apihelp-query+fileusage-param-show": "이 기준을 충족하는 항목만 표시합니다:\n;redirect:넘겨주기만 표시합니다.\n;!redirect:넘겨주기가 아닌 항목만 표시합니다.",
"apihelp-query+imageinfo-summary": "파일 정보와 업로드 역사를 반환합니다.",
+ "apihelp-query+imageinfo-param-prop": "가져올 파일 정보입니다:",
"apihelp-query+imageinfo-paramvalue-prop-timestamp": "업로드된 판에 대한 타임스탬프를 추가합니다.",
+ "apihelp-query+imageinfo-paramvalue-prop-parsedcomment": "판의 설명을 구문 분석합니다.",
"apihelp-query+imageinfo-paramvalue-prop-sha1": "파일에 대한 SHA-1 해시를 추가합니다.",
"apihelp-query+imageinfo-paramvalue-prop-mediatype": "파일의 미디어 유형을 추가합니다.",
"apihelp-query+imageinfo-param-urlheight": "$1urlwidth와 유사합니다.",
@@ -402,6 +445,7 @@
"apihelp-query+info-param-prop": "얻고자 하는 추가 속성:",
"apihelp-query+info-paramvalue-prop-protection": "각 문서의 보호 수준을 나열합니다.",
"apihelp-query+info-paramvalue-prop-readable": "사용자가 이 문서를 읽을 수 있는지의 여부.",
+ "apihelp-query+info-paramvalue-prop-varianttitles": "모든 종류의 사이트 내용 언어의 표시 제목을 지정합니다.",
"apihelp-query+iwbacklinks-summary": "제시된 인터위키 링크에 연결된 모든 문서를 찾습니다.",
"apihelp-query+iwbacklinks-param-prefix": "인터위키의 접두사.",
"apihelp-query+iwbacklinks-param-title": "검색할 인터위키 링크. <var>$1blprefix</var>와 함께 사용해야 합니다.",
@@ -439,7 +483,7 @@
"apihelp-query+pageswithprop-param-dir": "정렬 순서",
"apihelp-query+prefixsearch-summary": "문서 제목에 대해 두문자 검색을 수행합니다.",
"apihelp-query+prefixsearch-param-search": "문자열 검색",
- "apihelp-query+prefixsearch-param-namespace": "검색할 이름공간.",
+ "apihelp-query+prefixsearch-param-namespace": "검색할 이름공간입니다. <var>$1search</var>이 유효한 이름공간 접두사로 시작하면 무시합니다.",
"apihelp-query+prefixsearch-param-limit": "반환할 결과의 최대 수",
"apihelp-query+prefixsearch-param-profile": "검색 프로파일 사용",
"apihelp-query+protectedtitles-summary": "작성이 보호된 모든 제목을 나열합니다.",
@@ -588,7 +632,16 @@
"apihelp-stashedit-param-sectiontitle": "새 문단을 위한 제목.",
"apihelp-stashedit-param-text": "문서 내용.",
"apihelp-stashedit-param-contentmodel": "새 콘텐츠의 콘텐츠 모델.",
+ "apihelp-tag-summary": "개별 판이나 기록 항목에서 변경 태그를 추가하거나 제거합니다.",
+ "apihelp-tag-param-rcid": "태그를 변경하거나 추가할 하나 이상의 최근 바뀜 ID입니다.",
+ "apihelp-tag-param-revid": "태그를 추가하거나 제거할 하나 이상의 판 ID입니다.",
+ "apihelp-tag-param-logid": "태그를 추가하거나 제거할 하나 이상의 기록 항목 ID입니다.",
+ "apihelp-tag-param-add": "추가할 태그입니다. 수동으로 지정한 태그만 추가할 수 있습니다.",
+ "apihelp-tag-param-remove": "제거할 태그입니다. 수동으로 지정하거나 완전히 정의되지 않은 태그만 제거할 수 있습니다.",
"apihelp-tag-param-reason": "변경 이유.",
+ "apihelp-tag-param-tags": "이 동작의 결과로 생성되는 기록 항목에 적용할 태그입니다.",
+ "apihelp-tag-example-rev": "이유를 지정하지 않고 <kbd>vandalism</kbd> 태그를 판 ID 123에 추가합니다",
+ "apihelp-tag-example-log": "이유를 <kbd>Wrongly applied</kbd>로 지정하고 기록 항목 ID 123에서 <kbd>spam</kbd> 태그를 제거합니다",
"apihelp-tokens-summary": "데이터 수정 작업을 위해 토큰을 가져옵니다.",
"apihelp-tokens-extended-description": "이 모듈은 [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]의 선호에 따라 사용이 권장되지 않습니다.",
"apihelp-tokens-param-type": "요청할 토큰의 종류.",
@@ -606,7 +659,7 @@
"apihelp-undelete-extended-description": "삭제된 판의 목록(타임스탬프 포함)은 [[Special:ApiHelp/query+deletedrevisions|prop=deletedrevisions]]을 통해 검색할 수 있으며 삭제된 파일 ID의 목록은 [[Special:ApiHelp/query+filearchive|list=filearchive]]을 통해 검색할 수 있습니다.",
"apihelp-undelete-param-title": "복구할 문서의 제목입니다.",
"apihelp-undelete-param-reason": "복구할 이유입니다.",
- "apihelp-undelete-param-tags": "삭제 기록의 항목에 적용할 태그를 변경합니다.",
+ "apihelp-undelete-param-tags": "삭제 기록의 항목에 적용할 변경 태그입니다.",
"apihelp-undelete-param-timestamps": "복구할 판의 타임스탬프입니다. <var>$1timestamps</var>와 <var>$1fileids</var>가 둘 다 비어있으면 모든 판이 복구됩니다.",
"apihelp-undelete-param-fileids": "복구할 파일 판의 ID입니다. <var>$1timestamps</var>와 <var>$1fileids</var>가 둘 다 비어있으면 모든 판이 복구됩니다.",
"apihelp-undelete-param-watchlist": "현재 사용자의 주시목록에서 문서를 무조건적으로 추가하거나 제거하거나, 환경 설정을 사용하거나 주시를 변경하지 않습니다.",
@@ -615,6 +668,7 @@
"apihelp-unlinkaccount-summary": "현재 사용자에 연결된 타사 계정을 제거합니다.",
"apihelp-unlinkaccount-example-simple": "<kbd>FooAuthenticationRequest</kbd>와 연결된 제공자에 대한 현재 사용자의 토론 링크 제거를 시도합니다.",
"apihelp-upload-summary": "파일을 업로드하거나 대기 중인 업로드의 상태를 가져옵니다.",
+ "apihelp-upload-extended-description": "몇 가지 방식을 사용할 수 있습니다:\n* <var>$1file</var> 변수를 사용하여 파일의 내용을 직접 업로드합니다.\n* <var>$1filesize</var>, <var>$1chunk</var>, <var>$1offset</var> 변수를 사용하여 파일을 부분적으로 업로드합니다.\n* <var>$1url</var> 변수를 사용하여 미디어위키 서버가 URL로부터 파일을 가져오게 합니다.\n* <var>$1filekey</var> 변수를 사용하여 경고로 실패한 과거의 업로드를 완료합니다.\n<var>$1file</var>을(를) 보낼 때 HTTP POST는 파일 업로드로 끝나야 합니다. (예: <code>multipart/form-data</code>를 사용하여)",
"apihelp-upload-param-filename": "대상 파일 이름.",
"apihelp-upload-param-comment": "업로드 주석입니다. 또, <var>$1text</var>가 지정되지 않은 경우 새로운 파일들의 초기 페이지 텍스트로 사용됩니다.",
"apihelp-upload-param-tags": "업로드 기록 항목과 파일 문서 판에 적용할 태그를 변경합니다.",
@@ -659,6 +713,7 @@
"apihelp-watch-example-watch": "<kbd>대문</kbd> 문서를 주시합니다.",
"apihelp-watch-example-unwatch": "<kbd>대문</kbd> 문서의 주시를 해제합니다.",
"apihelp-watch-example-generator": "일반 이름공간의 일부 첫 문서들을 주시합니다.",
+ "apihelp-format-example-generic": "쿼리 결과를 $1 포맷으로 반환합니다.",
"apihelp-json-summary": "데이터를 JSON 형식으로 출력합니다.",
"apihelp-json-param-formatversion": "출력 형식:\n;1:하위 호환 포맷 (XML 스타일 불린, 콘텐츠 노드를 위한 <samp>*</samp> 키 등).\n;2:실험적인 모던 포맷. 상세 내용은 바뀔 수 있습니다!\n;latest:최신 포맷(현재 <kbd>2</kbd>)을 이용하지만 경고 없이 바뀔 수 있습니다.",
"apihelp-jsonfm-summary": "데이터를 JSON 포맷으로 출력합니다. (HTML의 가독성 증가)",
@@ -685,7 +740,7 @@
"api-help-lead": "이 페이지는 자동으로 생성된 미디어위키 API 도움말 문서입니다.\n\n설명 문서 및 예시: https://www.mediawiki.org/wiki/API",
"api-help-main-header": "메인 모듈",
"api-help-undocumented-module": "$1 모듈에 대한 설명문이 없습니다.",
- "api-help-flag-deprecated": "이 모듈은 사용되지 않습니다.",
+ "api-help-flag-deprecated": "이 모듈은 구식입니다.",
"api-help-flag-internal": "<strong>이 모듈은 내부용이거나 불안정합니다.</strong> 동작은 예고 없이 변경될 수 있습니다.",
"api-help-flag-readrights": "이 모듈은 read 권한을 요구합니다.",
"api-help-flag-writerights": "이 모듈은 write 권한을 요구합니다.",
@@ -697,13 +752,13 @@
"api-help-license-noname": "라이선스: [[$1|링크 참조]]",
"api-help-license-unknown": "라이선스: <span class=\"apihelp-unknown\">알 수 없음</span>",
"api-help-parameters": "{{PLURAL:$1|변수}}:",
- "api-help-param-deprecated": "사용되지 않습니다.",
+ "api-help-param-deprecated": "구식입니다.",
"api-help-param-required": "이 변수는 필수 입력 사항입니다.",
"api-help-datatypes-header": "데이터 유형",
"api-help-datatypes": "API 요청 내 몇몇 매개변수형에 대해 더 자세히 설명해보겠습니다:\n;boolean\n:Boolean 매개변수들은 HTML 체크박스처럼 동작합니다: 만약 매개변수가 지정되었다면, 값에 상관없이 참의 값으로 여겨집니다. 거짓값은 매개변수 전체를 생략하세요.\n;timestamp\n:타임스탬프들은 여러 형식으로 표현될 수 있으나 ISO 8601 날짜와 시간이 추천됩니다. 모든 시간은 UTC이어야 하며, 포함된 시간대는 모두 무시됩니다.\n:* ISO 8601 날짜와 시간, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (구두점과 <kbd>Z</kbd>는 선택입니다.)\n:* ISO 8601 날짜와 시간과 (무시되는) 소수 초, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (대시, 콜론과 <kbd>Z</kbd>는 선택입니다.)\n:* 미디어위키 형식, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* 일반적인 수 형식 <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (<kbd>GMT</kbd>, <kbd>+<var>##</var></kbd>, 또는 <kbd>-<var>##</var></kbd>와 같은 선택적 시간대는 무시됩니다)\n:*RFC 2822 형식 (시간대는 생략될 수 있음), <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 850 형식 (시간대는 생략될 수 있음), <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* C ctime 형식, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* 1부터 13자리까지의 숫자로 표현된 1970-01-01T00:00:00Z부터 흐른 시간(초) (<kbd>0</kbd>을 제외)\n:* 문자열 <kbd>now</kbd>",
"api-help-param-type-limit": "유형: 정수 또는 <kbd>max</kbd>",
"api-help-param-type-integer": "유형: {{PLURAL:$1|1=정수|2=정수 목록}}",
- "api-help-param-type-boolean": "유형: 부울 ([[Special:ApiHelp/main#main/datatypes|자세한 정보]])",
+ "api-help-param-type-boolean": "유형: 불리언 ([[Special:ApiHelp/main#main/datatypes|자세한 정보]])",
"api-help-param-type-timestamp": "유형: {{PLURAL:$1|1=타임스탬프|2=타임스탬프 목록}} ([[Special:ApiHelp/main#main/datatypes|허용되는 포맷]])",
"api-help-param-type-user": "유형: {{PLURAL:$1|1=사용자 이름|2=사용자 이름 목록}}",
"api-help-param-list": "{{PLURAL:$1|1=다음 값 중 하나|2=값 (<kbd>{{!}}</kbd>로 구분)}}: $2 또는 [[Special:ApiHelp/main#main/datatypes|alternative]]: $2",
@@ -717,12 +772,15 @@
"api-help-param-multi-separate": "<kbd>|</kbd> 또는 [[Special:ApiHelp/main#main/datatypes|대안]]으로 값을 구분합니다.",
"api-help-param-multi-max": "값들의 최대 수는 {{PLURAL:$1|$1}}입니다. (봇의 경우 {{PLURAL:$2|$2}})",
"api-help-param-multi-max-simple": "값의 최대 수는 {{PLURAL:$1|$1}}입니다.",
+ "api-help-param-multi-all": "모든 값을 지정하려면, <kbd>$1</kbd>를 사용하십시오.",
"api-help-param-default": "기본값: $1",
"api-help-param-default-empty": "기본값: <span class=\"apihelp-empty\">(비어 있음)</span>",
"api-help-param-token": "\"$1\" 토큰은 [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]에서 가져옵니다",
"api-help-param-token-webui": "호환성을 위해, 웹 UI에 사용된 토큰도 허용합니다.",
"api-help-param-continue": "더 많은 결과를 이용할 수 있을 때, 계속하려면 이것을 사용하십시오.",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(설명 없음)</span>",
+ "api-help-param-maxbytes": "$1{{PLURAL:$1|바이트}}를 초과할 수 없습니다.",
+ "api-help-param-maxchars": "$1{{PLURAL:$1|자}}를 초과할 수 없습니다.",
"api-help-examples": "{{PLURAL:$1|예시}}:",
"api-help-permissions": "{{PLURAL:$1|권한}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|다음 그룹에 부여됨}}: $2",
@@ -734,6 +792,7 @@
"api-help-authmanagerhelper-messageformat": "반환 메시지에 사용할 형식.",
"api-help-authmanagerhelper-mergerequestfields": "모든 인증 요청에 대한 필드 정보를 하나의 배열로 합칩니다.",
"api-help-authmanagerhelper-preservestate": "가능하면 과거에 실패한 로그인 시도의 상태를 보존합니다.",
+ "api-help-authmanagerhelper-returnurl": "서드파티 인증 플로의 URL을 반환하며, 절대 주소여야 합니다. 이것 또는 <var>$1continue</var>는 필수입니다.\n\n<samp>REDIRECT</samp> 응답을 받으면 일반적으로 서드파티 인증 플로를 위해 지정한 <samp>redirecttarget</samp> URL에 대해 브라우저나 웹 뷰를 열게 됩니다. 이 작업이 끝나면 서드파티는 브라우저나 웹 뷰를 이 URL로 보냅니다. URL로부터 쿼리나 POST 변수를 추출한 다음 이것들을 <var>$1continue</var> 요청으로서 이 API 모듈로 전달하는 것이 좋습니다.",
"api-help-authmanagerhelper-continue": "이 요청은 초기 <samp>UI</samp> 또는 <samp>REDIRECT</samp> 응답 이후에 계속됩니다. 이것 또는 <var>$1returnurl</var> 중 하나가 필요합니다.",
"api-help-authmanagerhelper-additional-params": "이 모듈은 사용 가능한 인증 요청에 따라 추가 변수를 허용합니다. 사용 가능한 요청 및 사용되는 필드를 결정하려면 <kbd>amirequestsfor=$1</kbd>(또는 해당되는 경우 이 모듈의 과거 응답)과 함께 <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>을(를) 사용하십시오.",
"apierror-articleexists": "작성하려는 문서가 이미 만들어져 있습니다.",
@@ -775,6 +834,8 @@
"apierror-invalidtitle": "잘못된 제목 \"$1\".",
"apierror-invaliduser": "잘못된 사용자 이름 \"$1\".",
"apierror-invaliduserid": "<var>$1</var> 사용자 ID는 유효하지 않습니다.",
+ "apierror-maxbytes": "<var>$1</var> 변수는 $2{{PLURAL:$2|바이트}}를 초과할 수 없습니다",
+ "apierror-maxchars": "<var>$1</var> 변수는 $2{{PLURAL:$2|자}}를 초과할 수 없습니다",
"apierror-maxlag-generic": "데이터베이스 서버 대기 중: $1 {{PLURAL:$1|초}} 지연되었습니다.",
"apierror-maxlag": "$2 대기 중: $1 {{PLURAL:$1|초}} 지연되었습니다.",
"apierror-missingcontent-revid": "ID $1 판에 해당하는 내용이 없습니다.",
diff --git a/www/wiki/includes/api/i18n/ksh.json b/www/wiki/includes/api/i18n/ksh.json
index 1ed917a7..b0231df9 100644
--- a/www/wiki/includes/api/i18n/ksh.json
+++ b/www/wiki/includes/api/i18n/ksh.json
@@ -152,8 +152,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Noh Makkehronge beschängke.",
"apihelp-feedrecentchanges-param-target": "Zeijsch Änderonge aan Sigge, op di vun heh dä Sigg ene Lengk jeihd.",
"apihelp-feedrecentchanges-param-showlinkedto": "Zeijsch Änderonge aan Sigge, op di vun dä ußjesöhk Sigg ene Lengk jeihd.",
- "apihelp-feedrecentchanges-param-categories": "Donn blohß de Änderonge aan de Zohjehüreshkeit för all heh di Saachjroppe zeije.",
- "apihelp-feedrecentchanges-param-categories_any": "Donn deföhr blohß de Änderonge aan de Zohjehüreshkeit för öhndseijn fun heh dä Saachjroppe zeije.",
"apihelp-feedrecentchanges-example-simple": "Zeijsch de {{LCFIRST:{{int:recentchanges}}}}",
"apihelp-feedrecentchanges-example-30days": "Zeijsch de {{LCFIRST:{{int:recentchanges}}}} vun de läzde 30 Dähsch.",
"apihelp-feedwatchlist-summary": "Donn ene Kannahl met dä Oppaßleß zerökjävve.",
diff --git a/www/wiki/includes/api/i18n/lb.json b/www/wiki/includes/api/i18n/lb.json
index 0d5f2248..f68bd699 100644
--- a/www/wiki/includes/api/i18n/lb.json
+++ b/www/wiki/includes/api/i18n/lb.json
@@ -2,7 +2,8 @@
"@metadata": {
"authors": [
"Robby",
- "Macofe"
+ "Macofe",
+ "Les Meloures"
]
},
"apihelp-main-param-assertuser": "Iwwerpréifen ob den aktuelle Benotzer de Benotzer mat deem Numm ass.",
@@ -52,8 +53,6 @@
"apihelp-feedrecentchanges-param-hideliu": "Ännerunge vu registréierte Benotzer verstoppen.",
"apihelp-feedrecentchanges-param-hidemyself": "Ännerunge vum aktuelle Benotzer verstoppen.",
"apihelp-feedrecentchanges-param-hidecategorization": "Ännerunge vun der Memberschaft a Kategorie verstoppen.",
- "apihelp-feedrecentchanges-param-categories": "Nëmmen Ännerunge vu Säiten aus all dëse Kategorië weisen.",
- "apihelp-feedrecentchanges-param-categories_any": "Nëmmen Ännerunge vu Säiten aus enger vun dëse Kategorië weisen.",
"apihelp-feedrecentchanges-example-simple": "Rezent Ännerunge weisen",
"apihelp-filerevert-param-comment": "Bemierkung eroplueden.",
"apihelp-help-example-main": "Hëllef fir den Haaptmodul.",
@@ -86,8 +85,8 @@
"apihelp-query+alldeletedrevisions-paraminfo-useronly": "Kann nëmme mam <var>$3user</var> benotzt ginn.",
"apihelp-query+alldeletedrevisions-param-user": "Nëmme Versioune vun dësem Benotzer opzielen.",
"apihelp-query+alldeletedrevisions-param-excludeuser": "Versioune vun dësem Benotzer net opzielen.",
- "apihelp-query+allfileusages-paramvalue-prop-title": "Setzt den Titel vum Fichier derbäi.",
- "apihelp-query+alllinks-paramvalue-prop-title": "Setzt den Titel vum Link derbäi.",
+ "apihelp-query+allfileusages-paramvalue-prop-title": "Setzt den Titel vum Fichier dobäi.",
+ "apihelp-query+alllinks-paramvalue-prop-title": "Setzt den Titel vum Link dobäi.",
"apihelp-query+allrevisions-summary": "Lëscht vun alle Versiounen.",
"apihelp-query+allrevisions-param-user": "Nëmme Versioune vun dësem Benotzer opzielen.",
"apihelp-query+allrevisions-param-excludeuser": "Versioune vun dësem Benotzer net opzielen.",
@@ -97,10 +96,10 @@
"apihelp-query+allusers-param-activeusers": "Nëmme Benotzer opzielen déi an de leschten $1 {{PLURAL:$1|Dag|Deeg}} aktiv waren.",
"apihelp-query+backlinks-example-simple": "Linken op d'<kbd>Main page</kbd> weisen.",
"apihelp-query+blocks-summary": "Lëscht vun de gespaarte Benotzer an IP-Adressen.",
- "apihelp-query+blocks-paramvalue-prop-range": "Setzt de Beräich vun den IP-Adressen derbäi déi vun der Spär betraff sinn.",
+ "apihelp-query+blocks-paramvalue-prop-range": "Setzt de Beräich vun den IP-Adressen dobäi déi vun der Spär betraff sinn.",
"apihelp-query+blocks-example-simple": "Lëscht vun de Spären",
"apihelp-query+categories-summary": "All Kategorien opzielen zu deenen dës Säit gehéiert.",
- "apihelp-query+categories-paramvalue-prop-timestamp": "Setzt den Zäitstempel vun dem Ament derbäi wou d'Kategorie derbäigesat gouf.",
+ "apihelp-query+categories-paramvalue-prop-timestamp": "Setzt den Zäitstempel vun dem Ament dobäi wou d'Kategorie dobäigesat gouf.",
"apihelp-query+categories-example-generator": "Informatioun iwwer all Kategorien, déi an der Säit <kbd>Albert Einstein</kbd> benotzt ginn, kréien.",
"apihelp-query+categorymembers-summary": "All Säiten aus enger bestëmmter Kategorie opzielen.",
"apihelp-query+categorymembers-example-simple": "Déi éischt 10 Säiten aus der <kbd>Category:Physics</kbd> kréien.",
@@ -108,6 +107,7 @@
"apihelp-query+deletedrevs-summary": "Geläscht Versiounen oplëschten.",
"apihelp-query+deletedrevs-param-unique": "Nëmmen eng Versioun fir all Säit weisen.",
"apihelp-query+embeddedin-param-filterredir": "Wéi Viruleedungen gefiltert gi sollen.",
+ "apihelp-query+exturlusage-example-simple": "Säiten. déi op <kbd>https://www.mediawiki.org</kbd> linken, weisen.",
"apihelp-query+filearchive-paramvalue-prop-dimensions": "Alias fir Gréisst.",
"apihelp-query+filearchive-example-simple": "Eng Lëscht vun alle geläschte Fichiere weisen",
"apihelp-query+fileusage-paramvalue-prop-title": "Titel vun all Säit.",
@@ -118,7 +118,7 @@
"apihelp-query+images-example-simple": "Eng Lëscht vun de Fichiere kréien déi op der [[Main Page|Haaptsäit]] benotzt ginn",
"apihelp-query+imageusage-example-simple": "Säite weisen déi [[:File:Albert Einstein Head.jpg]] benotzen",
"apihelp-query+info-paramvalue-prop-readable": "Ob de Benotzer dës Säit liese kann.",
- "apihelp-query+iwlinks-paramvalue-prop-url": "Setzt déi komplett URL derbäi.",
+ "apihelp-query+iwlinks-paramvalue-prop-url": "Setzt déi komplett URL dobäi.",
"apihelp-query+langlinks-param-lang": "Nëmme Sproochlinke mat dësem Sproochcode zréckginn.",
"apihelp-query+links-param-namespace": "Nëmme Linken an dësen Nummräim weisen.",
"apihelp-query+linkshere-summary": "All Säite fannen déi op déi Säit linken déi ugi gouf.",
@@ -129,7 +129,7 @@
"apihelp-query+random-param-redirect": "Benotzt dofir <kbd>$1filterredir=Viruleedungen</kbd>.",
"apihelp-query+recentchanges-summary": "Rezent Ännerungen opzielen.",
"apihelp-query+recentchanges-param-user": "Nëmmen Ännerunge vun dësem Benotzer opzielen.",
- "apihelp-query+recentchanges-paramvalue-prop-comment": "Setzt d'Bemierkung vun der Ännerung derbäi.",
+ "apihelp-query+recentchanges-paramvalue-prop-comment": "Setzt d'Bemierkung vun der Ännerung dobäi.",
"apihelp-query+recentchanges-example-simple": "Rezent Ännerunge weisen",
"apihelp-query+redirects-paramvalue-prop-title": "Titel vun all Viruleedung.",
"apihelp-query+revisions-example-last5": "Déi lescht 5 Versioune vun der <kbd>Haaptsäit</kbd> kréien.",
@@ -141,27 +141,27 @@
"apihelp-query+revisions+base-paramvalue-prop-comment": "Bemierkung vum Benotzer fir dës Versioun.",
"apihelp-query+revisions+base-paramvalue-prop-content": "Text vun der Versioun.",
"apihelp-query+search-param-namespace": "Nëmmen an dësen Nummräim sichen.",
- "apihelp-query+search-paramvalue-prop-wordcount": "Setzt d'Zuel vun de Wierder vun der Säit derbäi.",
- "apihelp-query+search-paramvalue-prop-timestamp": "Setzt den Zäitstempel vun der leschter Ännerung vun der Säit derbäi.",
+ "apihelp-query+search-paramvalue-prop-wordcount": "Setzt d'Zuel vun de Wierder vun der Säit dobäi.",
+ "apihelp-query+search-paramvalue-prop-timestamp": "Setzt den Zäitstempel vun der leschter Ännerung vun der Säit dobäi.",
"apihelp-query+search-paramvalue-prop-score": "Ignoréiert.",
"apihelp-query+search-paramvalue-prop-hasrelated": "Ignoréiert.",
"apihelp-query+templates-param-namespace": "Schablounen nëmmen an dësen Nummräim weisen.",
"apihelp-query+transcludedin-paramvalue-prop-title": "Titel vun all Säit.",
"apihelp-query+usercontribs-summary": "All Ännerunge vun engem Benotzer kréien.",
- "apihelp-query+usercontribs-paramvalue-prop-timestamp": "Setzt den Zäitstempel vun derÄnnerung derbäi.",
- "apihelp-query+usercontribs-paramvalue-prop-comment": "Setzt d'Bemierkung vun der Ännerung derbäi.",
+ "apihelp-query+usercontribs-paramvalue-prop-timestamp": "Setzt den Zäitstempel vun der Ännerung dobäi.",
+ "apihelp-query+usercontribs-paramvalue-prop-comment": "Setzt d'Bemierkung vun der Ännerung dobäi.",
"apihelp-query+userinfo-param-prop": "Informatioune fir dranzesetzen:",
"apihelp-query+userinfo-paramvalue-prop-options": "Lëscht vun allen Astellungen déi den aktuelle Benotzer gemaach huet.",
- "apihelp-query+userinfo-paramvalue-prop-editcount": "Setzt d'Gesamtzuel vun den Ännerunge vum aktuelle Benotzer derbäi.",
- "apihelp-query+userinfo-paramvalue-prop-realname": "Setzt dem Benotzer säi richtegen Numm derbäi.",
- "apihelp-query+userinfo-paramvalue-prop-registrationdate": "Setzt de Registréierungsdatum vum Benotzer derbäi.",
+ "apihelp-query+userinfo-paramvalue-prop-editcount": "Setzt d'Gesamtzuel vun den Ännerunge vum aktuelle Benotzer dobäi.",
+ "apihelp-query+userinfo-paramvalue-prop-realname": "Setzt dem Benotzer säi richtegen Numm dobäi.",
+ "apihelp-query+userinfo-paramvalue-prop-registrationdate": "Setzt de Registréierungsdatum vum Benotzer dobäi.",
"apihelp-query+users-paramvalue-prop-rights": "Weist all Rechter déi all Benotzer huet.",
"apihelp-query+watchlist-param-user": "Nëmmen Ännerunge vun dësem Benotzer opzielen.",
"apihelp-query+watchlist-param-excludeuser": "Ännerunge vun dësem Benotzer net opzielen.",
- "apihelp-query+watchlist-paramvalue-prop-title": "Setzt den Titel vun der Säit derbäi.",
- "apihelp-query+watchlist-paramvalue-prop-user": "Setzt de Benotzer derbäi deen d'Ännerung gemaach huet.",
- "apihelp-query+watchlist-paramvalue-prop-comment": "Setzt d'Bemierkung vun der Ännerung derbäi.",
- "apihelp-query+watchlist-paramvalue-prop-timestamp": "Setzt den Zäitstempel vun der Ännerung derbäi.",
+ "apihelp-query+watchlist-paramvalue-prop-title": "Setzt den Titel vun der Säit dobäi.",
+ "apihelp-query+watchlist-paramvalue-prop-user": "Setzt de Benotzer dobäi deen d'Ännerung gemaach huet.",
+ "apihelp-query+watchlist-paramvalue-prop-comment": "Setzt d'Bemierkung vun der Ännerung dobäi.",
+ "apihelp-query+watchlist-paramvalue-prop-timestamp": "Setzt den Zäitstempel vun der Ännerung dobäi.",
"apihelp-query+watchlist-paramvalue-type-external": "Extern Ännerungen.",
"apihelp-query+watchlist-paramvalue-type-new": "Ugeluecht Säiten.",
"apihelp-query+watchlistraw-param-show": "Nëmmen Elementer opzielen déi dëse Critèren entspriechen.",
@@ -203,6 +203,7 @@
"api-help-datatypes-header": "Datentypen",
"api-help-param-type-user": "Typ: {{PLURAL:$1|1=Benotzernumm|2=Lëscht vu Benotzernimm}}",
"api-help-param-multi-max-simple": "Maximal Zuel vun de Wäerter ass {{PLURAL:$1|$1}}.",
+ "api-help-param-maxbytes": "Däerf net méi laang si wéi {{PLURAL:$1|ee Byte|$1 Byten}}.",
"api-help-examples": "{{PLURAL:$1|Beispill|Beispiler}}:",
"api-help-permissions": "{{PLURAL:$1|Autorisatioun|Autorisatiounen}}:",
"api-help-open-in-apisandbox": "<small>[an der Sandkëscht opmaachen]</small>",
@@ -232,7 +233,7 @@
"apierror-pagelang-disabled": "D'Ännere vun der Sprooch vun enger Säit ass op dëser Wiki net erlaabt.",
"apierror-permissiondenied-generic": "Autorisatioun refuséiert.",
"apierror-permissiondenied-unblock": "Dir hutt net d'Recht fir d'Spär vu Benotzer opzehiewen.",
- "apierror-readonly": "D'Wiki kann elo just geliest ginn.",
+ "apierror-readonly": "D'Wiki ka fir den Ament nëmme gelies ginn.",
"apierror-revisions-badid": "Fir de Parameter <var>$1</var> gouf keng Versioun fonnt.",
"apierror-revwrongpage": "r$1 ass keng Versioun vu(n) $2.",
"apierror-stashwrongowner": "Falsche Besëtzer: $1",
diff --git a/www/wiki/includes/api/i18n/lt.json b/www/wiki/includes/api/i18n/lt.json
index 2726944c..f470a722 100644
--- a/www/wiki/includes/api/i18n/lt.json
+++ b/www/wiki/includes/api/i18n/lt.json
@@ -2,7 +2,8 @@
"@metadata": {
"authors": [
"Zygimantus",
- "Eitvys200"
+ "Eitvys200",
+ "Hugo.arg"
]
},
"apihelp-main-param-action": "Kurį veiksmą atlikti.",
@@ -72,15 +73,13 @@
"apihelp-feedrecentchanges-param-tagfilter": "Filtruoti pagal žymę.",
"apihelp-feedrecentchanges-param-target": "Rodyti tik keitimus puslapiuose, pasiekiamuose iš šio puslapio.",
"apihelp-feedrecentchanges-param-showlinkedto": "Vietoj to, rodyti pakeitimus puslapyje, susietame su pasirinktu puslapiu.",
- "apihelp-feedrecentchanges-param-categories": "Rodyti pakeitimus tik puslapiuose, esančiuose visuose šiuose kategorijose.",
- "apihelp-feedrecentchanges-param-categories_any": "Vietoj to, rodyti tik pakeitimus puslapiuse, esančiuose bet kurioje iš kategorijų.",
"apihelp-feedrecentchanges-example-simple": "Parodyti naujausius keitimus.",
"apihelp-feedrecentchanges-example-30days": "Rodyti naujausius pakeitimus per 30 dienų.",
"apihelp-feedwatchlist-summary": "Gražina stebimųjų sąrašo srautą.",
"apihelp-feedwatchlist-param-feedformat": "Srauto formatas.",
"apihelp-feedwatchlist-example-default": "Rodyti stebimųjų sąrašo srautą.",
"apihelp-feedwatchlist-example-all6hrs": "Rodyti visus pakeitimus stebimuose puslapiuose per paskutines 6 valandas.",
- "apihelp-filerevert-param-comment": "Įkėlimo komentaras.",
+ "apihelp-filerevert-param-comment": "Įkėlimo pastabos.",
"apihelp-help-summary": "Rodyti pagalbą pasirinktiems moduliams.",
"apihelp-help-example-main": "Pagalba pagrindiniam moduliui.",
"apihelp-help-example-recursive": "Visa pagalba viename puslapyje.",
@@ -200,7 +199,7 @@
"apihelp-query+exturlusage-paramvalue-prop-ids": "Prideda puslapio ID.",
"apihelp-query+exturlusage-paramvalue-prop-url": "Prideda URL, panaudota puslapyje.",
"apihelp-query+exturlusage-param-limit": "Kiek puslapių gražinti.",
- "apihelp-query+exturlusage-example-simple": "Rodyti puslapius, nurodančius į <kbd>http://www.mediawiki.org</kbd>.",
+ "apihelp-query+exturlusage-example-simple": "Rodyti puslapius, nurodančius į <kbd>https://www.mediawiki.org</kbd>.",
"apihelp-query+filearchive-param-prop": "Kokią paveikslėlio informaciją gauti:",
"apihelp-query+filearchive-paramvalue-prop-timestamp": "Prideda laiko žymę įkeltai versijai.",
"apihelp-query+filearchive-paramvalue-prop-user": "Prideda vartotoją, kuris įkėlė paveikslėlio versiją.",
@@ -265,6 +264,7 @@
"apihelp-query+logevents-param-prop": "Kurias savybes gauti:",
"apihelp-query+logevents-paramvalue-prop-ids": "Prideda žurnalo įvykio ID.",
"apihelp-query+logevents-paramvalue-prop-type": "Prideda žurnalo įvykio tipą.",
+ "apihelp-query+search-paramvalue-prop-extensiondata": "Prideda papildomus duomenis, sugeneruotus plėtinių.",
"apihelp-query+transcludedin-paramvalue-prop-pageid": "Kiekvieno puslapio ID.",
"apihelp-query+transcludedin-paramvalue-prop-title": "Kiekvieno puslapio pavadinimas.",
"apihelp-query+transcludedin-param-limit": "Kiek gražinti.",
diff --git a/www/wiki/includes/api/i18n/mk.json b/www/wiki/includes/api/i18n/mk.json
index 5af76673..3cec5d49 100644
--- a/www/wiki/includes/api/i18n/mk.json
+++ b/www/wiki/includes/api/i18n/mk.json
@@ -26,9 +26,9 @@
"apihelp-block-param-autoblock": "Автоматски блокирај ја последно употребената IP-адреса и сите понатамошни IP-адреси од кои лицето ќе се обиде да се најави.",
"apihelp-block-param-noemail": "Оневозможи му на корисникот да испаќа е-пошта преку викито. (Го бара правото code>blockemail</code>).",
"apihelp-block-param-hidename": "Скриј го корисничкото име од дневникот на блокирања. (Го бара правото <code>hideuser</code>)",
- "apihelp-block-param-allowusertalk": "Овозможи му на корисникот да си ја уредува сопствената страница за разговор (зависи од <var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).",
+ "apihelp-block-param-allowusertalk": "Овозможи му на корисникот да ја уредува неговата разговорна страница (зависи од <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).",
"apihelp-block-param-reblock": "Ако корисникот е веќе блокиран, наметни врз постоечкиот блок.",
- "apihelp-block-param-watchuser": "Набљудувај ја корисничката страница и страницата за разговор на овој корисник или IP-адреса",
+ "apihelp-block-param-watchuser": "Набљудувај ја корисничката страница и разговорна страница на овој корисник или IP-адреса",
"apihelp-block-example-ip-simple": "Блокирај ја IP-адресата <kbd>192.0.2.5</kbd> три дена со причината <kbd>Прва опомена</kbd>.",
"apihelp-block-example-user-complex": "Блокирај го корисникот <kbd>Vandal</kbd> (Вандал) бесконечно со причината <kbd>Vandal</kbd> (Вандализам) и оневозможи создавање на нови сметки и праќање е-пошта.",
"apihelp-checktoken-summary": "Проверка на полноважноста на шифрата од <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.",
@@ -116,21 +116,21 @@
"apihelp-expandtemplates-param-includecomments": "Дали во изводот да се вклучени HTML-коментари.",
"apihelp-expandtemplates-param-generatexml": "Создај XML-дрво на расчленување (заменето со $1prop=parsetree).",
"apihelp-expandtemplates-example-simple": "Прошири го викитекстот <kbd><nowiki>{{Project:Sandbox}}</nowiki></kbd>.",
- "apihelp-feedcontributions-summary": "Дава канал со придонеси на корисник.",
- "apihelp-feedcontributions-param-feedformat": "Формат на каналот.",
+ "apihelp-feedcontributions-summary": "Дава тековник со придонеси на корисник.",
+ "apihelp-feedcontributions-param-feedformat": "Формат на тековникот.",
"apihelp-feedcontributions-param-user": "За кои корисници да се прикажуваат придонесите.",
"apihelp-feedcontributions-param-namespace": "По кој именски простор да се филтрираат придонесите:",
"apihelp-feedcontributions-param-year": "Од година (и порано):",
"apihelp-feedcontributions-param-month": "Од месец (и порано):",
"apihelp-feedcontributions-param-tagfilter": "Филтрирај придонеси што имаат ознаки.",
"apihelp-feedcontributions-param-deletedonly": "Прикажувај само избришани придонеси.",
- "apihelp-feedcontributions-param-toponly": "Прикажувај само последни преработки.",
- "apihelp-feedcontributions-param-newonly": "Прикажувај само новосоздадени страници",
+ "apihelp-feedcontributions-param-toponly": "Само последни преработки.",
+ "apihelp-feedcontributions-param-newonly": "Само новосоздадени страници",
"apihelp-feedcontributions-param-hideminor": "Сокриј ситни уредувања.",
"apihelp-feedcontributions-param-showsizediff": "Покажувај ја големинската разлика меѓу преработките.",
"apihelp-feedcontributions-example-simple": "Покажувај придонеси на <kbd>Пример</kbd>.",
- "apihelp-feedrecentchanges-summary": "Дава канал со скорешни промени.",
- "apihelp-feedrecentchanges-param-feedformat": "Форматот на каналот.",
+ "apihelp-feedrecentchanges-summary": "Дава тековник со скорешни промени.",
+ "apihelp-feedrecentchanges-param-feedformat": "Форматот на тековникот.",
"apihelp-feedrecentchanges-param-namespace": "На кој именски простор да се ограничи исходот.",
"apihelp-feedrecentchanges-param-invert": "Сите именски простори освен избраниот.",
"apihelp-feedrecentchanges-param-associated": "Вклучи придружни именски простори (разговор или главен).",
@@ -147,15 +147,13 @@
"apihelp-feedrecentchanges-param-tagfilter": "Филтрирање по ознака.",
"apihelp-feedrecentchanges-param-target": "Прикажи само промени на страници што водат од оваа.",
"apihelp-feedrecentchanges-param-showlinkedto": "Наместо тоа, прикажи ги промените на страниците поврзани со избраната страница.",
- "apihelp-feedrecentchanges-param-categories": "Прикажи само промени на страниците во сите овие категории.",
- "apihelp-feedrecentchanges-param-categories_any": "Прикажи само промени на страниците во било која од категориите.",
"apihelp-feedrecentchanges-example-simple": "Прикажи скорешни промени",
"apihelp-feedrecentchanges-example-30days": "Прикажувај скорешни промени 30 дена",
- "apihelp-feedwatchlist-summary": "Дава канал од набљудуваните.",
- "apihelp-feedwatchlist-param-feedformat": "Форматот на каналот.",
+ "apihelp-feedwatchlist-summary": "Дава тековник со набљудуваните.",
+ "apihelp-feedwatchlist-param-feedformat": "Форматот на тековникот.",
"apihelp-feedwatchlist-param-hours": "Испиши страници изменети во рок од олку часови отсега.",
"apihelp-feedwatchlist-param-linktosections": "Давај ме право на изменетите делови, ако е можно.",
- "apihelp-feedwatchlist-example-default": "Прикажи го каналот од набљудуваните.",
+ "apihelp-feedwatchlist-example-default": "Прикажи го тековникот на набљудуваните.",
"apihelp-feedwatchlist-example-all6hrs": "Прикажи ги сите промени во набљудуваните во последните 6 часа",
"apihelp-filerevert-summary": "Врати податотека на претходна верзија.",
"apihelp-filerevert-param-filename": "Име на целната податотека, без претставката „Податотека:“.",
@@ -205,7 +203,7 @@
"apihelp-move-param-fromid": "Назнака на страницата што треба да се премести. Не може да се користи заедно со <var>$1from</var>.",
"apihelp-move-param-to": "Како да гласи новата страница.",
"apihelp-move-param-reason": "Причина за преименувањето.",
- "apihelp-move-param-movetalk": "Преименувај ја и страницата за разговор, ако ја има.",
+ "apihelp-move-param-movetalk": "Преименувај ја и разговорната страница, ако ја има.",
"apihelp-move-param-movesubpages": "Преименувај потстраници, ако има.",
"apihelp-move-param-noredirect": "Не прави пренасочување.",
"apihelp-move-param-watch": "Додај ги страницата и пренасочувањето во набљудуваните на тековниот корисник.",
diff --git a/www/wiki/includes/api/i18n/nb.json b/www/wiki/includes/api/i18n/nb.json
index 8b86af58..a1c92389 100644
--- a/www/wiki/includes/api/i18n/nb.json
+++ b/www/wiki/includes/api/i18n/nb.json
@@ -66,6 +66,7 @@
"apihelp-compare-param-totitle": "Andre tittel å sammenligne.",
"apihelp-compare-param-toid": "Andre side-ID å sammenligne.",
"apihelp-compare-param-torev": "Andre revisjon å sammenligne.",
+ "apihelp-compare-param-torelative": "Bruk en revisjon som er relativ til revisjonen som hentes fra <var>fromtitle</var>, <var>fromid</var> eller <var>fromrev</var>. Alle de andre «to»-alternativene vil ignoreres.",
"apihelp-compare-param-totext": "Bruk denne teksten i stedet for innholdet i revisjonen spesifisert av <var>totitle</var>, <var>toid</var> eller <var>torev</var>.",
"apihelp-compare-param-topst": "Gjør en transformering av <var>totext</var> før lagring.",
"apihelp-compare-param-tocontentmodel": "Innholdsmodellen til <var>totext</var>. Om denne ikke angis vil den bli gjettet ut fra andre parametere.",
@@ -78,6 +79,7 @@
"apihelp-compare-paramvalue-prop-title": "Sidetitlene for «from»- og «to»-revisjonene.",
"apihelp-compare-paramvalue-prop-user": "Brukernavnet og ID-en til «from»- og «to»-revisjonene.",
"apihelp-compare-paramvalue-prop-comment": "Kommentaren til «from»- og «to»-revisjonene.",
+ "apihelp-compare-paramvalue-prop-parsedcomment": "Den parsede kommentaren til «from»- og «to»-revisjonene.",
"apihelp-compare-paramvalue-prop-size": "Størrelsen til «from»- og «to»-revisjonene.",
"apihelp-compare-example-1": "Lag en diff mellom revisjon 1 og 2.",
"apihelp-createaccount-summary": "Opprett en ny brukerkonto.",
@@ -126,20 +128,39 @@
"apihelp-edit-param-watch": "Legg til siden til aktuell brukers overvåkningsliste.",
"apihelp-edit-param-unwatch": "Fjern siden fra aktuell brukers overvåkningsliste.",
"apihelp-edit-param-prependtext": "Legg til denne teksten til starten av siden. Overstyrer $1text.",
+ "apihelp-edit-param-undo": "Fjern (gjør om) denne revisjonen. Overstyrer $1text, $1prependtext og $1appendtext.",
+ "apihelp-edit-param-undoafter": "Fjern alle revisjoner fra $1undo til denne. Om den ikke er satt, fjern kun én revisjon.",
"apihelp-edit-param-redirect": "Bestem omdirigeringer automatisk.",
"apihelp-edit-param-contentformat": "Innholdsserialiseringsformat brukt for inndatateksten.",
"apihelp-edit-param-contentmodel": "Det nye innholdets innholdsmodell.",
+ "apihelp-edit-param-token": "Nøkkelen bør alltid sendes som siste parameter, eller i det minste etter parameteren $1text.",
"apihelp-edit-example-edit": "Rediger en side.",
+ "apihelp-edit-example-prepend": "Legg til <kbd>_&#95;NOTOC_&#95;</kbd> i begynnelsen av en side.",
+ "apihelp-edit-example-undo": "Fjerner revisjon 13579–13585 med automatisk redigeringsforklaring.",
"apihelp-emailuser-summary": "Send e-post til en bruker.",
"apihelp-emailuser-param-target": "Bruker som det skal sendes e-post til.",
"apihelp-emailuser-param-subject": "Emne.",
"apihelp-emailuser-param-text": "E-post innhold.",
"apihelp-emailuser-param-ccme": "Send en kopi av denne e-posten til meg.",
+ "apihelp-emailuser-example-email": "Send en epost til brukeren <kbd>WikiSysop</kbd> med teksten <kbd>Content</kbd>.",
"apihelp-expandtemplates-summary": "Ekspanderer alle maler i wikitekst.",
"apihelp-expandtemplates-param-title": "Sidetittel.",
"apihelp-expandtemplates-param-text": "Wikitekst som skal konverteres.",
+ "apihelp-expandtemplates-param-revid": "Revisjons-ID, for <code><nowiki>{{REVISIONID}}</nowiki></code> og lignende variabler.",
+ "apihelp-expandtemplates-param-prop": "Hvilken informasjon som skal hentes.\n\nMerk at om ingen verdier velges vil resultatet inneholde wikiteksten, men utdataene vil komme i et utdatert format.",
"apihelp-expandtemplates-paramvalue-prop-wikitext": "Den utvidede wikiteksten.",
"apihelp-expandtemplates-paramvalue-prop-categories": "Kategorier som er tilstede i innputt som ikke representeres i utputt.",
+ "apihelp-expandtemplates-paramvalue-prop-properties": "Sideegenskaper definert av utvidede magiske ord i wikiteksten.",
+ "apihelp-expandtemplates-paramvalue-prop-volatile": "Hvorvidt utdataene er ustabile og ikke burde gjenbrukes andre steder på siden.",
+ "apihelp-expandtemplates-paramvalue-prop-ttl": "Maksimal tid som skal ha gått før mellomlagrede resultater skal ugyldiggjøres.",
+ "apihelp-expandtemplates-paramvalue-prop-jsconfigvars": "Gir JavaScript-konfigurasjonsvariabler som er spesifikke for siden.",
+ "apihelp-expandtemplates-paramvalue-prop-encodedjsconfigvars": "Gir JavaScript-konfigurasjonsvariabler som er spesifikke for siden som en JSON-streng.",
+ "apihelp-expandtemplates-param-includecomments": "Hvorvidt HTML-kommentarer skal inkluderes i utdataene.",
+ "apihelp-expandtemplates-example-simple": "Utvid wikiteksten <kbd><nowiki>{{Project:Sandbox}}</nowiki></kbd>.",
+ "apihelp-feedcontributions-summary": "Returnerer en mating med brukerbidrag.",
+ "apihelp-feedcontributions-param-feedformat": "Matingens format.",
+ "apihelp-feedcontributions-param-user": "Hvilke brukere det skal hentes bidrag av.",
+ "apihelp-feedcontributions-param-namespace": "Hvilke navnerom bidragene skal filtreres med.",
"apihelp-feedcontributions-param-year": "Fra år (og tidligere).",
"apihelp-feedcontributions-param-month": "Fra måned (og tidligere).",
"apihelp-feedcontributions-param-tagfilter": "Filtrer bidrag som har disse merkene.",
@@ -149,6 +170,7 @@
"apihelp-feedcontributions-param-hideminor": "Skjul mindre endringer.",
"apihelp-feedcontributions-param-showsizediff": "Vis størrelsesforskjellen mellom revisjoner.",
"apihelp-feedcontributions-example-simple": "Returner bidrag for brukeren <kbd>Example</kbd>.",
+ "apihelp-feedrecentchanges-summary": "Returnerer en mating med siste endringer.",
"apihelp-feedrecentchanges-param-feedformat": "Matingens format.",
"apihelp-feedrecentchanges-param-namespace": "Navnerom resultater skal begrenses til.",
"apihelp-feedrecentchanges-param-invert": "Alle navnerom utenom det valgte.",
@@ -166,12 +188,13 @@
"apihelp-feedrecentchanges-param-tagfilter": "Filtrer etter tagger.",
"apihelp-feedrecentchanges-param-target": "Vis bare endringer på sider som lenkes fra denne siden.",
"apihelp-feedrecentchanges-param-showlinkedto": "Vis endringer på sider som lenker til den valgte siden i stedet.",
- "apihelp-feedrecentchanges-param-categories": "Vis bare endringer på sider i alle disse kategoriene.",
- "apihelp-feedrecentchanges-param-categories_any": "Vis bare endringer på sider som er i noen av kategoriene i stedet.",
"apihelp-feedrecentchanges-example-simple": "Vis siste endringer.",
"apihelp-feedrecentchanges-example-30days": "Vis siste endringer for 30 døgn.",
"apihelp-feedwatchlist-summary": "Returnerer en overvåkningslistemating.",
"apihelp-feedwatchlist-param-feedformat": "Matingens format.",
+ "apihelp-feedwatchlist-param-linktosections": "Lenk direkte til endrede seksjoner om mulig.",
+ "apihelp-feedwatchlist-example-default": "Vis matingen til overvåkningslisten.",
+ "apihelp-feedwatchlist-example-all6hrs": "Vis alle endringer på overvåkede sider de siste 6 timene.",
"apihelp-filerevert-summary": "Tilbakestill en fil til en gammel versjon.",
"apihelp-filerevert-param-filename": "Målfilnavn, uten prefikset File:.",
"apihelp-filerevert-param-comment": "Opplastingskommentar.",
@@ -197,6 +220,7 @@
"apihelp-import-extended-description": "Merk at HTTP POST må gjøres som filopplasting (altså med bruk av multipart/form-data) når man sender en fil for parameteren <var>xml</var>.",
"apihelp-import-param-summary": "Sammendrag for importering av loggelement.",
"apihelp-import-param-xml": "Opplastet XML-fil.",
+ "apihelp-import-param-assignknownusers": "Tildel redigeringer til lokale brukere der den navngitte brukeren finnes lokalt.",
"apihelp-import-param-interwikisource": "For interwikiimport: wiki det skal importeres fra.",
"apihelp-import-param-interwikipage": "For interwikiimport: side som skal importeres.",
"apihelp-import-param-fullhistory": "For interwikiimport: importer hele historikken, ikke bare den nåværende versjonen.",
@@ -205,6 +229,9 @@
"apihelp-import-param-rootpage": "Importer som underside av denne siden. Kan ikke brukes sammen med <var>$1namespace</var>.",
"apihelp-import-param-tags": "Endringstagger som skal klistres på oppføringen i importloggen og nullrevisjonen til de importerte sidene.",
"apihelp-import-example-import": "Importer [[meta:Help:ParserFunctions]] til navnerom 100 med full historikk.",
+ "apihelp-linkaccount-summary": "Lenk en konto fra en tredjepartsleverandør til den gjeldende brukeren.",
+ "apihelp-linkaccount-example-link": "Start prosessen med å lenke til en konto fra <kbd>Example</kbd>.",
+ "apihelp-login-summary": "Logg inn og få autentiseringsinformasjonskapsler.",
"apihelp-login-param-name": "Brukernavn.",
"apihelp-login-param-password": "Passord.",
"apihelp-login-param-domain": "Domene (valgfritt).",
@@ -245,19 +272,441 @@
"apihelp-options-example-complex": "Tilbakestill alle innstillinger, og sett så <kbd>skin</kbd> og <kbd>nickname</kbd>.",
"apihelp-paraminfo-summary": "Hent informasjon om API-moduler.",
"apihelp-paraminfo-param-helpformat": "Format for hjelpestrenger.",
+ "apihelp-parse-param-prop": "Hvilke informasjonsdeler som skal hentes:",
+ "apihelp-parse-paramvalue-prop-categorieshtml": "Gir HTML-versjonen av kategoriene.",
+ "apihelp-parse-paramvalue-prop-headitems": "Gir elementer som skal puttes i <code>&lt;head&gt;</code>-taggen til siden.",
+ "apihelp-patrol-summary": "Patruljer en side eller revisjon.",
+ "apihelp-query+allfileusages-param-prop": "Hvilken informasjon som skal inkluderes:",
+ "apihelp-query+allfileusages-paramvalue-prop-title": "Legger til filens tittel.",
+ "apihelp-query+allfileusages-param-limit": "Hvor mange elementer som skal returneres totalt.",
+ "apihelp-query+allfileusages-param-dir": "Retningen det skal listes opp i.",
+ "apihelp-query+allfileusages-example-unique": "List opp unike filtitler.",
+ "apihelp-query+allfileusages-example-unique-generator": "Hent alle filtitler og marker de som mangler.",
+ "apihelp-query+allfileusages-example-generator": "Hent sider som inneholder filene.",
+ "apihelp-query+allimages-param-sort": "Egenskap det skal sorteres etter.",
+ "apihelp-query+allimages-param-dir": "Retningen det skal sorteres i.",
+ "apihelp-query+allimages-param-minsize": "Begrens til bilder med minst dette antallet byte.",
+ "apihelp-query+allimages-param-maxsize": "Begrens til bilder med minst dette antallet byte.",
+ "apihelp-query+allimages-param-sha1": "SHA1-hash for bildet. Setter til side $1sha1base36.",
+ "apihelp-query+allimages-param-sha1base36": "SHA1-hash av bildet i base 36 (brukes i MediaWiki).",
+ "apihelp-query+allimages-param-user": "Returner bare filer lastet opp av denne brukeren. Kan kun brukes med $1sort=timestamp. Kan ikke brukes med $1filterbots.",
+ "apihelp-query+allimages-param-filterbots": "Hvordan opplastinger av roboter skal filtreres. Kan kun brukes med $1sort=timestamp. Kan ikke brukes sammen med $1user.",
+ "apihelp-query+allimages-param-mime": "Hvilke MIME-typer det skal søkes etter, f.eks. <kbd>image/jpeg</kbd>.",
+ "apihelp-query+allimages-param-limit": "Hvor mange bilder som skal returneres totalt.",
+ "apihelp-query+allimages-example-B": "Viser en liste over filer som begynner med bokstaven <kbd>B</kbd>.",
+ "apihelp-query+allimages-example-recent": "Viser en liste over nylig opplastede filer på samme måte som [[Special:NewFiles]].",
+ "apihelp-query+allimages-example-mimetypes": "Viser en liste over filer med MIME-typen <kbd>image/png</kbd> eller <kbd>image/gif</kbd>",
+ "apihelp-query+allimages-example-generator": "Viser informasjon om 4 filer fra og med bokstaven <kbd>T</kbd>.",
+ "apihelp-query+alllinks-param-prefix": "Søk etter alle lenkede titler som begynner med denne verdien.",
+ "apihelp-query+alllinks-param-prop": "Hvilken informasjon som skal inkluderes:",
+ "apihelp-query+alllinks-paramvalue-prop-title": "Legger til tittelen til lenka.",
+ "apihelp-query+alllinks-param-limit": "Hvor mange elementer som skal returneres totalt.",
+ "apihelp-query+alllinks-param-dir": "Retningen det skal listes i.",
+ "apihelp-query+alllinks-example-unique": "List opp unike lenkede titler.",
+ "apihelp-query+alllinks-example-unique-generator": "Henter alle lenkede titler og markerer de som mangler.",
+ "apihelp-query+alllinks-example-generator": "Hent sider som inneholder lenkene.",
+ "apihelp-query+allmessages-summary": "Returnerer meldinger fra denne siden.",
+ "apihelp-query+allmessages-param-prop": "Hvilke egenskaper som skal hentes.",
+ "apihelp-query+allmessages-param-filter": "Returner kun meldinger med navn som inneholder denne strengen.",
+ "apihelp-query+allmessages-param-lang": "Returner meldinger på dette språket.",
+ "apihelp-query+allmessages-param-title": "Sidenavn å bruke som kontekst når meldingen tolkes (for alternativet $1enableparser).",
+ "apihelp-query+allmessages-param-prefix": "Returner meldinger med dette prefikset.",
+ "apihelp-query+allmessages-example-ipb": "Vis meldinger som starter med <kbd>ipb-</kbd>.",
+ "apihelp-query+allmessages-example-de": "Vis meldingene <kbd>august</kbd> og <kbd>mainpage</kbd> på tysk.",
+ "apihelp-query+allpages-param-filterredir": "Hvilke sider som skal listes opp.",
+ "apihelp-query+allpages-param-minsize": "Begrens til sider med dette antallet byte.",
+ "apihelp-query+allpages-param-maxsize": "Begrens til sider med maksimalt dette antallet byte.",
+ "apihelp-query+allpages-param-prtype": "Begrens til beskyttede sider.",
+ "apihelp-query+allpages-param-limit": "Hvor mange sider som skal returneres totalt.",
+ "apihelp-query+allpages-param-dir": "Retningen det skal listes opp i.",
+ "apihelp-query+allpages-example-generator-revisions": "Vis innholdet til de to første ikke-omdirigeringssidene som begynner med <kbd>Re</kbd>.",
+ "apihelp-query+allredirects-summary": "Lister opp alle omdirigeringer til et navnerom.",
+ "apihelp-query+allredirects-param-prefix": "Søk etter alle målsider som begynner med denne verdien.",
+ "apihelp-query+allredirects-param-prop": "Hvilken informasjon som skal inkluderes:",
+ "apihelp-query+allredirects-paramvalue-prop-ids": "Legger til side-ID-en til den omdirigerende siden (kan ikke brukes med <var>$1unique</var>).",
+ "apihelp-query+allredirects-paramvalue-prop-title": "Legger til tittelen til omdirigeringen.",
+ "apihelp-query+allredirects-paramvalue-prop-fragment": "Legger til fragmentet fra omdirigeringen, om det er et (kan ikke brukes med <var>$1unique</var>).",
+ "apihelp-query+allredirects-paramvalue-prop-interwiki": "Legger til interwikiprefikset fra omdirigeringen, om det er et (kan ikke brukes med <var>$1unique</var>).",
+ "apihelp-query+allredirects-param-limit": "Hvor mange elementer som skal returneres totalt.",
+ "apihelp-query+allredirects-param-dir": "Retningen det skal listes opp i.",
+ "apihelp-query+allredirects-example-B": "List opp målsider, inkludert de som mangler, med side-ID-ene de kommer fra, begynner på <kbd>B</kbd>.",
+ "apihelp-query+allredirects-example-unique": "List opp unike målsider.",
+ "apihelp-query+allredirects-example-unique-generator": "Henter alle målsider, markerer de som mangler.",
+ "apihelp-query+allredirects-example-generator": "Henter sider som inneholder omdirigeringene.",
+ "apihelp-query+allrevisions-summary": "List opp alle revisjoner.",
+ "apihelp-query+allrevisions-param-user": "List bare opp revisjoner av denne brukeren.",
+ "apihelp-query+allrevisions-param-excludeuser": "Ikke list opp revisjoner av denne brukeren.",
+ "apihelp-query+allrevisions-param-namespace": "List kun opp sider i dette navnerommet.",
+ "apihelp-query+allrevisions-param-generatetitles": "Når denne brukes som generator, generer titler i stedet for revisjons-ID-er.",
+ "apihelp-query+allrevisions-example-user": "List opp de siste 50 bidragene fra brukeren <kbd>Example</kbd>.",
+ "apihelp-query+allrevisions-example-ns-main": "List opp de første 50 revisjonene i hovednavnerommet.",
+ "apihelp-query+mystashedfiles-param-prop": "Hvilke egenskaper som skal hentes for filene.",
+ "apihelp-query+mystashedfiles-paramvalue-prop-size": "Hent filstørrelse og bildedimensjoner.",
+ "apihelp-query+mystashedfiles-paramvalue-prop-type": "Hent filas MIME-type og medietype.",
+ "apihelp-query+mystashedfiles-param-limit": "Hvor mange filer som skal hentes.",
+ "apihelp-query+alltransclusions-param-prop": "Hvilken informasjon som skal inkluderes:",
+ "apihelp-query+alltransclusions-paramvalue-prop-title": "Legger til tittelen på transklusjonen.",
+ "apihelp-query+allusers-param-prop": "Hvilken informasjon som skal inkluderes:",
+ "apihelp-query+allusers-paramvalue-prop-blockinfo": "Legger til informasjon om en gjeldende blokkering av brukeren.",
+ "apihelp-query+allusers-paramvalue-prop-groups": "Lister opp grupper brukeren er i. Dette bruker flere tjenerressurser og kan returnere færre resultater enn grensa.",
+ "apihelp-query+allusers-paramvalue-prop-implicitgroups": "Lister opp alle grupper brukeren automatisk er med i.",
+ "apihelp-query+allusers-paramvalue-prop-rights": "Lister opp rettigheter brukeren har.",
+ "apihelp-query+allusers-paramvalue-prop-editcount": "Legger til redigeringstelleren til brukeren.",
+ "apihelp-query+allusers-paramvalue-prop-registration": "Legger til tidsstempelet for når brukeren ble registrert, om tilgjengelig (kan være blank).",
+ "apihelp-query+allusers-paramvalue-prop-centralids": "Legger til sentrale ID-er og tilkoblingsstatus for brukeren.",
+ "apihelp-query+allusers-param-limit": "Hvor mange brukernavn som skal returneres.",
+ "apihelp-query+allusers-param-witheditsonly": "List bare opp brukere som har gjort redigeringer.",
+ "apihelp-query+allusers-param-activeusers": "List bare opp brukere som har vært aktiv {{PLURAL:$1|den siste dagene|de siste $1 dagene}}.",
+ "apihelp-query+allusers-param-attachedwiki": "Med <kbd>$1prop</kbd>, indiker også hvorvidt brukeren er tilkoblet med wikien som identifiseres av denne ID-en.",
+ "apihelp-query+allusers-example-Y": "List opp brukere fra og med <kbd>Y</kbd>.",
+ "apihelp-query+authmanagerinfo-summary": "Hent informasjon om den gjeldende autentiseringsstatusen.",
+ "apihelp-query+backlinks-summary": "Finn alle sider som lenker til den gitte siden.",
+ "apihelp-query+backlinks-param-title": "Tittel det skal søkes etter. Kan ikke brukes sammen med <var>$1pageid</var>.",
+ "apihelp-query+backlinks-param-pageid": "Side-ID det skal søkes etter. Kan ikke brukes sammen med <var>$1title</var>.",
+ "apihelp-query+backlinks-param-dir": "Retningen det skal listes opp i.",
+ "apihelp-query+categorymembers-param-prop": "Hvilken informasjon som skal inkluderes:",
+ "apihelp-query+deletedrevs-paraminfo-modes": "{{PLURAL:$1|Modus|Moduser}}: $2",
+ "apihelp-query+exturlusage-param-prop": "Hvilken informasjon som skal inkluderes:",
+ "apihelp-query+iwbacklinks-param-limit": "Hvor mange sider som skal returneres totalt.",
+ "apihelp-query+iwbacklinks-param-prop": "Hvilke egenskaper som skal hentes:",
+ "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "Legger til prefikset til interwikien.",
+ "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "Legger til tittelen til interwikien.",
+ "apihelp-query+iwbacklinks-param-dir": "Retningen det skal listes opp i.",
+ "apihelp-query+iwbacklinks-example-simple": "Hent sider som lenker til [[wikibooks:Test]].",
+ "apihelp-query+iwbacklinks-example-generator": "Hent informasjon om sider som lenker til [[wikibooks:Test]].",
+ "apihelp-query+iwlinks-summary": "Returnerer alle interwikilenker fra de gitte sidene.",
+ "apihelp-query+iwlinks-param-url": "Hvorvidt den fulle URL-en skal hentes (kan ikke brukes med $1prop).",
+ "apihelp-query+iwlinks-param-prop": "Hvilke ekstra egenskaper som skal hentes for hver språklenke:",
+ "apihelp-query+iwlinks-paramvalue-prop-url": "Legger til den fulle URL-en.",
+ "apihelp-query+iwlinks-param-limit": "Hvor mange interikilenker som skal returneres.",
+ "apihelp-query+iwlinks-param-prefix": "Returner bare interwikilenker med dette prefikset.",
+ "apihelp-query+iwlinks-param-title": "Interwikilenker det skal søkes etter. Må brukes med <var>$1prefix</var>.",
+ "apihelp-query+iwlinks-param-dir": "Retningen det skal listes opp i.",
+ "apihelp-query+iwlinks-example-simple": "Hent interwikilenker fra sida <kbd>Main Page</kbd>.",
+ "apihelp-query+langbacklinks-summary": "Finn alle sider som lenker til den gitte språklenka.",
+ "apihelp-query+langbacklinks-param-lang": "Språket for språklenka.",
+ "apihelp-query+langbacklinks-param-title": "Språklenke det skal søkes etter. Må brukes med $1lang.",
+ "apihelp-query+langbacklinks-param-limit": "Hvor mange sider som skal returneres totalt.",
+ "apihelp-query+langbacklinks-param-prop": "Hvilke egenskaper som skal hentes:",
+ "apihelp-query+langbacklinks-paramvalue-prop-lllang": "Legger til språkkoden til språklenka.",
+ "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "Legger til tittelen til språklenka.",
+ "apihelp-query+langbacklinks-param-dir": "Retningen det skal listes opp i.",
+ "apihelp-query+langbacklinks-example-simple": "Hent sider som lenker til [[:fr:Test]].",
+ "apihelp-query+langbacklinks-example-generator": "Hent informasjon om sider som lenker til [[:fr:Test]].",
+ "apihelp-query+langlinks-summary": "Returnerer alle språklenker fra de gitte sidene.",
+ "apihelp-query+langlinks-param-limit": "Hvor mange språklenker som skal returneres.",
+ "apihelp-query+langlinks-param-url": "Hvorvidt den fulle URL-en skal hentes (kan ikke brukes med <var>$1prop</var>).",
+ "apihelp-query+langlinks-param-prop": "Hvilke ekstra egenskaper som skal hentes for hver språklenke:",
+ "apihelp-query+langlinks-paramvalue-prop-url": "Legger til den fulle URL-en.",
+ "apihelp-query+langlinks-param-lang": "Returner bare språklenker med denne språkkoden.",
+ "apihelp-query+langlinks-param-title": "Lenke det skal søkes etter. Må brukes med <var>$1lang</var>.",
+ "apihelp-query+langlinks-param-dir": "Retningen det skal listes opp i.",
+ "apihelp-query+langlinks-param-inlanguagecode": "Språkkode for oversatte språknavn.",
+ "apihelp-query+langlinks-example-simple": "Hent språklenker fra siden <kbd>Main Page</kbd>.",
+ "apihelp-query+links-summary": "Returnerer alle lenker fra de gitte sidene.",
+ "apihelp-query+links-param-namespace": "Viser lenker kun i disse navnerommene.",
+ "apihelp-query+links-param-limit": "Hvor mange lenker som skal returneres.",
+ "apihelp-query+links-param-titles": "List bare opp lenker til disse titlene. Nyttig for å sjekke om en viss side lenker til en annen viss side.",
+ "apihelp-query+links-param-dir": "Retningen det skal listes opp i.",
+ "apihelp-query+links-example-simple": "Hent lenker fra sida <kbd>Main Page</kbd>",
+ "apihelp-query+links-example-generator": "Hent informasjon om lenkesidene på sida <kbd>Main Page</kbd>.",
+ "apihelp-query+links-example-namespaces": "Hent lenker fra sida <kbd>Main Page</kbd> i navnerommene {{ns:user}} og {{ns:template}}.",
+ "apihelp-query+linkshere-summary": "Finn alle sider som lenker til de gitte sidene.",
+ "apihelp-query+linkshere-param-prop": "Hvilke egenskaper som skal hentes:",
+ "apihelp-query+linkshere-paramvalue-prop-pageid": "Side-ID for hver side.",
+ "apihelp-query+linkshere-paramvalue-prop-title": "Tittelen til hver side.",
+ "apihelp-query+linkshere-paramvalue-prop-redirect": "Marker om siden er omdirigering.",
+ "apihelp-query+linkshere-param-namespace": "Inkluder bare sider i disse navnerommene.",
+ "apihelp-query+linkshere-param-limit": "Hvor mange som skal returneres.",
+ "apihelp-query+linkshere-param-show": "Vis bare elementer som møter disse kriteriene:\n;redirect:Vis bare omdirigeringer.\n;!redirect:Vis bare ikke-omdirigeringer.",
+ "apihelp-query+linkshere-example-simple": "Hent ei liste over sider som lenker til [[Main Page]].",
+ "apihelp-query+linkshere-example-generator": "Hent informasjon om sider som lenker til [[Main Page]].",
+ "apihelp-query+logevents-summary": "Hent oppføringer fra logger.",
+ "apihelp-query+logevents-param-prop": "Hvilke egenskaper som skal hentes:",
+ "apihelp-query+logevents-paramvalue-prop-ids": "Legger til ID-en til loggoppføringen.",
+ "apihelp-query+logevents-paramvalue-prop-title": "Legger til tittelen på siden i loggoppføringen.",
+ "apihelp-query+logevents-paramvalue-prop-type": "Legger til typen loggoppføring.",
+ "apihelp-query+logevents-paramvalue-prop-user": "Legger til brukeren som er ansvarlig for loggoppføringen.",
+ "apihelp-query+logevents-paramvalue-prop-userid": "Legger til bruker-ID-en som var ansvarlig for loggoppføringen.",
+ "apihelp-query+logevents-paramvalue-prop-timestamp": "Legger til tidsstempelet for loggoppføringen.",
+ "apihelp-query+logevents-paramvalue-prop-comment": "Legger til kommentaren til loggoppføringen.",
+ "apihelp-query+logevents-paramvalue-prop-parsedcomment": "Legger til den parsede kommentaren til loggoppføringen.",
+ "apihelp-query+logevents-paramvalue-prop-details": "Lister opp ekstra detaljer om loggoppføringen.",
+ "apihelp-query+logevents-param-type": "Filtrer loggoppføringer til kun denne typen.",
+ "apihelp-query+logevents-param-action": "Filtrer logghandlinger til kun denne handlingen. Overstyrer <var>$1type</var>. I listen over mulige verdier kan verdier med jokertegnet stjerne, som <kbd>action/*</kbd> ha forskjellige strenger etter skråstreken.",
+ "apihelp-query+logevents-param-user": "Filtrer oppføringer til de som er gjort av den gitte brukeren.",
+ "apihelp-query+logevents-param-title": "Filtrer oppføringer til de som er relatert til ei side.",
+ "apihelp-query+logevents-param-namespace": "Filtrer oppføringer til de i det gitte navnerommet.",
+ "apihelp-query+logevents-param-prefix": "Filtrer oppføringer som starter med dette prefikset.",
+ "apihelp-query+logevents-param-tag": "List bare opp oppføringer som er tagget med denne taggen.",
+ "apihelp-query+logevents-param-limit": "Hvor mange oppføringer som skal returneres.",
+ "apihelp-query+logevents-example-simple": "List opp nylige loggoppføringer.",
+ "apihelp-query+pagepropnames-summary": "List opp alle sideegenskapsnavn i bruk på wikien.",
+ "apihelp-query+pagepropnames-param-limit": "Maksimalt antall navn som skal returneres.",
+ "apihelp-query+pagepropnames-example-simple": "Hent de 10 første egenskapsnavnene.",
+ "apihelp-query+pageprops-summary": "Hent diverse sideegenskaper definert i sideinnholdet.",
+ "apihelp-query+pageprops-param-prop": "List kun opp disse sideegenskapene (<kbd>[[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]]</kbd> returnerer sideegenskapsnavn i bruk). Nyttig for å sjekke om sider bruker en viss sideegenskap.",
+ "apihelp-query+pageprops-example-simple": "Hent egenskaper for sidene <kbd>Main Page</kbd> og <kbd>MediaWiki</kbd>.",
+ "apihelp-query+pageswithprop-summary": "Lister opp alle sider med en gitt sideegenskap.",
+ "apihelp-query+pageswithprop-param-prop": "Hvilken informasjon som skal inkluderes:",
+ "apihelp-query+pageswithprop-paramvalue-prop-ids": "Legger til side-ID-en.",
+ "apihelp-query+pageswithprop-paramvalue-prop-title": "Legger til tittel- og navneroms-ID-en til sida.",
+ "apihelp-query+pageswithprop-paramvalue-prop-value": "Legger til verdien til sideegenskapen.",
+ "apihelp-query+pageswithprop-param-limit": "Maksimalt antall sider som skal returneres.",
+ "apihelp-query+pageswithprop-param-dir": "Hvilken retning det skal sorteres i.",
+ "apihelp-query+pageswithprop-example-simple": "List opp de første 10 sidene som bruker <code>&#123;&#123;DISPLAYTITLE:&#125;&#125;</code>.",
+ "apihelp-query+pageswithprop-example-generator": "Hent ekstra informasjon om de 10 første sidene som bruker <code>_&#95;NOTOC_&#95;</code>.",
+ "apihelp-query+prefixsearch-summary": "Utfør et prefikssøk for sidetitler.",
+ "apihelp-query+prefixsearch-param-search": "Søkestreng.",
+ "apihelp-query+prefixsearch-param-namespace": "Navnerom det skal søkes i.",
+ "apihelp-query+prefixsearch-param-limit": "Maksimalt antall resultater som skal returneres.",
+ "apihelp-query+prefixsearch-param-offset": "Antall resultater som skal hoppes over.",
+ "apihelp-query+prefixsearch-example-simple": "Søk etter sidetitler som begynner med <kbd>meaning</kbd>.",
+ "apihelp-query+prefixsearch-param-profile": "Søkeprofil som skal brukes.",
+ "apihelp-query+protectedtitles-summary": "List opp alle titler som er beskyttet fra opprettelse.",
+ "apihelp-query+protectedtitles-param-namespace": "List kun opp titler i disse navnerommene.",
+ "apihelp-query+protectedtitles-param-level": "List kun opp titler med disse beskyttelsesnivåene.",
+ "apihelp-query+protectedtitles-param-limit": "Hvor mange sider som skal returneres totalt.",
+ "apihelp-query+protectedtitles-param-prop": "Hvilke egenskaper som skal hentes:",
+ "apihelp-query+querypage-param-limit": "Antall resultater som skal returneres.",
+ "apihelp-query+querypage-example-ancientpages": "Returner resultater fra [[Special:Ancientpages]].",
+ "apihelp-query+random-summary": "Hent et sett av tilfeldige sider.",
+ "apihelp-query+userinfo-param-prop": "Hvilken informasjon som skal inkluderes:",
+ "apihelp-query+users-param-prop": "Hvilken informasjon som skal inkluderes:",
+ "apihelp-query+watchlist-param-type": "Hvilke typer endringer som skal vises:",
+ "apihelp-query+watchlist-paramvalue-type-edit": "Vanlige sideredigeringer.",
+ "apihelp-query+watchlist-paramvalue-type-external": "Eksterne endringer.",
+ "apihelp-query+watchlist-paramvalue-type-new": "Sideopprettelser",
+ "apihelp-query+watchlist-paramvalue-type-log": "Loggoppføringer.",
+ "apihelp-query+watchlist-paramvalue-type-categorize": "Endringer i kategorimedlemskap.",
+ "apihelp-query+watchlist-param-owner": "Brukes sammen med $1token for å få tilgang til en annen brukers overvåkningsliste.",
+ "apihelp-query+watchlist-param-token": "En sikkerhetsnøkkel (tilgjengelig i brukerens [[Special:Preferences#mw-prefsection-watchlist|innstillinger]]) for å tillate tilgang til en annen brukers overvåkningsliste.",
+ "apihelp-query+watchlistraw-param-namespace": "List kun opp sider i de gitte navnerommene.",
+ "apihelp-query+watchlistraw-param-limit": "Hvor mange resultater som skal returneres totalt per forespørsel.",
+ "apihelp-query+watchlistraw-param-prop": "Hvilke ekstra egenskaper som skal hentes:",
+ "apihelp-query+watchlistraw-paramvalue-prop-changed": "Legger til tidsstempel for når brukeren sist ble varslet om redigeringen.",
+ "apihelp-query+watchlistraw-param-show": "List kun opp elementer som tilfredsstiller disse kriteriene.",
+ "apihelp-query+watchlistraw-param-owner": "Brukes sammen med $1token for å få tilgang til en annen brukers overvåkningsliste.",
+ "apihelp-query+watchlistraw-param-token": "En sikkerhetsnøkkel (tilgjengelig i brukerens [[Special:Preferences#mw-prefsection-watchlist|innstillinger]]) for å tillate tilgang til en annen brukers overvåkningsliste.",
+ "apihelp-query+watchlistraw-param-dir": "Retningen det skal listes opp i.",
+ "apihelp-query+watchlistraw-example-simple": "List opp sider på den gjeldende brukerens overvåkningsliste.",
+ "apihelp-query+watchlistraw-example-generator": "Hent sideinfo for sider på den gjeldende brukerens overvåkningsliste.",
+ "apihelp-removeauthenticationdata-summary": "Fjern autentiseringsdata for den gjeldende brukeren.",
+ "apihelp-removeauthenticationdata-example-simple": "Forsøk å fjerne den gjeldende brukerens data for <kbd>FooAuthenticationRequest</kbd>.",
+ "apihelp-resetpassword-summary": "Send en epost for nullstilling av passord til en bruker.",
+ "apihelp-revisiondelete-summary": "Slett og gjenopprett revisjoner.",
+ "apihelp-revisiondelete-param-type": "Type revisjonssletting som utføres.",
+ "apihelp-revisiondelete-param-target": "Sidetittelen for revisjonssletting, om det kreves for typen.",
+ "apihelp-revisiondelete-param-ids": "Identifikatorer for revisjonene som skal slettes.",
+ "apihelp-revisiondelete-param-hide": "Hva som skal skjules for hver revisjon.",
+ "apihelp-revisiondelete-param-show": "Hva som skal vises for hver revisjon.",
+ "apihelp-revisiondelete-param-suppress": "Hvorvidt data skal skjules for administratorer i tillegg til andre.",
+ "apihelp-revisiondelete-param-reason": "Årsak for slettingen eller gjenopprettingen.",
+ "apihelp-revisiondelete-param-tags": "Tagger som skal brukes på oppføringen i sletteloggen.",
+ "apihelp-revisiondelete-example-revision": "Skjul innhold for revisjon <kbd>12345</kbd> på siden <kbd>Main Page</kbd>.",
+ "apihelp-revisiondelete-example-log": "Skjul alle data om loggoppføringen <kbd>67890</kbd> med årsak <kbd>BLP violation</kbd>.",
+ "apihelp-rollback-summary": "Omgjør den siste redigeringen på siden.",
+ "apihelp-rollback-extended-description": "Om den siste brukeren som redigerte siden gjorde flere redigeringer på rad, vil alle disse redigeringene fjernes.",
+ "apihelp-rollback-param-title": "Tittelen på siden som skal tilbakestilles. Kan ikke brukes sammen med <var>$1pageid</var>.",
+ "apihelp-rollback-param-pageid": "Side-ID for siden som skal tilbakestilles. Kan ikke brukes sammen med <var>$1title</var>.",
+ "apihelp-rollback-param-tags": "Tagger som skal påføres tilbakestillingen.",
+ "apihelp-rollback-param-user": "Navnet til brukeren hvis redigeringer skal tilbakestilles.",
+ "apihelp-rollback-param-summary": "Egendefinert redigeringssammendrag. Om denne er tom vil standardsammendraget brukes.",
+ "apihelp-rollback-param-markbot": "Merk de tilbakestilte redigeringene og tilbakestillingen som botredigeringer.",
+ "apihelp-rollback-example-simple": "Tilbakestill de siste redigeringene på siden <kbd>Main Page</kbd> av brukeren <kbd>Example</kbd>.",
+ "apihelp-rollback-example-summary": "Tilbakestill de siste redigeringene på siden <kbd>Main Page</kbd> av IP-adressen <kbd>192.0.2.5</kbd> med sammendraget <kbd>Reverting vandalism</kbd>, og merk disse redigeringene samt tilbakestillingen som botredigeringer.",
+ "apihelp-setnotificationtimestamp-summary": "Oppdater varselstidsstempelet for overvåkede sider.",
+ "apihelp-setpagelanguage-summary": "Endre språket til en side.",
+ "apihelp-setpagelanguage-extended-description-disabled": "Endring av språket til en side tillates ikke på denne wikien.\n\nSlå på <var>[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> for å bruke denne handlingen.",
+ "apihelp-setpagelanguage-param-title": "Tittelen på siden som skal endre språk. Kan ikke brukes sammen med <var>$1pageid</var>.",
+ "apihelp-setpagelanguage-param-pageid": "Side-ID til siden du ønsker å endre språk på. Kan ikke brukes sammen med <var>$1title</var>.",
+ "apihelp-setpagelanguage-param-lang": "Språkkoden for språket du ønsker å endre siden til. Bruk <kbd>default</kbd> for å tilbakestille siden til wikiens standardspråk.",
+ "apihelp-setpagelanguage-param-reason": "Årsak for endringen.",
+ "apihelp-setpagelanguage-param-tags": "Endringstagger som skal påføres loggoppføringen som oppstår på grunn av denne handlingen.",
+ "apihelp-setpagelanguage-example-language": "Endre språket til <kbd>Main Page</kbd> til baskisk.",
+ "apihelp-setpagelanguage-example-default": "Endre språket til siden med ID 123 til wikiens standardspråk.",
+ "apihelp-stashedit-param-title": "Tittelen på siden som redigeres.",
+ "apihelp-stashedit-param-section": "Seksjonsnummer. <kbd>0</kbd> for toppseksjonen, <kbd>new</kbd> for en ny seksjon.",
+ "apihelp-stashedit-param-sectiontitle": "Tittelen til en ny seksjon.",
+ "apihelp-stashedit-param-text": "Sideinnhold.",
+ "apihelp-stashedit-param-contentmodel": "Innholdsmodellen til det nye innholdet.",
+ "apihelp-stashedit-param-baserevid": "Revisjons-ID-en til grunnrevisjonen.",
+ "apihelp-stashedit-param-summary": "Endringssammendrag.",
+ "apihelp-tag-summary": "Legg til eller fjern endringstagger fra individuelle revisjoner eller loggoppføringer.",
+ "apihelp-tag-param-revid": "Én eller flere revisjons-ID-er taggen skal legges til på eller fjernes fra.",
+ "apihelp-tag-param-logid": "Én eller flere loggoppførings-ID-er taggen skal legges til på eller fjernes fra.",
+ "apihelp-tag-param-add": "Tagger som skal legges til. Kun manuelt definerte tagger kan legges til.",
+ "apihelp-tag-param-remove": "Tagger som skal fjernes. Kun tagger som er enten manuelt definert eller helt udefinerte kan fjernes.",
+ "apihelp-tag-param-reason": "Årsak for endringen.",
+ "apihelp-tag-param-tags": "Tagger som skal påføres loggoppføringen som vil oppstå på grunn av denne handlingen.",
+ "apihelp-tag-example-rev": "Legg til taggen <kbd>vandalism</kbd> til revisjons-ID-en 123 uten å oppgi årsak",
+ "apihelp-tag-example-log": "Fjern taggen <kbd>spam</kbd> fra loggoppførings-ID-en 123 med årsaken <kbd>Wrongly applied</kbd>",
+ "apihelp-tokens-extended-description": "Denne modulen er foreldet til fordel for [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].",
+ "apihelp-tokens-example-edit": "Hent en redigeringsnøkkel (standard).",
+ "apihelp-tokens-example-emailmove": "Hent en epostnøkkel og en flyttingsnøkkel.",
+ "apihelp-unblock-summary": "Avblokker en bruker.",
+ "apihelp-userrights-param-user": "Brukernavn.",
+ "apihelp-userrights-param-userid": "Bruker-ID.",
+ "apihelp-userrights-param-remove": "Fjern brukeren fra disse gruppene.",
+ "apihelp-userrights-param-reason": "Årsak for endringen.",
+ "apihelp-userrights-param-tags": "Endringstagger som skal påføres oppføringen i brukerettighetsloggen.",
+ "apihelp-userrights-example-user": "Legg til brukeren <kbd>FooBot</kbd> i gruppa <kbd>bot</kbd>, og fjern den fra gruppene <kbd>sysop</kbd> og <kbd>bureaucrat</kbd>.",
+ "apihelp-userrights-example-userid": "Legg til brukeren med ID <kbd>123</kbd> til gruppa <kbd>bot</kbd>, og fjern den fra gruppene <kbd>sysop</kbd> og <kbd>bureaucrat</kbd>.",
+ "apihelp-userrights-example-expiry": "Legg til brukeren <kbd>SometimeSysop</kbd> til gruppa <kbd>sysop</kbd> midlertidig i én måned.",
+ "apihelp-validatepassword-summary": "Valider et passord mot wikiens passordkrav.",
+ "apihelp-validatepassword-param-password": "Passord som skal valideres.",
+ "apihelp-watch-example-watch": "Overvåk siden <kbd>Main Page</kbd>.",
+ "apihelp-watch-example-unwatch": "Avslutt overvåking av siden <kbd>Main Page</kbd>.",
+ "apihelp-format-example-generic": "Returner spørringsresultatet i formatet $1.",
"apihelp-json-summary": "Resultatdata i JSON-format.",
"apihelp-none-summary": "Ingen resultat.",
+ "api-help-main-header": "Hovedmodul",
+ "api-help-undocumented-module": "Ingen dokumentasjon for modulen $1.",
+ "api-help-flag-deprecated": "Modulen er foreldet.",
+ "api-help-flag-internal": "<strong>Denne modulen er intern eller ustabel.</strong> Hvordan den fungerer kan forandre seg uten forvarsel.",
"api-help-flag-readrights": "Denne modulen krever lesetilgang.",
"api-help-flag-writerights": "Denne modulen krever skrivetilgang.",
"api-help-flag-mustbeposted": "Denne modulen aksepterer bare POST forespørsler.",
"api-help-flag-generator": "Denne modulen kan brukes som en generator.",
+ "api-help-source": "Kilde: $1",
+ "api-help-source-unknown": "Kilde: <span class=\"apihelp-unknown\">ukjent</span>",
+ "api-help-license": "Lisens: [[$1|$2]]",
+ "api-help-license-noname": "Lisens: [[$1|Se lenke]]",
+ "api-help-license-unknown": "Lisens: <span class=\"apihelp-unknown\">ukjent</span>",
"api-help-parameters": "{{PLURAL:$1|Parameter|Parametre}}:",
"api-help-param-deprecated": "Utgått.",
"api-help-param-required": "Denne parameteren er påkrevd.",
- "apierror-multival-only-one": "Bare én verdi er tillatt for parameteret <var>$1</var>.",
+ "api-help-datatypes-header": "Datatyper",
+ "api-help-param-type-limit": "Type: heltall eller <kbd>max</kbd>",
+ "api-help-param-type-integer": "Type: {{PLURAL:$1|1=heltall|2=liste over heltall}}",
+ "api-help-param-type-boolean": "Type: boolsk verdi ([[Special:ApiHelp/main#main/datatypes|detaljer]])",
+ "api-help-param-type-timestamp": "Type: {{PLURAL:$1|1=tidsstempel|2=liste over tidsstempler}} ([[Special:ApiHelp/main#main/datatypes|tillatte formater]])",
+ "api-help-param-type-user": "Type: {{PLURAL:$1|1=brukernavn|2=liste over brukernavn}}",
+ "api-help-param-list": "{{PLURAL:$1|1=Én av følgende verdier|2=Verdier (separer med <kbd>{{!}}</kbd> eller [[Special:ApiHelp/main#main/datatypes|alternativ]])}}: $2",
+ "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=Må være tom|Kan være tom, eller $2}}",
+ "api-help-param-limit": "Ikke mer enn $1 er tillatt.",
+ "api-help-param-limit2": "Ikke mer enn $1 ($2 for botter) er tillatt.",
+ "api-help-param-integer-min": "{{PLURAL:$1|Verdien|Verdiene}} må ikke være mindre enn $2.",
+ "api-help-param-integer-max": "{{PLURAL:$1|Verdien|Verdiene}} må ikke være større enn $3.",
+ "api-help-param-integer-minmax": "{{PLURAL:$1|Verdien|Verdiene}} må være mellom $2 og $3.",
+ "api-help-param-multi-separate": "Separer verdier med <kbd>|</kbd> eller [[Special:ApiHelp/main#main/datatypes|alternativ]].",
+ "api-help-param-multi-max": "Maksimalt antall verdier er {{PLURAL:$1|$1}} ({{PLURAL:$2|$2}} for botter)",
+ "api-help-param-multi-max-simple": "Maksimalt antall verdier er {{PLURAL:$1|$1}}.",
+ "api-help-param-multi-all": "For å angi alle verdier, bruk <kbd>$1</kbd>.",
+ "api-help-param-default": "Standard: $1",
+ "api-help-param-default-empty": "Standard: <span class=\"apihelp-empty\">(tom)</span>",
+ "api-help-param-continue": "Når flere resultater er tilgjengelige, bruk denne for å fortsette.",
+ "api-help-param-no-description": "<span class=\"apihelp-empty\">(ingen beskrivelse)</span>",
+ "api-help-param-maxbytes": "Kan ikke være lengre enn $1 {{PLURAL:$1|byte}}.",
+ "api-help-param-maxchars": "Kan ikke være lengre enn $1 {{PLURAL:$1|tegn}}.",
+ "api-help-examples": "{{PLURAL:$1|Eksempel|Eksempler}}:",
+ "api-help-permissions": "{{PLURAL:$1|Tillatelse|Tillatelser}}:",
+ "api-help-permissions-granted-to": "{{PLURAL:$1|Gitt til}}: $2",
+ "api-help-open-in-apisandbox": "<small>[åpne i sandkasse]</small>",
+ "apierror-changeauth-norequest": "Kunne ikke opprette endringsforespørsel.",
+ "apierror-contentserializationexception": "Innholdsserialisering feliet: $1",
+ "apierror-contenttoobig": "Innholdet du oppga overskrider artikkelstørrelsesgrensen på $1 {{PLURAL:$1|kilobyte}}.",
+ "apierror-copyuploadbaddomain": "Opplasting via URL tillates ikke fra dette domenet.",
+ "apierror-copyuploadbadurl": "Opplasting tillates ikke fra denne URL-en.",
+ "apierror-create-titleexists": "Eksisterende titler kan ikke beskyttes med <kbd>create</kbd>.",
+ "apierror-csp-report": "Feil under prosessering av CSP-rapport: $1.",
+ "apierror-databaseerror": "[$1] Databasespørringsfeil.",
+ "apierror-deletedrevs-param-not-1-2": "Parameteren <var>$1</var> kan ikke brukes i modus 1 eller 2.",
+ "apierror-deletedrevs-param-not-3": "Parameteren <var>$1</var> kan ikke brukes i modus 3.",
+ "apierror-emptynewsection": "Oppretting av tomme nye seksjoner er ikke mulig.",
+ "apierror-emptypage": "Oppretting av nye, tomme sider tillates ikke.",
+ "apierror-exceptioncaught": "[$1] Unntak fanget: $2",
+ "apierror-filedoesnotexist": "Fila fins ikke.",
+ "apierror-fileexists-sharedrepo-perm": "Målfila fins på et delt fillager. Bruk parameteren <var>ignorewarnings</var> for å overstyre den.",
+ "apierror-filenopath": "Kan ikke hente lokal filsti.",
+ "apierror-filetypecannotberotated": "Filtypen kan ikke roteres.",
+ "apierror-formatphp": "Denne responsen kan ikke representeres med <kbd>format=php</kbd>. Se https://phabricator.wikimedia.org/T68776.",
+ "apierror-imageusage-badtitle": "Tittelen for <kbd>$1</kbd> må være ei fil.",
+ "apierror-import-unknownerror": "Ukjent feil under importering: $1.",
+ "apierror-integeroutofrange-abovebotmax": "<var>$1</var> kan ikke være over $2 (satt til $3) for botter eller administratorer.",
+ "apierror-integeroutofrange-abovemax": "<var>$1</var> kan ikke være over $2 (satt til $3) for brukere.",
+ "apierror-integeroutofrange-belowminimum": "<var>$1</var> kan ikke være mindre enn $2 (satt til $3).",
+ "apierror-invalidcategory": "Kategorinavnet du skrev inn er ikke gyldig.",
+ "apierror-invalidexpiry": "Ugyldig utløpstid «$1».",
+ "apierror-invalid-file-key": "Ikke en gyldig filnøkkel.",
+ "apierror-invalidlang": "Ugyldig språkkode for parameteren <var>$1</var>.",
+ "apierror-invalidoldimage": "Parameteren <var>oldimage</var> har et ugyldig format.",
+ "apierror-invalidparammix-cannotusewith": "Parameteren <kbd>$1</kbd> kan ikke brukes med <kbd>$2</kbd>.",
+ "apierror-invalidparammix-mustusewith": "Parameteren <kbd>$1</kbd> kan ikke brukes med <kbd>$2</kbd>.",
+ "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> kan ikke kombineres med parameterne <var>oldid</var>, <var>pageid</var> eller <var>page</var>. Bruk <var>title</var> og <var>text</var>.",
+ "apierror-invalidparammix": "{{PLURAL:$2|Parameterne}} $1 kan ikke brukes sammen.",
+ "apierror-invalidsection": "Parameteren <var>section</var> må være en gyldig seksjons-ID eller <kbd>new</kbd>.",
+ "apierror-invalidtitle": "Ugyldig tittel «$1».",
+ "apierror-invalidurlparam": "Ugyldig verdi for <var>$1urlparam</var> (<kbd>$2=$3</kbd>).",
+ "apierror-invaliduser": "Ugyldig brukernavn «$1».",
+ "apierror-invaliduserid": "Bruker-ID-en <var>$1</var> er ikke gyldig.",
+ "apierror-maxbytes": "Parameteren <var>$1</var> kan ikke være lengre enn $2 {{PLURAL:$2|byte}}",
+ "apierror-maxchars": "Parameteren <var>$1</var> kan ikke være lengre enn $2 {{PLURAL:$2|tegn}}",
+ "apierror-maxlag-generic": "Venter på en databasetjener: Henger etter med {{PLURAL:$1|ett sekund|$1 sekunder}}.",
+ "apierror-maxlag": "Venter på $2: Henger etter med {{PLURAL:$1|ett sekund|$1 sekunder}}.",
+ "apierror-mimesearchdisabled": "MIME-søk er slått av i Miser-modus.",
+ "apierror-missingcontent-pageid": "Manglende innhold for side-ID $1.",
+ "apierror-missingcontent-revid": "Manglende innhold for revisjons-ID $1.",
+ "apierror-missingparam-at-least-one-of": "{{PLURAL:$1|Parameteren|Minst én av parameterne}} $1 er påkrevd.",
+ "apierror-missingparam-one-of": "{{PLURAL:$2|Parameteren|Én av parameterne}} $1 er påkrevd.",
+ "apierror-missingparam": "Parameteren <var>$1</var> må være satt.",
+ "apierror-missingrev-pageid": "Ingen gjeldende revisjon av side-ID $1.",
+ "apierror-missingrev-title": "Ingen gjeldende revisjon av tittelen $1.",
+ "apierror-missingtitle-createonly": "Manglende titler kan bare beskyttes med <kbd>create</kbd>.",
+ "apierror-missingtitle": "Siden du oppga fins ikke.",
+ "apierror-missingtitle-byname": "Siden $1 fins ikke.",
+ "apierror-moduledisabled": "Modulen <kbd>$1</kbd> har blitt slått av.",
+ "apierror-multival-only-one-of": "{{PLURAL:$3|Kun|Kun én av} $2 tillates for parameteren <var>$3</var>.",
+ "apierror-multpages": "<var>$1</var> kan kun brukes med én enkel side.",
+ "apierror-mustbeloggedin-changeauth": "Du må være logget inn for å endre autentiseringsdata.",
+ "apierror-mustbeloggedin-generic": "Du må være logget inn.",
+ "apierror-mustbeloggedin-linkaccounts": "Du må være logget inn for å lenke kontoer.",
+ "apierror-mustbeloggedin-removeauth": "Du må være logget inn for å fjerne autentiseringsdata.",
"apierror-mustbeloggedin": "Du må være logget inn for å $1.",
+ "apierror-mustbeposted": "Modulen <kbd>$1</kbd> krever en POST-forespørsel.",
+ "apierror-mustpostparams": "Følgende {{PLURAL:$2|parameter|parametre}} ble funnet i spørringsstrengen, men må være i POST-innholdet: $1.",
+ "apierror-noapiwrite": "Redigering av denne wikien via API er slått av. Sjekk at utsagnet <code>$wgEnableWriteAPI=true;</code> inkluderes i wikiens <code>LocalSettings.php</code>-fil.",
+ "apierror-nochanges": "Ingen endringer ble forespurt.",
+ "apierror-no-direct-editing": "Direkte redigering via API-et støttes ikke for innholdsmodellen $1 som brukes av $2.",
+ "apierror-noedit-anon": "Anonyme brukere kan ikke redigere sider.",
+ "apierror-noedit": "Du har ikke tillatelse til å redigere sider.",
+ "apierror-noimageredirect-anon": "Anonyme brukere kan ikke opprette bildeomdirigeringer.",
+ "apierror-noimageredirect": "Du har ikke tillatelse til å opprette bildeomdirigeringer.",
+ "apierror-nosuchlogid": "Det er ingen loggoppføring med ID $1.",
+ "apierror-nosuchpageid": "Det er ingen side med ID $1.",
+ "apierror-nosuchrcid": "Det er ingen nylig endring med ID $1.",
+ "apierror-nosuchrevid": "Det er ingen revisjon med ID $1.",
+ "apierror-nosuchsection": "Det er ingen seksjon $1.",
+ "apierror-nosuchsection-what": "Det er ingen seksjon $1 i $2.",
+ "apierror-nosuchuserid": "Det er ingen bruker med ID $1.",
+ "apierror-notarget": "Du har ikke angitt et gyldig mål for denne handlingen.",
+ "apierror-notpatrollable": "Revisjonen r$1 kan ikke patruljeres fordi den er for gammel.",
+ "apierror-nouploadmodule": "Ingen opplastingsmodul satt.",
"apierror-offline": "Kunne ikke fortsette på grunn av tilkoblingsproblemer. Sjekk at internettforbindelsen din virker og prøv igjen.",
+ "apierror-opensearch-json-warnings": "Advarsel kan ikke representeres OpenSearch JSON-format.",
+ "apierror-pagecannotexist": "Navnerommet tillater ikke faktiske sider.",
+ "apierror-pagedeleted": "Siden har blitt slettet siden du hentet tidsstempelet dens.",
+ "apierror-pagelang-disabled": "Endring av sidespråk tillates ikke på denne wikien.",
+ "apierror-paramempty": "Parameteren <var>$1</var> kan ikke være tom.",
+ "apierror-parsetree-notwikitext": "<kbd>prop=parsetree</kbd> støttes kut for wikitekstinnhold.",
+ "apierror-parsetree-notwikitext-title": "<kbd>prop=parsetree</kbd> støttes kun for wikitekstinnhold. $1 bruker innholdsmodellen $2.",
+ "apierror-pastexpiry": "Utløpstiden «$1» er i fortiden.",
+ "apierror-permissiondenied": "Du har ikke tillatelse til å $1.",
"apierror-permissiondenied-generic": "Tilgang nektet.",
+ "apierror-permissiondenied-patrolflag": "Du trenger rettigheten <code>patrol</code> eller <code>patrolmarks</code> for å be om patruljert-flagget.",
+ "apierror-permissiondenied-unblock": "Du har ikke tillatelse til å avblokkere brukere.",
+ "apierror-prefixsearchdisabled": "Prefikssøk er slått av i Miser-modus.",
+ "apierror-protect-invalidaction": "Ugyldig beskyttelsestype «$1».",
+ "apierror-protect-invalidlevel": "Ugyldig beskyttelsesnivå «$1».",
+ "apierror-readapidenied": "Du må ha lesetilgang for å bruke denne modulen.",
+ "apierror-readonly": "Wikien er for tiden skrivebeskyttet.",
+ "apierror-revwrongpage": "r$1 er ikke en revisjon av $2.",
+ "apierror-searchdisabled": "<var>$1</var>-søk er slått av.",
+ "apierror-sectionreplacefailed": "Kunne ikke flette oppdatert seksjon.",
+ "apierror-sectionsnotsupported": "Seksjoner støttes ikke for innholdsmodellen $1.",
+ "apierror-sectionsnotsupported-what": "Seksjoner støttes ikke av $1.",
+ "apierror-siteinfo-includealldenied": "Kan ikke vise alle tjenernes info med mindre </var>$wgShowHostNames</var> er sann.",
+ "apierror-sizediffdisabled": "Størrelsesforskjell er slått av i Miser-modus.",
"apierror-timeout": "Tjeneren svarte ikke innenfor forventet tid.",
"apiwarn-validationfailed": "Bekreftelsesfeil <kbd>$1</kbd>: $2"
}
diff --git a/www/wiki/includes/api/i18n/nl.json b/www/wiki/includes/api/i18n/nl.json
index ff99dff7..d0148feb 100644
--- a/www/wiki/includes/api/i18n/nl.json
+++ b/www/wiki/includes/api/i18n/nl.json
@@ -15,128 +15,129 @@
"Edoderoo",
"Lemondoge",
"Hex",
- "Mainframe98"
+ "Mainframe98",
+ "Southparkfan"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentatie]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-maillijst]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-aankondigingen]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & verzoeken]\n</div>\n<strong>Status:</strong> Alle functies die op deze pagina worden weergegeven horen te werken. Aan de API wordt actief gewerkt, en deze kan gewijzigd worden. Abonneer u op de [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-maillijst mediawiki-api-announce] voor meldingen over aanpassingen.\n\n<strong>Foutieve verzoeken:</strong> als de API foutieve verzoeken ontvangt, wordt er geantwoord met een HTTP-header met de sleutel \"MediaWiki-API-Error\" en daarna worden de waarde van de header en de foutcode op dezelfde waarde ingesteld. Zie [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Foutmeldingen en waarschuwingen]] voor meer informatie.\n\n<strong>Testen:</strong> u kunt [[Special:ApiSandbox|eenvoudig API-verzoeken testen]].",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentatie]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-maillijst]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-aankondigingen]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & verzoeken]\n</div>\n<strong>Status:</strong> De MediaWiki API is een stabiele interface die actief ondersteund en verbeterd wordt. Hoewel we het proberen te voorkomen, is het mogelijk dat er soms wijzigingen worden aangebracht die bepaalde API-verzoek kunnen verhinderen; abonneer u op de [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-maillijst mediawiki-api-announce] voor meldingen over wijzigingen.\n\n<strong>Foutieve verzoeken:</strong> als de API foutieve verzoeken ontvangt, wordt er geantwoord met een HTTP-header met de sleutel \"MediaWiki-API-Error\" en daarna worden de waarde van de header en de foutcode op dezelfde waarde ingesteld. Zie [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Foutmeldingen en waarschuwingen]] voor meer informatie.\n\n<p class=\"mw-apisandbox-link\"><strong>Testen:</strong> u kunt [[Special:ApiSandbox|eenvoudig API-verzoeken testen]].</p>",
"apihelp-main-param-action": "Welke handeling uit te voeren.",
"apihelp-main-param-format": "De opmaak van de uitvoer.",
- "apihelp-main-param-maxlag": "De maximale vertraging kan gebruikt worden als MediaWiki is geïnstalleerd op een databasecluster die gebruik maakt van replicatie. Om te voorkomen dat handelingen nog meer databasereplicatievertraging veroorzaken, kan deze parameter er voor zorgen dat de client wacht totdat de replicatievertraging lager is dan de aangegeven waarde. In het geval van buitensporige vertraging, wordt de foutcode <samp>maxlag</samp> teruggegeven met een bericht als <samp>Waiting for $host: $lag seconds lagged</samp>.<br />Zie [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Handleiding:Maxlag parameter]] voor meer informatie.",
+ "apihelp-main-param-maxlag": "De maximale vertraging kan gebruikt worden als MediaWiki is geïnstalleerd op een databasecluster die gebruik maakt van replicatie. Om te voorkomen dat handelingen nog meer databasereplicatievertraging veroorzaken, kan deze parameter ervoor zorgen dat de client wacht totdat de replicatievertraging lager is dan de aangegeven waarde. In geval van buitensporige vertraging wordt de foutcode <samp>maxlag</samp> teruggegeven met een bericht als <samp>Waiting for $host: $lag seconds lagged</samp>.<br />Zie [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Handleiding:Maxlag parameter]] voor meer informatie.",
"apihelp-main-param-smaxage": "Stelt de <code>s-maxage</code> HTTP cache controle header in op het aangegeven aantal seconden. Foutmeldingen komen nooit in de cache.",
"apihelp-main-param-maxage": "Stelt de <code>max-age</code> HTTP cache controle header in op het aangegeven aantal seconden. Foutmeldingen komen nooit in de cache.",
- "apihelp-main-param-assert": "Controleer of de gebruiker is aangemeld als <kbd>user</kbd> is meegegeven, en of de gebruiker het robotgebruikersrecht heeft als <kbd>bot</kbd> is meegegeven.",
- "apihelp-main-param-assertuser": "Bevestig dat de huidige gebruiker de genoemde gebruiker is.",
+ "apihelp-main-param-assert": "Controleer of de gebruiker is aangemeld indien <kbd>assert=user</kbd>, of het botgebruikersrecht heeft indien <kbd>assert=bot</kbd>.",
+ "apihelp-main-param-assertuser": "Controleer of de huidige gebruiker de genoemde gebruiker is.",
"apihelp-main-param-requestid": "Elke waarde die hier gegeven wordt, wordt aan het antwoord toegevoegd. Dit kan gebruikt worden om verzoeken te onderscheiden.",
- "apihelp-main-param-servedby": "Voeg de hostnaam van de server die de aanvraag heeft afgehandeld toe aan het antwoord.",
- "apihelp-main-param-curtimestamp": "Huidige tijd aan het antwoord toevoegen.",
- "apihelp-main-param-responselanginfo": "Toon de talen gebruikt voor <var>uselang</var> en <var>errorlang</var> in het resultaat.",
- "apihelp-main-param-errorlang": "De taal om te gebruiken voor waarschuwingen en fouten. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> met <kbd>siprop=languages</kbd> toont een lijst van taalcodes, of stel <kbd>inhoud</kbd> in om gebruik te maken van de inhoudstaal van deze wiki, of stel <kbd>uselang</kbd> in om gebruik te maken van dezelfde waarde als de <var>uselang</var> parameter.",
- "apihelp-main-param-errorsuselocal": "Indien ingesteld maken foutmeldingen gebruik van lokaal-aangepaste berichten in de {{ns:MediaWiki}} naamruimte.",
+ "apihelp-main-param-servedby": "De hostnaam van de server die de aanvraag heeft afgehandeld aan de resultaten toevoegen.",
+ "apihelp-main-param-curtimestamp": "Huidige tijd aan de resultaten toevoegen.",
+ "apihelp-main-param-responselanginfo": "De voor <var>uselang</var> en <var>errorlang</var> gebruikte talen aan de resultaten toevoegen.",
+ "apihelp-main-param-errorlang": "De voor waarschuwingen en fouten te gebruiken taal. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> met <kbd>siprop=languages</kbd> geeft een lijst van taalcodes, of stel <kbd>content</kbd> in om de taal van de inhoud van deze wiki te gebruiken, of stel <kbd>uselang</kbd> in om dezelfde waarde als de parameter <var>uselang</var> te gebruiken.",
+ "apihelp-main-param-errorsuselocal": "Indien ingesteld maken foutmeldingen gebruik van lokaal-aangepaste berichten in de {{ns:MediaWiki}}-naamruimte.",
"apihelp-block-summary": "Gebruiker blokkeren.",
- "apihelp-block-param-user": "Gebruikersnaam, IP-adres of IP-range om te blokkeren. Kan niet samen worden gebruikt me <var>$1userid</var>",
- "apihelp-block-param-userid": "Gebruikers-ID om te blokkeren. Kan niet worden gebruikt in combinatie met <var>$1user</var>.",
- "apihelp-block-param-expiry": "Vervaldatum. Kan relatief zijn (bijv. <kbd>5 months</kbd> of <kbd>2 weeks</kbd>) of absoluut (<kbd>2014-09-18T12:34:56Z</kbd>). Indien ingesteld op <kbd>infinite</kbd>, <kbd>indefinite</kbd>, of <kbd>never</kbd> verloopt de blokkade nooit.",
+ "apihelp-block-param-user": "Te blokkeren gebruikersnaam, IP-adres of IP-range. Kan niet in combinatie met <var>$1userid</var> gebruikt worden.",
+ "apihelp-block-param-userid": "Te blokkeren gebruikers-ID. Kan niet in combinatie met <var>$1user</var> gebruikt worden.",
+ "apihelp-block-param-expiry": "Vervaldatum. Kan relatief zijn (bijv. <kbd>5 months</kbd> of <kbd>2 weeks</kbd>) of absoluut (bijv. <kbd>2014-09-18T12:34:56Z</kbd>). Indien ingesteld op <kbd>infinite</kbd>, <kbd>indefinite</kbd> of <kbd>never</kbd> zal de blokkade nooit verlopen.",
"apihelp-block-param-reason": "Reden voor blokkade.",
- "apihelp-block-param-anononly": "Alleen anonieme gebruikers blokkeren (uitschakelen van anonieme bewerkingen via dit IP-adres)",
- "apihelp-block-param-nocreate": "Voorkom registeren van accounts.",
- "apihelp-block-param-autoblock": "Blokkeer automatisch het laatst gebruikte IP-adres en ieder volgend IP-adres van waaruit ze proberen aan te melden.",
- "apihelp-block-param-noemail": "Gebruiker weerhouden van het sturen van e-mail. (Vereist het <code>blockemail</code> recht).",
- "apihelp-block-param-hidename": "Verberg de gebruikersnaam uit het blokkeerlogboek. (Vereist het <code>hideuser</code> recht).",
- "apihelp-block-param-allowusertalk": "De gebruiker toestaan om hun eigen overlegpagina te bewerken (afhankelijk van <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).",
- "apihelp-block-param-reblock": "De huidige blokkade aanpassen als de gebruiker al geblokkeerd is.",
- "apihelp-block-param-watchuser": "De gebruikerspagina en overlegpagina van de gebruiker of het IP-adres volgen.",
- "apihelp-block-param-tags": "Wijzigingslabels om toe te passen op de regel in het blokkeerlogboek.",
- "apihelp-block-example-ip-simple": "Het IP-adres <kbd>192.0.2.5</kbd> voor drie dagen blokkeren met <kbd>First strike</kbd> als opgegeven reden.",
- "apihelp-block-example-user-complex": "Blokkeer gebruiker<kbd>Vandal</kbd> voor altijd met reden <kbd>Vandalism</kbd> en voorkom het aanmaken van nieuwe accounts en het versturen van email",
- "apihelp-changeauthenticationdata-example-password": "Poging tot het wachtwoord van de huidige gebruiker te veranderen naar <kbd>ExamplePassword</kbd>.",
- "apihelp-checktoken-summary": "Controleer de geldigheid van een token van <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.",
- "apihelp-checktoken-param-type": "Tokentype wordt getest.",
- "apihelp-checktoken-param-token": "Token om te controleren.",
- "apihelp-checktoken-param-maxtokenage": "Maximum levensduur van de token, in seconden.",
- "apihelp-checktoken-example-simple": "Test de geldigheid van een <kbd>csrf</kbd> token.",
- "apihelp-clearhasmsg-summary": "Wist de <code>hasmsg</code> vlag voor de huidige gebruiker.",
- "apihelp-clearhasmsg-example-1": "Wis de <code>hasmsg</code> vlag voor de huidige gebruiker.",
- "apihelp-clientlogin-summary": "Log in op de wiki met behulp van de interactieve flow.",
- "apihelp-clientlogin-example-login": "Start het inlogproces op de wiki als gebruiker <kbd>Example</kbd> met wachtwoord <kbd>ExamplePassword</kbd>.",
- "apihelp-compare-summary": "Toon het verschil tussen 2 pagina's.",
- "apihelp-compare-extended-description": "Een versienummer, een paginatitel of een pagina-ID is vereist voor zowel de \"from\" en \"to\" parameter.",
- "apihelp-compare-param-fromtitle": "Eerste paginanaam om te vergelijken.",
- "apihelp-compare-param-fromid": "Eerste pagina-ID om te vergelijken.",
- "apihelp-compare-param-fromrev": "Eerste versie om te vergelijken.",
- "apihelp-compare-param-totitle": "Tweede paginanaam om te vergelijken.",
- "apihelp-compare-param-toid": "Tweede pagina-ID om te vergelijken.",
- "apihelp-compare-param-torev": "Tweede versie om te vergelijken.",
- "apihelp-createaccount-summary": "Nieuwe gebruikersaccount aanmaken.",
- "apihelp-createaccount-example-create": "Start het proces voor het aanmaken van de gebruiker <kbd>Example</kbd> met het wachtwoord <kbd>ExamplePassword</kbd>.",
+ "apihelp-block-param-anononly": "Alleen anonieme gebruikers blokkeren (d.w.z. anonieme bewerkingen via dit IP-adres uitschakelen).",
+ "apihelp-block-param-nocreate": "Het aanmaken van accounts verhinderen.",
+ "apihelp-block-param-autoblock": "Automatisch het laatst gebruikte IP-adres blokkeren, en tevens ieder volgend IP-adres waarvanaf de gebruiker probeert aan te melden.",
+ "apihelp-block-param-noemail": "Het sturen van e-mail via wiki verhinderen. (Vereist het <code>blockemail</code>-recht.)",
+ "apihelp-block-param-hidename": "De gebruikersnaam in het blokkeerlogboek verbergen. (Vereist het <code>hideuser</code>-recht.)",
+ "apihelp-block-param-allowusertalk": "De gebruiker toestaan om de eigen overlegpagina te bewerken (afhankelijk van <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).",
+ "apihelp-block-param-reblock": "De huidige blokkade overschrijven indien de gebruiker al geblokkeerd is.",
+ "apihelp-block-param-watchuser": "De gebruikers- en overlegpagina's van de gebruiker of het IP-adres volgen.",
+ "apihelp-block-param-tags": "De labels voor de regel in het blokkeerlogboek wijzigen.",
+ "apihelp-block-example-ip-simple": "IP-adres <kbd>192.0.2.5</kbd> voor drie dagen blokkeren met als reden <kbd>First strike</kbd>.",
+ "apihelp-block-example-user-complex": "Gebruiker <kbd>Vandal</kbd> voor onbepaalde tijd blokkeren met als reden <kbd>Vandalism</kbd>, en verhinder het aanmaken van nieuwe accounts en het versturen van e-mail.",
+ "apihelp-changeauthenticationdata-example-password": "Poging om het wachtwoord van de huidige gebruiker te veranderen in <kbd>ExamplePassword</kbd>.",
+ "apihelp-checktoken-summary": "De geldigheid van een token van <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> controleren.",
+ "apihelp-checktoken-param-type": "Het type token dat getest wordt.",
+ "apihelp-checktoken-param-token": "Te testen token.",
+ "apihelp-checktoken-param-maxtokenage": "Maximale levensduur van de token, in seconden.",
+ "apihelp-checktoken-example-simple": "Test de geldigheid van een <kbd>csrf</kbd>-token.",
+ "apihelp-clearhasmsg-summary": "Wist de <code>hasmsg</code>-vlag voor de huidige gebruiker.",
+ "apihelp-clearhasmsg-example-1": "De <code>hasmsg</code>-vlag voor de huidige gebruiker wissen.",
+ "apihelp-clientlogin-summary": "Bij de wiki aanmelden met behulp van de interactieve flow.",
+ "apihelp-clientlogin-example-login": "Start het aanmeldproces als gebruiker <kbd>Example</kbd> met wachtwoord <kbd>ExamplePassword</kbd>.",
+ "apihelp-compare-summary": "Het verschil tussen twee pagina's ophalen.",
+ "apihelp-compare-extended-description": "Voor zowel \"from\" als \"to\" moet een revisienummer, paginatitel, pagina-ID, tekst of relatieve referentie ingegeven worden.",
+ "apihelp-compare-param-fromtitle": "Eerste te vergelijken paginanaam.",
+ "apihelp-compare-param-fromid": "Eerste te vergelijken pagina-ID.",
+ "apihelp-compare-param-fromrev": "Eerste te vergelijken revisie.",
+ "apihelp-compare-param-totitle": "Tweede te vergelijken paginanaam.",
+ "apihelp-compare-param-toid": "Tweede te vergelijken pagina-ID.",
+ "apihelp-compare-param-torev": "Tweede te vergelijken revisie.",
+ "apihelp-createaccount-summary": "Een nieuw gebruikersaccount aanmaken.",
+ "apihelp-createaccount-example-create": "Start het proces voor het aanmaken van account <kbd>Example</kbd> met wachtwoord <kbd>ExamplePassword</kbd>.",
"apihelp-createaccount-param-name": "Gebruikersnaam.",
"apihelp-createaccount-param-password": "Wachtwoord (genegeerd als <var>$1mailpassword</var> is ingesteld).",
"apihelp-createaccount-param-domain": "Domein voor externe authentificatie (optioneel).",
"apihelp-createaccount-param-email": "E-mailadres van de gebruiker (optioneel).",
"apihelp-createaccount-param-realname": "Echte naam van de gebruiker (optioneel).",
- "apihelp-createaccount-param-reason": "Optionele reden voor het aanmaken van het account voor in het logboek.",
- "apihelp-createaccount-param-language": "Taalcode om als standaard in te stellen voor de gebruiker (optioneel, standaard de inhoudstaal).",
- "apihelp-createaccount-example-pass": "Maak gebruiker <kbd>testuser</kbd> aan met wachtwoord <kbd>test123</kbd>.",
- "apihelp-createaccount-example-mail": "Maak gebruiker <kbd>testmailuser</kbd> aan en e-mail een willekeurig gegenereerd wachtwoord.",
+ "apihelp-createaccount-param-reason": "Optionele reden voor het aanmaken van het account, om in de logboeken te zetten.",
+ "apihelp-createaccount-param-language": "Taalcode om als standaard voor de gebruiker in te stellen (optioneel, standaard ingesteld op de inhoudstaal).",
+ "apihelp-createaccount-example-pass": "Account <kbd>testuser</kbd> met wachtwoord <kbd>test123</kbd> aanmaken.",
+ "apihelp-createaccount-example-mail": "Account <kbd>testmailuser</kbd> aanmaken en een willekeurig gegenereerd wachtwoord e-mailen.",
"apihelp-delete-summary": "Een pagina verwijderen.",
- "apihelp-delete-param-title": "Titel van de pagina om te verwijderen. Kan niet samen worden gebruikt met <var>$1pageid</var>.",
- "apihelp-delete-param-pageid": "ID van de pagina om te verwijderen. Kan niet samen worden gebruikt met <var>$1title</var>.",
- "apihelp-delete-param-reason": "Reden voor verwijdering. Wanneer dit niet is opgegeven wordt een automatisch gegenereerde reden gebruikt.",
- "apihelp-delete-param-tags": "Wijzigingslabels om toe te passen op de regel in het verwijderlogboek.",
+ "apihelp-delete-param-title": "Titel van de te verwijderen pagina. Kan niet in combinatie met <var>$1pageid</var> gebruikt worden.",
+ "apihelp-delete-param-pageid": "Pagina-ID van de te verwijderen pagina. Kan niet in combinatie met <var>$1title</var> gebruikt worden.",
+ "apihelp-delete-param-reason": "Reden voor de verwijdering. Indien niet opgegeven, zal er een automatisch gegenereerde reden gebruikt worden.",
+ "apihelp-delete-param-tags": "De labels voor de regel in het verwijderlogboek wijzigen.",
"apihelp-delete-param-watch": "De pagina aan de volglijst van de huidige gebruiker toevoegen.",
"apihelp-delete-param-unwatch": "De pagina van de volglijst van de huidige gebruiker verwijderen.",
"apihelp-delete-example-simple": "Verwijder <kbd>Main Page</kbd>.",
"apihelp-delete-example-reason": "Verwijder <kbd>Main Page</kbd> met als reden <kbd>Preparing for move</kbd>.",
"apihelp-disabled-summary": "Deze module is uitgeschakeld.",
- "apihelp-edit-summary": "Aanmaken en bewerken van pagina's.",
- "apihelp-edit-param-title": "Naam van de pagina om te bewerken. Kan niet gebruikt worden samen met <var>$1pageid</var>.",
- "apihelp-edit-param-pageid": "ID van de pagina om te bewerken. Kan niet samen worden gebruikt met <var>$1title</var>.",
- "apihelp-edit-param-sectiontitle": "De naam van de nieuwe sectie.",
+ "apihelp-edit-summary": "Pagina's aanmaken en bewerken.",
+ "apihelp-edit-param-title": "Naam van de te bewerken pagina. Kan niet in combinatie met <var>$1pageid</var> gebruikt worden.",
+ "apihelp-edit-param-pageid": "Pagina-ID van de te bewerken pagina. Kan niet in combinatie met <var>$1title</var> gebruikt worden.",
+ "apihelp-edit-param-sectiontitle": "De naam van een nieuwe sectie.",
"apihelp-edit-param-text": "Pagina-inhoud.",
- "apihelp-edit-param-tags": "Wijzigingslabels om aan de versie toe te voegen.",
+ "apihelp-edit-param-tags": "De labels voor de revisie wijzigen.",
"apihelp-edit-param-minor": "Kleine bewerking.",
- "apihelp-edit-param-notminor": "Geen kleine bewerking.",
- "apihelp-edit-param-bot": "Deze bewerking markeren als gedaan door een robot.",
+ "apihelp-edit-param-notminor": "Niet-kleine bewerking.",
+ "apihelp-edit-param-bot": "Deze bewerking markeren als een botbewerking.",
"apihelp-edit-param-createonly": "De pagina niet bewerken als die al bestaat.",
"apihelp-edit-param-nocreate": "Een foutmelding geven als de pagina niet bestaat.",
"apihelp-edit-param-watch": "Voeg de pagina toe aan de volglijst van de huidige gebruiker.",
"apihelp-edit-param-unwatch": "Verwijder de pagina van de volglijst van de huidige gebruiker.",
- "apihelp-edit-param-md5": "De MD5-hash van de $1text parameter, of de $1prependtext en $1appendtext parameters samengevoegd. Indien ingesteld, wordt de bewerking niet gemaakt, tenzij de hash juist is.",
+ "apihelp-edit-param-md5": "De MD5-hash van de $1text parameter, of de $1prependtext en $1appendtext parameters samengevoegd. Indien ingesteld, wordt de bewerking niet gemaakt tenzij de hash juist is.",
"apihelp-edit-param-prependtext": "Voeg deze tekst toe aan het begin van de pagina. Overschrijft $1text.",
- "apihelp-edit-param-appendtext": "Voeg deze tekst toe aan het begin van de pagina. Overschrijft $1text.\n\nGebruik $1section=new in plaats van deze parameter om een nieuw kopje toe te voegen.",
- "apihelp-edit-param-undo": "Maak deze versie ongedaan. Overschrijft $1text, $1prependtext en $1appendtext.",
- "apihelp-edit-param-undoafter": "Maak alle versies vanaf $1undo to deze ongedaan maken. Indien niet ingesteld wordt slechts één versie ongedaan gemaakt.",
+ "apihelp-edit-param-appendtext": "Voeg deze tekst toe aan het eind van de pagina. Overschrijft $1text.\n\nGebruik $1section=new om een nieuwe sectie toe te voegen, in plaats van deze parameter.",
+ "apihelp-edit-param-undo": "Maak deze revisie ongedaan. Overschrijft $1text, $1prependtext en $1appendtext.",
+ "apihelp-edit-param-undoafter": "Maak alle revisies vanaf $1undo tot deze ongedaan. Indien niet ingesteld wordt slechts één revisie ongedaan gemaakt.",
"apihelp-edit-param-redirect": "Doorverwijzingen automatisch oplossen.",
"apihelp-edit-param-contentmodel": "Inhoudsmodel van de nieuwe inhoud.",
- "apihelp-edit-param-token": "Het token moet altijd worden verzonden als de laatste parameter, of tenminste na de $1text parameter.",
+ "apihelp-edit-param-token": "De token moet altijd als de laatste parameter worden verzonden, of in ieder geval na de $1text parameter.",
"apihelp-edit-example-edit": "Een pagina bewerken.",
- "apihelp-edit-example-prepend": "Voeg <kbd>__NOTOC__</kbd> toe aan het begin van een pagina.",
- "apihelp-edit-example-undo": "Versies 13579 tot 13585 ongedaan maken met automatische beschrijving.",
+ "apihelp-edit-example-prepend": "Voeg <kbd>_&#95;NOTOC_&#95;</kbd> toe aan het begin van een pagina.",
+ "apihelp-edit-example-undo": "Revisies 13579 tot 13585 ongedaan maken met automatische beschrijving.",
"apihelp-emailuser-summary": "Gebruiker e-mailen.",
"apihelp-emailuser-param-target": "Gebruiker naar wie de e-mail moet worden gestuurd.",
"apihelp-emailuser-param-subject": "Onderwerpkoptekst.",
"apihelp-emailuser-param-text": "E-mailtekst.",
- "apihelp-emailuser-param-ccme": "Mij een kopie sturen van deze e-mail.",
- "apihelp-emailuser-example-email": "Stuur een e-mail naar de gebruiker <kbd>WikiSysop</kbd> met de tekst <kbd>Inhoud</kbd>.",
+ "apihelp-emailuser-param-ccme": "Mij een kopie van deze e-mail sturen.",
+ "apihelp-emailuser-example-email": "Stuur een e-mail naar gebruiker <kbd>WikiSysop</kbd> met de tekst <kbd>Content</kbd>.",
"apihelp-expandtemplates-param-title": "Paginanaam.",
- "apihelp-expandtemplates-param-text": "Wikitekst om om te zetten.",
- "apihelp-expandtemplates-paramvalue-prop-wikitext": "De uitgevulde wikitekst.",
+ "apihelp-expandtemplates-param-text": "Om te zetten wikitekst.",
+ "apihelp-expandtemplates-paramvalue-prop-wikitext": "De uitgewerkte wikitekst.",
"apihelp-expandtemplates-paramvalue-prop-ttl": "De maximale tijdsduur waarna de cache van het resultaat moet worden weggegooid.",
- "apihelp-feedcontributions-summary": "Haalt de feed van de gebruikersbijdragen op.",
+ "apihelp-feedcontributions-summary": "Retourneert een feed van gebruikersbijdragen.",
"apihelp-feedcontributions-param-feedformat": "De indeling van de feed.",
- "apihelp-feedcontributions-param-user": "De gebruiker om de bijdragen voor te verkrijgen.",
+ "apihelp-feedcontributions-param-user": "De gebruikers voor wie de bijdragen verkregen moeten worden.",
"apihelp-feedcontributions-param-year": "Van jaar (en eerder).",
"apihelp-feedcontributions-param-month": "Van maand (en eerder).",
"apihelp-feedcontributions-param-deletedonly": "Alleen verwijderde bijdragen weergeven.",
- "apihelp-feedcontributions-param-toponly": "Alleen bewerkingen die de nieuwste versies zijn weergeven.",
- "apihelp-feedcontributions-param-newonly": "Alleen bewerkingen die nieuwe pagina's aanmaken weergeven.",
+ "apihelp-feedcontributions-param-toponly": "Alleen bewerkingen weergeven die de nieuwste revisies zijn.",
+ "apihelp-feedcontributions-param-newonly": "Alleen bewerkingen weergeven die nieuwe pagina's hebben gestart.",
"apihelp-feedcontributions-param-hideminor": "Verberg kleine bewerkingen.",
- "apihelp-feedcontributions-param-showsizediff": "Toon het verschil in grootte tussen versies.",
+ "apihelp-feedcontributions-param-showsizediff": "Toon het verschil in grootte tussen revisies.",
"apihelp-feedcontributions-example-simple": "Toon bijdragen voor gebruiker <kbd>Example</kbd>.",
"apihelp-feedrecentchanges-param-feedformat": "De indeling van de feed.",
"apihelp-feedrecentchanges-param-namespace": "Naamruimte om de resultaten tot te beperken.",
"apihelp-feedrecentchanges-param-invert": "Alle naamruimten behalve de geselecteerde.",
"apihelp-feedrecentchanges-param-days": "Aantal dagen om de resultaten tot te beperken.",
- "apihelp-feedrecentchanges-param-limit": "Het maximaal aantal weer te geven resultaten.",
+ "apihelp-feedrecentchanges-param-limit": "Het maximale aantal weer te geven resultaten.",
"apihelp-feedrecentchanges-param-hideminor": "Kleine wijzigingen verbergen.",
"apihelp-feedrecentchanges-param-hidebots": "Wijzigingen gedaan door bots verbergen.",
"apihelp-feedrecentchanges-param-hideanons": "Wijzigingen gedaan door anonieme gebruikers verbergen.",
@@ -146,199 +147,200 @@
"apihelp-feedrecentchanges-param-hidecategorization": "Wijzigingen in categorielidmaatschap verbergen.",
"apihelp-feedrecentchanges-param-tagfilter": "Filteren op label.",
"apihelp-feedrecentchanges-example-simple": "Recente wijzigingen weergeven.",
- "apihelp-feedrecentchanges-example-30days": "Recente wijzigingen van de afgelopen 30 dagen weergeven.",
+ "apihelp-feedrecentchanges-example-30days": "Wijzigingen van de afgelopen 30 dagen weergeven.",
"apihelp-feedwatchlist-param-feedformat": "De indeling van de feed.",
"apihelp-filerevert-summary": "Een oude versie van een bestand terugplaatsen.",
- "apihelp-filerevert-param-filename": "Doel bestandsnaam, zonder het Bestand: voorvoegsel.",
+ "apihelp-filerevert-param-filename": "Bestandsnaam, zonder het Bestand: voorvoegsel.",
"apihelp-filerevert-param-comment": "Opmerking voor het uploaden.",
"apihelp-filerevert-example-revert": "Zet <kbd>Wiki.png</kbd> terug naar de versie van <kbd>2011-03-05T15:27:40Z</kbd>.",
- "apihelp-help-summary": "Toon help voor de opgegeven modules.",
- "apihelp-help-param-helpformat": "Indeling van de help uitvoer.",
+ "apihelp-help-summary": "Toon hulp voor de opgegeven modules.",
+ "apihelp-help-param-helpformat": "Indeling van de hulpuitvoer.",
"apihelp-help-example-main": "Hulp voor de hoofdmodule.",
"apihelp-help-example-submodules": "Hulp voor <kbd>action=query</kbd> en alle submodules.",
- "apihelp-help-example-recursive": "Alle hulp op een pagina.",
- "apihelp-help-example-help": "Help voor de help-module zelf.",
+ "apihelp-help-example-recursive": "Alle hulp op één pagina.",
+ "apihelp-help-example-help": "Hulp voor de hulpmodule zelf.",
"apihelp-imagerotate-summary": "Een of meerdere afbeeldingen draaien.",
"apihelp-imagerotate-param-rotation": "Aantal graden om de afbeelding met de klok mee te draaien.",
- "apihelp-imagerotate-param-tags": "Labels om toe te voegen aan de regel in het uploadlogboek.",
+ "apihelp-imagerotate-param-tags": "Labels voor de regel in het uploadlogboek.",
"apihelp-imagerotate-example-simple": "Roteer <kbd>File:Example.png</kbd> met <kbd>90</kbd> graden.",
"apihelp-imagerotate-example-generator": "Roteer alle afbeeldingen in <kbd>Category:Flip</kbd> met <kbd>180</kbd> graden.",
- "apihelp-import-summary": "Importeer een pagina van een andere wiki, of van een XML bestand.",
- "apihelp-import-extended-description": "Merk op dat de HTTP POST moet worden uitgevoerd als bestandsupload (bijv. door middel van multipart/form-data) wanneer een bestand wordt verstuurd voor de <var>xml</var> parameter.",
- "apihelp-import-param-summary": "Importsamenvatting voor het logboek.",
+ "apihelp-import-summary": "Importeer een pagina van een andere wiki, of van een XML-bestand.",
+ "apihelp-import-extended-description": "Merk op dat de HTTP POST moet worden uitgevoerd als bestandsupload (d.w.z. door middel van multipart/form-data) wanneer een bestand wordt verstuurd voor de <var>xml</var> parameter.",
+ "apihelp-import-param-summary": "Importeersamenvatting voor het logboek.",
"apihelp-import-param-xml": "Geüpload XML-bestand.",
- "apihelp-import-param-interwikisource": "Voor interwiki imports: wiki om van te importeren.",
- "apihelp-import-param-namespace": "Importeren in deze naamruimte. Can niet samen gebruikt worden met <var>$1rootpage</var>.",
- "apihelp-import-param-rootpage": "Importeren als subpagina van deze pagina. Kan niet samen met <var>$1namespace</var> gebruikt worden.",
+ "apihelp-import-param-interwikisource": "Voor interwiki-imports: wiki om van te importeren.",
+ "apihelp-import-param-namespace": "Naar deze naamruimte importeren. Kan niet in combinatie met <var>$1rootpage</var> gebruikt worden.",
+ "apihelp-import-param-rootpage": "Als subpagina van deze pagina importeren. Kan niet in combinatie met <var>$1namespace</var> gebruikt worden.",
"apihelp-import-example-import": "Importeer [[meta:Help:ParserFunctions]] in naamruimte 100 met de volledige geschiedenis.",
"apihelp-login-param-name": "Gebruikersnaam.",
"apihelp-login-param-password": "Wachtwoord.",
"apihelp-login-param-domain": "Domein (optioneel).",
- "apihelp-login-example-login": "Aanmelden",
+ "apihelp-login-example-login": "Aanmelden.",
"apihelp-logout-summary": "Afmelden en sessiegegevens wissen.",
- "apihelp-logout-example-logout": "Meldt de huidige gebruiker af.",
- "apihelp-managetags-param-tag": "Label om aan te maken, te activeren of te deactiveren. Voor het aanmaken van een label, mag het niet bestaan. Voor het verwijderen van een label, moet het bestaan. Voor het activeren van een label, moet het bestaan en mag het niet gebruikt worden door een uitbreiding. Voor het deactiveren van een label, moet het gebruikt worden en handmatig gedefinieerd zijn.",
+ "apihelp-logout-example-logout": "Meld de huidige gebruiker af.",
+ "apihelp-managetags-param-tag": "Aan te maken, te verwijderen, te activeren of te deactiveren label. Voor het aanmaken mag het label nog niet bestaan. Voor het verwijderen moet het label bestaan. Voor het activeren moet het label bestaan en mag het niet door een uitbreiding gebruikt worden. Voor het deactiveren moet het label gebruikt worden en handmatig gedefinieerd zijn.",
"apihelp-managetags-example-create": "Maak een label met de naam <kbd>spam</kbd> aan met als reden <kbd>For use in edit patrolling</kbd>",
- "apihelp-managetags-example-delete": "Verwijder het <kbd>vandlaism</kbd> label met de reden <kbd>Misspelt</kbd>",
- "apihelp-mergehistory-summary": "Geschiedenis van pagina's samenvoegen.",
- "apihelp-mergehistory-param-reason": "Reden voor samenvoegen van de geschiedenis.",
- "apihelp-mergehistory-example-merge": "Voeg de hele geschiedenis van <kbd>Oldpage</kbd> samen met <kbd>Newpage</kbd>.",
+ "apihelp-managetags-example-delete": "Verwijder het label <kbd>vandlaism</kbd> met als reden <kbd>Misspelt</kbd>",
+ "apihelp-mergehistory-summary": "Geschiedenissen van pagina's samenvoegen.",
+ "apihelp-mergehistory-param-reason": "Reden voor de samenvoeging van de geschiedenissen.",
+ "apihelp-mergehistory-example-merge": "Voeg de hele geschiedenis van <kbd>Oldpage</kbd> samen in <kbd>Newpage</kbd>.",
"apihelp-move-summary": "Pagina hernoemen.",
"apihelp-move-param-to": "Nieuwe paginanaam.",
"apihelp-move-param-reason": "Reden voor de naamswijziging.",
"apihelp-move-param-movetalk": "Hernoem de overlegpagina, indien deze bestaat.",
"apihelp-move-param-noredirect": "Geen doorverwijzing achterlaten.",
- "apihelp-move-param-watch": "Pagina en de omleiding toevoegen aan de volglijst van de huidige gebruiker.",
+ "apihelp-move-param-watch": "Voeg de pagina en de doorverwijzing toe aan de volglijst van de huidige gebruiker.",
"apihelp-move-param-unwatch": "Verwijder de pagina en de doorverwijzing van de volglijst van de huidige gebruiker.",
"apihelp-move-param-watchlist": "De pagina onvoorwaardelijk toevoegen aan of verwijderen van de volglijst van de huidige gebruiker, gebruik voorkeuren of wijzig het volgen niet.",
"apihelp-move-param-ignorewarnings": "Eventuele waarschuwingen negeren.",
- "apihelp-move-example-move": "Hernoem <kbd>Badtitle</kbd> naar <kbd>Goodtitle</kbd> zonder een doorverwijzing te laten staan.",
- "apihelp-opensearch-summary": "Zoeken in de wiki met het OpenSearchprotocol.",
+ "apihelp-move-example-move": "Hernoem <kbd>Badtitle</kbd> naar <kbd>Goodtitle</kbd> zonder een doorverwijzing achter te laten.",
+ "apihelp-opensearch-summary": "In de wiki zoeken met behulp het OpenSearchprotocol.",
"apihelp-opensearch-param-search": "Zoektekst.",
- "apihelp-opensearch-param-limit": "Het maximaal aantal weer te geven resultaten.",
+ "apihelp-opensearch-param-limit": "Het maximale aantal weer te geven resultaten.",
"apihelp-opensearch-param-namespace": "Te doorzoeken naamruimten.",
"apihelp-opensearch-param-suggest": "Niets doen als <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> onwaar is.",
- "apihelp-opensearch-param-redirects": "Hoe om te gaan met doorverwijzingen:\n;return:Geef de doorverwijzing terug.\n;resolve:Geef de doelpagina terug. Kan minder dan de limiet $1 resultaten teruggeven.\nOm historische redenen is de standaardinstelling \"return\" voor <code>$1format=json<code> en \"resolve\" voor andere formaten.",
+ "apihelp-opensearch-param-redirects": "Hoe om te gaan met doorverwijzingen:\n;return:Retourneer de doorverwijzing.\n;resolve:Retourneer de doelpagina. Retourneert mogelijk minder dan $1limit resultaten.\nOm historische redenen is \"return\" de standaardinstelling voor <code>$1format=json</code>, en \"resolve\" voor andere formaten.",
"apihelp-opensearch-param-format": "Het uitvoerformaat.",
"apihelp-opensearch-param-warningsaserror": "Als er waarschuwingen zijn met <kbd>format=json</kbd>, geef dan een API-fout terug in plaats van deze te negeren.",
- "apihelp-opensearch-example-te": "Pagina's vinden die beginnen met <kbd>Te</kbd>.",
+ "apihelp-opensearch-example-te": "Vind pagina's die beginnen met <kbd>Te</kbd>.",
"apihelp-options-summary": "Voorkeuren van de huidige gebruiker wijzigen.",
- "apihelp-options-extended-description": "Alleen opties die zijn geregistreerd in core of in een van de geïnstalleerde uitbreidingen, of opties met de toetsen aangeduid met <code>userjs-</code> (bedoeld om te worden gebruikt door gebruikersscripts), kunnen worden ingesteld.",
- "apihelp-options-param-reset": "Zet de voorkeuren terug naar de standaard van de website.",
- "apihelp-options-param-resetkinds": "Lijst van de optiestypes die opnieuw ingesteld worden wanneer de optie <var>$1reset</var> is ingesteld.",
- "apihelp-options-param-change": "Lijst van wijzigingen, opgemaakt als <kbd>naam=waarde</kbd> (bijvoorbeeld <kbd>skin=vector</kbd>). Als er geen waarde wordt opgegeven (zelfs niet een is-gelijk teken), bijvoorbeeld <kbd>optienaam|andereoptie|...</kbd>, dan wordt de optie ingesteld op de standaardwaarde. Als een opgegeven waarde een sluisteken bevat (<kbd>|</kbd>), gebruik dan het [[Special:ApiHelp/main#main/datatypes|alternatieve scheidingsteken tussen meerdere waardes]] voor een juiste werking.",
- "apihelp-options-param-optionname": "De naam van de optie die moet worden ingesteld op de waarde gegeven door <var>$1optiewaarde</var>.",
+ "apihelp-options-extended-description": "Alleen opties die zijn geregistreerd in core of in een van de geïnstalleerde uitbreidingen, of opties met sleutels die beginnen met <code>userjs-</code> (bedoeld om door gebruikersscripts gebruikt te worden), kunnen worden ingesteld.",
+ "apihelp-options-param-reset": "Reset voorkeuren naar de standaard van de website.",
+ "apihelp-options-param-resetkinds": "Lijst van types van te resetten opties wanneer de optie <var>$1reset</var> is ingesteld.",
+ "apihelp-options-param-change": "Lijst van wijzigingen, opgemaakt als <kbd>naam=waarde</kbd> (bijv. <kbd>skin=vector</kbd>). Als er geen waarde wordt opgegeven (zelfs niet een isgelijkteken), bijvoorbeeld <kbd>optienaam|andereoptie|...</kbd>, dan wordt de optie ingesteld op de standaardwaarde. Als een opgegeven waarde een sluisteken bevat (<kbd>|</kbd>), gebruik dan het [[Special:ApiHelp/main#main/datatypes|alternatieve scheidingsteken tussen meerdere waardes]] voor een juiste werking.",
+ "apihelp-options-param-optionname": "De naam van de optie die moet worden ingesteld op de waarde gegeven door <var>$1optionvalue</var>.",
"apihelp-options-param-optionvalue": "De waarde voor de optie opgegeven door <var>$1optionname</var>.",
- "apihelp-options-example-reset": "Alle voorkeuren opnieuw instellen.",
- "apihelp-options-example-change": "Voorkeuren wijzigen voor <kbd>skin</kbd> en <kbd>hideminor</kbd>.",
+ "apihelp-options-example-reset": "Reset alle voorkeuren.",
+ "apihelp-options-example-change": "Wijzig <kbd>skin</kbd> en <kbd>hideminor</kbd> voorkeuren.",
"apihelp-paraminfo-summary": "Verkrijg informatie over API-modules.",
- "apihelp-parse-paramvalue-prop-categorieshtml": "Vraagt een HTML-versie van de categorieën op.",
+ "apihelp-parse-paramvalue-prop-categorieshtml": "Geeft de HTML-versie van de categorieën.",
"apihelp-parse-example-page": "Een pagina verwerken.",
- "apihelp-parse-example-text": "Wikitext verwerken.",
+ "apihelp-parse-example-text": "Wikitekst verwerken.",
+ "apihelp-parse-example-texttitle": "Wikitekst verwerken, waarbij de paginatitel opgeven is.",
"apihelp-parse-example-summary": "Een samenvatting verwerken.",
- "apihelp-patrol-summary": "Een pagina of versie markeren als gecontroleerd.",
- "apihelp-patrol-example-rcid": "Een recente wijziging markeren als gecontroleerd.",
- "apihelp-patrol-example-revid": "Een versie markeren als gecontroleerd.",
- "apihelp-protect-param-reason": "Reden voor opheffen van de beveiliging.",
- "apihelp-protect-example-protect": "Een pagina beveiligen",
+ "apihelp-patrol-summary": "Een pagina of revisie als gecontroleerd markeren.",
+ "apihelp-patrol-example-rcid": "Een recente wijziging als gecontroleerd markeren.",
+ "apihelp-patrol-example-revid": "Een revisie als gecontroleerd markeren.",
+ "apihelp-protect-param-reason": "Reden voor instellen of opheffen van de beveiliging.",
+ "apihelp-protect-example-protect": "Een pagina beveiligen.",
"apihelp-purge-param-forcelinkupdate": "Werk de koppelingstabellen bij.",
- "apihelp-purge-param-forcerecursivelinkupdate": "Werk de koppelingentabel bij, en werk de koppelingstabellen bij voor alle pagina's die gebruik maken van deze pagina als sjabloon.",
+ "apihelp-purge-param-forcerecursivelinkupdate": "Werk de koppelingentabel bij, en werk de koppelingstabellen bij voor alle pagina's die deze pagina als sjabloon gebruiken.",
"apihelp-query+allcategories-param-dir": "Richting om in te sorteren.",
"apihelp-query+allcategories-param-limit": "Hoeveel categorieën te tonen.",
"apihelp-query+allcategories-paramvalue-prop-size": "Voegt het aantal pagina's in de categorie toe.",
- "apihelp-query+allcategories-paramvalue-prop-hidden": "Markeert categorieën die verborgen zijn met <code>_&#95;HIDDENCAT_&#95;</code>",
- "apihelp-query+alldeletedrevisions-param-tag": "Alleen versies weergeven met dit label.",
- "apihelp-query+alldeletedrevisions-param-excludeuser": "Toon geen versies door deze gebruiker.",
- "apihelp-query+alldeletedrevisions-param-namespace": "Toon alleen pagina's in deze naamruimte.",
+ "apihelp-query+allcategories-paramvalue-prop-hidden": "Markeert categorieën die verborgen zijn met <code>_&#95;HIDDENCAT_&#95;</code>.",
+ "apihelp-query+alldeletedrevisions-param-tag": "Alleen versies met dit label weergeven.",
+ "apihelp-query+alldeletedrevisions-param-excludeuser": "Geen revisies door deze gebruiker weergeven.",
+ "apihelp-query+alldeletedrevisions-param-namespace": "Alleen pagina's in deze naamruimte weergeven.",
"apihelp-query+allfileusages-paramvalue-prop-title": "Voegt de titel van het bestand toe.",
"apihelp-query+allfileusages-param-limit": "Hoeveel items er in totaal moeten worden getoond.",
- "apihelp-query+allimages-example-recent": "Toon een lijst van recentlijk geüploade bestanden, vergelijkbaar met [[Special:NewFiles]].",
- "apihelp-query+alllinks-param-namespace": "De naamruimte om door te lopen.",
+ "apihelp-query+allimages-example-recent": "Toon een lijst van recentelijk geüploade bestanden, vergelijkbaar met [[Special:NewFiles]].",
+ "apihelp-query+alllinks-param-namespace": "De door te lopen naamruimte.",
"apihelp-query+alllinks-param-limit": "Hoeveel items er in totaal moeten worden getoond.",
- "apihelp-query+allmessages-param-enableparser": "Stel in om de parser in te schakelen, zorgt voor het voorverwerken van de wikitekst van een bericht (vervangen van magische woorden, de afhandeling van sjablonen, enzovoort).",
+ "apihelp-query+allmessages-param-enableparser": "Stel in om de parser in te schakelen, zorgt voor het voorverwerken van de wikitekst van een bericht (vervangen van magische woorden, afhandelen van sjablonen, enz.).",
"apihelp-query+allmessages-param-lang": "Toon berichten in deze taal.",
"apihelp-query+allmessages-param-from": "Toon berichten vanaf dit bericht.",
"apihelp-query+allmessages-param-to": "Toon berichten tot aan dit bericht.",
"apihelp-query+allredirects-summary": "Toon alle doorverwijzingen naar een naamruimte.",
- "apihelp-query+allrevisions-example-user": "Toon de laatste 50 bijdragen van de gebruiker <kbd>Example</kbd>.",
+ "apihelp-query+allrevisions-example-user": "Toon de laatste 50 bijdragen van gebruiker <kbd>Example</kbd>.",
"apihelp-query+mystashedfiles-paramvalue-prop-type": "Vraag het MIME- en mediatype van het bestand op.",
"apihelp-query+mystashedfiles-param-limit": "Hoeveel bestanden te tonen.",
"apihelp-query+allusers-param-excludegroup": "Sluit gebruikers in de gegeven groepen uit.",
- "apihelp-query+allusers-paramvalue-prop-blockinfo": "Voegt informatie over een actuale blokkade van de gebruiker toe.",
- "apihelp-query+allusers-paramvalue-prop-groups": "Toont de groepen waar de gebruiker in zit. Dit gebruikt meer serverbronnen en kan minder resultaten teruggeven dat de opgegeven limiet.",
- "apihelp-query+allusers-paramvalue-prop-implicitgroups": "Toon alle groepen de gebruiker automatisch in zit.",
- "apihelp-query+allusers-paramvalue-prop-rights": "Toon de rechten die de gebruiker heeft.",
+ "apihelp-query+allusers-paramvalue-prop-blockinfo": "Voegt informatie over een actuele blokkade van de gebruiker toe.",
+ "apihelp-query+allusers-paramvalue-prop-groups": "Toont de groepen waar de gebruiker in zit. Dit gebruikt meer serverbronnen en retourneert mogelijk minder resultaten dan de opgegeven limiet.",
+ "apihelp-query+allusers-paramvalue-prop-implicitgroups": "Toont alle groepen waar de gebruiker automatisch in zit.",
+ "apihelp-query+allusers-paramvalue-prop-rights": "Toont de rechten die de gebruiker heeft.",
"apihelp-query+allusers-paramvalue-prop-editcount": "Voegt het aantal bewerkingen van de gebruiker toe.",
"apihelp-query+allusers-paramvalue-prop-registration": "Voegt de registratiedatum van de gebruiker toe, indien beschikbaar (kan leeg zijn).",
"apihelp-query+allusers-param-witheditsonly": "Toon alleen gebruikers die bewerkingen hebben gemaakt.",
- "apihelp-query+allusers-param-activeusers": "Toon alleen gebruikers die actief zijn geweest in de laatste $1 {{PLURAL:$1|dag|dagen}}.",
+ "apihelp-query+allusers-param-activeusers": "Toon alleen gebruikers die actief zijn geweest in de laatste {{PLURAL:$1|dag|$1 dagen}}.",
"apihelp-query+allusers-example-Y": "Toon gebruikers vanaf <kbd>Y</kbd>.",
- "apihelp-query+authmanagerinfo-summary": "Haal informatie op over de huidige authentificatie status.",
+ "apihelp-query+authmanagerinfo-summary": "Haal informatie op over de huidige authentificatiestatus.",
"apihelp-query+backlinks-summary": "Vind alle pagina's die verwijzen naar de gegeven pagina.",
- "apihelp-query+backlinks-param-title": "Titel om op te zoeken. Kan niet worden gebruikt in combinatie met<var>$1pageid</var>.",
- "apihelp-query+backlinks-param-pageid": "Pagina ID om op te zoeken. Kan niet worden gebruikt in combinatie met <var>$1title</var>.",
- "apihelp-query+backlinks-param-namespace": "De naamruimte om door te lopen.",
- "apihelp-query+backlinks-example-simple": "Toon verwijzingen naar de <kbd>Hoofdpagina</kbd>.",
+ "apihelp-query+backlinks-param-title": "Te onderzoeken titel. Kan niet in combinatie met <var>$1pageid</var> gebruikt worden.",
+ "apihelp-query+backlinks-param-pageid": "Te onderzoeken pagina-ID. Kan niet in combinatie met <var>$1title</var> gebruikt worden.",
+ "apihelp-query+backlinks-param-namespace": "De door te lopen naamruimte.",
+ "apihelp-query+backlinks-example-simple": "Toon verwijzingen naar <kbd>Main page</kbd>.",
"apihelp-query+blocks-summary": "Toon alle geblokkeerde gebruikers en IP-adressen.",
- "apihelp-query+blocks-param-limit": "Het maximum aantal blokkades te tonen.",
- "apihelp-query+blocks-paramvalue-prop-id": "Voegt de blokkade ID toe.",
- "apihelp-query+blocks-paramvalue-prop-user": "Voegt de gebruikernaam van de geblokeerde gebruiker toe.",
- "apihelp-query+blocks-paramvalue-prop-userid": "Voegt de gebruiker-ID van de geblokkeerde gebruiker toe.",
+ "apihelp-query+blocks-param-limit": "Het maximale aantal te tonen blokkades.",
+ "apihelp-query+blocks-paramvalue-prop-id": "Voegt de blokkade-ID toe.",
+ "apihelp-query+blocks-paramvalue-prop-user": "Voegt de gebruikersnaam van de geblokkeerde gebruiker toe.",
+ "apihelp-query+blocks-paramvalue-prop-userid": "Voegt de gebruikers-ID van de geblokkeerde gebruiker toe.",
"apihelp-query+blocks-paramvalue-prop-flags": "Labelt de blokkade met (automatische blokkade, alleen anoniem, enzovoort).",
"apihelp-query+blocks-example-simple": "Toon blokkades.",
"apihelp-query+blocks-example-users": "Toon blokkades van gebruikers <kbd>Alice</kbd> en <kbd>Bob</kbd>.",
- "apihelp-query+categories-summary": "Toon alle categorieën waar de pagina in zit.",
+ "apihelp-query+categories-summary": "Toon alle categorieën waar de pagina's in zitten.",
"apihelp-query+categories-paramvalue-prop-hidden": "Markeert categorieën die verborgen zijn met <code>_&#95;HIDDENCAT_&#95;</code>",
"apihelp-query+categories-param-show": "Welke soort categorieën te tonen.",
"apihelp-query+categories-param-limit": "Hoeveel categorieën te tonen.",
"apihelp-query+categorymembers-paramvalue-prop-ids": "Voegt de pagina-ID toe.",
"apihelp-query+categorymembers-paramvalue-prop-title": "Voegt de titel en de naamruimte-ID van de pagina toe.",
"apihelp-query+categorymembers-param-dir": "Richting om in te sorteren.",
- "apihelp-query+deletedrevisions-param-tag": "Alleen versies weergeven met dit label.",
- "apihelp-query+deletedrevs-param-tag": "Alleen versies weergeven met dit label.",
- "apihelp-query+embeddedin-param-namespace": "De naamruimte om door te lopen.",
- "apihelp-query+fileusage-paramvalue-prop-pageid": "Pagina ID van elke pagina.",
+ "apihelp-query+deletedrevisions-param-tag": "Alleen revisies met dit label weergeven.",
+ "apihelp-query+deletedrevs-param-tag": "Alleen revisies met dit label weergeven.",
+ "apihelp-query+embeddedin-param-namespace": "De door te lopen naamruimte.",
+ "apihelp-query+fileusage-paramvalue-prop-pageid": "Pagina-ID van elke pagina.",
"apihelp-query+fileusage-paramvalue-prop-title": "Titel van elke pagina.",
- "apihelp-query+imageusage-param-namespace": "De naamruimte om door te lopen.",
- "apihelp-query+imageusage-example-simple": "Toon pagina's die gebruik maken van [[:File:Albert Einstein Head.jpg]].",
- "apihelp-query+imageusage-example-generator": "Toon informatie over pagina's die gebruik maken van [[:File:Albert Einstein Head.jpg]].",
+ "apihelp-query+imageusage-param-namespace": "De door te lopen naamruimte.",
+ "apihelp-query+imageusage-example-simple": "Toon pagina's die [[:File:Albert Einstein Head.jpg]] gebruiken.",
+ "apihelp-query+imageusage-example-generator": "Toon informatie over pagina's die [[:File:Albert Einstein Head.jpg]] gebruiken.",
"apihelp-query+iwbacklinks-param-prefix": "Voorvoegsel voor de interwiki.",
"apihelp-query+logevents-param-type": "Logboekregels alleen voor dit type filteren.",
- "apihelp-query+logevents-param-tag": "Alleen logboekregels weergeven met dit label.",
+ "apihelp-query+logevents-param-tag": "Alleen logboekregels met dit label weergeven.",
"apihelp-query+logevents-example-simple": "Recente logboekregels weergeven.",
"apihelp-query+protectedtitles-paramvalue-prop-level": "Voegt het beveiligingsniveau toe.",
"apihelp-query+protectedtitles-example-simple": "Toon beveiligde titels.",
- "apihelp-query+querypage-param-limit": "Aantal resultaten om te tonen.",
+ "apihelp-query+querypage-param-limit": "Aantal te tonen resultaten.",
"apihelp-query+querypage-example-ancientpages": "Toon resultaten van [[Special:Ancientpages]].",
"apihelp-query+random-param-namespace": "Toon alleen pagina's in deze naamruimten.",
- "apihelp-query+random-param-limit": "Beperk het aantal aan willekeurige pagina's dat wordt getoond.",
+ "apihelp-query+random-param-limit": "Beperk hoeveel willekeurige pagina's worden getoond.",
"apihelp-query+random-example-simple": "Toon twee willekeurige pagina's uit de hoofdnaamruimte.",
- "apihelp-query+random-example-generator": "Toon pagina informatie over twee willekeurige pagina's uit de hoofdnaamruimte.",
+ "apihelp-query+random-example-generator": "Toon pagina-informatie over twee willekeurige pagina's uit de hoofdnaamruimte.",
"apihelp-query+recentchanges-param-user": "Toon alleen wijzigingen door deze gebruiker.",
- "apihelp-query+recentchanges-param-excludeuser": "Toon geen wijzigingen door deze gebruiker",
- "apihelp-query+recentchanges-param-tag": "Alleen versies weergeven met dit label.",
+ "apihelp-query+recentchanges-param-excludeuser": "Toon geen wijzigingen door deze gebruiker.",
+ "apihelp-query+recentchanges-param-tag": "Toon alleen wijzigingen met dit label.",
"apihelp-query+recentchanges-paramvalue-prop-comment": "Voegt de bewerkingssamenvatting voor de bewerking toe.",
- "apihelp-query+recentchanges-paramvalue-prop-loginfo": "Voegt logboekgegevens toe aan logboekregels (logboek-ID, logboektype, enzovoort).",
+ "apihelp-query+recentchanges-paramvalue-prop-loginfo": "Voegt logboekgegevens (logboek-ID, logboektype, enz.) aan logboekregels toe.",
"apihelp-query+recentchanges-example-simple": "Toon recente wijzigingen.",
- "apihelp-query+redirects-paramvalue-prop-pageid": "Pagina ID van elke doorverwijzing.",
+ "apihelp-query+redirects-paramvalue-prop-pageid": "Pagina-ID van elke doorverwijzing.",
"apihelp-query+redirects-paramvalue-prop-title": "Titel van elke doorverwijzing.",
"apihelp-query+redirects-param-namespace": "Toon alleen pagina's in deze naamruimten.",
"apihelp-query+redirects-param-limit": "Hoeveel doorverwijzingen te tonen.",
- "apihelp-query+redirects-example-simple": "Toon een lijst van doorverwijzingen naar [[Main Page]].",
- "apihelp-query+redirects-example-generator": "Toon informatie over alle doorverwijzingen naar [[Main Page]].",
- "apihelp-query+revisions-param-tag": "Alleen versies weergeven met dit label.",
- "apihelp-query+revisions+base-paramvalue-prop-content": "Versietekst.",
+ "apihelp-query+redirects-example-simple": "Een lijst van doorverwijzingen naar [[Main Page]] ophalen.",
+ "apihelp-query+redirects-example-generator": "Informatie over alle doorverwijzingen naar [[Main Page]] ophalen.",
+ "apihelp-query+revisions-param-tag": "Alleen revisies met dit label weergeven.",
+ "apihelp-query+revisions+base-paramvalue-prop-content": "Tekst van de revisie.",
"apihelp-query+revisions+base-paramvalue-prop-tags": "Labels voor de versie.",
- "apihelp-query+revisions+base-param-difftotextpst": "Gebruik in plaats hiervan [[Special:ApiHelp/compare|action=compare]]. \"pre-save\"-transformatie uitvoeren op de tekst alvorens de verschillen te bepalen. Alleen geldig als dit wordt gebruikt met <var>$1difftotext</var>.",
- "apihelp-query+search-summary": "Voer een volledige tekst zoekopdracht uit.",
+ "apihelp-query+revisions+base-param-difftotextpst": "Gebruik in plaats hiervan [[Special:ApiHelp/compare|action=compare]]. Een \"pre-save\"-transformatie uitvoeren op de tekst alvorens de verschillen te bepalen. Alleen geldig indien gebruikt met <var>$1difftotext</var>.",
+ "apihelp-query+search-summary": "Voer een zoekopdracht in de volledige tekst uit.",
"apihelp-query+search-param-limit": "Hoeveel pagina's te tonen.",
- "apihelp-query+search-example-simple": "Zoeken naar <kbd>betekenis</kbd>.",
- "apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "Toon geregistreerde naamruimte aliassen.",
- "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "Toon speciale pagina aliassen.",
- "apihelp-query+siteinfo-paramvalue-prop-magicwords": "Toon magische woorden en hun aliassen.",
- "apihelp-query+siteinfo-paramvalue-prop-statistics": "Toon site statistieken.",
- "apihelp-query+siteinfo-paramvalue-prop-libraries": "Toont bibliotheken die op de wiki zijn geïnstalleerd.",
- "apihelp-query+siteinfo-paramvalue-prop-extensions": "Toont uitbreidingen die op de wiki zijn geïnstalleerd.",
- "apihelp-query+siteinfo-paramvalue-prop-fileextensions": "Geeft een lijst met bestandsextensies (bestandstypen) die geüpload mogen worden.",
- "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Toont wiki rechten (licentie) informatie als deze beschikbaar is.",
+ "apihelp-query+search-example-simple": "Zoeken naar <kbd>meaning</kbd>.",
+ "apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "Lijst van geregistreerde naamruimte-aliassen.",
+ "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "Lijst van aliassen voor speciale pagina's.",
+ "apihelp-query+siteinfo-paramvalue-prop-magicwords": "Lijst van magische woorden en hun aliassen.",
+ "apihelp-query+siteinfo-paramvalue-prop-statistics": "Site-statistieken.",
+ "apihelp-query+siteinfo-paramvalue-prop-libraries": "Bibliotheken die op de wiki zijn geïnstalleerd.",
+ "apihelp-query+siteinfo-paramvalue-prop-extensions": "Uitbreidingen die op de wiki zijn geïnstalleerd.",
+ "apihelp-query+siteinfo-paramvalue-prop-fileextensions": "Lijst van bestandsextensies (bestandstypen) die geüpload mogen worden.",
+ "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Informatie over wikirechten (licentie-informatie) indien beschikbaar.",
"apihelp-query+tags-summary": "Wijzigingslabels weergeven.",
"apihelp-query+tags-paramvalue-prop-name": "Voegt de naam van het label toe.",
- "apihelp-query+tags-paramvalue-prop-displayname": "Voegt het systeembericht toe voor het label.",
- "apihelp-query+tags-paramvalue-prop-description": "Voegt beschrijving van het label toe.",
+ "apihelp-query+tags-paramvalue-prop-displayname": "Voegt het systeembericht voor het label toe.",
+ "apihelp-query+tags-paramvalue-prop-description": "Voegt de beschrijving van het label toe.",
"apihelp-query+tags-paramvalue-prop-defined": "Geeft aan of het label is gedefinieerd.",
"apihelp-query+tags-paramvalue-prop-active": "Of het label nog steeds wordt toegepast.",
"apihelp-query+tags-example-simple": "Toon beschikbare labels.",
- "apihelp-query+templates-summary": "Toon alle pagina's ingesloten op de gegeven pagina's.",
- "apihelp-query+templates-param-limit": "Het aantal sjablonen om te tonen.",
- "apihelp-query+transcludedin-paramvalue-prop-pageid": "Pagina ID van elke pagina.",
+ "apihelp-query+templates-summary": "Retourneert alle pagina's die ingesloten zijn op de gegeven pagina's.",
+ "apihelp-query+templates-param-limit": "Hoeveel sjablonen te retourneren.",
+ "apihelp-query+transcludedin-paramvalue-prop-pageid": "Pagina-ID van elke pagina.",
"apihelp-query+transcludedin-paramvalue-prop-title": "Titel van elke pagina.",
- "apihelp-query+usercontribs-summary": "Toon alle bewerkingen door een gebruiker.",
- "apihelp-query+usercontribs-param-limit": "Het maximum aantal bewerkingen om te tonen.",
- "apihelp-query+usercontribs-param-namespace": "Toon alleen bijdragen in deze naamruimten.",
- "apihelp-query+usercontribs-param-tag": "Alleen versies weergeven met dit label.",
- "apihelp-query+usercontribs-example-ipprefix": "Toon bijdragen van alle IP-adressen met het voorvoegsel <kbd>192.0.2.</kbd>.",
- "apihelp-query+userinfo-summary": "Toon informatie over de huidige gebruiker.",
- "apihelp-query+userinfo-paramvalue-prop-realname": "Toon de gebruikers echte naam.",
+ "apihelp-query+usercontribs-summary": "Alle bewerkingen door een gebruiker opvragen.",
+ "apihelp-query+usercontribs-param-limit": "Het maximale aantal te tonen bijdragen.",
+ "apihelp-query+usercontribs-param-namespace": "Alleen bijdragen in deze naamruimten weergeven.",
+ "apihelp-query+usercontribs-param-tag": "Alleen revisies met dit label weergeven.",
+ "apihelp-query+usercontribs-example-ipprefix": "Bijdragen van alle IP-adressen met het voorvoegsel <kbd>192.0.2.</kbd> weergeven.",
+ "apihelp-query+userinfo-summary": "Informatie over de huidige gebruiker opvragen.",
+ "apihelp-query+userinfo-paramvalue-prop-realname": "Voegt de echte naam van de gebruiker toe.",
"apihelp-query+watchlist-paramvalue-prop-loginfo": "Voegt logboekgegevens toe waar van toepassing.",
"apihelp-query+watchlist-param-type": "Welke typen wijzigingen weer te geven:",
"apihelp-query+watchlist-paramvalue-type-edit": "Gewone paginabewerkingen.",
@@ -347,11 +349,11 @@
"apihelp-query+watchlist-paramvalue-type-log": "Logboekregels.",
"apihelp-query+watchlist-paramvalue-type-categorize": "Wijzigingen in categorielidmaatschap.",
"apihelp-stashedit-param-text": "Pagina-inhoud.",
- "apihelp-unblock-param-user": "Gebruikersnaam, IP-adres of IP-range om te deblokkeren. Kan niet samen worden gebruikt met <var>$1id</var> of <var>$1userid</var>.",
- "apihelp-unblock-param-userid": "Gebruikers-ID om te deblokkeren. Kan niet worden gebruikt in combinatie met <var>$1id</var> of <var>$1user</var>.",
- "apihelp-json-param-formatversion": "Uitvoeropmaak:\n;1:Achterwaarts compatibele opmaak (XML-stijl booleans, <samp>*</samp>-sleutels voor contentnodes, enzovoort).\n;2:Experimentele moderne opmaak. Details kunnen wijzigen!\n;latest:Gebruik de meest recente opmaak (op het moment <kbd>2</kbd>), kan zonder waarschuwing wijzigen.",
- "apihelp-php-param-formatversion": "Uitvoeropmaak:\n;1:Achterwaarts compatibele opmaak (XML-stijl booleans, <samp>*</samp>-sleutels voor contentnodes, enzovoort).\n;2:Experimentele moderne opmaak. Details kunnen wijzigen!\n;latest:Gebruik de meest recente opmaak (op het moment <kbd>2</kbd>), kan zonder waarschuwing wijzigen.",
- "apihelp-rawfm-summary": "Uitvoergegevens, inclusief debugelementen, opgemaakt in JSON (nette opmaak in HTML).",
+ "apihelp-unblock-param-user": "Te deblokkeren gebruikersnaam, IP-adres of IP-range. Kan niet in combinatie met <var>$1id</var> of <var>$1userid</var> gebruikt worden.",
+ "apihelp-unblock-param-userid": "Te deblokkeren gebruikers-ID. Kan niet in combinatie met <var>$1id</var> of <var>$1user</var> gebruikt worden.",
+ "apihelp-json-param-formatversion": "Uitvoeropmaak:\n;1:Achterwaarts-compatibele opmaak (booleans in XML-stijl, <samp>*</samp>-sleutels voor contentnodes, enz.).\n;2:Experimentele moderne opmaak. Details kunnen wijzigen!\n;latest:Gebruik de meest recente opmaak (op het moment <kbd>2</kbd>), kan zonder waarschuwing wijzigen.",
+ "apihelp-php-param-formatversion": "Uitvoeropmaak:\n;1:Achterwaarts-compatibele opmaak (booleans in XML-stijl, <samp>*</samp>-sleutels voor contentnodes, enz.).\n;2:Experimentele moderne opmaak. Details kunnen wijzigen!\n;latest:Gebruik de meest recente opmaak (op het moment <kbd>2</kbd>), kan zonder waarschuwing wijzigen.",
+ "apihelp-rawfm-summary": "Gegevens, inclusief debugelementen, in JSON-formaat (nette opmaak in HTML) uitvoeren.",
"api-help-flag-readrights": "Voor deze module zijn leesrechten nodig.",
"api-help-flag-writerights": "Voor deze module zijn schrijfrechten nodig.",
"api-help-parameters": "{{PLURAL:$1|Parameter|Parameters}}:",
@@ -359,21 +361,21 @@
"api-help-datatypes-header": "Gegevenstypen",
"api-help-param-default": "Standaard: $1",
"api-help-examples": "{{PLURAL:$1|Voorbeeld|Voorbeelden}}:",
- "apierror-autoblocked": "Uw IP-adres is automatisch geblokeerd, omdat het gebruikt is door een geblokkeerde gebruiker.",
+ "apierror-autoblocked": "Uw IP-adres is automatisch geblokkeerd, omdat het gebruikt werd door een geblokkeerde gebruiker.",
"apierror-badmodule-nosubmodules": "De module <kbd>$1</kbd> heeft geen submodules.",
- "apierror-blockedfrommail": "U bent geblokkeerd en kunt geen emails verzenden.",
- "apierror-blocked": "U bent geblokkeerd en kunt niet bewerken.",
+ "apierror-blockedfrommail": "Het versturen van e-mail is voor u geblokkeerd.",
+ "apierror-blocked": "Het bewerken is voor u geblokkeerd.",
"apierror-filedoesnotexist": "Bestand bestaat niet.",
"apierror-integeroutofrange-belowminimum": "<var>$1</var> mag niet minder zijn dan $2 (ingesteld op $3).",
"apierror-invalidcategory": "De opgegeven categorienaam is niet geldig.",
"apierror-invaliduser": "Ongeldige gebruikersnaam \"$1\".",
- "apierror-maxlag-generic": "Wachten op een database server: $1 {{PLURAL:$1|seconde|seconden}} vertraging.",
+ "apierror-maxlag-generic": "Wachten op een databaseserver: $1 {{PLURAL:$1|seconde|seconden}} vertraging.",
"apierror-maxlag": "Wachten op $2: $1 {{PLURAL:$1|seconde|seconden}} vertraging.",
"apierror-missingtitle": "De opgegeven pagina bestaat niet.",
"apierror-missingtitle-byname": "De pagina $1 bestaat niet.",
"apierror-mustbeloggedin-generic": "U moet ingelogd zijn.",
"apierror-nosuchuserid": "Er is geen gebruiker met ID $1.",
- "apierror-permissiondenied": "U heeft geen toestemming om $1.",
+ "apierror-permissiondenied": "U hebt geen toestemming om $1.",
"apierror-permissiondenied-generic": "Toegang geweigerd.",
"apierror-readonly": "De wiki is momenteel in alleen-lezen modus.",
"apierror-systemblocked": "U bent automatisch geblokkeerd door MediaWiki.",
@@ -386,7 +388,7 @@
"apiwarn-notfile": "\"$1\" is geen bestand.",
"apiwarn-validationfailed-badpref": "geen geldige voorkeur.",
"api-feed-error-title": "Fout ($1)",
- "api-usage-docref": "Zie $1 voor API gebruik.",
+ "api-usage-docref": "Zie $1 voor API-gebruik.",
"api-credits-header": "Vermeldingen",
- "api-credits": "API-ontwikkelaars:\n* Roan Kattouw (hoofdontwikkelaar september 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (oorspronkelijke ontwikkelaar, hoofdontwikkelaar september 2006 – september 2007)\n* Brad Jorsch (hoofdontwikkelaar 2013 – heden)\n\nStuur uw opmerkingen, suggesties en vragen naar mediawiki-api@lists.wikimedia.org\nof maak een melding aan op https://phabricator.wikimedia.org/."
+ "api-credits": "API-ontwikkelaars:\n* Yuri Astrakhan (oorspronkelijke ontwikkelaar, hoofdontwikkelaar september 2006 – september 2007)\n* Roan Kattouw (hoofdontwikkelaar september 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Brad Jorsch (hoofdontwikkelaar 2013–heden)\n\nStuur uw opmerkingen, suggesties en vragen naar mediawiki-api@lists.wikimedia.org\nof maak een bugrapport aan op https://phabricator.wikimedia.org/."
}
diff --git a/www/wiki/includes/api/i18n/pl.json b/www/wiki/includes/api/i18n/pl.json
index ac134f0b..8591ebae 100644
--- a/www/wiki/includes/api/i18n/pl.json
+++ b/www/wiki/includes/api/i18n/pl.json
@@ -149,8 +149,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Filtruj po znacznikach.",
"apihelp-feedrecentchanges-param-target": "Pokaż tylko zmiany na stronach linkowanych z tej strony.",
"apihelp-feedrecentchanges-param-showlinkedto": "Pokaż zmiany na stronach linkujących do wybranej strony.",
- "apihelp-feedrecentchanges-param-categories": "Pokaż zmiany tylko na stronach będących we wszystkich tych kategoriach.",
- "apihelp-feedrecentchanges-param-categories_any": "Pokaż zmiany tylko na stronach będących w jednej z tych kategorii.",
"apihelp-feedrecentchanges-example-simple": "Pokaż ostatnie zmiany.",
"apihelp-feedrecentchanges-example-30days": "Pokaż ostatnie zmiany z 30 dni.",
"apihelp-feedwatchlist-summary": "Zwraca kanał listy obserwowanych.",
@@ -367,6 +365,7 @@
"apihelp-query+allusers-param-activeusers": "Wyświetl tylko użytkowników, aktywnych w ciągu {{PLURAL:$1|ostatniego dnia|ostatnich $1 dni}}.",
"apihelp-query+allusers-example-Y": "Wyświetl użytkowników zaczynających się na <kbd>Y</kbd>.",
"apihelp-query+backlinks-summary": "Znajdź wszystkie strony, które linkują do danej strony.",
+ "apihelp-query+backlinks-param-title": "Tytuł strony do wyszukania. Nie może być użyty równocześnie z <var>$1pageid</var>.",
"apihelp-query+backlinks-param-namespace": "Przestrzeń nazw, z której wymieniać.",
"apihelp-query+backlinks-example-simple": "Pokazuj linki do <kbd>Main page</kbd>.",
"apihelp-query+blocks-summary": "Lista wszystkich zablokowanych użytkowników i adresów IP.",
@@ -468,6 +467,7 @@
"apihelp-query+pageswithprop-paramvalue-prop-value": "Dodaje wartość właściwości strony.",
"apihelp-query+pageswithprop-param-limit": "Maksymalna liczba zwracanych stron.",
"apihelp-query+pageswithprop-param-dir": "W jakim kierunku sortować.",
+ "apihelp-query+pageswithprop-example-simple": "Lista pierwszych 10 stron za pomocą <code>&#123;&#123;DISPLAYTITLE:&#125;&#125;</code>.",
"apihelp-query+pageswithprop-example-generator": "Pobierz dodatkowe informacje o pierwszych 10 stronach wykorzystując <code>_&#95;NOTOC_&#95;</code>.",
"apihelp-query+prefixsearch-param-search": "Wyszukaj tekst.",
"apihelp-query+prefixsearch-param-namespace": "Przestrzenie nazw do przeszukania.",
@@ -501,10 +501,13 @@
"apihelp-query+revisions+base-paramvalue-prop-tags": "Znaczniki wersji.",
"apihelp-query+revisions+base-param-limit": "Ograniczenie na liczbę wersji, które będą zwrócone.",
"apihelp-query+search-summary": "Wykonaj wyszukiwanie pełnotekstowe.",
+ "apihelp-query+search-param-namespace": "Szukaj tylko w tych przestrzeniach nazw.",
"apihelp-query+search-param-info": "Które metadane zwrócić.",
"apihelp-query+search-paramvalue-prop-size": "Dodaje rozmiar strony w bajtach.",
"apihelp-query+search-paramvalue-prop-wordcount": "Dodaje liczbę słów na stronie.",
"apihelp-query+search-paramvalue-prop-redirecttitle": "Dodaje tytuł pasującego przekierowania.",
+ "apihelp-query+search-paramvalue-prop-sectiontitle": "Dodaje tytuł pasującej sekcji.",
+ "apihelp-query+search-paramvalue-prop-extensiondata": "Dodaje dodatkowe dane generowane przez rozszerzenia.",
"apihelp-query+search-paramvalue-prop-hasrelated": "Zignorowano",
"apihelp-query+search-param-limit": "Łączna liczba stron do zwrócenia.",
"apihelp-query+search-param-interwiki": "Dołączaj wyniki wyszukiwań interwiki w wyszukiwarce, jeśli możliwe.",
@@ -512,10 +515,12 @@
"apihelp-query+siteinfo-paramvalue-prop-general": "Ogólne informacje o systemie.",
"apihelp-query+siteinfo-paramvalue-prop-namespaces": "Lista zarejestrowanych przestrzeni nazw i ich nazwy kanoniczne.",
"apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "Lista zarejestrowanych aliasów przestrzeni nazw.",
+ "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "Lista aliasów stron specjalnych.",
"apihelp-query+siteinfo-paramvalue-prop-magicwords": "Lista słów magicznych i ich aliasów.",
"apihelp-query+siteinfo-param-numberingroup": "Wyświetla liczbę użytkowników w grupach użytkowników.",
"apihelp-query+siteinfo-example-simple": "Pobierz informacje o stronie.",
"apihelp-query+stashimageinfo-param-sessionkey": "Alias dla $1filekey, dla kompatybilności wstecznej.",
+ "apihelp-query+stashimageinfo-example-simple": "Zwraca informacje o ukrytym pliku.",
"apihelp-query+tags-summary": "Lista znaczników zmian.",
"apihelp-query+tags-param-limit": "Maksymalna liczba znaczników do wyświetlenia.",
"apihelp-query+tags-paramvalue-prop-name": "Dodaje nazwę znacznika.",
@@ -663,6 +668,8 @@
"apierror-integeroutofrange-abovemax": "Wartość <var>$1</var> dla użytkowników nie może przekraczać $2 (ustawiono $3).",
"apierror-integeroutofrange-belowminimum": "Wartość <var>$1</var> nie może być mniejsza niż $2 (ustawiono $3).",
"apierror-invalidcategory": "Wprowadzona nazwa kategorii jest nieprawidłowa.",
+ "apierror-invalidexpiry": "Nieprawidłowy czas wygaśnięcia „$1”.",
+ "apierror-invalid-file-key": "Nieprawidłowy klucz pliku.",
"apierror-invalidlang": "Nieprawidłowy kod języka dla parametru <var>$1</var>.",
"apierror-invalidoldimage": "Parametr <var>oldimage</var> ma nieprawidłowy format.",
"apierror-invalidparammix": "{{PLURAL:$2|Parametry}} $1 nie mogą być używane razem.",
@@ -672,11 +679,14 @@
"apierror-invaliduserid": "Identyfikator użytkownika <var>$1</var> jest nieprawidłowy.",
"apierror-maxlag-generic": "Oczekiwania na serwer bazy danych: opóźnienie $1 {{PLURAL:$1|sekunda|sekundy|sekund}}.",
"apierror-missingparam": "Parametr <var>$1</var> musi być podany.",
+ "apierror-missingrev-title": "Brak aktualnej wersji tytułu $1.",
"apierror-missingtitle": "Wybrana przez ciebie strona nie istnieje.",
"apierror-missingtitle-byname": "Strona $1 nie istnieje.",
"apierror-moduledisabled": "Moduł <kbd>$1</kbd> został wyłączony.",
"apierror-mustbeloggedin-generic": "Musisz być zalogowany.",
+ "apierror-mustbeloggedin-removeauth": "Aby usunąć dane uwierzytelniające, musisz się zalogować.",
"apierror-mustbeloggedin": "Musisz się zalogować, aby mieć możliwość $1.",
+ "apierror-nochanges": "Nie zażądano żadnych zmian.",
"apierror-nodeleteablefile": "Nie ma takiej starej wersji pliku.",
"apierror-noedit-anon": "Niezarejestrowani użytkownicy nie mogą edytować stron.",
"apierror-noedit": "Nie masz uprawnień do edytowania stron.",
@@ -703,8 +713,11 @@
"apiwarn-invalidcategory": "„$1” nie jest kategorią.",
"apiwarn-invalidtitle": "„$1” nie jest poprawnym tytułem.",
"apiwarn-notfile": "„$1” nie jest plikiem.",
+ "apiwarn-tokennotallowed": "Działanie „$1” jest niedozwolone dla bieżącego użytkownika.",
"apiwarn-toomanyvalues": "Podano zbyt wiele wartości dla parametru <var>$1</var>. Ograniczenie do $2.",
+ "apiwarn-validationfailed-keytoolong": "klucz zbyt długi (dozwolone nie więcej niż $1 bajtów).",
"apiwarn-validationfailed": "Błąd walidacji dla <kbd>$1</kbd>: $2",
+ "apiwarn-wgDebugAPI": "<strong>Ostrzeżenie o zabezpieczeniach</strong>: włączone jest <var>$wgDebugAPI</var>.",
"api-feed-error-title": "Błąd ($1)",
"api-exception-trace": "$1 w $2($3)\n$4",
"api-credits-header": "Twórcy",
diff --git a/www/wiki/includes/api/i18n/pt-br.json b/www/wiki/includes/api/i18n/pt-br.json
index 68d51973..9330ca73 100644
--- a/www/wiki/includes/api/i18n/pt-br.json
+++ b/www/wiki/includes/api/i18n/pt-br.json
@@ -13,10 +13,11 @@
"Eduardo Addad de Oliveira",
"Warley Felipe C.",
"TheEduGobi",
- "Felipe L. Ewald"
+ "Felipe L. Ewald",
+ "Hamilton Abreu"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentação]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de discussão]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anúncios da API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & solicitações]\n</div>\n<strong>Status:</strong> Todos os recursos exibidos nesta página devem estar funcionando, mas a API ainda está em desenvolvimento ativo e pode mudar a qualquer momento. Inscrever-se na [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de discussão mediawiki-api-announce] para aviso de atualizações.\n\n<strong>Requisições incorretas:</strong> Quando requisições erradas são enviadas para a API, um cabeçalho HTTP será enviado com a chave \"MediaWiki-API-Error\" e então o valor do cabeçalho e o código de erro enviados de volta serão definidos para o mesmo valor. Para mais informações, veja [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Erros e avisos]].\n\n<strong>Testando:</strong> Para facilitar o teste das requisições da API, consulte [[Special:ApiSandbox]].",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentação]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de discussão]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anúncios da API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Erros e pedidos]\n</div>\n<strong>Estado:</strong> A API MediaWiki é uma interface madura e estável que é ativamente suportada e aprimorada. Enquanto tentamos evitá-lo, talvez ocortamente precisemos fazer mudanças de ruptura; se inscrever [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de discussão mediawiki-api-announce] para ser informado acerca das atualizações.\n\n<strong>Pedidos incorretos:</strong> Quando são enviados pedidos incorretos à API, será devolvido um cabeçalho HTTP com a chave \"MediaWiki-API-Error\" e depois tanto o valor desse cabeçalho como o código de erro devolvido serão definidos com o mesmo valor. Para mais informação, consulte [[mw:Special:MyLanguage/API:Errors_and_warnings|API:Erros e avisos]].\n\n<p class=\"mw-apisandbox-link\">\n<strong>Testes:</strong> Para testar facilmente pedidos à API, visite [[Special:ApiSandbox|Testes da API]].\n</p>",
"apihelp-main-param-action": "Qual ação executar.",
"apihelp-main-param-format": "O formato da saída.",
"apihelp-main-param-maxlag": "O atraso máximo pode ser usado quando o MediaWiki está instalado em um cluster replicado no banco de dados. Para salvar as ações que causam mais atraso na replicação do site, esse parâmetro pode fazer o cliente aguardar até que o atraso da replicação seja menor do que o valor especificado. Em caso de atraso excessivo, o código de erro <samp>maxlag</samp> é retornado com uma mensagem como <samp>Waiting for $host: $lag seconds lagged</samp>.<br />Veja [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: Maxlag parameter]] para mais informações.",
@@ -25,7 +26,7 @@
"apihelp-main-param-assert": "Verifique se o usuário está logado se configurado para <kbd>user</kbd> ou tem o direito do usuário do bot se <kbd>bot</kbd>.",
"apihelp-main-param-assertuser": "Verificar que o usuário atual é o utilizador nomeado.",
"apihelp-main-param-requestid": "Qualquer valor dado aqui será incluído na resposta. Pode ser usado para distinguir requisições.",
- "apihelp-main-param-servedby": "Inclua o nome de host que atendeu a solicitação nos resultados.",
+ "apihelp-main-param-servedby": "Incluir nos resultados o nome do servidor que serviu o pedido.",
"apihelp-main-param-curtimestamp": "Inclui o timestamp atual no resultado.",
"apihelp-main-param-responselanginfo": "Inclua os idiomas usados para <var>uselang</var> e <var>errorlang</var> no resultado.",
"apihelp-main-param-origin": "Ao acessar a API usando uma solicitação AJAX por domínio cruzado (CORS), defina isto como o domínio de origem. Isto deve estar incluso em toda solicitação ''pre-flight'', sendo portanto parte do URI da solicitação (ao invés do corpo do POST).\n\nPara solicitações autenticadas, isto deve corresponder a uma das origens no cabeçalho <code>Origin</code>, para que seja algo como <kbd>https://pt.wikipedia.org</kbd> ou <kbd>https://meta.wikimedia.org</kbd>. Se este parâmetro não corresponder ao cabeçalho <code>Origin</code>, uma resposta 403 será retornada. Se este parâmetro corresponder ao cabeçalho <code>Origin</code> e a origem for permitida (''whitelisted''), os cabeçalhos <code>Access-Control-Allow-Origin</code> e <code>Access-Control-Allow-Credentials</code> serão definidos.\n\nPara solicitações não autenticadas, especifique o valor <kbd>*</kbd>. Isto fará com que o cabeçalho <code>Access-Control-Allow-Origin</code> seja definido, porém o <code>Access-Control-Allow-Credentials</code> será <code>false</code> e todos os dados específicos para usuários tornar-se-ão restritos.",
@@ -67,6 +68,7 @@
"apihelp-compare-param-fromid": "Primeiro ID de página para comparar.",
"apihelp-compare-param-fromrev": "Primeira revisão para comparar.",
"apihelp-compare-param-fromtext": "Use este texto em vez do conteúdo da revisão especificada por <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>.",
+ "apihelp-compare-param-fromsection": "Utilizar apenas a secção especificada do conteúdo 'from' especificado.",
"apihelp-compare-param-frompst": "Faz uma transformação pré-salvar em <var>fromtext</var>.",
"apihelp-compare-param-fromcontentmodel": "Modelo de conteúdo de <var>fromtext</var>. Se não for fornecido, será adivinhado com base nos outros parâmetros.",
"apihelp-compare-param-fromcontentformat": "Formato de serialização de conteúdo de <var>fromtext</var>.",
@@ -203,8 +205,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Filtrar por tag.",
"apihelp-feedrecentchanges-param-target": "Mostrar apenas as alterações nas páginas vinculadas por esta página.",
"apihelp-feedrecentchanges-param-showlinkedto": "Mostra as alterações nas páginas vigiadas à página selecionada.",
- "apihelp-feedrecentchanges-param-categories": "Mostre apenas as alterações em páginas em todas essas categorias.",
- "apihelp-feedrecentchanges-param-categories_any": "Mostre apenas as alterações em páginas em qualquer uma das categorias.",
"apihelp-feedrecentchanges-example-simple": "Mostrar as mudanças recentes.",
"apihelp-feedrecentchanges-example-30days": "Mostrar as mudanças recentes por 30 dias.",
"apihelp-feedwatchlist-summary": "Retornar um feed da lista de páginas vigiadas.",
@@ -296,7 +296,7 @@
"apihelp-opensearch-summary": "Procure na wiki usando o protocolo OpenSearch.",
"apihelp-opensearch-param-search": "Pesquisar string.",
"apihelp-opensearch-param-limit": "Número máximo de resultados.",
- "apihelp-opensearch-param-namespace": "Espaço nominal para pesquisar.",
+ "apihelp-opensearch-param-namespace": "Espaços nominais a pesquisar. Ignorados se <var>$1search</var> começar com um prefixo de espaço nominal válido.",
"apihelp-opensearch-param-suggest": "Não fazer nada se <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> é false.",
"apihelp-opensearch-param-redirects": "Como lidar com os redirecionamentos:\n;return: Retornar o redirecionamento em si.\n;resolve: Retornar a página de destino. Pode retornar menos de $1 resultados.\nPor razões históricas, o padrão é \"return\" para $1format=json e \"resolve\" para outros formatos.",
"apihelp-opensearch-param-format": "O formato da saída.",
@@ -322,9 +322,10 @@
"apihelp-paraminfo-example-1": "Mostrar informações para <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>, <kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>, <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> e <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>.",
"apihelp-paraminfo-example-2": "Mostrar informações para todos os submódulos de <kbd>[[Special:ApiHelp/query|action=query]]</kbd>.",
"apihelp-parse-summary": "Analisa o conteúdo e retorna a saída do analisador.",
- "apihelp-parse-extended-description": "Veja os vários módulos de suporte de <kbd>[[Special:ApiHelp/query|action=query]]</kbd> para obter informações da versão atual de uma página.\n\nHá várias maneiras de especificar o texto para analisar:\n# Especifique uma página ou revisão, usando <var>$1page</var>, <var>$1pageid</var>, ou <var>$1oldid</var>.\n#Especifica o conteúdo explicitamente, Usando <var>$1text</var>, <var>$1title</var> e <var>$1contentmodel</var>.\n# Especifique apenas um resumo a analisar. <Var>$1prop</var> deve ter um valor vazio.",
+ "apihelp-parse-extended-description": "Veja os vários módulos de suporte de <kbd>[[Special:ApiHelp/query|action=query]]</kbd> para obter informações da versão atual de uma página.\n\nHá várias maneiras de especificar o texto para analisar:\n# Especifique uma página ou revisão, usando <var>$1page</var>, <var>$1pageid</var>, ou <var>$1oldid</var>.\n# Especifica o conteúdo explicitamente, usando <var>$1text</var>, <var>$1title</var>, <var>$1revid</var> e <var>$1contentmodel</var>.\n# Especifique apenas um resumo a analisar. <Var>$1prop</var> deve ter um valor vazio.",
"apihelp-parse-param-title": "Título da página ao qual o texto pertence. Se omitido, <var>$1contentmodel</var> deve ser especificado e [[API]] será usado como título.",
"apihelp-parse-param-text": "Texto para analisar. Use <var>$1title</var> ou <var>$1contentmodel</var> para controlar o modelo de conteúdo.",
+ "apihelp-parse-param-revid": "ID da revisão, para <code><nowiki>{{REVISIONID}}</nowiki></code> e variáveis similares.",
"apihelp-parse-param-summary": "Sumário para analisar.",
"apihelp-parse-param-page": "Analisa o conteúdo desta página. Não pode ser usado em conjunto com <var>$1text</var> e <var>$1title</var>.",
"apihelp-parse-param-pageid": "Analisa o conteúdo desta página. Sobrepõe <var>$1page</var>.",
@@ -365,6 +366,7 @@
"apihelp-parse-param-disablepp": "Use <var>$1disablelimitreport</var> em vez.",
"apihelp-parse-param-disableeditsection": "Omita os links da seção de edição da saída do analisador.",
"apihelp-parse-param-disabletidy": "Não executa a limpeza HTML (por exemplo, tidy) na saída do analisador.",
+ "apihelp-parse-param-disablestylededuplication": "Não desduplica as folhas de estilo inline na saída do analisador.",
"apihelp-parse-param-generatexml": "Gerar XML parse tree (requer modelo de conteúdo <code>$1</code>, substituído por <kbd>$2prop=parsetree</kbd>).",
"apihelp-parse-param-preview": "Analisar no mode de visualização.",
"apihelp-parse-param-sectionpreview": "Analise no modo de visualização de seção (também permite o modo de visualização).",
@@ -734,7 +736,7 @@
"apihelp-query+exturlusage-param-namespace": "O espaço nominal das páginas para enumerar.",
"apihelp-query+exturlusage-param-limit": "Quantas páginas retornar.",
"apihelp-query+exturlusage-param-expandurl": "Expandir URLs relativos ao protocolo com o protocolo canônico.",
- "apihelp-query+exturlusage-example-simple": "Mostra páginas vigiadas à <kbd>http://www.mediawiki.org</kbd>.",
+ "apihelp-query+exturlusage-example-simple": "Mostra páginas vigiadas à <kbd>https://www.mediawiki.org</kbd>.",
"apihelp-query+filearchive-summary": "Enumerar todos os arquivos excluídos sequencialmente.",
"apihelp-query+filearchive-param-from": "O título da imagem do qual começar a enumeração.",
"apihelp-query+filearchive-param-to": "O título da imagem no qual parar a enumeração.",
@@ -835,6 +837,7 @@
"apihelp-query+info-paramvalue-prop-readable": "Se o usuário pode ler esta página.",
"apihelp-query+info-paramvalue-prop-preload": "Fornece o texto retornado por EditFormPreloadText.",
"apihelp-query+info-paramvalue-prop-displaytitle": "Fornece o modo como o título da página é exibido.",
+ "apihelp-query+info-paramvalue-prop-varianttitles": "Fornece o título de apresentação em todas as variantes da língua de conteúdo da wiki.",
"apihelp-query+info-param-testactions": "Testa se o usuário atual pode executar determinadas ações na página.",
"apihelp-query+info-param-token": "Use [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] em vez.",
"apihelp-query+info-example-simple": "Obter informações sobre a página <kbd>Main Page</kbd>.",
@@ -942,7 +945,7 @@
"apihelp-query+prefixsearch-summary": "Execute uma pesquisa de prefixo para títulos de página.",
"apihelp-query+prefixsearch-extended-description": "Apesar da semelhança nos nomes, este módulo não se destina a ser equivalente a[[Special:PrefixIndex]]; para isso, veja <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> com o parâmetro <kbd>apprefix</kbd>.O propósito deste módulo é semelhante a <kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd>: para inserir o usuário e fornecer os títulos de melhor correspondência. Dependendo do backend do mecanismo de pesquisa, isso pode incluir correção de digitação, evasão de redirecionamento ou outras heurísticas.",
"apihelp-query+prefixsearch-param-search": "Pesquisar string.",
- "apihelp-query+prefixsearch-param-namespace": "Espaço nominal para pesquisar.",
+ "apihelp-query+prefixsearch-param-namespace": "Os espaços nominais onde realizar a pesquisa. Ignorados se <var>$1search</var> começar com um prefixo de espaço nominal válido.",
"apihelp-query+prefixsearch-param-limit": "Número máximo de resultados.",
"apihelp-query+prefixsearch-param-offset": "Número de resultados a ignorar.",
"apihelp-query+prefixsearch-example-simple": "Procure títulos de páginas começando com <kbd>meaning</kbd>.",
@@ -994,6 +997,7 @@
"apihelp-query+recentchanges-paramvalue-prop-sizes": "Adiciona o comprimento novo e antigo da página em bytes.",
"apihelp-query+recentchanges-paramvalue-prop-redirect": "Etiqueta a edição se a página é um redirecionamento.",
"apihelp-query+recentchanges-paramvalue-prop-patrolled": "Etiquete edições patrulháveis como sendo patrulhadas ou não-patrulhadas.",
+ "apihelp-query+recentchanges-paramvalue-prop-autopatrolled": "Etiqueta as edições que podem ser patrulhadas, marcando-as como autopatrulhadas ou não.",
"apihelp-query+recentchanges-paramvalue-prop-loginfo": "Adiciona informações de registro (ID de registro, tipo de registro, etc.) às entradas do log.",
"apihelp-query+recentchanges-paramvalue-prop-tags": "Listar as etiquetas para a entrada.",
"apihelp-query+recentchanges-paramvalue-prop-sha1": "Adiciona o checksum do conteúdo para entradas associadas a uma revisão.",
@@ -1171,6 +1175,7 @@
"apihelp-query+usercontribs-paramvalue-prop-sizediff": "Adiciona o tamanho delta da edição contra o seu pai.",
"apihelp-query+usercontribs-paramvalue-prop-flags": "Adiciona etiqueta da edição.",
"apihelp-query+usercontribs-paramvalue-prop-patrolled": "Etiquetas de edições patrulhadas.",
+ "apihelp-query+usercontribs-paramvalue-prop-autopatrolled": "Etiqueta as edições autopatrulhadas.",
"apihelp-query+usercontribs-paramvalue-prop-tags": "Lista as tags para editar.",
"apihelp-query+usercontribs-param-show": "Mostre apenas itens que atendam a esses critérios, por exemplo, apenas edições não-menores: <kbd>$2show=!minor</kbd>.\n\nSe <kbd>$2show=patrolled</kbd> ou <kbd>$2show=!patrolled</kbd> estiver definido, revisões mais antigas do que <var>[[mw:Special:MyLanguage/Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ($1 {{PLURAL:$1|segundo|segundos}}) não serão exibidas.",
"apihelp-query+usercontribs-param-tag": "Lista apenas as revisões com esta tag.",
@@ -1235,6 +1240,7 @@
"apihelp-query+watchlist-paramvalue-prop-parsedcomment": "Adiciona o comentário analisado da edição.",
"apihelp-query+watchlist-paramvalue-prop-timestamp": "Adiciona o timestamp da edição.",
"apihelp-query+watchlist-paramvalue-prop-patrol": "Edições de tags que são patrulhadas.",
+ "apihelp-query+watchlist-paramvalue-prop-autopatrol": "Etiqueta que indica as edições que são autopatrulhadas.",
"apihelp-query+watchlist-paramvalue-prop-sizes": "Adiciona os velhos e novos comprimentos da página.",
"apihelp-query+watchlist-paramvalue-prop-notificationtimestamp": "Adiciona o timestamp de quando o usuário foi notificado pela última vez sobre a edição.",
"apihelp-query+watchlist-paramvalue-prop-loginfo": "Adiciona informações de log, quando apropriado.",
@@ -1550,6 +1556,8 @@
"apierror-chunk-too-small": "O tamanho mínimo do bloco é $1 {{PLURAL:$1|byte|bytes}} para os pedaços não finais.",
"apierror-cidrtoobroad": "Os intervalos CIDR $1 maiores que /$2 não são aceitos.",
"apierror-compare-no-title": "Não é possível pré-salvar a transformação sem um título. Tente especificar <var>fromtitle</var> ou <var>totitle</var>.",
+ "apierror-compare-nosuchfromsection": "Não há nenhuma secção $1 no conteúdo 'from'.",
+ "apierror-compare-nosuchtosection": "Não há nenhuma seção $1 no conteúdo 'to'.",
"apierror-compare-relative-to-nothing": "Nenhuma revisão 'from' para <var>torelative</var> para ser relativa à.",
"apierror-contentserializationexception": "Falha na serialização de conteúdo: $1",
"apierror-contenttoobig": "O conteúdo fornecido excede o limite de tamanho do artigo de $1 {{PLURAL: $1|kilobyte|kilobytes}}.",
@@ -1605,7 +1613,6 @@
"apierror-missingtitle-byname": "A página $1 não existe.",
"apierror-moduledisabled": "O módulo <kbd>$1</kbd> foi desativado.",
"apierror-multival-only-one-of": "{{PLURAL:$3|Somente|Somente um de}} $2 é permitido para parâmetro <var>$1</var>.",
- "apierror-multival-only-one": "Apenas um valor é permitido para o parâmetro <var> $1</var>.",
"apierror-multpages": "<var>$1</var> só pode ser usada com uma única página.",
"apierror-mustbeloggedin-changeauth": "Você precisa estar autenticado para alterar dados de autenticação.",
"apierror-mustbeloggedin-generic": "Você deve estar logado.",
@@ -1726,6 +1733,7 @@
"apiwarn-notfile": "\"$1\" não é um arquivo.",
"apiwarn-nothumb-noimagehandler": "Não foi possível criar uma miniatura porque $1 não possui um manipulador de imagem associado.",
"apiwarn-parse-nocontentmodel": "Não foi dado <var>title</var> ou <var>contentmodel</var>, assumindo $1.",
+ "apiwarn-parse-revidwithouttext": "<var>revid</var> use sem <var>text</var> e as propriedades da página analisada são necessárias. Você quis usar <var>oldid</var> ao invés de <var>revid</var>?",
"apiwarn-parse-titlewithouttext": "<var>title</var> usado sem <var>text</var>, e as propriedades da página analisada foram solicitadas. Você quis usar <var>page</var> ao invés de <var>title</var>?",
"apiwarn-redirectsandrevids": "A resolução de redirecionamento não pode ser usada em conjunto com o parâmetro <var>revids</var>. Qualquer redirecionamento <var>revids</var> apontando para não foi resolvido.",
"apiwarn-tokennotallowed": "A ação \"$1\" não é permitida para o usuário atual.",
diff --git a/www/wiki/includes/api/i18n/pt.json b/www/wiki/includes/api/i18n/pt.json
index 78ec0be1..2a81d29d 100644
--- a/www/wiki/includes/api/i18n/pt.json
+++ b/www/wiki/includes/api/i18n/pt.json
@@ -7,15 +7,17 @@
"Jkb8",
"Hamilton Abreu",
"Mansil",
- "Felipe L. Ewald"
+ "Felipe L. Ewald",
+ "Athena in Wonderland",
+ "Waldir"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentação]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de discussão]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anúncios da API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Erros e pedidos]\n</div>\n<strong>Estado:</strong> Todas as funcionalidades mostradas nesta página devem ter o comportamento documentado, mas a API ainda está em desenvolvimento ativo e pode ser alterada a qualquer momento. Inscreva-se na [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de discussão mediawiki-api-announce] para ser informado acerca das atualizações.\n\n<strong>Pedidos incorretos:</strong> Quando são enviados pedidos incorretos à API, será devolvido um cabeçalho HTTP com a chave \"MediaWiki-API-Error\" e depois tanto o valor desse cabeçalho como o código de erro devolvido serão definidos com o mesmo valor. Para mais informação, consulte [[mw:Special:MyLanguage/API:Errors_and_warnings|API:Erros e avisos]].\n\n<strong>Testes:</strong> Para testar facilmente pedidos à API, visite [[Special:ApiSandbox|Testes da API]].",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentação]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de divulgação]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anúncios da API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Defeitos e pedidos]\n</div>\n<strong>Estado:</strong> A API do MediaWiki é uma interface consolidada e estável que é constantemente suportada e melhorada. Embora tentemos evitá-lo, podemos ocasionalmente realizar alterações disruptivas. Inscreva-se na [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de distribuição mediawiki-api-announce] para receber notificações das atualizações.\n\n<strong>Pedidos incorretos:</strong> Quando são enviados pedidos incorretos à API, será devolvido um cabeçalho HTTP com a chave \"MediaWiki-API-Error\" e depois tanto o valor desse cabeçalho como o código de erro devolvido serão definidos com o mesmo valor. Para mais informação, consulte [[mw:Special:MyLanguage/API:Errors_and_warnings|API:Erros e avisos]].\n\n<p class=\"mw-apisandbox-link\"><strong>Testes:</strong> Para testar facilmente pedidos à API, visite [[Special:ApiSandbox|Testes da API]].</p>",
"apihelp-main-param-action": "A operação a ser realizada.",
"apihelp-main-param-format": "O formato do resultado.",
- "apihelp-main-param-maxlag": "O atraso máximo pode ser usado quando o MediaWiki é instalado num ''cluster'' de bases de dados replicadas. Para impedir que as operações causem ainda mais atrasos de replicação do ''site'', este parâmetro pode fazer o cliente aguardar até que o atraso de replicação seja inferior ao valor especificado. Caso o atraso atual exceda esse valor, o código de erro <samp>maxlag</samp> é devolvido com uma mensagem como <samp>À espera do servidor $host: $lag segundos de atraso</samp>.<br />Consulte [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: Parâmetro maxlag]] para mais informações.",
- "apihelp-main-param-smaxage": "Definir no cabeçalho HTTP <code>s-maxage</code> de controlo da ''cache'' este número de segundos. Os erros nunca são armazenados na ''cache''.",
- "apihelp-main-param-maxage": "Definir no cabeçalho HTTP <code>max-age</code> de controlo da ''cache'' este número de segundos. Os erros nunca são armazenados na ''cache''.",
+ "apihelp-main-param-maxlag": "O atraso máximo pode ser usado quando o MediaWiki é instalado num ''cluster'' de bases de dados replicadas. Para impedir que as operações causem ainda mais atrasos de replicação do sítio, este parâmetro pode fazer o cliente aguardar até que o atraso de replicação seja inferior ao valor especificado. Caso o atraso atual exceda esse valor, o código de erro <samp>maxlag</samp> é devolvido com uma mensagem como <samp>À espera do servidor $host: $lag segundos de atraso</samp>.<br />Consulte [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: Parâmetro maxlag]] para mais informações.",
+ "apihelp-main-param-smaxage": "Definir no cabeçalho HTTP <code>s-maxage</code> de controlo da cache este número de segundos. Os erros nunca são armazenados na cache.",
+ "apihelp-main-param-maxage": "Definir no cabeçalho HTTP <code>max-age</code> de controlo da cache este número de segundos. Os erros nunca são armazenados na cache.",
"apihelp-main-param-assert": "Se definido com o valor <kbd>user</kbd>, verificar que o utilizador está autenticado. Se definido com o valor <kbd>bot</kbd>, verificar que o utilizador tem o privilégio de conta robô.",
"apihelp-main-param-assertuser": "Verificar que o utilizador atual é o utilizador nomeado.",
"apihelp-main-param-requestid": "Qualquer valor fornecido aqui será incluído na resposta. Pode ser usado para distinguir pedidos.",
@@ -61,6 +63,7 @@
"apihelp-compare-param-fromid": "Primeiro identificador de página a comparar.",
"apihelp-compare-param-fromrev": "Primeira revisão a comparar.",
"apihelp-compare-param-fromtext": "Usar este texto em vez do conteúdo da revisão especificada por <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>.",
+ "apihelp-compare-param-fromsection": "Utilizar apenas a secção especificada do conteúdo 'from' especificado.",
"apihelp-compare-param-frompst": "Fazer uma transformação anterior à gravação, de <var>fromtext</var>.",
"apihelp-compare-param-fromcontentmodel": "Modelo de conteúdo de <var>fromtext</var>. Se não for fornecido, ele será deduzido a partir dos outros parâmetros.",
"apihelp-compare-param-fromcontentformat": "Formato de seriação do conteúdo de <var>fromtext</var>.",
@@ -69,6 +72,7 @@
"apihelp-compare-param-torev": "Segunda revisão a comparar.",
"apihelp-compare-param-torelative": "Usar uma revisão relativa à revisão determinada a partir de <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>. Todas as outras opções 'to' serão ignoradas.",
"apihelp-compare-param-totext": "Usar este texto em vez do conteúdo da revisão especificada por <var>totitle</var>, <var>toid</var> ou <var>torev</var>.",
+ "apihelp-compare-param-tosection": "Utilizar apenas a secção especificada do conteúdo 'to' especificado.",
"apihelp-compare-param-topst": "Fazer uma transformação anterior à gravação, de <var>totext</var>.",
"apihelp-compare-param-tocontentmodel": "Modelo de conteúdo de <var>totext</var>. Se não for fornecido, ele será deduzido a partir dos outros parâmetros.",
"apihelp-compare-param-tocontentformat": "Formato de seriação do conteúdo de <var>totext</var>.",
@@ -149,23 +153,23 @@
"apihelp-emailuser-param-text": "Texto.",
"apihelp-emailuser-param-ccme": "Enviar-me uma cópia desta mensagem.",
"apihelp-emailuser-example-email": "Enviar uma mensagem de correio ao utilizador <kbd>WikiSysop</kbd> com o texto <kbd>Content</kbd>.",
- "apihelp-expandtemplates-summary": "Expande todas as predefinições incluídas num texto em notação wiki.",
+ "apihelp-expandtemplates-summary": "Expande todas as predefinições existentes num texto wiki.",
"apihelp-expandtemplates-param-title": "Título da página.",
- "apihelp-expandtemplates-param-text": "Texto em notação wiki a converter.",
+ "apihelp-expandtemplates-param-text": "Texto wiki a converter.",
"apihelp-expandtemplates-param-revid": "Identificador da revisão, para <code><nowiki>{{REVISIONID}}</nowiki></code> e variáveis semelhantes.",
- "apihelp-expandtemplates-param-prop": "As informações que devem ser obtidas:\n\nNote que se não for selecionado nenhum valor, o resultado irá conter texto em notação wiki mas a saída estará num formato obsoleto.",
- "apihelp-expandtemplates-paramvalue-prop-wikitext": "O texto em notação wiki expandido.",
- "apihelp-expandtemplates-paramvalue-prop-categories": "Quaisquer categorias existentes na entrada que não estão representadas no texto em notação wiki de saída.",
- "apihelp-expandtemplates-paramvalue-prop-properties": "Propriedades da página, definidas por palavras mágicas expandidas, no texto em notação wiki.",
+ "apihelp-expandtemplates-param-prop": "As informações que devem ser obtidas:\n\nNote que, se não for selecionado nenhum valor, o resultado irá conter o texto wiki mas a saída estará num formato obsoleto.",
+ "apihelp-expandtemplates-paramvalue-prop-wikitext": "O texto wiki expandido.",
+ "apihelp-expandtemplates-paramvalue-prop-categories": "Quaisquer categorias existentes na entrada que não estão representadas no texto wiki de saída.",
+ "apihelp-expandtemplates-paramvalue-prop-properties": "Propriedades da página, definidas por palavras mágicas expandidas, no texto wiki.",
"apihelp-expandtemplates-paramvalue-prop-volatile": "Indica se o resultado é volátil e não deve ser reutilizado noutra parte da página.",
- "apihelp-expandtemplates-paramvalue-prop-ttl": "O período máximo a partir do qual os armazenamentos do resultado na ''cache'' devem ser invalidados.",
+ "apihelp-expandtemplates-paramvalue-prop-ttl": "O período máximo a partir do qual os armazenamentos do resultado na cache devem ser invalidados.",
"apihelp-expandtemplates-paramvalue-prop-modules": "Quaisquer módulos ResourceLoader que as funções do analisador sintático solicitaram que fossem adicionados ao resultado de saída. Um dos valores <kbd>jsconfigvars</kbd> ou <kbd>encodedjsconfigvars</kbd> tem de ser solicitado em conjunto com o valor <kbd>modules</kbd>.",
"apihelp-expandtemplates-paramvalue-prop-jsconfigvars": "Devolve as variáveis de configuração JavaScript específicas desta página.",
"apihelp-expandtemplates-paramvalue-prop-encodedjsconfigvars": "Devolve as variáveis de configuração JavaScript específicas da página, no formato de uma ''string'' JSON.",
"apihelp-expandtemplates-paramvalue-prop-parsetree": "A árvore de análise sintática em XML do texto de entrada.",
"apihelp-expandtemplates-param-includecomments": "Indica se devem ser incluídos comentários HTML no resultado.",
"apihelp-expandtemplates-param-generatexml": "Gerar a árvore de análise sintática em XML (substituído por $1prop=parsetree).",
- "apihelp-expandtemplates-example-simple": "Expandir o texto em notação wiki <kbd><nowiki>{{Project:Sandbox}}</nowiki></kbd>.",
+ "apihelp-expandtemplates-example-simple": "Expandir o texto wiki <kbd><nowiki>{{Project:Sandbox}}</nowiki></kbd>.",
"apihelp-feedcontributions-summary": "Devolve um ''feed'' das contribuições do utilizador.",
"apihelp-feedcontributions-param-feedformat": "O formato do ''feed''.",
"apihelp-feedcontributions-param-user": "Os utilizadores dos quais serão obtidas as contribuições.",
@@ -196,9 +200,7 @@
"apihelp-feedrecentchanges-param-hidecategorization": "Ocultar mudanças de pertença a categorias.",
"apihelp-feedrecentchanges-param-tagfilter": "Filtrar por etiqueta.",
"apihelp-feedrecentchanges-param-target": "Mostrar apenas mudanças em páginas afluentes a esta.",
- "apihelp-feedrecentchanges-param-showlinkedto": "Mostrar mudanças em páginas com ligações para a página selecionada.",
- "apihelp-feedrecentchanges-param-categories": "Mostrar apenas mudanças nas páginas que estão em todas estas categorias.",
- "apihelp-feedrecentchanges-param-categories_any": "Mostrar apenas mudanças nas páginas que estão em qualquer uma das categorias.",
+ "apihelp-feedrecentchanges-param-showlinkedto": "Mostrar mudanças em páginas com hiperligações para a página selecionada.",
"apihelp-feedrecentchanges-example-simple": "Mostrar mudanças recentes.",
"apihelp-feedrecentchanges-example-30days": "Mostrar as mudanças recentes de 30 dias.",
"apihelp-feedwatchlist-summary": "Devolve um ''feed'' das páginas vigiadas.",
@@ -233,6 +235,8 @@
"apihelp-import-extended-description": "Note que o pedido POST de HTTP tem de ser feito como um carregamento de ficheiro (isto é, usando \"multipart/form-data\") ao enviar um ficheiro para o parâmetro <var>xml</var>.",
"apihelp-import-param-summary": "Resumo da importação para a entrada do registo.",
"apihelp-import-param-xml": "Ficheiro XML carregado.",
+ "apihelp-import-param-interwikiprefix": "Para importações carregadas: o prefixo interwikis a ser aplicado aos nomes de utilizador desconhecidos (e aos conhecidos se <var>$1assignknownusers</var> estiver definido).",
+ "apihelp-import-param-assignknownusers": "Atribuir as edições aos utilizadores locais se o utilizador nomeado existir localmente.",
"apihelp-import-param-interwikisource": "Para importações interwikis: a wiki de onde importar.",
"apihelp-import-param-interwikipage": "Para importações interwikis: a página a importar.",
"apihelp-import-param-fullhistory": "Para importações interwikis: importar o historial completo, não apenas a versão atual.",
@@ -290,7 +294,7 @@
"apihelp-opensearch-summary": "Pesquisar a wiki usando o protocolo OpenSearch.",
"apihelp-opensearch-param-search": "Texto a pesquisar.",
"apihelp-opensearch-param-limit": "O número máximo de resultados a serem devolvidos.",
- "apihelp-opensearch-param-namespace": "Espaços nominais a pesquisar.",
+ "apihelp-opensearch-param-namespace": "Espaços nominais a pesquisar. Ignorados se <var>$1search</var> começar com um prefixo de espaço nominal válido.",
"apihelp-opensearch-param-suggest": "Não fazer nada se <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> for falso.",
"apihelp-opensearch-param-redirects": "Como tratar redirecionamentos:\n;return:Devolver o próprio redirecionamento.\n;resolve:Devolver a página de destino. Pode devolver menos de $1limit resultados.\nPor razões históricas, o valor por omissão é \"return\" para o formato $1format=json e \"resolve\" para outros formatos.",
"apihelp-opensearch-param-format": "O formato do resultado.",
@@ -298,7 +302,7 @@
"apihelp-opensearch-example-te": "Encontrar as páginas que começam por <kbd>Te</kbd>.",
"apihelp-options-summary": "Alterar as preferências do utilizador atual.",
"apihelp-options-extended-description": "Só podem ser definidas as opções que estão registadas no núcleo do MediaWiki ou numa das extensões instaladas, ou opções cuja chave tem o prefixo <code>userjs-</code> (que são supostas ser usadas por ''scripts'' de utilizador).",
- "apihelp-options-param-reset": "Reiniciar preferências para os valores por omissão do ''site''.",
+ "apihelp-options-param-reset": "Reiniciar preferências para os valores por omissão do sítio.",
"apihelp-options-param-resetkinds": "Lista dos tipos de opções a reiniciar quando a opção <var>$1reset</var> está definida.",
"apihelp-options-param-change": "Listas das alterações, na forma nome=valor (isto é, skin=vector). Se não for fornecido nenhum valor (nem sequer um sinal de igualdade), por exemplo, nomedaopção|outraopção|..., a opção será reiniciada para o seu valor por omissão. Se qualquer dos valores passados contém uma barra vertical (<kbd>|</kbd>), use um [[Special:ApiHelp/main#main/datatypes|separador alternativo para valores múltiplos]] de forma a obter o comportamento correto.",
"apihelp-options-param-optionname": "O nome da opção que deve ser configurada com o valor dado por <var>$1optionvalue</var>.",
@@ -316,34 +320,35 @@
"apihelp-paraminfo-example-1": "Mostrar informação para <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>, <kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>, <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> e <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>.",
"apihelp-paraminfo-example-2": "Mostrar informação de todos os módulos de <kbd>[[Special:ApiHelp/query|action=query]]</kbd>.",
"apihelp-parse-summary": "Faz a análise sintática do conteúdo e devolve o resultado da análise.",
- "apihelp-parse-extended-description": "Consulte os vários módulos disponíveis no parâmetro prop de <kbd>[[Special:ApiHelp/query|action=query]]</kbd> para obter informação da versão atual de uma página.\n\nHá várias formas de especificar o texto a analisar:\n# Especificar uma página ou revisão, usando <var>$1page</var>, <var>$1pageid</var> ou <var>$1oldid</var>.\n# Especificar o conteúdo de forma explícita, usando <var>$1text</var>, <var>$1title</var> e <var>$1contentmodel</var>.\n# Especificar só um resumo a analisar. <var>$1prop</var> deve receber o valor vazio.",
+ "apihelp-parse-extended-description": "Consulte os vários módulos disponíveis no parâmetro prop de <kbd>[[Special:ApiHelp/query|action=query]]</kbd> para obter informação da versão atual de uma página.\n\nHá várias formas de especificar o texto a analisar:\n# Especificar uma página ou revisão, usando <var>$1page</var>, <var>$1pageid</var> ou <var>$1oldid</var>.\n# Especificar o conteúdo de forma explícita, usando <var>$1text</var>, <var>$1title</var>, <var>$1revid</var> e <var>$1contentmodel</var>.\n# Especificar só um resumo a analisar. <var>$1prop</var> deve receber o valor vazio.",
"apihelp-parse-param-title": "Título da página à qual o texto pertence. Se omitido, é preciso especificar <var>$1contentmodel</var> e deve usar [[API]] como título.",
"apihelp-parse-param-text": "Texto a analisar. Usar <var>$1title</var> ou <var>$1contentmodel</var> para controlar o modelo de conteúdo.",
+ "apihelp-parse-param-revid": "Identificador da revisão, para <code><nowiki>{{REVISIONID}}</nowiki></code> e variáveis semelhantes.",
"apihelp-parse-param-summary": "Resumo a analisar.",
"apihelp-parse-param-page": "Analisar o conteúdo desta página. Não pode ser usado em conjunto com <var>$1text</var> e <var>$1title</var>.",
"apihelp-parse-param-pageid": "Analisar o conteúdo desta página. Tem precedência sobre <var>$1page</var>.",
"apihelp-parse-param-redirects": "Se <var>$1page</var> ou <var>$1pageid</var> estiverem definidos para um redirecionamento, resolvê-lo.",
"apihelp-parse-param-oldid": "Analisar o conteúdo desta revisão. Tem precedência sobre <var>$1page</var> e <var>$1pageid</var>.",
"apihelp-parse-param-prop": "As informações que devem ser obtidas:",
- "apihelp-parse-paramvalue-prop-text": "Fornece o texto analisado, de um texto com notação wiki.",
- "apihelp-parse-paramvalue-prop-langlinks": "Fornece os links interlínguas do texto analisado.",
- "apihelp-parse-paramvalue-prop-categories": "Fornece as categorias do texto analisado.",
+ "apihelp-parse-paramvalue-prop-text": "Fornece o texto analisado resultante do texto wiki.",
+ "apihelp-parse-paramvalue-prop-langlinks": "Fornece as hiperligações interlínguas do texto wiki analisado.",
+ "apihelp-parse-paramvalue-prop-categories": "Fornece as categorias do texto wiki analisado.",
"apihelp-parse-paramvalue-prop-categorieshtml": "Fornece a versão HTML das categorias.",
- "apihelp-parse-paramvalue-prop-links": "Fornece os links internos do texto analisado.",
- "apihelp-parse-paramvalue-prop-templates": "Fornece as predefinições do texto analisado.",
- "apihelp-parse-paramvalue-prop-images": "Fornece as imagens do texto analisado.",
- "apihelp-parse-paramvalue-prop-externallinks": "Fornece os links externos do texto analisado.",
- "apihelp-parse-paramvalue-prop-sections": "Fornece as secções do texto analisado.",
+ "apihelp-parse-paramvalue-prop-links": "Fornece as hiperligações internas do texto wiki analisado.",
+ "apihelp-parse-paramvalue-prop-templates": "Fornece as predefinições do texto wiki analisado.",
+ "apihelp-parse-paramvalue-prop-images": "Fornece as imagens do texto wiki analisado.",
+ "apihelp-parse-paramvalue-prop-externallinks": "Fornece as hiperligações externas do texto wiki analisado.",
+ "apihelp-parse-paramvalue-prop-sections": "Fornece as secções do texto wiki analisado.",
"apihelp-parse-paramvalue-prop-revid": "Adiciona o identificador de revisão da página analisada.",
- "apihelp-parse-paramvalue-prop-displaytitle": "Adiciona o título do texto analisado.",
+ "apihelp-parse-paramvalue-prop-displaytitle": "Adiciona o título do texto wiki analisado.",
"apihelp-parse-paramvalue-prop-headitems": "Fornece os elementos a colocar no <code>&lt;head&gt;</code> da página.",
"apihelp-parse-paramvalue-prop-headhtml": "Fornece o <code>&lt;head&gt;</code> analisado da página.",
"apihelp-parse-paramvalue-prop-modules": "Fornece os módulos ResourceLoader usados na página. Para carregá-los, usar <code>mw.loader.using()</code>. Uma das variáveis <kbd>jsconfigvars</kbd> ou <kbd>encodedjsconfigvars</kbd> tem de ser pedida em conjunto com <kbd>modules</kbd>.",
"apihelp-parse-paramvalue-prop-jsconfigvars": "Fornece as variáveis de configuração JavaScript específicas da página. Para aplicá-las, usar <code>mw.config.set()</code>.",
"apihelp-parse-paramvalue-prop-encodedjsconfigvars": "Fornece as variáveis de configuração JavaScript específicas da página, no formato de uma ''string'' JSON.",
"apihelp-parse-paramvalue-prop-indicators": "Fornece o HTML dos indicadores de estado de página que são usados na página.",
- "apihelp-parse-paramvalue-prop-iwlinks": "Fornece os links interwikis do texto analisado.",
- "apihelp-parse-paramvalue-prop-wikitext": "Fornece o texto original com notação wiki que foi analisado.",
+ "apihelp-parse-paramvalue-prop-iwlinks": "Fornece as hiperligações interwikis do texto wiki analisado.",
+ "apihelp-parse-paramvalue-prop-wikitext": "Fornece o texto wiki original que foi analisado.",
"apihelp-parse-paramvalue-prop-properties": "Fornece várias propriedades definidas no texto analisado.",
"apihelp-parse-paramvalue-prop-limitreportdata": "Fornece o relatório de limites de forma estruturada. Não fornece dados quando <var>$1disablelimitreport</var> está definido.",
"apihelp-parse-paramvalue-prop-limitreporthtml": "Fornece a versão HTML do relatório de limites. Não fornece dados quando <var>$1disablelimitreport</var> está definido.",
@@ -352,13 +357,14 @@
"apihelp-parse-param-wrapoutputclass": "A classe CSS a utilizar para envolver o resultado do analisador sintático.",
"apihelp-parse-param-pst": "Fazer uma transformação anterior à gravação do texto de entrada, antes de analisá-lo. Só é válido quando usado com texto.",
"apihelp-parse-param-onlypst": "Fazer uma transformação anterior à gravação (PST, ''pre-save transform'') do texto de entrada, mas não o analisar. Devolve o mesmo texto após aplicação da PST. Só é válido quando usado com <var>$1text</var>.",
- "apihelp-parse-param-effectivelanglinks": "Inclui links interlínguas fornecidos por extensões (para ser usado com <kbd>$1prop=langlinks</kbd>).",
+ "apihelp-parse-param-effectivelanglinks": "Inclui hiperligações interlínguas fornecidas por extensões (para ser usado com <kbd>$1prop=langlinks</kbd>).",
"apihelp-parse-param-section": "Analisar apenas o conteúdo desta secção.\n\nQuando tiver o valor <kbd>new</kbd>, analisar <var>$1text</var> e <var>$1sectiontitle</var> como se fosse adicionar uma nova secção à página.\n\n<kbd>new</kbd> só é permitido quando se especifica <var>text</var>.",
"apihelp-parse-param-sectiontitle": "O novo título da secção quando <var>section</var> tem o valor <kbd>new</kbd>.\n\nAo contrário da edição de páginas, este não toma o valor de <var>summary</var> se for omitido ou estiver vazio.",
"apihelp-parse-param-disablelimitreport": "Omitir o relatório de limites (\"NewPP limit report\") do resultado de saída do analisador sintático.",
"apihelp-parse-param-disablepp": "Em vez deste, usar <var>$1disablelimitreport</var>.",
- "apihelp-parse-param-disableeditsection": "Omitir links para edição da secção no resultado da análise sintática.",
+ "apihelp-parse-param-disableeditsection": "Omitir as hiperligações para edição da secção no resultado da análise sintática.",
"apihelp-parse-param-disabletidy": "Não fazer a limpeza do HTML (isto é, o ''tidy'') no resultado da análise sintática.",
+ "apihelp-parse-param-disablestylededuplication": "Não desduplicar as folhas de estilo internas (etiquetas <nowiki><style></nowiki>) na saída do analisador sintático.",
"apihelp-parse-param-generatexml": "Gerar a árvore de análise XML (requer o modelo de conteúdo <code>$1</code>; substituído por <kbd>$2prop=parsetree</kbd>).",
"apihelp-parse-param-preview": "Executar a análise em modo de antevisão.",
"apihelp-parse-param-sectionpreview": "Executar a análise em modo de antevisão (também ativa o modo de antevisão).",
@@ -367,8 +373,8 @@
"apihelp-parse-param-contentformat": "O formato da seriação de conteúdo, usado para o texto de entrada. Só é válido quando usado com $1text.",
"apihelp-parse-param-contentmodel": "Modelo de conteúdo do texto de entrada. Se omitido, $1title tem de ser especificado e o valor por omissão será o modelo do título especificado. Só é válido quando usado com $1text.",
"apihelp-parse-example-page": "Fazer a análise sintática de uma página.",
- "apihelp-parse-example-text": "Fazer a análise sintática do texto com notação wiki.",
- "apihelp-parse-example-texttitle": "Fazer a análise sintática do texto com notação wiki, especificando o título da página.",
+ "apihelp-parse-example-text": "Fazer a análise sintática do texto wiki.",
+ "apihelp-parse-example-texttitle": "Fazer a análise sintática do texto wiki, especificando o título da página.",
"apihelp-parse-example-summary": "Fazer a análise sintática de um resumo.",
"apihelp-patrol-summary": "Patrulhar uma página ou revisão.",
"apihelp-patrol-param-rcid": "Identificador da mudança recente a patrulhar.",
@@ -389,22 +395,22 @@
"apihelp-protect-example-protect": "Proteger uma página.",
"apihelp-protect-example-unprotect": "Desproteger uma página definindo a restrição <kbd>all</kbd> (isto é, todos podem executar a operação).",
"apihelp-protect-example-unprotect2": "Desproteger uma página definindo que não há restrições.",
- "apihelp-purge-summary": "Limpar a ''cache'' para os títulos especificados.",
- "apihelp-purge-param-forcelinkupdate": "Atualizar as tabelas de ligações.",
- "apihelp-purge-param-forcerecursivelinkupdate": "Atualizar a tabela de ligações, e atualizar as tabelas de ligações de qualquer página que usa esta página como modelo.",
+ "apihelp-purge-summary": "Limpar a cache para os títulos especificados.",
+ "apihelp-purge-param-forcelinkupdate": "Atualizar as tabelas de hiperligações.",
+ "apihelp-purge-param-forcerecursivelinkupdate": "Atualizar a tabela de hiperligações, e atualizar as tabelas de hiperligações de qualquer página que usa esta página como modelo.",
"apihelp-purge-example-simple": "Purgar as páginas <kbd>Main Page</kbd> e <kbd>API</kbd>.",
"apihelp-purge-example-generator": "Purgar as primeiras 10 páginas no espaço nominal principal.",
"apihelp-query-summary": "Obter dados de, e sobre, o MediaWiki.",
- "apihelp-query-extended-description": "Todas as modificações de dados terão primeiro que usar uma consulta para adquirir uma chave, o que visa impedir abusos de sites maliciosos.",
+ "apihelp-query-extended-description": "Todas as modificações de dados terão primeiro que usar uma consulta para adquirir uma chave, o que visa impedir abusos de sítios maliciosos.",
"apihelp-query-param-prop": "As propriedades a serem obtidas para as páginas consultadas.",
"apihelp-query-param-list": "As listas a serem obtidas.",
"apihelp-query-param-meta": "Os metadados a serem obtidos.",
"apihelp-query-param-indexpageids": "Incluir uma secção adicional de identificadores de página que lista todos os identificadores de página devolvidos.",
"apihelp-query-param-export": "Exportar as revisões atuais de todas as páginas fornecidas ou geradas.",
"apihelp-query-param-exportnowrap": "Devolver o XML de exportação sem envolvê-lo num resultado XML (o mesmo formato que [[Special:Export]]). Só pode ser usado com $1export.",
- "apihelp-query-param-iwurl": "Indica se deve ser obtido o URL completo quando o título é um ''link'' interwikis.",
+ "apihelp-query-param-iwurl": "Indica se deve ser obtido o URL completo quando o título é uma hiperligação interwikis.",
"apihelp-query-param-rawcontinue": "Devolver os dados em bruto de <samp>query-continue</samp> para continuar.",
- "apihelp-query-example-revisions": "Obter [[Special:ApiHelp/query+siteinfo|informação do ''site'']] e as [[Special:ApiHelp/query+revisions|revisões]] da página <kbd>Main Page</kbd>.",
+ "apihelp-query-example-revisions": "Obter [[Special:ApiHelp/query+siteinfo|informação do sítio]] e as [[Special:ApiHelp/query+revisions|revisões]] da página <kbd>Main Page</kbd>.",
"apihelp-query-example-allpages": "Obter as revisões das páginas que começam por <kbd>API/</kbd>.",
"apihelp-query+allcategories-summary": "Enumerar todas as categorias.",
"apihelp-query+allcategories-param-from": "A categoria a partir da qual será começada a enumeração.",
@@ -469,25 +475,25 @@
"apihelp-query+allimages-example-recent": "Mostrar uma lista dos ficheiros carregados recentemente, semelhante a [[Special:NewFiles]].",
"apihelp-query+allimages-example-mimetypes": "Mostrar uma lista dos ficheiros com os tipos MIME <kbd>image/png</kbd> ou <kbd>image/gif</kbd>.",
"apihelp-query+allimages-example-generator": "Mostrar informação sobre 4 ficheiros, começando pela letra <kbd>T</kbd>.",
- "apihelp-query+alllinks-summary": "Enumerar todos os ''links'' que apontam para um determinado espaço nominal.",
- "apihelp-query+alllinks-param-from": "O título do ''link'' a partir do qual será começada a enumeração.",
- "apihelp-query+alllinks-param-to": "O título do ''link'' no qual será terminada a enumeração.",
+ "apihelp-query+alllinks-summary": "Enumerar todas as hiperligações que apontam para um determinado espaço nominal.",
+ "apihelp-query+alllinks-param-from": "O título da hiperligação a partir da qual será começada a enumeração.",
+ "apihelp-query+alllinks-param-to": "O título da hiperligação na qual será terminada a enumeração.",
"apihelp-query+alllinks-param-prefix": "Procurar todos os títulos ligados que começam por este valor.",
"apihelp-query+alllinks-param-unique": "Mostrar só títulos ligados únicos. Não pode ser usado com <kbd>$1prop=ids</kbd>.\nAo ser usado como gerador, produz páginas de destino em vez de páginas de origem.",
"apihelp-query+alllinks-param-prop": "As informações que devem ser incluídas:",
- "apihelp-query+alllinks-paramvalue-prop-ids": "Adiciona o identificador da página que contém a ligação (não pode ser usado com <var>$1unique</var>).",
- "apihelp-query+alllinks-paramvalue-prop-title": "Adiciona o título do ''link''.",
+ "apihelp-query+alllinks-paramvalue-prop-ids": "Adiciona o identificador da página que contém a hiperligação (não pode ser usado com <var>$1unique</var>).",
+ "apihelp-query+alllinks-paramvalue-prop-title": "Adiciona o título da hiperligação.",
"apihelp-query+alllinks-param-namespace": "O espaço nominal a ser enumerado.",
"apihelp-query+alllinks-param-limit": "O número total de entradas a serem devolvidas.",
"apihelp-query+alllinks-param-dir": "A direção de listagem.",
"apihelp-query+alllinks-example-B": "Listar os títulos para os quais existem ligações, incluindo títulos em falta, com os identificadores das páginas que contêm as respetivas ligações, começando pela letra <kbd>B</kbd>.",
- "apihelp-query+alllinks-example-unique": "Listar os títulos únicos para os quais existem ligações.",
- "apihelp-query+alllinks-example-unique-generator": "Obtém todos os títulos para os quais existem ligações, marcando aqueles em falta.",
- "apihelp-query+alllinks-example-generator": "Obtém as páginas que contêm as ligações.",
- "apihelp-query+allmessages-summary": "Devolver as mensagens deste ''site''.",
+ "apihelp-query+alllinks-example-unique": "Listar os títulos únicos para os quais existem hiperligações.",
+ "apihelp-query+alllinks-example-unique-generator": "Obtém todos os títulos para os quais existem hiperligações, marcando aqueles em falta.",
+ "apihelp-query+alllinks-example-generator": "Obtém as páginas que contêm as hiperligações.",
+ "apihelp-query+allmessages-summary": "Devolver as mensagens deste sítio.",
"apihelp-query+allmessages-param-messages": "Mensagens a serem produzidas no resultado. <kbd>*</kbd> (o valor por omissão) significa todas as mensagens.",
"apihelp-query+allmessages-param-prop": "As propriedades a serem obtidas:",
- "apihelp-query+allmessages-param-enableparser": "Definir, para ativar o analisador sintático e pré-processar o texto da mensagem com notação wiki (substituir palavras mágicas, processar predefinições, etc.).",
+ "apihelp-query+allmessages-param-enableparser": "Definir para ativar o analisador sintático; irá pré-processar o texto wiki da mensagem (substituir palavras mágicas, processar predefinições, etc.).",
"apihelp-query+allmessages-param-nocontent": "Se definido, não incluir o conteúdo das mensagens no resultado de saída.",
"apihelp-query+allmessages-param-includelocal": "Incluir também as mensagens locais, isto é, mensagens que não existem no software mas existem como uma página no espaço nominal {{ns:MediaWiki}}.\nIsto lista todas as páginas do espaço nominal {{ns:MediaWiki}}, portanto, também irá listar aquelas que não são verdadeiramente mensagens, como [[MediaWiki:Common.js|Common.js]].",
"apihelp-query+allmessages-param-args": "Os argumentos a serem substituídos na mensagem.",
@@ -513,7 +519,7 @@
"apihelp-query+allpages-param-prfiltercascade": "Filtrar as proteções com base na proteção em cascata (ignorado se $1prtype não estiver presente).",
"apihelp-query+allpages-param-limit": "O número total de páginas a serem devolvidas.",
"apihelp-query+allpages-param-dir": "A direção de listagem.",
- "apihelp-query+allpages-param-filterlanglinks": "Filtrar dependo de uma página ter ''links'' interlínguas. Note que isto pode não tomar em consideração ''links'' interlínguas adicionados por extensões.",
+ "apihelp-query+allpages-param-filterlanglinks": "Filtrar dependo de uma página conter hiperligações interlínguas. Note que isto pode não ter em consideração hiperligações interlínguas adicionadas por extensões.",
"apihelp-query+allpages-param-prexpiry": "O tipo de expiração pelo qual as páginas serão filtradas:\n;indefinite:Obter só páginas com um período de expiração indefinido.\n;definite:Obter só páginas com um período de expiração definido (específico).\n;all:Obter páginas com qualquer período de expiração.",
"apihelp-query+allpages-example-B": "Mostrar uma lista de páginas, começando na letra <kbd>B</kbd>.",
"apihelp-query+allpages-example-generator": "Mostrar informação sobre 4 páginas, começando na letra <kbd>T</kbd>.",
@@ -592,16 +598,16 @@
"apihelp-query+authmanagerinfo-example-login": "Obter os pedidos que podem ser usados ao iniciar uma sessão.",
"apihelp-query+authmanagerinfo-example-login-merged": "Obter os pedidos que podem ser usados ao iniciar uma sessão, com os campos combinados.",
"apihelp-query+authmanagerinfo-example-securitysensitiveoperation": "Testar se a autenticação é suficiente para a operação <kbd>foo</kbd>.",
- "apihelp-query+backlinks-summary": "Encontrar todas as páginas que contêm ligações para a página indicada.",
+ "apihelp-query+backlinks-summary": "Encontrar todas as páginas que contêm hiperligações para a página indicada.",
"apihelp-query+backlinks-param-title": "O título a ser procurado. Não pode ser usado em conjunto com <var>$1pageid</var>.",
"apihelp-query+backlinks-param-pageid": "O identificador do título a ser procurado. Não pode ser usado em conjunto com <var>$1title</var>.",
"apihelp-query+backlinks-param-namespace": "O espaço nominal a ser enumerado.",
"apihelp-query+backlinks-param-dir": "A direção de listagem.",
"apihelp-query+backlinks-param-filterredir": "Como filtrar os redirecionamentos. Se definido como <kbd>nonredirects</kbd> quando <var>$1redirect</var> está ativado, isto só é aplicado ao segundo nível.",
"apihelp-query+backlinks-param-limit": "O número total de páginas a serem devolvidas. Se <var>$1redirect</var> estiver ativado, o limite aplica-se a cada nível em separado (o que significa que até 2 * <var>$1limit</var> resultados podem ser devolvidos).",
- "apihelp-query+backlinks-param-redirect": "Se a página que contém a ligação é um redirecionamento, procurar também todas as páginas que contêm ligações para esse redirecionamento. O limite máximo é reduzido para metade.",
- "apihelp-query+backlinks-example-simple": "Mostrar as ligações para <kbd>Main page</kbd>.",
- "apihelp-query+backlinks-example-generator": "Obter informações sobre as páginas com ligações para <kbd>Main page</kbd>.",
+ "apihelp-query+backlinks-param-redirect": "Se a página que contém a hiperligação é um redirecionamento, procurar também todas as páginas que contêm hiperligações para esse redirecionamento. O limite máximo é reduzido para metade.",
+ "apihelp-query+backlinks-example-simple": "Mostrar as hiperligações para <kbd>Main page</kbd>.",
+ "apihelp-query+backlinks-example-generator": "Obter informações sobre as páginas com hiperligações para <kbd>Main page</kbd>.",
"apihelp-query+blocks-summary": "Listar todos os utilizadores e endereços IP bloqueados.",
"apihelp-query+blocks-param-start": "A data e hora a partir da qual será começada a enumeração.",
"apihelp-query+blocks-param-end": "A data e hora na qual será terminada a enumeração.",
@@ -713,22 +719,22 @@
"apihelp-query+embeddedin-example-simple": "Mostrar as páginas que transcluem <kbd>Template:Stub</kbd>.",
"apihelp-query+embeddedin-example-generator": "Obter informação sobre as páginas que transcluem <kbd>Template:Stub</kbd>.",
"apihelp-query+extlinks-summary": "Devolve todos os URL externos (que não sejam interwikis) das páginas especificadas.",
- "apihelp-query+extlinks-param-limit": "O número de ''links'' a serem devolvidos.",
- "apihelp-query+extlinks-param-protocol": "Protocolo do URL. Se vazio e <var>$1query</var> está definido, o protocolo é <kbd>http</kbd>. Deixe isto e <var>$1query</var> vazios para listar todos os ''links'' externos.",
+ "apihelp-query+extlinks-param-limit": "O número de hiperligações a serem devolvidas.",
+ "apihelp-query+extlinks-param-protocol": "Protocolo do URL. Se vazio e <var>$1query</var> está definido, o protocolo é <kbd>http</kbd>. Deixe este parâmetro e <var>$1query</var> vazios para listar todas as hiperligações externas.",
"apihelp-query+extlinks-param-query": "Texto de pesquisa sem protocolo. Útil para verificar se uma determinada página contém um determinado URL externo.",
"apihelp-query+extlinks-param-expandurl": "Expandir os URL relativos a protocolo com o protocolo canónico.",
- "apihelp-query+extlinks-example-simple": "Obter uma lista das ligações externas na <kbd>Main Page</kbd>.",
+ "apihelp-query+extlinks-example-simple": "Obter uma lista das hiperligações externas da <kbd>Main Page</kbd>.",
"apihelp-query+exturlusage-summary": "Enumerar as páginas que contêm um determinado URL.",
"apihelp-query+exturlusage-param-prop": "As informações que devem ser incluídas:",
"apihelp-query+exturlusage-paramvalue-prop-ids": "Adiciona o identificador da página.",
"apihelp-query+exturlusage-paramvalue-prop-title": "Adiciona o título e o identificador do espaço nominal da página.",
"apihelp-query+exturlusage-paramvalue-prop-url": "Adiciona o URL usado na página.",
- "apihelp-query+exturlusage-param-protocol": "Protocolo do URL. Se vazio e <var>$1query</var> está definido, o protocolo é <kbd>http</kbd>. Deixe isto e <var>$1query</var> vazios para listar todos os ''links'' externos.",
- "apihelp-query+exturlusage-param-query": "Texto da pesquisa sem um protocolo. Ver [[Special:LinkSearch]]. Deixar vazio para listar todos os ''links'' externos.",
+ "apihelp-query+exturlusage-param-protocol": "Protocolo do URL. Se vazio e <var>$1query</var> está definido, o protocolo é <kbd>http</kbd>. Deixe este parâmetro e <var>$1query</var> vazios para listar todas as hiperligações externas.",
+ "apihelp-query+exturlusage-param-query": "Texto da pesquisa sem um protocolo. Ver [[Special:LinkSearch]]. Deixar vazio para listar todas as hiperligações externas.",
"apihelp-query+exturlusage-param-namespace": "Os espaços nominais a serem enumerados.",
"apihelp-query+exturlusage-param-limit": "O número de páginas a serem devolvidas.",
"apihelp-query+exturlusage-param-expandurl": "Expandir os URL relativos a protocolo com o protocolo canónico.",
- "apihelp-query+exturlusage-example-simple": "Mostrar as páginas com ligações para <kbd>http://www.mediawiki.org</kbd>.",
+ "apihelp-query+exturlusage-example-simple": "Mostrar as páginas com hiperligações para <kbd>https://www.mediawiki.org</kbd>.",
"apihelp-query+filearchive-summary": "Enumerar todos os ficheiros eliminados sequencialmente.",
"apihelp-query+filearchive-param-from": "O título da imagem a partir do qual será começada a enumeração.",
"apihelp-query+filearchive-param-to": "O título da imagem no qual será terminada a enumeração.",
@@ -813,7 +819,7 @@
"apihelp-query+imageusage-param-dir": "A direção de listagem.",
"apihelp-query+imageusage-param-filterredir": "Como filtrar redirecionamentos. Se definido como <kbd>nonredirects</kbd> quando <var>$1redirect</var> está ativado, isto só é aplicado ao segundo nível.",
"apihelp-query+imageusage-param-limit": "O número total de páginas a serem devolvidas. Se <var>$1redirect</var> estiver ativado, o nível aplica-se a cada nível em separado (o que significa que até 2 * <var>$1limit</var> resultados podem ser devolvidos).",
- "apihelp-query+imageusage-param-redirect": "Se a página que contém a ligação é um redirecionamento, procurar também todas as páginas que contêm ligações para esse redirecionamento. O limite máximo é reduzido para metade.",
+ "apihelp-query+imageusage-param-redirect": "Se a página que contém a hiperligação é um redirecionamento, procurar também todas as páginas que contêm hiperligações para esse redirecionamento. O limite máximo é reduzido para metade.",
"apihelp-query+imageusage-example-simple": "Mostrar as páginas que usam [[:File:Albert Einstein Head.jpg]].",
"apihelp-query+imageusage-example-generator": "Obter informações sobre as páginas que usam o ficheiro [[:File:Albert Einstein Head.jpg]].",
"apihelp-query+info-summary": "Obter a informação básica da página.",
@@ -829,62 +835,63 @@
"apihelp-query+info-paramvalue-prop-readable": "Indica se o utilizador pode ler esta página.",
"apihelp-query+info-paramvalue-prop-preload": "Fornece o texto devolvido por EditFormPreloadText.",
"apihelp-query+info-paramvalue-prop-displaytitle": "Fornece a forma como o título da página é apresentado.",
+ "apihelp-query+info-paramvalue-prop-varianttitles": "Fornece o título de apresentação em todas as variantes da língua de conteúdo da wiki.",
"apihelp-query+info-param-testactions": "Testar se o utilizador pode realizar certas operações na página.",
"apihelp-query+info-param-token": "Em substituição, usar [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].",
"apihelp-query+info-example-simple": "Obter informações sobre a página <kbd>Main Page</kbd>.",
"apihelp-query+info-example-protection": "Obter informação geral e de proteção sobre a página <kbd>Main Page</kbd>.",
- "apihelp-query+iwbacklinks-summary": "Encontrar todas as páginas que contêm ''links'' para as páginas indicadas.",
- "apihelp-query+iwbacklinks-extended-description": "Pode ser usado para encontrar todos os ''links'' com um prefixo, ou todos os ''links'' para um título (com um prefixo especificado). Se nenhum parâmetro for usado, isso efetivamente significa \"todos os ''links'' interwikis\".",
+ "apihelp-query+iwbacklinks-summary": "Encontrar todas as páginas que contêm hiperligações para as páginas indicadas.",
+ "apihelp-query+iwbacklinks-extended-description": "Pode ser usado para encontrar todas as hiperligações com um prefixo, ou todas as hiperligações para um título (com um prefixo especificado). Se nenhum dos parâmetros for usado, isso efetivamente significa \"todas as hiperligações interwikis\".",
"apihelp-query+iwbacklinks-param-prefix": "O prefixo interwikis.",
- "apihelp-query+iwbacklinks-param-title": "O ''link'' interwikis a ser procurado. Tem de ser usado em conjunto com <var>$1blprefix</var>.",
+ "apihelp-query+iwbacklinks-param-title": "A hiperligação interwikis a ser procurada. Tem de ser usado em conjunto com <var>$1blprefix</var>.",
"apihelp-query+iwbacklinks-param-limit": "O número total de páginas a serem devolvidas.",
"apihelp-query+iwbacklinks-param-prop": "As propriedades a serem obtidas:",
- "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "Adiciona o prefixo do ''link'' interwikis.",
- "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "Adiciona o título do ''link'' interwikis.",
+ "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "Adiciona o prefixo da hiperligação interwikis.",
+ "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "Adiciona o título da hiperligação interwikis.",
"apihelp-query+iwbacklinks-param-dir": "A direção de listagem.",
- "apihelp-query+iwbacklinks-example-simple": "Obter as páginas que contêm ligações para [[wikibooks:Test]].",
- "apihelp-query+iwbacklinks-example-generator": "Obter informação sobre as páginas que contêm ligações para [[wikibooks:Test]].",
- "apihelp-query+iwlinks-summary": "Devolve todos os ''links'' interwikis das páginas indicadas.",
+ "apihelp-query+iwbacklinks-example-simple": "Obter as páginas que contêm hiperligações para [[wikibooks:Test]].",
+ "apihelp-query+iwbacklinks-example-generator": "Obter informação sobre as páginas que contêm hiperligações para [[wikibooks:Test]].",
+ "apihelp-query+iwlinks-summary": "Devolve todas as hiperligações interwikis das páginas indicadas.",
"apihelp-query+iwlinks-param-url": "Indica se deve ser obtido o URL completo (não pode ser usado com $1prop).",
- "apihelp-query+iwlinks-param-prop": "As propriedades adicionais que devem ser obtidas para cada ''link'' interlínguas:",
+ "apihelp-query+iwlinks-param-prop": "As propriedades adicionais que devem ser obtidas para cada hiperligação interlínguas:",
"apihelp-query+iwlinks-paramvalue-prop-url": "Adiciona o URL completo.",
- "apihelp-query+iwlinks-param-limit": "O número de ''links'' interwikis a serem devolvidos.",
- "apihelp-query+iwlinks-param-prefix": "Devolver só os ''links'' interwikis com este prefixo.",
- "apihelp-query+iwlinks-param-title": "Link interwikis a ser procurado. Tem de ser usado em conjunto com <var>$1prefix</var>.",
+ "apihelp-query+iwlinks-param-limit": "O número de hiperligações interwikis a serem devolvidas.",
+ "apihelp-query+iwlinks-param-prefix": "Devolver só as hiperligações interwikis com este prefixo.",
+ "apihelp-query+iwlinks-param-title": "Hiperligação interwikis a ser procurada. Tem de ser usado em conjunto com <var>$1prefix</var>.",
"apihelp-query+iwlinks-param-dir": "A direção de listagem.",
- "apihelp-query+iwlinks-example-simple": "Obter os ''links'' interwikis da página <kbd>Main Page</kbd>.",
- "apihelp-query+langbacklinks-summary": "Encontrar todas as páginas que contêm ''links'' para o ''link'' interlínguas indicado.",
- "apihelp-query+langbacklinks-extended-description": "Pode ser usado para encontrar todos os ''links'' para um determinado código de língua, ou todos os ''links'' para um determinado título (de uma língua). Se nenhum for usado, isso efetivamente significa \"todos os ''links'' interlínguas\".\n\nNote que os ''links'' interlínguas adicionados por extensões podem não ser considerados.",
- "apihelp-query+langbacklinks-param-lang": "A língua do ''link'' interlínguas.",
- "apihelp-query+langbacklinks-param-title": "Link interlínguas a ser procurado. Tem de ser usado com $1lang.",
+ "apihelp-query+iwlinks-example-simple": "Obter as hiperligações interwikis da página <kbd>Main Page</kbd>.",
+ "apihelp-query+langbacklinks-summary": "Encontrar todas as páginas que contêm hiperligações para a hiperligação interlínguas indicada.",
+ "apihelp-query+langbacklinks-extended-description": "Pode ser usado para encontrar todas as hiperligações para um determinado código de língua, ou todas as hiperligações para um determinado título (de uma língua). Se nenhum dos parâmetros for usado, isso efetivamente significa \"todas as hiperligações interlínguas\".\n\nNote que as hiperligações interlínguas adicionadas por extensões podem não ser consideradas.",
+ "apihelp-query+langbacklinks-param-lang": "A língua da hiperligação da língua.",
+ "apihelp-query+langbacklinks-param-title": "Hiperligação interlínguas a ser procurada. Tem de ser usado com $1lang.",
"apihelp-query+langbacklinks-param-limit": "O número total de páginas a serem devolvidas.",
"apihelp-query+langbacklinks-param-prop": "As propriedades a serem obtidas:",
- "apihelp-query+langbacklinks-paramvalue-prop-lllang": "Adiciona o código de língua da ligação interlínguas.",
- "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "Adiciona o título do ''link'' interlínguas.",
+ "apihelp-query+langbacklinks-paramvalue-prop-lllang": "Adiciona o código de língua da hiperligação interlínguas.",
+ "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "Adiciona o título da hiperligação interlínguas.",
"apihelp-query+langbacklinks-param-dir": "A direção de listagem.",
- "apihelp-query+langbacklinks-example-simple": "Obter as páginas que contêm ligações para [[:fr:Test]].",
- "apihelp-query+langbacklinks-example-generator": "Obter informações sobre as páginas que contêm ligações para [[:fr:Test]].",
- "apihelp-query+langlinks-summary": "Devolve todos os ''links'' interlínguas das páginas indicadas.",
- "apihelp-query+langlinks-param-limit": "O número de ''links'' interlínguas a serem devolvidos.",
+ "apihelp-query+langbacklinks-example-simple": "Obter as páginas que contêm hiperligações para [[:fr:Test]].",
+ "apihelp-query+langbacklinks-example-generator": "Obter informações sobre as páginas que contêm hiperligações para [[:fr:Test]].",
+ "apihelp-query+langlinks-summary": "Devolve todas as hiperligações interlínguas das páginas indicadas.",
+ "apihelp-query+langlinks-param-limit": "O número de hiperligações interlínguas a serem devolvidas.",
"apihelp-query+langlinks-param-url": "Indica se deve ser obtido o URL completo (não pode ser usado com $1prop).",
- "apihelp-query+langlinks-param-prop": "As propriedades adicionais que devem ser obtidas para cada ''link'' interlínguas:",
+ "apihelp-query+langlinks-param-prop": "As propriedades adicionais que devem ser obtidas para cada hiperligação interlínguas:",
"apihelp-query+langlinks-paramvalue-prop-url": "Adiciona o URL completo.",
"apihelp-query+langlinks-paramvalue-prop-langname": "Adiciona o nome da língua localizado (melhor esforço). Usar <var>$1inlanguagecode</var> para controlar a língua.",
"apihelp-query+langlinks-paramvalue-prop-autonym": "Adiciona o nome nativo da língua.",
- "apihelp-query+langlinks-param-lang": "Devolver só os ''links'' interlínguas com este código de língua.",
- "apihelp-query+langlinks-param-title": "''Link'' a ser procurado. Tem de ser usado com <var>$1lang</var>.",
+ "apihelp-query+langlinks-param-lang": "Devolver só as hiperligações interlínguas com este código de língua.",
+ "apihelp-query+langlinks-param-title": "A hiperligação a ser procurada. Tem de ser usado com <var>$1lang</var>.",
"apihelp-query+langlinks-param-dir": "A direção de listagem.",
"apihelp-query+langlinks-param-inlanguagecode": "O código de língua para os nomes de língua localizados.",
- "apihelp-query+langlinks-example-simple": "Obter os ''links'' interlínguas da página <kbd>Main Page</kbd>.",
- "apihelp-query+links-summary": "Devolve todos os ''links'' das páginas indicadas.",
- "apihelp-query+links-param-namespace": "Mostrar apenas os ''links'' destes espaços nominais.",
- "apihelp-query+links-param-limit": "O número de ''links'' a serem devolvidos.",
- "apihelp-query+links-param-titles": "Listar só as ligações para estes títulos. Útil para verificar se uma determinada página contém ligações para um determinado título.",
+ "apihelp-query+langlinks-example-simple": "Obter as hiperligações interlínguas da página <kbd>Main Page</kbd>.",
+ "apihelp-query+links-summary": "Devolve todas as hiperligações das páginas indicadas.",
+ "apihelp-query+links-param-namespace": "Mostrar apenas as hiperligações destes espaços nominais.",
+ "apihelp-query+links-param-limit": "O número de hiperligações a serem devolvidas.",
+ "apihelp-query+links-param-titles": "Listar só as hiperligações para estes títulos. Útil para verificar se uma determinada página contém hiperligações para um determinado título.",
"apihelp-query+links-param-dir": "A direção de listagem.",
- "apihelp-query+links-example-simple": "Obter os ''links'' da página <kbd>Main Page</kbd>.",
+ "apihelp-query+links-example-simple": "Obter as hiperligações da página <kbd>Main Page</kbd>.",
"apihelp-query+links-example-generator": "Obter informação sobre as páginas ligadas na página <kbd>Main Page</kbd>.",
- "apihelp-query+links-example-namespaces": "Obter os ''links'' da página <kbd>Main Page</kbd> nos espaços nominais {{ns:user}} e {{ns:template}}.",
- "apihelp-query+linkshere-summary": "Encontrar todas as páginas que contêm ''links'' para as páginas indicadas.",
+ "apihelp-query+links-example-namespaces": "Obter as hiperligações da página <kbd>Main Page</kbd> nos espaços nominais {{ns:user}} e {{ns:template}}.",
+ "apihelp-query+linkshere-summary": "Encontrar todas as páginas que contêm hiperligações para as páginas indicadas.",
"apihelp-query+linkshere-param-prop": "As propriedades a serem obtidas:",
"apihelp-query+linkshere-paramvalue-prop-pageid": "O identificador de cada página.",
"apihelp-query+linkshere-paramvalue-prop-title": "O título de cada página.",
@@ -892,8 +899,8 @@
"apihelp-query+linkshere-param-namespace": "Incluir só as páginas nestes espaços nominais.",
"apihelp-query+linkshere-param-limit": "O número de páginas a serem devolvidas.",
"apihelp-query+linkshere-param-show": "Mostrar só as páginas que correspondem a estes critérios:\n;redirect:Mostrar só os redirecionamentos.\n;!redirect:Mostrar só os não redirecionamentos.",
- "apihelp-query+linkshere-example-simple": "Obter uma lista das páginas com ligações para a página [[Main Page]].",
- "apihelp-query+linkshere-example-generator": "Obter informação sobre as páginas com ligações para a página [[Main Page]].",
+ "apihelp-query+linkshere-example-simple": "Obter uma lista das páginas com hiperligações para a página [[Main Page]].",
+ "apihelp-query+linkshere-example-generator": "Obter informação sobre as páginas com hiperligações para a página [[Main Page]].",
"apihelp-query+logevents-summary": "Obter eventos dos registos.",
"apihelp-query+logevents-param-prop": "As propriedades a serem obtidas:",
"apihelp-query+logevents-paramvalue-prop-ids": "Adiciona o identificador do evento do registo.",
@@ -936,7 +943,7 @@
"apihelp-query+prefixsearch-summary": "Realizar uma procura de prefixo nos títulos de página.",
"apihelp-query+prefixsearch-extended-description": "Apesar da semelhança de nomes, este módulo não pretende ser equivalente a [[Special:PrefixIndex]]; para este, consulte <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> com o parâmetro <kbd>apprefix</kbd>. O propósito deste módulo é semelhante a <kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd>: receber dados introduzidos pelo utilizador e devolver os títulos com melhor correspondência. Dependendo do motor de busca do servidor, isto pode incluir correções de erros ortográficos, evitar redirecionamentos, e outras heurísticas.",
"apihelp-query+prefixsearch-param-search": "O texto a ser pesquisado.",
- "apihelp-query+prefixsearch-param-namespace": "Os espaços nominais onde realizar a pesquisa.",
+ "apihelp-query+prefixsearch-param-namespace": "Os espaços nominais onde realizar a pesquisa. Ignorados se <var>$1search</var> começar com um prefixo de espaço nominal válido.",
"apihelp-query+prefixsearch-param-limit": "O número máximo de resultados a serem devolvidos.",
"apihelp-query+prefixsearch-param-offset": "O número de resultados a serem omitidos.",
"apihelp-query+prefixsearch-example-simple": "Procurar os títulos de página que começam por <kbd>meaning</kbd>.",
@@ -956,7 +963,7 @@
"apihelp-query+protectedtitles-paramvalue-prop-expiry": "Adiciona a data e hora a que a proteção será removida.",
"apihelp-query+protectedtitles-paramvalue-prop-level": "Adiciona o nível de proteção.",
"apihelp-query+protectedtitles-example-simple": "Lista os títulos protegidos.",
- "apihelp-query+protectedtitles-example-generator": "Encontrar as ligações para os títulos protegidos que pertencem ao espaço nominal principal.",
+ "apihelp-query+protectedtitles-example-generator": "Encontrar as hiperligações para os títulos protegidos que pertencem ao espaço nominal principal.",
"apihelp-query+querypage-summary": "Obter uma lista fornecida por uma página especial baseada em consultas (''QueryPage'').",
"apihelp-query+querypage-param-page": "O nome da página especial. Note que este é sensível a maiúsculas e minúsculas.",
"apihelp-query+querypage-param-limit": "O número de resultados a serem devolvidos.",
@@ -988,6 +995,7 @@
"apihelp-query+recentchanges-paramvalue-prop-sizes": "Adiciona os tamanhos antigo e novo da página em ''bytes''.",
"apihelp-query+recentchanges-paramvalue-prop-redirect": "Etiqueta a página se esta for um redirecionamento.",
"apihelp-query+recentchanges-paramvalue-prop-patrolled": "Etiqueta as edições que podem ser patrulhadas, marcando-as como patrulhadas ou não patrulhadas.",
+ "apihelp-query+recentchanges-paramvalue-prop-autopatrolled": "Etiqueta as edições que podem ser patrulhadas, marcando-as como autopatrulhadas ou não.",
"apihelp-query+recentchanges-paramvalue-prop-loginfo": "Adiciona informação de registo (identificador do registo, tipo de entrada, etc.) às entradas do registo.",
"apihelp-query+recentchanges-paramvalue-prop-tags": "Lista as etiquetas da entrada.",
"apihelp-query+recentchanges-paramvalue-prop-sha1": "Adiciona a soma de controlo do conteúdo para as entradas associadas com uma revisão.",
@@ -1067,6 +1075,7 @@
"apihelp-query+search-paramvalue-prop-sectiontitle": "Adiciona o título da secção correspondente.",
"apihelp-query+search-paramvalue-prop-categorysnippet": "Adiciona um fragmento de código com a categoria correspondente, após análise sintática.",
"apihelp-query+search-paramvalue-prop-isfilematch": "Adiciona um valor booleano que indica se a pesquisa encontrou correspondência no conteúdo de ficheiros.",
+ "apihelp-query+search-paramvalue-prop-extensiondata": "Acrescenta dados adicionais gerados por extensões.",
"apihelp-query+search-paramvalue-prop-score": "Ignorado.",
"apihelp-query+search-paramvalue-prop-hasrelated": "Ignorado.",
"apihelp-query+search-param-limit": "O número total de páginas a serem devolvidas.",
@@ -1076,14 +1085,14 @@
"apihelp-query+search-example-simple": "Pesquisar <kbd>meaning</kbd>.",
"apihelp-query+search-example-text": "Pesquisar <kbd>meaning</kbd> nos textos.",
"apihelp-query+search-example-generator": "Obter informação sobre as páginas devolvidas por uma pesquisa do termo <kbd>meaning</kbd>.",
- "apihelp-query+siteinfo-summary": "Devolver informação geral sobre o ''site''.",
+ "apihelp-query+siteinfo-summary": "Devolver informação geral sobre o sítio.",
"apihelp-query+siteinfo-param-prop": "A informação a ser obtida:",
"apihelp-query+siteinfo-paramvalue-prop-general": "Informação global do sistema.",
"apihelp-query+siteinfo-paramvalue-prop-namespaces": "Uma lista dos espaços nominais registados e dos seus nomes canónicos.",
"apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "Uma lista dos nomes alternativos dos espaços nominais registados.",
"apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "Uma lista dos nomes alternativos das páginas especiais.",
"apihelp-query+siteinfo-paramvalue-prop-magicwords": "Uma lista das palavras mágicas e dos seus nomes alternativos.",
- "apihelp-query+siteinfo-paramvalue-prop-statistics": "Devolve as estatísticas do ''site''.",
+ "apihelp-query+siteinfo-paramvalue-prop-statistics": "Devolve as estatísticas do sítio.",
"apihelp-query+siteinfo-paramvalue-prop-interwikimap": "Devolve o mapa de interwikis (opcionalmente filtrado, opcionalmente localizado usando <var>$1inlanguagecode</var>).",
"apihelp-query+siteinfo-paramvalue-prop-dbrepllag": "Devolve o servidor da base de dados com o maior atraso de replicação.",
"apihelp-query+siteinfo-paramvalue-prop-usergroups": "Devolve os grupos de utilizadores e as permissões associadas.",
@@ -1099,14 +1108,14 @@
"apihelp-query+siteinfo-paramvalue-prop-functionhooks": "Devolve uma lista dos ''hooks'' de funções do analisador sintático.",
"apihelp-query+siteinfo-paramvalue-prop-showhooks": "Devolve uma lista de todos os ''hooks'' subscritos (conteúdo de <var>[[mw:Special:MyLanguage/Manual:$wgHooks|$wgHooks]]</var>).",
"apihelp-query+siteinfo-paramvalue-prop-variables": "Devolve uma lista de identificadores de variáveis.",
- "apihelp-query+siteinfo-paramvalue-prop-protocols": "Devolve uma lista dos protocolos permitidos nos ''links'' externos.",
+ "apihelp-query+siteinfo-paramvalue-prop-protocols": "Devolve uma lista dos protocolos permitidos nas hiperligações externas.",
"apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "Devolve os valores padrão para as preferências dos utilizadores.",
"apihelp-query+siteinfo-paramvalue-prop-uploaddialog": "Devolve a configuração do diálogo de carregamento.",
"apihelp-query+siteinfo-param-filteriw": "Devolver só as entradas locais, ou só as não locais, do mapa de interwikis.",
"apihelp-query+siteinfo-param-showalldb": "Listar todos os servidores da base de dados, não só aquele que tem maior atraso.",
"apihelp-query+siteinfo-param-numberingroup": "Lista o número de utilizadores nos grupos de utilizadores.",
"apihelp-query+siteinfo-param-inlanguagecode": "O código de língua dos nomes localizados (o melhor possível) das línguas e dos temas.",
- "apihelp-query+siteinfo-example-simple": "Obter as informações do ''site''.",
+ "apihelp-query+siteinfo-example-simple": "Obter as informações do sítio.",
"apihelp-query+siteinfo-example-interwiki": "Obter uma lista dos prefixos interwikis locais.",
"apihelp-query+siteinfo-example-replag": "Verificar o atraso de replicação atual.",
"apihelp-query+stashimageinfo-summary": "Devolve informações dos ficheiros escondidos.",
@@ -1165,6 +1174,7 @@
"apihelp-query+usercontribs-paramvalue-prop-sizediff": "Adiciona a diferença de tamanho entre a edição e a sua progenitora.",
"apihelp-query+usercontribs-paramvalue-prop-flags": "Adiciona as etiquetas da edição.",
"apihelp-query+usercontribs-paramvalue-prop-patrolled": "Etiqueta as edições patrulhadas.",
+ "apihelp-query+usercontribs-paramvalue-prop-autopatrolled": "Etiqueta as edições autopatrulhadas.",
"apihelp-query+usercontribs-paramvalue-prop-tags": "Lista as etiquetas da edição.",
"apihelp-query+usercontribs-param-show": "Mostrar só as contribuições que correspondem a estes critérios; por exemplo, só as edições não menores: <kbd>$2show=!minor</kbd>.\n\nSe um dos valores <kbd>$2show=patrolled</kbd> ou <kbd>$2show=!patrolled</kbd> estiver definido, as revisões feitas há mais de <var>[[mw:Special:MyLanguage/Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ($1 {{PLURAL:$1|segundo|segundos}}) não serão mostradas.",
"apihelp-query+usercontribs-param-tag": "Listar só as revisões marcadas com esta etiqueta.",
@@ -1229,9 +1239,11 @@
"apihelp-query+watchlist-paramvalue-prop-parsedcomment": "Adiciona o comentário da edição, após análise sintática.",
"apihelp-query+watchlist-paramvalue-prop-timestamp": "Adiciona a data e hora da edição.",
"apihelp-query+watchlist-paramvalue-prop-patrol": "Etiqueta que indica as edições que são patrulhadas.",
- "apihelp-query+watchlist-paramvalue-prop-sizes": "Adiciona os tamanhos novo e antigo da página.",
+ "apihelp-query+watchlist-paramvalue-prop-autopatrol": "Etiqueta que indica as edições que são autopatrulhadas.",
+ "apihelp-query+watchlist-paramvalue-prop-sizes": "Adiciona o tamanho novo e antigo da página.",
"apihelp-query+watchlist-paramvalue-prop-notificationtimestamp": "Adiciona a data e hora da última vez em que o utilizador foi notificado da edição.",
"apihelp-query+watchlist-paramvalue-prop-loginfo": "Adiciona informação do registo quando apropriado.",
+ "apihelp-query+watchlist-paramvalue-prop-tags": "Lista as etiquetas da entrada.",
"apihelp-query+watchlist-param-show": "Mostrar só as entradas que correspondem a estes critérios. Por exemplo, para ver só as edições menores feitas por utilizadores autenticados, definir $1show=minor|!anon.",
"apihelp-query+watchlist-param-type": "Os tipos de alterações a serem mostradas:",
"apihelp-query+watchlist-paramvalue-type-edit": "Edições normais.",
@@ -1449,7 +1461,7 @@
"api-help-source": "Fonte: $1",
"api-help-source-unknown": "Fonte: <span class=\"apihelp-unknown\">desconhecida</span>",
"api-help-license": "Licença: [[$1|$2]]",
- "api-help-license-noname": "Licença: [[$1|Ver ligação]]",
+ "api-help-license-noname": "Licença: [[$1|Ver hiperligação]]",
"api-help-license-unknown": "Licença: <span class=\"apihelp-unknown\">desconhecida</span>",
"api-help-parameters": "{{PLURAL:$1|Parâmetro|Parâmetros}}:",
"api-help-param-deprecated": "Obsoleto.",
@@ -1482,6 +1494,8 @@
"api-help-param-direction": "A direção da enumeração:\n;newer:Listar o mais antigo primeiro. Nota: $1start tem de estar antes de $1end.\n;older:Listar o mais recente primeiro (padrão). Nota: $1start tem de estar depois de $1end.",
"api-help-param-continue": "Quando houver mais resultados disponíveis, usar isto para continuar",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(sem descrição)</span>",
+ "api-help-param-maxbytes": "Não pode exceder $1 {{PLURAL:$1|byte|bytes}}.",
+ "api-help-param-maxchars": "Não pode exceder $1 {{PLURAL:$1|carácter|caracteres}}.",
"api-help-examples": "{{PLURAL:$1|Exemplo|Exemplos}}:",
"api-help-permissions": "{{PLURAL:$1|Permissão|Permissões}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|Concedida a|Concedidas a}}: $2",
@@ -1544,6 +1558,8 @@
"apierror-chunk-too-small": "O tamanho de segmento mínimo é $1 {{PLURAL:$1|byte|bytes}} para segmentos que não sejam segmentos finais.",
"apierror-cidrtoobroad": "Não são aceites intervalos CIDR $1 maiores do que /$2.",
"apierror-compare-no-title": "Não é possível transformar antes da gravação, sem um título. Tente especificar <var>fromtitle</var> ou <var>totitle</var>.",
+ "apierror-compare-nosuchfromsection": "Não há nenhuma secção $1 no conteúdo 'from'.",
+ "apierror-compare-nosuchtosection": "Não há nenhuma secção $1 no conteúdo 'to'.",
"apierror-compare-relative-to-nothing": "Não existe uma revisão 'from' em relação à qual <var>torelative</var> possa ser relativo.",
"apierror-contentserializationexception": "A seriação do conteúdo falhou: $1",
"apierror-contenttoobig": "O conteúdo que forneceu excede o tamanho máximo dos artigos que é $1 {{PLURAL:$1|kilobyte|kilobytes}}.",
@@ -1584,6 +1600,8 @@
"apierror-invalidurlparam": "Valor inválido para <var>$1urlparam</var> (<kbd>$2=$3</kbd>).",
"apierror-invaliduser": "Nome de utilizador inválido \"$1\".",
"apierror-invaliduserid": "O identificador de utilizador <var>$1</var> não é válido.",
+ "apierror-maxbytes": "O parâmetro <var>$1</var> não pode exceder $2 {{PLURAL:$2|byte|bytes}}",
+ "apierror-maxchars": "O parâmetro <var>$1</var> não pode exceder $2 {{PLURAL:$2|carácter|caracteres}}",
"apierror-maxlag-generic": "À espera de um servidor de base de dados: $1 {{PLURAL:$1|segundo|segundos}} de atraso.",
"apierror-maxlag": "À espera de $2: $1 {{PLURAL:$1|segundo|segundos}} de atraso.",
"apierror-mimesearchdisabled": "A pesquisa MIME é desativada no modo avarento.",
@@ -1599,7 +1617,6 @@
"apierror-missingtitle-byname": "A página $1 não existe.",
"apierror-moduledisabled": "O módulo <kbd>$1</kbd> foi desativado.",
"apierror-multival-only-one-of": "Só é permitido {{PLURAL:$3|o valor|um dos valores}} $2 para o parâmetro <var>$1</var>.",
- "apierror-multival-only-one": "Só é permitido um valor para o parâmetro <var>$1</var>.",
"apierror-multpages": "<var>$1</var> só pode ser usado com uma única página.",
"apierror-mustbeloggedin-changeauth": "Tem de estar autenticado para alterar dados de autenticação.",
"apierror-mustbeloggedin-generic": "Tem de estar autenticado.",
@@ -1720,6 +1737,7 @@
"apiwarn-notfile": "\"$1\" não é um ficheiro.",
"apiwarn-nothumb-noimagehandler": "Não foi possível criar a miniatura porque $1 não tem uma rotina associada de tratamento de imagens.",
"apiwarn-parse-nocontentmodel": "Não foi fornecido um <var>title</var> ou <var>contentmodel</var>, será assumido $1.",
+ "apiwarn-parse-revidwithouttext": "<var>revid</var> foi usado sem <var>text</var>, e foram pedidas as propriedades da página analisada. Pretendia usar <var>oldid</var> em vez de <var>revid</var>?",
"apiwarn-parse-titlewithouttext": "<var>title</var> foi usado sem <var>text</var>, e foram pedidas as propriedades da página analisada. Pretendia usar <var>page</var> em vez de <var>title</var>?",
"apiwarn-redirectsandrevids": "Resolução de redirecionamentos não pode ser usada em conjunto com o parâmetro <var>revids</var>. Quaisquer redirecionamentos para os quais <var>revids</var> aponta não foram resolvidos.",
"apiwarn-tokennotallowed": "A operação \"$1\" não é permitida para o utilizador atual.",
diff --git a/www/wiki/includes/api/i18n/qqq.json b/www/wiki/includes/api/i18n/qqq.json
index 6aaaac70..594bf8e6 100644
--- a/www/wiki/includes/api/i18n/qqq.json
+++ b/www/wiki/includes/api/i18n/qqq.json
@@ -68,6 +68,7 @@
"apihelp-compare-param-fromid": "{{doc-apihelp-param|compare|fromid}}",
"apihelp-compare-param-fromrev": "{{doc-apihelp-param|compare|fromrev}}",
"apihelp-compare-param-fromtext": "{{doc-apihelp-param|compare|fromtext}}",
+ "apihelp-compare-param-fromsection": "{{doc-apihelp-param|compare|fromsection}}",
"apihelp-compare-param-frompst": "{{doc-apihelp-param|compare|frompst}}",
"apihelp-compare-param-fromcontentmodel": "{{doc-apihelp-param|compare|fromcontentmodel}}",
"apihelp-compare-param-fromcontentformat": "{{doc-apihelp-param|compare|fromcontentformat}}",
@@ -76,6 +77,7 @@
"apihelp-compare-param-torev": "{{doc-apihelp-param|compare|torev}}",
"apihelp-compare-param-torelative": "{{doc-apihelp-param|compare|torelative}}",
"apihelp-compare-param-totext": "{{doc-apihelp-param|compare|totext}}",
+ "apihelp-compare-param-tosection": "{{doc-apihelp-param|compare|tosection}}",
"apihelp-compare-param-topst": "{{doc-apihelp-param|compare|topst}}",
"apihelp-compare-param-tocontentmodel": "{{doc-apihelp-param|compare|tocontentmodel}}",
"apihelp-compare-param-tocontentformat": "{{doc-apihelp-param|compare|tocontentformat}}",
@@ -204,8 +206,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "{{doc-apihelp-param|feedrecentchanges|tagfilter}}",
"apihelp-feedrecentchanges-param-target": "{{doc-apihelp-param|feedrecentchanges|target}}",
"apihelp-feedrecentchanges-param-showlinkedto": "{{doc-apihelp-param|feedrecentchanges|showlinkedto}}",
- "apihelp-feedrecentchanges-param-categories": "{{doc-apihelp-param|feedrecentchanges|categories}}",
- "apihelp-feedrecentchanges-param-categories_any": "{{doc-apihelp-param|feedrecentchanges|categories_any}}",
"apihelp-feedrecentchanges-example-simple": "{{doc-apihelp-example|feedrecentchanges}}",
"apihelp-feedrecentchanges-example-30days": "{{doc-apihelp-example|feedrecentchanges}}",
"apihelp-feedwatchlist-summary": "{{doc-apihelp-summary|feedwatchlist}}",
@@ -240,6 +240,8 @@
"apihelp-import-extended-description": "{{doc-apihelp-extended-description|import}}",
"apihelp-import-param-summary": "{{doc-apihelp-param|import|summary|info=The parameter being documented here provides the summary used on the log messages about the import. The phrase \"Import summary\" here is grammatically equivalent to a phrase such as \"science book\", not \"eat food\".}}",
"apihelp-import-param-xml": "{{doc-apihelp-param|import|xml}}",
+ "apihelp-import-param-interwikiprefix": "{{doc-apihelp-param|import|interwikiprefix}}",
+ "apihelp-import-param-assignknownusers": "{{doc-apihelp-param|import|assignknownusers}}",
"apihelp-import-param-interwikisource": "{{doc-apihelp-param|import|interwikisource}}",
"apihelp-import-param-interwikipage": "{{doc-apihelp-param|import|interwikipage}}",
"apihelp-import-param-fullhistory": "{{doc-apihelp-param|import|fullhistory}}",
@@ -367,6 +369,7 @@
"apihelp-parse-param-disablepp": "{{doc-apihelp-param|parse|disablepp}}",
"apihelp-parse-param-disableeditsection": "{{doc-apihelp-param|parse|disableeditsection}}",
"apihelp-parse-param-disabletidy": "{{doc-apihelp-param|parse|disabletidy}}",
+ "apihelp-parse-param-disablestylededuplication": "{{doc-apihelp-param|parse|disablestylededuplication}}",
"apihelp-parse-param-generatexml": "{{doc-apihelp-param|parse|generatexml|params=* $1 - Value of the constant CONTENT_MODEL_WIKITEXT|paramstart=2}}",
"apihelp-parse-param-preview": "{{doc-apihelp-param|parse|preview}}",
"apihelp-parse-param-sectionpreview": "{{doc-apihelp-param|parse|sectionpreview}}",
@@ -837,6 +840,7 @@
"apihelp-query+info-paramvalue-prop-readable": "{{doc-apihelp-paramvalue|query+info|prop|readable}}",
"apihelp-query+info-paramvalue-prop-preload": "{{doc-apihelp-paramvalue|query+info|prop|preload}}",
"apihelp-query+info-paramvalue-prop-displaytitle": "{{doc-apihelp-paramvalue|query+info|prop|displaytitle}}",
+ "apihelp-query+info-paramvalue-prop-varianttitles": "{{doc-apihelp-paramvalue|query+info|prop|varianttitles}}",
"apihelp-query+info-param-testactions": "{{doc-apihelp-param|query+info|testactions}}",
"apihelp-query+info-param-token": "{{doc-apihelp-param|query+info|token}}",
"apihelp-query+info-example-simple": "{{doc-apihelp-example|query+info}}",
@@ -996,6 +1000,7 @@
"apihelp-query+recentchanges-paramvalue-prop-sizes": "{{doc-apihelp-paramvalue|query+recentchanges|prop|sizes}}",
"apihelp-query+recentchanges-paramvalue-prop-redirect": "{{doc-apihelp-paramvalue|query+recentchanges|prop|redirect}}",
"apihelp-query+recentchanges-paramvalue-prop-patrolled": "{{doc-apihelp-paramvalue|query+recentchanges|prop|patrolled}}",
+ "apihelp-query+recentchanges-paramvalue-prop-autopatrolled": "{{doc-apihelp-paramvalue|query+recentchanges|prop|autopatrolled}}",
"apihelp-query+recentchanges-paramvalue-prop-loginfo": "{{doc-apihelp-paramvalue|query+recentchanges|prop|loginfo}}",
"apihelp-query+recentchanges-paramvalue-prop-tags": "{{doc-apihelp-paramvalue|query+recentchanges|prop|tags}}",
"apihelp-query+recentchanges-paramvalue-prop-sha1": "{{doc-apihelp-paramvalue|query+recentchanges|prop|sha1}}",
@@ -1075,6 +1080,7 @@
"apihelp-query+search-paramvalue-prop-sectiontitle": "{{doc-apihelp-paramvalue|query+search|prop|sectiontitle}}",
"apihelp-query+search-paramvalue-prop-categorysnippet": "{{doc-apihelp-paramvalue|query+search|prop|categorysnippet}}",
"apihelp-query+search-paramvalue-prop-isfilematch": "{{doc-apihelp-paramvalue|query+search|prop|isfilematch}}",
+ "apihelp-query+search-paramvalue-prop-extensiondata": "{{doc-apihelp-paramvalue|query+search|prop|extensiondata}}",
"apihelp-query+search-paramvalue-prop-score": "{{doc-apihelp-paramvalue|query+search|prop|score}}\n{{Identical|Ignored}}",
"apihelp-query+search-paramvalue-prop-hasrelated": "{{doc-apihelp-paramvalue|query+search|prop|hasrelated}}\n{{Identical|Ignored}}",
"apihelp-query+search-param-limit": "{{doc-apihelp-param|query+search|limit}}",
@@ -1173,6 +1179,7 @@
"apihelp-query+usercontribs-paramvalue-prop-sizediff": "{{doc-apihelp-paramvalue|query+usercontribs|prop|sizediff}}",
"apihelp-query+usercontribs-paramvalue-prop-flags": "{{doc-apihelp-paramvalue|query+usercontribs|prop|flags}}",
"apihelp-query+usercontribs-paramvalue-prop-patrolled": "{{doc-apihelp-paramvalue|query+usercontribs|prop|patrolled}}",
+ "apihelp-query+usercontribs-paramvalue-prop-autopatrolled": "{{doc-apihelp-paramvalue|query+usercontribs|prop|autopatrolled}}",
"apihelp-query+usercontribs-paramvalue-prop-tags": "{{doc-apihelp-paramvalue|query+usercontribs|prop|tags}}",
"apihelp-query+usercontribs-param-show": "{{doc-apihelp-param|query+usercontribs|show|params=* $1 - Value of [[mw:Manual:$RCMaxAge|$RCMaxAge]]|paramstart=2}}",
"apihelp-query+usercontribs-param-tag": "{{doc-apihelp-param|query+usercontribs|tag}}",
@@ -1237,9 +1244,11 @@
"apihelp-query+watchlist-paramvalue-prop-parsedcomment": "{{doc-apihelp-paramvalue|query+watchlist|prop|parsedcomment}}",
"apihelp-query+watchlist-paramvalue-prop-timestamp": "{{doc-apihelp-paramvalue|query+watchlist|prop|timestamp}}",
"apihelp-query+watchlist-paramvalue-prop-patrol": "{{doc-apihelp-paramvalue|query+watchlist|prop|patrol}}",
+ "apihelp-query+watchlist-paramvalue-prop-autopatrol": "{{doc-apihelp-paramvalue|query+watchlist|prop|autopatrol}}",
"apihelp-query+watchlist-paramvalue-prop-sizes": "{{doc-apihelp-paramvalue|query+watchlist|prop|sizes}}",
"apihelp-query+watchlist-paramvalue-prop-notificationtimestamp": "{{doc-apihelp-paramvalue|query+watchlist|prop|notificationtimestamp}}",
"apihelp-query+watchlist-paramvalue-prop-loginfo": "{{doc-apihelp-paramvalue|query+watchlist|prop|loginfo}}",
+ "apihelp-query+watchlist-paramvalue-prop-tags": "{{doc-apihelp-paramvalue|query+watchlist|prop|tags}}",
"apihelp-query+watchlist-param-show": "{{doc-apihelp-param|query+watchlist|show}}",
"apihelp-query+watchlist-param-type": "{{doc-apihelp-param|query+watchlist|type}}",
"apihelp-query+watchlist-paramvalue-type-edit": "{{doc-apihelp-paramvalue|query+watchlist|type|edit}}",
@@ -1496,6 +1505,8 @@
"api-help-param-direction": "{{doc-apihelp-param|description=any standard \"dir\" parameter|noseealso=1}}",
"api-help-param-continue": "{{doc-apihelp-param|description=any standard \"continue\" parameter, or other parameter with the same semantics|noseealso=1}}",
"api-help-param-no-description": "Displayed on API parameters that lack any description",
+ "api-help-param-maxbytes": "Used to display the maximum allowed length of a parameter, in bytes.",
+ "api-help-param-maxchars": "Used to display the maximum allowed length of a parameter, in characters.",
"api-help-examples": "Label for the API help examples section\n\nParameters:\n* $1 - Number of examples to be displayed\n{{Identical|Example}}",
"api-help-permissions": "Label for the \"permissions\" section in the main module's help output.\n\nParameters:\n* $1 - Number of permissions displayed\n{{Identical|Permission}}",
"api-help-permissions-granted-to": "Used to introduce the list of groups each permission is assigned to.\n\nParameters:\n* $1 - Number of groups\n* $2 - List of group names, comma-separated",
@@ -1559,6 +1570,8 @@
"apierror-chunk-too-small": "{{doc-apierror}}\n\nParameters:\n* $1 - Minimum size in bytes.",
"apierror-cidrtoobroad": "{{doc-apierror}}\n\nParameters:\n* $1 - \"IPv4\" or \"IPv6\"\n* $2 - Minimum CIDR mask length.",
"apierror-compare-no-title": "{{doc-apierror}}",
+ "apierror-compare-nosuchfromsection": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.",
+ "apierror-compare-nosuchtosection": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.",
"apierror-compare-relative-to-nothing": "{{doc-apierror}}",
"apierror-contentserializationexception": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, may end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
"apierror-contenttoobig": "{{doc-apierror}}\n\nParameters:\n* $1 - Maximum article size in kilobytes.",
@@ -1599,6 +1612,8 @@
"apierror-invalidurlparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Key\n* $3 - Value.",
"apierror-invaliduser": "{{doc-apierror}}\n\nParameters:\n* $1 - User name that is invalid.",
"apierror-invaliduserid": "{{doc-apierror}}",
+ "apierror-maxbytes": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Maximum allowed bytes.",
+ "apierror-maxchars": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Maximum allowed characters.",
"apierror-maxlag-generic": "{{doc-apierror}}\n\nParameters:\n* $1 - Database is lag in seconds.",
"apierror-maxlag": "{{doc-apierror}}\n\nParameters:\n* $1 - Database lag in seconds.\n* $2 - Database server that is lagged.",
"apierror-mimesearchdisabled": "{{doc-apierror}}",
@@ -1614,7 +1629,6 @@
"apierror-missingtitle-byname": "{{doc-apierror}}",
"apierror-moduledisabled": "{{doc-apierror}}\n\nParameters:\n* $1 - Name of the module.",
"apierror-multival-only-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Possible values for the parameter.\n* $3 - Number of values.",
- "apierror-multival-only-one": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
"apierror-multpages": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name",
"apierror-mustbeloggedin-changeauth": "{{doc-apierror}}",
"apierror-mustbeloggedin-generic": "{{doc-apierror}}",
diff --git a/www/wiki/includes/api/i18n/ru.json b/www/wiki/includes/api/i18n/ru.json
index a264737e..7c8c1694 100644
--- a/www/wiki/includes/api/i18n/ru.json
+++ b/www/wiki/includes/api/i18n/ru.json
@@ -26,10 +26,14 @@
"Ivan-r",
"Redredsonia",
"Alexey zakharenkov",
- "Facenapalm"
+ "Facenapalm",
+ "Jack who built the house",
+ "Mouse21",
+ "Happy13241",
+ "Ole Yves"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Документация]]\n* [[mw:Special:MyLanguage/API:FAQ|ЧаВО]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Почтовая рассылка]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Новости API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Ошибки и запросы]\n</div>\n<strong>Статус:</strong> Все отображаемые на этой странице функции должны работать, однако API находится в статусе активной разработки и может измениться в любой момент. Подпишитесь на [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ почтовую рассылку mediawiki-api-announce], чтобы быть в курсе обновлений.\n\n<strong>Ошибочные запросы:</strong> Если API получает запрос с ошибкой, вернётся заголовок HTTP с ключом «MediaWiki-API-Error», после чего значение заголовка и код ошибки будут отправлены обратно и установлены в то же значение. Более подробную информацию см. [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Ошибки и предупреждения]].\n\n<strong>Тестирование:</strong> для удобства тестирования API-запросов, см. [[Special:ApiSandbox]].",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Документация]]\n* [[mw:Special:MyLanguage/API:FAQ|ЧаВО]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Почтовая рассылка]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Новости API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Ошибки и запросы]\n</div>\n<strong>Статус:</strong> MediaWiki API — зрелый и стабильный интерфейс, активно поддерживаемый и улучшаемый. Мы стараемся избегать ломающих изменений, однако изредка они могут быть необходимы. Подпишитесь на [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ почтовую рассылку mediawiki-api-announce], чтобы быть в курсе обновлений.\n\n<strong>Ошибочные запросы:</strong> Если API получает запрос с ошибкой, вернётся заголовок HTTP с ключом «MediaWiki-API-Error», после чего значение заголовка и код ошибки будут отправлены обратно и установлены в то же значение. Более подробную информацию см. [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Ошибки и предупреждения]].\n\n<p class=\"mw-apisandbox-link\"><strong>Тестирование:</strong> для удобства тестирования API-запросов, см. [[Special:ApiSandbox]].</p>",
"apihelp-main-param-action": "Действие, которое следует выполнить.",
"apihelp-main-param-format": "Формат вывода.",
"apihelp-main-param-maxlag": "Значение максимального отставания может использоваться, когда MediaWiki установлена на кластер из реплицируемых баз данных. Чтобы избежать ухудшения ситуации с отставанием репликации сайта, этот параметр может заставить клиента ждать, когда задержка репликации станет ниже указанного значения. В случае чрезмерной задержки возвращается код ошибки «<samp>maxlag</samp>» с сообщением «<samp>Waiting for $host: $lag seconds lagged</samp>».<br>См. подробнее на странице с описанием [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: параметра Maxlag]].",
@@ -42,9 +46,9 @@
"apihelp-main-param-curtimestamp": "Включить в результат временную метку.",
"apihelp-main-param-responselanginfo": "Включить языки, использованные для <var>uselang</var> и <var>errorlang</var>, в результат.",
"apihelp-main-param-origin": "При обращении к API с использованием кросс-доменного AJAX-запроса (CORS), задайте параметру значение исходного домена. Он должен быть включён в любой предварительный запрос и таким образом должен быть частью URI-запроса (не тела POST).\n\nДля аутентифицированных запросов он должен точно соответствовать одному из источников в заголовке <code>Origin</code>, так что он должен быть задан наподобие <kbd>https://ru.wikipedia.org</kbd> или <kbd>https://meta.wikimedia.org</kbd>. Если параметр не соответствует заголовку <code>Origin</code>, будет возвращён ответ с кодом ошибки 403. Если параметр соответствует заголовку <code>Origin</code>, и источник находится в белом списке, будут установлены заголовки <code>Access-Control-Allow-Origin</code> и <code>Access-Control-Allow-Credentials</code>.\n\nДля неаутентифицированных запросов укажите значение <kbd>*</kbd>. В результате заголовок <code>Access-Control-Allow-Origin</code> будет установлен, но <code>Access-Control-Allow-Credentials</code> примет значение <code>false</code> и все пользовательские данные будут ограничены.",
- "apihelp-main-param-uselang": "Язык, используемый для перевода сообщений. Запрос «<kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>» с «<kbd>siprop=languages</kbd>» возвращает список кодов языков; укажите «<kbd>user</kbd>», чтобы использовать текущие языковые настройки участника, или «<kbd>content</kbd>» для использования основного языка этой вики.",
+ "apihelp-main-param-uselang": "Язык, используемый для перевода сообщений. Запрос <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> с <kbd>siprop=languages</kbd> возвращает список кодов языков; укажите <kbd>user</kbd>, чтобы использовать текущие языковые настройки участника, или <kbd>content</kbd> для использования основного языка этой вики.",
"apihelp-main-param-errorformat": "Формат, используемый для вывода текста предупреждений и ошибок.\n; plaintext: Вики-текст с удалёнными HTML-тегами и замещёнными мнемониками.\n; wikitext: Нераспарсенный вики-текст.\n; html: HTML.\n; raw: Ключ сообщения и параметры.\n; none: Без текстового вывода, только коды ошибок.\n; bc: Формат, используемый до MediaWiki 1.29. <var>errorlang</var> и <var>errorsuselocal</var> игнорируются.",
- "apihelp-main-param-errorlang": "Язык, используемый для вывода предупреждений и сообщений об ошибках. Запрос «<kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>» с «<kbd>siprop=languages</kbd>» возвращает список кодов языков; укажите «<kbd>content</kbd>» для использования основного языка этой вики, или «<kbd>uselang</kbd>» для использования того же значения, что и в параметре «<var>uselang</var>».",
+ "apihelp-main-param-errorlang": "Язык, используемый для вывода предупреждений и сообщений об ошибках. Запрос <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> с <kbd>siprop=languages</kbd> возвращает список кодов языков; укажите <kbd>content</kbd> для использования основного языка этой вики, или <kbd>uselang</kbd> для использования того же значения, что и в параметре <var>uselang</var>.",
"apihelp-main-param-errorsuselocal": "Если задан, тексты ошибок будут использовать локально модифицированные сообщения из пространства имён {{ns:MediaWiki}}.",
"apihelp-block-summary": "Блокировка участника.",
"apihelp-block-param-user": "Имя участника, IP-адрес или диапазон IP-адресов, которые вы хотите заблокировать. Нельзя использовать вместе с <var>$1userid</var>",
@@ -80,6 +84,7 @@
"apihelp-compare-param-fromid": "Идентификатор первой сравниваемой страницы.",
"apihelp-compare-param-fromrev": "Первая сравниваемая версия.",
"apihelp-compare-param-fromtext": "Используйте этот текст вместо содержимого версии, заданной <var>fromtitle</var>, <var>fromid</var> или <var>fromrev</var>.",
+ "apihelp-compare-param-fromsection": "Использовать только указанную секцию из содержимого 'from'.",
"apihelp-compare-param-frompst": "Выполнить преобразование перед записью правки (PST) над <var>fromtext</var>.",
"apihelp-compare-param-fromcontentmodel": "Модель содержимого <var>fromtext</var>. Если не задана, будет угадана по другим параметрам.",
"apihelp-compare-param-fromcontentformat": "Формат сериализации содержимого <var>fromtext</var>.",
@@ -88,6 +93,7 @@
"apihelp-compare-param-torev": "Вторая сравниваемая версия.",
"apihelp-compare-param-torelative": "Использовать версию, относящуюся к определённой<var>fromtitle</var>, <var>fromid</var> или <var>fromrev</var> Все другие опции 'to' будут проигнорированы.",
"apihelp-compare-param-totext": "Используйте этот текст вместо содержимого версии, заданной <var>totitle</var>, <var>toid</var> или <var>torev</var>.",
+ "apihelp-compare-param-tosection": "Использовать только указанную секцию из содержимого 'to'.",
"apihelp-compare-param-topst": "Выполнить преобразование перед записью правки (PST) над <var>totext</var>.",
"apihelp-compare-param-tocontentmodel": "Модель содержимого <var>totext</var>. Если не задана, будет угадана по другим параметрам.",
"apihelp-compare-param-tocontentformat": "Формат сериализации содержимого <var>totext</var>.",
@@ -216,8 +222,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Фильтр по меткам.",
"apihelp-feedrecentchanges-param-target": "Показать только правки на страницах, на которые ссылается данная.",
"apihelp-feedrecentchanges-param-showlinkedto": "Показать правки на страницах, ссылающихся на данную.",
- "apihelp-feedrecentchanges-param-categories": "Показать только правки на страницах, включённых во все данные категории.",
- "apihelp-feedrecentchanges-param-categories_any": "Показать только правки на страницах, включённых в хотя бы одну из данных категорий.",
"apihelp-feedrecentchanges-example-simple": "Список последних изменений.",
"apihelp-feedrecentchanges-example-30days": "Список последних изменений за 30 дней.",
"apihelp-feedwatchlist-summary": "Возвращает ленту списка наблюдения.",
@@ -248,10 +252,12 @@
"apihelp-imagerotate-param-tags": "Изменить метки записи в журнале загрузок.",
"apihelp-imagerotate-example-simple": "Повернуть <kbd>File:Example.png</kbd> на <kbd>90</kbd> градусов.",
"apihelp-imagerotate-example-generator": "Повернуть все изображения в <kbd>Category:Flip</kbd> на <kbd>180</kbd> градусов.",
- "apihelp-import-summary": "Импорт страницы из другой вики, или из XML-файла.",
+ "apihelp-import-summary": "Импорт страницы из другой вики или XML-файла.",
"apihelp-import-extended-description": "Обратите внимание, что HTTP POST-запрос должен быть осуществлён как загрузка файла (то есть, с использованием многотомных данных) при отправки файла через параметр <var>xml</var>.",
"apihelp-import-param-summary": "Описание записи журнала импорта.",
"apihelp-import-param-xml": "Загруженный XML-файл.",
+ "apihelp-import-param-interwikiprefix": "Для загруженных импортов: префикс интервики для неизвестных имён участников (а также известных, если задан <var>$1assignknownusers</var>).",
+ "apihelp-import-param-assignknownusers": "Связать правки с локальными участниками, когда участники с такими именами существуют.",
"apihelp-import-param-interwikisource": "Для импорта из других вики: импортируемая вики.",
"apihelp-import-param-interwikipage": "Для импорта из других вики: импортируемая страница.",
"apihelp-import-param-fullhistory": "Для импорта из других вики: импортировать полную историю, а не только текущую страницу.",
@@ -263,7 +269,7 @@
"apihelp-linkaccount-summary": "Связать аккаунт третьей стороны с текущим участником.",
"apihelp-linkaccount-example-link": "Начать связывание аккаунта с <kbd>Example</kdb>.",
"apihelp-login-summary": "Вход и получение аутентификационных cookie.",
- "apihelp-login-extended-description": "Это действие должно быть использовано только в комбинации со [[Special:BotPasswords]]; использование этого модуля для входа в основной аккаунт не поддерживается и может сбиться без предупреждения. Для безопасного входа в основной аккаунт, используйте <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
+ "apihelp-login-extended-description": "Это действие должно быть использовано только в комбинации со [[Special:BotPasswords]]; использование этого модуля для входа в основной аккаунт устарело и может сбиться без предупреждения. Для безопасного входа в основной аккаунт, используйте <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
"apihelp-login-extended-description-nobotpasswords": "Это действие не поддерживается и может сбиться без предупреждения. Для безопасного входа, используйте <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
"apihelp-login-param-name": "Имя участника.",
"apihelp-login-param-password": "Пароль.",
@@ -309,7 +315,7 @@
"apihelp-opensearch-summary": "Поиск по вики с использованием протокола OpenSearch.",
"apihelp-opensearch-param-search": "Строка поиска.",
"apihelp-opensearch-param-limit": "Максимальное число возвращаемых результатов.",
- "apihelp-opensearch-param-namespace": "Пространства имён для поиска.",
+ "apihelp-opensearch-param-namespace": "Пространства имён для поиска. Игнорируется, если <var>$1search</var> начинается с корректного префикса пространства имён.",
"apihelp-opensearch-param-suggest": "Ничего не делать, если <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> ложно.",
"apihelp-opensearch-param-redirects": "Как обрабатывать перенаправления:\n;return: Вернуть само перенаправление.\n;resolve: Вернуть целевую страницу. Может вернуть меньше $1limit результатов.\nПо историческим причинам значением по умолчанию является «return» для $1format=json и «resolve» для остальных форматов.",
"apihelp-opensearch-param-format": "Формат вывода.",
@@ -335,9 +341,10 @@
"apihelp-paraminfo-example-1": "Показать информацию для <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>, <kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>, <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd>, и <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>.",
"apihelp-paraminfo-example-2": "Показать информацию для всех подмодулей <kbd>[[Special:ApiHelp/query|action=query]]</kbd>.",
"apihelp-parse-summary": "Парсит содержимое и возвращает результат парсинга.",
- "apihelp-parse-extended-description": "См. различные prop-модули <kbd>[[Special:ApiHelp/query|action=query]]</kbd> для получения информации о текущей версии страницы.\n\nЕсть несколько способов указать текст для парсинга:\n# Указать страницы или версию, используя <var>$1page</var>, <var>$1pageid</var> или <var>$1oldid</var>.\n# Явно указать содержимое, используя <var>$1text</var>, <var>$1title</var> и <var>$1contentmodel</var>.\n# Указать описание правки. Параметру <var>$1prop</var> должно быть присвоено пустое значение.",
+ "apihelp-parse-extended-description": "См. различные prop-модули <kbd>[[Special:ApiHelp/query|action=query]]</kbd> для получения информации о текущей версии страницы.\n\nЕсть несколько способов указать текст для парсинга:\n# Указать страницу или версию, используя <var>$1page</var>, <var>$1pageid</var> или <var>$1oldid</var>.\n# Явно указать содержимое, используя <var>$1text</var>, <var>$1title</var> и <var>$1contentmodel</var>.\n# Указать описание правки. Параметру <var>$1prop</var> должно быть присвоено пустое значение.",
"apihelp-parse-param-title": "Название страницы, которой принадлежит текст. Если опущено, должен быть указан параметр <var>$1contentmodel</var>, и в качестве заголовка будет использовано [[API]].",
"apihelp-parse-param-text": "Распарсиваемый текст. Используйте <var>$1title</var> или <var>$1contentmodel</var> для управления моделью содержимого.",
+ "apihelp-parse-param-revid": "Номер версии, для <code><nowiki>{{REVISIONID}}</nowiki></code> и аналогичных переменных.",
"apihelp-parse-param-summary": "Анализируемое описание правки.",
"apihelp-parse-param-page": "Распарсить содержимое этой страницы. Не может быть использовано совместно с <var>$1text</var> и <var>$1title</var>.",
"apihelp-parse-param-pageid": "Анализировать содержимое этой страницы. Переопределяет <var>$1page</var>.",
@@ -378,6 +385,7 @@
"apihelp-parse-param-disablepp": "Вместо этого используйте <var>$1disablelimitreport</var>.",
"apihelp-parse-param-disableeditsection": "Опустить ссылки на редактирование разделов из результата парсинга.",
"apihelp-parse-param-disabletidy": "Не проводить очистку HTML (например, с помощью tidy) результатов парсинга.",
+ "apihelp-parse-param-disablestylededuplication": "Не дедуплицируйте встроенные таблицы стилей в выходе парсера.",
"apihelp-parse-param-generatexml": "Сгенерировать дерево парсинга XML (требуется модель содержимого <code>$1</code>, замещено <kbd>$2prop=parsetree</kbd>).",
"apihelp-parse-param-preview": "Проанализировать в режиме препросмотра.",
"apihelp-parse-param-sectionpreview": "Распарсить в режиме предпросмотра раздела (также активирует режим предпросмотра).",
@@ -563,12 +571,12 @@
"apihelp-query+allrevisions-param-generatetitles": "При использовании в качестве генератора, генерирует названия страниц вместо идентификаторов версий.",
"apihelp-query+allrevisions-example-user": "Перечислить последние 50 правок участника <kbd>Example</kbd>.",
"apihelp-query+allrevisions-example-ns-main": "Перечислить первые 50 правок в основном пространстве.",
- "apihelp-query+mystashedfiles-summary": "Получить список файлов в тайнике (upload stash) текущего участника.",
+ "apihelp-query+mystashedfiles-summary": "Получить список файлов во временном хранилище текущего участника.",
"apihelp-query+mystashedfiles-param-prop": "Какие свойства файлов запрашивать.",
"apihelp-query+mystashedfiles-paramvalue-prop-size": "Запросить размер и разрешение изображения.",
"apihelp-query+mystashedfiles-paramvalue-prop-type": "Запросить MIME- и медиа-тип файла.",
"apihelp-query+mystashedfiles-param-limit": "Сколько файлов получить.",
- "apihelp-query+mystashedfiles-example-simple": "Получить ключ, размер и разрешение файлов в тайнике текущего участника.",
+ "apihelp-query+mystashedfiles-example-simple": "Получить ключ, размер и разрешение файлов во временном хранилище текущего участника.",
"apihelp-query+alltransclusions-summary": "Перечисление всех включений (страниц, вставленных с помощью &#123;&#123;x&#125;&#125;), включая несуществующие.",
"apihelp-query+alltransclusions-param-from": "Название включения, с которого начать перечисление.",
"apihelp-query+alltransclusions-param-to": "Название включения, на котором закончить перечисление.",
@@ -710,7 +718,7 @@
"apihelp-query+deletedrevs-param-excludeuser": "Не перечислять правки данного участника.",
"apihelp-query+deletedrevs-param-namespace": "Перечислять только страницы этого пространства имён.",
"apihelp-query+deletedrevs-param-limit": "Максимальное количество правок в списке.",
- "apihelp-query+deletedrevs-param-prop": "Какие свойства возвращать:\n;revid: Добавляет идентификатор удалённой правки.\n;parentid: Добавляет идентификатор предыдущей версии страницы.\n;user: Добавляет ник участника, сделавшего правку.\n;userid: Добавляет идентификатор участника, сделавшего правку.\n;comment: Добавляет описание правки.\n;parsedcomment: Добавляет распарсенное описание правки.\n;minor: Отмечает, была ли правка малым.\n;len: Добавляет длину (в байтах) правки.\n;sha1: Добавляет хэш SHA-1 (base 16) правки.\n;content: Добавляет содержимое правки.\n;token: <span class=\"apihelp-deprecated\">Не поддерживается.</span> Возвращает токен редактирования.\n;tags: Теги правки.",
+ "apihelp-query+deletedrevs-param-prop": "Какие свойства возвращать:\n;revid: Добавляет идентификатор удалённой правки.\n;parentid: Добавляет идентификатор предыдущей версии страницы.\n;user: Добавляет ник участника, сделавшего правку.\n;userid: Добавляет идентификатор участника, сделавшего правку.\n;comment: Добавляет описание правки.\n;parsedcomment: Добавляет распарсенное описание правки.\n;minor: Отмечает, была ли правка малым.\n;len: Добавляет длину (в байтах) правки.\n;sha1: Добавляет хэш SHA-1 (base 16) правки.\n;content: Добавляет содержимое правки.\n;token: <span class=\"apihelp-deprecated\">Устарело.</span> Возвращает токен редактирования.\n;tags: Метки правки.",
"apihelp-query+deletedrevs-example-mode1": "Список последних удалённых правок страниц <kbd>Main Page</kbd> и <kbd>Talk:Main Page</kbd> с содержимым (режим 1).",
"apihelp-query+deletedrevs-example-mode2": "Список последних 50 удалённых правок участника <kbd>Bob</kbd> (режим 2).",
"apihelp-query+deletedrevs-example-mode3-main": "Список последних 50 удалённых правок в основном пространстве имён (режим 3)",
@@ -747,7 +755,7 @@
"apihelp-query+exturlusage-param-namespace": "Пространства имён для перечисления.",
"apihelp-query+exturlusage-param-limit": "Сколько страниц вернуть.",
"apihelp-query+exturlusage-param-expandurl": "Раскрыть зависимые от протокола ссылки с какноничным протоколом.",
- "apihelp-query+exturlusage-example-simple": "Показать страницы, ссылающиеся на <kbd>http://www.mediawiki.org</kbd>.",
+ "apihelp-query+exturlusage-example-simple": "Показать страницы, ссылающиеся на <kbd>https://www.mediawiki.org</kbd>.",
"apihelp-query+filearchive-summary": "Перечисление всех удалённых файлов.",
"apihelp-query+filearchive-param-from": "Название изображения, с которого начать перечисление.",
"apihelp-query+filearchive-param-to": "Название изображения, на котором закончить перечисление.",
@@ -955,7 +963,7 @@
"apihelp-query+prefixsearch-summary": "Осуществление поиска по префиксу названий страниц.",
"apihelp-query+prefixsearch-extended-description": "Не смотря на похожесть названий, этот модуль не является эквивалентом [[Special:PrefixIndex]]; если вы ищете его, см. <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> с параметром <kbd>apprefix</kbd>. Задача этого модуля близка к <kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd>: получение пользовательского ввода и представление наиболее подходящих заголовков. В зависимости от поискового движка, используемого на сервере, сюда может включаться исправление опечаток, избегание перенаправлений и другие эвристики.",
"apihelp-query+prefixsearch-param-search": "Поисковый запрос.",
- "apihelp-query+prefixsearch-param-namespace": "Пространства имён для поиска.",
+ "apihelp-query+prefixsearch-param-namespace": "Пространства имён для поиска. Игнорируется, если <var>$1search</var> начинается с корректного префикса пространства имён.",
"apihelp-query+prefixsearch-param-limit": "Максимальное число возвращаемых результатов.",
"apihelp-query+prefixsearch-param-offset": "Количество пропускаемых результатов.",
"apihelp-query+prefixsearch-example-simple": "Поиск названий страниц, начинающихся с <kbd>meaning</kbd>.",
@@ -1007,8 +1015,9 @@
"apihelp-query+recentchanges-paramvalue-prop-sizes": "Добавляет старую и новую длину страницы в байтах.",
"apihelp-query+recentchanges-paramvalue-prop-redirect": "Отмечает правку, если страница является перенаправлением.",
"apihelp-query+recentchanges-paramvalue-prop-patrolled": "Отмечает патрулируемые правки как отпатрулированные или неотпатрулированные.",
+ "apihelp-query+recentchanges-paramvalue-prop-autopatrolled": "Отмечает патрулируемые правки как отпатрулированные или неотпатрулированные.",
"apihelp-query+recentchanges-paramvalue-prop-loginfo": "Добавляет информацию о записи журнала (идентификатор записи, её тип, и так далее).",
- "apihelp-query+recentchanges-paramvalue-prop-tags": "Перечисляет теги записи.",
+ "apihelp-query+recentchanges-paramvalue-prop-tags": "Перечисляет метки записи.",
"apihelp-query+recentchanges-paramvalue-prop-sha1": "Добавляет значение контрольных сумм для записей, связанных с версией.",
"apihelp-query+recentchanges-param-token": "Вместо этого используйте <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.",
"apihelp-query+recentchanges-param-show": "Показать только элементы, удовлетворяющие данным критериям. Например, чтобы отобразить только малые правки, сделанные зарегистрированными участниками, установите $1show=minor|!anon.",
@@ -1058,7 +1067,7 @@
"apihelp-query+revisions+base-paramvalue-prop-parsedcomment": "Распарсенное описание правки.",
"apihelp-query+revisions+base-paramvalue-prop-content": "Текст версии.",
"apihelp-query+revisions+base-paramvalue-prop-tags": "Метки версии.",
- "apihelp-query+revisions+base-paramvalue-prop-parsetree": "<span class=\"apihelp-deprecated\">Не поддерживается.</span> Вместо этого используйте <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd> или <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>. Дерево парсинга XML содержимого версии (требуется модель содержимого <code>$1</code>).",
+ "apihelp-query+revisions+base-paramvalue-prop-parsetree": "<span class=\"apihelp-deprecated\">Устарело.</span> Вместо этого используйте <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd> или <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>. Дерево парсинга XML содержимого версии (требуется модель содержимого <code>$1</code>).",
"apihelp-query+revisions+base-param-limit": "Сколько версий вернуть.",
"apihelp-query+revisions+base-param-expandtemplates": "Вместо этого используйте <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd>. Раскрыть шаблоны в содержимом версии (требуется $1prop=content).",
"apihelp-query+revisions+base-param-generatexml": "Вместо этого используйте <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd> или <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>. Сгенерировать дерево парсинга XML содержимого версии (требуется $1prop=content).",
@@ -1086,6 +1095,7 @@
"apihelp-query+search-paramvalue-prop-sectiontitle": "Добавляет заголовок найденного раздела.",
"apihelp-query+search-paramvalue-prop-categorysnippet": "Добавляет распарсенный фрагмент найденной категории.",
"apihelp-query+search-paramvalue-prop-isfilematch": "Добавляет логическое значение, обозначающее, удовлетворяет ли поисковому запросу содержимое файла.",
+ "apihelp-query+search-paramvalue-prop-extensiondata": "Добавляет дополнительные данные, сгенерированные расширениями.",
"apihelp-query+search-paramvalue-prop-score": "Игнорируется.",
"apihelp-query+search-paramvalue-prop-hasrelated": "Игнорируется.",
"apihelp-query+search-param-limit": "Сколько страниц вернуть.",
@@ -1114,7 +1124,7 @@
"apihelp-query+siteinfo-paramvalue-prop-languages": "Возвращает список языков, поддерживаемых MediaWiki (опционально локализованных с помощью <var>$1inlanguagecode</var>).",
"apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Возвращает список языковых кодов, для которых включён [[mw:Special:MyLanguage/LanguageConverter|LanguageConverter]], а также варианты, поддерживаемые для каждого языка.",
"apihelp-query+siteinfo-paramvalue-prop-skins": "Возвращает список доступных скинов (опционально локализованных с помощью <var>$1inlanguagecode</var>, в противном случае — на языке вики).",
- "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "Возвращает список тегов рашсирений парсера.",
+ "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "Возвращает список меток рашсирений парсера.",
"apihelp-query+siteinfo-paramvalue-prop-functionhooks": "Возвращает список перехватчиков функций парсера.",
"apihelp-query+siteinfo-paramvalue-prop-showhooks": "Возвращает список всех подписанных перехватчиков (содержимое <var>[[mw:Special:MyLanguage/Manual:$wgHooks|$wgHooks]]</var>).",
"apihelp-query+siteinfo-paramvalue-prop-variables": "Возвращает список идентификаторов переменных.",
@@ -1128,10 +1138,10 @@
"apihelp-query+siteinfo-example-simple": "Запросить информацию о сайте.",
"apihelp-query+siteinfo-example-interwiki": "Запросить список локальных префиксов интервик.",
"apihelp-query+siteinfo-example-replag": "Проверить текущее отставание репликации.",
- "apihelp-query+stashimageinfo-summary": "Возвращает информацию о файлах в тайнике (upload stash).",
+ "apihelp-query+stashimageinfo-summary": "Возвращает информацию о файлах во временном хранилище.",
"apihelp-query+stashimageinfo-param-filekey": "Ключ, идентифицирующий предыдущую временную загрузку.",
"apihelp-query+stashimageinfo-param-sessionkey": "Синоним $1filekey для обратной совместимости.",
- "apihelp-query+stashimageinfo-example-simple": "Вернуть информацию о файле в тайнике.",
+ "apihelp-query+stashimageinfo-example-simple": "Вернуть информацию о файле во временном хранилище.",
"apihelp-query+stashimageinfo-example-params": "Вернуть эскизы двух файлов в тайнике.",
"apihelp-query+tags-summary": "Список меток правок.",
"apihelp-query+tags-param-limit": "Максимальное количество меток в списке.",
@@ -1184,6 +1194,7 @@
"apihelp-query+usercontribs-paramvalue-prop-sizediff": "Добавляет разницу между размерами страницы до и после правки.",
"apihelp-query+usercontribs-paramvalue-prop-flags": "Добавляет флаги правки.",
"apihelp-query+usercontribs-paramvalue-prop-patrolled": "Отмечает отпатрулированные правки.",
+ "apihelp-query+usercontribs-paramvalue-prop-autopatrolled": "Отмечает автоматически отпатрулированные правки.",
"apihelp-query+usercontribs-paramvalue-prop-tags": "Перечисляет метки правки.",
"apihelp-query+usercontribs-param-show": "Показать только элементы, удовлетворяющие данным критериям, например, только не малые правки: <kbd>$2show=!minor</kbd>.\n\nЕсли установлено <kbd>$2show=patrolled</kbd> или <kbd>$2show=!patrolled</kbd>, правки старее <var>[[mw:Special:MyLanguage/Manual:$wgRCMaxAge|$wgRCMaxAge]]</var> ($1 {{PLURAL:$1|секунды|секунд}}) не будут показаны.",
"apihelp-query+usercontribs-param-tag": "Только правки с заданной меткой.",
@@ -1248,9 +1259,11 @@
"apihelp-query+watchlist-paramvalue-prop-parsedcomment": "Добавляет распарсенное описание правки.",
"apihelp-query+watchlist-paramvalue-prop-timestamp": "Добавляет временную метку правки.",
"apihelp-query+watchlist-paramvalue-prop-patrol": "Определяет, была ли правка отпатрулирована.",
+ "apihelp-query+watchlist-paramvalue-prop-autopatrol": "Отмечает автоматически отпатрулированные правки.",
"apihelp-query+watchlist-paramvalue-prop-sizes": "Добавляет старую и новую длину страницы.",
"apihelp-query+watchlist-paramvalue-prop-notificationtimestamp": "Добавляет метку времени, когда участник был уведомлён о правке.",
"apihelp-query+watchlist-paramvalue-prop-loginfo": "Добавляет информацию о журнале, где уместно.",
+ "apihelp-query+watchlist-paramvalue-prop-tags": "Перечисляет метки записи.",
"apihelp-query+watchlist-param-show": "Показать только элементы, удовлетворяющие данным критериям. Например, чтобы отобразить только малые правки, сделанные зарегистрированными участниками, установите $1show=minor|!anon.",
"apihelp-query+watchlist-param-type": "Какие типы правок показать:",
"apihelp-query+watchlist-paramvalue-type-edit": "Обычные правки страниц.",
@@ -1327,7 +1340,7 @@
"apihelp-setpagelanguage-param-pageid": "Идентификатор страницы, язык которой вы желаете поменять. Не может быть использовано одновременно с <var>$1title</var>.",
"apihelp-setpagelanguage-param-lang": "Код нового языка. Используйте <kbd>default</kbd> для смены на язык содержимого по умолчанию для этой вики.",
"apihelp-setpagelanguage-param-reason": "Причина изменения.",
- "apihelp-setpagelanguage-param-tags": "Изменить теги записей в журнале, возникающих в результате этого действия.",
+ "apihelp-setpagelanguage-param-tags": "Изменить метки записей в журнале, возникающих в результате этого действия.",
"apihelp-setpagelanguage-example-language": "Изменить язык <kbd>Main Page</kbd> на баскский.",
"apihelp-setpagelanguage-example-default": "Изменить язык страницы с идентификатором 123 на язык по умолчанию.",
"apihelp-stashedit-summary": "Подготовка правки в общем кэше.",
@@ -1352,7 +1365,7 @@
"apihelp-tag-example-rev": "Добавить метку <kbd>vandalism</kbd> к версии с идентификатором 123 без указания причины.",
"apihelp-tag-example-log": "Удаление метки <kbd>spam</kbd> из записи журнала с идентификатором 123 с причиной <kbd>Wrongly applied</kbd>.",
"apihelp-tokens-summary": "Получение токенов для действий, связанных с редактированием данных.",
- "apihelp-tokens-extended-description": "Этот модуль не поддерживается в пользу [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].",
+ "apihelp-tokens-extended-description": "Этот модуль устарел в пользу [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].",
"apihelp-tokens-param-type": "Типы запрашиваемых токенов.",
"apihelp-tokens-example-edit": "Получить токен редактирования (по умолчанию).",
"apihelp-tokens-example-emailmove": "Получить токен электронной почты и переименования.",
@@ -1389,7 +1402,7 @@
"apihelp-upload-param-url": "Ссылка на запрашиваемый файл.",
"apihelp-upload-param-filekey": "Ключ, идентифицирующий предыдущую временную загрузку.",
"apihelp-upload-param-sessionkey": "Синоним $1filekey, обслуживаемый для обратной совместимости.",
- "apihelp-upload-param-stash": "Если задано, сервер временно поместит файл в тайник вместо загрузки его в хранилище.",
+ "apihelp-upload-param-stash": "Если задано, сервер поместит файл во временное хранилище, не добавив в постоянное.",
"apihelp-upload-param-filesize": "Полны размер файла.",
"apihelp-upload-param-offset": "Смещение блока в байтах.",
"apihelp-upload-param-chunk": "Содержимое кусочка.",
@@ -1459,7 +1472,7 @@
"api-help-lead": "Это автоматически сгенерированная страница документации MediaWiki API.\n\nДокументация и примеры: https://www.mediawiki.org/wiki/API",
"api-help-main-header": "Главный модуль",
"api-help-undocumented-module": "Нет документации для модуля $1.",
- "api-help-flag-deprecated": "Этот модуль не поддерживается.",
+ "api-help-flag-deprecated": "Этот модуль устарел.",
"api-help-flag-internal": "<strong>Этот модуль внутренний или нестабильный.</strong> Его операции могут измениться без предупреждения.",
"api-help-flag-readrights": "Этот модуль требует прав на чтение.",
"api-help-flag-writerights": "Этот модуль требует прав на запись.",
@@ -1471,7 +1484,7 @@
"api-help-license-noname": "Лицензия: [[$1|см. ссылку]]",
"api-help-license-unknown": "Лицензия: <span class=\"apihelp-unknown\">unknown</span>",
"api-help-parameters": "Параметр{{PLURAL:$1||ы}}:",
- "api-help-param-deprecated": "Не поддерживается.",
+ "api-help-param-deprecated": "Устарело.",
"api-help-param-required": "Это обязательный параметр.",
"api-help-datatypes-header": "Типы данных",
"api-help-datatypes": "Ввод в MediaWiki должен быть NFC-нормализованным UTF-8. MediaWiki может попытаться преобразовать другой ввод, но это приведёт к провалу некоторых операций (таких, как [[Special:ApiHelp/edit|редактирование]] со сверкой MD5).\n\nНекоторые типы параметров в запросах API требуют дополнительных пояснений:\n;логический\n:Логические параметры работают как флажки (checkboxes) в HTML: если параметр задан, независимо от его значения, он воспринимается за истину. Для передачи ложного значения просто опустите параметр.\n;временные метки\n:Временные метки могут быть заданы в нескольких форматах. Рекомендуемым является дата и время ISO 8601. Всё время считается в UTC, любые включённые часовые пояса игнорируются.\n:* Дата и время ISO 8601: <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (знаки препинания и <kbd>Z</kbd> необязательны)\n:* Дата и время ISO 8601 с (игнорируемой) дробной частью секунд: <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (дефисы, двоеточия и <kbd>Z</kbd> необязательны)\n:* Формат MediaWiki: <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* Общий числовой формат: <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (необязательный часовой пояс <kbd>GMT</kbd>, <kbd>+<var>##</var></kbd> или <kbd>-<var>##</var></kbd> игнорируется)\n:* Формат EXIF: <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Формат RFC 2822 (часовой пояс может быть опущен): <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Формат RFC 850 (часовой пояс может быть опущен): <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Формат ctime языка программирования C: <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* Количество секунд, прошедших с 1970-01-01T00:00:00Z, в виде челого числа с от 1 до 13 знаками (исключая <kbd>0</kbd>)\n:* Строка <kbd>now</kbd>\n;альтернативный разделитель значений\n:Параметры, принимающие несколько значений, обычно отправляются со значениями, разделёнными с помощью символа пайпа, например, <kbd>param=value1|value2</kbd> или <kbd>param=value1%7Cvalue2</kbd>. Если значение должно содержать символ пайпа, используйте U+001F (Unit Separator) в качестве разделителя ''и'' добавьте в начало значения U+001F, например, <kbd>param=%1Fvalue1%1Fvalue2</kbd>.",
@@ -1501,6 +1514,8 @@
"api-help-param-direction": "В каком порядке перечислять:\n;newer: Начать с самых старых. Обратите внимание: $1start должно быть раньше $1end.\n;older: Начать с самых новых (по умолчанию). Обратите внимание: $1start должно быть позже $1end.",
"api-help-param-continue": "Когда доступно больше результатов, используйте это для продолжения.",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(описание отсутствует)</span>",
+ "api-help-param-maxbytes": "Не может быть длиннее $1 {{PLURAL:$1|байта|байтов}}.",
+ "api-help-param-maxchars": "Не может быть длиннее $1 {{PLURAL:$1|символа|символов}}.",
"api-help-examples": "Пример{{PLURAL:$1||ы}}:",
"api-help-permissions": "{{PLURAL:$1|Разрешение|Разрешения}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|Гарантируется}}: $2",
@@ -1547,7 +1562,7 @@
"apierror-blockedfrommail": "Отправка электронной почты была для вас заблокирована.",
"apierror-blocked": "Редактирование было для вас заблокировано.",
"apierror-botsnotsupported": "Этот интерфейс не поддерживается для ботов.",
- "apierror-cannot-async-upload-file": "Параметры <var>async</var> и <var>file</var> не могут применяться вместе. Если вы хотите ассинхронно обработать загруженный файл, сначала загрузите его в тайник (используя параметр <var>stash</var>), а затем опубликуйте этот файл ассинхронно (используя параметры <var>filekey</var> и <var>async</var>).",
+ "apierror-cannot-async-upload-file": "Параметры <var>async</var> и <var>file</var> не могут применяться вместе. Если вы хотите ассинхронно обработать загруженный файл, сначала загрузите его во временное хранилище (используя параметр <var>stash</var>), а затем опубликуйте этот файл ассинхронно (используя параметры <var>filekey</var> и <var>async</var>).",
"apierror-cannotreauthenticate": "Это действие недоступно, так как ваша личность не может быть подтверждена.",
"apierror-cannotviewtitle": "У вас нет прав на просмотр $1.",
"apierror-cantblock-email": "У вас нет прав блокировать участникам отправку электронной почты через интерфейс вики.",
@@ -1563,6 +1578,8 @@
"apierror-chunk-too-small": "Минимальный размер кусочка — $1 {{PLURAL:$1|байт|байта|байт}}, если кусочек не является последним.",
"apierror-cidrtoobroad": "Диапазоны $1 CIDR, шире /$2, не разрешены.",
"apierror-compare-no-title": "Невозможно выполнить преобразование перед записью правки без заголовка. Попробуйте задать <var>fromtitle</var> или <var>totitle</var>.",
+ "apierror-compare-nosuchfromsection": "Нет секции $1 в содержимом 'from'.",
+ "apierror-compare-nosuchtosection": "Нет секции $1 в содержимом 'to'.",
"apierror-compare-relative-to-nothing": "Нет версии 'from', к которой относится <var>torelative</var>.",
"apierror-contentserializationexception": "Сериализация содержимого провалилась: $1",
"apierror-contenttoobig": "Предоставленное вами содержимое превышает максимальный размер страницы в $1 {{PLURAL:$1|килобайт|килобайта|килобайтов}}.",
@@ -1603,6 +1620,8 @@
"apierror-invalidurlparam": "Некорректное значение <var>$1urlparam</var> (<kbd>$2=$3</kbd>).",
"apierror-invaliduser": "Некорректное имя участника «$1».",
"apierror-invaliduserid": "Некорректный идентификатор участника <var>$1</var>.",
+ "apierror-maxbytes": "Параметр <var>$1</var> не может быть длиннее $2 {{PLURAL:$2|байта|байтов}}",
+ "apierror-maxchars": "Параметр <var>$1</var> не может быть длиннее $2 {{PLURAL:$2|символа|символов}}",
"apierror-maxlag-generic": "Ожидание сервера базы данных: $1 {{PLURAL:$1|секунда|секунды|секунд}} задержки.",
"apierror-maxlag": "Ожидание $2: $1 {{PLURAL:$1|секунда|секунды|секунд}} задержки.",
"apierror-mimesearchdisabled": "Поиск по MIME отключен в жадном режиме.",
@@ -1618,13 +1637,12 @@
"apierror-missingtitle-byname": "Страница $1 не существует.",
"apierror-moduledisabled": "Модуль <kbd>$1</kbd> был отключён.",
"apierror-multival-only-one-of": "Параметру <var>$1</var> может быть присвоено только {{PLURAL:$3|значение|одно из значений}} $2.",
- "apierror-multival-only-one": "Параметру <var>$1</var> может быть присвоено только одно значение.",
"apierror-multpages": "Параметр <var>$1</var> может быть применён только к одной странице.",
"apierror-mustbeloggedin-changeauth": "Вы должны быть авторизованы для смены аутентификационных данных.",
"apierror-mustbeloggedin-generic": "Вы должны быть авторизованы.",
"apierror-mustbeloggedin-linkaccounts": "Вы должны быть авторизованы для привязывания аккаунтов.",
"apierror-mustbeloggedin-removeauth": "Вы должны быть авторизованы для удаления аутентификационных данных.",
- "apierror-mustbeloggedin-uploadstash": "Тайник загрузки (upload stash) доступен только для авторизованных участников.",
+ "apierror-mustbeloggedin-uploadstash": "Временное хранилище доступно только для авторизованных участников.",
"apierror-mustbeloggedin": "Вы должны быть авторизованы в $1.",
"apierror-mustbeposted": "Модуль <kbd>$1</kbd> требует запроса POST.",
"apierror-mustpostparams": "{{PLURAL:$2|Следующий параметр был найден|Следующие параметры были найдены}} в строке запроса, но {{PLURAL:$2|должен|должны}} находиться в теле POST: $1.",
@@ -1684,16 +1702,16 @@
"apierror-sizediffdisabled": "Подсчёт разницы размеров отключён в жадном режиме.",
"apierror-spamdetected": "Ваша правка была отклонена, так как содержит спам: <code>$1</code>.",
"apierror-specialpage-cantexecute": "У вас нет прав, чтобы просматривать результаты этой служебной страницы.",
- "apierror-stashedfilenotfound": "Невозможно найти файл в тайнике: $1.",
+ "apierror-stashedfilenotfound": "Невозможно найти файл во временном хранилище: $1.",
"apierror-stashedit-missingtext": "Не найдено содержимого тайника для данного хэша.",
"apierror-stashfailed-complete": "Загрузка по кусочкам уже завершена, проверьте статус для получения подробной информации.",
"apierror-stashfailed-nosession": "Не найдено сессии загрузки по кусочкам с заданным ключом.",
- "apierror-stashfilestorage": "Невозможно сохранить загрузку в тайник: $1",
+ "apierror-stashfilestorage": "Невозможно сохранить файл во временном хранилище: $1",
"apierror-stashinvalidfile": "Некорректный файл в тайнике.",
"apierror-stashnosuchfilekey": "Нет такого ключа файла: $1.",
"apierror-stashpathinvalid": "Ключ файла относится к некорректному формату или сам некорректен: $1.",
"apierror-stashwrongowner": "Некорректный владелец: $1",
- "apierror-stashzerolength": "Файл имеет нулевую длину и не может быть сохранён в тайник: $1",
+ "apierror-stashzerolength": "Файл имеет нулевую длину и не может быть сохранён во временное хранилище: $1",
"apierror-systemblocked": "Вы были заблокированы автоматически MediaWiki.",
"apierror-templateexpansion-notwikitext": "Раскрытие шаблонов разрешено только для вики-текстового содержимого. $1 использует модель содержимого $2.",
"apierror-timeout": "Сервер не ответил за ожидаемое время.",
@@ -1708,7 +1726,7 @@
"apierror-unsupportedrepo": "Локальное хранилище файлов не поддерживает запрос всех изображений.",
"apierror-upload-filekeyneeded": "Необходимо задать <var>filekey</var>, если <var>offset</var> не ноль.",
"apierror-upload-filekeynotallowed": "Невозможно обработать <var>filekey</var>, если <var>offset</var> равен 0.",
- "apierror-upload-inprogress": "Процесс загрузки из тайника уже запущен.",
+ "apierror-upload-inprogress": "Процесс загрузки из временного хранилища уже запущен.",
"apierror-upload-missingresult": "Нет результатов данных статуса.",
"apierror-urlparamnormal": "Невозможно нормализовать параметры изображения для $1.",
"apierror-writeapidenied": "У вас нет прав на редактирование этой вики через API.",
@@ -1717,15 +1735,15 @@
"apiwarn-badutf8": "Значение, переданное <var>$1</var>, содержит некорректные или ненормализованные данные. Текстовые данные должны быть корректным NFC-нормализованным Юникодом без символов управления C0, кроме HT (\\t), LF (\\n) и CR (\\r).",
"apiwarn-checktoken-percentencoding": "Проверьте, что символы вроде «+» в токене корректно закодированы %-последовательностями в ссылке.",
"apiwarn-compare-nocontentmodel": "Модель содержимого не может быть определена, предполагается $1.",
- "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> не поддерживается. Пожалуйста, вместо него используйте <kbd>prop=deletedrevisions</kbd> или <kbd>list=alldeletedrevisions</kbd>.",
+ "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> устарел. Пожалуйста, вместо него используйте <kbd>prop=deletedrevisions</kbd> или <kbd>list=alldeletedrevisions</kbd>.",
"apiwarn-deprecation-expandtemplates-prop": "Поскольку никакие значения не были указаны в параметре <var>prop</var>, был использован наследованный формат. Этот формат является устаревшим, и в будущем параметру <var>prop</var> будет присвоено значение по умолчанию, что приведёт к повсеместному использованию нового формата.",
"apiwarn-deprecation-httpsexpected": "Использован HTTP, где ожидался HTTPS.",
- "apiwarn-deprecation-login-botpw": "Вход в основной аккаунт через <kbd>action=login</kbd> не поддерживается и может быть отключен без предупреждения. Для продолжения авторизации с <kbd>action=login</kbd>, см.\n[[Special:BotPasswords]]. Для безопасного продолжения использования входа в основной аккаунт, см. <kbd>action=clientlogin</kbd>.",
+ "apiwarn-deprecation-login-botpw": "Вход в основной аккаунт через <kbd>action=login</kbd> устарел и может быть отключен без предупреждения. Для продолжения авторизации с <kbd>action=login</kbd>, см.\n[[Special:BotPasswords]]. Для безопасного продолжения использования входа в основной аккаунт, см. <kbd>action=clientlogin</kbd>.",
"apiwarn-deprecation-login-nobotpw": "Вход в основной аккаунт через <kbd>action=login</kbd> не поддерживается и может быть отключен без предупреждения. Для безопасной авторизации, см. <kbd>action=clientlogin</kbd>.",
- "apiwarn-deprecation-login-token": "Запрос токена через <kbd>action=login</kbd> не поддерживается. Вместо этого, см. <kbd>action=query&meta=tokens&type=login</kbd>.",
+ "apiwarn-deprecation-login-token": "Запрос токена через <kbd>action=login</kbd> устарел. Используйте <kbd>action=query&meta=tokens&type=login</kbd>.",
"apiwarn-deprecation-parameter": "Параметр <var>$1</var> не поддерживается.",
- "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> не поддерживается с MediaWiki 1.28. Используйте <kbd>prop=headhtml</kbd> при создании новых HTML документов, или <kbd>prop=modules|jsconfigvars</kbd> при обновлении документов на стороне клиента.",
- "apiwarn-deprecation-purge-get": "Использование <kbd>action=purge</kbd> посредством GET не поддерживается. Используйте POST.",
+ "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> устарело с MediaWiki 1.28. Используйте <kbd>prop=headhtml</kbd> при создании новых HTML документов или <kbd>prop=modules|jsconfigvars</kbd> при обновлении документов на стороне клиента.",
+ "apiwarn-deprecation-purge-get": "Использование <kbd>action=purge</kbd> посредством GET устарело. Используйте POST.",
"apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> не поддерживается. Пожалуйста, используйте <kbd>$2</kbd>.",
"apiwarn-difftohidden": "Невозможно сравнить с r$1: содержимое скрыто.",
"apiwarn-errorprinterfailed": "Сборщик ошибок упал. Будет совершена повторная попытка без параметров.",
@@ -1739,13 +1757,14 @@
"apiwarn-notfile": "«$1» не является файлом.",
"apiwarn-nothumb-noimagehandler": "Невозможно создать эскиз, поскольку у $1 нет связанного обработчика изображений.",
"apiwarn-parse-nocontentmodel": "Параметры <var>title</var> или <var>contentmodel</var> не заданы, предполагается $1.",
+ "apiwarn-parse-revidwithouttext": "<var>revid</var> использован без <var>text</var>, при этом запрошены распарсенные свойства страницы. Возможно, вы хотели использовать <var>oldid</var> вместо <var>revid</var>?",
"apiwarn-parse-titlewithouttext": "<var>title</var> использован без <var>text</var>, при этом запрошены распарсенные свойства страницы. Возможно, вы хотели использовать <var>page</var> вместо <var>title</var>?",
"apiwarn-redirectsandrevids": "Раскрытие перенаправлений не может быть использовано вместе с параметром <var>revids</var>. Все перенаправления на точку <var>revids</var> не должны быть раскрыты.",
"apiwarn-tokennotallowed": "Действие «$1» не разрешено для текущего участника.",
"apiwarn-tokens-origin": "Токены не могут быть получены, пока не применено правило ограничения домена.",
"apiwarn-toomanyvalues": "Слишком много значений передано параметру <var>$1</var>. Максимальное число — $2.",
"apiwarn-truncatedresult": "Результат был усечён, поскольку в противном случае он был бы больше лимита в $1 {{PLURAL:$1|байт|байта|байт}}.",
- "apiwarn-unclearnowtimestamp": "Передача «$2» в качестве параметра временной метки <var>$1</var> не поддерживается. Если по какой-то причине вы хотите прямо указать текущее время без вычисления его на стороне клиента, используйте <kbd>now</kbd>.",
+ "apiwarn-unclearnowtimestamp": "Передача «$2» в качестве параметра временной метки <var>$1</var> устарело. Если по какой-то причине вы хотите прямо указать текущее время без вычисления его на стороне клиента, используйте <kbd>now</kbd>.",
"apiwarn-unrecognizedvalues": "{{PLURAL:$3|Нераспознанное значение|Нераспознанные значения}} параметра <var>$1</var>: $2.",
"apiwarn-unsupportedarray": "Параметр <var>$1</var> использует неподдерживаемый синтаксис массивов PHP.",
"apiwarn-urlparamwidth": "Значение ширины ($2), переданное в <var>$1urlparam</var>, было проигнорировано в пользу значения ($3), полученного из параметров <var>$1urlwidth</var>/<var>$1urlheight</var>.",
diff --git a/www/wiki/includes/api/i18n/sv.json b/www/wiki/includes/api/i18n/sv.json
index 025254d1..5c0a12ee 100644
--- a/www/wiki/includes/api/i18n/sv.json
+++ b/www/wiki/includes/api/i18n/sv.json
@@ -15,10 +15,11 @@
"Josve05a",
"Rockyfelle",
"Macofe",
- "Magol"
+ "Magol",
+ "Bengtsson96"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentation]]\n* [[mw:Special:MyLanguage/API:FAQ|Vanliga frågor]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Sändlista]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-nyheter]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Buggar och begäran]\n</div>\n<strong>Status:</strong> Alla funktioner som visas på denna sida bör fungera, men API:et är fortfarande under utveckling och kan ändras när som helst. Prenumerera på [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ sändlistan mediawiki-api-announce] för uppdateringsaviseringar.\n\n<strong>Felaktiga begäran:</strong> När felaktiga begäran skickas till API:et kommer en HTTP-header skickas med nyckeln \"MediaWiki-API-Error\" och sedan kommer både värdet i headern och felkoden som skickades tillbaka anges som samma värde. För mer information se [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Fel och varningar]].\n\n<strong>Testning:</strong> För enkelt testning av API-begäran, se [[Special:ApiSandbox]].",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentation]]\n* [[mw:Special:MyLanguage/API:FAQ|Vanliga frågor]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Sändlista]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-nyheter]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Buggar och begäran]\n</div>\n<strong>Status:</strong> Alla funktioner som visas på denna sida bör fungera, men API:et är fortfarande under utveckling och kan ändras när som helst. Prenumerera på [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ sändlistan mediawiki-api-announce] för uppdateringsaviseringar.\n\n<strong>Felaktiga begäran:</strong> När felaktiga begäran skickas till API:et kommer en HTTP-header skickas med nyckeln \"MediaWiki-API-Error\" och sedan kommer både värdet i headern och felkoden som skickades tillbaka anges som samma värde. För mer information se [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Fel och varningar]].\n\n<p class=\"mw-apisandbox-link\"><strong>Testning:</strong> För enkelt testning av API-begäran, se [[Special:ApiSandbox]].</p>",
"apihelp-main-param-action": "Vilken åtgärd som ska utföras.",
"apihelp-main-param-format": "Formatet för utdata.",
"apihelp-main-param-smaxage": "Ange headervärdet <code>s-maxage</code> till så här många sekunder. Fel cachelagras aldrig.",
@@ -135,7 +136,7 @@
"apihelp-expandtemplates-summary": "Expanderar alla mallar inom wikitext.",
"apihelp-expandtemplates-param-title": "Sidans rubrik.",
"apihelp-expandtemplates-param-text": "Wikitext att konvertera.",
- "apihelp-expandtemplates-param-revid": "Revision ID, för <code><nowiki>{{REVISIONID}}</nowiki></code> och liknande variabler.",
+ "apihelp-expandtemplates-param-revid": "Revisions-ID, för <code><nowiki>{{REVISIONID}}</nowiki></code> och liknande variabler.",
"apihelp-expandtemplates-paramvalue-prop-wikitext": "Den expanderade wikitexten.",
"apihelp-expandtemplates-param-includecomments": "Om HTML-kommentarer skall inkluderas i utdata.",
"apihelp-expandtemplates-param-generatexml": "Generera ett XML tolknings träd (ersatt av $1prop=parsetree).",
@@ -169,8 +170,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Filtrera efter tagg.",
"apihelp-feedrecentchanges-param-target": "Visa endast ändringarna av sidor som den här sidan länkar till.",
"apihelp-feedrecentchanges-param-showlinkedto": "Visa ändringarna på sidor som är länkade till den valda sidan i stället.",
- "apihelp-feedrecentchanges-param-categories": "Visa endast ändringar på sidor i alla dessa kategorier.",
- "apihelp-feedrecentchanges-param-categories_any": "Visa endast ändringar på sidor i någon av kategorierna istället.",
"apihelp-feedrecentchanges-example-simple": "Visa senaste ändringar",
"apihelp-feedrecentchanges-example-30days": "Visa senaste ändringar för 30 dygn",
"apihelp-feedwatchlist-summary": "Returnerar ett flöde från bevakningslistan.",
@@ -253,7 +252,7 @@
"apihelp-opensearch-summary": "Sök wikin med protokollet OpenSearch.",
"apihelp-opensearch-param-search": "Söksträng.",
"apihelp-opensearch-param-limit": "Maximalt antal resultat att returnera.",
- "apihelp-opensearch-param-namespace": "Namnrymder att genomsöka.",
+ "apihelp-opensearch-param-namespace": "Namnrymder att genomsöka. Ignoreras om <var>$1search</var> börjar med ett giltigt namnrymdsprefix.",
"apihelp-opensearch-param-suggest": "Gör ingenting om <var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> är falskt.",
"apihelp-opensearch-param-format": "Formatet för utdata.",
"apihelp-opensearch-example-te": "Hitta sidor som börjar med <kbd>Te</kbd>.",
@@ -269,6 +268,7 @@
"apihelp-parse-param-pageid": "Tolka innehållet på denna sida. Åsidosätter <var>$1sidan</var>.",
"apihelp-parse-param-prop": "Vilka bitar av information att få:",
"apihelp-parse-paramvalue-prop-categorieshtml": "Ger HTML-version av kategorierna.",
+ "apihelp-parse-param-disablepp": "Använd <var>$1disablelimitreport</var> istället.",
"apihelp-parse-param-preview": "Tolka i preview-läget.",
"apihelp-parse-example-page": "Tolka en sida.",
"apihelp-parse-example-text": "Tolka wikitext.",
@@ -281,6 +281,7 @@
"apihelp-protect-summary": "Ändra skyddsnivån för en sida.",
"apihelp-protect-example-protect": "Skydda en sida",
"apihelp-purge-summary": "Rensa cachen för angivna titlar.",
+ "apihelp-purge-param-forcelinkupdate": "Uppdatera länktabellerna.",
"apihelp-query-param-list": "Vilka listor att hämta.",
"apihelp-query-param-meta": "Vilka metadata att hämta.",
"apihelp-query-example-allpages": "Hämta sidversioner av sidor som börjar med <kbd>API/</kbd>.",
@@ -289,6 +290,7 @@
"apihelp-query+allcategories-param-min": "Returnera endast kategorier med minst så här många medlemmar.",
"apihelp-query+allcategories-param-max": "Returnera endast kategorier med som mest så här många medlemmar.",
"apihelp-query+allcategories-param-limit": "Hur många kategorier att returnera.",
+ "apihelp-query+allcategories-param-prop": "Vilka egenskaper att hämta:",
"apihelp-query+allcategories-paramvalue-prop-size": "Lägger till antal sidor i kategorin.",
"apihelp-query+allcategories-paramvalue-prop-hidden": "Märker kategorier som är dolda med <code>_&#95;HIDDENCAT_&#95;</code>.",
"apihelp-query+alldeletedrevisions-summary": "Lista alla raderade revisioner av en användare or inom en namnrymd.",
@@ -305,6 +307,7 @@
"apihelp-query+alldeletedrevisions-example-ns-main": "Lista dem första 50 revideringarna i huvud-namnrymden",
"apihelp-query+allfileusages-summary": "Lista all fil användningsområden, inklusive icke-existerande.",
"apihelp-query+allfileusages-param-prefix": "Sök för all fil-titlar som börjar med detta värde.",
+ "apihelp-query+allfileusages-paramvalue-prop-title": "Lägger till filens titel.",
"apihelp-query+allfileusages-param-limit": "Hur många saker att returnera totalt.",
"apihelp-query+allfileusages-param-dir": "Riktningen att lista mot.",
"apihelp-query+allfileusages-example-unique": "Lista unika filtitlar",
@@ -398,6 +401,7 @@
"apihelp-query+categorymembers-summary": "Lista alla sidor i en angiven kategori.",
"apihelp-query+categorymembers-paramvalue-prop-ids": "Lägger till sid-ID.",
"apihelp-query+categorymembers-paramvalue-prop-title": "Lägger till titeln och namnrymds-ID för sidan.",
+ "apihelp-query+categorymembers-param-sort": "Egenskap att sortera efter.",
"apihelp-query+categorymembers-param-dir": "I vilken riktning att sortera.",
"apihelp-query+categorymembers-param-startsortkey": "Använd $1starthexsortkey istället.",
"apihelp-query+categorymembers-param-endsortkey": "Använd $1endhexsortkey istället.",
@@ -419,23 +423,36 @@
"apihelp-query+embeddedin-summary": "Hitta alla sidor som bäddar in (inkluderar) angiven titel.",
"apihelp-query+embeddedin-param-dir": "Riktningen att lista mot.",
"apihelp-query+embeddedin-param-limit": "Hur många sidor att returnera totalt.",
+ "apihelp-query+extlinks-param-limit": "Hur många länkar som ska returneras.",
"apihelp-query+extlinks-example-simple": "Hämta en lista över externa länkar på <kbd>Main Page</kbd>.",
+ "apihelp-query+exturlusage-param-limit": "Hur många sidor att returnera.",
+ "apihelp-query+exturlusage-example-simple": "Visa sidor som länkar till <kbd>https://www.mediawiki.org</kbd>.",
+ "apihelp-query+filearchive-param-limit": "Hur många bilder att returnera totalt.",
"apihelp-query+filearchive-param-dir": "Riktningen att lista mot.",
"apihelp-query+filearchive-paramvalue-prop-timestamp": "Lägger till tidsstämpel för den uppladdade versionen.",
"apihelp-query+filearchive-paramvalue-prop-user": "Lägger till användaren som laddade upp bildversionen.",
+ "apihelp-query+filearchive-paramvalue-prop-dimensions": "Alias för storlek.",
+ "apihelp-query+filearchive-paramvalue-prop-description": "Lägger till beskrivning till bildversionen.",
"apihelp-query+filearchive-example-simple": "Visa en lista över alla borttagna filer.",
"apihelp-query+filerepoinfo-summary": "Returnera metainformation om bildegenskaper som konfigureras på wikin.",
"apihelp-query+fileusage-summary": "Hitta alla sidor som använder angivna filer.",
+ "apihelp-query+fileusage-param-prop": "Vilka egenskaper att hämta:",
"apihelp-query+fileusage-paramvalue-prop-title": "Titel för varje sida.",
"apihelp-query+fileusage-paramvalue-prop-redirect": "Flagga om sidan är en omdirigering.",
"apihelp-query+imageinfo-summary": "Returnerar filinformation och uppladdningshistorik.",
+ "apihelp-query+imageinfo-param-prop": "Vilka filinformation att hämta:",
+ "apihelp-query+imageinfo-paramvalue-prop-timestamp": "Lägger till tidsstämpel för den uppladdade versionen.",
"apihelp-query+imageinfo-paramvalue-prop-userid": "Lägg till det användar-ID som laddade upp varje filversion.",
+ "apihelp-query+imageinfo-paramvalue-prop-dimensions": "Alias för storlek.",
+ "apihelp-query+images-param-limit": "Hur många filer att returnera.",
"apihelp-query+images-param-dir": "Riktningen att lista mot.",
+ "apihelp-query+images-example-simple": "Hämta en lista över filer som används på [[Main Page]].",
"apihelp-query+imageusage-summary": "Hitta alla sidor som användare angiven bildtitel.",
"apihelp-query+imageusage-param-dir": "Riktningen att lista mot.",
"apihelp-query+imageusage-example-simple": "Visa sidor med hjälp av [[:File:Albert Einstein Head.jpg]].",
"apihelp-query+imageusage-example-generator": "Hämta information om sidor med hjälp av [[:File:Albert Einstein Head.jpg]].",
"apihelp-query+info-summary": "Få grundläggande sidinformation.",
+ "apihelp-query+info-paramvalue-prop-varianttitles": "Ger visningstiteln i alla variationer på webbplatsens innehållsspråk.",
"apihelp-query+iwbacklinks-param-limit": "Hur många sidor att returnera totalt.",
"apihelp-query+iwbacklinks-param-dir": "Riktningen att lista mot.",
"apihelp-query+iwlinks-param-dir": "Riktningen att lista mot.",
@@ -448,6 +465,7 @@
"apihelp-query+linkshere-summary": "Hitta alla sidor som länkar till angivna sidor.",
"apihelp-query+logevents-summary": "Hämta händelser från loggar.",
"apihelp-query+pageswithprop-summary": "Lista alla sidor som använder en angiven sidegenskap.",
+ "apihelp-query+prefixsearch-param-namespace": "Namnrymder att söka efter. Ignoreras om <var>$1search</var> börjar med ett giltigt namnrymdsprefix.",
"apihelp-query+prefixsearch-param-profile": "Sök profil att använda.",
"apihelp-query+protectedtitles-param-limit": "Hur många sidor att returnera totalt.",
"apihelp-query+protectedtitles-example-simple": "Lista skyddade titlar.",
@@ -520,10 +538,14 @@
"api-help-param-limit": "Inte mer än $1 tillåts.",
"api-help-param-limit2": "Inte mer än $1 ($2 för robotar) tillåts.",
"api-help-param-multi-separate": "Separera värden med <kbd>|</kbd> eller [[Special:ApiHelp/main#main/datatypes|alternativ]].",
+ "api-help-param-maxbytes": "Kan inte vara längre än $1 {{PLURAL:$1|byte}}.",
+ "api-help-param-maxchars": "Kan inte vara längre än $1 {{PLURAL:$1|tecken}}.",
"apierror-articleexists": "Artikeln du försökte skapa har redan skapats.",
"apierror-baddiff": "Diff kan inte hämtas. En eller båda sidversioner finns inte eller du har inte behörighet för att visa dem.",
"apierror-invalidoldimage": "Parametern <var>oldimage</var> har ett ogiltigt format.",
"apierror-invalidsection": "Parametern <var>section</var> måste vara ett giltigt avsnitts-ID eller <kbd>new</kbd>.",
+ "apierror-maxbytes": "Parametern <var>$1</var> kan inte var längre än $2 {{PLURAL:$2|byte}}",
+ "apierror-maxchars": "Parametern <var>$1</var> kan inte vara längre än $2 {{PLURAL:$2|tecken}}",
"apierror-nosuchuserid": "Det finns ingen användare med ID $1.",
"apierror-offline": "Kunde inte fortsätta p.g.a. problem med nätverksanslutningen. Se till att du har en fungerande Internetanslutning och försök igen.",
"apierror-protect-invalidaction": "Ogiltig skyddstyp \"$1\".",
diff --git a/www/wiki/includes/api/i18n/uk.json b/www/wiki/includes/api/i18n/uk.json
index e43c3830..bc0b365b 100644
--- a/www/wiki/includes/api/i18n/uk.json
+++ b/www/wiki/includes/api/i18n/uk.json
@@ -12,7 +12,8 @@
"Andriykopanytsia",
"Максим Підліснюк",
"AS",
- "Umherirrender"
+ "Umherirrender",
+ "Choomaq"
]
},
"apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Документація]]\n* [[mw:Special:MyLanguage/API:FAQ|ЧаПи]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Список розсилки]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Оголошення API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Баґи і запити]\n</div>\n<strong>Статус:</strong> Усі функції, вказані на цій сторінці, мають працювати, але API далі перебуває в активній розробці і може змінитися у будь-який момент. Підпишіться на [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ список розсилки mediawiki-api-announce], щоб помічати оновлення.\n\n<strong>Хибні запити:</strong> Коли до API надсилаються хибні запити, буде відіслано HTTP-шапку з ключем «MediaWiki-API-Error», а тоді і значення шапки, і код помилки, надіслані назад, будуть встановлені з тим же значенням. Більше інформації див. на [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]].\n\n<strong>Тестування:</strong> Для зручності тестування запитів API, див. [[Special:ApiSandbox]].",
@@ -202,8 +203,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "Фільтрувати за теґом.",
"apihelp-feedrecentchanges-param-target": "Показати лише зміни на сторінках, на які посилається ця сторінка.",
"apihelp-feedrecentchanges-param-showlinkedto": "Показати натомість лише зміни на сторінках, які посилаються на цю сторінку.",
- "apihelp-feedrecentchanges-param-categories": "Показати лише зміни сторінок у всіх цих категоріях.",
- "apihelp-feedrecentchanges-param-categories_any": "Показати натомість лише зміни на сторінках у будь-якій з цих категорій.",
"apihelp-feedrecentchanges-example-simple": "Показати нещодавні зміни.",
"apihelp-feedrecentchanges-example-30days": "Показати нещодавні зміни за 30 днів.",
"apihelp-feedwatchlist-summary": "Видає стрічку списку спостереження.",
@@ -324,6 +323,7 @@
"apihelp-parse-extended-description": "Див. різні prop-модулі <kbd>[[Special:ApiHelp/query|action=query]]</kbd>, щоб отримати інформацію з поточної версії сторінки.\n\nЄ декілька способів вказати текст для аналізу:\n# Вказати сторінку або версію, використавши <var>$1page</var>, <var>$1pageid</var> або <var>$1oldid</var>.\n# Вказати безпосередньо, використавши <var>$1text</var>, <var>$1title</var> і <var>$1contentmodel</var>.\n# Вказати лише підсумок аналізу. <var>$1prop</var> повинен мати порожнє значення.",
"apihelp-parse-param-title": "Назва сторінки, якій належить текст. Якщо пропущена, має бути вказано <var>$1contentmodel</var>, а як назву буде вжито [[API]].",
"apihelp-parse-param-text": "Текст для аналізу. Використати <var>$1title</var> або <var>$1contentmodel</var> для контролю моделі вмісту.",
+ "apihelp-parse-param-revid": "Ідентифікатор версії, для <code><nowiki>{{REVISIONID}}</nowiki></code> та подібних змінних.",
"apihelp-parse-param-summary": "Підсумок для аналізу.",
"apihelp-parse-param-page": "Аналізувати вміст цієї сторінки. Не можна використати разом з <var>$1text</var> і <var>$1title</var>.",
"apihelp-parse-param-pageid": "Аналізувати вміст цієї сторінки. Перевизначає <var>$1page</var>.",
@@ -733,7 +733,7 @@
"apihelp-query+exturlusage-param-namespace": "Простори назв для переліку.",
"apihelp-query+exturlusage-param-limit": "Скільки сторінок виводити.",
"apihelp-query+exturlusage-param-expandurl": "Розгорнути протокол-залежні URL за канонічним протоколом.",
- "apihelp-query+exturlusage-example-simple": "Показати сторінки, які посилаються на <kbd>http://www.mediawiki.org</kbd>.",
+ "apihelp-query+exturlusage-example-simple": "Показати сторінки, які посилаються на <kbd>https://www.mediawiki.org</kbd>.",
"apihelp-query+filearchive-summary": "Перерахувати всі вилучені файли послідовно.",
"apihelp-query+filearchive-param-from": "Назва зображення, з якої почати перелічувати.",
"apihelp-query+filearchive-param-to": "Назва зображення, якою закінчити перелічувати.",
@@ -790,7 +790,7 @@
"apihelp-query+imageinfo-paramvalue-prop-archivename": "Додає назву файлу архівної версії для неостанніх версій.",
"apihelp-query+imageinfo-paramvalue-prop-bitdepth": "Додає бітну глибину версії.",
"apihelp-query+imageinfo-paramvalue-prop-uploadwarning": "Використовується на Special:Upload page для отримання інформації про наявний файл. Не призначено для використання поза ядром MediaWiki.",
- "apihelp-query+imageinfo-paramvalue-prop-badfile": "Додає інформацію про те, чи перебуває файл у \n[[MediaWiki:Bad image list|списку недозволених файлів]]",
+ "apihelp-query+imageinfo-paramvalue-prop-badfile": "Додає інформацію про те, чи перебуває файл у [[MediaWiki:Bad image list|списку недозволених файлів]]",
"apihelp-query+imageinfo-param-limit": "Скільки виводити версій кожного файлу.",
"apihelp-query+imageinfo-param-start": "Часова мітка, з якої почати список.",
"apihelp-query+imageinfo-param-end": "Часова мітка, на якій закінчити список.",
@@ -1340,8 +1340,8 @@
"apihelp-tokens-summary": "Отримати жетони для дій пов'язаних зі зміною даних.",
"apihelp-tokens-extended-description": "Цей модуль застарів на користь [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].",
"apihelp-tokens-param-type": "Які типи жетонів запитати.",
- "apihelp-tokens-example-edit": "Отримати жетон редагування (за замовчуванням).",
- "apihelp-tokens-example-emailmove": "Отримати жетон електронної пошти та жетон перейменування.",
+ "apihelp-tokens-example-edit": "Отримати токен редагування (за замовчуванням).",
+ "apihelp-tokens-example-emailmove": "Отримати токен електронної пошти та токен перейменування.",
"apihelp-unblock-summary": "Розблокувати користувача.",
"apihelp-unblock-param-id": "Ідентифікатор блоку чи розблокування (отриманий через <kbd>list=blocks</kbd>). Не може бути використано разом із <var>$1user</var> або <var>$1userid</var>.",
"apihelp-unblock-param-user": "Ім'я користувача, IP-адреса чи IP-діапазон до розблокування. Не може бути використано разом із <var>$1id</var> або <var>$1userid</var>.",
@@ -1604,7 +1604,6 @@
"apierror-missingtitle-byname": "Сторінка $1 не існує.",
"apierror-moduledisabled": "Модуль <kbd>$1</kbd> було вимкнено.",
"apierror-multival-only-one-of": "{{PLURAL:$3|Лише значення|Лише одне значення з}} $2 дозволене для параметра <var>$1</var>.",
- "apierror-multival-only-one": "Лише одне значення дозволене для параметра <var>$1</var>.",
"apierror-multpages": "<var>$1</var> може використовуватись тільки з однією сторінкою.",
"apierror-mustbeloggedin-changeauth": "Вам треба увійти в систему, щоб змінити автентифікаційні дані.",
"apierror-mustbeloggedin-generic": "Ви повинні перебувати в системі.",
@@ -1725,6 +1724,7 @@
"apiwarn-notfile": "«$1» не є файлом.",
"apiwarn-nothumb-noimagehandler": "Не вдалося створити мініатюру, оскільки $1 не має пов'язаного обробника зображень.",
"apiwarn-parse-nocontentmodel": "Не задано <var>title</var> або <var>contentmodel</var>, буде використано $1.",
+ "apiwarn-parse-revidwithouttext": "<var>revid</var> використано без <var>text</var>, та запитано синтаксично проаналізовані властивості сторінки. Можливо, Ви хотіли використати <var>oldid</var> замість <var>revid</var>?",
"apiwarn-parse-titlewithouttext": "<var>title</var> використано без <var>text</var>, і надіслано запит на оброблені властивості сторінки. Може \nВи хотіли використати <var>page</var> замість <var>title</var>?",
"apiwarn-redirectsandrevids": "Вирішення перенаправлень не може використовуватись разом з параметром <var>revids</var>. Усі перенаправлення, на які вказує <var>revids</var>, не було вирішено.",
"apiwarn-tokennotallowed": "Дія «$1» недозволена для поточного користувача.",
diff --git a/www/wiki/includes/api/i18n/zh-hans.json b/www/wiki/includes/api/i18n/zh-hans.json
index ef6fa601..fba891b6 100644
--- a/www/wiki/includes/api/i18n/zh-hans.json
+++ b/www/wiki/includes/api/i18n/zh-hans.json
@@ -22,10 +22,11 @@
"損齋",
"Myy730",
"D41D8CD98F",
- "Umherirrender"
+ "Umherirrender",
+ "NeverBehave"
]
},
- "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|文档]]\n* [[mw:Special:MyLanguage/API:FAQ|常见问题]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 邮件列表]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API公告]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R 程序错误与功能请求]\n</div>\n<strong>状态信息:</strong>本页所展示的所有特性都应正常工作,但是API仍在开发当中,将会随时变化。请订阅[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 邮件列表]以便获得更新通知。\n\n<strong>错误请求:</strong>当API收到错误请求时,HTTP header将会返回一个包含\"MediaWiki-API-Error\"的值,随后header的值与error code将会送回并设置为相同的值。详细信息请参阅[[mw:Special:MyLanguage/API:Errors_and_warnings|API:错误与警告]]。\n\n<strong>测试中:</strong>测试API请求的易用性,请参见[[Special:ApiSandbox]]。",
+ "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|文档]]\n* [[mw:Special:MyLanguage/API:FAQ|常见问题]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 邮件列表]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API公告]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R 程序错误与功能请求]\n</div>\n<strong>状态信息:</strong>MediaWiki API是一个成熟稳定的,不断受到支持和改进的界面。尽管我们尽力避免,但偶尔也需要作出重大更新;请订阅[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 邮件列表]以便获得更新通知。\n\n<strong>错误请求:</strong>当API收到错误请求时,HTTP header将会返回一个包含\"MediaWiki-API-Error\"的值,随后header的值与error code将会送回并设置为相同的值。详细信息请参阅[[mw:Special:MyLanguage/API:Errors_and_warnings|API:错误与警告]]。\n\n<p class=\"mw-apisandbox-link\"><strong>测试中:</strong>测试API请求的易用性,请参见[[Special:ApiSandbox]]。</p>",
"apihelp-main-param-action": "要执行的操作。",
"apihelp-main-param-format": "输出的格式。",
"apihelp-main-param-maxlag": "最大延迟可被用于MediaWiki安装于数据库复制集中。要保存导致更多网站复制延迟的操作,此参数可使客户端等待直到复制延迟少于指定值时。万一发生过多延迟,错误代码<samp>maxlag</samp>会返回消息,例如<samp>等待$host中:延迟$lag秒</samp>。<br />参见[[mw:Special:MyLanguage/Manual:Maxlag_parameter|手册:Maxlag参数]]以获取更多信息。",
@@ -76,6 +77,7 @@
"apihelp-compare-param-fromid": "要比较的第一个页面 ID。",
"apihelp-compare-param-fromrev": "要比较的第一个修订版本。",
"apihelp-compare-param-fromtext": "使用该文本而不是由<var>fromtitle</var>、<var>fromid</var>或<var>fromrev</var>指定的修订版本内容。",
+ "apihelp-compare-param-fromsection": "只使用指定“from”内容的指定章节。",
"apihelp-compare-param-frompst": "在<var>fromtext</var>执行预保存转变。",
"apihelp-compare-param-fromcontentmodel": "<var>fromtext</var>的内容模型。如果未指定,这将基于其他参数猜想。",
"apihelp-compare-param-fromcontentformat": "<var>fromtext</var>的内容序列化格式。",
@@ -84,6 +86,7 @@
"apihelp-compare-param-torev": "要比较的第二个修订版本。",
"apihelp-compare-param-torelative": "使用与定义自<var>fromtitle</var>、<var>fromid</var>或<var>fromrev</var>的修订版本相关的修订版本。所有其他“to”的选项将被忽略。",
"apihelp-compare-param-totext": "使用该文本而不是由<var>totitle</var>、<var>toid</var>或<var>torev</var>指定的修订版本内容。",
+ "apihelp-compare-param-tosection": "只使用指定“to”内容的指定章节。",
"apihelp-compare-param-topst": "在<var>totext</var>执行预保存转换。",
"apihelp-compare-param-tocontentmodel": "<var>totext</var>的内容模型。如果未指定,这将基于其他参数猜想。",
"apihelp-compare-param-tocontentformat": "<var>totext</var>的内容序列化格式。",
@@ -212,8 +215,6 @@
"apihelp-feedrecentchanges-param-tagfilter": "按标签过滤。",
"apihelp-feedrecentchanges-param-target": "仅仅显示从该页面链出的那些页面的变更。",
"apihelp-feedrecentchanges-param-showlinkedto": "仅仅显示链入到该页面的那些页面的变更。",
- "apihelp-feedrecentchanges-param-categories": "只显示所有这些分类中的页面上的更改。",
- "apihelp-feedrecentchanges-param-categories_any": "只显示这些分类以外页面的更改。",
"apihelp-feedrecentchanges-example-simple": "显示最近更改。",
"apihelp-feedrecentchanges-example-30days": "显示最近30天的更改。",
"apihelp-feedwatchlist-summary": "返回监视列表纲要。",
@@ -248,6 +249,8 @@
"apihelp-import-extended-description": "注意当发送用于<var>xml</var>参数的文件时,HTTP POST必须作为文件上传完成(即使用multipart/form-data)",
"apihelp-import-param-summary": "日志记录导入摘要。",
"apihelp-import-param-xml": "上传的XML文件。",
+ "apihelp-import-param-interwikiprefix": "对于上传导入:要应用到位置用户名的跨wiki前缀(如果设置了<var>$1assignknownusers</var>的话,则也包含已知用户)。",
+ "apihelp-import-param-assignknownusers": "分配编辑至本地用户,只要命名用户存在于本地。",
"apihelp-import-param-interwikisource": "用于跨wiki导入:导入的来源wiki。",
"apihelp-import-param-interwikipage": "用于跨wiki导入:导入的页面。",
"apihelp-import-param-fullhistory": "用于跨wiki导入:完整导入历史,而不只是最新版本。",
@@ -302,10 +305,10 @@
"apihelp-move-param-ignorewarnings": "忽略任何警告。",
"apihelp-move-param-tags": "要在移动日志,以及在目标页面的空修订版本中应用到实体的更改标签。",
"apihelp-move-example-move": "移动<kbd>Badtitle</kbd>到<kbd>Goodtitle</kbd>,不保留重定向。",
- "apihelp-opensearch-summary": "使用OpenSearch协议搜索wiki。",
+ "apihelp-opensearch-summary": "使用开放搜索协议搜索wiki。",
"apihelp-opensearch-param-search": "搜索字符串。",
"apihelp-opensearch-param-limit": "要返回的结果最大数。",
- "apihelp-opensearch-param-namespace": "搜索的名字空间。",
+ "apihelp-opensearch-param-namespace": "搜索的名字空间。如果<var>$1search</var>以有效名字空间前缀开头则忽略。",
"apihelp-opensearch-param-suggest": "如果<var>[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var>设置为false则不做任何事情。",
"apihelp-opensearch-param-redirects": "如何处理重定向:\n;return:返回重定向本身。\n;resolve:返回目标页面。可能返回少于$1limit个结果。\n由于历史原因,$1format=json默认为\"return\",其他格式默认为\"resolve\"。",
"apihelp-opensearch-param-format": "输出格式。",
@@ -331,9 +334,10 @@
"apihelp-paraminfo-example-1": "显示<kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>、<kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>、<kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd>和<kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>的信息。",
"apihelp-paraminfo-example-2": "显示<kbd>[[Special:ApiHelp/query|action=query]]</kbd>的所有子模块的信息。",
"apihelp-parse-summary": "解析内容并返回解析器输出。",
- "apihelp-parse-extended-description": "参见<kbd>[[Special:ApiHelp/query|action=query]]</kbd>的各种prop-module以从页面的当前版本获得信息。\n\n这里有几种方法可以指定解析的文本:\n# 指定一个页面或修订,使用<var>$1page</var>、<var>$1pageid</var>或<var>$1oldid</var>。\n# 明确指定内容,使用<var>$1text</var>、<var>$1title</var>和<var>$1contentmodel</var>。\n# 只指定一段摘要解析。<var>$1prop</var>应提供一个空值。",
+ "apihelp-parse-extended-description": "参见<kbd>[[Special:ApiHelp/query|action=query]]</kbd>的各种prop-module以从页面的当前版本获得信息。\n\n这里有几种方法可以指定解析的文本:\n# 指定一个页面或修订,使用<var>$1page</var>、<var>$1pageid</var>或<var>$1oldid</var>。\n# 明确指定内容,使用<var>$1text</var>、<var>$1title</var>、<var>$1revid</var>和<var>$1contentmodel</var>。\n# 只指定一段摘要解析。<var>$1prop</var>应提供一个空值。",
"apihelp-parse-param-title": "文本属于的页面标题。如果省略,<var>$1contentmodel</var>就必须被指定,且[[API]]将作为标题使用。",
"apihelp-parse-param-text": "要解析的文本。使用<var>$1title</var>或<var>$1contentmodel</var>以控制内容模型。",
+ "apihelp-parse-param-revid": "修订版本ID,用于<code><nowiki>{{REVISIONID}}</nowiki></code>和类似变体。",
"apihelp-parse-param-summary": "要解析的摘要。",
"apihelp-parse-param-page": "解析此页的内容。不能与<var>$1text</var>和<var>$1title</var>一起使用。",
"apihelp-parse-param-pageid": "解析此页的内容。覆盖<var>$1page</var>。",
@@ -374,6 +378,7 @@
"apihelp-parse-param-disablepp": "请改用<var>$1disablelimitreport</var>。",
"apihelp-parse-param-disableeditsection": "从解析器输出中省略编辑段落链接。",
"apihelp-parse-param-disabletidy": "不要在解析器输出中运行HTML清理(例如tidy)。",
+ "apihelp-parse-param-disablestylededuplication": "不要在解析器输出中删除重复的行内样式表。",
"apihelp-parse-param-generatexml": "生成XML解析树(需要内容模型<code>$1</code>;被<kbd>$2prop=parsetree</kbd>所取代)。",
"apihelp-parse-param-preview": "在预览模式下解析。",
"apihelp-parse-param-sectionpreview": "在段落预览模式下解析(同时要启用预览模式)。",
@@ -743,7 +748,7 @@
"apihelp-query+exturlusage-param-namespace": "要列举的页面名字空间。",
"apihelp-query+exturlusage-param-limit": "返回多少页面。",
"apihelp-query+exturlusage-param-expandurl": "用标准协议展开协议相关URL。",
- "apihelp-query+exturlusage-example-simple": "显示链接至<kbd>http://www.mediawiki.org</kbd>的页面。",
+ "apihelp-query+exturlusage-example-simple": "显示链接至<kbd>https://www.mediawiki.org</kbd>的页面。",
"apihelp-query+filearchive-summary": "循序列举所有被删除的文件。",
"apihelp-query+filearchive-param-from": "枚举的起始图片标题。",
"apihelp-query+filearchive-param-to": "枚举的结束图片标题。",
@@ -844,6 +849,7 @@
"apihelp-query+info-paramvalue-prop-readable": "用户是否可以阅读此页面。",
"apihelp-query+info-paramvalue-prop-preload": "提供由EditFormPreloadText返回的文本。",
"apihelp-query+info-paramvalue-prop-displaytitle": "在页面标题实际显示的地方提供方式。",
+ "apihelp-query+info-paramvalue-prop-varianttitles": "提供网站内容语言所有变体的显示标题。",
"apihelp-query+info-param-testactions": "测试当前用户是否可以在页面上执行某种操作。",
"apihelp-query+info-param-token": "请改用[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]。",
"apihelp-query+info-example-simple": "获取有关页面<kbd>Main Page</kbd>的信息。",
@@ -951,7 +957,7 @@
"apihelp-query+prefixsearch-summary": "为页面标题执行前缀搜索。",
"apihelp-query+prefixsearch-extended-description": "尽管名称类似,但此模块不等于[[Special:PrefixIndex]];详见<kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd>中的<kbd>apprefix</kbd>参数。此模块的目的类似<kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd>:基于用户的输入提供最佳匹配的标题。取决于搜索引擎后端,这可能包括错拼纠正、避免重定向和其他启发性行为。",
"apihelp-query+prefixsearch-param-search": "搜索字符串。",
- "apihelp-query+prefixsearch-param-namespace": "搜索的名字空间。",
+ "apihelp-query+prefixsearch-param-namespace": "搜索的名字空间。如果<var>$1search</var>以有效名字空间前缀开头则忽略。",
"apihelp-query+prefixsearch-param-limit": "要返回的结果最大数。",
"apihelp-query+prefixsearch-param-offset": "跳过的结果数。",
"apihelp-query+prefixsearch-example-simple": "搜索以<kbd>meaning</kbd>开头的页面标题。",
@@ -1003,6 +1009,7 @@
"apihelp-query+recentchanges-paramvalue-prop-sizes": "添加新旧页面长度(字节)。",
"apihelp-query+recentchanges-paramvalue-prop-redirect": "如果页面是重定向的话,标记编辑。",
"apihelp-query+recentchanges-paramvalue-prop-patrolled": "将可巡查编辑标记为已巡查或未巡查。",
+ "apihelp-query+recentchanges-paramvalue-prop-autopatrolled": "将可巡查编辑标记为自动巡查或未巡查。",
"apihelp-query+recentchanges-paramvalue-prop-loginfo": "添加日志信息(日志ID、日志类型等)至日志记录。",
"apihelp-query+recentchanges-paramvalue-prop-tags": "列举条目的标签。",
"apihelp-query+recentchanges-paramvalue-prop-sha1": "为与某一修订版本有关的记录添加内容校验和。",
@@ -1082,6 +1089,7 @@
"apihelp-query+search-paramvalue-prop-sectiontitle": "添加匹配章节的标题。",
"apihelp-query+search-paramvalue-prop-categorysnippet": "添加已解析的匹配分类片段。",
"apihelp-query+search-paramvalue-prop-isfilematch": "添加布尔值,表明搜索是否匹配文件内容。",
+ "apihelp-query+search-paramvalue-prop-extensiondata": "添加由扩展生成的额外数据。",
"apihelp-query+search-paramvalue-prop-score": "已忽略。",
"apihelp-query+search-paramvalue-prop-hasrelated": "已忽略。",
"apihelp-query+search-param-limit": "返回的总计页面数。",
@@ -1180,6 +1188,7 @@
"apihelp-query+usercontribs-paramvalue-prop-sizediff": "添加与父编辑相比该编辑的大小变化。",
"apihelp-query+usercontribs-paramvalue-prop-flags": "添加编辑标记。",
"apihelp-query+usercontribs-paramvalue-prop-patrolled": "标记已巡查编辑。",
+ "apihelp-query+usercontribs-paramvalue-prop-autopatrolled": "编辑自动巡查编辑。",
"apihelp-query+usercontribs-paramvalue-prop-tags": "列举用于编辑的标签。",
"apihelp-query+usercontribs-param-show": "只显示符合这些标准的项目,例如只显示不是小编辑的编辑:<kbd>$2show=!minor</kbd>。\n\n如果<kbd>$2show=patrolled</kbd>或<kbd>$2show=!patrolled</kbd>被设定,早于<var>[[mw:Special:MyLanguage/Manual:$wgRCMaxAge|$wgRCMaxAge]]</var>($1秒)的修订不会被显示。",
"apihelp-query+usercontribs-param-tag": "只列出被此标签标记的修订。",
@@ -1230,7 +1239,7 @@
"apihelp-query+watchlist-param-allrev": "将同一页面的多个修订包含于指定的时间表内。",
"apihelp-query+watchlist-param-start": "枚举的起始时间戳。",
"apihelp-query+watchlist-param-end": "枚举的结束时间戳。",
- "apihelp-query+watchlist-param-namespace": "过滤更改为仅限指定的名字空间。",
+ "apihelp-query+watchlist-param-namespace": "过滤更改为仅限指定名字空间。",
"apihelp-query+watchlist-param-user": "只列出此用户的更改。",
"apihelp-query+watchlist-param-excludeuser": "不要列出此用户的更改。",
"apihelp-query+watchlist-param-limit": "根据结果返回的结果总数。",
@@ -1244,9 +1253,11 @@
"apihelp-query+watchlist-paramvalue-prop-parsedcomment": "添加解析过的编辑摘要。",
"apihelp-query+watchlist-paramvalue-prop-timestamp": "添加编辑时间戳。",
"apihelp-query+watchlist-paramvalue-prop-patrol": "将编辑标记为已巡查。",
+ "apihelp-query+watchlist-paramvalue-prop-autopatrol": "将编辑标记为自动巡查。",
"apihelp-query+watchlist-paramvalue-prop-sizes": "添加页面的旧有长度和新长度。",
"apihelp-query+watchlist-paramvalue-prop-notificationtimestamp": "添加最近被通知有关编辑的用户的时间戳。",
"apihelp-query+watchlist-paramvalue-prop-loginfo": "在适当位置添加日志信息。",
+ "apihelp-query+watchlist-paramvalue-prop-tags": "列举条目的标签。",
"apihelp-query+watchlist-param-show": "只显示满足这些标准的项目。例如,要只查看由登录用户做出的小编辑,设置$1show=minor|!anon。",
"apihelp-query+watchlist-param-type": "要显示的更改类型:",
"apihelp-query+watchlist-paramvalue-type-edit": "普通页面编辑。",
@@ -1497,6 +1508,8 @@
"api-help-param-direction": "列举的方向:\n;newer:最早的优先。注意:$1start应早于$1end。\n;older:最新的优先(默认)。注意:$1start应晚于$1end。",
"api-help-param-continue": "当更多结果可用时,使用这个继续。",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(没有说明)</span>",
+ "api-help-param-maxbytes": "不能超过$1{{PLURAL:$1|字节}}。",
+ "api-help-param-maxchars": "不能超过$1个{{PLURAL:$1|字符}}。",
"api-help-examples": "{{PLURAL:$1|例子}}:",
"api-help-permissions": "{{PLURAL:$1|权限}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|授予}}:$2",
@@ -1559,6 +1572,8 @@
"apierror-chunk-too-small": "对于非最终块,最小块大小为$1{{PLURAL:$1|字节}}。",
"apierror-cidrtoobroad": "比/$2更宽的$1 CIDR地址段不被接受。",
"apierror-compare-no-title": "不能在没有标题的情况下预保存转换。尝试指定<var>fromtitle</var>或<var>totitle</var>。",
+ "apierror-compare-nosuchfromsection": "在“from”内容中没有章节$1。",
+ "apierror-compare-nosuchtosection": "在“to”内容中没有章节$1。",
"apierror-compare-relative-to-nothing": "没有与<var>torelative</var>的“from”修订版本相对的版本。",
"apierror-contentserializationexception": "内容序列化失败:$1",
"apierror-contenttoobig": "您提供的内容超过了$1{{PLURAL:$1|千字节}}的条目大小限制。",
@@ -1599,6 +1614,8 @@
"apierror-invalidurlparam": "<var>$1urlparam</var>的值无效(<kbd>$2=$3</kbd>)。",
"apierror-invaliduser": "无效用户名“$1”。",
"apierror-invaliduserid": "用户ID<var>$1</var>无效。",
+ "apierror-maxbytes": "参数<var>$1</var>不能超过$2{{PLURAL:$2|字节}}",
+ "apierror-maxchars": "参数<var>$1</var>不能超过$2个{{PLURAL:$2|字符}}",
"apierror-maxlag-generic": "正在等待数据库服务器:已延迟$1{{PLURAL:$1|秒}}。",
"apierror-maxlag": "正在等待$2:已延迟$1{{PLURAL:$1|秒}}。",
"apierror-mimesearchdisabled": "MIME搜索在Miser模式中被禁用。",
@@ -1614,7 +1631,6 @@
"apierror-missingtitle-byname": "页面$1不存在。",
"apierror-moduledisabled": "<kbd>$1</kbd>模块已被禁用。",
"apierror-multival-only-one-of": "参数<var>$1</var>只允许$2{{PLURAL:$3||之一}}。",
- "apierror-multival-only-one": "参数<var>$1</var>只允许一个值。",
"apierror-multpages": "<var>$1</var>只可以在单一页面使用。",
"apierror-mustbeloggedin-changeauth": "您必须登录以更改身份验证数据。",
"apierror-mustbeloggedin-generic": "您必须登录。",
@@ -1735,6 +1751,7 @@
"apiwarn-notfile": "“$1”不是文件。",
"apiwarn-nothumb-noimagehandler": "不能创建缩略图,因为$1没有关联的图片处理器。",
"apiwarn-parse-nocontentmodel": "<var>title</var>或<var>contentmodel</var>未提供,假设$1。",
+ "apiwarn-parse-revidwithouttext": "<var>revid</var>在没有<var>text</var>的情况下被使用,并且请求了已解析的页面属性。您是想用<var>oldid</var>而不是<var>revid</var>么?",
"apiwarn-parse-titlewithouttext": "<var>title</var>在没有<var>text</var>的情况下被使用,并且请求了已解析页面的属性。您是想用<var>page</var>而不是<var>title</var>么?",
"apiwarn-redirectsandrevids": "重定向解决方案不能与<var>revids</var>参数一起使用。任何<var>revids</var>所指向的重定向都未被解决。",
"apiwarn-tokennotallowed": "操作“$1”不允许当前用户使用。",
diff --git a/www/wiki/includes/api/i18n/zh-hant.json b/www/wiki/includes/api/i18n/zh-hant.json
index 0767b3ea..b61d201b 100644
--- a/www/wiki/includes/api/i18n/zh-hant.json
+++ b/www/wiki/includes/api/i18n/zh-hant.json
@@ -13,7 +13,9 @@
"烈羽",
"Corainn",
"A2093064",
- "Wwycheuk"
+ "Wwycheuk",
+ "Wbxshiori",
+ "Sanmosa"
]
},
"apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|說明文件]]\n* [[mw:Special:MyLanguage/API:FAQ|常見問題]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 郵遞清單]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API公告]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R 報告錯誤及請求功能]\n</div>\n<strong>狀態資訊:</strong>本頁所展示的所有功能都應正常運作,但API仍在開發,會隨時變化。請訂閱[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 郵遞清單]以便獲得更新通知。\n\n<strong>錯誤的請求:</strong>當API收到錯誤的請求,會發出以「MediaWiki-API-Error」為鍵的HTTP標頭欄位,隨後標頭欄位的值,以及傳回的錯誤碼會設為相同值。詳細資訊請參閱[[mw:Special:MyLanguage/API:Errors_and_warnings|API: 錯誤與警告]]。\n\n<strong>測試:</strong>要簡化API請求的測試過程,請見[[Special:ApiSandbox]]。",
@@ -27,7 +29,7 @@
"apihelp-main-param-servedby": "在結果中包括提出請求的主機名。",
"apihelp-main-param-curtimestamp": "在結果中包括目前的時間戳。",
"apihelp-main-param-responselanginfo": "在結果中包括<var>uselang</var>和<var>errorlang</var>所用的語言。",
- "apihelp-block-summary": "封鎖使用者。",
+ "apihelp-block-summary": "封鎖用戶。",
"apihelp-block-param-user": "要封鎖的使用者名稱、IP 位址或 IP 範圍。不能與 <var>$1userid</var> 一起使用",
"apihelp-block-param-reason": "封鎖原因。",
"apihelp-block-param-anononly": "僅封鎖匿名使用者 (禁止這個 IP 位址的匿名使用者編輯)。",
@@ -58,7 +60,7 @@
"apihelp-compare-param-torev": "要比對的第二個修訂。",
"apihelp-compare-example-1": "建立修訂 1 與 1 的差異檔",
"apihelp-createaccount-summary": "建立新使用者帳號。",
- "apihelp-createaccount-param-name": "使用者名稱。",
+ "apihelp-createaccount-param-name": "用戶名。",
"apihelp-createaccount-param-password": "密碼 (若有設定 <var>$1mailpassword</var> 則可略過)。",
"apihelp-createaccount-param-domain": "外部身分核對使用的網域 (可有可無)。",
"apihelp-createaccount-param-token": "在第一次請求時已取得的帳號建立金鑰。",
@@ -138,7 +140,7 @@
"apihelp-import-param-namespace": "匯入至此命名空間。無法與 <var>$1rootpage</var> 一起使用。",
"apihelp-import-param-rootpage": "匯入作為此頁面的子頁面。無法與 <var>$1namespace</var> 一起使用。",
"apihelp-login-summary": "登入並取得身分核對 cookies",
- "apihelp-login-param-name": "使用者名稱。",
+ "apihelp-login-param-name": "用戶名。",
"apihelp-login-param-password": "密碼。",
"apihelp-login-param-domain": "網域名稱(可有可無)。",
"apihelp-login-example-login": "登入",
@@ -185,6 +187,13 @@
"apihelp-protect-param-protections": "保護層級清單,格式為 <kbd>action=level</kbd> (例如 <kbd>edit=sysop</kbd>)。<kbd>all</kbd> 層級代表所有人都可以進行行動,亦即無限制。\n\n<strong>注意:</strong>未列入清單項目的限制皆會移除。",
"apihelp-protect-param-expiry": "期限時間戳記,若只設定一個時間戳記,該時間戳記將會套用至所有的保護層級。 使用 <kbd>infinite</kbd>、<kbd>indefinite</kbd>、<kbd>infinity</kbd> 或 <kbd>never</kbd> 來設定保護層級期限為永遠。",
"apihelp-protect-param-reason": "(解除)保護的原因。",
+ "apihelp-protect-param-tags": "修改標籤以套用於保護日誌裡的項目。",
+ "apihelp-protect-param-cascade": "啟用連鎖保護(也就是保護包含於此頁面的頁面)。如果所有提供的保護等級不支援連鎖,就將其忽略。",
+ "apihelp-protect-param-watch": "如果被設定,就將被(解除)保護的頁面加至目前使用者的監視列表。",
+ "apihelp-protect-param-watchlist": "無條件地將該頁面加入至或移除自目前使用者的監視列表、使用偏好設定或不更改監視。",
+ "apihelp-protect-example-protect": "保護一個頁面。",
+ "apihelp-purge-summary": "為指定標題清除快取。",
+ "apihelp-purge-example-generator": "重新整理主要命名空間的前10個頁面。",
"apihelp-query-summary": "擷取來自及有關MediaWiki的數據。",
"apihelp-query+allcategories-param-limit": "要回傳的分類數量。",
"apihelp-query+allfileusages-param-limit": "要回傳的項目總數。",
@@ -260,8 +269,8 @@
"apihelp-unblock-example-id": "解除封銷 ID #<kbd>105</kbd>。",
"apihelp-undelete-param-reason": "還原的原因。",
"apihelp-userrights-summary": "更改一位使用者的群組成員。",
- "apihelp-userrights-param-user": "使用者名稱。",
- "apihelp-userrights-param-userid": "使用者 ID。",
+ "apihelp-userrights-param-user": "用戶名。",
+ "apihelp-userrights-param-userid": "用戶ID。",
"apihelp-userrights-param-add": "加入使用者至這些群組;若已是成員,則更新失效時間。",
"apihelp-userrights-param-remove": "從這些群組移除使用者。",
"apihelp-userrights-param-reason": "變更的原因。",
@@ -275,6 +284,7 @@
"apihelp-xml-summary": "使用 XML 格式輸出資料。",
"apihelp-xmlfm-summary": "使用 XML 格式輸出資料 (使用 HTML 格式顯示)。",
"api-format-title": "MediaWiki API 結果",
+ "api-format-prettyprint-header": "這是$1格式的HTML呈現。HTML適合用於除錯,但不適合應用程式使用。\n\n指定<var>format</var>參數以更改輸出格式。要檢視$1格式的非HTML呈現,設定<kbd>format=$2</kbd>。\n\n參考 [[mw:Special:MyLanguage/API|完整說明文件]] 或 [[Special:ApiHelp/main|API說明]] 以取得更多資訊。",
"api-pageset-param-titles": "要使用的標題清單。",
"api-pageset-param-pageids": "要使用的頁面 ID 清單。",
"api-pageset-param-revids": "要使用的修訂 ID 清單。",
@@ -308,8 +318,10 @@
"api-help-authmanager-general-usage": "使用此模組的一般程式是:\n# 通過<kbd>amirequestsfor=$4</kbd>取得來自<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>的可用欄位,和來自<kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>的<kbd>$5</kbd>令牌。\n# 向用戶顯示欄位,並獲得其提交的內容。\n# 提交(POST)至此模組,提供<var>$1returnurl</var>及任何相關欄位。\n# 在回应中檢查<samp>status</samp>。\n#* 如果您收到了<samp>PASS</samp>(成功)或<samp>FAIL</samp>(失敗),則認為操作結束。成功與否如上句所示。\n#* 如果您收到了<samp>UI</samp>,向用戶顯示新欄位,並再次獲取其提交的內容。然後再次使用<var>$1continue</var>,向本模組提交相關欄位,並重復第四步。\n#* 如果您收到了<samp>REDIRECT</samp>,將使用者指向<samp>redirecttarget</samp>中的目標,等待其返回<var>$1returnurl</var>。然後再次使用<var>$1continue</var>,向本模組提交返回URL中提供的一切欄位,並重復第四步。\n#* 如果您收到了<samp>RESTART</samp>,這意味著身份驗證正常運作,但我們沒有連結的使用者賬戶。您可以將此看做<samp>UI</samp>或<samp>FAIL</samp>。",
"apierror-mustbeloggedin-changeauth": "必須登入,才能變更身分核對資取。",
"apierror-mustbeloggedin-removeauth": "必須登入,才能移除身分核對資取。",
+ "apierror-permissiondenied": "您沒有權限$1。",
"apierror-reauthenticate": "於本工作階段還未核對身分,請重新核對。",
"apierror-timeout": "伺服器未有在預計的時間內回應。",
+ "api-feed-error-title": "錯誤($1)",
"api-credits-header": "製作群",
"api-credits": "API 開發人員:\n* Roan Kattouw (首席開發者 Sep 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (創立者,首席開發者 Sep 2006–Sep 2007)\n* Brad Jorsch (首席開發者 2013–present)\n\n請傳送您的評論、建議以及問題至 mediawiki-api@lists.wikimedia.org\n或者回報問題至 https://phabricator.wikimedia.org/。"
}
diff --git a/www/wiki/includes/auth/AuthManager.php b/www/wiki/includes/auth/AuthManager.php
index 9407c422..611a8cdc 100644
--- a/www/wiki/includes/auth/AuthManager.php
+++ b/www/wiki/includes/auth/AuthManager.php
@@ -31,6 +31,7 @@ use Status;
use StatusValue;
use User;
use WebRequest;
+use Wikimedia\ObjectFactory;
/**
* This serves as the entry point to the authentication system.
@@ -680,8 +681,9 @@ class AuthManager implements LoggerAwareInterface {
// Step 4: Authentication complete! Set the user in the session and
// clean up.
- $this->logger->info( 'Login for {user} succeeded', [
+ $this->logger->info( 'Login for {user} succeeded from {clientip}', [
'user' => $user->getName(),
+ 'clientip' => $this->request->getIP(),
] );
/** @var RememberMeAuthenticationRequest $req */
$req = AuthenticationRequest::getRequestByClass(
@@ -1415,7 +1417,7 @@ class AuthManager implements LoggerAwareInterface {
$state['userid'] = $user->getId();
// Update user count
- \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
+ \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
// Watch user's userpage and talk page
$user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
@@ -1551,7 +1553,10 @@ class AuthManager implements LoggerAwareInterface {
// Fetch the user ID from the master, so that we don't try to create the user
// when they already exist, due to replication lag
// @codeCoverageIgnoreStart
- if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
+ if (
+ !$localId &&
+ MediaWikiServices::getInstance()->getDBLoadBalancer()->getReaderIndex() != 0
+ ) {
$localId = User::idFromName( $username, User::READ_LATEST );
$flags = User::READ_LATEST;
}
@@ -1726,7 +1731,7 @@ class AuthManager implements LoggerAwareInterface {
$user->saveSettings();
// Update user count
- \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
+ \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
// Watch user's userpage and talk page
\DeferredUpdates::addCallableUpdate( function () use ( $user ) {
$user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
@@ -2289,7 +2294,7 @@ class AuthManager implements LoggerAwareInterface {
$ret = [];
foreach ( $specs as $spec ) {
- $provider = \ObjectFactory::getObjectFromSpec( $spec );
+ $provider = ObjectFactory::getObjectFromSpec( $spec );
if ( !$provider instanceof $class ) {
throw new \RuntimeException(
"Expected instance of $class, got " . get_class( $provider )
diff --git a/www/wiki/includes/auth/AuthManagerAuthPlugin.php b/www/wiki/includes/auth/AuthManagerAuthPlugin.php
index 88458582..4f84b4c6 100644
--- a/www/wiki/includes/auth/AuthManagerAuthPlugin.php
+++ b/www/wiki/includes/auth/AuthManagerAuthPlugin.php
@@ -20,6 +20,7 @@
namespace MediaWiki\Auth;
+use Psr\Log\LoggerInterface;
use User;
/**
@@ -31,7 +32,7 @@ class AuthManagerAuthPlugin extends \AuthPlugin {
/** @var string|null */
protected $domain = null;
- /** @var \\Psr\\Log\\LoggerInterface */
+ /** @var LoggerInterface */
protected $logger = null;
public function __construct() {
@@ -152,8 +153,9 @@ class AuthManagerAuthPlugin extends \AuthPlugin {
}
public function updateExternalDBGroups( $user, $addgroups, $delgroups = [] ) {
- \Hooks::run( 'UserGroupsChanged', [ $user, $addgroups, $delgroups ] );
- return true;
+ throw new \BadMethodCallException(
+ 'Update of user groups via AuthPlugin is not supported with AuthManager.'
+ );
}
public function canCreateAccounts() {
diff --git a/www/wiki/includes/auth/AuthPluginPrimaryAuthenticationProvider.php b/www/wiki/includes/auth/AuthPluginPrimaryAuthenticationProvider.php
index b8e36bc4..cd0734d8 100644
--- a/www/wiki/includes/auth/AuthPluginPrimaryAuthenticationProvider.php
+++ b/www/wiki/includes/auth/AuthPluginPrimaryAuthenticationProvider.php
@@ -272,7 +272,7 @@ class AuthPluginPrimaryAuthenticationProvider
if ( $failed ) {
throw new \UnexpectedValueException(
"AuthPlugin failed to reset password for $username in the following domains: "
- . join( ' ', $failed )
+ . implode( ' ', $failed )
);
}
}
diff --git a/www/wiki/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php b/www/wiki/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php
index f7a7ec19..7488fbaa 100644
--- a/www/wiki/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php
+++ b/www/wiki/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php
@@ -77,9 +77,19 @@ class CheckBlocksSecondaryAuthenticationProvider extends AbstractSecondaryAuthen
public function testUserForCreation( $user, $autocreate, array $options = [] ) {
$block = $user->isBlockedFromCreateAccount();
if ( $block ) {
+ if ( $block->mReason ) {
+ $reason = $block->mReason;
+ } else {
+ $msg = \Message::newFromKey( 'blockednoreason' );
+ if ( !\RequestContext::getMain()->getUser()->isSafeToLoad() ) {
+ $msg->inContentLanguage();
+ }
+ $reason = $msg->text();
+ }
+
$errorParams = [
$block->getTarget(),
- $block->mReason ?: \Message::newFromKey( 'blockednoreason' )->text(),
+ $reason,
$block->getByName()
];
diff --git a/www/wiki/includes/auth/LegacyHookPreAuthenticationProvider.php b/www/wiki/includes/auth/LegacyHookPreAuthenticationProvider.php
index cab6e32d..95fe3ab8 100644
--- a/www/wiki/includes/auth/LegacyHookPreAuthenticationProvider.php
+++ b/www/wiki/includes/auth/LegacyHookPreAuthenticationProvider.php
@@ -58,14 +58,14 @@ class LegacyHookPreAuthenticationProvider extends AbstractPreAuthenticationProvi
$msg = null;
if ( !\Hooks::run( 'LoginUserMigrated', [ $user, &$msg ] ) ) {
return $this->makeFailResponse(
- $user, null, LoginForm::USER_MIGRATED, $msg, 'LoginUserMigrated'
+ $user, LoginForm::USER_MIGRATED, $msg, 'LoginUserMigrated'
);
}
$abort = LoginForm::ABORTED;
$msg = null;
if ( !\Hooks::run( 'AbortLogin', [ $user, $password, &$abort, &$msg ] ) ) {
- return $this->makeFailResponse( $user, null, $abort, $msg, 'AbortLogin' );
+ return $this->makeFailResponse( $user, $abort, $msg, 'AbortLogin' );
}
return StatusValue::newGood();
@@ -103,7 +103,7 @@ class LegacyHookPreAuthenticationProvider extends AbstractPreAuthenticationProvi
// Hook point to add extra creation throttles and blocks
$this->logger->debug( __METHOD__ . ": a hook blocked auto-creation: $abortError\n" );
return $this->makeFailResponse(
- $user, $user, LoginForm::ABORTED, $abortError, 'AbortAutoAccount'
+ $user, LoginForm::ABORTED, $abortError, 'AbortAutoAccount'
);
}
}
@@ -114,13 +114,12 @@ class LegacyHookPreAuthenticationProvider extends AbstractPreAuthenticationProvi
/**
* Construct an appropriate failure response
* @param User $user
- * @param User|null $creator
- * @param int $constant LoginForm constant
- * @param string|null $msg Message
- * @param string $hook Hook
+ * @param int $constant One of the LoginForm::… constants
+ * @param string|null $msg Optional message key, will be derived from $constant otherwise
+ * @param string $hook Name of the hook for error logging and exception messages
* @return StatusValue
*/
- protected function makeFailResponse( $user, $creator, $constant, $msg, $hook ) {
+ private function makeFailResponse( User $user, $constant, $msg, $hook ) {
switch ( $constant ) {
case LoginForm::SUCCESS:
// WTF?
diff --git a/www/wiki/includes/cache/BacklinkCache.php b/www/wiki/includes/cache/BacklinkCache.php
index 4341daaf..48809d07 100644
--- a/www/wiki/includes/cache/BacklinkCache.php
+++ b/www/wiki/includes/cache/BacklinkCache.php
@@ -28,6 +28,7 @@
use Wikimedia\Rdbms\ResultWrapper;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\MediaWikiServices;
/**
* Class for fetching backlink lists, approximate backlink counts and
@@ -71,6 +72,11 @@ class BacklinkCache {
protected $fullResultCache = [];
/**
+ * @var WANObjectCache
+ */
+ protected $wanCache;
+
+ /**
* Local copy of a database object.
*
* Accessor: BacklinkCache::getDB()
@@ -93,6 +99,7 @@ class BacklinkCache {
*/
public function __construct( Title $title ) {
$this->title = $title;
+ $this->wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
}
/**
@@ -122,11 +129,12 @@ class BacklinkCache {
}
/**
- * Clear locally stored data and database object.
+ * Clear locally stored data and database object. Invalidate data in memcache.
*/
public function clear() {
$this->partitionCache = [];
$this->fullResultCache = [];
+ $this->wanCache->touchCheckKey( $this->makeCheckKey() );
unset( $this->db );
}
@@ -324,7 +332,6 @@ class BacklinkCache {
public function getNumLinks( $table, $max = INF ) {
global $wgUpdateRowsPerJob;
- $cache = ObjectCache::getMainWANInstance();
// 1) try partition cache ...
if ( isset( $this->partitionCache[$table] ) ) {
$entry = reset( $this->partitionCache[$table] );
@@ -337,15 +344,22 @@ class BacklinkCache {
return min( $max, $this->fullResultCache[$table]->numRows() );
}
- $memcKey = $cache->makeKey(
+ $memcKey = $this->wanCache->makeKey(
'numbacklinks',
md5( $this->title->getPrefixedDBkey() ),
$table
);
// 3) ... fallback to memcached ...
- $count = $cache->get( $memcKey );
- if ( $count ) {
+ $curTTL = INF;
+ $count = $this->wanCache->get(
+ $memcKey,
+ $curTTL,
+ [
+ $this->makeCheckKey()
+ ]
+ );
+ if ( $count && ( $curTTL > 0 ) ) {
return min( $max, $count );
}
@@ -359,7 +373,7 @@ class BacklinkCache {
// Fetch the full title info, since the caller will likely need it next
$count = $this->getLinks( $table, false, false, $max )->count();
if ( $count < $max ) { // full count
- $cache->set( $memcKey, $count, self::CACHE_EXPIRY );
+ $this->wanCache->set( $memcKey, $count, self::CACHE_EXPIRY );
}
}
@@ -383,7 +397,6 @@ class BacklinkCache {
return $this->partitionCache[$table][$batchSize]['batches'];
}
- $cache = ObjectCache::getMainWANInstance();
$this->partitionCache[$table][$batchSize] = false;
$cacheEntry =& $this->partitionCache[$table][$batchSize];
@@ -395,7 +408,7 @@ class BacklinkCache {
return $cacheEntry['batches'];
}
- $memcKey = $cache->makeKey(
+ $memcKey = $this->wanCache->makeKey(
'backlinks',
md5( $this->title->getPrefixedDBkey() ),
$table,
@@ -403,8 +416,15 @@ class BacklinkCache {
);
// 3) ... fallback to memcached ...
- $memcValue = $cache->get( $memcKey );
- if ( is_array( $memcValue ) ) {
+ $curTTL = 0;
+ $memcValue = $this->wanCache->get(
+ $memcKey,
+ $curTTL,
+ [
+ $this->makeCheckKey()
+ ]
+ );
+ if ( is_array( $memcValue ) && ( $curTTL > 0 ) ) {
$cacheEntry = $memcValue;
wfDebug( __METHOD__ . ": got from memcached $memcKey\n" );
@@ -435,15 +455,15 @@ class BacklinkCache {
}
// Save partitions to memcached
- $cache->set( $memcKey, $cacheEntry, self::CACHE_EXPIRY );
+ $this->wanCache->set( $memcKey, $cacheEntry, self::CACHE_EXPIRY );
// Save backlink count to memcached
- $memcKey = $cache->makeKey(
+ $memcKey = $this->wanCache->makeKey(
'numbacklinks',
md5( $this->title->getPrefixedDBkey() ),
$table
);
- $cache->set( $memcKey, $cacheEntry['numRows'], self::CACHE_EXPIRY );
+ $this->wanCache->set( $memcKey, $cacheEntry['numRows'], self::CACHE_EXPIRY );
wfDebug( __METHOD__ . ": got from database\n" );
@@ -543,4 +563,16 @@ class BacklinkCache {
return TitleArray::newFromResult(
new FakeResultWrapper( array_values( $mergedRes ) ) );
}
+
+ /**
+ * Returns check key for the backlinks cache for a particular title
+ *
+ * @return String
+ */
+ private function makeCheckKey() {
+ return $this->wanCache->makeKey(
+ 'backlinks',
+ md5( $this->title->getPrefixedDBkey() )
+ );
+ }
}
diff --git a/www/wiki/includes/cache/CacheDependency.php b/www/wiki/includes/cache/CacheDependency.php
index a59ba97d..4ff10047 100644
--- a/www/wiki/includes/cache/CacheDependency.php
+++ b/www/wiki/includes/cache/CacheDependency.php
@@ -34,7 +34,6 @@ class DependencyWrapper {
private $deps;
/**
- * Create an instance.
* @param mixed $value The user-supplied value
* @param CacheDependency|CacheDependency[] $deps A dependency or dependency
* array. All dependencies must be objects implementing CacheDependency.
@@ -99,7 +98,7 @@ class DependencyWrapper {
* it will be generated with the callback function (if present), and the newly
* calculated value will be stored to the cache in a wrapper.
*
- * @param BagOStuff $cache A cache object
+ * @param BagOStuff $cache
* @param string $key The cache key
* @param int $expiry The expiry timestamp or interval in seconds
* @param bool|callable $callback The callback for generating the value, or false
@@ -182,11 +181,11 @@ class FileDependency extends CacheDependency {
function loadDependencyValues() {
if ( is_null( $this->timestamp ) ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
# Dependency on a non-existent file stores "false"
# This is a valid concept!
$this->timestamp = filemtime( $this->filename );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
}
@@ -194,9 +193,9 @@ class FileDependency extends CacheDependency {
* @return bool
*/
function isExpired() {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$lastmod = filemtime( $this->filename );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $lastmod === false ) {
if ( $this->timestamp === false ) {
# Still nonexistent
diff --git a/www/wiki/includes/cache/CacheHelper.php b/www/wiki/includes/cache/CacheHelper.php
index 8c70be24..e77e2515 100644
--- a/www/wiki/includes/cache/CacheHelper.php
+++ b/www/wiki/includes/cache/CacheHelper.php
@@ -273,7 +273,7 @@ class CacheHelper implements ICacheHelper {
$itemKey = array_keys( array_slice( $this->cachedChunks, 0, 1 ) );
$itemKey = array_shift( $itemKey );
- if ( !is_integer( $itemKey ) ) {
+ if ( !is_int( $itemKey ) ) {
wfWarn( "Attempted to get item with non-numeric key while " .
"the next item in the queue has a key ($itemKey) in " . __METHOD__ );
} elseif ( is_null( $itemKey ) ) {
diff --git a/www/wiki/includes/cache/FileCacheBase.php b/www/wiki/includes/cache/FileCacheBase.php
index f2da08a3..ce5a019b 100644
--- a/www/wiki/includes/cache/FileCacheBase.php
+++ b/www/wiki/includes/cache/FileCacheBase.php
@@ -179,9 +179,9 @@ abstract class FileCacheBase {
* @return void
*/
public function clearCache() {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
unlink( $this->cachePath() );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
$this->mCached = false;
}
diff --git a/www/wiki/includes/cache/GenderCache.php b/www/wiki/includes/cache/GenderCache.php
index a34d2358..099a986f 100644
--- a/www/wiki/includes/cache/GenderCache.php
+++ b/www/wiki/includes/cache/GenderCache.php
@@ -56,7 +56,7 @@ class GenderCache {
/**
* Returns the gender for given username.
- * @param string|User $username Username
+ * @param string|User $username
* @param string $caller The calling method
* @return string
*/
diff --git a/www/wiki/includes/cache/MessageBlobStore.php b/www/wiki/includes/cache/MessageBlobStore.php
index b076a083..b262eab6 100644
--- a/www/wiki/includes/cache/MessageBlobStore.php
+++ b/www/wiki/includes/cache/MessageBlobStore.php
@@ -129,14 +129,6 @@ class MessageBlobStore implements LoggerAwareInterface {
}
/**
- * @deprecated since 1.27 Obsolete. Used to populate a cache table in the database.
- * @return bool
- */
- public function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) {
- return false;
- }
-
- /**
* @since 1.27
* @param ResourceLoaderModule $module
* @param string $lang
diff --git a/www/wiki/includes/cache/MessageCache.php b/www/wiki/includes/cache/MessageCache.php
index 768f980b..71fcd8bd 100644
--- a/www/wiki/includes/cache/MessageCache.php
+++ b/www/wiki/includes/cache/MessageCache.php
@@ -156,22 +156,22 @@ class MessageCache {
}
/**
- * @param WANObjectCache $wanCache WAN cache instance
- * @param BagOStuff $clusterCache Cluster cache instance
- * @param BagOStuff $srvCache Server cache instance
+ * @param WANObjectCache $wanCache
+ * @param BagOStuff $clusterCache
+ * @param BagOStuff $serverCache
* @param bool $useDB Whether to look for message overrides (e.g. MediaWiki: pages)
* @param int $expiry Lifetime for cache. @see $mExpiry.
*/
public function __construct(
WANObjectCache $wanCache,
BagOStuff $clusterCache,
- BagOStuff $srvCache,
+ BagOStuff $serverCache,
$useDB,
$expiry
) {
$this->wanCache = $wanCache;
$this->clusterCache = $clusterCache;
- $this->srvCache = $srvCache;
+ $this->srvCache = $serverCache;
$this->mDisable = !$useDB;
$this->mExpiry = $expiry;
@@ -191,23 +191,15 @@ class MessageCache {
// ParserOptions for it. And don't cache this ParserOptions
// either.
$po = ParserOptions::newFromAnon();
- $po->setEditSection( false );
$po->setAllowUnsafeRawHtml( false );
- $po->setWrapOutputClass( false );
return $po;
}
$this->mParserOptions = new ParserOptions;
- $this->mParserOptions->setEditSection( false );
// Messages may take parameters that could come
// from malicious sources. As a precaution, disable
// the <html> parser tag when parsing messages.
$this->mParserOptions->setAllowUnsafeRawHtml( false );
- // Wrapping messages in an extra <div> is probably not expected. If
- // they're outside the content area they probably shouldn't be
- // targeted by CSS that's targeting the parser output, and if
- // they're inside they already are from the outer div.
- $this->mParserOptions->setWrapOutputClass( false );
}
return $this->mParserOptions;
@@ -308,7 +300,7 @@ class MessageCache {
}
if ( !$success ) {
- $cacheKey = $this->clusterCache->makeKey( 'messages', $code ); # Key in memc for messages
+ $cacheKey = $this->clusterCache->makeKey( 'messages', $code );
# Try the global cache. If it is empty, try to acquire a lock. If
# the lock can't be acquired, wait for the other thread to finish
# and then try the global cache a second time.
@@ -621,7 +613,7 @@ class MessageCache {
// load() calls do try to refresh the cache with replica DB data
$this->mCache[$code]['LATEST'] = time();
// Pre-emptively update the local datacenter cache so things like edit filter and
- // blacklist changes are reflect immediately, as these often use MediaWiki: pages.
+ // blacklist changes are reflected immediately; these often use MediaWiki: pages.
// The datacenter handling replace() calls should be the same one handling edits
// as they require HTTP POST.
$this->saveToCaches( $this->mCache[$code], 'all', $code );
@@ -630,19 +622,7 @@ class MessageCache {
// Relay the purge. Touching this check key expires cache contents
// and local cache (APC) validation hash across all datacenters.
- $this->wanCache->touchCheckKey( $this->wanCache->makeKey( 'messages', $code ) );
- // Also delete cached sidebar... just in case it is affected
- // @TODO: shouldn't this be $code === $wgLanguageCode?
- if ( $code === 'en' ) {
- // Purge all language sidebars, e.g. on ?action=purge to the sidebar messages
- $codes = array_keys( Language::fetchLanguageNames() );
- } else {
- // Purge only the sidebar for this language
- $codes = [ $code ];
- }
- foreach ( $codes as $code ) {
- $this->wanCache->delete( $this->wanCache->makeKey( 'sidebar', $code ) );
- }
+ $this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
// Purge the message in the message blob store
$resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader();
@@ -709,7 +689,7 @@ class MessageCache {
$value = $this->wanCache->get(
$this->wanCache->makeKey( 'messages', $code, 'hash', 'v1' ),
$curTTL,
- [ $this->wanCache->makeKey( 'messages', $code ) ]
+ [ $this->getCheckKey( $code ) ]
);
if ( $value ) {
@@ -914,7 +894,7 @@ class MessageCache {
if ( $useDB ) {
$uckey = $wgContLang->ucfirst( $lckey );
- if ( !isset( $alreadyTried[ $langcode ] ) ) {
+ if ( !isset( $alreadyTried[$langcode] ) ) {
$message = $this->getMsgFromNamespace(
$this->getMessagePageName( $langcode, $uckey ),
$langcode
@@ -923,7 +903,7 @@ class MessageCache {
if ( $message !== false ) {
return $message;
}
- $alreadyTried[ $langcode ] = true;
+ $alreadyTried[$langcode] = true;
}
} else {
$uckey = null;
@@ -940,7 +920,7 @@ class MessageCache {
$fallbackChain = Language::getFallbacksFor( $langcode );
foreach ( $fallbackChain as $code ) {
- if ( isset( $alreadyTried[ $code ] ) ) {
+ if ( isset( $alreadyTried[$code] ) ) {
continue;
}
@@ -950,7 +930,7 @@ class MessageCache {
if ( $message !== false ) {
return $message;
}
- $alreadyTried[ $code ] = true;
+ $alreadyTried[$code] = true;
}
}
@@ -993,13 +973,12 @@ class MessageCache {
if ( isset( $this->mCache[$code][$title] ) ) {
$entry = $this->mCache[$code][$title];
if ( substr( $entry, 0, 1 ) === ' ' ) {
- // The message exists, so make sure a string is returned.
+ // The message exists and is not '!TOO BIG'
return (string)substr( $entry, 1 );
} elseif ( $entry === '!NONEXISTENT' ) {
return false;
- } elseif ( $entry === '!TOO BIG' ) {
- // Fall through and try invididual message cache below
}
+ // Fall through and try invididual message cache below
} else {
// XXX: This is not cached in process cache, should it?
$message = false;
@@ -1048,8 +1027,7 @@ class MessageCache {
if ( $titleObj->getLatestRevID() ) {
$revision = Revision::newKnownCurrent(
$dbr,
- $titleObj->getArticleID(),
- $titleObj->getLatestRevID()
+ $titleObj
);
} else {
$revision = false;
@@ -1086,11 +1064,11 @@ class MessageCache {
/**
* @param string $message
* @param bool $interface
- * @param string $language Language code
+ * @param Language $language
* @param Title $title
* @return string
*/
- function transform( $message, $interface = false, $language = null, $title = null ) {
+ public function transform( $message, $interface = false, $language = null, $title = null ) {
// Avoid creating parser if nothing to transform
if ( strpos( $message, '{{' ) === false ) {
return $message;
@@ -1119,7 +1097,7 @@ class MessageCache {
/**
* @return Parser
*/
- function getParser() {
+ public function getParser() {
global $wgParser, $wgParserConf;
if ( !$this->mParser && isset( $wgParser ) ) {
@@ -1127,7 +1105,7 @@ class MessageCache {
$wgParser->firstCallInit();
# Clone it and store it
$class = $wgParserConf['class'];
- if ( $class == 'ParserDiffTest' ) {
+ if ( $class == ParserDiffTest::class ) {
# Uncloneable
$this->mParser = new $class( $wgParserConf );
} else {
@@ -1183,11 +1161,11 @@ class MessageCache {
return $res;
}
- function disable() {
+ public function disable() {
$this->mDisable = true;
}
- function enable() {
+ public function enable() {
$this->mDisable = false;
}
@@ -1208,13 +1186,14 @@ class MessageCache {
}
/**
- * Clear all stored messages. Mainly used after a mass rebuild.
+ * Clear all stored messages in global and local cache
+ *
+ * Mainly used after a mass rebuild
*/
function clear() {
$langs = Language::fetchLanguageNames( null, 'mw' );
foreach ( array_keys( $langs ) as $code ) {
- # Global and local caches
- $this->wanCache->touchCheckKey( $this->wanCache->makeKey( 'messages', $code ) );
+ $this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
}
$this->mLoadedLanguages = [];
@@ -1293,6 +1272,14 @@ class MessageCache {
}
/**
+ * @param string $code Language code
+ * @return string WAN cache key usable as a "check key" against language page edits
+ */
+ public function getCheckKey( $code ) {
+ return $this->wanCache->makeKey( 'messages', $code );
+ }
+
+ /**
* @param Content|null $content Content or null if the message page does not exist
* @return string|bool|null Returns false if $content is null and null on error
*/
diff --git a/www/wiki/includes/cache/UserCache.php b/www/wiki/includes/cache/UserCache.php
index 5c752926..cb685712 100644
--- a/www/wiki/includes/cache/UserCache.php
+++ b/www/wiki/includes/cache/UserCache.php
@@ -80,6 +80,8 @@ class UserCache {
* @param string $caller The calling method
*/
public function doQuery( array $userIds, $options = [], $caller = '' ) {
+ global $wgActorTableSchemaMigrationStage;
+
$usersToCheck = [];
$usersToQuery = [];
@@ -100,21 +102,34 @@ class UserCache {
// Lookup basic info for users not yet loaded...
if ( count( $usersToQuery ) ) {
$dbr = wfGetDB( DB_REPLICA );
- $table = [ 'user' ];
+ $tables = [ 'user' ];
$conds = [ 'user_id' => $usersToQuery ];
$fields = [ 'user_name', 'user_real_name', 'user_registration', 'user_id' ];
+ $joinConds = [];
+
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+ $tables[] = 'actor';
+ $fields[] = 'actor_id';
+ $joinConds['actor'] = [
+ $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
+ [ 'actor_user = user_id' ]
+ ];
+ }
$comment = __METHOD__;
if ( strval( $caller ) !== '' ) {
$comment .= "/$caller";
}
- $res = $dbr->select( $table, $fields, $conds, $comment );
+ $res = $dbr->select( $tables, $fields, $conds, $comment, [], $joinConds );
foreach ( $res as $row ) { // load each user into cache
$userId = (int)$row->user_id;
$this->cache[$userId]['name'] = $row->user_name;
$this->cache[$userId]['real_name'] = $row->user_real_name;
$this->cache[$userId]['registration'] = $row->user_registration;
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+ $this->cache[$userId]['actor'] = $row->actor_id;
+ }
$usersToCheck[$userId] = $row->user_name;
}
}
diff --git a/www/wiki/includes/cache/localisation/LCStoreStaticArray.php b/www/wiki/includes/cache/localisation/LCStoreStaticArray.php
index 1e20082f..602c0ac4 100644
--- a/www/wiki/includes/cache/localisation/LCStoreStaticArray.php
+++ b/www/wiki/includes/cache/localisation/LCStoreStaticArray.php
@@ -97,17 +97,17 @@ class LCStoreStaticArray implements LCStore {
$data = $encoded[1];
switch ( $type ) {
- case 'v':
- return $data;
- case 's':
- return unserialize( $data );
- case 'a':
- return array_map( function ( $v ) {
- return LCStoreStaticArray::decode( $v );
- }, $data );
- default:
- throw new RuntimeException(
- 'Unable to decode ' . var_export( $encoded, true ) );
+ case 'v':
+ return $data;
+ case 's':
+ return unserialize( $data );
+ case 'a':
+ return array_map( function ( $v ) {
+ return LCStoreStaticArray::decode( $v );
+ }, $data );
+ default:
+ throw new RuntimeException(
+ 'Unable to decode ' . var_export( $encoded, true ) );
}
}
diff --git a/www/wiki/includes/cache/localisation/LocalisationCache.php b/www/wiki/includes/cache/localisation/LocalisationCache.php
index a0ce95e4..e7b95548 100644
--- a/www/wiki/includes/cache/localisation/LocalisationCache.php
+++ b/www/wiki/includes/cache/localisation/LocalisationCache.php
@@ -109,7 +109,8 @@ class LocalisationCache {
static public $allKeys = [
'fallback', 'namespaceNames', 'bookstoreList',
'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable',
- 'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension',
+ 'separatorTransformTable', 'minimumGroupingDigits',
+ 'fallback8bitEncoding', 'linkPrefixExtension',
'linkTrail', 'linkPrefixCharset', 'namespaceAliases',
'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
@@ -198,22 +199,22 @@ class LocalisationCache {
switch ( $conf['store'] ) {
case 'files':
case 'file':
- $storeClass = 'LCStoreCDB';
+ $storeClass = LCStoreCDB::class;
break;
case 'db':
- $storeClass = 'LCStoreDB';
+ $storeClass = LCStoreDB::class;
break;
case 'array':
- $storeClass = 'LCStoreStaticArray';
+ $storeClass = LCStoreStaticArray::class;
break;
case 'detect':
if ( !empty( $conf['storeDirectory'] ) ) {
- $storeClass = 'LCStoreCDB';
+ $storeClass = LCStoreCDB::class;
} elseif ( $wgCacheDirectory ) {
$storeConf['directory'] = $wgCacheDirectory;
- $storeClass = 'LCStoreCDB';
+ $storeClass = LCStoreCDB::class;
} else {
- $storeClass = 'LCStoreDB';
+ $storeClass = LCStoreDB::class;
}
break;
default:
@@ -516,20 +517,30 @@ class LocalisationCache {
*/
protected function readPHPFile( $_fileName, $_fileType ) {
// Disable APC caching
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
include $_fileName;
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
ini_set( 'apc.cache_by_default', $_apcEnabled );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
+ $data = [];
if ( $_fileType == 'core' || $_fileType == 'extension' ) {
- $data = compact( self::$allKeys );
+ foreach ( self::$allKeys as $key ) {
+ // Not all keys are set in language files, so
+ // check they exist first
+ if ( isset( $$key ) ) {
+ $data[$key] = $$key;
+ }
+ }
} elseif ( $_fileType == 'aliases' ) {
- $data = compact( 'aliases' );
+ if ( isset( $aliases ) ) {
+ /** @suppress PhanUndeclaredVariable */
+ $data['aliases'] = $aliases;
+ }
} else {
throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
}
diff --git a/www/wiki/includes/changes/CategoryMembershipChange.php b/www/wiki/includes/changes/CategoryMembershipChange.php
index 6fa69070..f095b64f 100644
--- a/www/wiki/includes/changes/CategoryMembershipChange.php
+++ b/www/wiki/includes/changes/CategoryMembershipChange.php
@@ -71,7 +71,7 @@ class CategoryMembershipChange {
$this->timestamp = $revision->getTimestamp();
}
$this->revision = $revision;
- $this->newForCategorizationCallback = [ 'RecentChange', 'newForCategorization' ];
+ $this->newForCategorizationCallback = [ RecentChange::class, 'newForCategorization' ];
}
/**
diff --git a/www/wiki/includes/changes/ChangesFeed.php b/www/wiki/includes/changes/ChangesFeed.php
index df964e0a..7ac8cd0e 100644
--- a/www/wiki/includes/changes/ChangesFeed.php
+++ b/www/wiki/includes/changes/ChangesFeed.php
@@ -82,10 +82,11 @@ class ChangesFeed {
return null;
}
+ $cache = ObjectCache::getMainWANInstance();
$optionsHash = md5( serialize( $opts->getAllValues() ) ) . $wgRenderHashAppend;
- $timekey = wfMemcKey(
+ $timekey = $cache->makeKey(
$this->type, $this->format, $wgLang->getCode(), $optionsHash, 'timestamp' );
- $key = wfMemcKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash );
+ $key = $cache->makeKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash );
FeedUtils::checkPurge( $timekey, $key );
diff --git a/www/wiki/includes/changes/ChangesList.php b/www/wiki/includes/changes/ChangesList.php
index bc50096f..ac029a2b 100644
--- a/www/wiki/includes/changes/ChangesList.php
+++ b/www/wiki/includes/changes/ChangesList.php
@@ -576,7 +576,9 @@ class ChangesList extends ContextSource {
return '';
}
$cache = $this->watchMsgCache;
- return $cache->getWithSetCallback( $count, $cache::TTL_INDEFINITE,
+ return $cache->getWithSetCallback(
+ $cache->makeKey( 'watching-users-msg', $count ),
+ $cache::TTL_INDEFINITE,
function () use ( $count ) {
return $this->msg( 'number_of_watching_users_RCview' )
->numParams( $count )->escaped();
@@ -644,6 +646,7 @@ class ChangesList extends ContextSource {
'id' => $rc->mAttribs['rc_this_oldid'],
'user' => $rc->mAttribs['rc_user'],
'user_text' => $rc->mAttribs['rc_user_text'],
+ 'actor' => isset( $rc->mAttribs['rc_actor'] ) ? $rc->mAttribs['rc_actor'] : null,
'deleted' => $rc->mAttribs['rc_deleted']
] );
$s .= ' ' . Linker::generateRollback( $rev, $this->getContext() );
diff --git a/www/wiki/includes/changes/ChangesListBooleanFilter.php b/www/wiki/includes/changes/ChangesListBooleanFilter.php
index 2a7ba884..f37ed2dd 100644
--- a/www/wiki/includes/changes/ChangesListBooleanFilter.php
+++ b/www/wiki/includes/changes/ChangesListBooleanFilter.php
@@ -18,14 +18,13 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Matthew Flaschen
*/
use Wikimedia\Rdbms\IDatabase;
/**
- * An individual filter in a boolean group
+ * Represents a hide-based boolean filter (used on ChangesListSpecialPage and descendants)
*
* @since 1.29
*/
diff --git a/www/wiki/includes/changes/ChangesListBooleanFilterGroup.php b/www/wiki/includes/changes/ChangesListBooleanFilterGroup.php
index 0622211f..4401378b 100644
--- a/www/wiki/includes/changes/ChangesListBooleanFilterGroup.php
+++ b/www/wiki/includes/changes/ChangesListBooleanFilterGroup.php
@@ -1,5 +1,7 @@
<?php
+use Wikimedia\Rdbms\IDatabase;
+
/**
* If the group is active, any unchecked filters will
* translate to hide parameters in the URL. E.g. if 'Human (not bot)' is checked,
@@ -52,7 +54,7 @@ class ChangesListBooleanFilterGroup extends ChangesListFilterGroup {
/**
* Registers a filter in this group
*
- * @param ChangesListBooleanFilter $filter ChangesListBooleanFilter
+ * @param ChangesListBooleanFilter $filter
*/
public function registerFilter( ChangesListBooleanFilter $filter ) {
$this->filters[$filter->getName()] = $filter;
@@ -61,7 +63,27 @@ class ChangesListBooleanFilterGroup extends ChangesListFilterGroup {
/**
* @inheritDoc
*/
- public function isPerGroupRequestParameter() {
- return false;
+ public function modifyQuery( IDatabase $dbr, ChangesListSpecialPage $specialPage,
+ &$tables, &$fields, &$conds, &$query_options, &$join_conds,
+ FormOptions $opts, $isStructuredFiltersEnabled
+ ) {
+ /** @var ChangesListBooleanFilter $filter */
+ foreach ( $this->getFilters() as $filter ) {
+ if ( $filter->isActive( $opts, $isStructuredFiltersEnabled ) ) {
+ $filter->modifyQuery( $dbr, $specialPage, $tables, $fields, $conds,
+ $query_options, $join_conds );
+ }
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function addOptions( FormOptions $opts, $allowDefaults, $isStructuredFiltersEnabled ) {
+ /** @var ChangesListBooleanFilter $filter */
+ foreach ( $this->getFilters() as $filter ) {
+ $defaultValue = $allowDefaults ? $filter->getDefault( $isStructuredFiltersEnabled ) : false;
+ $opts->add( $filter->getName(), $defaultValue );
+ }
}
}
diff --git a/www/wiki/includes/changes/ChangesListFilter.php b/www/wiki/includes/changes/ChangesListFilter.php
index 2fc1006e..d1914536 100644
--- a/www/wiki/includes/changes/ChangesListFilter.php
+++ b/www/wiki/includes/changes/ChangesListFilter.php
@@ -18,7 +18,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Matthew Flaschen
*/
@@ -103,6 +102,12 @@ abstract class ChangesListFilter {
*/
protected $priority;
+ /**
+ *
+ * @var string $defaultHighlightColor
+ */
+ protected $defaultHighlightColor;
+
const RESERVED_NAME_CHAR = '_';
/**
@@ -178,8 +183,7 @@ abstract class ChangesListFilter {
* (not filtered out), even for the hide-based filters. So e.g. conflicting with
* 'hideanons' means there is a conflict if only anonymous users are *shown*.
*
- * @param ChangesListFilterGroup|ChangesListFilter $other Other
- * ChangesListFilterGroup or ChangesListFilter
+ * @param ChangesListFilterGroup|ChangesListFilter $other
* @param string $globalKey i18n key for top-level conflict message
* @param string $forwardKey i18n key for conflict message in this
* direction (when in UI context of $this object)
@@ -210,8 +214,7 @@ abstract class ChangesListFilter {
*
* Internal use ONLY.
*
- * @param ChangesListFilterGroup|ChangesListFilter $other Other
- * ChangesListFilterGroup or ChangesListFilter
+ * @param ChangesListFilterGroup|ChangesListFilter $other
* @param string $globalDescription i18n key for top-level conflict message
* @param string $contextDescription i18n key for conflict message in this
* direction (when in UI context of $this object)
@@ -368,6 +371,7 @@ abstract class ChangesListFilter {
'priority' => $this->priority,
'subset' => $this->subsetFilters,
'conflicts' => [],
+ 'defaultHighlightColor' => $this->defaultHighlightColor
];
$output['messageKeys'] = [
@@ -461,7 +465,7 @@ abstract class ChangesListFilter {
* @param FormOptions $opts
* @return bool
*/
- public function activelyInConflictWithFilter( ChangeslistFilter $filter, FormOptions $opts ) {
+ public function activelyInConflictWithFilter( ChangesListFilter $filter, FormOptions $opts ) {
if ( $this->isSelected( $opts ) && $filter->isSelected( $opts ) ) {
/** @var ChangesListFilter $siblingFilter */
foreach ( $this->getSiblings() as $siblingFilter ) {
@@ -477,7 +481,7 @@ abstract class ChangesListFilter {
return false;
}
- private function hasConflictWithFilter( ChangeslistFilter $filter ) {
+ private function hasConflictWithFilter( ChangesListFilter $filter ) {
return in_array( $filter, $this->getConflictingFilters() );
}
@@ -494,4 +498,11 @@ abstract class ChangesListFilter {
}
);
}
+
+ /**
+ * @param string $defaultHighlightColor
+ */
+ public function setDefaultHighlightColor( $defaultHighlightColor ) {
+ $this->defaultHighlightColor = $defaultHighlightColor;
+ }
}
diff --git a/www/wiki/includes/changes/ChangesListFilterGroup.php b/www/wiki/includes/changes/ChangesListFilterGroup.php
index e9140da2..3e2c464a 100644
--- a/www/wiki/includes/changes/ChangesListFilterGroup.php
+++ b/www/wiki/includes/changes/ChangesListFilterGroup.php
@@ -18,7 +18,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Matthew Flaschen
*/
@@ -27,6 +26,8 @@
// What to call it. FilterStructure? That would also let me make
// setUnidirectionalConflict protected.
+use Wikimedia\Rdbms\IDatabase;
+
/**
* Represents a filter group (used on ChangesListSpecialPage and descendants)
*
@@ -221,8 +222,7 @@ abstract class ChangesListFilterGroup {
* (not filtered out), even for the hide-based filters. So e.g. conflicting with
* 'hideanons' means there is a conflict if only anonymous users are *shown*.
*
- * @param ChangesListFilterGroup|ChangesListFilter $other Other
- * ChangesListFilterGroup or ChangesListFilter
+ * @param ChangesListFilterGroup|ChangesListFilter $other
* @param string $globalKey i18n key for top-level conflict message
* @param string $forwardKey i18n key for conflict message in this
* direction (when in UI context of $this object)
@@ -253,8 +253,7 @@ abstract class ChangesListFilterGroup {
*
* Internal use ONLY.
*
- * @param ChangesListFilterGroup|ChangesListFilter $other Other
- * ChangesListFilterGroup or ChangesListFilter
+ * @param ChangesListFilterGroup|ChangesListFilter $other
* @param string $globalDescription i18n key for top-level conflict message
* @param string $contextDescription i18n key for conflict message in this
* direction (when in UI context of $this object)
@@ -327,14 +326,6 @@ abstract class ChangesListFilterGroup {
}
/**
- * Check whether the URL parameter is for the group, or for individual filters.
- * Defaults can also be defined on the group if and only if this is true.
- *
- * @return bool True if and only if the URL parameter is per-group
- */
- abstract public function isPerGroupRequestParameter();
-
- /**
* Gets the JS data in the format required by the front-end of the structured UI
*
* @return array|null Associative array, or null if there are no filters that
@@ -449,4 +440,34 @@ abstract class ChangesListFilterGroup {
}
) );
}
+
+ /**
+ * Modifies the query to include the filter group.
+ *
+ * The modification is only done if the filter group is in effect. This means that
+ * one or more valid and allowed filters were selected.
+ *
+ * @param IDatabase $dbr Database, for addQuotes, makeList, and similar
+ * @param ChangesListSpecialPage $specialPage Current special page
+ * @param array &$tables Array of tables; see IDatabase::select $table
+ * @param array &$fields Array of fields; see IDatabase::select $vars
+ * @param array &$conds Array of conditions; see IDatabase::select $conds
+ * @param array &$query_options Array of query options; see IDatabase::select $options
+ * @param array &$join_conds Array of join conditions; see IDatabase::select $join_conds
+ * @param FormOptions $opts Wrapper for the current request options and their defaults
+ * @param bool $isStructuredFiltersEnabled True if the Structured UI is currently enabled
+ */
+ abstract public function modifyQuery( IDatabase $dbr, ChangesListSpecialPage $specialPage,
+ &$tables, &$fields, &$conds, &$query_options, &$join_conds,
+ FormOptions $opts, $isStructuredFiltersEnabled );
+
+ /**
+ * All the options represented by this filter group to $opts
+ *
+ * @param FormOptions $opts
+ * @param bool $allowDefaults
+ * @param bool $isStructuredFiltersEnabled
+ */
+ abstract public function addOptions( FormOptions $opts, $allowDefaults,
+ $isStructuredFiltersEnabled );
}
diff --git a/www/wiki/includes/changes/ChangesListStringOptionsFilterGroup.php b/www/wiki/includes/changes/ChangesListStringOptionsFilterGroup.php
index 59efd82b..8cd7ba8d 100644
--- a/www/wiki/includes/changes/ChangesListStringOptionsFilterGroup.php
+++ b/www/wiki/includes/changes/ChangesListStringOptionsFilterGroup.php
@@ -18,7 +18,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Matthew Flaschen
*/
@@ -35,7 +34,6 @@ use Wikimedia\Rdbms\IDatabase;
*
* @since 1.29
*/
-
class ChangesListStringOptionsFilterGroup extends ChangesListFilterGroup {
/**
* Type marker, used by JavaScript
@@ -129,13 +127,6 @@ class ChangesListStringOptionsFilterGroup extends ChangesListFilterGroup {
}
/**
- * @inheritDoc
- */
- public function isPerGroupRequestParameter() {
- return true;
- }
-
- /**
* Sets default of filter group.
*
* @param string $defaultValue
@@ -163,30 +154,24 @@ class ChangesListStringOptionsFilterGroup extends ChangesListFilterGroup {
/**
* Registers a filter in this group
*
- * @param ChangesListStringOptionsFilter $filter ChangesListStringOptionsFilter
+ * @param ChangesListStringOptionsFilter $filter
*/
public function registerFilter( ChangesListStringOptionsFilter $filter ) {
$this->filters[$filter->getName()] = $filter;
}
/**
- * Modifies the query to include the filter group.
- *
- * The modification is only done if the filter group is in effect. This means that
- * one or more valid and allowed filters were selected.
- *
- * @param IDatabase $dbr Database, for addQuotes, makeList, and similar
- * @param ChangesListSpecialPage $specialPage Current special page
- * @param array &$tables Array of tables; see IDatabase::select $table
- * @param array &$fields Array of fields; see IDatabase::select $vars
- * @param array &$conds Array of conditions; see IDatabase::select $conds
- * @param array &$query_options Array of query options; see IDatabase::select $options
- * @param array &$join_conds Array of join conditions; see IDatabase::select $join_conds
- * @param string $value URL parameter value
+ * @inheritDoc
*/
public function modifyQuery( IDatabase $dbr, ChangesListSpecialPage $specialPage,
- &$tables, &$fields, &$conds, &$query_options, &$join_conds, $value
+ &$tables, &$fields, &$conds, &$query_options, &$join_conds,
+ FormOptions $opts, $isStructuredFiltersEnabled
) {
+ if ( !$this->isActive( $isStructuredFiltersEnabled ) ) {
+ return;
+ }
+
+ $value = $opts[ $this->getName() ];
$allowedFilterNames = [];
foreach ( $this->filters as $filter ) {
$allowedFilterNames[] = $filter->getName();
@@ -242,4 +227,22 @@ class ChangesListStringOptionsFilterGroup extends ChangesListFilterGroup {
return $output;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function addOptions( FormOptions $opts, $allowDefaults, $isStructuredFiltersEnabled ) {
+ $opts->add( $this->getName(), $allowDefaults ? $this->getDefault() : '' );
+ }
+
+ /**
+ * Check if this filter group is currently active
+ *
+ * @param bool $isStructuredUI Is structured filters UI current enabled
+ * @return bool
+ */
+ private function isActive( $isStructuredUI ) {
+ // STRING_OPTIONS filter groups are exclusively active on Structured UI
+ return $isStructuredUI;
+ }
}
diff --git a/www/wiki/includes/changes/EnhancedChangesList.php b/www/wiki/includes/changes/EnhancedChangesList.php
index 0df68281..6c7666fb 100644
--- a/www/wiki/includes/changes/EnhancedChangesList.php
+++ b/www/wiki/includes/changes/EnhancedChangesList.php
@@ -324,7 +324,7 @@ class EnhancedChangesList extends ChangesList {
$first--;
}
# Get net change
- $charDifference = $this->formatCharacterDifference( $block[$first], $block[$last] );
+ $charDifference = $this->formatCharacterDifference( $block[$first], $block[$last] ) ?: false;
}
$numberofWatchingusers = $this->numberofWatchingusers( $block[0]->numberofWatchingusers );
diff --git a/www/wiki/includes/changes/RecentChange.php b/www/wiki/includes/changes/RecentChange.php
index fd789a64..8e8b93f1 100644
--- a/www/wiki/includes/changes/RecentChange.php
+++ b/www/wiki/includes/changes/RecentChange.php
@@ -34,6 +34,7 @@
* rc_cur_id page_id of associated page entry
* rc_user user id who made the entry
* rc_user_text user name who made the entry
+ * rc_actor actor id who made the entry
* rc_comment edit summary
* rc_this_oldid rev_id associated with this entry (or zero)
* rc_last_oldid rev_id associated with the entry before this one (or zero)
@@ -73,6 +74,10 @@ class RecentChange {
const SRC_EXTERNAL = 'mw.external'; // obsolete
const SRC_CATEGORIZE = 'mw.categorize';
+ const PRC_UNPATROLLED = 0;
+ const PRC_PATROLLED = 1;
+ const PRC_AUTOPATROLLED = 2;
+
public $mAttribs = [];
public $mExtra = [];
@@ -192,7 +197,10 @@ class RecentChange {
$dbType = DB_REPLICA
) {
$db = wfGetDB( $dbType );
- $row = $db->selectRow( 'recentchanges', self::selectFields(), $conds, $fname );
+ $rcQuery = self::getQueryInfo();
+ $row = $db->selectRow(
+ $rcQuery['tables'], $rcQuery['fields'], $conds, $fname, [], $rcQuery['joins']
+ );
if ( $row !== false ) {
return self::newFromRow( $row );
} else {
@@ -203,16 +211,29 @@ class RecentChange {
/**
* Return the list of recentchanges fields that should be selected to create
* a new recentchanges object.
- * @todo Deprecate this in favor of a method that returns tables and joins
- * as well, and use CommentStore::getJoin().
+ * @deprecated since 1.31, use self::getQueryInfo() instead.
* @return array
*/
public static function selectFields() {
+ global $wgActorTableSchemaMigrationStage;
+
+ wfDeprecated( __METHOD__, '1.31' );
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+ // If code is using this instead of self::getQueryInfo(), there's a
+ // decent chance it's going to try to directly access
+ // $row->rc_user or $row->rc_user_text and we can't give it
+ // useful values here once those aren't being written anymore.
+ throw new BadMethodCallException(
+ 'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+ );
+ }
+
return [
'rc_id',
'rc_timestamp',
'rc_user',
'rc_user_text',
+ 'rc_actor' => 'NULL',
'rc_namespace',
'rc_title',
'rc_minor',
@@ -232,7 +253,48 @@ class RecentChange {
'rc_log_type',
'rc_log_action',
'rc_params',
- ] + CommentStore::newKey( 'rc_comment' )->getFields();
+ ] + CommentStore::getStore()->getFields( 'rc_comment' );
+ }
+
+ /**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new recentchanges object.
+ * @since 1.31
+ * @return array With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ */
+ public static function getQueryInfo() {
+ $commentQuery = CommentStore::getStore()->getJoin( 'rc_comment' );
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
+ return [
+ 'tables' => [ 'recentchanges' ] + $commentQuery['tables'] + $actorQuery['tables'],
+ 'fields' => [
+ 'rc_id',
+ 'rc_timestamp',
+ 'rc_namespace',
+ 'rc_title',
+ 'rc_minor',
+ 'rc_bot',
+ 'rc_new',
+ 'rc_cur_id',
+ 'rc_this_oldid',
+ 'rc_last_oldid',
+ 'rc_type',
+ 'rc_source',
+ 'rc_patrolled',
+ 'rc_ip',
+ 'rc_old_len',
+ 'rc_new_len',
+ 'rc_deleted',
+ 'rc_logid',
+ 'rc_log_type',
+ 'rc_log_action',
+ 'rc_params',
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
+ 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
+ ];
}
# Accessors
@@ -269,10 +331,14 @@ class RecentChange {
*/
public function getPerformer() {
if ( $this->mPerformer === false ) {
- if ( $this->mAttribs['rc_user'] ) {
+ if ( !empty( $this->mAttribs['rc_actor'] ) ) {
+ $this->mPerformer = User::newFromActorId( $this->mAttribs['rc_actor'] );
+ } elseif ( !empty( $this->mAttribs['rc_user'] ) ) {
$this->mPerformer = User::newFromId( $this->mAttribs['rc_user'] );
- } else {
+ } elseif ( !empty( $this->mAttribs['rc_user_text'] ) ) {
$this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false );
+ } else {
+ throw new MWException( 'RecentChange object lacks rc_actor, rc_user, and rc_user_text' );
}
}
@@ -323,11 +389,21 @@ class RecentChange {
unset( $this->mAttribs['rc_cur_id'] );
}
- # Convert mAttribs['rc_comment'] for CommentStore
$row = $this->mAttribs;
+
+ # Convert mAttribs['rc_comment'] for CommentStore
$comment = $row['rc_comment'];
unset( $row['rc_comment'], $row['rc_comment_text'], $row['rc_comment_data'] );
- $row += CommentStore::newKey( 'rc_comment' )->insert( $dbw, $comment );
+ $row += CommentStore::getStore()->insert( $dbw, 'rc_comment', $comment );
+
+ # Convert mAttribs['rc_user'] etc for ActorMigration
+ $user = User::newFromAnyId(
+ isset( $row['rc_user'] ) ? $row['rc_user'] : null,
+ isset( $row['rc_user_text'] ) ? $row['rc_user_text'] : null,
+ isset( $row['rc_actor'] ) ? $row['rc_actor'] : null
+ );
+ unset( $row['rc_user'], $row['rc_user_text'], $row['rc_actor'] );
+ $row += ActorMigration::newMigration()->getInsertValues( $dbw, 'rc_user', $user );
# Don't reuse an existing rc_id for the new row, if one happens to be
# set for some reason.
@@ -546,7 +622,7 @@ class RecentChange {
$dbw->update(
'recentchanges',
[
- 'rc_patrolled' => 1
+ 'rc_patrolled' => self::PRC_PATROLLED
],
[
'rc_id' => $this->getAttribute( 'rc_id' )
@@ -597,6 +673,7 @@ class RecentChange {
'rc_cur_id' => $title->getArticleID(),
'rc_user' => $user->getId(),
'rc_user_text' => $user->getName(),
+ 'rc_actor' => $user->getActorId(),
'rc_comment' => &$comment,
'rc_comment_text' => &$comment,
'rc_comment_data' => null,
@@ -627,9 +704,6 @@ class RecentChange {
function () use ( $rc, $tags ) {
$rc->addTags( $tags );
$rc->save();
- if ( $rc->mAttribs['rc_patrolled'] ) {
- PatrolLog::record( $rc, true, $rc->getPerformer() );
- }
},
DeferredUpdates::POSTSEND,
wfGetDB( DB_MASTER )
@@ -672,6 +746,7 @@ class RecentChange {
'rc_cur_id' => $title->getArticleID(),
'rc_user' => $user->getId(),
'rc_user_text' => $user->getName(),
+ 'rc_actor' => $user->getActorId(),
'rc_comment' => &$comment,
'rc_comment_text' => &$comment,
'rc_comment_data' => null,
@@ -702,9 +777,6 @@ class RecentChange {
function () use ( $rc, $tags ) {
$rc->addTags( $tags );
$rc->save();
- if ( $rc->mAttribs['rc_patrolled'] ) {
- PatrolLog::record( $rc, true, $rc->getPerformer() );
- }
},
DeferredUpdates::POSTSEND,
wfGetDB( DB_MASTER )
@@ -804,6 +876,7 @@ class RecentChange {
'rc_cur_id' => $target->getArticleID(),
'rc_user' => $user->getId(),
'rc_user_text' => $user->getName(),
+ 'rc_actor' => $user->getActorId(),
'rc_comment' => &$logComment,
'rc_comment_text' => &$logComment,
'rc_comment_data' => null,
@@ -811,7 +884,7 @@ class RecentChange {
'rc_last_oldid' => 0,
'rc_bot' => $user->isAllowed( 'bot' ) ? (int)$wgRequest->getBool( 'bot', true ) : 0,
'rc_ip' => self::checkIPAddress( $ip ),
- 'rc_patrolled' => $markPatrolled ? 1 : 0,
+ 'rc_patrolled' => $markPatrolled ? self::PRC_PATROLLED : self::PRC_UNPATROLLED,
'rc_new' => 0, # obsolete
'rc_old_len' => null,
'rc_new_len' => null,
@@ -889,6 +962,7 @@ class RecentChange {
'rc_cur_id' => $pageTitle->getArticleID(),
'rc_user' => $user ? $user->getId() : 0,
'rc_user_text' => $user ? $user->getName() : '',
+ 'rc_actor' => $user ? $user->getActorId() : null,
'rc_comment' => &$comment,
'rc_comment_text' => &$comment,
'rc_comment_data' => null,
@@ -896,7 +970,7 @@ class RecentChange {
'rc_last_oldid' => $oldRevId,
'rc_bot' => $bot ? 1 : 0,
'rc_ip' => self::checkIPAddress( $ip ),
- 'rc_patrolled' => 1, // Always patrolled, just like log entries
+ 'rc_patrolled' => self::PRC_PATROLLED, // Always patrolled, just like log entries
'rc_new' => 0, # obsolete
'rc_old_len' => null,
'rc_new_len' => null,
@@ -950,12 +1024,22 @@ class RecentChange {
}
}
- $comment = CommentStore::newKey( 'rc_comment' )
- // Legacy because $row probably came from self::selectFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text;
+ $comment = CommentStore::getStore()
+ // Legacy because $row may have come from self::selectFields()
+ ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $row, true )
+ ->text;
$this->mAttribs['rc_comment'] = &$comment;
$this->mAttribs['rc_comment_text'] = &$comment;
$this->mAttribs['rc_comment_data'] = null;
+
+ $user = User::newFromAnyId(
+ isset( $this->mAttribs['rc_user'] ) ? $this->mAttribs['rc_user'] : null,
+ isset( $this->mAttribs['rc_user_text'] ) ? $this->mAttribs['rc_user_text'] : null,
+ isset( $this->mAttribs['rc_actor'] ) ? $this->mAttribs['rc_actor'] : null
+ );
+ $this->mAttribs['rc_user'] = $user->getId();
+ $this->mAttribs['rc_user_text'] = $user->getName();
+ $this->mAttribs['rc_actor'] = $user->getActorId();
}
/**
@@ -966,8 +1050,27 @@ class RecentChange {
*/
public function getAttribute( $name ) {
if ( $name === 'rc_comment' ) {
- return CommentStore::newKey( 'rc_comment' )->getComment( $this->mAttribs, true )->text;
+ return CommentStore::getStore()
+ ->getComment( 'rc_comment', $this->mAttribs, true )->text;
}
+
+ if ( $name === 'rc_user' || $name === 'rc_user_text' || $name === 'rc_actor' ) {
+ $user = User::newFromAnyId(
+ isset( $this->mAttribs['rc_user'] ) ? $this->mAttribs['rc_user'] : null,
+ isset( $this->mAttribs['rc_user_text'] ) ? $this->mAttribs['rc_user_text'] : null,
+ isset( $this->mAttribs['rc_actor'] ) ? $this->mAttribs['rc_actor'] : null
+ );
+ if ( $name === 'rc_user' ) {
+ return $user->getId();
+ }
+ if ( $name === 'rc_user_text' ) {
+ return $user->getName();
+ }
+ if ( $name === 'rc_actor' ) {
+ return $user->getActorId();
+ }
+ }
+
return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : null;
}
@@ -1063,9 +1166,9 @@ class RecentChange {
public function parseParams() {
$rcParams = $this->getAttribute( 'rc_params' );
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$unserializedParams = unserialize( $rcParams );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return $unserializedParams;
}
diff --git a/www/wiki/includes/changetags/ChangeTags.php b/www/wiki/includes/changetags/ChangeTags.php
index fa981247..5b6088d5 100644
--- a/www/wiki/includes/changetags/ChangeTags.php
+++ b/www/wiki/includes/changetags/ChangeTags.php
@@ -32,10 +32,45 @@ class ChangeTags {
*/
const MAX_DELETE_USES = 5000;
+ private static $definedSoftwareTags = [
+ 'mw-contentmodelchange',
+ 'mw-new-redirect',
+ 'mw-removed-redirect',
+ 'mw-changed-redirect-target',
+ 'mw-blank',
+ 'mw-replace',
+ 'mw-rollback',
+ 'mw-undo',
+ ];
+
/**
- * @var string[]
+ * Loads defined core tags, checks for invalid types (if not array),
+ * and filters for supported and enabled (if $all is false) tags only.
+ *
+ * @param bool $all If true, return all valid defined tags. Otherwise, return only enabled ones.
+ * @return array Array of all defined/enabled tags.
*/
- private static $coreTags = [ 'mw-contentmodelchange' ];
+ public static function getSoftwareTags( $all = false ) {
+ global $wgSoftwareTags;
+ $softwareTags = [];
+
+ if ( !is_array( $wgSoftwareTags ) ) {
+ wfWarn( 'wgSoftwareTags should be associative array of enabled tags.
+ Please refer to documentation for the list of tags you can enable' );
+ return $softwareTags;
+ }
+
+ $availableSoftwareTags = !$all ?
+ array_keys( array_filter( $wgSoftwareTags ) ) :
+ array_keys( $wgSoftwareTags );
+
+ $softwareTags = array_intersect(
+ $availableSoftwareTags,
+ self::$definedSoftwareTags
+ );
+
+ return $softwareTags;
+ }
/**
* Creates HTML for the given tags
@@ -100,7 +135,7 @@ class ChangeTags {
* exists, provided it is not disabled. If the message is disabled,
* we consider the tag hidden, and return false.
*
- * @param string $tag Tag
+ * @param string $tag
* @param IContextSource $context
* @return string|bool Tag description or false if tag is to be hidden.
* @since 1.25 Returns false if tag is to be hidden.
@@ -127,7 +162,7 @@ class ChangeTags {
* or if message is disabled, returns false. Otherwise, returns the message object
* for the long description.
*
- * @param string $tag Tag
+ * @param string $tag
* @param IContextSource $context
* @return Message|bool Message object of the tag long description or false if
* there is no description.
@@ -147,6 +182,28 @@ class ChangeTags {
}
/**
+ * Get truncated message for the tag's long description.
+ *
+ * @param string $tag Tag name.
+ * @param int $length Maximum length of truncated message, including ellipsis.
+ * @param IContextSource $context
+ *
+ * @return string Truncated long tag description.
+ */
+ public static function truncateTagDescription( $tag, $length, IContextSource $context ) {
+ $originalDesc = self::tagLongDescriptionMessage( $tag, $context );
+ // If there is no tag description, return empty string
+ if ( !$originalDesc ) {
+ return '';
+ }
+
+ $taglessDesc = Sanitizer::stripAllTags( $originalDesc->parse() );
+ $escapedDesc = Sanitizer::escapeHtmlAllowEntities( $taglessDesc );
+
+ return $context->getLanguage()->truncateForVisual( $escapedDesc, $length );
+ }
+
+ /**
* Add tags to a change given its rc_id, rev_id and/or log_id
*
* @param string|string[] $tags Tags to add to the change
@@ -373,19 +430,24 @@ class ChangeTags {
sort( $prevTags );
sort( $newTags );
if ( $prevTags == $newTags ) {
- // No change.
return false;
}
if ( !$newTags ) {
- // no tags left, so delete the row altogether
+ // No tags left, so delete the row altogether
$dbw->delete( 'tag_summary', $tsConds, __METHOD__ );
} else {
- $dbw->replace( 'tag_summary',
- [ 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ],
- array_filter( array_merge( $tsConds, [ 'ts_tags' => implode( ',', $newTags ) ] ) ),
- __METHOD__
- );
+ // Specify the non-DEFAULT value columns in the INSERT/REPLACE clause
+ $row = array_filter( [ 'ts_tags' => implode( ',', $newTags ) ] + $tsConds );
+ // Check the unique keys for conflicts, ignoring any NULL *_id values
+ $uniqueKeys = [];
+ foreach ( [ 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ] as $uniqueColumn ) {
+ if ( isset( $row[$uniqueColumn] ) ) {
+ $uniqueKeys[] = [ $uniqueColumn ];
+ }
+ }
+
+ $dbw->replace( 'tag_summary', $uniqueKeys, $row, __METHOD__ );
}
return true;
@@ -1210,7 +1272,7 @@ class ChangeTags {
*/
public static function listSoftwareActivatedTags() {
// core active tags
- $tags = self::$coreTags;
+ $tags = self::getSoftwareTags();
if ( !Hooks::isRegistered( 'ChangeTagsListActive' ) ) {
return $tags;
}
@@ -1234,19 +1296,8 @@ class ChangeTags {
}
/**
- * @see listSoftwareActivatedTags
- * @deprecated since 1.28 call listSoftwareActivatedTags directly
- * @return array
- */
- public static function listExtensionActivatedTags() {
- wfDeprecated( __METHOD__, '1.28' );
- return self::listSoftwareActivatedTags();
- }
-
- /**
* 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().
+ * It returns a union of the results of listExplicitlyDefinedTags()
*
* @return string[] Array of strings: tags
*/
@@ -1301,7 +1352,7 @@ class ChangeTags {
*/
public static function listSoftwareDefinedTags() {
// core defined tags
- $tags = self::$coreTags;
+ $tags = self::getSoftwareTags( true );
if ( !Hooks::isRegistered( 'ListDefinedTags' ) ) {
return $tags;
}
@@ -1324,18 +1375,6 @@ class ChangeTags {
}
/**
- * Call listSoftwareDefinedTags directly
- *
- * @see listSoftwareDefinedTags
- * @deprecated since 1.28
- * @return array
- */
- public static function listExtensionDefinedTags() {
- wfDeprecated( __METHOD__, '1.28' );
- return self::listSoftwareDefinedTags();
- }
-
- /**
* Invalidates the short-term cache of defined tags used by the
* list*DefinedTags functions, as well as the tag statistics cache.
* @since 1.25
diff --git a/www/wiki/includes/changetags/ChangeTagsList.php b/www/wiki/includes/changetags/ChangeTagsList.php
index afbbb2bf..1559e1d6 100644
--- a/www/wiki/includes/changetags/ChangeTagsList.php
+++ b/www/wiki/includes/changetags/ChangeTagsList.php
@@ -43,10 +43,10 @@ abstract class ChangeTagsList extends RevisionListBase {
) {
switch ( $typeName ) {
case 'revision':
- $className = 'ChangeTagsRevisionList';
+ $className = ChangeTagsRevisionList::class;
break;
case 'logentry':
- $className = 'ChangeTagsLogList';
+ $className = ChangeTagsLogList::class;
break;
default:
throw new Exception( "Class $typeName requested, but does not exist" );
diff --git a/www/wiki/includes/changetags/ChangeTagsLogItem.php b/www/wiki/includes/changetags/ChangeTagsLogItem.php
index b78efafa..1b9fd92c 100644
--- a/www/wiki/includes/changetags/ChangeTagsLogItem.php
+++ b/www/wiki/includes/changetags/ChangeTagsLogItem.php
@@ -44,8 +44,12 @@ class ChangeTagsLogItem extends RevisionItemBase {
return 'log_user_text';
}
+ public function getAuthorActorField() {
+ return 'log_actor';
+ }
+
public function canView() {
- return LogEventsList::userCan( $this->row, Revision::DELETED_RESTRICTED, $this->list->getUser() );
+ return LogEventsList::userCan( $this->row, Revision::SUPPRESSED_ALL, $this->list->getUser() );
}
public function canViewContent() {
diff --git a/www/wiki/includes/changetags/ChangeTagsLogList.php b/www/wiki/includes/changetags/ChangeTagsLogList.php
index e6d918a6..69771726 100644
--- a/www/wiki/includes/changetags/ChangeTagsLogList.php
+++ b/www/wiki/includes/changetags/ChangeTagsLogList.php
@@ -72,9 +72,8 @@ class ChangeTagsLogList extends ChangeTagsList {
* @return Status
*/
public function updateChangeTagsOnAll( $tagsToAdd, $tagsToRemove, $params, $reason, $user ) {
- // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
+ // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
for ( $this->reset(); $this->current(); $this->next() ) {
- // @codingStandardsIgnoreEnd
$item = $this->current();
$status = ChangeTags::updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
null, null, $item->getId(), $params, $reason, $user );
diff --git a/www/wiki/includes/changetags/ChangeTagsRevisionList.php b/www/wiki/includes/changetags/ChangeTagsRevisionList.php
index 91193b0e..19b7e20d 100644
--- a/www/wiki/includes/changetags/ChangeTagsRevisionList.php
+++ b/www/wiki/includes/changetags/ChangeTagsRevisionList.php
@@ -36,18 +36,16 @@ class ChangeTagsRevisionList extends ChangeTagsList {
*/
public function doQuery( $db ) {
$ids = array_map( 'intval', $this->ids );
+ $revQuery = Revision::getQueryInfo( [ 'user' ] );
$queryInfo = [
- 'tables' => [ 'revision', 'user' ],
- 'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ),
+ 'tables' => $revQuery['tables'],
+ 'fields' => $revQuery['fields'],
'conds' => [
'rev_page' => $this->title->getArticleID(),
'rev_id' => $ids,
],
'options' => [ 'ORDER BY' => 'rev_id DESC' ],
- 'join_conds' => [
- 'page' => Revision::pageJoinCond(),
- 'user' => Revision::userJoinCond(),
- ],
+ 'join_conds' => $revQuery['joins'],
];
ChangeTags::modifyDisplayQuery(
$queryInfo['tables'],
@@ -82,9 +80,8 @@ class ChangeTagsRevisionList extends ChangeTagsList {
* @return Status
*/
public function updateChangeTagsOnAll( $tagsToAdd, $tagsToRemove, $params, $reason, $user ) {
- // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
+ // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
for ( $this->reset(); $this->current(); $this->next() ) {
- // @codingStandardsIgnoreEnd
$item = $this->current();
$status = ChangeTags::updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
null, $item->getId(), null, $params, $reason, $user );
diff --git a/www/wiki/includes/clientpool/RedisConnectionPool.php b/www/wiki/includes/clientpool/RedisConnectionPool.php
deleted file mode 100644
index a9bc5937..00000000
--- a/www/wiki/includes/clientpool/RedisConnectionPool.php
+++ /dev/null
@@ -1,581 +0,0 @@
-<?php
-/**
- * Redis client connection pooling manager.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @defgroup Redis Redis
- * @author Aaron Schulz
- */
-
-use MediaWiki\Logger\LoggerFactory;
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerInterface;
-
-/**
- * Helper class to manage Redis connections.
- *
- * This can be used to get handle wrappers that free the handle when the wrapper
- * leaves scope. The maximum number of free handles (connections) is configurable.
- * This provides an easy way to cache connection handles that may also have state,
- * such as a handle does between multi() and exec(), and without hoarding connections.
- * The wrappers use PHP magic methods so that calling functions on them calls the
- * function of the actual Redis object handle.
- *
- * @ingroup Redis
- * @since 1.21
- */
-class RedisConnectionPool implements LoggerAwareInterface {
- /**
- * @name Pool settings.
- * Settings there are shared for any connection made in this pool.
- * See the singleton() method documentation for more details.
- * @{
- */
- /** @var string Connection timeout in seconds */
- protected $connectTimeout;
- /** @var string Read timeout in seconds */
- protected $readTimeout;
- /** @var string Plaintext auth password */
- protected $password;
- /** @var bool Whether connections persist */
- protected $persistent;
- /** @var int Serializer to use (Redis::SERIALIZER_*) */
- protected $serializer;
- /** @} */
-
- /** @var int Current idle pool size */
- protected $idlePoolSize = 0;
-
- /** @var array (server name => ((connection info array),...) */
- protected $connections = [];
- /** @var array (server name => UNIX timestamp) */
- protected $downServers = [];
-
- /** @var array (pool ID => RedisConnectionPool) */
- protected static $instances = [];
-
- /** integer; seconds to cache servers as "down". */
- const SERVER_DOWN_TTL = 30;
-
- /**
- * @var LoggerInterface
- */
- protected $logger;
-
- /**
- * @param array $options
- * @throws Exception
- */
- protected function __construct( array $options ) {
- if ( !class_exists( 'Redis' ) ) {
- throw new Exception( __CLASS__ . ' requires a Redis client library. ' .
- 'See https://www.mediawiki.org/wiki/Redis#Setup' );
- }
- if ( isset( $options['logger'] ) ) {
- $this->setLogger( $options['logger'] );
- } else {
- $this->setLogger( LoggerFactory::getInstance( 'redis' ) );
- }
- $this->connectTimeout = $options['connectTimeout'];
- $this->readTimeout = $options['readTimeout'];
- $this->persistent = $options['persistent'];
- $this->password = $options['password'];
- if ( !isset( $options['serializer'] ) || $options['serializer'] === 'php' ) {
- $this->serializer = Redis::SERIALIZER_PHP;
- } elseif ( $options['serializer'] === 'igbinary' ) {
- $this->serializer = Redis::SERIALIZER_IGBINARY;
- } elseif ( $options['serializer'] === 'none' ) {
- $this->serializer = Redis::SERIALIZER_NONE;
- } else {
- throw new InvalidArgumentException( "Invalid serializer specified." );
- }
- }
-
- /**
- * @param LoggerInterface $logger
- * @return null
- */
- public function setLogger( LoggerInterface $logger ) {
- $this->logger = $logger;
- }
-
- /**
- * @param array $options
- * @return array
- */
- protected static function applyDefaultConfig( array $options ) {
- if ( !isset( $options['connectTimeout'] ) ) {
- $options['connectTimeout'] = 1;
- }
- if ( !isset( $options['readTimeout'] ) ) {
- $options['readTimeout'] = 1;
- }
- if ( !isset( $options['persistent'] ) ) {
- $options['persistent'] = false;
- }
- if ( !isset( $options['password'] ) ) {
- $options['password'] = null;
- }
-
- return $options;
- }
-
- /**
- * @param array $options
- * $options include:
- * - connectTimeout : The timeout for new connections, in seconds.
- * Optional, default is 1 second.
- * - readTimeout : The timeout for operation reads, in seconds.
- * Commands like BLPOP can fail if told to wait longer than this.
- * Optional, default is 1 second.
- * - persistent : Set this to true to allow connections to persist across
- * multiple web requests. False by default.
- * - password : The authentication password, will be sent to Redis in clear text.
- * Optional, if it is unspecified, no AUTH command will be sent.
- * - serializer : Set to "php", "igbinary", or "none". Default is "php".
- * @return RedisConnectionPool
- */
- public static function singleton( array $options ) {
- $options = self::applyDefaultConfig( $options );
- // Map the options to a unique hash...
- ksort( $options ); // normalize to avoid pool fragmentation
- $id = sha1( serialize( $options ) );
- // Initialize the object at the hash as needed...
- if ( !isset( self::$instances[$id] ) ) {
- self::$instances[$id] = new self( $options );
- LoggerFactory::getInstance( 'redis' )->debug(
- "Creating a new " . __CLASS__ . " instance with id $id."
- );
- }
-
- return self::$instances[$id];
- }
-
- /**
- * Destroy all singleton() instances
- * @since 1.27
- */
- public static function destroySingletons() {
- self::$instances = [];
- }
-
- /**
- * Get a connection to a redis server. Based on code in RedisBagOStuff.php.
- *
- * @param string $server A hostname/port combination or the absolute path of a UNIX socket.
- * If a hostname is specified but no port, port 6379 will be used.
- * @return RedisConnRef|bool Returns false on failure
- * @throws MWException
- */
- public function getConnection( $server ) {
- // Check the listing "dead" servers which have had a connection errors.
- // Servers are marked dead for a limited period of time, to
- // avoid excessive overhead from repeated connection timeouts.
- if ( isset( $this->downServers[$server] ) ) {
- $now = time();
- if ( $now > $this->downServers[$server] ) {
- // Dead time expired
- unset( $this->downServers[$server] );
- } else {
- // Server is dead
- $this->logger->debug(
- 'Server "{redis_server}" is marked down for another ' .
- ( $this->downServers[$server] - $now ) . 'seconds',
- [ 'redis_server' => $server ]
- );
-
- return false;
- }
- }
-
- // Check if a connection is already free for use
- if ( isset( $this->connections[$server] ) ) {
- foreach ( $this->connections[$server] as &$connection ) {
- if ( $connection['free'] ) {
- $connection['free'] = false;
- --$this->idlePoolSize;
-
- return new RedisConnRef(
- $this, $server, $connection['conn'], $this->logger
- );
- }
- }
- }
-
- if ( substr( $server, 0, 1 ) === '/' ) {
- // UNIX domain socket
- // These are required by the redis extension to start with a slash, but
- // we still need to set the port to a special value to make it work.
- $host = $server;
- $port = 0;
- } else {
- // TCP connection
- $hostPort = IP::splitHostAndPort( $server );
- if ( !$server || !$hostPort ) {
- throw new InvalidArgumentException(
- __CLASS__ . ": invalid configured server \"$server\""
- );
- }
- list( $host, $port ) = $hostPort;
- if ( $port === false ) {
- $port = 6379;
- }
- }
-
- $conn = new Redis();
- try {
- if ( $this->persistent ) {
- $result = $conn->pconnect( $host, $port, $this->connectTimeout );
- } else {
- $result = $conn->connect( $host, $port, $this->connectTimeout );
- }
- if ( !$result ) {
- $this->logger->error(
- 'Could not connect to server "{redis_server}"',
- [ 'redis_server' => $server ]
- );
- // Mark server down for some time to avoid further timeouts
- $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
-
- return false;
- }
- if ( $this->password !== null ) {
- if ( !$conn->auth( $this->password ) ) {
- $this->logger->error(
- 'Authentication error connecting to "{redis_server}"',
- [ 'redis_server' => $server ]
- );
- }
- }
- } catch ( RedisException $e ) {
- $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
- $this->logger->error(
- 'Redis exception connecting to "{redis_server}"',
- [
- 'redis_server' => $server,
- 'exception' => $e,
- ]
- );
-
- return false;
- }
-
- if ( $conn ) {
- $conn->setOption( Redis::OPT_READ_TIMEOUT, $this->readTimeout );
- $conn->setOption( Redis::OPT_SERIALIZER, $this->serializer );
- $this->connections[$server][] = [ 'conn' => $conn, 'free' => false ];
-
- return new RedisConnRef( $this, $server, $conn, $this->logger );
- } else {
- return false;
- }
- }
-
- /**
- * Mark a connection to a server as free to return to the pool
- *
- * @param string $server
- * @param Redis $conn
- * @return bool
- */
- public function freeConnection( $server, Redis $conn ) {
- $found = false;
-
- foreach ( $this->connections[$server] as &$connection ) {
- if ( $connection['conn'] === $conn && !$connection['free'] ) {
- $connection['free'] = true;
- ++$this->idlePoolSize;
- break;
- }
- }
-
- $this->closeExcessIdleConections();
-
- return $found;
- }
-
- /**
- * Close any extra idle connections if there are more than the limit
- */
- protected function closeExcessIdleConections() {
- if ( $this->idlePoolSize <= count( $this->connections ) ) {
- return; // nothing to do (no more connections than servers)
- }
-
- foreach ( $this->connections as &$serverConnections ) {
- foreach ( $serverConnections as $key => &$connection ) {
- if ( $connection['free'] ) {
- unset( $serverConnections[$key] );
- if ( --$this->idlePoolSize <= count( $this->connections ) ) {
- return; // done (no more connections than servers)
- }
- }
- }
- }
- }
-
- /**
- * The redis extension throws an exception in response to various read, write
- * and protocol errors. Sometimes it also closes the connection, sometimes
- * not. The safest response for us is to explicitly destroy the connection
- * object and let it be reopened during the next request.
- *
- * @param string $server
- * @param RedisConnRef $cref
- * @param RedisException $e
- * @deprecated since 1.23
- */
- public function handleException( $server, RedisConnRef $cref, RedisException $e ) {
- $this->handleError( $cref, $e );
- }
-
- /**
- * The redis extension throws an exception in response to various read, write
- * and protocol errors. Sometimes it also closes the connection, sometimes
- * not. The safest response for us is to explicitly destroy the connection
- * object and let it be reopened during the next request.
- *
- * @param RedisConnRef $cref
- * @param RedisException $e
- */
- public function handleError( RedisConnRef $cref, RedisException $e ) {
- $server = $cref->getServer();
- $this->logger->error(
- 'Redis exception on server "{redis_server}"',
- [
- 'redis_server' => $server,
- 'exception' => $e,
- ]
- );
- foreach ( $this->connections[$server] as $key => $connection ) {
- if ( $cref->isConnIdentical( $connection['conn'] ) ) {
- $this->idlePoolSize -= $connection['free'] ? 1 : 0;
- unset( $this->connections[$server][$key] );
- break;
- }
- }
- }
-
- /**
- * Re-send an AUTH request to the redis server (useful after disconnects).
- *
- * This works around an upstream bug in phpredis. phpredis hides disconnects by transparently
- * reconnecting, but it neglects to re-authenticate the new connection. To the user of the
- * phpredis client API this manifests as a seemingly random tendency of connections to lose
- * their authentication status.
- *
- * This method is for internal use only.
- *
- * @see https://github.com/nicolasff/phpredis/issues/403
- *
- * @param string $server
- * @param Redis $conn
- * @return bool Success
- */
- public function reauthenticateConnection( $server, Redis $conn ) {
- if ( $this->password !== null ) {
- if ( !$conn->auth( $this->password ) ) {
- $this->logger->error(
- 'Authentication error connecting to "{redis_server}"',
- [ 'redis_server' => $server ]
- );
-
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Adjust or reset the connection handle read timeout value
- *
- * @param Redis $conn
- * @param int $timeout Optional
- */
- public function resetTimeout( Redis $conn, $timeout = null ) {
- $conn->setOption( Redis::OPT_READ_TIMEOUT, $timeout ?: $this->readTimeout );
- }
-
- /**
- * Make sure connections are closed for sanity
- */
- function __destruct() {
- foreach ( $this->connections as $server => &$serverConnections ) {
- foreach ( $serverConnections as $key => &$connection ) {
- $connection['conn']->close();
- }
- }
- }
-}
-
-/**
- * Helper class to handle automatically marking connectons as reusable (via RAII pattern)
- *
- * This class simply wraps the Redis class and can be used the same way
- *
- * @ingroup Redis
- * @since 1.21
- */
-class RedisConnRef {
- /** @var RedisConnectionPool */
- protected $pool;
- /** @var Redis */
- protected $conn;
-
- protected $server; // string
- protected $lastError; // string
-
- /**
- * @var LoggerInterface
- */
- protected $logger;
-
- /**
- * @param RedisConnectionPool $pool
- * @param string $server
- * @param Redis $conn
- * @param LoggerInterface $logger
- */
- public function __construct(
- RedisConnectionPool $pool, $server, Redis $conn, LoggerInterface $logger
- ) {
- $this->pool = $pool;
- $this->server = $server;
- $this->conn = $conn;
- $this->logger = $logger;
- }
-
- /**
- * @return string
- * @since 1.23
- */
- public function getServer() {
- return $this->server;
- }
-
- public function getLastError() {
- return $this->lastError;
- }
-
- public function clearLastError() {
- $this->lastError = null;
- }
-
- public function __call( $name, $arguments ) {
- $conn = $this->conn; // convenience
-
- // Work around https://github.com/nicolasff/phpredis/issues/70
- $lname = strtolower( $name );
- if ( ( $lname === 'blpop' || $lname == 'brpop' )
- && is_array( $arguments[0] ) && isset( $arguments[1] )
- ) {
- $this->pool->resetTimeout( $conn, $arguments[1] + 1 );
- } elseif ( $lname === 'brpoplpush' && isset( $arguments[2] ) ) {
- $this->pool->resetTimeout( $conn, $arguments[2] + 1 );
- }
-
- $conn->clearLastError();
- try {
- $res = call_user_func_array( [ $conn, $name ], $arguments );
- if ( preg_match( '/^ERR operation not permitted\b/', $conn->getLastError() ) ) {
- $this->pool->reauthenticateConnection( $this->server, $conn );
- $conn->clearLastError();
- $res = call_user_func_array( [ $conn, $name ], $arguments );
- $this->logger->info(
- "Used automatic re-authentication for method '$name'.",
- [ 'redis_server' => $this->server ]
- );
- }
- } catch ( RedisException $e ) {
- $this->pool->resetTimeout( $conn ); // restore
- throw $e;
- }
-
- $this->lastError = $conn->getLastError() ?: $this->lastError;
-
- $this->pool->resetTimeout( $conn ); // restore
-
- return $res;
- }
-
- /**
- * @param string $script
- * @param array $params
- * @param int $numKeys
- * @return mixed
- * @throws RedisException
- */
- public function luaEval( $script, array $params, $numKeys ) {
- $sha1 = sha1( $script ); // 40 char hex
- $conn = $this->conn; // convenience
- $server = $this->server; // convenience
-
- // Try to run the server-side cached copy of the script
- $conn->clearLastError();
- $res = $conn->evalSha( $sha1, $params, $numKeys );
- // If we got a permission error reply that means that (a) we are not in
- // multi()/pipeline() and (b) some connection problem likely occurred. If
- // the password the client gave was just wrong, an exception should have
- // been thrown back in getConnection() previously.
- if ( preg_match( '/^ERR operation not permitted\b/', $conn->getLastError() ) ) {
- $this->pool->reauthenticateConnection( $server, $conn );
- $conn->clearLastError();
- $res = $conn->eval( $script, $params, $numKeys );
- $this->logger->info(
- "Used automatic re-authentication for Lua script '$sha1'.",
- [ 'redis_server' => $server ]
- );
- }
- // If the script is not in cache, use eval() to retry and cache it
- if ( preg_match( '/^NOSCRIPT/', $conn->getLastError() ) ) {
- $conn->clearLastError();
- $res = $conn->eval( $script, $params, $numKeys );
- $this->logger->info(
- "Used eval() for Lua script '$sha1'.",
- [ 'redis_server' => $server ]
- );
- }
-
- if ( $conn->getLastError() ) { // script bug?
- $this->logger->error(
- 'Lua script error on server "{redis_server}": {lua_error}',
- [
- 'redis_server' => $server,
- 'lua_error' => $conn->getLastError()
- ]
- );
- }
-
- $this->lastError = $conn->getLastError() ?: $this->lastError;
-
- return $res;
- }
-
- /**
- * @param Redis $conn
- * @return bool
- */
- public function isConnIdentical( Redis $conn ) {
- return $this->conn === $conn;
- }
-
- function __destruct() {
- $this->pool->freeConnection( $this->server, $this->conn );
- }
-}
diff --git a/www/wiki/includes/clientpool/SquidPurgeClient.php b/www/wiki/includes/clientpool/SquidPurgeClient.php
index f454bd4c..33888605 100644
--- a/www/wiki/includes/clientpool/SquidPurgeClient.php
+++ b/www/wiki/includes/clientpool/SquidPurgeClient.php
@@ -95,9 +95,9 @@ class SquidPurgeClient {
}
$this->socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
socket_set_nonblock( $this->socket );
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ok = socket_connect( $this->socket, $ip, $this->port );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$ok ) {
$error = socket_last_error( $this->socket );
if ( $error !== self::EINPROGRESS ) {
@@ -153,12 +153,12 @@ class SquidPurgeClient {
} elseif ( IP::isIPv6( $this->host ) ) {
throw new MWException( '$wgSquidServers does not support IPv6' );
} else {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$this->ip = gethostbyname( $this->host );
if ( $this->ip === $this->host ) {
$this->ip = false;
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
}
return $this->ip;
@@ -178,11 +178,11 @@ class SquidPurgeClient {
*/
public function close() {
if ( $this->socket ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
socket_set_block( $this->socket );
socket_shutdown( $this->socket );
socket_close( $this->socket );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
$this->socket = null;
$this->readBuffer = '';
@@ -252,9 +252,9 @@ class SquidPurgeClient {
$buf = substr( $this->writeBuffer, 0, self::BUFFER_SIZE );
$flags = 0;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$bytesSent = socket_send( $socket, $buf, strlen( $buf ), $flags );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $bytesSent === false ) {
$error = socket_last_error( $socket );
@@ -278,9 +278,9 @@ class SquidPurgeClient {
}
$buf = '';
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$bytesRead = socket_recv( $socket, $buf, self::BUFFER_SIZE, 0 );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $bytesRead === false ) {
$error = socket_last_error( $socket );
if ( $error != self::EAGAIN && $error != self::EINTR ) {
@@ -304,40 +304,40 @@ class SquidPurgeClient {
*/
protected function processReadBuffer() {
switch ( $this->readState ) {
- case 'idle':
- return 'done';
- case 'status':
- case 'header':
- $lines = explode( "\r\n", $this->readBuffer, 2 );
- if ( count( $lines ) < 2 ) {
+ case 'idle':
return 'done';
- }
- if ( $this->readState == 'status' ) {
- $this->processStatusLine( $lines[0] );
- } else { // header
- $this->processHeaderLine( $lines[0] );
- }
- $this->readBuffer = $lines[1];
- return 'continue';
- case 'body':
- if ( $this->bodyRemaining !== null ) {
- if ( $this->bodyRemaining > strlen( $this->readBuffer ) ) {
- $this->bodyRemaining -= strlen( $this->readBuffer );
- $this->readBuffer = '';
+ case 'status':
+ case 'header':
+ $lines = explode( "\r\n", $this->readBuffer, 2 );
+ if ( count( $lines ) < 2 ) {
return 'done';
+ }
+ if ( $this->readState == 'status' ) {
+ $this->processStatusLine( $lines[0] );
+ } else { // header
+ $this->processHeaderLine( $lines[0] );
+ }
+ $this->readBuffer = $lines[1];
+ return 'continue';
+ case 'body':
+ if ( $this->bodyRemaining !== null ) {
+ if ( $this->bodyRemaining > strlen( $this->readBuffer ) ) {
+ $this->bodyRemaining -= strlen( $this->readBuffer );
+ $this->readBuffer = '';
+ return 'done';
+ } else {
+ $this->readBuffer = substr( $this->readBuffer, $this->bodyRemaining );
+ $this->bodyRemaining = 0;
+ $this->nextRequest();
+ return 'continue';
+ }
} else {
- $this->readBuffer = substr( $this->readBuffer, $this->bodyRemaining );
- $this->bodyRemaining = 0;
- $this->nextRequest();
- return 'continue';
+ // No content length, read all data to EOF
+ $this->readBuffer = '';
+ return 'done';
}
- } else {
- // No content length, read all data to EOF
- $this->readBuffer = '';
- return 'done';
- }
- default:
- throw new MWException( __METHOD__ . ': unexpected state' );
+ default:
+ throw new MWException( __METHOD__ . ': unexpected state' );
}
}
diff --git a/www/wiki/includes/clientpool/SquidPurgeClientPool.php b/www/wiki/includes/clientpool/SquidPurgeClientPool.php
index 7b327d65..f6109f1d 100644
--- a/www/wiki/includes/clientpool/SquidPurgeClientPool.php
+++ b/www/wiki/includes/clientpool/SquidPurgeClientPool.php
@@ -66,9 +66,9 @@ class SquidPurgeClientPool {
}
$exceptSockets = null;
$timeout = min( $startTime + $this->timeout - microtime( true ), 1 );
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$numReady = socket_select( $readSockets, $writeSockets, $exceptSockets, $timeout );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $numReady === false ) {
wfDebugLog( 'squid', __METHOD__ . ': Error in stream_select: ' .
socket_strerror( socket_last_error() ) . "\n" );
diff --git a/www/wiki/includes/collation/AbkhazUppercaseCollation.php b/www/wiki/includes/collation/AbkhazUppercaseCollation.php
new file mode 100644
index 00000000..e0ea237f
--- /dev/null
+++ b/www/wiki/includes/collation/AbkhazUppercaseCollation.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @since 1.31
+ *
+ * @file
+ */
+
+class AbkhazUppercaseCollation extends CustomUppercaseCollation {
+
+ public function __construct() {
+ parent::__construct( [
+ 'А',
+ 'Б',
+ 'В',
+ 'Г',
+ 'Гь',
+ 'Гә',
+ 'Ҕ',
+ 'Ҕь',
+ 'Ҕә',
+ 'Д',
+ 'Дә',
+ 'Е',
+ 'Ж',
+ 'Жь',
+ 'Жә',
+ 'З',
+ 'Ӡ',
+ 'Ӡә',
+ 'И',
+ 'К',
+ 'Кь',
+ 'Кә',
+ 'Қ',
+ 'Қь',
+ 'Қә',
+ 'Ҟ',
+ 'Ҟь',
+ 'Ҟә',
+ 'Л',
+ 'М',
+ 'Н',
+ 'О',
+ 'П',
+ 'Ҧ',
+ 'Р',
+ 'С',
+ 'Т',
+ 'Тә',
+ 'Ҭ',
+ 'Ҭә',
+ 'У',
+ 'Ф',
+ 'Х',
+ 'Хь',
+ 'Хә',
+ 'Ҳ',
+ 'Ҳә',
+ 'Ц',
+ 'Цә',
+ 'Ҵ',
+ 'Ҵә',
+ 'Ч',
+ 'Ҷ',
+ 'Ҽ',
+ 'Ҿ',
+ 'Ш',
+ 'Шь',
+ 'Шә',
+ 'Ы',
+ 'Ҩ',
+ 'Џ',
+ 'Џь',
+ 'ь',
+ 'ә',
+ ], Language::factory( 'ab' ) );
+ }
+}
diff --git a/www/wiki/includes/collation/Collation.php b/www/wiki/includes/collation/Collation.php
index d009168d..30cae5a8 100644
--- a/www/wiki/includes/collation/Collation.php
+++ b/www/wiki/includes/collation/Collation.php
@@ -65,8 +65,12 @@ abstract class Collation {
return new CollationEt;
case 'xx-uca-fa':
return new CollationFa;
+ case 'uppercase-ab':
+ return new AbkhazUppercaseCollation;
case 'uppercase-ba':
return new BashkirUppercaseCollation;
+ case 'uppercase-se':
+ return new NorthernSamiUppercaseCollation;
default:
$match = [];
if ( preg_match( '/^uca-([A-Za-z@=-]+)$/', $collationName, $match ) ) {
diff --git a/www/wiki/includes/collation/CustomUppercaseCollation.php b/www/wiki/includes/collation/CustomUppercaseCollation.php
index 301972d9..170d5c2c 100644
--- a/www/wiki/includes/collation/CustomUppercaseCollation.php
+++ b/www/wiki/includes/collation/CustomUppercaseCollation.php
@@ -32,6 +32,7 @@
* conflicts with other people using private use area)
*
* This does not support fancy things like secondary differences, etc.
+ * (It supports digraphs, trigraphs etc. though.)
*
* It is expected most people will subclass this and just override the
* constructor to hard-code an alphabet.
@@ -45,25 +46,30 @@ class CustomUppercaseCollation extends NumericUppercaseCollation {
private $puaSubset;
/**
- * @note This assumes $alphabet does not contain U+F3000-U+F303F
+ * @note This assumes $alphabet does not contain U+F3000-U+F3FFF
*
* @param array $alphabet Sorted array of uppercase characters.
* @param Language $lang What language for number sorting.
*/
public function __construct( array $alphabet, Language $lang ) {
- // It'd be trivial to extend this past 64, you'd just
- // need a bit of bit-fiddling. Doesn't seem necessary right
- // now.
- if ( count( $alphabet ) < 1 || count( $alphabet ) >= 64 ) {
- throw new UnexpectedValueException( "Alphabet must be < 64 items" );
+ if ( count( $alphabet ) < 1 || count( $alphabet ) >= 4096 ) {
+ throw new UnexpectedValueException( "Alphabet must be < 4096 items" );
}
- $this->alphabet = $alphabet;
+ $this->firstLetters = $alphabet;
+ // For digraphs, only the first letter is capitalized in input
+ $this->alphabet = array_map( [ $lang, 'uc' ], $alphabet );
$this->puaSubset = [];
$len = count( $alphabet );
for ( $i = 0; $i < $len; $i++ ) {
- $this->puaSubset[] = "\xF3\xB3\x80" . chr( $i + 128 );
+ $this->puaSubset[] = "\xF3\xB3" . chr( floor( $i / 64 ) + 128 ) . chr( ( $i % 64 ) + 128 );
}
+
+ // Sort these arrays so that any trigraphs, digraphs etc. are first
+ // (and they get replaced first in convertToPua()).
+ $lengths = array_map( 'mb_strlen', $this->alphabet );
+ array_multisort( $lengths, SORT_DESC, $this->firstLetters, $this->alphabet, $this->puaSubset );
+
parent::__construct( $lang );
}
@@ -76,12 +82,17 @@ class CustomUppercaseCollation extends NumericUppercaseCollation {
}
public function getFirstLetter( $string ) {
- // In case a title has a PUA code in it, make it sort
- // under the header for the character it would replace
- // to avoid inconsistent behaviour. This class mostly
- // assumes that people will not use PUA codes.
- return parent::getFirstLetter(
- str_replace( $this->puaSubset, $this->alphabet, $string )
- );
+ $sortkey = $this->getSortKey( $string );
+
+ // In case a title begins with a character from our alphabet, return the corresponding
+ // first-letter. (This also happens if the title has a corresponding PUA code in it, to avoid
+ // inconsistent behaviour. This class mostly assumes that people will not use PUA codes.)
+ $index = array_search( substr( $sortkey, 0, 4 ), $this->puaSubset );
+ if ( $index !== false ) {
+ return $this->firstLetters[ $index ];
+ }
+
+ // String begins with a character outside of our alphabet, fall back
+ return parent::getFirstLetter( $string );
}
}
diff --git a/www/wiki/includes/collation/IcuCollation.php b/www/wiki/includes/collation/IcuCollation.php
index efda5963..9ac81ae0 100644
--- a/www/wiki/includes/collation/IcuCollation.php
+++ b/www/wiki/includes/collation/IcuCollation.php
@@ -384,9 +384,17 @@ class IcuCollation extends Collation {
foreach ( $letters as $letter ) {
$key = $this->getPrimarySortKey( $letter );
if ( isset( $letterMap[$key] ) ) {
- // Primary collision
- // Keep whichever one sorts first in the main collator
- if ( $this->mainCollator->compare( $letter, $letterMap[$key] ) < 0 ) {
+ // Primary collision (two characters with the same sort position).
+ // Keep whichever one sorts first in the main collator.
+ $comp = $this->mainCollator->compare( $letter, $letterMap[$key] );
+ wfDebug( "Primary collision '$letter' '{$letterMap[$key]}' (comparison: $comp)\n" );
+ // If that also has a collision, use codepoint as a tiebreaker.
+ if ( $comp === 0 ) {
+ // TODO Use <=> operator when PHP 7 is allowed.
+ $comp = UtfNormal\Utils::utf8ToCodepoint( $letter ) -
+ UtfNormal\Utils::utf8ToCodepoint( $letterMap[$key] );
+ }
+ if ( $comp < 0 ) {
$letterMap[$key] = $letter;
}
} else {
@@ -492,7 +500,6 @@ class IcuCollation extends Collation {
}
/**
- * @param string $index
* @return string
* @since 1.16.3
*/
@@ -551,6 +558,8 @@ class IcuCollation extends Collation {
$versionPrefix = substr( $icuVersion, 0, 3 );
// Source: http://site.icu-project.org/download
$map = [
+ '59.' => '9.0',
+ '58.' => '9.0',
'57.' => '8.0',
'56.' => '8.0',
'55.' => '7.0',
diff --git a/www/wiki/includes/collation/NorthernSamiUppercaseCollation.php b/www/wiki/includes/collation/NorthernSamiUppercaseCollation.php
new file mode 100644
index 00000000..d373749e
--- /dev/null
+++ b/www/wiki/includes/collation/NorthernSamiUppercaseCollation.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @since 1.31
+ *
+ * @file
+ */
+
+/**
+ * Temporary workaround for incorrect collation of Northern Sami
+ * language ('se') in Wikimedia servers (see bug T181503).
+ *
+ * When the ICU's 'se' collation has been included in PHP-intl and Wikimedia
+ * servers updated to that new version of PHP, this file should be deleted
+ * and the collation for 'se' set to 'uca-se'.
+ *
+ * @since 1.31
+ */
+
+class NorthernSamiUppercaseCollation extends CustomUppercaseCollation {
+
+ public function __construct() {
+ parent::__construct( [
+ 'A',
+ 'Á',
+ 'B',
+ 'C',
+ 'Č',
+ 'Ʒ', // Not part of modern alphabet, but part of ICU
+ 'Ǯ', // Not part of modern alphabet, but part of ICU
+ 'D',
+ 'Đ',
+ 'E',
+ 'F',
+ 'G',
+ 'Ǧ', // Not part of modern alphabet, but part of ICU
+ 'Ǥ', // Not part of modern alphabet, but part of ICU
+ 'H',
+ 'I',
+ 'J',
+ 'K',
+ 'Ǩ', // Not part of modern alphabet, but part of ICU
+ 'L',
+ 'M',
+ 'N',
+ 'Ŋ',
+ 'O',
+ 'P',
+ 'Q',
+ 'R',
+ 'S',
+ 'Š',
+ 'T',
+ 'Ŧ',
+ 'U',
+ 'V',
+ 'W',
+ 'X',
+ 'Y',
+ 'Z',
+ 'Ž',
+ 'Ø', // Not part of native alphabet, but part of ICU
+ 'Æ', // Not part of native alphabet, but part of ICU
+ 'Å', // Not part of native alphabet, but part of ICU
+ 'Ä', // Not part of native alphabet, but part of ICU
+ 'Ö', // Not part of native alphabet, but part of ICU
+ ], Language::factory( 'se' ) );
+ }
+}
diff --git a/www/wiki/includes/collation/NumericUppercaseCollation.php b/www/wiki/includes/collation/NumericUppercaseCollation.php
index da78a051..4a2d776f 100644
--- a/www/wiki/includes/collation/NumericUppercaseCollation.php
+++ b/www/wiki/includes/collation/NumericUppercaseCollation.php
@@ -25,7 +25,7 @@
* or pretty-formatted numbers may be unexpected.
*
* Digits will be based on the wiki's content language settings. If
- * you change the content langauge of a wiki you will need to run
+ * you change the content language of a wiki you will need to run
* updateCollation.php --force. Only English (ASCII 0-9) and the
* localized version will be counted. Localized digits from other languages
* or weird unicode digit equivalents (e.g. 4, 𝟜, ⓸ , ⁴, etc) will not count.
diff --git a/www/wiki/includes/compat/IPSetCompat.php b/www/wiki/includes/compat/ObjectFactory.php
index 79c60004..76462380 100644
--- a/www/wiki/includes/compat/IPSetCompat.php
+++ b/www/wiki/includes/compat/ObjectFactory.php
@@ -19,10 +19,9 @@
*/
/**
- * Backward-compatibility alias for IPSet, which was moved out
- * into an external library and namespaced.
+ * Construct objects from configuration instructions.
*
- * @deprecated since 1.26 use IPSet\IPSet directly
+ * @deprecated since 1.31, use \Wikimedia\ObjectFactory instead
*/
-class IPSet extends IPSet\IPSet {
+class ObjectFactory extends \Wikimedia\ObjectFactory {
}
diff --git a/www/wiki/includes/compat/Timestamp.php b/www/wiki/includes/compat/Timestamp.php
index bd254327..63b87ae1 100644
--- a/www/wiki/includes/compat/Timestamp.php
+++ b/www/wiki/includes/compat/Timestamp.php
@@ -7,12 +7,12 @@
// loading context. This file will then register the alias and, as class_alias() does
// by default, it will trigger a plain autoload for the destination class.
-// The below uses string concatenation for the alias to avoid being seen by ClassCollector,
-// which would insist on adding it to autoload.php, after which AutoLoaderTest will
+// The below uses a namespaced class reference, to to avoid being seen by ClassCollector,
+// which would otherwise add it to autoload.php, after which AutoLoaderTest will
// complain about class_alias() not being in the target class file.
/**
* @deprecated since 1.29
* @since 1.20
*/
-class_alias( Wikimedia\Timestamp\TimestampException::class, 'Timestamp' . 'Exception' );
+class_alias( Wikimedia\Timestamp\TimestampException::class, 'TimestampException' );
diff --git a/www/wiki/includes/compat/normal/UtfNormalUtil.php b/www/wiki/includes/compat/normal/UtfNormalUtil.php
index d60c8e33..0e29dd07 100644
--- a/www/wiki/includes/compat/normal/UtfNormalUtil.php
+++ b/www/wiki/includes/compat/normal/UtfNormalUtil.php
@@ -37,6 +37,7 @@ use UtfNormal\Utils;
* @deprecated since 1.25, use UtfNormal\Utils directly
*/
function codepointToUtf8( $codepoint ) {
+ wfDeprecated( __FUNCTION__, '1.25' );
return Utils::codepointToUtf8( $codepoint );
}
@@ -52,6 +53,7 @@ function codepointToUtf8( $codepoint ) {
* @deprecated since 1.25, use UtfNormal\Utils directly
*/
function hexSequenceToUtf8( $sequence ) {
+ wfDeprecated( __FUNCTION__, '1.25' );
return Utils::hexSequenceToUtf8( $sequence );
}
@@ -65,6 +67,7 @@ function hexSequenceToUtf8( $sequence ) {
* @private
*/
function utf8ToHexSequence( $str ) {
+ wfDeprecated( __FUNCTION__, '1.25' );
$buf = '';
foreach ( preg_split( '//u', $str, -1, PREG_SPLIT_NO_EMPTY ) as $cp ) {
$buf .= sprintf( '%04x ', UtfNormal\Utils::utf8ToCodepoint( $cp ) );
@@ -83,6 +86,7 @@ function utf8ToHexSequence( $str ) {
* @deprecated since 1.25, use UtfNormal\Utils directly
*/
function utf8ToCodepoint( $char ) {
+ wfDeprecated( __FUNCTION__, '1.25' );
return Utils::utf8ToCodepoint( $char );
}
@@ -95,5 +99,6 @@ function utf8ToCodepoint( $char ) {
* @deprecated since 1.25, use UtfNormal\Utils directly
*/
function escapeSingleString( $string ) {
+ wfDeprecated( __FUNCTION__, '1.25' );
return Utils::escapeSingleString( $string );
}
diff --git a/www/wiki/includes/composer/ComposerHookHandler.php b/www/wiki/includes/composer/ComposerHookHandler.php
index 2587b1d8..a1943be5 100644
--- a/www/wiki/includes/composer/ComposerHookHandler.php
+++ b/www/wiki/includes/composer/ComposerHookHandler.php
@@ -7,7 +7,7 @@ $GLOBALS['IP'] = __DIR__ . '/../../';
require_once __DIR__ . '/../AutoLoader.php';
/**
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
class ComposerHookHandler {
diff --git a/www/wiki/includes/composer/ComposerPackageModifier.php b/www/wiki/includes/composer/ComposerPackageModifier.php
index 9f603947..168336b2 100644
--- a/www/wiki/includes/composer/ComposerPackageModifier.php
+++ b/www/wiki/includes/composer/ComposerPackageModifier.php
@@ -5,7 +5,7 @@ use Composer\Package\Package;
use Composer\Semver\Constraint\Constraint;
/**
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
class ComposerPackageModifier {
diff --git a/www/wiki/includes/composer/ComposerVersionNormalizer.php b/www/wiki/includes/composer/ComposerVersionNormalizer.php
index a0d31cf2..2194bede 100644
--- a/www/wiki/includes/composer/ComposerVersionNormalizer.php
+++ b/www/wiki/includes/composer/ComposerVersionNormalizer.php
@@ -1,7 +1,7 @@
<?php
/**
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
class ComposerVersionNormalizer {
diff --git a/www/wiki/includes/config/ConfigFactory.php b/www/wiki/includes/config/ConfigFactory.php
index cd25352d..2c7afdae 100644
--- a/www/wiki/includes/config/ConfigFactory.php
+++ b/www/wiki/includes/config/ConfigFactory.php
@@ -99,13 +99,18 @@ class ConfigFactory implements SalvageableService {
* Will override if it's already registered.
* Use "*" for $name to provide a fallback config for all unknown names.
* @param string $name
- * @param callable|Config $callback A factory callabck that takes this ConfigFactory
+ * @param callable|Config $callback A factory callback that takes this ConfigFactory
* as an argument and returns a Config instance, or an existing Config instance.
* @throws InvalidArgumentException If an invalid callback is provided
*/
public function register( $name, $callback ) {
if ( !is_callable( $callback ) && !( $callback instanceof Config ) ) {
- throw new InvalidArgumentException( 'Invalid callback provided' );
+ if ( is_array( $callback ) ) {
+ $callback = '[ ' . implode( ', ', $callback ) . ' ]';
+ } elseif ( is_object( $callback ) ) {
+ $callback = 'instanceof ' . get_class( $callback );
+ }
+ throw new InvalidArgumentException( 'Invalid callback \'' . $callback . '\' provided' );
}
unset( $this->configs[$name] );
diff --git a/www/wiki/includes/config/EtcdConfig.php b/www/wiki/includes/config/EtcdConfig.php
index 0ec21cb9..7020159f 100644
--- a/www/wiki/includes/config/EtcdConfig.php
+++ b/www/wiki/includes/config/EtcdConfig.php
@@ -20,6 +20,7 @@
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
+use Wikimedia\ObjectFactory;
use Wikimedia\WaitConditionLoop;
/**
@@ -118,6 +119,11 @@ class EtcdConfig implements Config, LoggerAwareInterface {
return $this->procCache['config'][$name];
}
+ public function getModifiedIndex() {
+ $this->load();
+ return $this->procCache['modifiedIndex'];
+ }
+
/**
* @throws ConfigException
*/
@@ -150,13 +156,17 @@ class EtcdConfig implements Config, LoggerAwareInterface {
// refresh the cache from etcd, using a mutex to reduce stampedes...
if ( $this->srvCache->lock( $key, 0, $this->baseCacheTTL ) ) {
try {
- list( $config, $error, $retry ) = $this->fetchAllFromEtcd();
- if ( is_array( $config ) ) {
+ $etcdResponse = $this->fetchAllFromEtcd();
+ $error = $etcdResponse['error'];
+ if ( is_array( $etcdResponse['config'] ) ) {
// Avoid having all servers expire cache keys at the same time
$expiry = microtime( true ) + $this->baseCacheTTL;
$expiry += mt_rand( 0, 1e6 ) / 1e6 * $this->skewCacheTTL;
-
- $data = [ 'config' => $config, 'expires' => $expiry ];
+ $data = [
+ 'config' => $etcdResponse['config'],
+ 'expires' => $expiry,
+ 'modifiedIndex' => $etcdResponse['modifiedIndex']
+ ];
$this->srvCache->set( $key, $data, BagOStuff::TTL_INDEFINITE );
$this->logger->info( "Refreshed stale etcd configuration cache." );
@@ -164,7 +174,7 @@ class EtcdConfig implements Config, LoggerAwareInterface {
return WaitConditionLoop::CONDITION_REACHED;
} else {
$this->logger->error( "Failed to fetch configuration: $error" );
- if ( !$retry ) {
+ if ( !$etcdResponse['retry'] ) {
// Fail fast since the error is likely to keep happening
return WaitConditionLoop::CONDITION_FAILED;
}
@@ -194,9 +204,10 @@ class EtcdConfig implements Config, LoggerAwareInterface {
}
/**
- * @return array (config array or null, error string, allow retries)
+ * @return array (containing the keys config, error, retry, modifiedIndex)
*/
public function fetchAllFromEtcd() {
+ // TODO: inject DnsSrvDiscoverer in order to be able to test this method
$dsd = new DnsSrvDiscoverer( $this->host );
$servers = $dsd->getServers();
if ( !$servers ) {
@@ -208,8 +219,8 @@ class EtcdConfig implements Config, LoggerAwareInterface {
$server = $dsd->pickServer( $servers );
$host = IP::combineHostAndPort( $server['target'], $server['port'] );
// Try to load the config from this particular server
- list( $config, $error, $retry ) = $this->fetchAllFromEtcdServer( $host );
- if ( is_array( $config ) || !$retry ) {
+ $response = $this->fetchAllFromEtcdServer( $host );
+ if ( is_array( $response['config'] ) || $response['retry'] ) {
break;
}
@@ -217,12 +228,12 @@ class EtcdConfig implements Config, LoggerAwareInterface {
$servers = $dsd->removeServer( $server, $servers );
} while ( $servers );
- return [ $config, $error, $retry ];
+ return $response;
}
/**
* @param string $address Host and port
- * @return array (config array or null, error string, whether to allow retries)
+ * @return array (containing the keys config, error, retry, modifiedIndex)
*/
protected function fetchAllFromEtcdServer( $address ) {
// Retrieve all the values under the MediaWiki config directory
@@ -232,19 +243,21 @@ class EtcdConfig implements Config, LoggerAwareInterface {
'headers' => [ 'content-type' => 'application/json' ]
] );
+ $response = [ 'config' => null, 'error' => null, 'retry' => false, 'modifiedIndex' => 0 ];
+
static $terminalCodes = [ 404 => true ];
if ( $rcode < 200 || $rcode > 399 ) {
- return [
- null,
- strlen( $rerr ) ? $rerr : "HTTP $rcode ($rdesc)",
- empty( $terminalCodes[$rcode] )
- ];
+ $response['error'] = strlen( $rerr ) ? $rerr : "HTTP $rcode ($rdesc)";
+ $response['retry'] = empty( $terminalCodes[$rcode] );
+ return $response;
}
+
try {
- return [ $this->parseResponse( $rbody ), null, false ];
+ $parsedResponse = $this->parseResponse( $rbody );
} catch ( EtcdConfigParseError $e ) {
- return [ null, $e->getMessage(), false ];
+ $parsedResponse = [ 'error' => $e->getMessage() ];
}
+ return array_merge( $response, $parsedResponse );
}
/**
@@ -263,8 +276,8 @@ class EtcdConfig implements Config, LoggerAwareInterface {
"Unexpected JSON response: Missing or invalid node at top level." );
}
$config = [];
- $this->parseDirectory( '', $info['node'], $config );
- return $config;
+ $lastModifiedIndex = $this->parseDirectory( '', $info['node'], $config );
+ return [ 'modifiedIndex' => $lastModifiedIndex, 'config' => $config ];
}
/**
@@ -274,8 +287,10 @@ class EtcdConfig implements Config, LoggerAwareInterface {
* @param string $dirName The relative directory name
* @param array $dirNode The decoded directory node
* @param array &$config The output array
+ * @return int lastModifiedIndex The maximum last modified index across all keys in the directory
*/
protected function parseDirectory( $dirName, $dirNode, &$config ) {
+ $lastModifiedIndex = 0;
if ( !isset( $dirNode['nodes'] ) ) {
throw new EtcdConfigParseError(
"Unexpected JSON response in dir '$dirName'; missing 'nodes' list." );
@@ -289,16 +304,19 @@ class EtcdConfig implements Config, LoggerAwareInterface {
$baseName = basename( $node['key'] );
$fullName = $dirName === '' ? $baseName : "$dirName/$baseName";
if ( !empty( $node['dir'] ) ) {
- $this->parseDirectory( $fullName, $node, $config );
+ $lastModifiedIndex = max(
+ $this->parseDirectory( $fullName, $node, $config ),
+ $lastModifiedIndex );
} else {
$value = $this->unserialize( $node['value'] );
if ( !is_array( $value ) || !array_key_exists( 'val', $value ) ) {
throw new EtcdConfigParseError( "Failed to parse value for '$fullName'." );
}
-
+ $lastModifiedIndex = max( $node['modifiedIndex'], $lastModifiedIndex );
$config[$fullName] = $value['val'];
}
}
+ return $lastModifiedIndex;
}
/**
diff --git a/www/wiki/includes/content/AbstractContent.php b/www/wiki/includes/content/AbstractContent.php
index c12d28d9..b21eadcd 100644
--- a/www/wiki/includes/content/AbstractContent.php
+++ b/www/wiki/includes/content/AbstractContent.php
@@ -426,7 +426,7 @@ abstract class AbstractContent implements Content {
* @param WikiPage $page
* @param ParserOutput|null $parserOutput
*
- * @return LinksDeletionUpdate[]
+ * @return DeferrableUpdate[]
*
* @see Content::getDeletionUpdates
*/
@@ -492,7 +492,7 @@ abstract class AbstractContent implements Content {
*
* @param Title $title Context title for parsing
* @param int|null $revId Revision ID (for {{REVISIONID}})
- * @param ParserOptions|null $options Parser options
+ * @param ParserOptions|null $options
* @param bool $generateHtml Whether or not to generate HTML
*
* @return ParserOutput Containing information derived from this content.
@@ -536,7 +536,7 @@ abstract class AbstractContent implements Content {
*
* @param Title $title Context title for parsing
* @param int|null $revId Revision ID (for {{REVISIONID}})
- * @param ParserOptions $options Parser options
+ * @param ParserOptions $options
* @param bool $generateHtml Whether or not to generate HTML
* @param ParserOutput &$output The output object to fill (reference).
*
diff --git a/www/wiki/includes/content/Content.php b/www/wiki/includes/content/Content.php
index 6a0a63bf..3e587828 100644
--- a/www/wiki/includes/content/Content.php
+++ b/www/wiki/includes/content/Content.php
@@ -483,11 +483,11 @@ interface Content {
* @since 1.21
*
* @param WikiPage $page The deleted page
- * @param ParserOutput $parserOutput Optional parser output object
+ * @param ParserOutput|null $parserOutput Optional parser output object
* for efficient access to meta-information about the content object.
* Provide if you have one handy.
*
- * @return DataUpdate[] A list of DataUpdate instances that will clean up the
+ * @return DeferrableUpdate[] A list of DeferrableUpdate instances that will clean up the
* database after deletion.
*/
public function getDeletionUpdates( WikiPage $page,
diff --git a/www/wiki/includes/content/ContentHandler.php b/www/wiki/includes/content/ContentHandler.php
index 0509e292..3cfac8f9 100644
--- a/www/wiki/includes/content/ContentHandler.php
+++ b/www/wiki/includes/content/ContentHandler.php
@@ -333,6 +333,13 @@ abstract class ContentHandler {
}
/**
+ * Clean up handlers cache.
+ */
+ public static function cleanupHandlersCache() {
+ self::$handlers = [];
+ }
+
+ /**
* Returns the localized name for a given content model.
*
* Model names are localized using system messages. Message keys
@@ -755,81 +762,189 @@ abstract class ContentHandler {
}
/**
+ * Return type of change if one exists for the given edit.
+ *
+ * @since 1.31
+ *
+ * @param Content|null $oldContent The previous text of the page.
+ * @param Content|null $newContent The submitted text of the page.
+ * @param int $flags Bit mask: a bit mask of flags submitted for the edit.
+ *
+ * @return string|null String key representing type of change, or null.
+ */
+ private function getChangeType(
+ Content $oldContent = null,
+ Content $newContent = null,
+ $flags = 0
+ ) {
+ $oldTarget = $oldContent !== null ? $oldContent->getRedirectTarget() : null;
+ $newTarget = $newContent !== null ? $newContent->getRedirectTarget() : null;
+
+ // We check for the type of change in the given edit, and return string key accordingly
+
+ // Blanking of a page
+ if ( $oldContent && $oldContent->getSize() > 0 &&
+ $newContent && $newContent->getSize() === 0
+ ) {
+ return 'blank';
+ }
+
+ // Redirects
+ if ( $newTarget ) {
+ if ( !$oldTarget ) {
+ // New redirect page (by creating new page or by changing content page)
+ return 'new-redirect';
+ } elseif ( !$newTarget->equals( $oldTarget ) ||
+ $oldTarget->getFragment() !== $newTarget->getFragment()
+ ) {
+ // Redirect target changed
+ return 'changed-redirect-target';
+ }
+ } elseif ( $oldTarget ) {
+ // Changing an existing redirect into a non-redirect
+ return 'removed-redirect';
+ }
+
+ // New page created
+ if ( $flags & EDIT_NEW && $newContent ) {
+ if ( $newContent->getSize() === 0 ) {
+ // New blank page
+ return 'newblank';
+ } else {
+ return 'newpage';
+ }
+ }
+
+ // Removing more than 90% of the page
+ if ( $oldContent && $newContent && $oldContent->getSize() > 10 * $newContent->getSize() ) {
+ return 'replace';
+ }
+
+ // Content model changed
+ if ( $oldContent && $newContent && $oldContent->getModel() !== $newContent->getModel() ) {
+ return 'contentmodelchange';
+ }
+
+ return null;
+ }
+
+ /**
* Return an applicable auto-summary if one exists for the given edit.
*
* @since 1.21
*
- * @param Content $oldContent The previous text of the page.
- * @param Content $newContent The submitted text of the page.
+ * @param Content|null $oldContent The previous text of the page.
+ * @param Content|null $newContent The submitted text of the page.
* @param int $flags Bit mask: a bit mask of flags submitted for the edit.
*
* @return string An appropriate auto-summary, or an empty string.
*/
- public function getAutosummary( Content $oldContent = null, Content $newContent = null,
- $flags ) {
- // Decide what kind of auto-summary is needed.
-
- // Redirect auto-summaries
-
- /**
- * @var $ot Title
- * @var $rt Title
- */
+ public function getAutosummary(
+ Content $oldContent = null,
+ Content $newContent = null,
+ $flags = 0
+ ) {
+ $changeType = $this->getChangeType( $oldContent, $newContent, $flags );
- $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null;
- $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null;
+ // There's no applicable auto-summary for our case, so our auto-summary is empty.
+ if ( !$changeType ) {
+ return '';
+ }
- if ( is_object( $rt ) ) {
- if ( !is_object( $ot )
- || !$rt->equals( $ot )
- || $ot->getFragment() != $rt->getFragment()
- ) {
+ // Decide what kind of auto-summary is needed.
+ switch ( $changeType ) {
+ case 'new-redirect':
+ $newTarget = $newContent->getRedirectTarget();
$truncatedtext = $newContent->getTextForSummary(
250
- strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
- - strlen( $rt->getFullText() ) );
+ - strlen( $newTarget->getFullText() )
+ );
- return wfMessage( 'autoredircomment', $rt->getFullText() )
- ->rawParams( $truncatedtext )->inContentLanguage()->text();
- }
- }
+ return wfMessage( 'autoredircomment', $newTarget->getFullText() )
+ ->plaintextParams( $truncatedtext )->inContentLanguage()->text();
+ case 'changed-redirect-target':
+ $oldTarget = $oldContent->getRedirectTarget();
+ $newTarget = $newContent->getRedirectTarget();
- // New page auto-summaries
- if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) {
- // If they're making a new article, give its text, truncated, in
- // the summary.
+ $truncatedtext = $newContent->getTextForSummary(
+ 250
+ - strlen( wfMessage( 'autosumm-changed-redirect-target' )
+ ->inContentLanguage()->text() )
+ - strlen( $oldTarget->getFullText() )
+ - strlen( $newTarget->getFullText() )
+ );
+
+ return wfMessage( 'autosumm-changed-redirect-target',
+ $oldTarget->getFullText(),
+ $newTarget->getFullText() )
+ ->rawParams( $truncatedtext )->inContentLanguage()->text();
+ case 'removed-redirect':
+ $oldTarget = $oldContent->getRedirectTarget();
+ $truncatedtext = $newContent->getTextForSummary(
+ 250
+ - strlen( wfMessage( 'autosumm-removed-redirect' )
+ ->inContentLanguage()->text() )
+ - strlen( $oldTarget->getFullText() ) );
- $truncatedtext = $newContent->getTextForSummary(
- 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) );
+ return wfMessage( 'autosumm-removed-redirect', $oldTarget->getFullText() )
+ ->rawParams( $truncatedtext )->inContentLanguage()->text();
+ case 'newpage':
+ // If they're making a new article, give its text, truncated, in the summary.
+ $truncatedtext = $newContent->getTextForSummary(
+ 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) );
- return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
- ->inContentLanguage()->text();
+ return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
+ ->inContentLanguage()->text();
+ case 'blank':
+ return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
+ case 'replace':
+ $truncatedtext = $newContent->getTextForSummary(
+ 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) );
+
+ return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
+ ->inContentLanguage()->text();
+ case 'newblank':
+ return wfMessage( 'autosumm-newblank' )->inContentLanguage()->text();
+ default:
+ return '';
}
+ }
- // Blanking auto-summaries
- if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
- return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
- } elseif ( !empty( $oldContent )
- && $oldContent->getSize() > 10 * $newContent->getSize()
- && $newContent->getSize() < 500
- ) {
- // Removing more than 90% of the article
-
- $truncatedtext = $newContent->getTextForSummary(
- 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) );
+ /**
+ * Return an applicable tag if one exists for the given edit or return null.
+ *
+ * @since 1.31
+ *
+ * @param Content|null $oldContent The previous text of the page.
+ * @param Content|null $newContent The submitted text of the page.
+ * @param int $flags Bit mask: a bit mask of flags submitted for the edit.
+ *
+ * @return string|null An appropriate tag, or null.
+ */
+ public function getChangeTag(
+ Content $oldContent = null,
+ Content $newContent = null,
+ $flags = 0
+ ) {
+ $changeType = $this->getChangeType( $oldContent, $newContent, $flags );
- return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
- ->inContentLanguage()->text();
+ // There's no applicable tag for this change.
+ if ( !$changeType ) {
+ return null;
}
- // New blank article auto-summary
- if ( $flags & EDIT_NEW && $newContent->isEmpty() ) {
- return wfMessage( 'autosumm-newblank' )->inContentLanguage()->text();
+ // Core tags use the same keys as ones returned from $this->getChangeType()
+ // but prefixed with pseudo namespace 'mw-', so we add the prefix before checking
+ // if this type of change should be tagged
+ $tag = 'mw-' . $changeType;
+
+ // Not all change types are tagged, so we check against the list of defined tags.
+ if ( in_array( $tag, ChangeTags::getSoftwareTags() ) ) {
+ return $tag;
}
- // If we reach this point, there's no applicable auto-summary for our
- // case, so our auto-summary is empty.
- return '';
+ return null;
}
/**
@@ -878,13 +993,17 @@ abstract class ContentHandler {
// Find out if there was only one contributor
// Only scan the last 20 revisions
- $res = $dbr->select( 'revision', 'rev_user_text',
+ $revQuery = Revision::getQueryInfo();
+ $res = $dbr->select(
+ $revQuery['tables'],
+ [ 'rev_user_text' => $revQuery['fields']['rev_user_text'] ],
[
'rev_page' => $title->getArticleID(),
$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'
],
__METHOD__,
- [ 'LIMIT' => 20 ]
+ [ 'LIMIT' => 20 ],
+ $revQuery['joins']
);
if ( $res === false ) {
diff --git a/www/wiki/includes/content/JsonContent.php b/www/wiki/includes/content/JsonContent.php
index 2b94f3f7..7d8f67ce 100644
--- a/www/wiki/includes/content/JsonContent.php
+++ b/www/wiki/includes/content/JsonContent.php
@@ -75,8 +75,8 @@ class JsonContent extends TextContent {
/**
* Beautifies JSON prior to save.
*
- * @param Title $title Title
- * @param User $user User
+ * @param Title $title
+ * @param User $user
* @param ParserOptions $popts
* @return JsonContent
*/
diff --git a/www/wiki/includes/content/TextContent.php b/www/wiki/includes/content/TextContent.php
index 5f585bc9..71f65b37 100644
--- a/www/wiki/includes/content/TextContent.php
+++ b/www/wiki/includes/content/TextContent.php
@@ -35,6 +35,11 @@
class TextContent extends AbstractContent {
/**
+ * @var string
+ */
+ protected $mText;
+
+ /**
* @param string $text
* @param string $model_id
* @throws MWException
@@ -231,7 +236,7 @@ class TextContent extends AbstractContent {
*
* @param Title $title Context title for parsing
* @param int $revId Revision ID (for {{REVISIONID}})
- * @param ParserOptions $options Parser options
+ * @param ParserOptions $options
* @param bool $generateHtml Whether or not to generate HTML
* @param ParserOutput &$output The output object to fill (reference).
*/
diff --git a/www/wiki/includes/content/TextContentHandler.php b/www/wiki/includes/content/TextContentHandler.php
index ced2a665..4a7944ca 100644
--- a/www/wiki/includes/content/TextContentHandler.php
+++ b/www/wiki/includes/content/TextContentHandler.php
@@ -30,11 +30,9 @@
*/
class TextContentHandler extends ContentHandler {
- // @codingStandardsIgnoreStart T59585
public function __construct( $modelId = CONTENT_MODEL_TEXT, $formats = [ CONTENT_FORMAT_TEXT ] ) {
parent::__construct( $modelId, $formats );
}
- // @codingStandardsIgnoreEnd
/**
* Returns the content's text as-is.
diff --git a/www/wiki/includes/content/WikiTextStructure.php b/www/wiki/includes/content/WikiTextStructure.php
index aeb96b65..1128d7bd 100644
--- a/www/wiki/includes/content/WikiTextStructure.php
+++ b/www/wiki/includes/content/WikiTextStructure.php
@@ -29,6 +29,8 @@ class WikiTextStructure {
private $excludedElementSelectors = [
// "it looks like you don't have javascript enabled..." – do not need to index
'audio', 'video',
+ // CSS stylesheets aren't content
+ 'style',
// The [1] for references
'sup.reference',
// The ↑ next to references in the references section
@@ -146,9 +148,10 @@ class WikiTextStructure {
if ( !is_null( $this->allText ) ) {
return;
}
- $this->parserOutput->setEditSectionTokens( false );
- $this->parserOutput->setTOCEnabled( false );
- $text = $this->parserOutput->getText();
+ $text = $this->parserOutput->getText( [
+ 'enableSectionEditTokens' => false,
+ 'allowTOC' => false,
+ ] );
if ( strlen( $text ) == 0 ) {
$this->allText = "";
// empty text - nothing to seek here
diff --git a/www/wiki/includes/content/WikitextContent.php b/www/wiki/includes/content/WikitextContent.php
index 942390f6..5beef31b 100644
--- a/www/wiki/includes/content/WikitextContent.php
+++ b/www/wiki/includes/content/WikitextContent.php
@@ -87,7 +87,7 @@ class WikitextContent extends TextContent {
if ( $sectionId === 'new' ) {
# Inserting a new section
$subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' )
- ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : '';
+ ->plaintextParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : '';
if ( Hooks::run( 'PlaceNewSection', [ $this, $oldtext, $subject, &$text ] ) ) {
$text = strlen( trim( $oldtext ) ) > 0
? "{$oldtext}\n\n{$subject}{$text}"
@@ -270,28 +270,22 @@ class WikitextContent extends TextContent {
return false;
}
- switch ( $wgArticleCountMethod ) {
- case 'any':
- return true;
- case 'comma':
- $text = $this->getNativeData();
- return strpos( $text, ',' ) !== false;
- case 'link':
- if ( $hasLinks === null ) { # not known, find out
- if ( !$title ) {
- $context = RequestContext::getMain();
- $title = $context->getTitle();
- }
-
- $po = $this->getParserOutput( $title, null, null, false );
- $links = $po->getLinks();
- $hasLinks = !empty( $links );
+ if ( $wgArticleCountMethod === 'link' ) {
+ if ( $hasLinks === null ) { # not known, find out
+ if ( !$title ) {
+ $context = RequestContext::getMain();
+ $title = $context->getTitle();
}
- return $hasLinks;
+ $po = $this->getParserOutput( $title, null, null, false );
+ $links = $po->getLinks();
+ $hasLinks = !empty( $links );
+ }
+
+ return $hasLinks;
}
- return false;
+ return true;
}
/**
diff --git a/www/wiki/includes/context/ContextSource.php b/www/wiki/includes/context/ContextSource.php
index cea84605..03fb9e2b 100644
--- a/www/wiki/includes/context/ContextSource.php
+++ b/www/wiki/includes/context/ContextSource.php
@@ -18,7 +18,6 @@
* @author Happy-melon
* @file
*/
-use Liuggio\StatsdClient\Factory\StatsdDataFactory;
use MediaWiki\MediaWikiServices;
/**
@@ -50,8 +49,6 @@ abstract class ContextSource implements IContextSource {
}
/**
- * Set the IContextSource object
- *
* @since 1.18
* @param IContextSource $context
*/
@@ -60,8 +57,6 @@ abstract class ContextSource implements IContextSource {
}
/**
- * Get the Config object
- *
* @since 1.23
* @return Config
*/
@@ -70,8 +65,6 @@ abstract class ContextSource implements IContextSource {
}
/**
- * Get the WebRequest object
- *
* @since 1.18
* @return WebRequest
*/
@@ -80,8 +73,6 @@ abstract class ContextSource implements IContextSource {
}
/**
- * Get the Title object
- *
* @since 1.18
* @return Title|null
*/
@@ -115,8 +106,6 @@ abstract class ContextSource implements IContextSource {
}
/**
- * Get the OutputPage object
- *
* @since 1.18
* @return OutputPage
*/
@@ -125,8 +114,6 @@ abstract class ContextSource implements IContextSource {
}
/**
- * Get the User object
- *
* @since 1.18
* @return User
*/
@@ -135,8 +122,6 @@ abstract class ContextSource implements IContextSource {
}
/**
- * Get the Language object
- *
* @since 1.19
* @return Language
*/
@@ -145,8 +130,6 @@ abstract class ContextSource implements IContextSource {
}
/**
- * Get the Skin object
- *
* @since 1.18
* @return Skin
*/
@@ -155,8 +138,6 @@ abstract class ContextSource implements IContextSource {
}
/**
- * Get the Timing object
- *
* @since 1.27
* @return Timing
*/
@@ -165,8 +146,6 @@ abstract class ContextSource implements IContextSource {
}
/**
- * Get the Stats object
- *
* @deprecated since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)
*
* @since 1.25
diff --git a/www/wiki/includes/context/DerivativeContext.php b/www/wiki/includes/context/DerivativeContext.php
index 6e3eda6f..f7a1815d 100644
--- a/www/wiki/includes/context/DerivativeContext.php
+++ b/www/wiki/includes/context/DerivativeContext.php
@@ -18,7 +18,6 @@
* @author Daniel Friesen
* @file
*/
-use Liuggio\StatsdClient\Factory\StatsdDataFactory;
use MediaWiki\MediaWikiServices;
/**
@@ -82,17 +81,13 @@ class DerivativeContext extends ContextSource implements MutableContext {
}
/**
- * Set the SiteConfiguration object
- *
- * @param Config $s
+ * @param Config $config
*/
- public function setConfig( Config $s ) {
- $this->config = $s;
+ public function setConfig( Config $config ) {
+ $this->config = $config;
}
/**
- * Get the Config object
- *
* @return Config
*/
public function getConfig() {
@@ -104,8 +99,6 @@ class DerivativeContext extends ContextSource implements MutableContext {
}
/**
- * Get the stats object
- *
* @deprecated since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)
*
* @return IBufferingStatsdDataFactory
@@ -115,8 +108,6 @@ class DerivativeContext extends ContextSource implements MutableContext {
}
/**
- * Get the timing object
- *
* @return Timing
*/
public function getTiming() {
@@ -128,17 +119,13 @@ class DerivativeContext extends ContextSource implements MutableContext {
}
/**
- * Set the WebRequest object
- *
- * @param WebRequest $r
+ * @param WebRequest $request
*/
- public function setRequest( WebRequest $r ) {
- $this->request = $r;
+ public function setRequest( WebRequest $request ) {
+ $this->request = $request;
}
/**
- * Get the WebRequest object
- *
* @return WebRequest
*/
public function getRequest() {
@@ -150,17 +137,13 @@ class DerivativeContext extends ContextSource implements MutableContext {
}
/**
- * Set the Title object
- *
- * @param Title $t
+ * @param Title $title
*/
- public function setTitle( Title $t ) {
- $this->title = $t;
+ public function setTitle( Title $title ) {
+ $this->title = $title;
}
/**
- * Get the Title object
- *
* @return Title|null
*/
public function getTitle() {
@@ -190,13 +173,11 @@ class DerivativeContext extends ContextSource implements MutableContext {
}
/**
- * Set the WikiPage object
- *
* @since 1.19
- * @param WikiPage $p
+ * @param WikiPage $wikiPage
*/
- public function setWikiPage( WikiPage $p ) {
- $this->wikipage = $p;
+ public function setWikiPage( WikiPage $wikiPage ) {
+ $this->wikipage = $wikiPage;
}
/**
@@ -217,17 +198,13 @@ class DerivativeContext extends ContextSource implements MutableContext {
}
/**
- * Set the OutputPage object
- *
- * @param OutputPage $o
+ * @param OutputPage $output
*/
- public function setOutput( OutputPage $o ) {
- $this->output = $o;
+ public function setOutput( OutputPage $output ) {
+ $this->output = $output;
}
/**
- * Get the OutputPage object
- *
* @return OutputPage
*/
public function getOutput() {
@@ -239,17 +216,13 @@ class DerivativeContext extends ContextSource implements MutableContext {
}
/**
- * Set the User object
- *
- * @param User $u
+ * @param User $user
*/
- public function setUser( User $u ) {
- $this->user = $u;
+ public function setUser( User $user ) {
+ $this->user = $user;
}
/**
- * Get the User object
- *
* @return User
*/
public function getUser() {
@@ -261,18 +234,16 @@ class DerivativeContext extends ContextSource implements MutableContext {
}
/**
- * Set the Language object
- *
- * @param Language|string $l Language instance or language code
+ * @param Language|string $language Language instance or language code
* @throws MWException
* @since 1.19
*/
- public function setLanguage( $l ) {
- if ( $l instanceof Language ) {
- $this->lang = $l;
- } elseif ( is_string( $l ) ) {
- $l = RequestContext::sanitizeLangCode( $l );
- $obj = Language::factory( $l );
+ public function setLanguage( $language ) {
+ if ( $language instanceof Language ) {
+ $this->lang = $language;
+ } elseif ( is_string( $language ) ) {
+ $language = RequestContext::sanitizeLangCode( $language );
+ $obj = Language::factory( $language );
$this->lang = $obj;
} else {
throw new MWException( __METHOD__ . " was passed an invalid type of data." );
@@ -280,8 +251,6 @@ class DerivativeContext extends ContextSource implements MutableContext {
}
/**
- * Get the Language object
- *
* @return Language
* @since 1.19
*/
@@ -294,18 +263,14 @@ class DerivativeContext extends ContextSource implements MutableContext {
}
/**
- * Set the Skin object
- *
- * @param Skin $s
+ * @param Skin $skin
*/
- public function setSkin( Skin $s ) {
- $this->skin = clone $s;
+ public function setSkin( Skin $skin ) {
+ $this->skin = clone $skin;
$this->skin->setContext( $this );
}
/**
- * Get the Skin object
- *
* @return Skin
*/
public function getSkin() {
diff --git a/www/wiki/includes/context/IContextSource.php b/www/wiki/includes/context/IContextSource.php
index 895e9e4b..6e48e1eb 100644
--- a/www/wiki/includes/context/IContextSource.php
+++ b/www/wiki/includes/context/IContextSource.php
@@ -21,8 +21,6 @@
* @file
*/
-use Liuggio\StatsdClient\Factory\StatsdDataFactory;
-
/**
* Interface for objects which can provide a MediaWiki context on request
*
@@ -53,16 +51,13 @@ use Liuggio\StatsdClient\Factory\StatsdDataFactory;
* shutdown by separate persistence handler objects, for example.
*/
interface IContextSource extends MessageLocalizer {
+
/**
- * Get the WebRequest object
- *
* @return WebRequest
*/
public function getRequest();
/**
- * Get the Title object
- *
* @return Title|null
*/
public function getTitle();
@@ -89,30 +84,22 @@ interface IContextSource extends MessageLocalizer {
public function getWikiPage();
/**
- * Get the OutputPage object
- *
* @return OutputPage
*/
public function getOutput();
/**
- * Get the User object
- *
* @return User
*/
public function getUser();
/**
- * Get the Language object
- *
* @return Language
* @since 1.19
*/
public function getLanguage();
/**
- * Get the Skin object
- *
* @return Skin
*/
public function getSkin();
@@ -126,8 +113,6 @@ interface IContextSource extends MessageLocalizer {
public function getConfig();
/**
- * Get the stats object
- *
* @deprecated since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)
*
* @since 1.25
@@ -136,8 +121,6 @@ interface IContextSource extends MessageLocalizer {
public function getStats();
/**
- * Get the timing object
- *
* @since 1.27
* @return Timing
*/
diff --git a/www/wiki/includes/context/MutableContext.php b/www/wiki/includes/context/MutableContext.php
index 6358f11c..56ec9601 100644
--- a/www/wiki/includes/context/MutableContext.php
+++ b/www/wiki/includes/context/MutableContext.php
@@ -23,60 +23,45 @@
*/
interface MutableContext {
+
/**
- * Set the Config object
- *
- * @param Config $c
+ * @param Config $config
*/
- public function setConfig( Config $c );
+ public function setConfig( Config $config );
/**
- * Set the WebRequest object
- *
- * @param WebRequest $r
+ * @param WebRequest $request
*/
- public function setRequest( WebRequest $r );
+ public function setRequest( WebRequest $request );
/**
- * Set the Title object
- *
- * @param Title $t
+ * @param Title $title
*/
- public function setTitle( Title $t );
+ public function setTitle( Title $title );
/**
- * Set the WikiPage object
- *
- * @param WikiPage $p
+ * @param WikiPage $wikiPage
*/
- public function setWikiPage( WikiPage $p );
+ public function setWikiPage( WikiPage $wikiPage );
/**
- * Set the OutputPage object
- *
- * @param OutputPage $o
+ * @param OutputPage $output
*/
- public function setOutput( OutputPage $o );
+ public function setOutput( OutputPage $output );
/**
- * Set the User object
- *
- * @param User $u
+ * @param User $user
*/
- public function setUser( User $u );
+ public function setUser( User $user );
/**
- * Set the Language object
- *
- * @param Language|string $l Language instance or language code
+ * @param Language|string $language Language instance or language code
*/
- public function setLanguage( $l );
+ public function setLanguage( $language );
/**
- * Set the Skin object
- *
- * @param Skin $s
+ * @param Skin $skin
*/
- public function setSkin( Skin $s );
+ public function setSkin( Skin $skin );
}
diff --git a/www/wiki/includes/context/RequestContext.php b/www/wiki/includes/context/RequestContext.php
index 4a772eec..db3a7a96 100644
--- a/www/wiki/includes/context/RequestContext.php
+++ b/www/wiki/includes/context/RequestContext.php
@@ -22,7 +22,6 @@
* @file
*/
-use Liuggio\StatsdClient\Factory\StatsdDataFactory;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use Wikimedia\ScopedCallback;
@@ -82,17 +81,13 @@ class RequestContext implements IContextSource, MutableContext {
private static $instance = null;
/**
- * Set the Config object
- *
- * @param Config $c
+ * @param Config $config
*/
- public function setConfig( Config $c ) {
- $this->config = $c;
+ public function setConfig( Config $config ) {
+ $this->config = $config;
}
/**
- * Get the Config object
- *
* @return Config
*/
public function getConfig() {
@@ -106,17 +101,13 @@ class RequestContext implements IContextSource, MutableContext {
}
/**
- * Set the WebRequest object
- *
- * @param WebRequest $r
+ * @param WebRequest $request
*/
- public function setRequest( WebRequest $r ) {
- $this->request = $r;
+ public function setRequest( WebRequest $request ) {
+ $this->request = $request;
}
/**
- * Get the WebRequest object
- *
* @return WebRequest
*/
public function getRequest() {
@@ -134,8 +125,6 @@ class RequestContext implements IContextSource, MutableContext {
}
/**
- * Get the Stats object
- *
* @deprecated since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)
*
* @return IBufferingStatsdDataFactory
@@ -145,8 +134,6 @@ class RequestContext implements IContextSource, MutableContext {
}
/**
- * Get the timing object
- *
* @return Timing
*/
public function getTiming() {
@@ -159,8 +146,6 @@ class RequestContext implements IContextSource, MutableContext {
}
/**
- * Set the Title object
- *
* @param Title|null $title
*/
public function setTitle( Title $title = null ) {
@@ -170,8 +155,6 @@ class RequestContext implements IContextSource, MutableContext {
}
/**
- * Get the Title object
- *
* @return Title|null
*/
public function getTitle() {
@@ -217,18 +200,16 @@ class RequestContext implements IContextSource, MutableContext {
}
/**
- * Set the WikiPage object
- *
* @since 1.19
- * @param WikiPage $p
+ * @param WikiPage $wikiPage
*/
- public function setWikiPage( WikiPage $p ) {
- $pageTitle = $p->getTitle();
+ public function setWikiPage( WikiPage $wikiPage ) {
+ $pageTitle = $wikiPage->getTitle();
if ( !$this->hasTitle() || !$pageTitle->equals( $this->getTitle() ) ) {
$this->setTitle( $pageTitle );
}
// Defer this to the end since setTitle sets it to null.
- $this->wikipage = $p;
+ $this->wikipage = $wikiPage;
}
/**
@@ -254,15 +235,13 @@ class RequestContext implements IContextSource, MutableContext {
}
/**
- * @param OutputPage $o
+ * @param OutputPage $output
*/
- public function setOutput( OutputPage $o ) {
- $this->output = $o;
+ public function setOutput( OutputPage $output ) {
+ $this->output = $output;
}
/**
- * Get the OutputPage object
- *
* @return OutputPage
*/
public function getOutput() {
@@ -274,17 +253,13 @@ class RequestContext implements IContextSource, MutableContext {
}
/**
- * Set the User object
- *
- * @param User $u
+ * @param User $user
*/
- public function setUser( User $u ) {
- $this->user = $u;
+ public function setUser( User $user ) {
+ $this->user = $user;
}
/**
- * Get the User object
- *
* @return User
*/
public function getUser() {
@@ -309,7 +284,6 @@ class RequestContext implements IContextSource, MutableContext {
# Validate $code
if ( !$code || !Language::isValidCode( $code ) || $code === 'qqq' ) {
- wfDebug( "Invalid user language code\n" );
$code = $wgLanguageCode;
}
@@ -317,18 +291,16 @@ class RequestContext implements IContextSource, MutableContext {
}
/**
- * Set the Language object
- *
- * @param Language|string $l Language instance or language code
+ * @param Language|string $language Language instance or language code
* @throws MWException
* @since 1.19
*/
- public function setLanguage( $l ) {
- if ( $l instanceof Language ) {
- $this->lang = $l;
- } elseif ( is_string( $l ) ) {
- $l = self::sanitizeLangCode( $l );
- $obj = Language::factory( $l );
+ public function setLanguage( $language ) {
+ if ( $language instanceof Language ) {
+ $this->lang = $language;
+ } elseif ( is_string( $language ) ) {
+ $language = self::sanitizeLangCode( $language );
+ $obj = Language::factory( $language );
$this->lang = $obj;
} else {
throw new MWException( __METHOD__ . " was passed an invalid type of data." );
@@ -386,18 +358,14 @@ class RequestContext implements IContextSource, MutableContext {
}
/**
- * Set the Skin object
- *
- * @param Skin $s
+ * @param Skin $skin
*/
- public function setSkin( Skin $s ) {
- $this->skin = clone $s;
+ public function setSkin( Skin $skin ) {
+ $this->skin = clone $skin;
$this->skin->setContext( $this );
}
/**
- * Get the Skin object
- *
* @return Skin
*/
public function getSkin() {
@@ -443,8 +411,6 @@ class RequestContext implements IContextSource, MutableContext {
return $this->skin;
}
- /** Helpful methods **/
-
/**
* Get a Message object with context set
* Parameters are the same as wfMessage()
@@ -460,8 +426,6 @@ class RequestContext implements IContextSource, MutableContext {
return call_user_func_array( 'wfMessage', $args )->setContext( $this );
}
- /** Static methods **/
-
/**
* Get the RequestContext object associated with the main request
*
@@ -593,7 +557,7 @@ class RequestContext implements IContextSource, MutableContext {
$wgUser = $context->getUser(); // b/c
if ( $session && MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
session_id( $session->getId() );
- MediaWiki\quietCall( 'session_start' );
+ Wikimedia\quietCall( 'session_start' );
}
$request = new FauxRequest( [], false, $session );
$request->setIP( $params['ip'] );
diff --git a/www/wiki/includes/dao/DBAccessBase.php b/www/wiki/includes/dao/DBAccessBase.php
index 0f8d7f73..3947f4b1 100644
--- a/www/wiki/includes/dao/DBAccessBase.php
+++ b/www/wiki/includes/dao/DBAccessBase.php
@@ -1,6 +1,6 @@
<?php
-use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LoadBalancer;
/**
@@ -27,7 +27,7 @@ use Wikimedia\Rdbms\LoadBalancer;
* @file
* @ingroup Database
*
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Daniel Kinzler
*/
abstract class DBAccessBase implements IDBAccessObject {
@@ -56,7 +56,7 @@ abstract class DBAccessBase implements IDBAccessObject {
* @param int $id Which connection to use
* @param array $groups Query groups
*
- * @return Database
+ * @return IDatabase
*/
protected function getConnection( $id, $groups = [] ) {
$loadBalancer = wfGetLB( $this->wiki );
@@ -71,9 +71,9 @@ abstract class DBAccessBase implements IDBAccessObject {
*
* @since 1.21
*
- * @param Database $db The database connection to release.
+ * @param IDatabase $db The database connection to release.
*/
- protected function releaseConnection( Database $db ) {
+ protected function releaseConnection( IDatabase $db ) {
if ( $this->wiki !== false ) {
$loadBalancer = $this->getLoadBalancer();
$loadBalancer->reuseConnection( $db );
diff --git a/www/wiki/includes/db/ChronologyProtector.php b/www/wiki/includes/db/ChronologyProtector.php
deleted file mode 100644
index cc359996..00000000
--- a/www/wiki/includes/db/ChronologyProtector.php
+++ /dev/null
@@ -1,209 +0,0 @@
-<?php
-/**
- * Generator of database load balancing objects.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
- * Kind of like Hawking's [[Chronology Protection Agency]].
- */
-class ChronologyProtector {
- /** @var BagOStuff */
- protected $store;
-
- /** @var string Storage key name */
- protected $key;
- /** @var array Map of (ip: <IP>, agent: <user-agent>) */
- protected $client;
- /** @var bool Whether to no-op all method calls */
- protected $enabled = true;
- /** @var bool Whether to check and wait on positions */
- protected $wait = true;
-
- /** @var bool Whether the client data was loaded */
- protected $initialized = false;
- /** @var DBMasterPos[] Map of (DB master name => position) */
- protected $startupPositions = [];
- /** @var DBMasterPos[] Map of (DB master name => position) */
- protected $shutdownPositions = [];
-
- /**
- * @param BagOStuff $store
- * @param array $client Map of (ip: <IP>, agent: <user-agent>)
- * @since 1.27
- */
- public function __construct( BagOStuff $store, array $client ) {
- $this->store = $store;
- $this->client = $client;
- $this->key = $store->makeGlobalKey(
- 'ChronologyProtector',
- md5( $client['ip'] . "\n" . $client['agent'] )
- );
- }
-
- /**
- * @param bool $enabled Whether to no-op all method calls
- * @since 1.27
- */
- public function setEnabled( $enabled ) {
- $this->enabled = $enabled;
- }
-
- /**
- * @param bool $enabled Whether to check and wait on positions
- * @since 1.27
- */
- public function setWaitEnabled( $enabled ) {
- $this->wait = $enabled;
- }
-
- /**
- * Initialise a LoadBalancer to give it appropriate chronology protection.
- *
- * If the stash has a previous master position recorded, this will try to
- * make sure that the next query to a slave of that master will see changes up
- * to that position by delaying execution. The delay may timeout and allow stale
- * data if no non-lagged slaves are available.
- *
- * @param LoadBalancer $lb
- * @return void
- */
- public function initLB( LoadBalancer $lb ) {
- if ( !$this->enabled || $lb->getServerCount() <= 1 ) {
- return; // non-replicated setup or disabled
- }
-
- $this->initPositions();
-
- $masterName = $lb->getServerName( $lb->getWriterIndex() );
- if ( !empty( $this->startupPositions[$masterName] ) ) {
- $info = $lb->parentInfo();
- $pos = $this->startupPositions[$masterName];
- wfDebugLog( 'replication', __METHOD__ .
- ": LB '" . $info['id'] . "' waiting for master pos $pos\n" );
- $lb->waitFor( $pos );
- }
- }
-
- /**
- * Notify the ChronologyProtector that the LoadBalancer is about to shut
- * down. Saves replication positions.
- *
- * @param LoadBalancer $lb
- * @return void
- */
- public function shutdownLB( LoadBalancer $lb ) {
- if ( !$this->enabled || $lb->getServerCount() <= 1 ) {
- return; // non-replicated setup or disabled
- }
-
- $info = $lb->parentInfo();
- $masterName = $lb->getServerName( $lb->getWriterIndex() );
-
- // Only save the position if writes have been done on the connection
- $db = $lb->getAnyOpenConnection( $lb->getWriterIndex() );
- if ( !$db || !$db->doneWrites() ) {
- wfDebugLog( 'replication', __METHOD__ . ": LB {$info['id']}, no writes done\n" );
-
- return; // nothing to do
- }
-
- $pos = $db->getMasterPos();
- wfDebugLog( 'replication', __METHOD__ . ": LB {$info['id']} has master pos $pos\n" );
- $this->shutdownPositions[$masterName] = $pos;
- }
-
- /**
- * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
- * May commit chronology data to persistent storage.
- *
- * @return array Empty on success; returns the (db name => position) map on failure
- */
- public function shutdown() {
- if ( !$this->enabled || !count( $this->shutdownPositions ) ) {
- return true; // nothing to save
- }
-
- wfDebugLog( 'replication',
- __METHOD__ . ": saving master pos for " .
- implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
- );
-
- $shutdownPositions = $this->shutdownPositions;
- $ok = $this->store->merge(
- $this->key,
- function ( $store, $key, $curValue ) use ( $shutdownPositions ) {
- /** @var $curPositions DBMasterPos[] */
- if ( $curValue === false ) {
- $curPositions = $shutdownPositions;
- } else {
- $curPositions = $curValue['positions'];
- // Use the newest positions for each DB master
- foreach ( $shutdownPositions as $db => $pos ) {
- if ( !isset( $curPositions[$db] )
- || $pos->asOfTime() > $curPositions[$db]->asOfTime()
- ) {
- $curPositions[$db] = $pos;
- }
- }
- }
-
- return [ 'positions' => $curPositions ];
- },
- BagOStuff::TTL_MINUTE,
- 10,
- BagOStuff::WRITE_SYNC // visible in all datacenters
- );
-
- if ( !$ok ) {
- // Raced out too many times or stash is down
- wfDebugLog( 'replication',
- __METHOD__ . ": failed to save master pos for " .
- implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
- );
-
- return $this->shutdownPositions;
- }
-
- return [];
- }
-
- /**
- * Load in previous master positions for the client
- */
- protected function initPositions() {
- if ( $this->initialized ) {
- return;
- }
-
- $this->initialized = true;
- if ( $this->wait ) {
- $data = $this->store->get( $this->key );
- $this->startupPositions = $data ? $data['positions'] : [];
-
- wfDebugLog( 'replication', __METHOD__ . ": key is {$this->key} (read)\n" );
- } else {
- $this->startupPositions = [];
-
- wfDebugLog( 'replication', __METHOD__ . ": key is {$this->key} (unread)\n" );
- }
- }
-}
diff --git a/www/wiki/includes/db/CloneDatabase.php b/www/wiki/includes/db/CloneDatabase.php
index 3d22c037..edb54aee 100644
--- a/www/wiki/includes/db/CloneDatabase.php
+++ b/www/wiki/includes/db/CloneDatabase.php
@@ -2,9 +2,6 @@
/**
* Helper class for making a copy of the database, mostly for unit testing.
*
- * Copyright © 2010 Chad Horohoe <chad@anyonecanedit.org>
- * https://www.mediawiki.org/
- *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
@@ -53,12 +50,12 @@ class CloneDatabase {
* @param bool $dropCurrentTables
*/
public function __construct( IMaintainableDatabase $db, array $tablesToClone,
- $newTablePrefix, $oldTablePrefix = '', $dropCurrentTables = true
+ $newTablePrefix, $oldTablePrefix = null, $dropCurrentTables = true
) {
$this->db = $db;
$this->tablesToClone = $tablesToClone;
$this->newTablePrefix = $newTablePrefix;
- $this->oldTablePrefix = $oldTablePrefix ? $oldTablePrefix : $this->db->tablePrefix();
+ $this->oldTablePrefix = $oldTablePrefix !== null ? $oldTablePrefix : $this->db->tablePrefix();
$this->dropCurrentTables = $dropCurrentTables;
}
diff --git a/www/wiki/includes/db/DBConnRef.php b/www/wiki/includes/db/DBConnRef.php
deleted file mode 100644
index d73ba85f..00000000
--- a/www/wiki/includes/db/DBConnRef.php
+++ /dev/null
@@ -1,544 +0,0 @@
-<?php
-/**
- * Helper class to handle automatically marking connections as reusable (via RAII pattern)
- * as well handling deferring the actual network connection until the handle is used
- *
- * @note: proxy methods are defined explicity to avoid interface errors
- * @ingroup Database
- * @since 1.22
- */
-class DBConnRef implements IDatabase {
- /** @var LoadBalancer */
- private $lb;
-
- /** @var DatabaseBase|null */
- private $conn;
-
- /** @var array|null */
- private $params;
-
- /**
- * @param LoadBalancer $lb
- * @param DatabaseBase|array $conn Connection or (server index, group, wiki ID) array
- */
- public function __construct( LoadBalancer $lb, $conn ) {
- $this->lb = $lb;
- if ( $conn instanceof DatabaseBase ) {
- $this->conn = $conn;
- } else {
- $this->params = $conn;
- }
- }
-
- function __call( $name, array $arguments ) {
- if ( $this->conn === null ) {
- list( $db, $groups, $wiki ) = $this->params;
- $this->conn = $this->lb->getConnection( $db, $groups, $wiki );
- }
-
- return call_user_func_array( [ $this->conn, $name ], $arguments );
- }
-
- public function getServerInfo() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function bufferResults( $buffer = null ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function trxLevel() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function trxTimestamp() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function tablePrefix( $prefix = null ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function dbSchema( $schema = null ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getLBInfo( $name = null ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function setLBInfo( $name, $value = null ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function implicitGroupby() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function implicitOrderby() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function lastQuery() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function doneWrites() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function lastDoneWrites() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function writesPending() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function writesOrCallbacksPending() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function pendingWriteQueryDuration() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function pendingWriteCallers() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function isOpen() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function setFlag( $flag ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function clearFlag( $flag ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getFlag( $flag ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getProperty( $name ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getWikiID() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getType() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function open( $server, $user, $password, $dbName ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function fetchObject( $res ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function fetchRow( $res ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function numRows( $res ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function numFields( $res ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function fieldName( $res, $n ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function insertId() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function dataSeek( $res, $row ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function lastErrno() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function lastError() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function fieldInfo( $table, $field ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function affectedRows() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getSoftwareLink() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getServerVersion() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function close() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function reportConnectionError( $error = 'Unknown error' ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function freeResult( $res ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function selectField(
- $table, $var, $cond = '', $fname = __METHOD__, $options = []
- ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function selectFieldValues(
- $table, $var, $cond = '', $fname = __METHOD__, $options = []
- ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function select(
- $table, $vars, $conds = '', $fname = __METHOD__,
- $options = [], $join_conds = []
- ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function selectSQLText(
- $table, $vars, $conds = '', $fname = __METHOD__,
- $options = [], $join_conds = []
- ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function selectRow(
- $table, $vars, $conds, $fname = __METHOD__,
- $options = [], $join_conds = []
- ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function estimateRowCount(
- $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
- ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function selectRowCount(
- $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
- ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function fieldExists( $table, $field, $fname = __METHOD__ ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function indexExists( $table, $index, $fname = __METHOD__ ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function tableExists( $table, $fname = __METHOD__ ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function indexUnique( $table, $index ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function makeList( $a, $mode = LIST_COMMA ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function bitNot( $field ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function bitAnd( $fieldLeft, $fieldRight ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function bitOr( $fieldLeft, $fieldRight ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function buildConcat( $stringList ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function buildGroupConcatField(
- $delim, $table, $field, $conds = '', $join_conds = []
- ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function selectDB( $db ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getDBname() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getServer() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function addQuotes( $s ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function buildLike() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function anyChar() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function anyString() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function nextSequenceValue( $seqName ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function upsert(
- $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__
- ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function deleteJoin(
- $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__
- ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function delete( $table, $conds, $fname = __METHOD__ ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function insertSelect(
- $destTable, $srcTable, $varMap, $conds,
- $fname = __METHOD__, $insertOptions = [], $selectOptions = []
- ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function unionSupportsOrderAndLimit() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function unionQueries( $sqls, $all ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function conditional( $cond, $trueVal, $falseVal ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function strreplace( $orig, $old, $new ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getServerUptime() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function wasDeadlock() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function wasLockTimeout() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function wasErrorReissuable() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function wasReadOnlyError() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function masterPosWait( DBMasterPos $pos, $timeout ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getSlavePos() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getMasterPos() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function onTransactionIdle( $callback ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function onTransactionPreCommitOrIdle( $callback ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function startAtomic( $fname = __METHOD__ ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function endAtomic( $fname = __METHOD__ ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function doAtomicSection( $fname, $callback ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function begin( $fname = __METHOD__ ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function commit( $fname = __METHOD__, $flush = '' ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function rollback( $fname = __METHOD__, $flush = '' ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function listTables( $prefix = null, $fname = __METHOD__ ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function timestamp( $ts = 0 ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function timestampOrNull( $ts = null ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function ping() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getLag() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getSessionLagStatus() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function maxListLen() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function encodeBlob( $b ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function decodeBlob( $b ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function setSessionOptions( array $options ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function setSchemaVars( $vars ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function lockIsFree( $lockName, $method ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function lock( $lockName, $method, $timeout = 5 ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function unlock( $lockName, $method ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function namedLocksEnqueue() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function getInfinity() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function encodeExpiry( $expiry ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function decodeExpiry( $expiry, $format = TS_MW ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function setBigSelects( $value = true ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- public function isReadOnly() {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
- /**
- * Clean up the connection when out of scope
- */
- function __destruct() {
- if ( $this->conn !== null ) {
- $this->lb->reuseConnection( $this->conn );
- }
- }
-}
diff --git a/www/wiki/includes/db/Database.php b/www/wiki/includes/db/Database.php
deleted file mode 100644
index 9daead3d..00000000
--- a/www/wiki/includes/db/Database.php
+++ /dev/null
@@ -1,3325 +0,0 @@
-<?php
-
-/**
- * @defgroup Database Database
- *
- * This file deals with database interface functions
- * and query specifics/optimisations.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * Database abstraction object
- * @ingroup Database
- */
-abstract class DatabaseBase implements IDatabase {
- /** Number of times to re-try an operation in case of deadlock */
- const DEADLOCK_TRIES = 4;
-
- /** Minimum time to wait before retry, in microseconds */
- const DEADLOCK_DELAY_MIN = 500000;
-
- /** Maximum time to wait before retry */
- const DEADLOCK_DELAY_MAX = 1500000;
-
- protected $mLastQuery = '';
- protected $mDoneWrites = false;
- protected $mPHPError = false;
-
- protected $mServer, $mUser, $mPassword, $mDBname;
-
- /** @var BagOStuff APC cache */
- protected $srvCache;
-
- /** @var resource Database connection */
- protected $mConn = null;
- protected $mOpened = false;
-
- /** @var callable[] */
- protected $mTrxIdleCallbacks = [];
- /** @var callable[] */
- protected $mTrxPreCommitCallbacks = [];
-
- protected $mTablePrefix;
- protected $mSchema;
- protected $mFlags;
- protected $mForeign;
- protected $mLBInfo = [];
- protected $mDefaultBigSelects = null;
- protected $mSchemaVars = false;
- /** @var array */
- protected $mSessionVars = [];
-
- protected $preparedArgs;
-
- protected $htmlErrors;
-
- protected $delimiter = ';';
-
- /**
- * Either 1 if a transaction is active or 0 otherwise.
- * The other Trx fields may not be meaningfull if this is 0.
- *
- * @var int
- */
- protected $mTrxLevel = 0;
-
- /**
- * Either a short hexidecimal string if a transaction is active or ""
- *
- * @var string
- * @see DatabaseBase::mTrxLevel
- */
- protected $mTrxShortId = '';
-
- /**
- * The UNIX time that the transaction started. Callers can assume that if
- * snapshot isolation is used, then the data is *at least* up to date to that
- * point (possibly more up-to-date since the first SELECT defines the snapshot).
- *
- * @var float|null
- * @see DatabaseBase::mTrxLevel
- */
- private $mTrxTimestamp = null;
-
- /** @var float Lag estimate at the time of BEGIN */
- private $mTrxSlaveLag = null;
-
- /**
- * Remembers the function name given for starting the most recent transaction via begin().
- * Used to provide additional context for error reporting.
- *
- * @var string
- * @see DatabaseBase::mTrxLevel
- */
- private $mTrxFname = null;
-
- /**
- * Record if possible write queries were done in the last transaction started
- *
- * @var bool
- * @see DatabaseBase::mTrxLevel
- */
- private $mTrxDoneWrites = false;
-
- /**
- * Record if the current transaction was started implicitly due to DBO_TRX being set.
- *
- * @var bool
- * @see DatabaseBase::mTrxLevel
- */
- private $mTrxAutomatic = false;
-
- /**
- * Array of levels of atomicity within transactions
- *
- * @var array
- */
- private $mTrxAtomicLevels = [];
-
- /**
- * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
- *
- * @var bool
- */
- private $mTrxAutomaticAtomic = false;
-
- /**
- * Track the write query callers of the current transaction
- *
- * @var string[]
- */
- private $mTrxWriteCallers = [];
-
- /**
- * Track the seconds spent in write queries for the current transaction
- *
- * @var float
- */
- private $mTrxWriteDuration = 0.0;
-
- /** @var array Map of (name => 1) for locks obtained via lock() */
- private $mNamedLocksHeld = [];
-
- /** @var IDatabase|null Lazy handle to the master DB this server replicates from */
- private $lazyMasterHandle;
-
- /**
- * @since 1.21
- * @var resource File handle for upgrade
- */
- protected $fileHandle = null;
-
- /**
- * @since 1.22
- * @var string[] Process cache of VIEWs names in the database
- */
- protected $allViews = null;
-
- /** @var TransactionProfiler */
- protected $trxProfiler;
-
- public function getServerInfo() {
- return $this->getServerVersion();
- }
-
- /**
- * @return string Command delimiter used by this database engine
- */
- public function getDelimiter() {
- return $this->delimiter;
- }
-
- /**
- * Boolean, controls output of large amounts of debug information.
- * @param bool|null $debug
- * - true to enable debugging
- * - false to disable debugging
- * - omitted or null to do nothing
- *
- * @return bool|null Previous value of the flag
- */
- public function debug( $debug = null ) {
- return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
- }
-
- public function bufferResults( $buffer = null ) {
- if ( is_null( $buffer ) ) {
- return !(bool)( $this->mFlags & DBO_NOBUFFER );
- } else {
- return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
- }
- }
-
- /**
- * Turns on (false) or off (true) the automatic generation and sending
- * of a "we're sorry, but there has been a database error" page on
- * database errors. Default is on (false). When turned off, the
- * code should use lastErrno() and lastError() to handle the
- * situation as appropriate.
- *
- * Do not use this function outside of the Database classes.
- *
- * @param null|bool $ignoreErrors
- * @return bool The previous value of the flag.
- */
- protected function ignoreErrors( $ignoreErrors = null ) {
- return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
- }
-
- public function trxLevel() {
- return $this->mTrxLevel;
- }
-
- public function trxTimestamp() {
- return $this->mTrxLevel ? $this->mTrxTimestamp : null;
- }
-
- public function tablePrefix( $prefix = null ) {
- return wfSetVar( $this->mTablePrefix, $prefix );
- }
-
- public function dbSchema( $schema = null ) {
- return wfSetVar( $this->mSchema, $schema );
- }
-
- /**
- * Set the filehandle to copy write statements to.
- *
- * @param resource $fh File handle
- */
- public function setFileHandle( $fh ) {
- $this->fileHandle = $fh;
- }
-
- public function getLBInfo( $name = null ) {
- if ( is_null( $name ) ) {
- return $this->mLBInfo;
- } else {
- if ( array_key_exists( $name, $this->mLBInfo ) ) {
- return $this->mLBInfo[$name];
- } else {
- return null;
- }
- }
- }
-
- public function setLBInfo( $name, $value = null ) {
- if ( is_null( $value ) ) {
- $this->mLBInfo = $name;
- } else {
- $this->mLBInfo[$name] = $value;
- }
- }
-
- /**
- * Set a lazy-connecting DB handle to the master DB (for replication status purposes)
- *
- * @param IDatabase $conn
- * @since 1.27
- */
- public function setLazyMasterHandle( IDatabase $conn ) {
- $this->lazyMasterHandle = $conn;
- }
-
- /**
- * @return IDatabase|null
- * @see setLazyMasterHandle()
- * @since 1.27
- */
- public function getLazyMasterHandle() {
- return $this->lazyMasterHandle;
- }
-
- /**
- * @return TransactionProfiler
- */
- protected function getTransactionProfiler() {
- if ( !$this->trxProfiler ) {
- $this->trxProfiler = new TransactionProfiler();
- }
-
- return $this->trxProfiler;
- }
-
- /**
- * @param TransactionProfiler $profiler
- * @since 1.27
- */
- public function setTransactionProfiler( TransactionProfiler $profiler ) {
- $this->trxProfiler = $profiler;
- }
-
- /**
- * Returns true if this database supports (and uses) cascading deletes
- *
- * @return bool
- */
- public function cascadingDeletes() {
- return false;
- }
-
- /**
- * Returns true if this database supports (and uses) triggers (e.g. on the page table)
- *
- * @return bool
- */
- public function cleanupTriggers() {
- return false;
- }
-
- /**
- * Returns true if this database is strict about what can be put into an IP field.
- * Specifically, it uses a NULL value instead of an empty string.
- *
- * @return bool
- */
- public function strictIPs() {
- return false;
- }
-
- /**
- * Returns true if this database uses timestamps rather than integers
- *
- * @return bool
- */
- public function realTimestamps() {
- return false;
- }
-
- public function implicitGroupby() {
- return true;
- }
-
- public function implicitOrderby() {
- return true;
- }
-
- /**
- * Returns true if this database can do a native search on IP columns
- * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
- *
- * @return bool
- */
- public function searchableIPs() {
- return false;
- }
-
- /**
- * Returns true if this database can use functional indexes
- *
- * @return bool
- */
- public function functionalIndexes() {
- return false;
- }
-
- public function lastQuery() {
- return $this->mLastQuery;
- }
-
- public function doneWrites() {
- return (bool)$this->mDoneWrites;
- }
-
- public function lastDoneWrites() {
- return $this->mDoneWrites ?: false;
- }
-
- public function writesPending() {
- return $this->mTrxLevel && $this->mTrxDoneWrites;
- }
-
- public function writesOrCallbacksPending() {
- return $this->mTrxLevel && (
- $this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
- );
- }
-
- public function pendingWriteQueryDuration() {
- return $this->mTrxLevel ? $this->mTrxWriteDuration : false;
- }
-
- public function pendingWriteCallers() {
- return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
- }
-
- public function isOpen() {
- return $this->mOpened;
- }
-
- public function setFlag( $flag ) {
- $this->mFlags |= $flag;
- }
-
- public function clearFlag( $flag ) {
- $this->mFlags &= ~$flag;
- }
-
- public function getFlag( $flag ) {
- return !!( $this->mFlags & $flag );
- }
-
- public function getProperty( $name ) {
- return $this->$name;
- }
-
- public function getWikiID() {
- if ( $this->mTablePrefix ) {
- return "{$this->mDBname}-{$this->mTablePrefix}";
- } else {
- return $this->mDBname;
- }
- }
-
- /**
- * Return a path to the DBMS-specific SQL file if it exists,
- * otherwise default SQL file
- *
- * @param string $filename
- * @return string
- */
- private function getSqlFilePath( $filename ) {
- global $IP;
- $dbmsSpecificFilePath = "$IP/maintenance/" . $this->getType() . "/$filename";
- if ( file_exists( $dbmsSpecificFilePath ) ) {
- return $dbmsSpecificFilePath;
- } else {
- return "$IP/maintenance/$filename";
- }
- }
-
- /**
- * Return a path to the DBMS-specific schema file,
- * otherwise default to tables.sql
- *
- * @return string
- */
- public function getSchemaPath() {
- return $this->getSqlFilePath( 'tables.sql' );
- }
-
- /**
- * Return a path to the DBMS-specific update key file,
- * otherwise default to update-keys.sql
- *
- * @return string
- */
- public function getUpdateKeysPath() {
- return $this->getSqlFilePath( 'update-keys.sql' );
- }
-
- /**
- * Get information about an index into an object
- * @param string $table Table name
- * @param string $index Index name
- * @param string $fname Calling function name
- * @return mixed Database-specific index description class or false if the index does not exist
- */
- abstract function indexInfo( $table, $index, $fname = __METHOD__ );
-
- /**
- * Wrapper for addslashes()
- *
- * @param string $s String to be slashed.
- * @return string Slashed string.
- */
- abstract function strencode( $s );
-
- /**
- * Constructor.
- *
- * FIXME: It is possible to construct a Database object with no associated
- * connection object, by specifying no parameters to __construct(). This
- * feature is deprecated and should be removed.
- *
- * DatabaseBase subclasses should not be constructed directly in external
- * code. DatabaseBase::factory() should be used instead.
- *
- * @param array $params Parameters passed from DatabaseBase::factory()
- */
- function __construct( array $params ) {
- global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode;
-
- $this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
-
- $server = $params['host'];
- $user = $params['user'];
- $password = $params['password'];
- $dbName = $params['dbname'];
- $flags = $params['flags'];
- $tablePrefix = $params['tablePrefix'];
- $schema = $params['schema'];
- $foreign = $params['foreign'];
-
- $this->mFlags = $flags;
- if ( $this->mFlags & DBO_DEFAULT ) {
- if ( $wgCommandLineMode ) {
- $this->mFlags &= ~DBO_TRX;
- } else {
- $this->mFlags |= DBO_TRX;
- }
- }
-
- $this->mSessionVars = $params['variables'];
-
- /** Get the default table prefix*/
- if ( $tablePrefix === 'get from global' ) {
- $this->mTablePrefix = $wgDBprefix;
- } else {
- $this->mTablePrefix = $tablePrefix;
- }
-
- /** Get the database schema*/
- if ( $schema === 'get from global' ) {
- $this->mSchema = $wgDBmwschema;
- } else {
- $this->mSchema = $schema;
- }
-
- $this->mForeign = $foreign;
-
- if ( isset( $params['trxProfiler'] ) ) {
- $this->trxProfiler = $params['trxProfiler']; // override
- }
-
- if ( $user ) {
- $this->open( $server, $user, $password, $dbName );
- }
- }
-
- /**
- * Called by serialize. Throw an exception when DB connection is serialized.
- * This causes problems on some database engines because the connection is
- * not restored on unserialize.
- */
- public function __sleep() {
- throw new MWException( 'Database serialization may cause problems, since ' .
- 'the connection is not restored on wakeup.' );
- }
-
- /**
- * Given a DB type, construct the name of the appropriate child class of
- * DatabaseBase. This is designed to replace all of the manual stuff like:
- * $class = 'Database' . ucfirst( strtolower( $dbType ) );
- * as well as validate against the canonical list of DB types we have
- *
- * This factory function is mostly useful for when you need to connect to a
- * database other than the MediaWiki default (such as for external auth,
- * an extension, et cetera). Do not use this to connect to the MediaWiki
- * database. Example uses in core:
- * @see LoadBalancer::reallyOpenConnection()
- * @see ForeignDBRepo::getMasterDB()
- * @see WebInstallerDBConnect::execute()
- *
- * @since 1.18
- *
- * @param string $dbType A possible DB type
- * @param array $p An array of options to pass to the constructor.
- * Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
- * @throws MWException If the database driver or extension cannot be found
- * @return DatabaseBase|null DatabaseBase subclass or null
- */
- final public static function factory( $dbType, $p = [] ) {
- $canonicalDBTypes = [
- 'mysql' => [ 'mysqli', 'mysql' ],
- 'postgres' => [],
- 'sqlite' => [],
- 'oracle' => [],
- 'mssql' => [],
- ];
-
- $driver = false;
- $dbType = strtolower( $dbType );
- if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
- $possibleDrivers = $canonicalDBTypes[$dbType];
- if ( !empty( $p['driver'] ) ) {
- if ( in_array( $p['driver'], $possibleDrivers ) ) {
- $driver = $p['driver'];
- } else {
- throw new MWException( __METHOD__ .
- " cannot construct Database with type '$dbType' and driver '{$p['driver']}'" );
- }
- } else {
- foreach ( $possibleDrivers as $posDriver ) {
- if ( extension_loaded( $posDriver ) ) {
- $driver = $posDriver;
- break;
- }
- }
- }
- } else {
- $driver = $dbType;
- }
- if ( $driver === false ) {
- throw new MWException( __METHOD__ .
- " no viable database extension found for type '$dbType'" );
- }
-
- // Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema,
- // and everything else doesn't use a schema (e.g. null)
- // Although postgres and oracle support schemas, we don't use them (yet)
- // to maintain backwards compatibility
- $defaultSchemas = [
- 'mssql' => 'get from global',
- ];
-
- $class = 'Database' . ucfirst( $driver );
- if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
- // Resolve some defaults for b/c
- $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
- $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
- $p['password'] = isset( $p['password'] ) ? $p['password'] : false;
- $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
- $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
- $p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
- $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global';
- if ( !isset( $p['schema'] ) ) {
- $p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
- }
- $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
-
- return new $class( $p );
- } else {
- return null;
- }
- }
-
- protected function installErrorHandler() {
- $this->mPHPError = false;
- $this->htmlErrors = ini_set( 'html_errors', '0' );
- set_error_handler( [ $this, 'connectionErrorHandler' ] );
- }
-
- /**
- * @return bool|string
- */
- protected function restoreErrorHandler() {
- restore_error_handler();
- if ( $this->htmlErrors !== false ) {
- ini_set( 'html_errors', $this->htmlErrors );
- }
- if ( $this->mPHPError ) {
- $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
- $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
-
- return $error;
- } else {
- return false;
- }
- }
-
- /**
- * @param int $errno
- * @param string $errstr
- */
- public function connectionErrorHandler( $errno, $errstr ) {
- $this->mPHPError = $errstr;
- }
-
- /**
- * Create a log context to pass to wfLogDBError or other logging functions.
- *
- * @param array $extras Additional data to add to context
- * @return array
- */
- protected function getLogContext( array $extras = [] ) {
- return array_merge(
- [
- 'db_server' => $this->mServer,
- 'db_name' => $this->mDBname,
- 'db_user' => $this->mUser,
- ],
- $extras
- );
- }
-
- public function close() {
- if ( count( $this->mTrxIdleCallbacks ) ) { // sanity
- throw new MWException( "Transaction idle callbacks still pending." );
- }
- if ( $this->mConn ) {
- if ( $this->trxLevel() ) {
- if ( !$this->mTrxAutomatic ) {
- wfWarn( "Transaction still in progress (from {$this->mTrxFname}), " .
- " performing implicit commit before closing connection!" );
- }
-
- $this->commit( __METHOD__, 'flush' );
- }
-
- $closed = $this->closeConnection();
- $this->mConn = false;
- } else {
- $closed = true;
- }
- $this->mOpened = false;
-
- return $closed;
- }
-
- /**
- * Make sure isOpen() returns true as a sanity check
- *
- * @throws DBUnexpectedError
- */
- protected function assertOpen() {
- if ( !$this->isOpen() ) {
- throw new DBUnexpectedError( $this, "DB connection was already closed." );
- }
- }
-
- /**
- * Closes underlying database connection
- * @since 1.20
- * @return bool Whether connection was closed successfully
- */
- abstract protected function closeConnection();
-
- function reportConnectionError( $error = 'Unknown error' ) {
- $myError = $this->lastError();
- if ( $myError ) {
- $error = $myError;
- }
-
- # New method
- throw new DBConnectionError( $this, $error );
- }
-
- /**
- * The DBMS-dependent part of query()
- *
- * @param string $sql SQL query.
- * @return ResultWrapper|bool Result object to feed to fetchObject,
- * fetchRow, ...; or false on failure
- */
- abstract protected function doQuery( $sql );
-
- /**
- * Determine whether a query writes to the DB.
- * Should return true if unsure.
- *
- * @param string $sql
- * @return bool
- */
- protected function isWriteQuery( $sql ) {
- return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
- }
-
- /**
- * Determine whether a SQL statement is sensitive to isolation level.
- * A SQL statement is considered transactable if its result could vary
- * depending on the transaction isolation level. Operational commands
- * such as 'SET' and 'SHOW' are not considered to be transactable.
- *
- * @param string $sql
- * @return bool
- */
- protected function isTransactableQuery( $sql ) {
- $verb = substr( $sql, 0, strcspn( $sql, " \t\r\n" ) );
- return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ] );
- }
-
- public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
- global $wgUser;
-
- $this->mLastQuery = $sql;
-
- $isWriteQuery = $this->isWriteQuery( $sql );
- if ( $isWriteQuery ) {
- $reason = $this->getReadOnlyReason();
- if ( $reason !== false ) {
- throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
- }
- # Set a flag indicating that writes have been done
- $this->mDoneWrites = microtime( true );
- }
-
- # Add a comment for easy SHOW PROCESSLIST interpretation
- if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) {
- $userName = $wgUser->getName();
- if ( mb_strlen( $userName ) > 15 ) {
- $userName = mb_substr( $userName, 0, 15 ) . '...';
- }
- $userName = str_replace( '/', '', $userName );
- } else {
- $userName = '';
- }
-
- // Add trace comment to the begin of the sql string, right after the operator.
- // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
- $commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
-
- if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX ) && $this->isTransactableQuery( $sql ) ) {
- $this->begin( __METHOD__ . " ($fname)" );
- $this->mTrxAutomatic = true;
- }
-
- # Keep track of whether the transaction has write queries pending
- if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWriteQuery ) {
- $this->mTrxDoneWrites = true;
- $this->getTransactionProfiler()->transactionWritingIn(
- $this->mServer, $this->mDBname, $this->mTrxShortId );
- }
-
- $isMaster = !is_null( $this->getLBInfo( 'master' ) );
- # generalizeSQL will probably cut down the query to reasonable
- # logging size most of the time. The substr is really just a sanity check.
- if ( $isMaster ) {
- $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'DatabaseBase::query-master';
- } else {
- $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'DatabaseBase::query';
- }
- # Include query transaction state
- $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
-
- $profiler = Profiler::instance();
- if ( !$profiler instanceof ProfilerStub ) {
- $totalProfSection = $profiler->scopedProfileIn( $totalProf );
- $queryProfSection = $profiler->scopedProfileIn( $queryProf );
- }
-
- if ( $this->debug() ) {
- wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
- }
-
- $queryId = MWDebug::query( $sql, $fname, $isMaster );
-
- # Avoid fatals if close() was called
- $this->assertOpen();
-
- # Do the query and handle errors
- $startTime = microtime( true );
- $ret = $this->doQuery( $commentedSql );
- $queryRuntime = microtime( true ) - $startTime;
- # Log the query time and feed it into the DB trx profiler
- $this->getTransactionProfiler()->recordQueryCompletion(
- $queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
-
- MWDebug::queryTime( $queryId );
-
- # Try reconnecting if the connection was lost
- if ( false === $ret && $this->wasErrorReissuable() ) {
- # Transaction is gone; this can mean lost writes or REPEATABLE-READ snapshots
- $hadTrx = $this->mTrxLevel;
- # T127428: for non-write transactions, a disconnect and a COMMIT are similar:
- # neither changed data and in both cases any read snapshots are reset anyway.
- $isNoopCommit = ( !$this->writesOrCallbacksPending() && $sql === 'COMMIT' );
- # Update state tracking to reflect transaction loss
- $this->mTrxLevel = 0;
- $this->mTrxIdleCallbacks = []; // bug 65263
- $this->mTrxPreCommitCallbacks = []; // bug 65263
- wfDebug( "Connection lost, reconnecting...\n" );
- # Stash the last error values since ping() might clear them
- $lastError = $this->lastError();
- $lastErrno = $this->lastErrno();
- if ( $this->ping() ) {
- wfDebug( "Reconnected\n" );
- $server = $this->getServer();
- $msg = __METHOD__ . ": lost connection to $server; reconnected";
- wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
-
- if ( ( $hadTrx && !$isNoopCommit ) || $this->mNamedLocksHeld ) {
- # Leave $ret as false and let an error be reported.
- # Callers may catch the exception and continue to use the DB.
- $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
- } else {
- # Should be safe to silently retry (no trx/callbacks/locks)
- $startTime = microtime( true );
- $ret = $this->doQuery( $commentedSql );
- $queryRuntime = microtime( true ) - $startTime;
- # Log the query time and feed it into the DB trx profiler
- $this->getTransactionProfiler()->recordQueryCompletion(
- $queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
- }
- } else {
- wfDebug( "Failed\n" );
- }
- }
-
- if ( false === $ret ) {
- $this->reportQueryError(
- $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
- }
-
- $res = $this->resultObject( $ret );
-
- // Destroy profile sections in the opposite order to their creation
- ScopedCallback::consume( $queryProfSection );
- ScopedCallback::consume( $totalProfSection );
-
- if ( $isWriteQuery && $this->mTrxLevel ) {
- $this->mTrxWriteDuration += $queryRuntime;
- $this->mTrxWriteCallers[] = $fname;
- }
-
- return $res;
- }
-
- public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- if ( $this->ignoreErrors() || $tempIgnore ) {
- wfDebug( "SQL ERROR (ignored): $error\n" );
- } else {
- $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
- wfLogDBError(
- "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
- $this->getLogContext( [
- 'method' => __METHOD__,
- 'errno' => $errno,
- 'error' => $error,
- 'sql1line' => $sql1line,
- 'fname' => $fname,
- ] )
- );
- wfDebug( "SQL ERROR: " . $error . "\n" );
- throw new DBQueryError( $this, $error, $errno, $sql, $fname );
- }
- }
-
- /**
- * Intended to be compatible with the PEAR::DB wrapper functions.
- * http://pear.php.net/manual/en/package.database.db.intro-execute.php
- *
- * ? = scalar value, quoted as necessary
- * ! = raw SQL bit (a function for instance)
- * & = filename; reads the file and inserts as a blob
- * (we don't use this though...)
- *
- * @param string $sql
- * @param string $func
- *
- * @return array
- */
- protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) {
- /* MySQL doesn't support prepared statements (yet), so just
- * pack up the query for reference. We'll manually replace
- * the bits later.
- */
- return [ 'query' => $sql, 'func' => $func ];
- }
-
- /**
- * Free a prepared query, generated by prepare().
- * @param string $prepared
- */
- protected function freePrepared( $prepared ) {
- /* No-op by default */
- }
-
- /**
- * Execute a prepared query with the various arguments
- * @param string $prepared The prepared sql
- * @param mixed $args Either an array here, or put scalars as varargs
- *
- * @return ResultWrapper
- */
- public function execute( $prepared, $args = null ) {
- if ( !is_array( $args ) ) {
- # Pull the var args
- $args = func_get_args();
- array_shift( $args );
- }
-
- $sql = $this->fillPrepared( $prepared['query'], $args );
-
- return $this->query( $sql, $prepared['func'] );
- }
-
- /**
- * For faking prepared SQL statements on DBs that don't support it directly.
- *
- * @param string $preparedQuery A 'preparable' SQL statement
- * @param array $args Array of Arguments to fill it with
- * @return string Executable SQL
- */
- public function fillPrepared( $preparedQuery, $args ) {
- reset( $args );
- $this->preparedArgs =& $args;
-
- return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
- [ &$this, 'fillPreparedArg' ], $preparedQuery );
- }
-
- /**
- * preg_callback func for fillPrepared()
- * The arguments should be in $this->preparedArgs and must not be touched
- * while we're doing this.
- *
- * @param array $matches
- * @throws DBUnexpectedError
- * @return string
- */
- protected function fillPreparedArg( $matches ) {
- switch ( $matches[1] ) {
- case '\\?':
- return '?';
- case '\\!':
- return '!';
- case '\\&':
- return '&';
- }
-
- list( /* $n */, $arg ) = each( $this->preparedArgs );
-
- switch ( $matches[1] ) {
- case '?':
- return $this->addQuotes( $arg );
- case '!':
- return $arg;
- case '&':
- # return $this->addQuotes( file_get_contents( $arg ) );
- throw new DBUnexpectedError(
- $this,
- '& mode is not implemented. If it\'s really needed, uncomment the line above.'
- );
- default:
- throw new DBUnexpectedError(
- $this,
- 'Received invalid match. This should never happen!'
- );
- }
- }
-
- public function freeResult( $res ) {
- }
-
- public function selectField(
- $table, $var, $cond = '', $fname = __METHOD__, $options = []
- ) {
- if ( $var === '*' ) { // sanity
- throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
- }
-
- if ( !is_array( $options ) ) {
- $options = [ $options ];
- }
-
- $options['LIMIT'] = 1;
-
- $res = $this->select( $table, $var, $cond, $fname, $options );
- if ( $res === false || !$this->numRows( $res ) ) {
- return false;
- }
-
- $row = $this->fetchRow( $res );
-
- if ( $row !== false ) {
- return reset( $row );
- } else {
- return false;
- }
- }
-
- public function selectFieldValues(
- $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
- ) {
- if ( $var === '*' ) { // sanity
- throw new DBUnexpectedError( $this, "Cannot use a * field" );
- } elseif ( !is_string( $var ) ) { // sanity
- throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
- }
-
- if ( !is_array( $options ) ) {
- $options = [ $options ];
- }
-
- $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
- if ( $res === false ) {
- return false;
- }
-
- $values = [];
- foreach ( $res as $row ) {
- $values[] = $row->$var;
- }
-
- return $values;
- }
-
- /**
- * Returns an optional USE INDEX clause to go after the table, and a
- * string to go at the end of the query.
- *
- * @param array $options Associative array of options to be turned into
- * an SQL query, valid keys are listed in the function.
- * @return array
- * @see DatabaseBase::select()
- */
- public function makeSelectOptions( $options ) {
- $preLimitTail = $postLimitTail = '';
- $startOpts = '';
-
- $noKeyOptions = [];
-
- foreach ( $options as $key => $option ) {
- if ( is_numeric( $key ) ) {
- $noKeyOptions[$option] = true;
- }
- }
-
- $preLimitTail .= $this->makeGroupByWithHaving( $options );
-
- $preLimitTail .= $this->makeOrderBy( $options );
-
- // if (isset($options['LIMIT'])) {
- // $tailOpts .= $this->limitResult('', $options['LIMIT'],
- // isset($options['OFFSET']) ? $options['OFFSET']
- // : false);
- // }
-
- if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
- $postLimitTail .= ' FOR UPDATE';
- }
-
- if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
- $postLimitTail .= ' LOCK IN SHARE MODE';
- }
-
- if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
- $startOpts .= 'DISTINCT';
- }
-
- # Various MySQL extensions
- if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
- $startOpts .= ' /*! STRAIGHT_JOIN */';
- }
-
- if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
- $startOpts .= ' HIGH_PRIORITY';
- }
-
- if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
- $startOpts .= ' SQL_BIG_RESULT';
- }
-
- if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
- $startOpts .= ' SQL_BUFFER_RESULT';
- }
-
- if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
- $startOpts .= ' SQL_SMALL_RESULT';
- }
-
- if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
- $startOpts .= ' SQL_CALC_FOUND_ROWS';
- }
-
- if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
- $startOpts .= ' SQL_CACHE';
- }
-
- if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
- $startOpts .= ' SQL_NO_CACHE';
- }
-
- if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
- $useIndex = $this->useIndexClause( $options['USE INDEX'] );
- } else {
- $useIndex = '';
- }
-
- return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
- }
-
- /**
- * Returns an optional GROUP BY with an optional HAVING
- *
- * @param array $options Associative array of options
- * @return string
- * @see DatabaseBase::select()
- * @since 1.21
- */
- public function makeGroupByWithHaving( $options ) {
- $sql = '';
- if ( isset( $options['GROUP BY'] ) ) {
- $gb = is_array( $options['GROUP BY'] )
- ? implode( ',', $options['GROUP BY'] )
- : $options['GROUP BY'];
- $sql .= ' GROUP BY ' . $gb;
- }
- if ( isset( $options['HAVING'] ) ) {
- $having = is_array( $options['HAVING'] )
- ? $this->makeList( $options['HAVING'], LIST_AND )
- : $options['HAVING'];
- $sql .= ' HAVING ' . $having;
- }
-
- return $sql;
- }
-
- /**
- * Returns an optional ORDER BY
- *
- * @param array $options Associative array of options
- * @return string
- * @see DatabaseBase::select()
- * @since 1.21
- */
- public function makeOrderBy( $options ) {
- if ( isset( $options['ORDER BY'] ) ) {
- $ob = is_array( $options['ORDER BY'] )
- ? implode( ',', $options['ORDER BY'] )
- : $options['ORDER BY'];
-
- return ' ORDER BY ' . $ob;
- }
-
- return '';
- }
-
- // See IDatabase::select for the docs for this function
- public function select( $table, $vars, $conds = '', $fname = __METHOD__,
- $options = [], $join_conds = [] ) {
- $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
-
- return $this->query( $sql, $fname );
- }
-
- public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
- $options = [], $join_conds = []
- ) {
- if ( is_array( $vars ) ) {
- $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
- }
-
- $options = (array)$options;
- $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
- ? $options['USE INDEX']
- : [];
-
- if ( is_array( $table ) ) {
- $from = ' FROM ' .
- $this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
- } elseif ( $table != '' ) {
- if ( $table[0] == ' ' ) {
- $from = ' FROM ' . $table;
- } else {
- $from = ' FROM ' .
- $this->tableNamesWithUseIndexOrJOIN( [ $table ], $useIndexes, [] );
- }
- } else {
- $from = '';
- }
-
- list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
- $this->makeSelectOptions( $options );
-
- if ( !empty( $conds ) ) {
- if ( is_array( $conds ) ) {
- $conds = $this->makeList( $conds, LIST_AND );
- }
- $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
- } else {
- $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
- }
-
- if ( isset( $options['LIMIT'] ) ) {
- $sql = $this->limitResult( $sql, $options['LIMIT'],
- isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
- }
- $sql = "$sql $postLimitTail";
-
- if ( isset( $options['EXPLAIN'] ) ) {
- $sql = 'EXPLAIN ' . $sql;
- }
-
- return $sql;
- }
-
- public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
- $options = [], $join_conds = []
- ) {
- $options = (array)$options;
- $options['LIMIT'] = 1;
- $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
-
- if ( $res === false ) {
- return false;
- }
-
- if ( !$this->numRows( $res ) ) {
- return false;
- }
-
- $obj = $this->fetchObject( $res );
-
- return $obj;
- }
-
- public function estimateRowCount(
- $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
- ) {
- $rows = 0;
- $res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
-
- if ( $res ) {
- $row = $this->fetchRow( $res );
- $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
- }
-
- return $rows;
- }
-
- public function selectRowCount(
- $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
- ) {
- $rows = 0;
- $sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
- // The identifier quotes is primarily for MSSQL.
- $rowCountCol = $this->addIdentifierQuotes( "rowcount" );
- $tableName = $this->addIdentifierQuotes( "tmp_count" );
- $res = $this->query( "SELECT COUNT(*) AS $rowCountCol FROM ($sql) $tableName", $fname );
-
- if ( $res ) {
- $row = $this->fetchRow( $res );
- $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
- }
-
- return $rows;
- }
-
- /**
- * Removes most variables from an SQL query and replaces them with X or N for numbers.
- * It's only slightly flawed. Don't use for anything important.
- *
- * @param string $sql A SQL Query
- *
- * @return string
- */
- protected static function generalizeSQL( $sql ) {
- # This does the same as the regexp below would do, but in such a way
- # as to avoid crashing php on some large strings.
- # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
-
- $sql = str_replace( "\\\\", '', $sql );
- $sql = str_replace( "\\'", '', $sql );
- $sql = str_replace( "\\\"", '', $sql );
- $sql = preg_replace( "/'.*'/s", "'X'", $sql );
- $sql = preg_replace( '/".*"/s', "'X'", $sql );
-
- # All newlines, tabs, etc replaced by single space
- $sql = preg_replace( '/\s+/', ' ', $sql );
-
- # All numbers => N,
- # except the ones surrounded by characters, e.g. l10n
- $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
- $sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
-
- return $sql;
- }
-
- public function fieldExists( $table, $field, $fname = __METHOD__ ) {
- $info = $this->fieldInfo( $table, $field );
-
- return (bool)$info;
- }
-
- public function indexExists( $table, $index, $fname = __METHOD__ ) {
- if ( !$this->tableExists( $table ) ) {
- return null;
- }
-
- $info = $this->indexInfo( $table, $index, $fname );
- if ( is_null( $info ) ) {
- return null;
- } else {
- return $info !== false;
- }
- }
-
- public function tableExists( $table, $fname = __METHOD__ ) {
- $table = $this->tableName( $table );
- $old = $this->ignoreErrors( true );
- $res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
- $this->ignoreErrors( $old );
-
- return (bool)$res;
- }
-
- public function indexUnique( $table, $index ) {
- $indexInfo = $this->indexInfo( $table, $index );
-
- if ( !$indexInfo ) {
- return null;
- }
-
- return !$indexInfo[0]->Non_unique;
- }
-
- /**
- * Helper for DatabaseBase::insert().
- *
- * @param array $options
- * @return string
- */
- protected function makeInsertOptions( $options ) {
- return implode( ' ', $options );
- }
-
- public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
- # No rows to insert, easy just return now
- if ( !count( $a ) ) {
- return true;
- }
-
- $table = $this->tableName( $table );
-
- if ( !is_array( $options ) ) {
- $options = [ $options ];
- }
-
- $fh = null;
- if ( isset( $options['fileHandle'] ) ) {
- $fh = $options['fileHandle'];
- }
- $options = $this->makeInsertOptions( $options );
-
- if ( isset( $a[0] ) && is_array( $a[0] ) ) {
- $multi = true;
- $keys = array_keys( $a[0] );
- } else {
- $multi = false;
- $keys = array_keys( $a );
- }
-
- $sql = 'INSERT ' . $options .
- " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
-
- if ( $multi ) {
- $first = true;
- foreach ( $a as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeList( $row ) . ')';
- }
- } else {
- $sql .= '(' . $this->makeList( $a ) . ')';
- }
-
- if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
- return false;
- } elseif ( $fh !== null ) {
- return true;
- }
-
- return (bool)$this->query( $sql, $fname );
- }
-
- /**
- * Make UPDATE options array for DatabaseBase::makeUpdateOptions
- *
- * @param array $options
- * @return array
- */
- protected function makeUpdateOptionsArray( $options ) {
- if ( !is_array( $options ) ) {
- $options = [ $options ];
- }
-
- $opts = [];
-
- if ( in_array( 'LOW_PRIORITY', $options ) ) {
- $opts[] = $this->lowPriorityOption();
- }
-
- if ( in_array( 'IGNORE', $options ) ) {
- $opts[] = 'IGNORE';
- }
-
- return $opts;
- }
-
- /**
- * Make UPDATE options for the DatabaseBase::update function
- *
- * @param array $options The options passed to DatabaseBase::update
- * @return string
- */
- protected function makeUpdateOptions( $options ) {
- $opts = $this->makeUpdateOptionsArray( $options );
-
- return implode( ' ', $opts );
- }
-
- function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
- $table = $this->tableName( $table );
- $opts = $this->makeUpdateOptions( $options );
- $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
-
- if ( $conds !== [] && $conds !== '*' ) {
- $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
- }
-
- return $this->query( $sql, $fname );
- }
-
- public function makeList( $a, $mode = LIST_COMMA ) {
- if ( !is_array( $a ) ) {
- throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' );
- }
-
- $first = true;
- $list = '';
-
- foreach ( $a as $field => $value ) {
- if ( !$first ) {
- if ( $mode == LIST_AND ) {
- $list .= ' AND ';
- } elseif ( $mode == LIST_OR ) {
- $list .= ' OR ';
- } else {
- $list .= ',';
- }
- } else {
- $first = false;
- }
-
- if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
- $list .= "($value)";
- } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
- $list .= "$value";
- } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
- // Remove null from array to be handled separately if found
- $includeNull = false;
- foreach ( array_keys( $value, null, true ) as $nullKey ) {
- $includeNull = true;
- unset( $value[$nullKey] );
- }
- if ( count( $value ) == 0 && !$includeNull ) {
- throw new MWException( __METHOD__ . ": empty input for field $field" );
- } elseif ( count( $value ) == 0 ) {
- // only check if $field is null
- $list .= "$field IS NULL";
- } else {
- // IN clause contains at least one valid element
- if ( $includeNull ) {
- // Group subconditions to ensure correct precedence
- $list .= '(';
- }
- if ( count( $value ) == 1 ) {
- // Special-case single values, as IN isn't terribly efficient
- // Don't necessarily assume the single key is 0; we don't
- // enforce linear numeric ordering on other arrays here.
- $value = array_values( $value )[0];
- $list .= $field . " = " . $this->addQuotes( $value );
- } else {
- $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
- }
- // if null present in array, append IS NULL
- if ( $includeNull ) {
- $list .= " OR $field IS NULL)";
- }
- }
- } elseif ( $value === null ) {
- if ( $mode == LIST_AND || $mode == LIST_OR ) {
- $list .= "$field IS ";
- } elseif ( $mode == LIST_SET ) {
- $list .= "$field = ";
- }
- $list .= 'NULL';
- } else {
- if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
- $list .= "$field = ";
- }
- $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
- }
- }
-
- return $list;
- }
-
- public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
- $conds = [];
-
- foreach ( $data as $base => $sub ) {
- if ( count( $sub ) ) {
- $conds[] = $this->makeList(
- [ $baseKey => $base, $subKey => array_keys( $sub ) ],
- LIST_AND );
- }
- }
-
- if ( $conds ) {
- return $this->makeList( $conds, LIST_OR );
- } else {
- // Nothing to search for...
- return false;
- }
- }
-
- /**
- * Return aggregated value alias
- *
- * @param array $valuedata
- * @param string $valuename
- *
- * @return string
- */
- public function aggregateValue( $valuedata, $valuename = 'value' ) {
- return $valuename;
- }
-
- public function bitNot( $field ) {
- return "(~$field)";
- }
-
- public function bitAnd( $fieldLeft, $fieldRight ) {
- return "($fieldLeft & $fieldRight)";
- }
-
- public function bitOr( $fieldLeft, $fieldRight ) {
- return "($fieldLeft | $fieldRight)";
- }
-
- public function buildConcat( $stringList ) {
- return 'CONCAT(' . implode( ',', $stringList ) . ')';
- }
-
- public function buildGroupConcatField(
- $delim, $table, $field, $conds = '', $join_conds = []
- ) {
- $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
-
- return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
- }
-
- public function selectDB( $db ) {
- # Stub. Shouldn't cause serious problems if it's not overridden, but
- # if your database engine supports a concept similar to MySQL's
- # databases you may as well.
- $this->mDBname = $db;
-
- return true;
- }
-
- public function getDBname() {
- return $this->mDBname;
- }
-
- public function getServer() {
- return $this->mServer;
- }
-
- /**
- * Format a table name ready for use in constructing an SQL query
- *
- * This does two important things: it quotes the table names to clean them up,
- * and it adds a table prefix if only given a table name with no quotes.
- *
- * All functions of this object which require a table name call this function
- * themselves. Pass the canonical name to such functions. This is only needed
- * when calling query() directly.
- *
- * @note This function does not sanitize user input. It is not safe to use
- * this function to escape user input.
- * @param string $name Database table name
- * @param string $format One of:
- * quoted - Automatically pass the table name through addIdentifierQuotes()
- * so that it can be used in a query.
- * raw - Do not add identifier quotes to the table name
- * @return string Full database name
- */
- public function tableName( $name, $format = 'quoted' ) {
- global $wgSharedDB, $wgSharedPrefix, $wgSharedTables, $wgSharedSchema;
- # Skip the entire process when we have a string quoted on both ends.
- # Note that we check the end so that we will still quote any use of
- # use of `database`.table. But won't break things if someone wants
- # to query a database table with a dot in the name.
- if ( $this->isQuotedIdentifier( $name ) ) {
- return $name;
- }
-
- # Lets test for any bits of text that should never show up in a table
- # name. Basically anything like JOIN or ON which are actually part of
- # SQL queries, but may end up inside of the table value to combine
- # sql. Such as how the API is doing.
- # Note that we use a whitespace test rather than a \b test to avoid
- # any remote case where a word like on may be inside of a table name
- # surrounded by symbols which may be considered word breaks.
- if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
- return $name;
- }
-
- # Split database and table into proper variables.
- # We reverse the explode so that database.table and table both output
- # the correct table.
- $dbDetails = explode( '.', $name, 3 );
- if ( count( $dbDetails ) == 3 ) {
- list( $database, $schema, $table ) = $dbDetails;
- # We don't want any prefix added in this case
- $prefix = '';
- } elseif ( count( $dbDetails ) == 2 ) {
- list( $database, $table ) = $dbDetails;
- # We don't want any prefix added in this case
- # In dbs that support it, $database may actually be the schema
- # but that doesn't affect any of the functionality here
- $prefix = '';
- $schema = null;
- } else {
- list( $table ) = $dbDetails;
- if ( $wgSharedDB !== null # We have a shared database
- && $this->mForeign == false # We're not working on a foreign database
- && !$this->isQuotedIdentifier( $table ) # Prevent shared tables listing '`table`'
- && in_array( $table, $wgSharedTables ) # A shared table is selected
- ) {
- $database = $wgSharedDB;
- $schema = $wgSharedSchema === null ? $this->mSchema : $wgSharedSchema;
- $prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix;
- } else {
- $database = null;
- $schema = $this->mSchema; # Default schema
- $prefix = $this->mTablePrefix; # Default prefix
- }
- }
-
- # Quote $table and apply the prefix if not quoted.
- # $tableName might be empty if this is called from Database::replaceVars()
- $tableName = "{$prefix}{$table}";
- if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) && $tableName !== '' ) {
- $tableName = $this->addIdentifierQuotes( $tableName );
- }
-
- # Quote $schema and merge it with the table name if needed
- if ( strlen( $schema ) ) {
- if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
- $schema = $this->addIdentifierQuotes( $schema );
- }
- $tableName = $schema . '.' . $tableName;
- }
-
- # Quote $database and merge it with the table name if needed
- if ( $database !== null ) {
- if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
- $database = $this->addIdentifierQuotes( $database );
- }
- $tableName = $database . '.' . $tableName;
- }
-
- return $tableName;
- }
-
- /**
- * Fetch a number of table names into an array
- * This is handy when you need to construct SQL for joins
- *
- * Example:
- * extract( $dbr->tableNames( 'user', 'watchlist' ) );
- * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
- * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
- *
- * @return array
- */
- public function tableNames() {
- $inArray = func_get_args();
- $retVal = [];
-
- foreach ( $inArray as $name ) {
- $retVal[$name] = $this->tableName( $name );
- }
-
- return $retVal;
- }
-
- /**
- * Fetch a number of table names into an zero-indexed numerical array
- * This is handy when you need to construct SQL for joins
- *
- * Example:
- * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
- * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
- * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
- *
- * @return array
- */
- public function tableNamesN() {
- $inArray = func_get_args();
- $retVal = [];
-
- foreach ( $inArray as $name ) {
- $retVal[] = $this->tableName( $name );
- }
-
- return $retVal;
- }
-
- /**
- * Get an aliased table name
- * e.g. tableName AS newTableName
- *
- * @param string $name Table name, see tableName()
- * @param string|bool $alias Alias (optional)
- * @return string SQL name for aliased table. Will not alias a table to its own name
- */
- public function tableNameWithAlias( $name, $alias = false ) {
- if ( !$alias || $alias == $name ) {
- return $this->tableName( $name );
- } else {
- return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
- }
- }
-
- /**
- * Gets an array of aliased table names
- *
- * @param array $tables Array( [alias] => table )
- * @return string[] See tableNameWithAlias()
- */
- public function tableNamesWithAlias( $tables ) {
- $retval = [];
- foreach ( $tables as $alias => $table ) {
- if ( is_numeric( $alias ) ) {
- $alias = $table;
- }
- $retval[] = $this->tableNameWithAlias( $table, $alias );
- }
-
- return $retval;
- }
-
- /**
- * Get an aliased field name
- * e.g. fieldName AS newFieldName
- *
- * @param string $name Field name
- * @param string|bool $alias Alias (optional)
- * @return string SQL name for aliased field. Will not alias a field to its own name
- */
- public function fieldNameWithAlias( $name, $alias = false ) {
- if ( !$alias || (string)$alias === (string)$name ) {
- return $name;
- } else {
- return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
- }
- }
-
- /**
- * Gets an array of aliased field names
- *
- * @param array $fields Array( [alias] => field )
- * @return string[] See fieldNameWithAlias()
- */
- public function fieldNamesWithAlias( $fields ) {
- $retval = [];
- foreach ( $fields as $alias => $field ) {
- if ( is_numeric( $alias ) ) {
- $alias = $field;
- }
- $retval[] = $this->fieldNameWithAlias( $field, $alias );
- }
-
- return $retval;
- }
-
- /**
- * Get the aliased table name clause for a FROM clause
- * which might have a JOIN and/or USE INDEX clause
- *
- * @param array $tables ( [alias] => table )
- * @param array $use_index Same as for select()
- * @param array $join_conds Same as for select()
- * @return string
- */
- protected function tableNamesWithUseIndexOrJOIN(
- $tables, $use_index = [], $join_conds = []
- ) {
- $ret = [];
- $retJOIN = [];
- $use_index = (array)$use_index;
- $join_conds = (array)$join_conds;
-
- foreach ( $tables as $alias => $table ) {
- if ( !is_string( $alias ) ) {
- // No alias? Set it equal to the table name
- $alias = $table;
- }
- // Is there a JOIN clause for this table?
- if ( isset( $join_conds[$alias] ) ) {
- list( $joinType, $conds ) = $join_conds[$alias];
- $tableClause = $joinType;
- $tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
- if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
- $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
- if ( $use != '' ) {
- $tableClause .= ' ' . $use;
- }
- }
- $on = $this->makeList( (array)$conds, LIST_AND );
- if ( $on != '' ) {
- $tableClause .= ' ON (' . $on . ')';
- }
-
- $retJOIN[] = $tableClause;
- } elseif ( isset( $use_index[$alias] ) ) {
- // Is there an INDEX clause for this table?
- $tableClause = $this->tableNameWithAlias( $table, $alias );
- $tableClause .= ' ' . $this->useIndexClause(
- implode( ',', (array)$use_index[$alias] )
- );
-
- $ret[] = $tableClause;
- } else {
- $tableClause = $this->tableNameWithAlias( $table, $alias );
-
- $ret[] = $tableClause;
- }
- }
-
- // We can't separate explicit JOIN clauses with ',', use ' ' for those
- $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
- $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
-
- // Compile our final table clause
- return implode( ' ', [ $implicitJoins, $explicitJoins ] );
- }
-
- /**
- * Get the name of an index in a given table.
- *
- * @param string $index
- * @return string
- */
- protected function indexName( $index ) {
- // Backwards-compatibility hack
- $renamed = [
- 'ar_usertext_timestamp' => 'usertext_timestamp',
- 'un_user_id' => 'user_id',
- 'un_user_ip' => 'user_ip',
- ];
-
- if ( isset( $renamed[$index] ) ) {
- return $renamed[$index];
- } else {
- return $index;
- }
- }
-
- public function addQuotes( $s ) {
- if ( $s instanceof Blob ) {
- $s = $s->fetch();
- }
- if ( $s === null ) {
- return 'NULL';
- } else {
- # This will also quote numeric values. This should be harmless,
- # and protects against weird problems that occur when they really
- # _are_ strings such as article titles and string->number->string
- # conversion is not 1:1.
- return "'" . $this->strencode( $s ) . "'";
- }
- }
-
- /**
- * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
- * MySQL uses `backticks` while basically everything else uses double quotes.
- * Since MySQL is the odd one out here the double quotes are our generic
- * and we implement backticks in DatabaseMysql.
- *
- * @param string $s
- * @return string
- */
- public function addIdentifierQuotes( $s ) {
- return '"' . str_replace( '"', '""', $s ) . '"';
- }
-
- /**
- * Returns if the given identifier looks quoted or not according to
- * the database convention for quoting identifiers .
- *
- * @note Do not use this to determine if untrusted input is safe.
- * A malicious user can trick this function.
- * @param string $name
- * @return bool
- */
- public function isQuotedIdentifier( $name ) {
- return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
- }
-
- /**
- * @param string $s
- * @return string
- */
- protected function escapeLikeInternal( $s ) {
- return addcslashes( $s, '\%_' );
- }
-
- public function buildLike() {
- $params = func_get_args();
-
- if ( count( $params ) > 0 && is_array( $params[0] ) ) {
- $params = $params[0];
- }
-
- $s = '';
-
- foreach ( $params as $value ) {
- if ( $value instanceof LikeMatch ) {
- $s .= $value->toString();
- } else {
- $s .= $this->escapeLikeInternal( $value );
- }
- }
-
- return " LIKE {$this->addQuotes( $s )} ";
- }
-
- public function anyChar() {
- return new LikeMatch( '_' );
- }
-
- public function anyString() {
- return new LikeMatch( '%' );
- }
-
- public function nextSequenceValue( $seqName ) {
- return null;
- }
-
- /**
- * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
- * is only needed because a) MySQL must be as efficient as possible due to
- * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
- * which index to pick. Anyway, other databases might have different
- * indexes on a given table. So don't bother overriding this unless you're
- * MySQL.
- * @param string $index
- * @return string
- */
- public function useIndexClause( $index ) {
- return '';
- }
-
- public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
- $quotedTable = $this->tableName( $table );
-
- if ( count( $rows ) == 0 ) {
- return;
- }
-
- # Single row case
- if ( !is_array( reset( $rows ) ) ) {
- $rows = [ $rows ];
- }
-
- // @FXIME: this is not atomic, but a trx would break affectedRows()
- foreach ( $rows as $row ) {
- # Delete rows which collide
- if ( $uniqueIndexes ) {
- $sql = "DELETE FROM $quotedTable WHERE ";
- $first = true;
- foreach ( $uniqueIndexes as $index ) {
- if ( $first ) {
- $first = false;
- $sql .= '( ';
- } else {
- $sql .= ' ) OR ( ';
- }
- if ( is_array( $index ) ) {
- $first2 = true;
- foreach ( $index as $col ) {
- if ( $first2 ) {
- $first2 = false;
- } else {
- $sql .= ' AND ';
- }
- $sql .= $col . '=' . $this->addQuotes( $row[$col] );
- }
- } else {
- $sql .= $index . '=' . $this->addQuotes( $row[$index] );
- }
- }
- $sql .= ' )';
- $this->query( $sql, $fname );
- }
-
- # Now insert the row
- $this->insert( $table, $row, $fname );
- }
- }
-
- /**
- * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
- * statement.
- *
- * @param string $table Table name
- * @param array|string $rows Row(s) to insert
- * @param string $fname Caller function name
- *
- * @return ResultWrapper
- */
- protected function nativeReplace( $table, $rows, $fname ) {
- $table = $this->tableName( $table );
-
- # Single row case
- if ( !is_array( reset( $rows ) ) ) {
- $rows = [ $rows ];
- }
-
- $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
- $first = true;
-
- foreach ( $rows as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
-
- $sql .= '(' . $this->makeList( $row ) . ')';
- }
-
- return $this->query( $sql, $fname );
- }
-
- public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
- $fname = __METHOD__
- ) {
- if ( !count( $rows ) ) {
- return true; // nothing to do
- }
-
- if ( !is_array( reset( $rows ) ) ) {
- $rows = [ $rows ];
- }
-
- if ( count( $uniqueIndexes ) ) {
- $clauses = []; // list WHERE clauses that each identify a single row
- foreach ( $rows as $row ) {
- foreach ( $uniqueIndexes as $index ) {
- $index = is_array( $index ) ? $index : [ $index ]; // columns
- $rowKey = []; // unique key to this row
- foreach ( $index as $column ) {
- $rowKey[$column] = $row[$column];
- }
- $clauses[] = $this->makeList( $rowKey, LIST_AND );
- }
- }
- $where = [ $this->makeList( $clauses, LIST_OR ) ];
- } else {
- $where = false;
- }
-
- $useTrx = !$this->mTrxLevel;
- if ( $useTrx ) {
- $this->begin( $fname );
- }
- try {
- # Update any existing conflicting row(s)
- if ( $where !== false ) {
- $ok = $this->update( $table, $set, $where, $fname );
- } else {
- $ok = true;
- }
- # Now insert any non-conflicting row(s)
- $ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
- } catch ( Exception $e ) {
- if ( $useTrx ) {
- $this->rollback( $fname );
- }
- throw $e;
- }
- if ( $useTrx ) {
- $this->commit( $fname );
- }
-
- return $ok;
- }
-
- public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
- $fname = __METHOD__
- ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this,
- 'DatabaseBase::deleteJoin() called with empty $conds' );
- }
-
- $delTable = $this->tableName( $delTable );
- $joinTable = $this->tableName( $joinTable );
- $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
- if ( $conds != '*' ) {
- $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
- }
- $sql .= ')';
-
- $this->query( $sql, $fname );
- }
-
- /**
- * Returns the size of a text field, or -1 for "unlimited"
- *
- * @param string $table
- * @param string $field
- * @return int
- */
- public function textFieldSize( $table, $field ) {
- $table = $this->tableName( $table );
- $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
- $res = $this->query( $sql, 'DatabaseBase::textFieldSize' );
- $row = $this->fetchObject( $res );
-
- $m = [];
-
- if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
- $size = $m[1];
- } else {
- $size = -1;
- }
-
- return $size;
- }
-
- /**
- * A string to insert into queries to show that they're low-priority, like
- * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
- * string and nothing bad should happen.
- *
- * @return string Returns the text of the low priority option if it is
- * supported, or a blank string otherwise
- */
- public function lowPriorityOption() {
- return '';
- }
-
- public function delete( $table, $conds, $fname = __METHOD__ ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
- }
-
- $table = $this->tableName( $table );
- $sql = "DELETE FROM $table";
-
- if ( $conds != '*' ) {
- if ( is_array( $conds ) ) {
- $conds = $this->makeList( $conds, LIST_AND );
- }
- $sql .= ' WHERE ' . $conds;
- }
-
- return $this->query( $sql, $fname );
- }
-
- public function insertSelect( $destTable, $srcTable, $varMap, $conds,
- $fname = __METHOD__,
- $insertOptions = [], $selectOptions = []
- ) {
- $destTable = $this->tableName( $destTable );
-
- if ( !is_array( $insertOptions ) ) {
- $insertOptions = [ $insertOptions ];
- }
-
- $insertOptions = $this->makeInsertOptions( $insertOptions );
-
- if ( !is_array( $selectOptions ) ) {
- $selectOptions = [ $selectOptions ];
- }
-
- list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
-
- if ( is_array( $srcTable ) ) {
- $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) );
- } else {
- $srcTable = $this->tableName( $srcTable );
- }
-
- $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
- " SELECT $startOpts " . implode( ',', $varMap ) .
- " FROM $srcTable $useIndex ";
-
- if ( $conds != '*' ) {
- if ( is_array( $conds ) ) {
- $conds = $this->makeList( $conds, LIST_AND );
- }
- $sql .= " WHERE $conds";
- }
-
- $sql .= " $tailOpts";
-
- return $this->query( $sql, $fname );
- }
-
- /**
- * Construct a LIMIT query with optional offset. This is used for query
- * pages. The SQL should be adjusted so that only the first $limit rows
- * are returned. If $offset is provided as well, then the first $offset
- * rows should be discarded, and the next $limit rows should be returned.
- * If the result of the query is not ordered, then the rows to be returned
- * are theoretically arbitrary.
- *
- * $sql is expected to be a SELECT, if that makes a difference.
- *
- * The version provided by default works in MySQL and SQLite. It will very
- * likely need to be overridden for most other DBMSes.
- *
- * @param string $sql SQL query we will append the limit too
- * @param int $limit The SQL limit
- * @param int|bool $offset The SQL offset (default false)
- * @throws DBUnexpectedError
- * @return string
- */
- public function limitResult( $sql, $limit, $offset = false ) {
- if ( !is_numeric( $limit ) ) {
- throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
- }
-
- return "$sql LIMIT "
- . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
- . "{$limit} ";
- }
-
- public function unionSupportsOrderAndLimit() {
- return true; // True for almost every DB supported
- }
-
- public function unionQueries( $sqls, $all ) {
- $glue = $all ? ') UNION ALL (' : ') UNION (';
-
- return '(' . implode( $glue, $sqls ) . ')';
- }
-
- public function conditional( $cond, $trueVal, $falseVal ) {
- if ( is_array( $cond ) ) {
- $cond = $this->makeList( $cond, LIST_AND );
- }
-
- return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
- }
-
- public function strreplace( $orig, $old, $new ) {
- return "REPLACE({$orig}, {$old}, {$new})";
- }
-
- public function getServerUptime() {
- return 0;
- }
-
- public function wasDeadlock() {
- return false;
- }
-
- public function wasLockTimeout() {
- return false;
- }
-
- public function wasErrorReissuable() {
- return false;
- }
-
- public function wasReadOnlyError() {
- return false;
- }
-
- /**
- * Determines if the given query error was a connection drop
- * STUB
- *
- * @param integer|string $errno
- * @return bool
- */
- public function wasConnectionError( $errno ) {
- return false;
- }
-
- /**
- * Perform a deadlock-prone transaction.
- *
- * This function invokes a callback function to perform a set of write
- * queries. If a deadlock occurs during the processing, the transaction
- * will be rolled back and the callback function will be called again.
- *
- * Usage:
- * $dbw->deadlockLoop( callback, ... );
- *
- * Extra arguments are passed through to the specified callback function.
- *
- * Returns whatever the callback function returned on its successful,
- * iteration, or false on error, for example if the retry limit was
- * reached.
- * @return mixed
- * @throws DBUnexpectedError
- * @throws Exception
- */
- public function deadlockLoop() {
- $args = func_get_args();
- $function = array_shift( $args );
- $tries = self::DEADLOCK_TRIES;
-
- $this->begin( __METHOD__ );
-
- $retVal = null;
- /** @var Exception $e */
- $e = null;
- do {
- try {
- $retVal = call_user_func_array( $function, $args );
- break;
- } catch ( DBQueryError $e ) {
- if ( $this->wasDeadlock() ) {
- // Retry after a randomized delay
- usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
- } else {
- // Throw the error back up
- throw $e;
- }
- }
- } while ( --$tries > 0 );
-
- if ( $tries <= 0 ) {
- // Too many deadlocks; give up
- $this->rollback( __METHOD__ );
- throw $e;
- } else {
- $this->commit( __METHOD__ );
-
- return $retVal;
- }
- }
-
- public function masterPosWait( DBMasterPos $pos, $timeout ) {
- # Real waits are implemented in the subclass.
- return 0;
- }
-
- public function getSlavePos() {
- # Stub
- return false;
- }
-
- public function getMasterPos() {
- # Stub
- return false;
- }
-
- final public function onTransactionIdle( $callback ) {
- $this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
- if ( !$this->mTrxLevel ) {
- $this->runOnTransactionIdleCallbacks();
- }
- }
-
- final public function onTransactionPreCommitOrIdle( $callback ) {
- if ( $this->mTrxLevel ) {
- $this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
- } else {
- $this->onTransactionIdle( $callback ); // this will trigger immediately
- }
- }
-
- /**
- * Actually any "on transaction idle" callbacks.
- *
- * @since 1.20
- */
- protected function runOnTransactionIdleCallbacks() {
- $autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
-
- $e = $ePrior = null; // last exception
- do { // callbacks may add callbacks :)
- $callbacks = $this->mTrxIdleCallbacks;
- $this->mTrxIdleCallbacks = []; // recursion guard
- foreach ( $callbacks as $callback ) {
- try {
- list( $phpCallback ) = $callback;
- $this->clearFlag( DBO_TRX ); // make each query its own transaction
- call_user_func( $phpCallback );
- if ( $autoTrx ) {
- $this->setFlag( DBO_TRX ); // restore automatic begin()
- } else {
- $this->clearFlag( DBO_TRX ); // restore auto-commit
- }
- } catch ( Exception $e ) {
- if ( $ePrior ) {
- MWExceptionHandler::logException( $ePrior );
- }
- $ePrior = $e;
- // Some callbacks may use startAtomic/endAtomic, so make sure
- // their transactions are ended so other callbacks don't fail
- if ( $this->trxLevel() ) {
- $this->rollback( __METHOD__ );
- }
- }
- }
- } while ( count( $this->mTrxIdleCallbacks ) );
-
- if ( $e instanceof Exception ) {
- throw $e; // re-throw any last exception
- }
- }
-
- /**
- * Actually any "on transaction pre-commit" callbacks.
- *
- * @since 1.22
- */
- protected function runOnTransactionPreCommitCallbacks() {
- $e = $ePrior = null; // last exception
- do { // callbacks may add callbacks :)
- $callbacks = $this->mTrxPreCommitCallbacks;
- $this->mTrxPreCommitCallbacks = []; // recursion guard
- foreach ( $callbacks as $callback ) {
- try {
- list( $phpCallback ) = $callback;
- call_user_func( $phpCallback );
- } catch ( Exception $e ) {
- if ( $ePrior ) {
- MWExceptionHandler::logException( $ePrior );
- }
- $ePrior = $e;
- }
- }
- } while ( count( $this->mTrxPreCommitCallbacks ) );
-
- if ( $e instanceof Exception ) {
- throw $e; // re-throw any last exception
- }
- }
-
- final public function startAtomic( $fname = __METHOD__ ) {
- if ( !$this->mTrxLevel ) {
- $this->begin( $fname );
- $this->mTrxAutomatic = true;
- // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
- // in all changes being in one transaction to keep requests transactional.
- if ( !$this->getFlag( DBO_TRX ) ) {
- $this->mTrxAutomaticAtomic = true;
- }
- }
-
- $this->mTrxAtomicLevels[] = $fname;
- }
-
- final public function endAtomic( $fname = __METHOD__ ) {
- if ( !$this->mTrxLevel ) {
- throw new DBUnexpectedError( $this, 'No atomic transaction is open.' );
- }
- if ( !$this->mTrxAtomicLevels ||
- array_pop( $this->mTrxAtomicLevels ) !== $fname
- ) {
- throw new DBUnexpectedError( $this, 'Invalid atomic section ended.' );
- }
-
- if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
- $this->commit( $fname, 'flush' );
- }
- }
-
- final public function doAtomicSection( $fname, $callback ) {
- if ( !is_callable( $callback ) ) {
- throw new UnexpectedValueException( "Invalid callback." );
- };
-
- $this->startAtomic( $fname );
- try {
- call_user_func_array( $callback, [ $this, $fname ] );
- } catch ( Exception $e ) {
- $this->rollback( $fname );
- throw $e;
- }
- $this->endAtomic( $fname );
- }
-
- final public function begin( $fname = __METHOD__ ) {
- if ( $this->mTrxLevel ) { // implicit commit
- if ( $this->mTrxAtomicLevels ) {
- // If the current transaction was an automatic atomic one, then we definitely have
- // a problem. Same if there is any unclosed atomic level.
- $levels = implode( ', ', $this->mTrxAtomicLevels );
- throw new DBUnexpectedError(
- $this,
- "Got explicit BEGIN from $fname while atomic section(s) $levels are open."
- );
- } elseif ( !$this->mTrxAutomatic ) {
- // We want to warn about inadvertently nested begin/commit pairs, but not about
- // auto-committing implicit transactions that were started by query() via DBO_TRX
- $msg = "$fname: Transaction already in progress (from {$this->mTrxFname}), " .
- " performing implicit commit!";
- wfWarn( $msg );
- wfLogDBError( $msg,
- $this->getLogContext( [
- 'method' => __METHOD__,
- 'fname' => $fname,
- ] )
- );
- } else {
- // if the transaction was automatic and has done write operations
- if ( $this->mTrxDoneWrites ) {
- wfDebug( "$fname: Automatic transaction with writes in progress" .
- " (from {$this->mTrxFname}), performing implicit commit!\n"
- );
- }
- }
-
- $this->runOnTransactionPreCommitCallbacks();
- $writeTime = $this->pendingWriteQueryDuration();
- $this->doCommit( $fname );
- if ( $this->mTrxDoneWrites ) {
- $this->mDoneWrites = microtime( true );
- $this->getTransactionProfiler()->transactionWritingOut(
- $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
- }
- $this->runOnTransactionIdleCallbacks();
- }
-
- # Avoid fatals if close() was called
- $this->assertOpen();
-
- $this->doBegin( $fname );
- $this->mTrxTimestamp = microtime( true );
- $this->mTrxFname = $fname;
- $this->mTrxDoneWrites = false;
- $this->mTrxAutomatic = false;
- $this->mTrxAutomaticAtomic = false;
- $this->mTrxAtomicLevels = [];
- $this->mTrxIdleCallbacks = [];
- $this->mTrxPreCommitCallbacks = [];
- $this->mTrxShortId = wfRandomString( 12 );
- $this->mTrxWriteDuration = 0.0;
- $this->mTrxWriteCallers = [];
- // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
- // Get an estimate of the slave lag before then, treating estimate staleness
- // as lag itself just to be safe
- $status = $this->getApproximateLagStatus();
- $this->mTrxSlaveLag = $status['lag'] + ( microtime( true ) - $status['since'] );
- }
-
- /**
- * Issues the BEGIN command to the database server.
- *
- * @see DatabaseBase::begin()
- * @param string $fname
- */
- protected function doBegin( $fname ) {
- $this->query( 'BEGIN', $fname );
- $this->mTrxLevel = 1;
- }
-
- final public function commit( $fname = __METHOD__, $flush = '' ) {
- if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
- // There are still atomic sections open. This cannot be ignored
- $levels = implode( ', ', $this->mTrxAtomicLevels );
- throw new DBUnexpectedError(
- $this,
- "Got COMMIT while atomic sections $levels are still open"
- );
- }
-
- if ( $flush === 'flush' ) {
- if ( !$this->mTrxLevel ) {
- return; // nothing to do
- } elseif ( !$this->mTrxAutomatic ) {
- throw new DBUnexpectedError(
- $this,
- "$fname: Flushing an explicit transaction, getting out of sync!"
- );
- }
- } else {
- if ( !$this->mTrxLevel ) {
- wfWarn( "$fname: No transaction to commit, something got out of sync!" );
- return; // nothing to do
- } elseif ( $this->mTrxAutomatic ) {
- wfWarn( "$fname: Explicit commit of implicit transaction. Something may be out of sync!" );
- }
- }
-
- # Avoid fatals if close() was called
- $this->assertOpen();
-
- $this->runOnTransactionPreCommitCallbacks();
- $writeTime = $this->pendingWriteQueryDuration();
- $this->doCommit( $fname );
- if ( $this->mTrxDoneWrites ) {
- $this->mDoneWrites = microtime( true );
- $this->getTransactionProfiler()->transactionWritingOut(
- $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
- }
- $this->runOnTransactionIdleCallbacks();
- }
-
- /**
- * Issues the COMMIT command to the database server.
- *
- * @see DatabaseBase::commit()
- * @param string $fname
- */
- protected function doCommit( $fname ) {
- if ( $this->mTrxLevel ) {
- $this->query( 'COMMIT', $fname );
- $this->mTrxLevel = 0;
- }
- }
-
- final public function rollback( $fname = __METHOD__, $flush = '' ) {
- if ( $flush !== 'flush' ) {
- if ( !$this->mTrxLevel ) {
- wfWarn( "$fname: No transaction to rollback, something got out of sync!" );
- return; // nothing to do
- }
- } else {
- if ( !$this->mTrxLevel ) {
- return; // nothing to do
- }
- }
-
- # Avoid fatals if close() was called
- $this->assertOpen();
-
- $this->doRollback( $fname );
- $this->mTrxIdleCallbacks = []; // cancel
- $this->mTrxPreCommitCallbacks = []; // cancel
- $this->mTrxAtomicLevels = [];
- if ( $this->mTrxDoneWrites ) {
- $this->getTransactionProfiler()->transactionWritingOut(
- $this->mServer, $this->mDBname, $this->mTrxShortId );
- }
- }
-
- /**
- * Issues the ROLLBACK command to the database server.
- *
- * @see DatabaseBase::rollback()
- * @param string $fname
- */
- protected function doRollback( $fname ) {
- if ( $this->mTrxLevel ) {
- $this->query( 'ROLLBACK', $fname, true );
- $this->mTrxLevel = 0;
- }
- }
-
- /**
- * Creates a new table with structure copied from existing table
- * Note that unlike most database abstraction functions, this function does not
- * automatically append database prefix, because it works at a lower
- * abstraction level.
- * The table names passed to this function shall not be quoted (this
- * function calls addIdentifierQuotes when needed).
- *
- * @param string $oldName Name of table whose structure should be copied
- * @param string $newName Name of table to be created
- * @param bool $temporary Whether the new table should be temporary
- * @param string $fname Calling function name
- * @throws MWException
- * @return bool True if operation was successful
- */
- public function duplicateTableStructure( $oldName, $newName, $temporary = false,
- $fname = __METHOD__
- ) {
- throw new MWException(
- 'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
- }
-
- function listTables( $prefix = null, $fname = __METHOD__ ) {
- throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
- }
-
- /**
- * Reset the views process cache set by listViews()
- * @since 1.22
- */
- final public function clearViewsCache() {
- $this->allViews = null;
- }
-
- /**
- * Lists all the VIEWs in the database
- *
- * For caching purposes the list of all views should be stored in
- * $this->allViews. The process cache can be cleared with clearViewsCache()
- *
- * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
- * @param string $fname Name of calling function
- * @throws MWException
- * @return array
- * @since 1.22
- */
- public function listViews( $prefix = null, $fname = __METHOD__ ) {
- throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
- }
-
- /**
- * Differentiates between a TABLE and a VIEW
- *
- * @param string $name Name of the database-structure to test.
- * @throws MWException
- * @return bool
- * @since 1.22
- */
- public function isView( $name ) {
- throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
- }
-
- public function timestamp( $ts = 0 ) {
- return wfTimestamp( TS_MW, $ts );
- }
-
- public function timestampOrNull( $ts = null ) {
- if ( is_null( $ts ) ) {
- return null;
- } else {
- return $this->timestamp( $ts );
- }
- }
-
- /**
- * Take the result from a query, and wrap it in a ResultWrapper if
- * necessary. Boolean values are passed through as is, to indicate success
- * of write queries or failure.
- *
- * Once upon a time, DatabaseBase::query() returned a bare MySQL result
- * resource, and it was necessary to call this function to convert it to
- * a wrapper. Nowadays, raw database objects are never exposed to external
- * callers, so this is unnecessary in external code.
- *
- * @param bool|ResultWrapper|resource|object $result
- * @return bool|ResultWrapper
- */
- protected function resultObject( $result ) {
- if ( !$result ) {
- return false;
- } elseif ( $result instanceof ResultWrapper ) {
- return $result;
- } elseif ( $result === true ) {
- // Successful write query
- return $result;
- } else {
- return new ResultWrapper( $this, $result );
- }
- }
-
- public function ping() {
- # Stub. Not essential to override.
- return true;
- }
-
- public function getSessionLagStatus() {
- return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
- }
-
- /**
- * Get the slave lag when the current transaction started
- *
- * This is useful when transactions might use snapshot isolation
- * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
- * is this lag plus transaction duration. If they don't, it is still
- * safe to be pessimistic. This returns null if there is no transaction.
- *
- * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
- * @since 1.27
- */
- public function getTransactionLagStatus() {
- return $this->mTrxLevel
- ? [ 'lag' => $this->mTrxSlaveLag, 'since' => $this->trxTimestamp() ]
- : null;
- }
-
- /**
- * Get a slave lag estimate for this server
- *
- * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
- * @since 1.27
- */
- public function getApproximateLagStatus() {
- return [
- 'lag' => $this->getLBInfo( 'slave' ) ? $this->getLag() : 0,
- 'since' => microtime( true )
- ];
- }
-
- /**
- * Merge the result of getSessionLagStatus() for several DBs
- * using the most pessimistic values to estimate the lag of
- * any data derived from them in combination
- *
- * This is information is useful for caching modules
- *
- * @see WANObjectCache::set()
- * @see WANObjectCache::getWithSetCallback()
- *
- * @param IDatabase $db1
- * @param IDatabase ...
- * @return array Map of values:
- * - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
- * - since: oldest UNIX timestamp of any of the DB lag estimates
- * - pending: whether any of the DBs have uncommitted changes
- * @since 1.27
- */
- public static function getCacheSetOptions( IDatabase $db1 ) {
- $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
- foreach ( func_get_args() as $db ) {
- /** @var IDatabase $db */
- $status = $db->getSessionLagStatus();
- if ( $status['lag'] === false ) {
- $res['lag'] = false;
- } elseif ( $res['lag'] !== false ) {
- $res['lag'] = max( $res['lag'], $status['lag'] );
- }
- $res['since'] = min( $res['since'], $status['since'] );
- $res['pending'] = $res['pending'] ?: $db->writesPending();
- }
-
- return $res;
- }
-
- public function getLag() {
- return 0;
- }
-
- function maxListLen() {
- return 0;
- }
-
- public function encodeBlob( $b ) {
- return $b;
- }
-
- public function decodeBlob( $b ) {
- if ( $b instanceof Blob ) {
- $b = $b->fetch();
- }
- return $b;
- }
-
- public function setSessionOptions( array $options ) {
- }
-
- /**
- * Read and execute SQL commands from a file.
- *
- * Returns true on success, error string or exception on failure (depending
- * on object's error ignore settings).
- *
- * @param string $filename File name to open
- * @param bool|callable $lineCallback Optional function called before reading each line
- * @param bool|callable $resultCallback Optional function called for each MySQL result
- * @param bool|string $fname Calling function name or false if name should be
- * generated dynamically using $filename
- * @param bool|callable $inputCallback Optional function called for each
- * complete line sent
- * @throws Exception|MWException
- * @return bool|string
- */
- public function sourceFile(
- $filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
- ) {
- MediaWiki\suppressWarnings();
- $fp = fopen( $filename, 'r' );
- MediaWiki\restoreWarnings();
-
- if ( false === $fp ) {
- throw new MWException( "Could not open \"{$filename}\".\n" );
- }
-
- if ( !$fname ) {
- $fname = __METHOD__ . "( $filename )";
- }
-
- try {
- $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
- } catch ( Exception $e ) {
- fclose( $fp );
- throw $e;
- }
-
- fclose( $fp );
-
- return $error;
- }
-
- /**
- * Get the full path of a patch file. Originally based on archive()
- * from updaters.inc. Keep in mind this always returns a patch, as
- * it fails back to MySQL if no DB-specific patch can be found
- *
- * @param string $patch The name of the patch, like patch-something.sql
- * @return string Full path to patch file
- */
- public function patchPath( $patch ) {
- global $IP;
-
- $dbType = $this->getType();
- if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
- return "$IP/maintenance/$dbType/archives/$patch";
- } else {
- return "$IP/maintenance/archives/$patch";
- }
- }
-
- public function setSchemaVars( $vars ) {
- $this->mSchemaVars = $vars;
- }
-
- /**
- * Read and execute commands from an open file handle.
- *
- * Returns true on success, error string or exception on failure (depending
- * on object's error ignore settings).
- *
- * @param resource $fp File handle
- * @param bool|callable $lineCallback Optional function called before reading each query
- * @param bool|callable $resultCallback Optional function called for each MySQL result
- * @param string $fname Calling function name
- * @param bool|callable $inputCallback Optional function called for each complete query sent
- * @return bool|string
- */
- public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
- $fname = __METHOD__, $inputCallback = false
- ) {
- $cmd = '';
-
- while ( !feof( $fp ) ) {
- if ( $lineCallback ) {
- call_user_func( $lineCallback );
- }
-
- $line = trim( fgets( $fp ) );
-
- if ( $line == '' ) {
- continue;
- }
-
- if ( '-' == $line[0] && '-' == $line[1] ) {
- continue;
- }
-
- if ( $cmd != '' ) {
- $cmd .= ' ';
- }
-
- $done = $this->streamStatementEnd( $cmd, $line );
-
- $cmd .= "$line\n";
-
- if ( $done || feof( $fp ) ) {
- $cmd = $this->replaceVars( $cmd );
-
- if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
- $res = $this->query( $cmd, $fname );
-
- if ( $resultCallback ) {
- call_user_func( $resultCallback, $res, $this );
- }
-
- if ( false === $res ) {
- $err = $this->lastError();
-
- return "Query \"{$cmd}\" failed with error code \"$err\".\n";
- }
- }
- $cmd = '';
- }
- }
-
- return true;
- }
-
- /**
- * Called by sourceStream() to check if we've reached a statement end
- *
- * @param string $sql SQL assembled so far
- * @param string $newLine New line about to be added to $sql
- * @return bool Whether $newLine contains end of the statement
- */
- public function streamStatementEnd( &$sql, &$newLine ) {
- if ( $this->delimiter ) {
- $prev = $newLine;
- $newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
- if ( $newLine != $prev ) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Database independent variable replacement. Replaces a set of variables
- * in an SQL statement with their contents as given by $this->getSchemaVars().
- *
- * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
- *
- * - '{$var}' should be used for text and is passed through the database's
- * addQuotes method.
- * - `{$var}` should be used for identifiers (e.g. table and database names).
- * It is passed through the database's addIdentifierQuotes method which
- * can be overridden if the database uses something other than backticks.
- * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
- * database's tableName method.
- * - / *i* / passes the name that follows through the database's indexName method.
- * - In all other cases, / *$var* / is left unencoded. Except for table options,
- * its use should be avoided. In 1.24 and older, string encoding was applied.
- *
- * @param string $ins SQL statement to replace variables in
- * @return string The new SQL statement with variables replaced
- */
- protected function replaceVars( $ins ) {
- $vars = $this->getSchemaVars();
- return preg_replace_callback(
- '!
- /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
- \'\{\$ (\w+) }\' | # 3. addQuotes
- `\{\$ (\w+) }` | # 4. addIdentifierQuotes
- /\*\$ (\w+) \*/ # 5. leave unencoded
- !x',
- function ( $m ) use ( $vars ) {
- // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
- // check for both nonexistent keys *and* the empty string.
- if ( isset( $m[1] ) && $m[1] !== '' ) {
- if ( $m[1] === 'i' ) {
- return $this->indexName( $m[2] );
- } else {
- return $this->tableName( $m[2] );
- }
- } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
- return $this->addQuotes( $vars[$m[3]] );
- } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
- return $this->addIdentifierQuotes( $vars[$m[4]] );
- } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
- return $vars[$m[5]];
- } else {
- return $m[0];
- }
- },
- $ins
- );
- }
-
- /**
- * Get schema variables. If none have been set via setSchemaVars(), then
- * use some defaults from the current object.
- *
- * @return array
- */
- protected function getSchemaVars() {
- if ( $this->mSchemaVars ) {
- return $this->mSchemaVars;
- } else {
- return $this->getDefaultSchemaVars();
- }
- }
-
- /**
- * Get schema variables to use if none have been set via setSchemaVars().
- *
- * Override this in derived classes to provide variables for tables.sql
- * and SQL patch files.
- *
- * @return array
- */
- protected function getDefaultSchemaVars() {
- return [];
- }
-
- public function lockIsFree( $lockName, $method ) {
- return true;
- }
-
- public function lock( $lockName, $method, $timeout = 5 ) {
- $this->mNamedLocksHeld[$lockName] = 1;
-
- return true;
- }
-
- public function unlock( $lockName, $method ) {
- unset( $this->mNamedLocksHeld[$lockName] );
-
- return true;
- }
-
- public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
- if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
- return null;
- }
-
- $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
- $this->commit( __METHOD__, 'flush' );
- $this->unlock( $lockKey, $fname );
- } );
-
- $this->commit( __METHOD__, 'flush' );
-
- return $unlocker;
- }
-
- public function namedLocksEnqueue() {
- return false;
- }
-
- /**
- * Lock specific tables
- *
- * @param array $read Array of tables to lock for read access
- * @param array $write Array of tables to lock for write access
- * @param string $method Name of caller
- * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
- * @return bool
- */
- public function lockTables( $read, $write, $method, $lowPriority = true ) {
- return true;
- }
-
- /**
- * Unlock specific tables
- *
- * @param string $method The caller
- * @return bool
- */
- public function unlockTables( $method ) {
- return true;
- }
-
- /**
- * Delete a table
- * @param string $tableName
- * @param string $fName
- * @return bool|ResultWrapper
- * @since 1.18
- */
- public function dropTable( $tableName, $fName = __METHOD__ ) {
- if ( !$this->tableExists( $tableName, $fName ) ) {
- return false;
- }
- $sql = "DROP TABLE " . $this->tableName( $tableName );
- if ( $this->cascadingDeletes() ) {
- $sql .= " CASCADE";
- }
-
- return $this->query( $sql, $fName );
- }
-
- /**
- * Get search engine class. All subclasses of this need to implement this
- * if they wish to use searching.
- *
- * @return string
- */
- public function getSearchEngine() {
- return 'SearchEngineDummy';
- }
-
- public function getInfinity() {
- return 'infinity';
- }
-
- public function encodeExpiry( $expiry ) {
- return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
- ? $this->getInfinity()
- : $this->timestamp( $expiry );
- }
-
- public function decodeExpiry( $expiry, $format = TS_MW ) {
- return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
- ? 'infinity'
- : wfTimestamp( $format, $expiry );
- }
-
- public function setBigSelects( $value = true ) {
- // no-op
- }
-
- public function isReadOnly() {
- return ( $this->getReadOnlyReason() !== false );
- }
-
- /**
- * @return string|bool Reason this DB is read-only or false if it is not
- */
- protected function getReadOnlyReason() {
- $reason = $this->getLBInfo( 'readOnlyReason' );
-
- return is_string( $reason ) ? $reason : false;
- }
-
- /**
- * @since 1.19
- * @return string
- */
- public function __toString() {
- return (string)$this->mConn;
- }
-
- /**
- * Run a few simple sanity checks
- */
- public function __destruct() {
- if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
- trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
- }
- if ( count( $this->mTrxIdleCallbacks ) || count( $this->mTrxPreCommitCallbacks ) ) {
- $callers = [];
- foreach ( $this->mTrxIdleCallbacks as $callbackInfo ) {
- $callers[] = $callbackInfo[1];
- }
- $callers = implode( ', ', $callers );
- trigger_error( "DB transaction callbacks still pending (from $callers)." );
- }
- }
-}
-
-/**
- * @since 1.27
- */
-abstract class Database extends DatabaseBase {
- // B/C until nothing type hints for DatabaseBase
- // @TODO: finish renaming DatabaseBase => Database
-}
diff --git a/www/wiki/includes/db/DatabaseError.php b/www/wiki/includes/db/DatabaseError.php
deleted file mode 100644
index 4cd02b1f..00000000
--- a/www/wiki/includes/db/DatabaseError.php
+++ /dev/null
@@ -1,472 +0,0 @@
-<?php
-/**
- * This file contains database error classes.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * Database error base class
- * @ingroup Database
- */
-class DBError extends MWException {
- /** @var DatabaseBase */
- public $db;
-
- /**
- * Construct a database error
- * @param DatabaseBase $db Object which threw the error
- * @param string $error A simple error message to be used for debugging
- */
- function __construct( DatabaseBase $db = null, $error ) {
- $this->db = $db;
- parent::__construct( $error );
- }
-}
-
-/**
- * Base class for the more common types of database errors. These are known to occur
- * frequently, so we try to give friendly error messages for them.
- *
- * @ingroup Database
- * @since 1.23
- */
-class DBExpectedError extends DBError {
- /**
- * @return string
- */
- function getText() {
- global $wgShowDBErrorBacktrace;
-
- $s = $this->getTextContent() . "\n";
-
- if ( $wgShowDBErrorBacktrace ) {
- $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n";
- }
-
- return $s;
- }
-
- /**
- * @return string
- */
- function getHTML() {
- global $wgShowDBErrorBacktrace;
-
- $s = $this->getHTMLContent();
-
- if ( $wgShowDBErrorBacktrace ) {
- $s .= '<p>Backtrace:</p><pre>' . htmlspecialchars( $this->getTraceAsString() ) . '</pre>';
- }
-
- return $s;
- }
-
- function getPageTitle() {
- return $this->msg( 'databaseerror', 'Database error' );
- }
-
- /**
- * @return string
- */
- protected function getTextContent() {
- return $this->getMessage();
- }
-
- /**
- * @return string
- */
- protected function getHTMLContent() {
- return '<p>' . nl2br( htmlspecialchars( $this->getTextContent() ) ) . '</p>';
- }
-}
-
-/**
- * @ingroup Database
- */
-class DBConnectionError extends DBExpectedError {
- /** @var string Error text */
- public $error;
-
- /**
- * @param DatabaseBase $db Object throwing the error
- * @param string $error Error text
- */
- function __construct( DatabaseBase $db = null, $error = 'unknown error' ) {
- $msg = 'DB connection error';
-
- if ( trim( $error ) != '' ) {
- $msg .= ": $error";
- } elseif ( $db ) {
- $error = $this->db->getServer();
- }
-
- parent::__construct( $db, $msg );
- $this->error = $error;
- }
-
- /**
- * @return bool
- */
- function useOutputPage() {
- // Not likely to work
- return false;
- }
-
- /**
- * @param string $key
- * @param string $fallback Unescaped alternative error text in case the
- * message cache cannot be used. Can contain parameters as in regular
- * messages, that should be passed as additional parameters.
- * @return string Unprocessed plain error text with parameters replaced
- */
- function msg( $key, $fallback /*[, params...] */ ) {
- $args = array_slice( func_get_args(), 2 );
-
- if ( $this->useMessageCache() ) {
- return wfMessage( $key, $args )->useDatabase( false )->text();
- } else {
- return wfMsgReplaceArgs( $fallback, $args );
- }
- }
-
- /**
- * @return bool
- */
- function isLoggable() {
- // Don't send to the exception log, already in dberror log
- return false;
- }
-
- /**
- * @return string Safe HTML
- */
- function getHTML() {
- global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors;
-
- $sorry = htmlspecialchars( $this->msg(
- 'dberr-problems',
- 'Sorry! This site is experiencing technical difficulties.'
- ) );
- $again = htmlspecialchars( $this->msg(
- 'dberr-again',
- 'Try waiting a few minutes and reloading.'
- ) );
-
- if ( $wgShowHostnames || $wgShowSQLErrors ) {
- $info = str_replace(
- '$1', Html::element( 'span', [ 'dir' => 'ltr' ], $this->error ),
- htmlspecialchars( $this->msg( 'dberr-info', '(Cannot access the database: $1)' ) )
- );
- } else {
- $info = htmlspecialchars( $this->msg(
- 'dberr-info-hidden',
- '(Cannot access the database)'
- ) );
- }
-
- # No database access
- MessageCache::singleton()->disable();
-
- $html = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
-
- if ( $wgShowDBErrorBacktrace ) {
- $html .= '<p>Backtrace:</p><pre>' . htmlspecialchars( $this->getTraceAsString() ) . '</pre>';
- }
-
- $html .= '<hr />';
- $html .= $this->searchForm();
-
- return $html;
- }
-
- protected function getTextContent() {
- global $wgShowHostnames, $wgShowSQLErrors;
-
- if ( $wgShowHostnames || $wgShowSQLErrors ) {
- return $this->getMessage();
- } else {
- return 'DB connection error';
- }
- }
-
- /**
- * Output the exception report using HTML.
- *
- * @return void
- */
- public function reportHTML() {
- global $wgUseFileCache;
-
- // Check whether we can serve a file-cached copy of the page with the error underneath
- if ( $wgUseFileCache ) {
- try {
- $cache = $this->fileCachedPage();
- // Cached version on file system?
- if ( $cache !== null ) {
- // Hack: extend the body for error messages
- $cache = str_replace( [ '</html>', '</body>' ], '', $cache );
- // Add cache notice...
- $cache .= '<div style="border:1px solid #ffd0d0;padding:1em;">' .
- htmlspecialchars( $this->msg( 'dberr-cachederror',
- 'This is a cached copy of the requested page, and may not be up to date.' ) ) .
- '</div>';
-
- // Output cached page with notices on bottom and re-close body
- echo "{$cache}<hr />{$this->getHTML()}</body></html>";
-
- return;
- }
- } catch ( Exception $e ) {
- // Do nothing, just use the default page
- }
- }
-
- // We can't, cough and die in the usual fashion
- parent::reportHTML();
- }
-
- /**
- * @return string
- */
- function searchForm() {
- global $wgSitename, $wgCanonicalServer, $wgRequest;
-
- $usegoogle = htmlspecialchars( $this->msg(
- 'dberr-usegoogle',
- 'You can try searching via Google in the meantime.'
- ) );
- $outofdate = htmlspecialchars( $this->msg(
- 'dberr-outofdate',
- 'Note that their indexes of our content may be out of date.'
- ) );
- $googlesearch = htmlspecialchars( $this->msg( 'searchbutton', 'Search' ) );
-
- $search = htmlspecialchars( $wgRequest->getVal( 'search' ) );
-
- $server = htmlspecialchars( $wgCanonicalServer );
- $sitename = htmlspecialchars( $wgSitename );
-
- $trygoogle = <<<EOT
-<div style="margin: 1.5em">$usegoogle<br />
-<small>$outofdate</small>
-</div>
-<form method="get" action="//www.google.com/search" id="googlesearch">
- <input type="hidden" name="domains" value="$server" />
- <input type="hidden" name="num" value="50" />
- <input type="hidden" name="ie" value="UTF-8" />
- <input type="hidden" name="oe" value="UTF-8" />
-
- <input type="text" name="q" size="31" maxlength="255" value="$search" />
- <input type="submit" name="btnG" value="$googlesearch" />
- <p>
- <label><input type="radio" name="sitesearch" value="$server" checked="checked" />$sitename</label>
- <label><input type="radio" name="sitesearch" value="" />WWW</label>
- </p>
-</form>
-EOT;
-
- return $trygoogle;
- }
-
- /**
- * @return string
- */
- private function fileCachedPage() {
- $context = RequestContext::getMain();
-
- if ( $context->getOutput()->isDisabled() ) {
- // Done already?
- return '';
- }
-
- if ( $context->getTitle() ) {
- // Use the main context's title if we managed to set it
- $t = $context->getTitle()->getPrefixedDBkey();
- } else {
- // Fallback to the raw title URL param. We can't use the Title
- // class is it may hit the interwiki table and give a DB error.
- // We may get a cache miss due to not sanitizing the title though.
- $t = str_replace( ' ', '_', $context->getRequest()->getVal( 'title' ) );
- if ( $t == '' ) { // fallback to main page
- $t = Title::newFromText(
- $this->msg( 'mainpage', 'Main Page' ) )->getPrefixedDBkey();
- }
- }
-
- $cache = new HTMLFileCache( $t, 'view' );
- if ( $cache->isCached() ) {
- return $cache->fetchText();
- } else {
- return '';
- }
- }
-}
-
-/**
- * @ingroup Database
- */
-class DBQueryError extends DBExpectedError {
- public $error, $errno, $sql, $fname;
-
- /**
- * @param DatabaseBase $db
- * @param string $error
- * @param int|string $errno
- * @param string $sql
- * @param string $fname
- */
- function __construct( DatabaseBase $db, $error, $errno, $sql, $fname ) {
- if ( $db->wasConnectionError( $errno ) ) {
- $message = "A connection error occured. \n" .
- "Query: $sql\n" .
- "Function: $fname\n" .
- "Error: $errno $error\n";
- } else {
- $message = "A database error has occurred. Did you forget to run " .
- "maintenance/update.php after upgrading? See: " .
- "https://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" .
- "Query: $sql\n" .
- "Function: $fname\n" .
- "Error: $errno $error\n";
- }
- parent::__construct( $db, $message );
-
- $this->error = $error;
- $this->errno = $errno;
- $this->sql = $sql;
- $this->fname = $fname;
- }
-
- /**
- * @return string
- */
- function getPageTitle() {
- return $this->msg( 'databaseerror', 'Database error' );
- }
-
- /**
- * @return string
- */
- protected function getHTMLContent() {
- $key = 'databaseerror-text';
- $s = Html::element( 'p', [], $this->msg( $key, $this->getFallbackMessage( $key ) ) );
-
- $details = $this->getTechnicalDetails();
- if ( $details ) {
- $s .= '<ul>';
- foreach ( $details as $key => $detail ) {
- $s .= str_replace(
- '$1', call_user_func_array( 'Html::element', $detail ),
- Html::element( 'li', [],
- $this->msg( $key, $this->getFallbackMessage( $key ) )
- )
- );
- }
- $s .= '</ul>';
- }
-
- return $s;
- }
-
- /**
- * @return string
- */
- protected function getTextContent() {
- $key = 'databaseerror-textcl';
- $s = $this->msg( $key, $this->getFallbackMessage( $key ) ) . "\n";
-
- foreach ( $this->getTechnicalDetails() as $key => $detail ) {
- $s .= $this->msg( $key, $this->getFallbackMessage( $key ), $detail[2] ) . "\n";
- }
-
- return $s;
- }
-
- /**
- * Make a list of technical details that can be shown to the user. This information can
- * aid in debugging yet may be useful to an attacker trying to exploit a security weakness
- * in the software or server configuration.
- *
- * Thus no such details are shown by default, though if $wgShowHostnames is true, only the
- * full SQL query is hidden; in fact, the error message often does contain a hostname, and
- * sites using this option probably don't care much about "security by obscurity". Of course,
- * if $wgShowSQLErrors is true, the SQL query *is* shown.
- *
- * @return array Keys are message keys; values are arrays of arguments for Html::element().
- * Array will be empty if users are not allowed to see any of these details at all.
- */
- protected function getTechnicalDetails() {
- global $wgShowHostnames, $wgShowSQLErrors;
-
- $attribs = [ 'dir' => 'ltr' ];
- $details = [];
-
- if ( $wgShowSQLErrors ) {
- $details['databaseerror-query'] = [
- 'div', [ 'class' => 'mw-code' ] + $attribs, $this->sql ];
- }
-
- if ( $wgShowHostnames || $wgShowSQLErrors ) {
- $errorMessage = $this->errno . ' ' . $this->error;
- $details['databaseerror-function'] = [ 'code', $attribs, $this->fname ];
- $details['databaseerror-error'] = [ 'samp', $attribs, $errorMessage ];
- }
-
- return $details;
- }
-
- /**
- * @param string $key Message key
- * @return string English message text
- */
- private function getFallbackMessage( $key ) {
- $messages = [
- 'databaseerror-text' => 'A database query error has occurred.
-This may indicate a bug in the software.',
- 'databaseerror-textcl' => 'A database query error has occurred.',
- 'databaseerror-query' => 'Query: $1',
- 'databaseerror-function' => 'Function: $1',
- 'databaseerror-error' => 'Error: $1',
- ];
-
- return $messages[$key];
- }
-}
-
-/**
- * @ingroup Database
- */
-class DBUnexpectedError extends DBError {
-}
-
-/**
- * @ingroup Database
- */
-class DBReadOnlyError extends DBExpectedError {
- function getPageTitle() {
- return $this->msg( 'readonly', 'Database is locked' );
- }
-}
-
-/**
- * @ingroup Database
- */
-class DBTransactionError extends DBExpectedError {
-}
diff --git a/www/wiki/includes/db/DatabaseMssql.php b/www/wiki/includes/db/DatabaseMssql.php
deleted file mode 100644
index 33f81623..00000000
--- a/www/wiki/includes/db/DatabaseMssql.php
+++ /dev/null
@@ -1,1558 +0,0 @@
-<?php
-/**
- * This is the MS SQL Server Native database abstraction layer.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- * @author Joel Penner <a-joelpe at microsoft dot com>
- * @author Chris Pucci <a-cpucci at microsoft dot com>
- * @author Ryan Biesemeyer <v-ryanbi at microsoft dot com>
- * @author Ryan Schmidt <skizzerz at gmail dot com>
- */
-
-/**
- * @ingroup Database
- */
-class DatabaseMssql extends Database {
- protected $mInsertId = null;
- protected $mLastResult = null;
- protected $mAffectedRows = null;
- protected $mSubqueryId = 0;
- protected $mScrollableCursor = true;
- protected $mPrepareStatements = true;
- protected $mBinaryColumnCache = null;
- protected $mBitColumnCache = null;
- protected $mIgnoreDupKeyErrors = false;
- protected $mIgnoreErrors = [];
-
- protected $mPort;
-
- public function cascadingDeletes() {
- return true;
- }
-
- public function cleanupTriggers() {
- return false;
- }
-
- public function strictIPs() {
- return false;
- }
-
- public function realTimestamps() {
- return false;
- }
-
- public function implicitGroupby() {
- return false;
- }
-
- public function implicitOrderby() {
- return false;
- }
-
- public function functionalIndexes() {
- return true;
- }
-
- public function unionSupportsOrderAndLimit() {
- return false;
- }
-
- /**
- * Usually aborts on failure
- * @param string $server
- * @param string $user
- * @param string $password
- * @param string $dbName
- * @throws DBConnectionError
- * @return bool|DatabaseBase|null
- */
- public function open( $server, $user, $password, $dbName ) {
- # Test for driver support, to avoid suppressed fatal error
- if ( !function_exists( 'sqlsrv_connect' ) ) {
- throw new DBConnectionError(
- $this,
- "Microsoft SQL Server Native (sqlsrv) functions missing.
- You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n"
- );
- }
-
- global $wgDBport, $wgDBWindowsAuthentication;
-
- # e.g. the class is being loaded
- if ( !strlen( $user ) ) {
- return null;
- }
-
- $this->close();
- $this->mServer = $server;
- $this->mPort = $wgDBport;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
-
- $connectionInfo = [];
-
- if ( $dbName ) {
- $connectionInfo['Database'] = $dbName;
- }
-
- // Decide which auth scenerio to use
- // if we are using Windows auth, don't add credentials to $connectionInfo
- if ( !$wgDBWindowsAuthentication ) {
- $connectionInfo['UID'] = $user;
- $connectionInfo['PWD'] = $password;
- }
-
- MediaWiki\suppressWarnings();
- $this->mConn = sqlsrv_connect( $server, $connectionInfo );
- MediaWiki\restoreWarnings();
-
- if ( $this->mConn === false ) {
- throw new DBConnectionError( $this, $this->lastError() );
- }
-
- $this->mOpened = true;
-
- return $this->mConn;
- }
-
- /**
- * Closes a database connection, if it is open
- * Returns success, true if already closed
- * @return bool
- */
- protected function closeConnection() {
- return sqlsrv_close( $this->mConn );
- }
-
- /**
- * @param bool|MssqlResultWrapper|resource $result
- * @return bool|MssqlResultWrapper
- */
- protected function resultObject( $result ) {
- if ( !$result ) {
- return false;
- } elseif ( $result instanceof MssqlResultWrapper ) {
- return $result;
- } elseif ( $result === true ) {
- // Successful write query
- return $result;
- } else {
- return new MssqlResultWrapper( $this, $result );
- }
- }
-
- /**
- * @param string $sql
- * @return bool|MssqlResult
- * @throws DBUnexpectedError
- */
- protected function doQuery( $sql ) {
- if ( $this->debug() ) {
- wfDebug( "SQL: [$sql]\n" );
- }
- $this->offset = 0;
-
- // several extensions seem to think that all databases support limits
- // via LIMIT N after the WHERE clause well, MSSQL uses SELECT TOP N,
- // so to catch any of those extensions we'll do a quick check for a
- // LIMIT clause and pass $sql through $this->LimitToTopN() which parses
- // the limit clause and passes the result to $this->limitResult();
- if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) {
- // massage LIMIT -> TopN
- $sql = $this->LimitToTopN( $sql );
- }
-
- // MSSQL doesn't have EXTRACT(epoch FROM XXX)
- if ( preg_match( '#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
- // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
- $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
- }
-
- // perform query
-
- // SQLSRV_CURSOR_STATIC is slower than SQLSRV_CURSOR_CLIENT_BUFFERED (one of the two is
- // needed if we want to be able to seek around the result set), however CLIENT_BUFFERED
- // has a bug in the sqlsrv driver where wchar_t types (such as nvarchar) that are empty
- // strings make php throw a fatal error "Severe error translating Unicode"
- if ( $this->mScrollableCursor ) {
- $scrollArr = [ 'Scrollable' => SQLSRV_CURSOR_STATIC ];
- } else {
- $scrollArr = [];
- }
-
- if ( $this->mPrepareStatements ) {
- // we do prepare + execute so we can get its field metadata for later usage if desired
- $stmt = sqlsrv_prepare( $this->mConn, $sql, [], $scrollArr );
- $success = sqlsrv_execute( $stmt );
- } else {
- $stmt = sqlsrv_query( $this->mConn, $sql, [], $scrollArr );
- $success = (bool)$stmt;
- }
-
- // make a copy so that anything we add below does not get reflected in future queries
- $ignoreErrors = $this->mIgnoreErrors;
-
- if ( $this->mIgnoreDupKeyErrors ) {
- // ignore duplicate key errors
- // this emulates INSERT IGNORE in MySQL
- $ignoreErrors[] = '2601'; // duplicate key error caused by unique index
- $ignoreErrors[] = '2627'; // duplicate key error caused by primary key
- $ignoreErrors[] = '3621'; // generic "the statement has been terminated" error
- }
-
- if ( $success === false ) {
- $errors = sqlsrv_errors();
- $success = true;
-
- foreach ( $errors as $err ) {
- if ( !in_array( $err['code'], $ignoreErrors ) ) {
- $success = false;
- break;
- }
- }
-
- if ( $success === false ) {
- return false;
- }
- }
- // remember number of rows affected
- $this->mAffectedRows = sqlsrv_rows_affected( $stmt );
-
- return $stmt;
- }
-
- public function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- sqlsrv_free_stmt( $res );
- }
-
- /**
- * @param MssqlResultWrapper $res
- * @return stdClass
- */
- public function fetchObject( $res ) {
- // $res is expected to be an instance of MssqlResultWrapper here
- return $res->fetchObject();
- }
-
- /**
- * @param MssqlResultWrapper $res
- * @return array
- */
- public function fetchRow( $res ) {
- return $res->fetchRow();
- }
-
- /**
- * @param mixed $res
- * @return int
- */
- public function numRows( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- $ret = sqlsrv_num_rows( $res );
-
- if ( $ret === false ) {
- // we cannot get an amount of rows from this cursor type
- // has_rows returns bool true/false if the result has rows
- $ret = (int)sqlsrv_has_rows( $res );
- }
-
- return $ret;
- }
-
- /**
- * @param mixed $res
- * @return int
- */
- public function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return sqlsrv_num_fields( $res );
- }
-
- /**
- * @param mixed $res
- * @param int $n
- * @return int
- */
- public function fieldName( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return sqlsrv_field_metadata( $res )[$n]['Name'];
- }
-
- /**
- * This must be called after nextSequenceVal
- * @return int|null
- */
- public function insertId() {
- return $this->mInsertId;
- }
-
- /**
- * @param MssqlResultWrapper $res
- * @param int $row
- * @return bool
- */
- public function dataSeek( $res, $row ) {
- return $res->seek( $row );
- }
-
- /**
- * @return string
- */
- public function lastError() {
- $strRet = '';
- $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
- if ( $retErrors != null ) {
- foreach ( $retErrors as $arrError ) {
- $strRet .= $this->formatError( $arrError ) . "\n";
- }
- } else {
- $strRet = "No errors found";
- }
-
- return $strRet;
- }
-
- /**
- * @param array $err
- * @return string
- */
- private function formatError( $err ) {
- return '[SQLSTATE ' . $err['SQLSTATE'] . '][Error Code ' . $err['code'] . ']' . $err['message'];
- }
-
- /**
- * @return string
- */
- public function lastErrno() {
- $err = sqlsrv_errors( SQLSRV_ERR_ALL );
- if ( $err !== null && isset( $err[0] ) ) {
- return $err[0]['code'];
- } else {
- return 0;
- }
- }
-
- /**
- * @return int
- */
- public function affectedRows() {
- return $this->mAffectedRows;
- }
-
- /**
- * SELECT wrapper
- *
- * @param mixed $table Array or string, table name(s) (prefix auto-added)
- * @param mixed $vars Array or string, field name(s) to be retrieved
- * @param mixed $conds Array or string, condition(s) for WHERE
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @param array $options Associative array of options (e.g.
- * array('GROUP BY' => 'page_title')), see Database::makeSelectOptions
- * code for list of supported stuff
- * @param array $join_conds Associative array of table join conditions
- * (optional) (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
- * @return mixed Database result resource (feed to Database::fetchObject
- * or whatever), or false on failure
- * @throws DBQueryError
- * @throws DBUnexpectedError
- * @throws Exception
- */
- public function select( $table, $vars, $conds = '', $fname = __METHOD__,
- $options = [], $join_conds = []
- ) {
- $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
- if ( isset( $options['EXPLAIN'] ) ) {
- try {
- $this->mScrollableCursor = false;
- $this->mPrepareStatements = false;
- $this->query( "SET SHOWPLAN_ALL ON" );
- $ret = $this->query( $sql, $fname );
- $this->query( "SET SHOWPLAN_ALL OFF" );
- } catch ( DBQueryError $dqe ) {
- if ( isset( $options['FOR COUNT'] ) ) {
- // likely don't have privs for SHOWPLAN, so run a select count instead
- $this->query( "SET SHOWPLAN_ALL OFF" );
- unset( $options['EXPLAIN'] );
- $ret = $this->select(
- $table,
- 'COUNT(*) AS EstimateRows',
- $conds,
- $fname,
- $options,
- $join_conds
- );
- } else {
- // someone actually wanted the query plan instead of an est row count
- // let them know of the error
- $this->mScrollableCursor = true;
- $this->mPrepareStatements = true;
- throw $dqe;
- }
- }
- $this->mScrollableCursor = true;
- $this->mPrepareStatements = true;
- return $ret;
- }
- return $this->query( $sql, $fname );
- }
-
- /**
- * SELECT wrapper
- *
- * @param mixed $table Array or string, table name(s) (prefix auto-added)
- * @param mixed $vars Array or string, field name(s) to be retrieved
- * @param mixed $conds Array or string, condition(s) for WHERE
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
- * see Database::makeSelectOptions code for list of supported stuff
- * @param array $join_conds Associative array of table join conditions (optional)
- * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
- * @return string The SQL text
- */
- public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
- $options = [], $join_conds = []
- ) {
- if ( isset( $options['EXPLAIN'] ) ) {
- unset( $options['EXPLAIN'] );
- }
-
- $sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
-
- // try to rewrite aggregations of bit columns (currently MAX and MIN)
- if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) {
- $bitColumns = [];
- if ( is_array( $table ) ) {
- foreach ( $table as $t ) {
- $bitColumns += $this->getBitColumns( $this->tableName( $t ) );
- }
- } else {
- $bitColumns = $this->getBitColumns( $this->tableName( $table ) );
- }
-
- foreach ( $bitColumns as $col => $info ) {
- $replace = [
- "MAX({$col})" => "MAX(CAST({$col} AS tinyint))",
- "MIN({$col})" => "MIN(CAST({$col} AS tinyint))",
- ];
- $sql = str_replace( array_keys( $replace ), array_values( $replace ), $sql );
- }
- }
-
- return $sql;
- }
-
- public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
- $fname = __METHOD__
- ) {
- $this->mScrollableCursor = false;
- try {
- parent::deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname );
- } catch ( Exception $e ) {
- $this->mScrollableCursor = true;
- throw $e;
- }
- $this->mScrollableCursor = true;
- }
-
- public function delete( $table, $conds, $fname = __METHOD__ ) {
- $this->mScrollableCursor = false;
- try {
- parent::delete( $table, $conds, $fname );
- } catch ( Exception $e ) {
- $this->mScrollableCursor = true;
- throw $e;
- }
- $this->mScrollableCursor = true;
- }
-
- /**
- * Estimate rows in dataset
- * Returns estimated count, based on SHOWPLAN_ALL output
- * This is not necessarily an accurate estimate, so use sparingly
- * Returns -1 if count cannot be found
- * Takes same arguments as Database::select()
- * @param string $table
- * @param string $vars
- * @param string $conds
- * @param string $fname
- * @param array $options
- * @return int
- */
- public function estimateRowCount( $table, $vars = '*', $conds = '',
- $fname = __METHOD__, $options = []
- ) {
- // http://msdn2.microsoft.com/en-us/library/aa259203.aspx
- $options['EXPLAIN'] = true;
- $options['FOR COUNT'] = true;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
-
- $rows = -1;
- if ( $res ) {
- $row = $this->fetchRow( $res );
-
- if ( isset( $row['EstimateRows'] ) ) {
- $rows = (int)$row['EstimateRows'];
- }
- }
-
- return $rows;
- }
-
- /**
- * Returns information about an index
- * If errors are explicitly ignored, returns NULL on failure
- * @param string $table
- * @param string $index
- * @param string $fname
- * @return array|bool|null
- */
- public function indexInfo( $table, $index, $fname = __METHOD__ ) {
- # This does not return the same info as MYSQL would, but that's OK
- # because MediaWiki never uses the returned value except to check for
- # the existance of indexes.
- $sql = "sp_helpindex '" . $this->tableName( $table ) . "'";
- $res = $this->query( $sql, $fname );
-
- if ( !$res ) {
- return null;
- }
-
- $result = [];
- foreach ( $res as $row ) {
- if ( $row->index_name == $index ) {
- $row->Non_unique = !stristr( $row->index_description, "unique" );
- $cols = explode( ", ", $row->index_keys );
- foreach ( $cols as $col ) {
- $row->Column_name = trim( $col );
- $result[] = clone $row;
- }
- } elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) {
- $row->Non_unique = 0;
- $cols = explode( ", ", $row->index_keys );
- foreach ( $cols as $col ) {
- $row->Column_name = trim( $col );
- $result[] = clone $row;
- }
- }
- }
-
- return empty( $result ) ? false : $result;
- }
-
- /**
- * INSERT wrapper, inserts an array into a table
- *
- * $arrToInsert may be a single associative array, or an array of these with numeric keys, for
- * multi-row insert.
- *
- * Usually aborts on failure
- * If errors are explicitly ignored, returns success
- * @param string $table
- * @param array $arrToInsert
- * @param string $fname
- * @param array $options
- * @return bool
- * @throws Exception
- */
- public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = [] ) {
- # No rows to insert, easy just return now
- if ( !count( $arrToInsert ) ) {
- return true;
- }
-
- if ( !is_array( $options ) ) {
- $options = [ $options ];
- }
-
- $table = $this->tableName( $table );
-
- if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) { // Not multi row
- $arrToInsert = [ 0 => $arrToInsert ]; // make everything multi row compatible
- }
-
- // We know the table we're inserting into, get its identity column
- $identity = null;
- // strip matching square brackets and the db/schema from table name
- $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
- $tableRaw = array_pop( $tableRawArr );
- $res = $this->doQuery(
- "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS " .
- "WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'"
- );
- if ( $res && sqlsrv_has_rows( $res ) ) {
- // There is an identity for this table.
- $identityArr = sqlsrv_fetch_array( $res, SQLSRV_FETCH_ASSOC );
- $identity = array_pop( $identityArr );
- }
- sqlsrv_free_stmt( $res );
-
- // Determine binary/varbinary fields so we can encode data as a hex string like 0xABCDEF
- $binaryColumns = $this->getBinaryColumns( $table );
-
- // INSERT IGNORE is not supported by SQL Server
- // remove IGNORE from options list and set ignore flag to true
- if ( in_array( 'IGNORE', $options ) ) {
- $options = array_diff( $options, [ 'IGNORE' ] );
- $this->mIgnoreDupKeyErrors = true;
- }
-
- foreach ( $arrToInsert as $a ) {
- // start out with empty identity column, this is so we can return
- // it as a result of the insert logic
- $sqlPre = '';
- $sqlPost = '';
- $identityClause = '';
-
- // if we have an identity column
- if ( $identity ) {
- // iterate through
- foreach ( $a as $k => $v ) {
- if ( $k == $identity ) {
- if ( !is_null( $v ) ) {
- // there is a value being passed to us,
- // we need to turn on and off inserted identity
- $sqlPre = "SET IDENTITY_INSERT $table ON;";
- $sqlPost = ";SET IDENTITY_INSERT $table OFF;";
- } else {
- // we can't insert NULL into an identity column,
- // so remove the column from the insert.
- unset( $a[$k] );
- }
- }
- }
-
- // we want to output an identity column as result
- $identityClause = "OUTPUT INSERTED.$identity ";
- }
-
- $keys = array_keys( $a );
-
- // Build the actual query
- $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
- " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
-
- $first = true;
- foreach ( $a as $key => $value ) {
- if ( isset( $binaryColumns[$key] ) ) {
- $value = new MssqlBlob( $value );
- }
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- if ( is_null( $value ) ) {
- $sql .= 'null';
- } elseif ( is_array( $value ) || is_object( $value ) ) {
- if ( is_object( $value ) && $value instanceof Blob ) {
- $sql .= $this->addQuotes( $value );
- } else {
- $sql .= $this->addQuotes( serialize( $value ) );
- }
- } else {
- $sql .= $this->addQuotes( $value );
- }
- }
- $sql .= ')' . $sqlPost;
-
- // Run the query
- $this->mScrollableCursor = false;
- try {
- $ret = $this->query( $sql );
- } catch ( Exception $e ) {
- $this->mScrollableCursor = true;
- $this->mIgnoreDupKeyErrors = false;
- throw $e;
- }
- $this->mScrollableCursor = true;
-
- if ( !is_null( $identity ) ) {
- // then we want to get the identity column value we were assigned and save it off
- $row = $ret->fetchObject();
- if ( is_object( $row ) ) {
- $this->mInsertId = $row->$identity;
-
- // it seems that mAffectedRows is -1 sometimes when OUTPUT INSERTED.identity is used
- // if we got an identity back, we know for sure a row was affected, so adjust that here
- if ( $this->mAffectedRows == -1 ) {
- $this->mAffectedRows = 1;
- }
- }
- }
- }
- $this->mIgnoreDupKeyErrors = false;
- return $ret;
- }
-
- /**
- * INSERT SELECT wrapper
- * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
- * Source items may be literals rather than field names, but strings should
- * be quoted with Database::addQuotes().
- * @param string $destTable
- * @param array|string $srcTable May be an array of tables.
- * @param array $varMap
- * @param array $conds May be "*" to copy the whole table.
- * @param string $fname
- * @param array $insertOptions
- * @param array $selectOptions
- * @return null|ResultWrapper
- * @throws Exception
- */
- public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
- $insertOptions = [], $selectOptions = []
- ) {
- $this->mScrollableCursor = false;
- try {
- $ret = parent::insertSelect(
- $destTable,
- $srcTable,
- $varMap,
- $conds,
- $fname,
- $insertOptions,
- $selectOptions
- );
- } catch ( Exception $e ) {
- $this->mScrollableCursor = true;
- throw $e;
- }
- $this->mScrollableCursor = true;
-
- return $ret;
- }
-
- /**
- * UPDATE wrapper. Takes a condition array and a SET array.
- *
- * @param string $table Name of the table to UPDATE. This will be passed through
- * DatabaseBase::tableName().
- *
- * @param array $values An array of values to SET. For each array element,
- * the key gives the field name, and the value gives the data
- * to set that field to. The data will be quoted by
- * DatabaseBase::addQuotes().
- *
- * @param array $conds An array of conditions (WHERE). See
- * DatabaseBase::select() for the details of the format of
- * condition arrays. Use '*' to update all rows.
- *
- * @param string $fname The function name of the caller (from __METHOD__),
- * for logging and profiling.
- *
- * @param array $options An array of UPDATE options, can be:
- * - IGNORE: Ignore unique key conflicts
- * - LOW_PRIORITY: MySQL-specific, see MySQL manual.
- * @return bool
- * @throws DBUnexpectedError
- * @throws Exception
- * @throws MWException
- */
- function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
- $table = $this->tableName( $table );
- $binaryColumns = $this->getBinaryColumns( $table );
-
- $opts = $this->makeUpdateOptions( $options );
- $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET, $binaryColumns );
-
- if ( $conds !== [] && $conds !== '*' ) {
- $sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns );
- }
-
- $this->mScrollableCursor = false;
- try {
- $ret = $this->query( $sql );
- } catch ( Exception $e ) {
- $this->mScrollableCursor = true;
- throw $e;
- }
- $this->mScrollableCursor = true;
- return true;
- }
-
- /**
- * Makes an encoded list of strings from an array
- * @param array $a Containing the data
- * @param int $mode Constant
- * - LIST_COMMA: comma separated, no field names
- * - LIST_AND: ANDed WHERE clause (without the WHERE). See
- * the documentation for $conds in DatabaseBase::select().
- * - LIST_OR: ORed WHERE clause (without the WHERE)
- * - LIST_SET: comma separated with field names, like a SET clause
- * - LIST_NAMES: comma separated field names
- * @param array $binaryColumns Contains a list of column names that are binary types
- * This is a custom parameter only present for MS SQL.
- *
- * @throws MWException|DBUnexpectedError
- * @return string
- */
- public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = [] ) {
- if ( !is_array( $a ) ) {
- throw new DBUnexpectedError( $this,
- 'DatabaseBase::makeList called with incorrect parameters' );
- }
-
- if ( $mode != LIST_NAMES ) {
- // In MS SQL, values need to be specially encoded when they are
- // inserted into binary fields. Perform this necessary encoding
- // for the specified set of columns.
- foreach ( array_keys( $a ) as $field ) {
- if ( !isset( $binaryColumns[$field] ) ) {
- continue;
- }
-
- if ( is_array( $a[$field] ) ) {
- foreach ( $a[$field] as &$v ) {
- $v = new MssqlBlob( $v );
- }
- unset( $v );
- } else {
- $a[$field] = new MssqlBlob( $a[$field] );
- }
- }
- }
-
- return parent::makeList( $a, $mode );
- }
-
- /**
- * @param string $table
- * @param string $field
- * @return int Returns the size of a text field, or -1 for "unlimited"
- */
- public function textFieldSize( $table, $field ) {
- $table = $this->tableName( $table );
- $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns
- WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'";
- $res = $this->query( $sql );
- $row = $this->fetchRow( $res );
- $size = -1;
- if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) {
- $size = $row['CHARACTER_MAXIMUM_LENGTH'];
- }
-
- return $size;
- }
-
- /**
- * Construct a LIMIT query with optional offset
- * This is used for query pages
- *
- * @param string $sql SQL query we will append the limit too
- * @param int $limit The SQL limit
- * @param bool|int $offset The SQL offset (default false)
- * @return array|string
- * @throws DBUnexpectedError
- */
- public function limitResult( $sql, $limit, $offset = false ) {
- if ( $offset === false || $offset == 0 ) {
- if ( strpos( $sql, "SELECT" ) === false ) {
- return "TOP {$limit} " . $sql;
- } else {
- return preg_replace( '/\bSELECT(\s+DISTINCT)?\b/Dsi',
- 'SELECT$1 TOP ' . $limit, $sql, 1 );
- }
- } else {
- // This one is fun, we need to pull out the select list as well as any ORDER BY clause
- $select = $orderby = [];
- $s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select );
- $s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby );
- $overOrder = $postOrder = '';
- $first = $offset + 1;
- $last = $offset + $limit;
- $sub1 = 'sub_' . $this->mSubqueryId;
- $sub2 = 'sub_' . ( $this->mSubqueryId + 1 );
- $this->mSubqueryId += 2;
- if ( !$s1 ) {
- // wat
- throw new DBUnexpectedError( $this, "Attempting to LIMIT a non-SELECT query\n" );
- }
- if ( !$s2 ) {
- // no ORDER BY
- $overOrder = 'ORDER BY (SELECT 1)';
- } else {
- if ( !isset( $orderby[2] ) || !$orderby[2] ) {
- // don't need to strip it out if we're using a FOR XML clause
- $sql = str_replace( $orderby[1], '', $sql );
- }
- $overOrder = $orderby[1];
- $postOrder = ' ' . $overOrder;
- }
- $sql = "SELECT {$select[1]}
- FROM (
- SELECT ROW_NUMBER() OVER({$overOrder}) AS rowNumber, *
- FROM ({$sql}) {$sub1}
- ) {$sub2}
- WHERE rowNumber BETWEEN {$first} AND {$last}{$postOrder}";
-
- return $sql;
- }
- }
-
- /**
- * If there is a limit clause, parse it, strip it, and pass the remaining
- * SQL through limitResult() with the appropriate parameters. Not the
- * prettiest solution, but better than building a whole new parser. This
- * exists becase there are still too many extensions that don't use dynamic
- * sql generation.
- *
- * @param string $sql
- * @return array|mixed|string
- */
- public function LimitToTopN( $sql ) {
- // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
- $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
- if ( preg_match( $pattern, $sql, $matches ) ) {
- $row_count = $matches[4];
- $offset = $matches[3] ?: $matches[6] ?: false;
-
- // strip the matching LIMIT clause out
- $sql = str_replace( $matches[0], '', $sql );
-
- return $this->limitResult( $sql, $row_count, $offset );
- }
-
- return $sql;
- }
-
- /**
- * @return string Wikitext of a link to the server software's web site
- */
- public function getSoftwareLink() {
- return "[{{int:version-db-mssql-url}} MS SQL Server]";
- }
-
- /**
- * @return string Version information from the database
- */
- public function getServerVersion() {
- $server_info = sqlsrv_server_info( $this->mConn );
- $version = 'Error';
- if ( isset( $server_info['SQLServerVersion'] ) ) {
- $version = $server_info['SQLServerVersion'];
- }
-
- return $version;
- }
-
- /**
- * @param string $table
- * @param string $fname
- * @return bool
- */
- public function tableExists( $table, $fname = __METHOD__ ) {
- list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
-
- if ( $db !== false ) {
- // remote database
- wfDebug( "Attempting to call tableExists on a remote table" );
- return false;
- }
-
- if ( $schema === false ) {
- global $wgDBmwschema;
- $schema = $wgDBmwschema;
- }
-
- $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES
- WHERE TABLE_TYPE = 'BASE TABLE'
- AND TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table'" );
-
- if ( $res->numRows() ) {
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Query whether a given column exists in the mediawiki schema
- * @param string $table
- * @param string $field
- * @param string $fname
- * @return bool
- */
- public function fieldExists( $table, $field, $fname = __METHOD__ ) {
- list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
-
- if ( $db !== false ) {
- // remote database
- wfDebug( "Attempting to call fieldExists on a remote table" );
- return false;
- }
-
- $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
- WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
-
- if ( $res->numRows() ) {
- return true;
- } else {
- return false;
- }
- }
-
- public function fieldInfo( $table, $field ) {
- list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
-
- if ( $db !== false ) {
- // remote database
- wfDebug( "Attempting to call fieldInfo on a remote table" );
- return false;
- }
-
- $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.COLUMNS
- WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
-
- $meta = $res->fetchRow();
- if ( $meta ) {
- return new MssqlField( $meta );
- }
-
- return false;
- }
-
- /**
- * Begin a transaction, committing any previously open transaction
- * @param string $fname
- */
- protected function doBegin( $fname = __METHOD__ ) {
- sqlsrv_begin_transaction( $this->mConn );
- $this->mTrxLevel = 1;
- }
-
- /**
- * End a transaction
- * @param string $fname
- */
- protected function doCommit( $fname = __METHOD__ ) {
- sqlsrv_commit( $this->mConn );
- $this->mTrxLevel = 0;
- }
-
- /**
- * Rollback a transaction.
- * No-op on non-transactional databases.
- * @param string $fname
- */
- protected function doRollback( $fname = __METHOD__ ) {
- sqlsrv_rollback( $this->mConn );
- $this->mTrxLevel = 0;
- }
-
- /**
- * Escapes a identifier for use inm SQL.
- * Throws an exception if it is invalid.
- * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx
- * @param string $identifier
- * @throws MWException
- * @return string
- */
- private function escapeIdentifier( $identifier ) {
- if ( strlen( $identifier ) == 0 ) {
- throw new MWException( "An identifier must not be empty" );
- }
- if ( strlen( $identifier ) > 128 ) {
- throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
- }
- if ( ( strpos( $identifier, '[' ) !== false )
- || ( strpos( $identifier, ']' ) !== false )
- ) {
- // It may be allowed if you quoted with double quotation marks, but
- // that would break if QUOTED_IDENTIFIER is OFF
- throw new MWException( "Square brackets are not allowed in '$identifier'" );
- }
-
- return "[$identifier]";
- }
-
- /**
- * @param string $s
- * @return string
- */
- public function strencode( $s ) {
- // Should not be called by us
-
- return str_replace( "'", "''", $s );
- }
-
- /**
- * @param string|Blob $s
- * @return string
- */
- public function addQuotes( $s ) {
- if ( $s instanceof MssqlBlob ) {
- return $s->fetch();
- } elseif ( $s instanceof Blob ) {
- // this shouldn't really ever be called, but it's here if needed
- // (and will quite possibly make the SQL error out)
- $blob = new MssqlBlob( $s->fetch() );
- return $blob->fetch();
- } else {
- if ( is_bool( $s ) ) {
- $s = $s ? 1 : 0;
- }
- return parent::addQuotes( $s );
- }
- }
-
- /**
- * @param string $s
- * @return string
- */
- public function addIdentifierQuotes( $s ) {
- // http://msdn.microsoft.com/en-us/library/aa223962.aspx
- return '[' . $s . ']';
- }
-
- /**
- * @param string $name
- * @return bool
- */
- public function isQuotedIdentifier( $name ) {
- return strlen( $name ) && $name[0] == '[' && substr( $name, -1, 1 ) == ']';
- }
-
- /**
- * MS SQL supports more pattern operators than other databases (ex: [,],^)
- *
- * @param string $s
- * @return string
- */
- protected function escapeLikeInternal( $s ) {
- return addcslashes( $s, '\%_[]^' );
- }
-
- /**
- * MS SQL requires specifying the escape character used in a LIKE query
- * or using Square brackets to surround characters that are to be escaped
- * http://msdn.microsoft.com/en-us/library/ms179859.aspx
- * Here we take the Specify-Escape-Character approach since it's less
- * invasive, renders a query that is closer to other DB's and better at
- * handling square bracket escaping
- *
- * @return string Fully built LIKE statement
- */
- public function buildLike() {
- $params = func_get_args();
- if ( count( $params ) > 0 && is_array( $params[0] ) ) {
- $params = $params[0];
- }
-
- return parent::buildLike( $params ) . " ESCAPE '\' ";
- }
-
- /**
- * @param string $db
- * @return bool
- */
- public function selectDB( $db ) {
- try {
- $this->mDBname = $db;
- $this->query( "USE $db" );
- return true;
- } catch ( Exception $e ) {
- return false;
- }
- }
-
- /**
- * @param array $options An associative array of options to be turned into
- * an SQL query, valid keys are listed in the function.
- * @return array
- */
- public function makeSelectOptions( $options ) {
- $tailOpts = '';
- $startOpts = '';
-
- $noKeyOptions = [];
- foreach ( $options as $key => $option ) {
- if ( is_numeric( $key ) ) {
- $noKeyOptions[$option] = true;
- }
- }
-
- $tailOpts .= $this->makeGroupByWithHaving( $options );
-
- $tailOpts .= $this->makeOrderBy( $options );
-
- if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
- $startOpts .= 'DISTINCT';
- }
-
- if ( isset( $noKeyOptions['FOR XML'] ) ) {
- // used in group concat field emulation
- $tailOpts .= " FOR XML PATH('')";
- }
-
- // we want this to be compatible with the output of parent::makeSelectOptions()
- return [ $startOpts, '', $tailOpts, '' ];
- }
-
- /**
- * Get the type of the DBMS, as it appears in $wgDBtype.
- * @return string
- */
- public function getType() {
- return 'mssql';
- }
-
- /**
- * @param array $stringList
- * @return string
- */
- public function buildConcat( $stringList ) {
- return implode( ' + ', $stringList );
- }
-
- /**
- * Build a GROUP_CONCAT or equivalent statement for a query.
- * MS SQL doesn't have GROUP_CONCAT so we emulate it with other stuff (and boy is it nasty)
- *
- * This is useful for combining a field for several rows into a single string.
- * NULL values will not appear in the output, duplicated values will appear,
- * and the resulting delimiter-separated values have no defined sort order.
- * Code using the results may need to use the PHP unique() or sort() methods.
- *
- * @param string $delim Glue to bind the results together
- * @param string|array $table Table name
- * @param string $field Field name
- * @param string|array $conds Conditions
- * @param string|array $join_conds Join conditions
- * @return string SQL text
- * @since 1.23
- */
- public function buildGroupConcatField( $delim, $table, $field, $conds = '',
- $join_conds = []
- ) {
- $gcsq = 'gcsq_' . $this->mSubqueryId;
- $this->mSubqueryId++;
-
- $delimLen = strlen( $delim );
- $fld = "{$field} + {$this->addQuotes( $delim )}";
- $sql = "(SELECT LEFT({$field}, LEN({$field}) - {$delimLen}) FROM ("
- . $this->selectSQLText( $table, $fld, $conds, null, [ 'FOR XML' ], $join_conds )
- . ") {$gcsq} ({$field}))";
-
- return $sql;
- }
-
- /**
- * @return string
- */
- public function getSearchEngine() {
- return "SearchMssql";
- }
-
- /**
- * Returns an associative array for fields that are of type varbinary, binary, or image
- * $table can be either a raw table name or passed through tableName() first
- * @param string $table
- * @return array
- */
- private function getBinaryColumns( $table ) {
- $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
- $tableRaw = array_pop( $tableRawArr );
-
- if ( $this->mBinaryColumnCache === null ) {
- $this->populateColumnCaches();
- }
-
- return isset( $this->mBinaryColumnCache[$tableRaw] )
- ? $this->mBinaryColumnCache[$tableRaw]
- : [];
- }
-
- /**
- * @param string $table
- * @return array
- */
- private function getBitColumns( $table ) {
- $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
- $tableRaw = array_pop( $tableRawArr );
-
- if ( $this->mBitColumnCache === null ) {
- $this->populateColumnCaches();
- }
-
- return isset( $this->mBitColumnCache[$tableRaw] )
- ? $this->mBitColumnCache[$tableRaw]
- : [];
- }
-
- private function populateColumnCaches() {
- $res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*',
- [
- 'TABLE_CATALOG' => $this->mDBname,
- 'TABLE_SCHEMA' => $this->mSchema,
- 'DATA_TYPE' => [ 'varbinary', 'binary', 'image', 'bit' ]
- ] );
-
- $this->mBinaryColumnCache = [];
- $this->mBitColumnCache = [];
- foreach ( $res as $row ) {
- if ( $row->DATA_TYPE == 'bit' ) {
- $this->mBitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
- } else {
- $this->mBinaryColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
- }
- }
- }
-
- /**
- * @param string $name
- * @param string $format
- * @return string
- */
- function tableName( $name, $format = 'quoted' ) {
- # Replace reserved words with better ones
- switch ( $name ) {
- case 'user':
- return $this->realTableName( 'mwuser', $format );
- default:
- return $this->realTableName( $name, $format );
- }
- }
-
- /**
- * call this instead of tableName() in the updater when renaming tables
- * @param string $name
- * @param string $format One of quoted, raw, or split
- * @return string
- */
- function realTableName( $name, $format = 'quoted' ) {
- $table = parent::tableName( $name, $format );
- if ( $format == 'split' ) {
- // Used internally, we want the schema split off from the table name and returned
- // as a list with 3 elements (database, schema, table)
- $table = explode( '.', $table );
- while ( count( $table ) < 3 ) {
- array_unshift( $table, false );
- }
- }
- return $table;
- }
-
- /**
- * Delete a table
- * @param string $tableName
- * @param string $fName
- * @return bool|ResultWrapper
- * @since 1.18
- */
- public function dropTable( $tableName, $fName = __METHOD__ ) {
- if ( !$this->tableExists( $tableName, $fName ) ) {
- return false;
- }
-
- // parent function incorrectly appends CASCADE, which we don't want
- $sql = "DROP TABLE " . $this->tableName( $tableName );
-
- return $this->query( $sql, $fName );
- }
-
- /**
- * Called in the installer and updater.
- * Probably doesn't need to be called anywhere else in the codebase.
- * @param bool|null $value
- * @return bool|null
- */
- public function prepareStatements( $value = null ) {
- return wfSetVar( $this->mPrepareStatements, $value );
- }
-
- /**
- * Called in the installer and updater.
- * Probably doesn't need to be called anywhere else in the codebase.
- * @param bool|null $value
- * @return bool|null
- */
- public function scrollableCursor( $value = null ) {
- return wfSetVar( $this->mScrollableCursor, $value );
- }
-
- /**
- * Called in the installer and updater.
- * Probably doesn't need to be called anywhere else in the codebase.
- * @param array|null $value
- * @return array|null
- */
- public function ignoreErrors( array $value = null ) {
- return wfSetVar( $this->mIgnoreErrors, $value );
- }
-} // end DatabaseMssql class
-
-/**
- * Utility class.
- *
- * @ingroup Database
- */
-class MssqlField implements Field {
- private $name, $tableName, $default, $max_length, $nullable, $type;
-
- function __construct( $info ) {
- $this->name = $info['COLUMN_NAME'];
- $this->tableName = $info['TABLE_NAME'];
- $this->default = $info['COLUMN_DEFAULT'];
- $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH'];
- $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' );
- $this->type = $info['DATA_TYPE'];
- }
-
- function name() {
- return $this->name;
- }
-
- function tableName() {
- return $this->tableName;
- }
-
- function defaultValue() {
- return $this->default;
- }
-
- function maxLength() {
- return $this->max_length;
- }
-
- function isNullable() {
- return $this->nullable;
- }
-
- function type() {
- return $this->type;
- }
-}
-
-class MssqlBlob extends Blob {
- public function __construct( $data ) {
- if ( $data instanceof MssqlBlob ) {
- return $data;
- } elseif ( $data instanceof Blob ) {
- $this->mData = $data->fetch();
- } elseif ( is_array( $data ) && is_object( $data ) ) {
- $this->mData = serialize( $data );
- } else {
- $this->mData = $data;
- }
- }
-
- /**
- * Returns an unquoted hex representation of a binary string
- * for insertion into varbinary-type fields
- * @return string
- */
- public function fetch() {
- if ( $this->mData === null ) {
- return 'null';
- }
-
- $ret = '0x';
- $dataLength = strlen( $this->mData );
- for ( $i = 0; $i < $dataLength; $i++ ) {
- $ret .= bin2hex( pack( 'C', ord( $this->mData[$i] ) ) );
- }
-
- return $ret;
- }
-}
-
-class MssqlResultWrapper extends ResultWrapper {
- private $mSeekTo = null;
-
- /**
- * @return stdClass|bool
- */
- public function fetchObject() {
- $res = $this->result;
-
- if ( $this->mSeekTo !== null ) {
- $result = sqlsrv_fetch_object( $res, 'stdClass', [],
- SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo );
- $this->mSeekTo = null;
- } else {
- $result = sqlsrv_fetch_object( $res );
- }
-
- // MediaWiki expects us to return boolean false when there are no more rows instead of null
- if ( $result === null ) {
- return false;
- }
-
- return $result;
- }
-
- /**
- * @return array|bool
- */
- public function fetchRow() {
- $res = $this->result;
-
- if ( $this->mSeekTo !== null ) {
- $result = sqlsrv_fetch_array( $res, SQLSRV_FETCH_BOTH,
- SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo );
- $this->mSeekTo = null;
- } else {
- $result = sqlsrv_fetch_array( $res );
- }
-
- // MediaWiki expects us to return boolean false when there are no more rows instead of null
- if ( $result === null ) {
- return false;
- }
-
- return $result;
- }
-
- /**
- * @param int $row
- * @return bool
- */
- public function seek( $row ) {
- $res = $this->result;
-
- // check bounds
- $numRows = $this->db->numRows( $res );
- $row = intval( $row );
-
- if ( $numRows === 0 ) {
- return false;
- } elseif ( $row < 0 || $row > $numRows - 1 ) {
- return false;
- }
-
- // Unlike MySQL, the seek actually happens on the next access
- $this->mSeekTo = $row;
- return true;
- }
-}
diff --git a/www/wiki/includes/db/DatabaseMysql.php b/www/wiki/includes/db/DatabaseMysql.php
deleted file mode 100644
index 5b151477..00000000
--- a/www/wiki/includes/db/DatabaseMysql.php
+++ /dev/null
@@ -1,210 +0,0 @@
-<?php
-/**
- * This is the MySQL database abstraction layer.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * Database abstraction object for PHP extension mysql.
- *
- * @ingroup Database
- * @see Database
- */
-class DatabaseMysql extends DatabaseMysqlBase {
- /**
- * @param string $sql
- * @return resource False on error
- */
- protected function doQuery( $sql ) {
- $conn = $this->getBindingHandle();
-
- if ( $this->bufferResults() ) {
- $ret = mysql_query( $sql, $conn );
- } else {
- $ret = mysql_unbuffered_query( $sql, $conn );
- }
-
- return $ret;
- }
-
- /**
- * @param string $realServer
- * @return bool|resource MySQL Database connection or false on failure to connect
- * @throws DBConnectionError
- */
- protected function mysqlConnect( $realServer ) {
- # Avoid a suppressed fatal error, which is very hard to track down
- if ( !extension_loaded( 'mysql' ) ) {
- throw new DBConnectionError(
- $this,
- "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n"
- );
- }
-
- $connFlags = 0;
- if ( $this->mFlags & DBO_SSL ) {
- $connFlags |= MYSQL_CLIENT_SSL;
- }
- if ( $this->mFlags & DBO_COMPRESS ) {
- $connFlags |= MYSQL_CLIENT_COMPRESS;
- }
-
- if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) {
- $numAttempts = 2;
- } else {
- $numAttempts = 1;
- }
-
- $conn = false;
-
- # The kernel's default SYN retransmission period is far too slow for us,
- # so we use a short timeout plus a manual retry. Retrying means that a small
- # but finite rate of SYN packet loss won't cause user-visible errors.
- for ( $i = 0; $i < $numAttempts && !$conn; $i++ ) {
- if ( $i > 1 ) {
- usleep( 1000 );
- }
- if ( $this->mFlags & DBO_PERSISTENT ) {
- $conn = mysql_pconnect( $realServer, $this->mUser, $this->mPassword, $connFlags );
- } else {
- # Create a new connection...
- $conn = mysql_connect( $realServer, $this->mUser, $this->mPassword, true, $connFlags );
- }
- }
-
- return $conn;
- }
-
- /**
- * @param string $charset
- * @return bool
- */
- protected function mysqlSetCharset( $charset ) {
- $conn = $this->getBindingHandle();
-
- if ( function_exists( 'mysql_set_charset' ) ) {
- return mysql_set_charset( $charset, $conn );
- } else {
- return $this->query( 'SET NAMES ' . $charset, __METHOD__ );
- }
- }
-
- /**
- * @return bool
- */
- protected function closeConnection() {
- $conn = $this->getBindingHandle();
-
- return mysql_close( $conn );
- }
-
- /**
- * @return int
- */
- function insertId() {
- $conn = $this->getBindingHandle();
-
- return mysql_insert_id( $conn );
- }
-
- /**
- * @return int
- */
- function lastErrno() {
- if ( $this->mConn ) {
- return mysql_errno( $this->mConn );
- } else {
- return mysql_errno();
- }
- }
-
- /**
- * @return int
- */
- function affectedRows() {
- $conn = $this->getBindingHandle();
-
- return mysql_affected_rows( $conn );
- }
-
- /**
- * @param string $db
- * @return bool
- */
- function selectDB( $db ) {
- $conn = $this->getBindingHandle();
-
- $this->mDBname = $db;
-
- return mysql_select_db( $db, $conn );
- }
-
- protected function mysqlFreeResult( $res ) {
- return mysql_free_result( $res );
- }
-
- protected function mysqlFetchObject( $res ) {
- return mysql_fetch_object( $res );
- }
-
- protected function mysqlFetchArray( $res ) {
- return mysql_fetch_array( $res );
- }
-
- protected function mysqlNumRows( $res ) {
- return mysql_num_rows( $res );
- }
-
- protected function mysqlNumFields( $res ) {
- return mysql_num_fields( $res );
- }
-
- protected function mysqlFetchField( $res, $n ) {
- return mysql_fetch_field( $res, $n );
- }
-
- protected function mysqlFieldName( $res, $n ) {
- return mysql_field_name( $res, $n );
- }
-
- protected function mysqlFieldType( $res, $n ) {
- return mysql_field_type( $res, $n );
- }
-
- protected function mysqlDataSeek( $res, $row ) {
- return mysql_data_seek( $res, $row );
- }
-
- protected function mysqlError( $conn = null ) {
- return ( $conn !== null ) ? mysql_error( $conn ) : mysql_error(); // avoid warning
- }
-
- protected function mysqlRealEscapeString( $s ) {
- $conn = $this->getBindingHandle();
-
- return mysql_real_escape_string( $s, $conn );
- }
-
- protected function mysqlPing() {
- $conn = $this->getBindingHandle();
-
- return mysql_ping( $conn );
- }
-}
diff --git a/www/wiki/includes/db/DatabaseMysqlBase.php b/www/wiki/includes/db/DatabaseMysqlBase.php
deleted file mode 100644
index 13be9116..00000000
--- a/www/wiki/includes/db/DatabaseMysqlBase.php
+++ /dev/null
@@ -1,1512 +0,0 @@
-<?php
-/**
- * This is the MySQL database abstraction layer.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * Database abstraction object for MySQL.
- * Defines methods independent on used MySQL extension.
- *
- * @ingroup Database
- * @since 1.22
- * @see Database
- */
-abstract class DatabaseMysqlBase extends Database {
- /** @var MysqlMasterPos */
- protected $lastKnownSlavePos;
- /** @var string Method to detect slave lag */
- protected $lagDetectionMethod;
- /** @var array Method to detect slave lag */
- protected $lagDetectionOptions = [];
-
- /** @var string|null */
- private $serverVersion = null;
-
- /**
- * Additional $params include:
- * - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
- * pt-heartbeat assumes the table is at heartbeat.heartbeat
- * and uses UTC timestamps in the heartbeat.ts column.
- * (https://www.percona.com/doc/percona-toolkit/2.2/pt-heartbeat.html)
- * - lagDetectionOptions : if using pt-heartbeat, this can be set to an array map to change
- * the default behavior. Normally, the heartbeat row with the server
- * ID of this server's master will be used. Set the "conds" field to
- * override the query conditions, e.g. ['shard' => 's1'].
- * @param array $params
- */
- function __construct( array $params ) {
- parent::__construct( $params );
-
- $this->lagDetectionMethod = isset( $params['lagDetectionMethod'] )
- ? $params['lagDetectionMethod']
- : 'Seconds_Behind_Master';
- $this->lagDetectionOptions = isset( $params['lagDetectionOptions'] )
- ? $params['lagDetectionOptions']
- : [];
- }
-
- /**
- * @return string
- */
- function getType() {
- return 'mysql';
- }
-
- /**
- * @param string $server
- * @param string $user
- * @param string $password
- * @param string $dbName
- * @throws Exception|DBConnectionError
- * @return bool
- */
- function open( $server, $user, $password, $dbName ) {
- global $wgAllDBsAreLocalhost, $wgSQLMode;
-
- # Close/unset connection handle
- $this->close();
-
- # Debugging hack -- fake cluster
- $realServer = $wgAllDBsAreLocalhost ? 'localhost' : $server;
- $this->mServer = $server;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
-
- $this->installErrorHandler();
- try {
- $this->mConn = $this->mysqlConnect( $realServer );
- } catch ( Exception $ex ) {
- $this->restoreErrorHandler();
- throw $ex;
- }
- $error = $this->restoreErrorHandler();
-
- # Always log connection errors
- if ( !$this->mConn ) {
- if ( !$error ) {
- $error = $this->lastError();
- }
- wfLogDBError(
- "Error connecting to {db_server}: {error}",
- $this->getLogContext( [
- 'method' => __METHOD__,
- 'error' => $error,
- ] )
- );
- wfDebug( "DB connection error\n" .
- "Server: $server, User: $user, Password: " .
- substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
-
- $this->reportConnectionError( $error );
- }
-
- if ( $dbName != '' ) {
- MediaWiki\suppressWarnings();
- $success = $this->selectDB( $dbName );
- MediaWiki\restoreWarnings();
- if ( !$success ) {
- wfLogDBError(
- "Error selecting database {db_name} on server {db_server}",
- $this->getLogContext( [
- 'method' => __METHOD__,
- ] )
- );
- wfDebug( "Error selecting database $dbName on server {$this->mServer} " .
- "from client host " . wfHostname() . "\n" );
-
- $this->reportConnectionError( "Error selecting database $dbName" );
- }
- }
-
- // Tell the server what we're communicating with
- if ( !$this->connectInitCharset() ) {
- $this->reportConnectionError( "Error setting character set" );
- }
-
- // Abstract over any insane MySQL defaults
- $set = [ 'group_concat_max_len = 262144' ];
- // Set SQL mode, default is turning them all off, can be overridden or skipped with null
- if ( is_string( $wgSQLMode ) ) {
- $set[] = 'sql_mode = ' . $this->addQuotes( $wgSQLMode );
- }
- // Set any custom settings defined by site config
- // (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html)
- foreach ( $this->mSessionVars as $var => $val ) {
- // Escape strings but not numbers to avoid MySQL complaining
- if ( !is_int( $val ) && !is_float( $val ) ) {
- $val = $this->addQuotes( $val );
- }
- $set[] = $this->addIdentifierQuotes( $var ) . ' = ' . $val;
- }
-
- if ( $set ) {
- // Use doQuery() to avoid opening implicit transactions (DBO_TRX)
- $success = $this->doQuery( 'SET ' . implode( ', ', $set ) );
- if ( !$success ) {
- wfLogDBError(
- 'Error setting MySQL variables on server {db_server} (check $wgSQLMode)',
- $this->getLogContext( [
- 'method' => __METHOD__,
- ] )
- );
- $this->reportConnectionError(
- 'Error setting MySQL variables on server {db_server} (check $wgSQLMode)' );
- }
- }
-
- $this->mOpened = true;
-
- return true;
- }
-
- /**
- * Set the character set information right after connection
- * @return bool
- */
- protected function connectInitCharset() {
- global $wgDBmysql5;
-
- if ( $wgDBmysql5 ) {
- // Tell the server we're communicating with it in UTF-8.
- // This may engage various charset conversions.
- return $this->mysqlSetCharset( 'utf8' );
- } else {
- return $this->mysqlSetCharset( 'binary' );
- }
- }
-
- /**
- * Open a connection to a MySQL server
- *
- * @param string $realServer
- * @return mixed Raw connection
- * @throws DBConnectionError
- */
- abstract protected function mysqlConnect( $realServer );
-
- /**
- * Set the character set of the MySQL link
- *
- * @param string $charset
- * @return bool
- */
- abstract protected function mysqlSetCharset( $charset );
-
- /**
- * @param ResultWrapper|resource $res
- * @throws DBUnexpectedError
- */
- function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- MediaWiki\suppressWarnings();
- $ok = $this->mysqlFreeResult( $res );
- MediaWiki\restoreWarnings();
- if ( !$ok ) {
- throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
- }
- }
-
- /**
- * Free result memory
- *
- * @param resource $res Raw result
- * @return bool
- */
- abstract protected function mysqlFreeResult( $res );
-
- /**
- * @param ResultWrapper|resource $res
- * @return stdClass|bool
- * @throws DBUnexpectedError
- */
- function fetchObject( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- MediaWiki\suppressWarnings();
- $row = $this->mysqlFetchObject( $res );
- MediaWiki\restoreWarnings();
-
- $errno = $this->lastErrno();
- // Unfortunately, mysql_fetch_object does not reset the last errno.
- // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
- // these are the only errors mysql_fetch_object can cause.
- // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
- if ( $errno == 2000 || $errno == 2013 ) {
- throw new DBUnexpectedError(
- $this,
- 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() )
- );
- }
-
- return $row;
- }
-
- /**
- * Fetch a result row as an object
- *
- * @param resource $res Raw result
- * @return stdClass
- */
- abstract protected function mysqlFetchObject( $res );
-
- /**
- * @param ResultWrapper|resource $res
- * @return array|bool
- * @throws DBUnexpectedError
- */
- function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- MediaWiki\suppressWarnings();
- $row = $this->mysqlFetchArray( $res );
- MediaWiki\restoreWarnings();
-
- $errno = $this->lastErrno();
- // Unfortunately, mysql_fetch_array does not reset the last errno.
- // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
- // these are the only errors mysql_fetch_array can cause.
- // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
- if ( $errno == 2000 || $errno == 2013 ) {
- throw new DBUnexpectedError(
- $this,
- 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() )
- );
- }
-
- return $row;
- }
-
- /**
- * Fetch a result row as an associative and numeric array
- *
- * @param resource $res Raw result
- * @return array
- */
- abstract protected function mysqlFetchArray( $res );
-
- /**
- * @throws DBUnexpectedError
- * @param ResultWrapper|resource $res
- * @return int
- */
- function numRows( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- MediaWiki\suppressWarnings();
- $n = $this->mysqlNumRows( $res );
- MediaWiki\restoreWarnings();
-
- // Unfortunately, mysql_num_rows does not reset the last errno.
- // We are not checking for any errors here, since
- // these are no errors mysql_num_rows can cause.
- // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
- // See https://phabricator.wikimedia.org/T44430
- return $n;
- }
-
- /**
- * Get number of rows in result
- *
- * @param resource $res Raw result
- * @return int
- */
- abstract protected function mysqlNumRows( $res );
-
- /**
- * @param ResultWrapper|resource $res
- * @return int
- */
- function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return $this->mysqlNumFields( $res );
- }
-
- /**
- * Get number of fields in result
- *
- * @param resource $res Raw result
- * @return int
- */
- abstract protected function mysqlNumFields( $res );
-
- /**
- * @param ResultWrapper|resource $res
- * @param int $n
- * @return string
- */
- function fieldName( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return $this->mysqlFieldName( $res, $n );
- }
-
- /**
- * Get the name of the specified field in a result
- *
- * @param ResultWrapper|resource $res
- * @param int $n
- * @return string
- */
- abstract protected function mysqlFieldName( $res, $n );
-
- /**
- * mysql_field_type() wrapper
- * @param ResultWrapper|resource $res
- * @param int $n
- * @return string
- */
- public function fieldType( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return $this->mysqlFieldType( $res, $n );
- }
-
- /**
- * Get the type of the specified field in a result
- *
- * @param ResultWrapper|resource $res
- * @param int $n
- * @return string
- */
- abstract protected function mysqlFieldType( $res, $n );
-
- /**
- * @param ResultWrapper|resource $res
- * @param int $row
- * @return bool
- */
- function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return $this->mysqlDataSeek( $res, $row );
- }
-
- /**
- * Move internal result pointer
- *
- * @param ResultWrapper|resource $res
- * @param int $row
- * @return bool
- */
- abstract protected function mysqlDataSeek( $res, $row );
-
- /**
- * @return string
- */
- function lastError() {
- if ( $this->mConn ) {
- # Even if it's non-zero, it can still be invalid
- MediaWiki\suppressWarnings();
- $error = $this->mysqlError( $this->mConn );
- if ( !$error ) {
- $error = $this->mysqlError();
- }
- MediaWiki\restoreWarnings();
- } else {
- $error = $this->mysqlError();
- }
- if ( $error ) {
- $error .= ' (' . $this->mServer . ')';
- }
-
- return $error;
- }
-
- /**
- * Returns the text of the error message from previous MySQL operation
- *
- * @param resource $conn Raw connection
- * @return string
- */
- abstract protected function mysqlError( $conn = null );
-
- /**
- * @param string $table
- * @param array $uniqueIndexes
- * @param array $rows
- * @param string $fname
- * @return ResultWrapper
- */
- function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
- return $this->nativeReplace( $table, $rows, $fname );
- }
-
- /**
- * Estimate rows in dataset
- * Returns estimated count, based on EXPLAIN output
- * Takes same arguments as Database::select()
- *
- * @param string|array $table
- * @param string|array $vars
- * @param string|array $conds
- * @param string $fname
- * @param string|array $options
- * @return bool|int
- */
- public function estimateRowCount( $table, $vars = '*', $conds = '',
- $fname = __METHOD__, $options = []
- ) {
- $options['EXPLAIN'] = true;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
- if ( $res === false ) {
- return false;
- }
- if ( !$this->numRows( $res ) ) {
- return 0;
- }
-
- $rows = 1;
- foreach ( $res as $plan ) {
- $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
- }
-
- return (int)$rows;
- }
-
- /**
- * @param string $table
- * @param string $field
- * @return bool|MySQLField
- */
- function fieldInfo( $table, $field ) {
- $table = $this->tableName( $table );
- $res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true );
- if ( !$res ) {
- return false;
- }
- $n = $this->mysqlNumFields( $res->result );
- for ( $i = 0; $i < $n; $i++ ) {
- $meta = $this->mysqlFetchField( $res->result, $i );
- if ( $field == $meta->name ) {
- return new MySQLField( $meta );
- }
- }
-
- return false;
- }
-
- /**
- * Get column information from a result
- *
- * @param resource $res Raw result
- * @param int $n
- * @return stdClass
- */
- abstract protected function mysqlFetchField( $res, $n );
-
- /**
- * Get information about an index into an object
- * Returns false if the index does not exist
- *
- * @param string $table
- * @param string $index
- * @param string $fname
- * @return bool|array|null False or null on failure
- */
- function indexInfo( $table, $index, $fname = __METHOD__ ) {
- # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
- # SHOW INDEX should work for 3.x and up:
- # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
- $table = $this->tableName( $table );
- $index = $this->indexName( $index );
-
- $sql = 'SHOW INDEX FROM ' . $table;
- $res = $this->query( $sql, $fname );
-
- if ( !$res ) {
- return null;
- }
-
- $result = [];
-
- foreach ( $res as $row ) {
- if ( $row->Key_name == $index ) {
- $result[] = $row;
- }
- }
-
- return empty( $result ) ? false : $result;
- }
-
- /**
- * @param string $s
- * @return string
- */
- function strencode( $s ) {
- $sQuoted = $this->mysqlRealEscapeString( $s );
-
- if ( $sQuoted === false ) {
- $this->ping();
- $sQuoted = $this->mysqlRealEscapeString( $s );
- }
-
- return $sQuoted;
- }
-
- /**
- * @param string $s
- * @return mixed
- */
- abstract protected function mysqlRealEscapeString( $s );
-
- /**
- * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes".
- *
- * @param string $s
- * @return string
- */
- public function addIdentifierQuotes( $s ) {
- // Characters in the range \u0001-\uFFFF are valid in a quoted identifier
- // Remove NUL bytes and escape backticks by doubling
- return '`' . str_replace( [ "\0", '`' ], [ '', '``' ], $s ) . '`';
- }
-
- /**
- * @param string $name
- * @return bool
- */
- public function isQuotedIdentifier( $name ) {
- return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
- }
-
- /**
- * @return bool
- */
- function ping() {
- $ping = $this->mysqlPing();
- if ( $ping ) {
- // Connection was good or lost but reconnected...
- // @note: mysqlnd (php 5.6+) does not support this (PHP bug 52561)
- return true;
- }
-
- // Try a full disconnect/reconnect cycle if ping() failed
- $this->closeConnection();
- $this->mOpened = false;
- $this->mConn = false;
- $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
-
- return true;
- }
-
- /**
- * Ping a server connection or reconnect if there is no connection
- *
- * @return bool
- */
- abstract protected function mysqlPing();
-
- function getLag() {
- if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
- return $this->getLagFromPtHeartbeat();
- } else {
- return $this->getLagFromSlaveStatus();
- }
- }
-
- /**
- * @return string
- */
- protected function getLagDetectionMethod() {
- return $this->lagDetectionMethod;
- }
-
- /**
- * @return bool|int
- */
- protected function getLagFromSlaveStatus() {
- $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
- $row = $res ? $res->fetchObject() : false;
- if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
- return intval( $row->Seconds_Behind_Master );
- }
-
- return false;
- }
-
- /**
- * @return bool|float
- */
- protected function getLagFromPtHeartbeat() {
- $options = $this->lagDetectionOptions;
-
- if ( isset( $options['conds'] ) ) {
- // Best method for multi-DC setups: use logical channel names
- $data = $this->getHeartbeatData( $options['conds'] );
- } else {
- // Standard method: use master server ID (works with stock pt-heartbeat)
- $masterInfo = $this->getMasterServerInfo();
- if ( !$masterInfo ) {
- wfLogDBError(
- "Unable to query master of {db_server} for server ID",
- $this->getLogContext( [
- 'method' => __METHOD__
- ] )
- );
-
- return false; // could not get master server ID
- }
-
- $conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ];
- $data = $this->getHeartbeatData( $conds );
- }
-
- list( $time, $nowUnix ) = $data;
- if ( $time !== null ) {
- // @time is in ISO format like "2015-09-25T16:48:10.000510"
- $dateTime = new DateTime( $time, new DateTimeZone( 'UTC' ) );
- $timeUnix = (int)$dateTime->format( 'U' ) + $dateTime->format( 'u' ) / 1e6;
-
- return max( $nowUnix - $timeUnix, 0.0 );
- }
-
- wfLogDBError(
- "Unable to find pt-heartbeat row for {db_server}",
- $this->getLogContext( [
- 'method' => __METHOD__
- ] )
- );
-
- return false;
- }
-
- protected function getMasterServerInfo() {
- $cache = $this->srvCache;
- $key = $cache->makeGlobalKey(
- 'mysql',
- 'master-info',
- // Using one key for all cluster slaves is preferable
- $this->getLBInfo( 'clusterMasterHost' ) ?: $this->getServer()
- );
-
- return $cache->getWithSetCallback(
- $key,
- $cache::TTL_INDEFINITE,
- function () use ( $cache, $key ) {
- // Get and leave a lock key in place for a short period
- if ( !$cache->lock( $key, 0, 10 ) ) {
- return false; // avoid master connection spike slams
- }
-
- $conn = $this->getLazyMasterHandle();
- if ( !$conn ) {
- return false; // something is misconfigured
- }
-
- // Connect to and query the master; catch errors to avoid outages
- try {
- $res = $conn->query( 'SELECT @@server_id AS id', __METHOD__ );
- $row = $res ? $res->fetchObject() : false;
- $id = $row ? (int)$row->id : 0;
- } catch ( DBError $e ) {
- $id = 0;
- }
-
- // Cache the ID if it was retrieved
- return $id ? [ 'serverId' => $id, 'asOf' => time() ] : false;
- }
- );
- }
-
- /**
- * @param array $conds WHERE clause conditions to find a row
- * @return array (heartbeat `ts` column value or null, UNIX timestamp) for the newest beat
- * @see https://www.percona.com/doc/percona-toolkit/2.1/pt-heartbeat.html
- */
- protected function getHeartbeatData( array $conds ) {
- $whereSQL = $this->makeList( $conds, LIST_AND );
- // Use ORDER BY for channel based queries since that field might not be UNIQUE.
- // Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
- // percision field is not supported in MySQL <= 5.5.
- $res = $this->query(
- "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1"
- );
- $row = $res ? $res->fetchObject() : false;
-
- return [ $row ? $row->ts : null, microtime( true ) ];
- }
-
- public function getApproximateLagStatus() {
- if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
- // Disable caching since this is fast enough and we don't wan't
- // to be *too* pessimistic by having both the cache TTL and the
- // pt-heartbeat interval count as lag in getSessionLagStatus()
- return parent::getApproximateLagStatus();
- }
-
- $key = $this->srvCache->makeGlobalKey( 'mysql-lag', $this->getServer() );
- $approxLag = $this->srvCache->get( $key );
- if ( !$approxLag ) {
- $approxLag = parent::getApproximateLagStatus();
- $this->srvCache->set( $key, $approxLag, 1 );
- }
-
- return $approxLag;
- }
-
- function masterPosWait( DBMasterPos $pos, $timeout ) {
- if ( !( $pos instanceof MySQLMasterPos ) ) {
- throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" );
- }
-
- if ( $this->lastKnownSlavePos && $this->lastKnownSlavePos->hasReached( $pos ) ) {
- return 0;
- }
-
- # Commit any open transactions
- $this->commit( __METHOD__, 'flush' );
-
- # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
- $encFile = $this->addQuotes( $pos->file );
- $encPos = intval( $pos->pos );
- $res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
-
- $row = $res ? $this->fetchRow( $res ) : false;
- if ( !$row ) {
- throw new DBExpectedError( $this, "Failed to query MASTER_POS_WAIT()" );
- }
-
- // Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
- $status = ( $row[0] !== null ) ? intval( $row[0] ) : null;
- if ( $status === null ) {
- // T126436: jobs programmed to wait on master positions might be referencing binlogs
- // with an old master hostname. Such calls make MASTER_POS_WAIT() return null. Try
- // to detect this and treat the slave as having reached the position; a proper master
- // switchover already requires that the new master be caught up before the switch.
- $slavePos = $this->getSlavePos();
- if ( $slavePos && !$slavePos->channelsMatch( $pos ) ) {
- $this->lastKnownSlavePos = $slavePos;
- $status = 0;
- }
- } elseif ( $status >= 0 ) {
- // Remember that this position was reached to save queries next time
- $this->lastKnownSlavePos = $pos;
- }
-
- return $status;
- }
-
- /**
- * Get the position of the master from SHOW SLAVE STATUS
- *
- * @return MySQLMasterPos|bool
- */
- function getSlavePos() {
- $res = $this->query( 'SHOW SLAVE STATUS', 'DatabaseBase::getSlavePos' );
- $row = $this->fetchObject( $res );
-
- if ( $row ) {
- $pos = isset( $row->Exec_master_log_pos )
- ? $row->Exec_master_log_pos
- : $row->Exec_Master_Log_Pos;
-
- return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos );
- } else {
- return false;
- }
- }
-
- /**
- * Get the position of the master from SHOW MASTER STATUS
- *
- * @return MySQLMasterPos|bool
- */
- function getMasterPos() {
- $res = $this->query( 'SHOW MASTER STATUS', 'DatabaseBase::getMasterPos' );
- $row = $this->fetchObject( $res );
-
- if ( $row ) {
- return new MySQLMasterPos( $row->File, $row->Position );
- } else {
- return false;
- }
- }
-
- /**
- * @param string $index
- * @return string
- */
- function useIndexClause( $index ) {
- return "FORCE INDEX (" . $this->indexName( $index ) . ")";
- }
-
- /**
- * @return string
- */
- function lowPriorityOption() {
- return 'LOW_PRIORITY';
- }
-
- /**
- * @return string
- */
- public function getSoftwareLink() {
- // MariaDB includes its name in its version string; this is how MariaDB's version of
- // the mysql command-line client identifies MariaDB servers (see mariadb_connection()
- // in libmysql/libmysql.c).
- $version = $this->getServerVersion();
- if ( strpos( $version, 'MariaDB' ) !== false || strpos( $version, '-maria-' ) !== false ) {
- return '[{{int:version-db-mariadb-url}} MariaDB]';
- }
-
- // Percona Server's version suffix is not very distinctive, and @@version_comment
- // doesn't give the necessary info for source builds, so assume the server is MySQL.
- // (Even Percona's version of mysql doesn't try to make the distinction.)
- return '[{{int:version-db-mysql-url}} MySQL]';
- }
-
- /**
- * @return string
- */
- public function getServerVersion() {
- // Not using mysql_get_server_info() or similar for consistency: in the handshake,
- // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
- // it off (see RPL_VERSION_HACK in include/mysql_com.h).
- if ( $this->serverVersion === null ) {
- $this->serverVersion = $this->selectField( '', 'VERSION()', '', __METHOD__ );
- }
- return $this->serverVersion;
- }
-
- /**
- * @param array $options
- */
- public function setSessionOptions( array $options ) {
- if ( isset( $options['connTimeout'] ) ) {
- $timeout = (int)$options['connTimeout'];
- $this->query( "SET net_read_timeout=$timeout" );
- $this->query( "SET net_write_timeout=$timeout" );
- }
- }
-
- /**
- * @param string $sql
- * @param string $newLine
- * @return bool
- */
- public function streamStatementEnd( &$sql, &$newLine ) {
- if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) {
- preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m );
- $this->delimiter = $m[1];
- $newLine = '';
- }
-
- return parent::streamStatementEnd( $sql, $newLine );
- }
-
- /**
- * Check to see if a named lock is available. This is non-blocking.
- *
- * @param string $lockName Name of lock to poll
- * @param string $method Name of method calling us
- * @return bool
- * @since 1.20
- */
- public function lockIsFree( $lockName, $method ) {
- $lockName = $this->addQuotes( $this->makeLockName( $lockName ) );
- $result = $this->query( "SELECT IS_FREE_LOCK($lockName) AS lockstatus", $method );
- $row = $this->fetchObject( $result );
-
- return ( $row->lockstatus == 1 );
- }
-
- /**
- * @param string $lockName
- * @param string $method
- * @param int $timeout
- * @return bool
- */
- public function lock( $lockName, $method, $timeout = 5 ) {
- $lockName = $this->addQuotes( $this->makeLockName( $lockName ) );
- $result = $this->query( "SELECT GET_LOCK($lockName, $timeout) AS lockstatus", $method );
- $row = $this->fetchObject( $result );
-
- if ( $row->lockstatus == 1 ) {
- parent::lock( $lockName, $method, $timeout ); // record
- return true;
- }
-
- wfDebug( __METHOD__ . " failed to acquire lock\n" );
-
- return false;
- }
-
- /**
- * FROM MYSQL DOCS:
- * http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
- * @param string $lockName
- * @param string $method
- * @return bool
- */
- public function unlock( $lockName, $method ) {
- $lockName = $this->addQuotes( $this->makeLockName( $lockName ) );
- $result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method );
- $row = $this->fetchObject( $result );
-
- if ( $row->lockstatus == 1 ) {
- parent::unlock( $lockName, $method ); // record
- return true;
- }
-
- wfDebug( __METHOD__ . " failed to release lock\n" );
-
- return false;
- }
-
- private function makeLockName( $lockName ) {
- // http://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
- // Newer version enforce a 64 char length limit.
- return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName;
- }
-
- public function namedLocksEnqueue() {
- return true;
- }
-
- /**
- * @param array $read
- * @param array $write
- * @param string $method
- * @param bool $lowPriority
- * @return bool
- */
- public function lockTables( $read, $write, $method, $lowPriority = true ) {
- $items = [];
-
- foreach ( $write as $table ) {
- $tbl = $this->tableName( $table ) .
- ( $lowPriority ? ' LOW_PRIORITY' : '' ) .
- ' WRITE';
- $items[] = $tbl;
- }
- foreach ( $read as $table ) {
- $items[] = $this->tableName( $table ) . ' READ';
- }
- $sql = "LOCK TABLES " . implode( ',', $items );
- $this->query( $sql, $method );
-
- return true;
- }
-
- /**
- * @param string $method
- * @return bool
- */
- public function unlockTables( $method ) {
- $this->query( "UNLOCK TABLES", $method );
-
- return true;
- }
-
- /**
- * Get search engine class. All subclasses of this
- * need to implement this if they wish to use searching.
- *
- * @return string
- */
- public function getSearchEngine() {
- return 'SearchMySQL';
- }
-
- /**
- * @param bool $value
- */
- public function setBigSelects( $value = true ) {
- if ( $value === 'default' ) {
- if ( $this->mDefaultBigSelects === null ) {
- # Function hasn't been called before so it must already be set to the default
- return;
- } else {
- $value = $this->mDefaultBigSelects;
- }
- } elseif ( $this->mDefaultBigSelects === null ) {
- $this->mDefaultBigSelects =
- (bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ );
- }
- $encValue = $value ? '1' : '0';
- $this->query( "SET sql_big_selects=$encValue", __METHOD__ );
- }
-
- /**
- * DELETE where the condition is a join. MySql uses multi-table deletes.
- * @param string $delTable
- * @param string $joinTable
- * @param string $delVar
- * @param string $joinVar
- * @param array|string $conds
- * @param bool|string $fname
- * @throws DBUnexpectedError
- * @return bool|ResultWrapper
- */
- function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this, 'DatabaseBase::deleteJoin() called with empty $conds' );
- }
-
- $delTable = $this->tableName( $delTable );
- $joinTable = $this->tableName( $joinTable );
- $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
-
- if ( $conds != '*' ) {
- $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
- }
-
- return $this->query( $sql, $fname );
- }
-
- /**
- * @param string $table
- * @param array $rows
- * @param array $uniqueIndexes
- * @param array $set
- * @param string $fname
- * @return bool
- */
- public function upsert( $table, array $rows, array $uniqueIndexes,
- array $set, $fname = __METHOD__
- ) {
- if ( !count( $rows ) ) {
- return true; // nothing to do
- }
-
- if ( !is_array( reset( $rows ) ) ) {
- $rows = [ $rows ];
- }
-
- $table = $this->tableName( $table );
- $columns = array_keys( $rows[0] );
-
- $sql = "INSERT INTO $table (" . implode( ',', $columns ) . ') VALUES ';
- $rowTuples = [];
- foreach ( $rows as $row ) {
- $rowTuples[] = '(' . $this->makeList( $row ) . ')';
- }
- $sql .= implode( ',', $rowTuples );
- $sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, LIST_SET );
-
- return (bool)$this->query( $sql, $fname );
- }
-
- /**
- * Determines how long the server has been up
- *
- * @return int
- */
- function getServerUptime() {
- $vars = $this->getMysqlStatus( 'Uptime' );
-
- return (int)$vars['Uptime'];
- }
-
- /**
- * Determines if the last failure was due to a deadlock
- *
- * @return bool
- */
- function wasDeadlock() {
- return $this->lastErrno() == 1213;
- }
-
- /**
- * Determines if the last failure was due to a lock timeout
- *
- * @return bool
- */
- function wasLockTimeout() {
- return $this->lastErrno() == 1205;
- }
-
- /**
- * Determines if the last query error was something that should be dealt
- * with by pinging the connection and reissuing the query
- *
- * @return bool
- */
- function wasErrorReissuable() {
- return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
- }
-
- /**
- * Determines if the last failure was due to the database being read-only.
- *
- * @return bool
- */
- function wasReadOnlyError() {
- return $this->lastErrno() == 1223 ||
- ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
- }
-
- function wasConnectionError( $errno ) {
- return $errno == 2013 || $errno == 2006;
- }
-
- /**
- * Get the underlying binding handle, mConn
- *
- * Makes sure that mConn is set (disconnects and ping() failure can unset it).
- * This catches broken callers than catch and ignore disconnection exceptions.
- * Unlike checking isOpen(), this is safe to call inside of open().
- *
- * @return resource|object
- * @throws DBUnexpectedError
- * @since 1.26
- */
- protected function getBindingHandle() {
- if ( !$this->mConn ) {
- throw new DBUnexpectedError(
- $this,
- 'DB connection was already closed or the connection dropped.'
- );
- }
-
- return $this->mConn;
- }
-
- /**
- * @param string $oldName
- * @param string $newName
- * @param bool $temporary
- * @param string $fname
- * @return bool
- */
- function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
- $tmp = $temporary ? 'TEMPORARY ' : '';
- $newName = $this->addIdentifierQuotes( $newName );
- $oldName = $this->addIdentifierQuotes( $oldName );
- $query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
-
- return $this->query( $query, $fname );
- }
-
- /**
- * List all tables on the database
- *
- * @param string $prefix Only show tables with this prefix, e.g. mw_
- * @param string $fname Calling function name
- * @return array
- */
- function listTables( $prefix = null, $fname = __METHOD__ ) {
- $result = $this->query( "SHOW TABLES", $fname );
-
- $endArray = [];
-
- foreach ( $result as $table ) {
- $vars = get_object_vars( $table );
- $table = array_pop( $vars );
-
- if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
- $endArray[] = $table;
- }
- }
-
- return $endArray;
- }
-
- /**
- * @param string $tableName
- * @param string $fName
- * @return bool|ResultWrapper
- */
- public function dropTable( $tableName, $fName = __METHOD__ ) {
- if ( !$this->tableExists( $tableName, $fName ) ) {
- return false;
- }
-
- return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName );
- }
-
- /**
- * @return array
- */
- protected function getDefaultSchemaVars() {
- $vars = parent::getDefaultSchemaVars();
- $vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] );
- $vars['wgDBTableOptions'] = str_replace(
- 'CHARSET=mysql4',
- 'CHARSET=binary',
- $vars['wgDBTableOptions']
- );
-
- return $vars;
- }
-
- /**
- * Get status information from SHOW STATUS in an associative array
- *
- * @param string $which
- * @return array
- */
- function getMysqlStatus( $which = "%" ) {
- $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
- $status = [];
-
- foreach ( $res as $row ) {
- $status[$row->Variable_name] = $row->Value;
- }
-
- return $status;
- }
-
- /**
- * Lists VIEWs in the database
- *
- * @param string $prefix Only show VIEWs with this prefix, eg.
- * unit_test_, or $wgDBprefix. Default: null, would return all views.
- * @param string $fname Name of calling function
- * @return array
- * @since 1.22
- */
- public function listViews( $prefix = null, $fname = __METHOD__ ) {
-
- if ( !isset( $this->allViews ) ) {
-
- // The name of the column containing the name of the VIEW
- $propertyName = 'Tables_in_' . $this->mDBname;
-
- // Query for the VIEWS
- $result = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
- $this->allViews = [];
- while ( ( $row = $this->fetchRow( $result ) ) !== false ) {
- array_push( $this->allViews, $row[$propertyName] );
- }
- }
-
- if ( is_null( $prefix ) || $prefix === '' ) {
- return $this->allViews;
- }
-
- $filteredViews = [];
- foreach ( $this->allViews as $viewName ) {
- // Does the name of this VIEW start with the table-prefix?
- if ( strpos( $viewName, $prefix ) === 0 ) {
- array_push( $filteredViews, $viewName );
- }
- }
-
- return $filteredViews;
- }
-
- /**
- * Differentiates between a TABLE and a VIEW.
- *
- * @param string $name Name of the TABLE/VIEW to test
- * @param string $prefix
- * @return bool
- * @since 1.22
- */
- public function isView( $name, $prefix = null ) {
- return in_array( $name, $this->listViews( $prefix ) );
- }
-}
-
-/**
- * Utility class.
- * @ingroup Database
- */
-class MySQLField implements Field {
- private $name, $tablename, $default, $max_length, $nullable,
- $is_pk, $is_unique, $is_multiple, $is_key, $type, $binary,
- $is_numeric, $is_blob, $is_unsigned, $is_zerofill;
-
- function __construct( $info ) {
- $this->name = $info->name;
- $this->tablename = $info->table;
- $this->default = $info->def;
- $this->max_length = $info->max_length;
- $this->nullable = !$info->not_null;
- $this->is_pk = $info->primary_key;
- $this->is_unique = $info->unique_key;
- $this->is_multiple = $info->multiple_key;
- $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple );
- $this->type = $info->type;
- $this->binary = isset( $info->binary ) ? $info->binary : false;
- $this->is_numeric = isset( $info->numeric ) ? $info->numeric : false;
- $this->is_blob = isset( $info->blob ) ? $info->blob : false;
- $this->is_unsigned = isset( $info->unsigned ) ? $info->unsigned : false;
- $this->is_zerofill = isset( $info->zerofill ) ? $info->zerofill : false;
- }
-
- /**
- * @return string
- */
- function name() {
- return $this->name;
- }
-
- /**
- * @return string
- */
- function tableName() {
- return $this->tablename;
- }
-
- /**
- * @return string
- */
- function type() {
- return $this->type;
- }
-
- /**
- * @return bool
- */
- function isNullable() {
- return $this->nullable;
- }
-
- function defaultValue() {
- return $this->default;
- }
-
- /**
- * @return bool
- */
- function isKey() {
- return $this->is_key;
- }
-
- /**
- * @return bool
- */
- function isMultipleKey() {
- return $this->is_multiple;
- }
-
- /**
- * @return bool
- */
- function isBinary() {
- return $this->binary;
- }
-
- /**
- * @return bool
- */
- function isNumeric() {
- return $this->is_numeric;
- }
-
- /**
- * @return bool
- */
- function isBlob() {
- return $this->is_blob;
- }
-
- /**
- * @return bool
- */
- function isUnsigned() {
- return $this->is_unsigned;
- }
-
- /**
- * @return bool
- */
- function isZerofill() {
- return $this->is_zerofill;
- }
-}
-
-class MySQLMasterPos implements DBMasterPos {
- /** @var string */
- public $file;
- /** @var int Position */
- public $pos;
- /** @var float UNIX timestamp */
- public $asOfTime = 0.0;
-
- function __construct( $file, $pos ) {
- $this->file = $file;
- $this->pos = $pos;
- $this->asOfTime = microtime( true );
- }
-
- function asOfTime() {
- return $this->asOfTime;
- }
-
- function hasReached( DBMasterPos $pos ) {
- if ( !( $pos instanceof self ) ) {
- throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
- }
-
- $thisPos = $this->getCoordinates();
- $thatPos = $pos->getCoordinates();
-
- return ( $thisPos && $thatPos && $thisPos >= $thatPos );
- }
-
- function channelsMatch( DBMasterPos $pos ) {
- if ( !( $pos instanceof self ) ) {
- throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
- }
-
- $thisBinlog = $this->getBinlogName();
- $thatBinlog = $pos->getBinlogName();
-
- return ( $thisBinlog !== false && $thisBinlog === $thatBinlog );
- }
-
- function __toString() {
- // e.g db1034-bin.000976/843431247
- return "{$this->file}/{$this->pos}";
- }
-
- /**
- * @return string|bool
- */
- protected function getBinlogName() {
- $m = [];
- if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', (string)$this, $m ) ) {
- return $m[1];
- }
-
- return false;
- }
-
- /**
- * @return array|bool (int, int)
- */
- protected function getCoordinates() {
- $m = [];
- if ( preg_match( '!\.(\d+)/(\d+)$!', (string)$this, $m ) ) {
- return [ (int)$m[1], (int)$m[2] ];
- }
-
- return false;
- }
-}
diff --git a/www/wiki/includes/db/DatabaseMysqli.php b/www/wiki/includes/db/DatabaseMysqli.php
deleted file mode 100644
index d45805ad..00000000
--- a/www/wiki/includes/db/DatabaseMysqli.php
+++ /dev/null
@@ -1,332 +0,0 @@
-<?php
-/**
- * This is the MySQLi database abstraction layer.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * Database abstraction object for PHP extension mysqli.
- *
- * @ingroup Database
- * @since 1.22
- * @see Database
- */
-class DatabaseMysqli extends DatabaseMysqlBase {
- /** @var mysqli */
- protected $mConn;
-
- /**
- * @param string $sql
- * @return resource
- */
- protected function doQuery( $sql ) {
- $conn = $this->getBindingHandle();
-
- if ( $this->bufferResults() ) {
- $ret = $conn->query( $sql );
- } else {
- $ret = $conn->query( $sql, MYSQLI_USE_RESULT );
- }
-
- return $ret;
- }
-
- /**
- * @param string $realServer
- * @return bool|mysqli
- * @throws DBConnectionError
- */
- protected function mysqlConnect( $realServer ) {
- global $wgDBmysql5;
-
- # Avoid suppressed fatal error, which is very hard to track down
- if ( !function_exists( 'mysqli_init' ) ) {
- throw new DBConnectionError( $this, "MySQLi functions missing,"
- . " have you compiled PHP with the --with-mysqli option?\n" );
- }
-
- // Other than mysql_connect, mysqli_real_connect expects an explicit port
- // and socket parameters. So we need to parse the port and socket out of
- // $realServer
- $port = null;
- $socket = null;
- $hostAndPort = IP::splitHostAndPort( $realServer );
- if ( $hostAndPort ) {
- $realServer = $hostAndPort[0];
- if ( $hostAndPort[1] ) {
- $port = $hostAndPort[1];
- }
- } elseif ( substr_count( $realServer, ':' ) == 1 ) {
- // If we have a colon and something that's not a port number
- // inside the hostname, assume it's the socket location
- $hostAndSocket = explode( ':', $realServer );
- $realServer = $hostAndSocket[0];
- $socket = $hostAndSocket[1];
- }
-
- $connFlags = 0;
- if ( $this->mFlags & DBO_SSL ) {
- $connFlags |= MYSQLI_CLIENT_SSL;
- }
- if ( $this->mFlags & DBO_COMPRESS ) {
- $connFlags |= MYSQLI_CLIENT_COMPRESS;
- }
- if ( $this->mFlags & DBO_PERSISTENT ) {
- $realServer = 'p:' . $realServer;
- }
-
- $mysqli = mysqli_init();
- if ( $wgDBmysql5 ) {
- // Tell the server we're communicating with it in UTF-8.
- // This may engage various charset conversions.
- $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'utf8' );
- } else {
- $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'binary' );
- }
- $mysqli->options( MYSQLI_OPT_CONNECT_TIMEOUT, 3 );
-
- if ( $mysqli->real_connect( $realServer, $this->mUser,
- $this->mPassword, $this->mDBname, $port, $socket, $connFlags )
- ) {
- return $mysqli;
- }
-
- return false;
- }
-
- protected function connectInitCharset() {
- // already done in mysqlConnect()
- return true;
- }
-
- /**
- * @param string $charset
- * @return bool
- */
- protected function mysqlSetCharset( $charset ) {
- $conn = $this->getBindingHandle();
-
- if ( method_exists( $conn, 'set_charset' ) ) {
- return $conn->set_charset( $charset );
- } else {
- return $this->query( 'SET NAMES ' . $charset, __METHOD__ );
- }
- }
-
- /**
- * @return bool
- */
- protected function closeConnection() {
- $conn = $this->getBindingHandle();
-
- return $conn->close();
- }
-
- /**
- * @return int
- */
- function insertId() {
- $conn = $this->getBindingHandle();
-
- return (int)$conn->insert_id;
- }
-
- /**
- * @return int
- */
- function lastErrno() {
- if ( $this->mConn ) {
- return $this->mConn->errno;
- } else {
- return mysqli_connect_errno();
- }
- }
-
- /**
- * @return int
- */
- function affectedRows() {
- $conn = $this->getBindingHandle();
-
- return $conn->affected_rows;
- }
-
- /**
- * @param string $db
- * @return bool
- */
- function selectDB( $db ) {
- $conn = $this->getBindingHandle();
-
- $this->mDBname = $db;
-
- return $conn->select_db( $db );
- }
-
- /**
- * @param mysqli $res
- * @return bool
- */
- protected function mysqlFreeResult( $res ) {
- $res->free_result();
-
- return true;
- }
-
- /**
- * @param mysqli $res
- * @return bool
- */
- protected function mysqlFetchObject( $res ) {
- $object = $res->fetch_object();
- if ( $object === null ) {
- return false;
- }
-
- return $object;
- }
-
- /**
- * @param mysqli $res
- * @return bool
- */
- protected function mysqlFetchArray( $res ) {
- $array = $res->fetch_array();
- if ( $array === null ) {
- return false;
- }
-
- return $array;
- }
-
- /**
- * @param mysqli $res
- * @return mixed
- */
- protected function mysqlNumRows( $res ) {
- return $res->num_rows;
- }
-
- /**
- * @param mysqli $res
- * @return mixed
- */
- protected function mysqlNumFields( $res ) {
- return $res->field_count;
- }
-
- /**
- * @param mysqli $res
- * @param int $n
- * @return mixed
- */
- protected function mysqlFetchField( $res, $n ) {
- $field = $res->fetch_field_direct( $n );
-
- // Add missing properties to result (using flags property)
- // which will be part of function mysql-fetch-field for backward compatibility
- $field->not_null = $field->flags & MYSQLI_NOT_NULL_FLAG;
- $field->primary_key = $field->flags & MYSQLI_PRI_KEY_FLAG;
- $field->unique_key = $field->flags & MYSQLI_UNIQUE_KEY_FLAG;
- $field->multiple_key = $field->flags & MYSQLI_MULTIPLE_KEY_FLAG;
- $field->binary = $field->flags & MYSQLI_BINARY_FLAG;
- $field->numeric = $field->flags & MYSQLI_NUM_FLAG;
- $field->blob = $field->flags & MYSQLI_BLOB_FLAG;
- $field->unsigned = $field->flags & MYSQLI_UNSIGNED_FLAG;
- $field->zerofill = $field->flags & MYSQLI_ZEROFILL_FLAG;
-
- return $field;
- }
-
- /**
- * @param resource|ResultWrapper $res
- * @param int $n
- * @return mixed
- */
- protected function mysqlFieldName( $res, $n ) {
- $field = $res->fetch_field_direct( $n );
-
- return $field->name;
- }
-
- /**
- * @param resource|ResultWrapper $res
- * @param int $n
- * @return mixed
- */
- protected function mysqlFieldType( $res, $n ) {
- $field = $res->fetch_field_direct( $n );
-
- return $field->type;
- }
-
- /**
- * @param resource|ResultWrapper $res
- * @param int $row
- * @return mixed
- */
- protected function mysqlDataSeek( $res, $row ) {
- return $res->data_seek( $row );
- }
-
- /**
- * @param mysqli $conn Optional connection object
- * @return string
- */
- protected function mysqlError( $conn = null ) {
- if ( $conn === null ) {
- return mysqli_connect_error();
- } else {
- return $conn->error;
- }
- }
-
- /**
- * Escapes special characters in a string for use in an SQL statement
- * @param string $s
- * @return string
- */
- protected function mysqlRealEscapeString( $s ) {
- $conn = $this->getBindingHandle();
-
- return $conn->real_escape_string( $s );
- }
-
- protected function mysqlPing() {
- $conn = $this->getBindingHandle();
-
- return $conn->ping();
- }
-
- /**
- * Give an id for the connection
- *
- * mysql driver used resource id, but mysqli objects cannot be cast to string.
- * @return string
- */
- public function __toString() {
- if ( $this->mConn instanceof mysqli ) {
- return (string)$this->mConn->thread_id;
- } else {
- // mConn might be false or something.
- return (string)$this->mConn;
- }
- }
-}
diff --git a/www/wiki/includes/db/DatabaseOracle.php b/www/wiki/includes/db/DatabaseOracle.php
index e2feb1fa..3362f0f9 100644
--- a/www/wiki/includes/db/DatabaseOracle.php
+++ b/www/wiki/includes/db/DatabaseOracle.php
@@ -61,10 +61,10 @@ class DatabaseOracle extends Database {
}
function __destruct() {
- if ( $this->mOpened ) {
- MediaWiki\suppressWarnings();
+ if ( $this->opened ) {
+ Wikimedia\suppressWarnings();
$this->close();
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
}
@@ -100,21 +100,21 @@ class DatabaseOracle extends Database {
}
$this->close();
- $this->mUser = $user;
- $this->mPassword = $password;
+ $this->user = $user;
+ $this->password = $password;
// changed internal variables functions
// mServer now holds the TNS endpoint
// mDBname is schema name if different from username
if ( !$server ) {
// backward compatibillity (server used to be null and TNS was supplied in dbname)
- $this->mServer = $dbName;
- $this->mDBname = $user;
+ $this->server = $dbName;
+ $this->dbName = $user;
} else {
- $this->mServer = $server;
+ $this->server = $server;
if ( !$dbName ) {
- $this->mDBname = $user;
+ $this->dbName = $user;
} else {
- $this->mDBname = $dbName;
+ $this->dbName = $dbName;
}
}
@@ -126,53 +126,53 @@ class DatabaseOracle extends Database {
$this->setFlag( DBO_PERSISTENT );
}
- $session_mode = $this->mFlags & DBO_SYSDBA ? OCI_SYSDBA : OCI_DEFAULT;
+ $session_mode = $this->flags & DBO_SYSDBA ? OCI_SYSDBA : OCI_DEFAULT;
- MediaWiki\suppressWarnings();
- if ( $this->mFlags & DBO_PERSISTENT ) {
- $this->mConn = oci_pconnect(
- $this->mUser,
- $this->mPassword,
- $this->mServer,
+ Wikimedia\suppressWarnings();
+ if ( $this->flags & DBO_PERSISTENT ) {
+ $this->conn = oci_pconnect(
+ $this->user,
+ $this->password,
+ $this->server,
$this->defaultCharset,
$session_mode
);
- } elseif ( $this->mFlags & DBO_DEFAULT ) {
- $this->mConn = oci_new_connect(
- $this->mUser,
- $this->mPassword,
- $this->mServer,
+ } elseif ( $this->flags & DBO_DEFAULT ) {
+ $this->conn = oci_new_connect(
+ $this->user,
+ $this->password,
+ $this->server,
$this->defaultCharset,
$session_mode
);
} else {
- $this->mConn = oci_connect(
- $this->mUser,
- $this->mPassword,
- $this->mServer,
+ $this->conn = oci_connect(
+ $this->user,
+ $this->password,
+ $this->server,
$this->defaultCharset,
$session_mode
);
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
- if ( $this->mUser != $this->mDBname ) {
+ if ( $this->user != $this->dbName ) {
// change current schema in session
- $this->selectDB( $this->mDBname );
+ $this->selectDB( $this->dbName );
}
- if ( !$this->mConn ) {
+ if ( !$this->conn ) {
throw new DBConnectionError( $this, $this->lastError() );
}
- $this->mOpened = true;
+ $this->opened = true;
# removed putenv calls because they interfere with the system globaly
$this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' );
$this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' );
$this->doQuery( 'ALTER SESSION SET NLS_NUMERIC_CHARACTERS=\'.,\'' );
- return $this->mConn;
+ return $this->conn;
}
/**
@@ -181,11 +181,11 @@ class DatabaseOracle extends Database {
* @return bool
*/
protected function closeConnection() {
- return oci_close( $this->mConn );
+ return oci_close( $this->conn );
}
function execFlags() {
- return $this->mTrxLevel ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS;
+ return $this->trxLevel ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS;
}
protected function doQuery( $sql ) {
@@ -215,11 +215,11 @@ class DatabaseOracle extends Database {
$explain_count
);
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
- $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql );
+ $this->mLastResult = $stmt = oci_parse( $this->conn, $sql );
if ( $stmt === false ) {
- $e = oci_error( $this->mConn );
+ $e = oci_error( $this->conn );
$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
return false;
@@ -234,7 +234,7 @@ class DatabaseOracle extends Database {
}
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $explain_count > 0 ) {
return $this->doQuery( 'SELECT id, cardinality "ROWS" FROM plan_table ' .
@@ -335,26 +335,26 @@ class DatabaseOracle extends Database {
}
function lastError() {
- if ( $this->mConn === false ) {
+ if ( $this->conn === false ) {
$e = oci_error();
} else {
- $e = oci_error( $this->mConn );
+ $e = oci_error( $this->conn );
}
return $e['message'];
}
function lastErrno() {
- if ( $this->mConn === false ) {
+ if ( $this->conn === false ) {
$e = oci_error();
} else {
- $e = oci_error( $this->mConn );
+ $e = oci_error( $this->conn );
}
return $e['code'];
}
- function affectedRows() {
+ protected function fetchAffectedRowCount() {
return $this->mAffectedRows;
}
@@ -470,9 +470,9 @@ class DatabaseOracle extends Database {
}
$sql .= ')';
- $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql );
+ $this->mLastResult = $stmt = oci_parse( $this->conn, $sql );
if ( $stmt === false ) {
- $e = oci_error( $this->mConn );
+ $e = oci_error( $this->conn );
$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
return false;
@@ -502,7 +502,7 @@ class DatabaseOracle extends Database {
}
} else {
/** @var OCI_Lob[] $lob */
- $lob[$col] = oci_new_descriptor( $this->mConn, OCI_D_LOB );
+ $lob[$col] = oci_new_descriptor( $this->conn, OCI_D_LOB );
if ( $lob[$col] === false ) {
$e = oci_error( $stmt );
throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] );
@@ -522,7 +522,7 @@ class DatabaseOracle extends Database {
}
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
if ( oci_execute( $stmt, $this->execFlags() ) === false ) {
$e = oci_error( $stmt );
@@ -537,7 +537,7 @@ class DatabaseOracle extends Database {
$this->mAffectedRows = oci_num_rows( $stmt );
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( isset( $lob ) ) {
foreach ( $lob as $lob_v ) {
@@ -545,8 +545,8 @@ class DatabaseOracle extends Database {
}
}
- if ( !$this->mTrxLevel ) {
- oci_commit( $this->mConn );
+ if ( !$this->trxLevel ) {
+ oci_commit( $this->conn );
}
return oci_free_statement( $stmt );
@@ -658,13 +658,13 @@ class DatabaseOracle extends Database {
FROM all_sequences asq, all_tab_columns atc
WHERE decode(
atc.table_name,
- '{$this->mTablePrefix}MWUSER',
- '{$this->mTablePrefix}USER',
+ '{$this->tablePrefix}MWUSER',
+ '{$this->tablePrefix}USER',
atc.table_name
) || '_' ||
- atc.column_name || '_SEQ' = '{$this->mTablePrefix}' || asq.sequence_name
- AND asq.sequence_owner = upper('{$this->mDBname}')
- AND atc.owner = upper('{$this->mDBname}')" );
+ atc.column_name || '_SEQ' = '{$this->tablePrefix}' || asq.sequence_name
+ AND asq.sequence_owner = upper('{$this->dbName}')
+ AND atc.owner = upper('{$this->dbName}')" );
while ( ( $row = $result->fetchRow() ) !== false ) {
$this->sequenceData[$row[1]] = [
@@ -730,9 +730,9 @@ class DatabaseOracle extends Database {
$newName = strtoupper( $newName );
$oldName = strtoupper( $oldName );
- $tabName = substr( $newName, strlen( $this->mTablePrefix ) );
+ $tabName = substr( $newName, strlen( $this->tablePrefix ) );
$oldPrefix = substr( $oldName, 0, strlen( $oldName ) - strlen( $tabName ) );
- $newPrefix = strtoupper( $this->mTablePrefix );
+ $newPrefix = strtoupper( $this->tablePrefix );
return $this->doQuery( "BEGIN DUPLICATE_TABLE( '$tabName', " .
"'$oldPrefix', '$newPrefix', $temporary ); END;" );
@@ -744,7 +744,7 @@ class DatabaseOracle extends Database {
$listWhere = ' AND table_name LIKE \'' . strtoupper( $prefix ) . '%\'';
}
- $owner = strtoupper( $this->mDBname );
+ $owner = strtoupper( $this->dbName );
$result = $this->doQuery( "SELECT table_name FROM all_tables " .
"WHERE owner='$owner' AND table_name NOT LIKE '%!_IDX\$_' ESCAPE '!' $listWhere" );
@@ -805,7 +805,7 @@ class DatabaseOracle extends Database {
);
$row = $rset->fetchRow();
if ( !$row ) {
- return oci_server_version( $this->mConn );
+ return oci_server_version( $this->conn );
}
return $row['version'];
@@ -822,7 +822,7 @@ class DatabaseOracle extends Database {
$table = $this->tableName( $table );
$table = strtoupper( $this->removeIdentifierQuotes( $table ) );
$index = strtoupper( $index );
- $owner = strtoupper( $this->mDBname );
+ $owner = strtoupper( $this->dbName );
$sql = "SELECT 1 FROM all_indexes WHERE owner='$owner' AND index_name='{$table}_{$index}'";
$res = $this->doQuery( $sql );
if ( $res ) {
@@ -844,7 +844,7 @@ class DatabaseOracle extends Database {
function tableExists( $table, $fname = __METHOD__ ) {
$table = $this->tableName( $table );
$table = $this->addQuotes( strtoupper( $this->removeIdentifierQuotes( $table ) ) );
- $owner = $this->addQuotes( strtoupper( $this->mDBname ) );
+ $owner = $this->addQuotes( strtoupper( $this->dbName ) );
$sql = "SELECT 1 FROM all_tables WHERE owner=$owner AND table_name=$table";
$res = $this->doQuery( $sql );
if ( $res && $res->numRows() > 0 ) {
@@ -890,7 +890,7 @@ class DatabaseOracle extends Database {
}
$fieldInfoStmt = oci_parse(
- $this->mConn,
+ $this->conn,
'SELECT * FROM wiki_field_info_full WHERE table_name ' .
$tableWhere . ' and column_name = \'' . $field . '\''
);
@@ -935,25 +935,25 @@ class DatabaseOracle extends Database {
}
protected function doBegin( $fname = __METHOD__ ) {
- $this->mTrxLevel = 1;
+ $this->trxLevel = 1;
$this->doQuery( 'SET CONSTRAINTS ALL DEFERRED' );
}
protected function doCommit( $fname = __METHOD__ ) {
- if ( $this->mTrxLevel ) {
- $ret = oci_commit( $this->mConn );
+ if ( $this->trxLevel ) {
+ $ret = oci_commit( $this->conn );
if ( !$ret ) {
throw new DBUnexpectedError( $this, $this->lastError() );
}
- $this->mTrxLevel = 0;
+ $this->trxLevel = 0;
$this->doQuery( 'SET CONSTRAINTS ALL IMMEDIATE' );
}
}
protected function doRollback( $fname = __METHOD__ ) {
- if ( $this->mTrxLevel ) {
- oci_rollback( $this->mConn );
- $this->mTrxLevel = 0;
+ if ( $this->trxLevel ) {
+ oci_rollback( $this->conn );
+ $this->trxLevel = 0;
$this->doQuery( 'SET CONSTRAINTS ALL IMMEDIATE' );
}
}
@@ -1041,15 +1041,15 @@ class DatabaseOracle extends Database {
}
function selectDB( $db ) {
- $this->mDBname = $db;
- if ( $db == null || $db == $this->mUser ) {
+ $this->dbName = $db;
+ if ( $db == null || $db == $this->user ) {
return true;
}
$sql = 'ALTER SESSION SET CURRENT_SCHEMA=' . strtoupper( $db );
- $stmt = oci_parse( $this->mConn, $sql );
- MediaWiki\suppressWarnings();
+ $stmt = oci_parse( $this->conn, $sql );
+ Wikimedia\suppressWarnings();
$success = oci_execute( $stmt );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$success ) {
$e = oci_error( $stmt );
if ( $e['code'] != '1435' ) {
@@ -1179,13 +1179,16 @@ class DatabaseOracle extends Database {
}
public function delete( $table, $conds, $fname = __METHOD__ ) {
+ global $wgActorTableSchemaMigrationStage;
+
if ( is_array( $conds ) ) {
$conds = $this->wrapConditionsForWhere( $table, $conds );
}
// a hack for deleting pages, users and images (which have non-nullable FKs)
// all deletions on these tables have transactions so final failure rollbacks these updates
+ // @todo: Normalize the schema to match MySQL, no special FKs and such
$table = $this->tableName( $table );
- if ( $table == $this->tableName( 'user' ) ) {
+ if ( $table == $this->tableName( 'user' ) && $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
$this->update( 'archive', [ 'ar_user' => 0 ],
[ 'ar_user' => $conds['user_id'] ], $fname );
$this->update( 'ipblocks', [ 'ipb_user' => 0 ],
@@ -1245,9 +1248,9 @@ class DatabaseOracle extends Database {
$sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
}
- $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql );
+ $this->mLastResult = $stmt = oci_parse( $this->conn, $sql );
if ( $stmt === false ) {
- $e = oci_error( $this->mConn );
+ $e = oci_error( $this->conn );
$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
return false;
@@ -1276,7 +1279,7 @@ class DatabaseOracle extends Database {
}
} else {
/** @var OCI_Lob[] $lob */
- $lob[$col] = oci_new_descriptor( $this->mConn, OCI_D_LOB );
+ $lob[$col] = oci_new_descriptor( $this->conn, OCI_D_LOB );
if ( $lob[$col] === false ) {
$e = oci_error( $stmt );
throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] );
@@ -1296,7 +1299,7 @@ class DatabaseOracle extends Database {
}
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
if ( oci_execute( $stmt, $this->execFlags() ) === false ) {
$e = oci_error( $stmt );
@@ -1311,7 +1314,7 @@ class DatabaseOracle extends Database {
$this->mAffectedRows = oci_num_rows( $stmt );
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( isset( $lob ) ) {
foreach ( $lob as $lob_v ) {
@@ -1319,8 +1322,8 @@ class DatabaseOracle extends Database {
}
}
- if ( !$this->mTrxLevel ) {
- oci_commit( $this->mConn );
+ if ( !$this->trxLevel ) {
+ oci_commit( $this->conn );
}
return oci_free_statement( $stmt );
@@ -1340,11 +1343,11 @@ class DatabaseOracle extends Database {
}
function getDBname() {
- return $this->mDBname;
+ return $this->dbName;
}
function getServer() {
- return $this->mServer;
+ return $this->server;
}
public function buildGroupConcatField(
@@ -1355,6 +1358,15 @@ class DatabaseOracle extends Database {
return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
}
+ public function buildSubstring( $input, $startPosition, $length = null ) {
+ $this->assertBuildSubstringParams( $startPosition, $length );
+ $params = [ $input, $startPosition ];
+ if ( $length !== null ) {
+ $params[] = $length;
+ }
+ return 'SUBSTR(' . implode( ',', $params ) . ')';
+ }
+
/**
* @param string $field Field or column to cast
* @return string
diff --git a/www/wiki/includes/db/DatabasePostgres.php b/www/wiki/includes/db/DatabasePostgres.php
deleted file mode 100644
index 6e76cdbf..00000000
--- a/www/wiki/includes/db/DatabasePostgres.php
+++ /dev/null
@@ -1,1626 +0,0 @@
-<?php
-/**
- * This is the Postgres database abstraction layer.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-class PostgresField implements Field {
- private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname,
- $has_default, $default;
-
- /**
- * @param DatabaseBase $db
- * @param string $table
- * @param string $field
- * @return null|PostgresField
- */
- static function fromText( $db, $table, $field ) {
- $q = <<<SQL
-SELECT
- attnotnull, attlen, conname AS conname,
- atthasdef,
- adsrc,
- COALESCE(condeferred, 'f') AS deferred,
- COALESCE(condeferrable, 'f') AS deferrable,
- CASE WHEN typname = 'int2' THEN 'smallint'
- WHEN typname = 'int4' THEN 'integer'
- WHEN typname = 'int8' THEN 'bigint'
- WHEN typname = 'bpchar' THEN 'char'
- ELSE typname END AS typname
-FROM pg_class c
-JOIN pg_namespace n ON (n.oid = c.relnamespace)
-JOIN pg_attribute a ON (a.attrelid = c.oid)
-JOIN pg_type t ON (t.oid = a.atttypid)
-LEFT JOIN pg_constraint o ON (o.conrelid = c.oid AND a.attnum = ANY(o.conkey) AND o.contype = 'f')
-LEFT JOIN pg_attrdef d on c.oid=d.adrelid and a.attnum=d.adnum
-WHERE relkind = 'r'
-AND nspname=%s
-AND relname=%s
-AND attname=%s;
-SQL;
-
- $table = $db->tableName( $table, 'raw' );
- $res = $db->query(
- sprintf( $q,
- $db->addQuotes( $db->getCoreSchema() ),
- $db->addQuotes( $table ),
- $db->addQuotes( $field )
- )
- );
- $row = $db->fetchObject( $res );
- if ( !$row ) {
- return null;
- }
- $n = new PostgresField;
- $n->type = $row->typname;
- $n->nullable = ( $row->attnotnull == 'f' );
- $n->name = $field;
- $n->tablename = $table;
- $n->max_length = $row->attlen;
- $n->deferrable = ( $row->deferrable == 't' );
- $n->deferred = ( $row->deferred == 't' );
- $n->conname = $row->conname;
- $n->has_default = ( $row->atthasdef === 't' );
- $n->default = $row->adsrc;
-
- return $n;
- }
-
- function name() {
- return $this->name;
- }
-
- function tableName() {
- return $this->tablename;
- }
-
- function type() {
- return $this->type;
- }
-
- function isNullable() {
- return $this->nullable;
- }
-
- function maxLength() {
- return $this->max_length;
- }
-
- function is_deferrable() {
- return $this->deferrable;
- }
-
- function is_deferred() {
- return $this->deferred;
- }
-
- function conname() {
- return $this->conname;
- }
-
- /**
- * @since 1.19
- * @return bool|mixed
- */
- function defaultValue() {
- if ( $this->has_default ) {
- return $this->default;
- } else {
- return false;
- }
- }
-}
-
-/**
- * Manage savepoints within a transaction
- * @ingroup Database
- * @since 1.19
- */
-class SavepointPostgres {
- /** @var DatabasePostgres Establish a savepoint within a transaction */
- protected $dbw;
- protected $id;
- protected $didbegin;
-
- /**
- * @param DatabaseBase $dbw
- * @param int $id
- */
- public function __construct( $dbw, $id ) {
- $this->dbw = $dbw;
- $this->id = $id;
- $this->didbegin = false;
- /* If we are not in a transaction, we need to be for savepoint trickery */
- if ( !$dbw->trxLevel() ) {
- $dbw->begin( "FOR SAVEPOINT" );
- $this->didbegin = true;
- }
- }
-
- public function __destruct() {
- if ( $this->didbegin ) {
- $this->dbw->rollback();
- $this->didbegin = false;
- }
- }
-
- public function commit() {
- if ( $this->didbegin ) {
- $this->dbw->commit();
- $this->didbegin = false;
- }
- }
-
- protected function query( $keyword, $msg_ok, $msg_failed ) {
- if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) {
- } else {
- wfDebug( sprintf( $msg_failed, $this->id ) );
- }
- }
-
- public function savepoint() {
- $this->query( "SAVEPOINT",
- "Transaction state: savepoint \"%s\" established.\n",
- "Transaction state: establishment of savepoint \"%s\" FAILED.\n"
- );
- }
-
- public function release() {
- $this->query( "RELEASE",
- "Transaction state: savepoint \"%s\" released.\n",
- "Transaction state: release of savepoint \"%s\" FAILED.\n"
- );
- }
-
- public function rollback() {
- $this->query( "ROLLBACK TO",
- "Transaction state: savepoint \"%s\" rolled back.\n",
- "Transaction state: rollback of savepoint \"%s\" FAILED.\n"
- );
- }
-
- public function __toString() {
- return (string)$this->id;
- }
-}
-
-/**
- * @ingroup Database
- */
-class DatabasePostgres extends Database {
- /** @var resource */
- protected $mLastResult = null;
-
- /** @var int The number of rows affected as an integer */
- protected $mAffectedRows = null;
-
- /** @var int */
- private $mInsertId = null;
-
- /** @var float|string */
- private $numericVersion = null;
-
- /** @var string Connect string to open a PostgreSQL connection */
- private $connectString;
-
- /** @var string */
- private $mCoreSchema;
-
- function getType() {
- return 'postgres';
- }
-
- function cascadingDeletes() {
- return true;
- }
-
- function cleanupTriggers() {
- return true;
- }
-
- function strictIPs() {
- return true;
- }
-
- function realTimestamps() {
- return true;
- }
-
- function implicitGroupby() {
- return false;
- }
-
- function implicitOrderby() {
- return false;
- }
-
- function searchableIPs() {
- return true;
- }
-
- function functionalIndexes() {
- return true;
- }
-
- function hasConstraint( $name ) {
- $sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " .
- "WHERE c.connamespace = n.oid AND conname = '" .
- pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" .
- pg_escape_string( $this->mConn, $this->getCoreSchema() ) . "'";
- $res = $this->doQuery( $sql );
-
- return $this->numRows( $res );
- }
-
- /**
- * Usually aborts on failure
- * @param string $server
- * @param string $user
- * @param string $password
- * @param string $dbName
- * @throws DBConnectionError|Exception
- * @return DatabaseBase|null
- */
- function open( $server, $user, $password, $dbName ) {
- # Test for Postgres support, to avoid suppressed fatal error
- if ( !function_exists( 'pg_connect' ) ) {
- throw new DBConnectionError(
- $this,
- "Postgres functions missing, have you compiled PHP with the --with-pgsql\n" .
- "option? (Note: if you recently installed PHP, you may need to restart your\n" .
- "webserver and database)\n"
- );
- }
-
- global $wgDBport;
-
- if ( !strlen( $user ) ) { # e.g. the class is being loaded
- return null;
- }
-
- $this->mServer = $server;
- $port = $wgDBport;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
-
- $connectVars = [
- 'dbname' => $dbName,
- 'user' => $user,
- 'password' => $password
- ];
- if ( $server != false && $server != '' ) {
- $connectVars['host'] = $server;
- }
- if ( $port != false && $port != '' ) {
- $connectVars['port'] = $port;
- }
- if ( $this->mFlags & DBO_SSL ) {
- $connectVars['sslmode'] = 1;
- }
-
- $this->connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW );
- $this->close();
- $this->installErrorHandler();
-
- try {
- $this->mConn = pg_connect( $this->connectString );
- } catch ( Exception $ex ) {
- $this->restoreErrorHandler();
- throw $ex;
- }
-
- $phpError = $this->restoreErrorHandler();
-
- if ( !$this->mConn ) {
- wfDebug( "DB connection error\n" );
- wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " .
- substr( $password, 0, 3 ) . "...\n" );
- wfDebug( $this->lastError() . "\n" );
- throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
- }
-
- $this->mOpened = true;
-
- global $wgCommandLineMode;
- # If called from the command-line (e.g. importDump), only show errors
- if ( $wgCommandLineMode ) {
- $this->doQuery( "SET client_min_messages = 'ERROR'" );
- }
-
- $this->query( "SET client_encoding='UTF8'", __METHOD__ );
- $this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ );
- $this->query( "SET timezone = 'GMT'", __METHOD__ );
- $this->query( "SET standard_conforming_strings = on", __METHOD__ );
- if ( $this->getServerVersion() >= 9.0 ) {
- $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
- }
-
- global $wgDBmwschema;
- $this->determineCoreSchema( $wgDBmwschema );
-
- return $this->mConn;
- }
-
- /**
- * Postgres doesn't support selectDB in the same way MySQL does. So if the
- * DB name doesn't match the open connection, open a new one
- * @param string $db
- * @return bool
- */
- function selectDB( $db ) {
- if ( $this->mDBname !== $db ) {
- return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
- } else {
- return true;
- }
- }
-
- function makeConnectionString( $vars ) {
- $s = '';
- foreach ( $vars as $name => $value ) {
- $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' ";
- }
-
- return $s;
- }
-
- /**
- * Closes a database connection, if it is open
- * Returns success, true if already closed
- * @return bool
- */
- protected function closeConnection() {
- return pg_close( $this->mConn );
- }
-
- public function doQuery( $sql ) {
- $sql = mb_convert_encoding( $sql, 'UTF-8' );
- // Clear previously left over PQresult
- while ( $res = pg_get_result( $this->mConn ) ) {
- pg_free_result( $res );
- }
- if ( pg_send_query( $this->mConn, $sql ) === false ) {
- throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
- }
- $this->mLastResult = pg_get_result( $this->mConn );
- $this->mAffectedRows = null;
- if ( pg_result_error( $this->mLastResult ) ) {
- return false;
- }
-
- return $this->mLastResult;
- }
-
- protected function dumpError() {
- $diags = [
- PGSQL_DIAG_SEVERITY,
- PGSQL_DIAG_SQLSTATE,
- PGSQL_DIAG_MESSAGE_PRIMARY,
- PGSQL_DIAG_MESSAGE_DETAIL,
- PGSQL_DIAG_MESSAGE_HINT,
- PGSQL_DIAG_STATEMENT_POSITION,
- PGSQL_DIAG_INTERNAL_POSITION,
- PGSQL_DIAG_INTERNAL_QUERY,
- PGSQL_DIAG_CONTEXT,
- PGSQL_DIAG_SOURCE_FILE,
- PGSQL_DIAG_SOURCE_LINE,
- PGSQL_DIAG_SOURCE_FUNCTION
- ];
- foreach ( $diags as $d ) {
- wfDebug( sprintf( "PgSQL ERROR(%d): %s\n",
- $d, pg_result_error_field( $this->mLastResult, $d ) ) );
- }
- }
-
- function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- if ( $tempIgnore ) {
- /* Check for constraint violation */
- if ( $errno === '23505' ) {
- parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore );
-
- return;
- }
- }
- /* Transaction stays in the ERROR state until rolled back */
- if ( $this->mTrxLevel ) {
- $ignore = $this->ignoreErrors( true );
- $this->rollback( __METHOD__ );
- $this->ignoreErrors( $ignore );
- }
- parent::reportQueryError( $error, $errno, $sql, $fname, false );
- }
-
- function queryIgnore( $sql, $fname = __METHOD__ ) {
- return $this->query( $sql, $fname, true );
- }
-
- /**
- * @param stdClass|ResultWrapper $res
- * @throws DBUnexpectedError
- */
- function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- MediaWiki\suppressWarnings();
- $ok = pg_free_result( $res );
- MediaWiki\restoreWarnings();
- if ( !$ok ) {
- throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
- }
- }
-
- /**
- * @param ResultWrapper|stdClass $res
- * @return stdClass
- * @throws DBUnexpectedError
- */
- function fetchObject( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- MediaWiki\suppressWarnings();
- $row = pg_fetch_object( $res );
- MediaWiki\restoreWarnings();
- # @todo FIXME: HACK HACK HACK HACK debug
-
- # @todo hashar: not sure if the following test really trigger if the object
- # fetching failed.
- if ( pg_last_error( $this->mConn ) ) {
- throw new DBUnexpectedError(
- $this,
- 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
- );
- }
-
- return $row;
- }
-
- function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- MediaWiki\suppressWarnings();
- $row = pg_fetch_array( $res );
- MediaWiki\restoreWarnings();
- if ( pg_last_error( $this->mConn ) ) {
- throw new DBUnexpectedError(
- $this,
- 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
- );
- }
-
- return $row;
- }
-
- function numRows( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- MediaWiki\suppressWarnings();
- $n = pg_num_rows( $res );
- MediaWiki\restoreWarnings();
- if ( pg_last_error( $this->mConn ) ) {
- throw new DBUnexpectedError(
- $this,
- 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
- );
- }
-
- return $n;
- }
-
- function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return pg_num_fields( $res );
- }
-
- function fieldName( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return pg_field_name( $res, $n );
- }
-
- /**
- * Return the result of the last call to nextSequenceValue();
- * This must be called after nextSequenceValue().
- *
- * @return int|null
- */
- function insertId() {
- return $this->mInsertId;
- }
-
- /**
- * @param mixed $res
- * @param int $row
- * @return bool
- */
- function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return pg_result_seek( $res, $row );
- }
-
- function lastError() {
- if ( $this->mConn ) {
- if ( $this->mLastResult ) {
- return pg_result_error( $this->mLastResult );
- } else {
- return pg_last_error();
- }
- } else {
- return 'No database connection';
- }
- }
-
- function lastErrno() {
- if ( $this->mLastResult ) {
- return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
- } else {
- return false;
- }
- }
-
- function affectedRows() {
- if ( !is_null( $this->mAffectedRows ) ) {
- // Forced result for simulated queries
- return $this->mAffectedRows;
- }
- if ( empty( $this->mLastResult ) ) {
- return 0;
- }
-
- return pg_affected_rows( $this->mLastResult );
- }
-
- /**
- * Estimate rows in dataset
- * Returns estimated count, based on EXPLAIN output
- * This is not necessarily an accurate estimate, so use sparingly
- * Returns -1 if count cannot be found
- * Takes same arguments as Database::select()
- *
- * @param string $table
- * @param string $vars
- * @param string $conds
- * @param string $fname
- * @param array $options
- * @return int
- */
- function estimateRowCount( $table, $vars = '*', $conds = '',
- $fname = __METHOD__, $options = []
- ) {
- $options['EXPLAIN'] = true;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
- $rows = -1;
- if ( $res ) {
- $row = $this->fetchRow( $res );
- $count = [];
- if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
- $rows = (int)$count[1];
- }
- }
-
- return $rows;
- }
-
- /**
- * Returns information about an index
- * If errors are explicitly ignored, returns NULL on failure
- *
- * @param string $table
- * @param string $index
- * @param string $fname
- * @return bool|null
- */
- function indexInfo( $table, $index, $fname = __METHOD__ ) {
- $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
- $res = $this->query( $sql, $fname );
- if ( !$res ) {
- return null;
- }
- foreach ( $res as $row ) {
- if ( $row->indexname == $this->indexName( $index ) ) {
- return $row;
- }
- }
-
- return false;
- }
-
- /**
- * Returns is of attributes used in index
- *
- * @since 1.19
- * @param string $index
- * @param bool|string $schema
- * @return array
- */
- function indexAttributes( $index, $schema = false ) {
- if ( $schema === false ) {
- $schema = $this->getCoreSchema();
- }
- /*
- * A subquery would be not needed if we didn't care about the order
- * of attributes, but we do
- */
- $sql = <<<__INDEXATTR__
-
- SELECT opcname,
- attname,
- i.indoption[s.g] as option,
- pg_am.amname
- FROM
- (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g
- FROM
- pg_index isub
- JOIN pg_class cis
- ON cis.oid=isub.indexrelid
- JOIN pg_namespace ns
- ON cis.relnamespace = ns.oid
- WHERE cis.relname='$index' AND ns.nspname='$schema') AS s,
- pg_attribute,
- pg_opclass opcls,
- pg_am,
- pg_class ci
- JOIN pg_index i
- ON ci.oid=i.indexrelid
- JOIN pg_class ct
- ON ct.oid = i.indrelid
- JOIN pg_namespace n
- ON ci.relnamespace = n.oid
- WHERE
- ci.relname='$index' AND n.nspname='$schema'
- AND attrelid = ct.oid
- AND i.indkey[s.g] = attnum
- AND i.indclass[s.g] = opcls.oid
- AND pg_am.oid = opcls.opcmethod
-__INDEXATTR__;
- $res = $this->query( $sql, __METHOD__ );
- $a = [];
- if ( $res ) {
- foreach ( $res as $row ) {
- $a[] = [
- $row->attname,
- $row->opcname,
- $row->amname,
- $row->option ];
- }
- } else {
- return null;
- }
-
- return $a;
- }
-
- function indexUnique( $table, $index, $fname = __METHOD__ ) {
- $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" .
- " AND indexdef LIKE 'CREATE UNIQUE%(" .
- $this->strencode( $this->indexName( $index ) ) .
- ")'";
- $res = $this->query( $sql, $fname );
- if ( !$res ) {
- return null;
- }
-
- return $res->numRows() > 0;
- }
-
- /**
- * Change the FOR UPDATE option as necessary based on the join conditions. Then pass
- * to the parent function to get the actual SQL text.
- *
- * In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
- * can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to do
- * so causes a DB error. This wrapper checks which tables can be locked and adjusts it accordingly.
- *
- * MySQL uses "ORDER BY NULL" as an optimization hint, but that syntax is illegal in PostgreSQL.
- * @see DatabaseBase::selectSQLText
- */
- function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
- $options = [], $join_conds = []
- ) {
- if ( is_array( $options ) ) {
- $forUpdateKey = array_search( 'FOR UPDATE', $options, true );
- if ( $forUpdateKey !== false && $join_conds ) {
- unset( $options[$forUpdateKey] );
-
- foreach ( $join_conds as $table_cond => $join_cond ) {
- if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
- $options['FOR UPDATE'][] = $table_cond;
- }
- }
- }
-
- if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) {
- unset( $options['ORDER BY'] );
- }
- }
-
- return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
- }
-
- /**
- * INSERT wrapper, inserts an array into a table
- *
- * $args may be a single associative array, or an array of these with numeric keys,
- * for multi-row insert (Postgres version 8.2 and above only).
- *
- * @param string $table Name of the table to insert to.
- * @param array $args Items to insert into the table.
- * @param string $fname Name of the function, for profiling
- * @param array|string $options String or array. Valid options: IGNORE
- * @return bool Success of insert operation. IGNORE always returns true.
- */
- function insert( $table, $args, $fname = __METHOD__, $options = [] ) {
- if ( !count( $args ) ) {
- return true;
- }
-
- $table = $this->tableName( $table );
- if ( !isset( $this->numericVersion ) ) {
- $this->getServerVersion();
- }
-
- if ( !is_array( $options ) ) {
- $options = [ $options ];
- }
-
- if ( isset( $args[0] ) && is_array( $args[0] ) ) {
- $multi = true;
- $keys = array_keys( $args[0] );
- } else {
- $multi = false;
- $keys = array_keys( $args );
- }
-
- // If IGNORE is set, we use savepoints to emulate mysql's behavior
- $savepoint = null;
- if ( in_array( 'IGNORE', $options ) ) {
- $savepoint = new SavepointPostgres( $this, 'mw' );
- $olde = error_reporting( 0 );
- // For future use, we may want to track the number of actual inserts
- // Right now, insert (all writes) simply return true/false
- $numrowsinserted = 0;
- }
-
- $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
-
- if ( $multi ) {
- if ( $this->numericVersion >= 8.2 && !$savepoint ) {
- $first = true;
- foreach ( $args as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeList( $row ) . ')';
- }
- $res = (bool)$this->query( $sql, $fname, $savepoint );
- } else {
- $res = true;
- $origsql = $sql;
- foreach ( $args as $row ) {
- $tempsql = $origsql;
- $tempsql .= '(' . $this->makeList( $row ) . ')';
-
- if ( $savepoint ) {
- $savepoint->savepoint();
- }
-
- $tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
-
- if ( $savepoint ) {
- $bar = pg_result_error( $this->mLastResult );
- if ( $bar != false ) {
- $savepoint->rollback();
- } else {
- $savepoint->release();
- $numrowsinserted++;
- }
- }
-
- // If any of them fail, we fail overall for this function call
- // Note that this will be ignored if IGNORE is set
- if ( !$tempres ) {
- $res = false;
- }
- }
- }
- } else {
- // Not multi, just a lone insert
- if ( $savepoint ) {
- $savepoint->savepoint();
- }
-
- $sql .= '(' . $this->makeList( $args ) . ')';
- $res = (bool)$this->query( $sql, $fname, $savepoint );
- if ( $savepoint ) {
- $bar = pg_result_error( $this->mLastResult );
- if ( $bar != false ) {
- $savepoint->rollback();
- } else {
- $savepoint->release();
- $numrowsinserted++;
- }
- }
- }
- if ( $savepoint ) {
- error_reporting( $olde );
- $savepoint->commit();
-
- // Set the affected row count for the whole operation
- $this->mAffectedRows = $numrowsinserted;
-
- // IGNORE always returns true
- return true;
- }
-
- return $res;
- }
-
- /**
- * INSERT SELECT wrapper
- * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
- * Source items may be literals rather then field names, but strings should
- * be quoted with Database::addQuotes()
- * $conds may be "*" to copy the whole table
- * srcTable may be an array of tables.
- * @todo FIXME: Implement this a little better (seperate select/insert)?
- *
- * @param string $destTable
- * @param array|string $srcTable
- * @param array $varMap
- * @param array $conds
- * @param string $fname
- * @param array $insertOptions
- * @param array $selectOptions
- * @return bool
- */
- function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
- $insertOptions = [], $selectOptions = [] ) {
- $destTable = $this->tableName( $destTable );
-
- if ( !is_array( $insertOptions ) ) {
- $insertOptions = [ $insertOptions ];
- }
-
- /*
- * If IGNORE is set, we use savepoints to emulate mysql's behavior
- * Ignore LOW PRIORITY option, since it is MySQL-specific
- */
- $savepoint = null;
- if ( in_array( 'IGNORE', $insertOptions ) ) {
- $savepoint = new SavepointPostgres( $this, 'mw' );
- $olde = error_reporting( 0 );
- $numrowsinserted = 0;
- $savepoint->savepoint();
- }
-
- if ( !is_array( $selectOptions ) ) {
- $selectOptions = [ $selectOptions ];
- }
- list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
- if ( is_array( $srcTable ) ) {
- $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) );
- } else {
- $srcTable = $this->tableName( $srcTable );
- }
-
- $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
- " SELECT $startOpts " . implode( ',', $varMap ) .
- " FROM $srcTable $useIndex";
-
- if ( $conds != '*' ) {
- $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
- }
-
- $sql .= " $tailOpts";
-
- $res = (bool)$this->query( $sql, $fname, $savepoint );
- if ( $savepoint ) {
- $bar = pg_result_error( $this->mLastResult );
- if ( $bar != false ) {
- $savepoint->rollback();
- } else {
- $savepoint->release();
- $numrowsinserted++;
- }
- error_reporting( $olde );
- $savepoint->commit();
-
- // Set the affected row count for the whole operation
- $this->mAffectedRows = $numrowsinserted;
-
- // IGNORE always returns true
- return true;
- }
-
- return $res;
- }
-
- function tableName( $name, $format = 'quoted' ) {
- # Replace reserved words with better ones
- switch ( $name ) {
- case 'user':
- return $this->realTableName( 'mwuser', $format );
- case 'text':
- return $this->realTableName( 'pagecontent', $format );
- default:
- return $this->realTableName( $name, $format );
- }
- }
-
- /* Don't cheat on installer */
- function realTableName( $name, $format = 'quoted' ) {
- return parent::tableName( $name, $format );
- }
-
- /**
- * Return the next in a sequence, save the value for retrieval via insertId()
- *
- * @param string $seqName
- * @return int|null
- */
- function nextSequenceValue( $seqName ) {
- $safeseq = str_replace( "'", "''", $seqName );
- $res = $this->query( "SELECT nextval('$safeseq')" );
- $row = $this->fetchRow( $res );
- $this->mInsertId = $row[0];
-
- return $this->mInsertId;
- }
-
- /**
- * Return the current value of a sequence. Assumes it has been nextval'ed in this session.
- *
- * @param string $seqName
- * @return int
- */
- function currentSequenceValue( $seqName ) {
- $safeseq = str_replace( "'", "''", $seqName );
- $res = $this->query( "SELECT currval('$safeseq')" );
- $row = $this->fetchRow( $res );
- $currval = $row[0];
-
- return $currval;
- }
-
- # Returns the size of a text field, or -1 for "unlimited"
- function textFieldSize( $table, $field ) {
- $table = $this->tableName( $table );
- $sql = "SELECT t.typname as ftype,a.atttypmod as size
- FROM pg_class c, pg_attribute a, pg_type t
- WHERE relname='$table' AND a.attrelid=c.oid AND
- a.atttypid=t.oid and a.attname='$field'";
- $res = $this->query( $sql );
- $row = $this->fetchObject( $res );
- if ( $row->ftype == 'varchar' ) {
- $size = $row->size - 4;
- } else {
- $size = $row->size;
- }
-
- return $size;
- }
-
- function limitResult( $sql, $limit, $offset = false ) {
- return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' );
- }
-
- function wasDeadlock() {
- return $this->lastErrno() == '40P01';
- }
-
- function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
- $newName = $this->addIdentifierQuotes( $newName );
- $oldName = $this->addIdentifierQuotes( $oldName );
-
- return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName " .
- "(LIKE $oldName INCLUDING DEFAULTS)", $fname );
- }
-
- function listTables( $prefix = null, $fname = __METHOD__ ) {
- $eschema = $this->addQuotes( $this->getCoreSchema() );
- $result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
- $endArray = [];
-
- foreach ( $result as $table ) {
- $vars = get_object_vars( $table );
- $table = array_pop( $vars );
- if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
- $endArray[] = $table;
- }
- }
-
- return $endArray;
- }
-
- function timestamp( $ts = 0 ) {
- return wfTimestamp( TS_POSTGRES, $ts );
- }
-
- /**
- * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12
- * to http://www.php.net/manual/en/ref.pgsql.php
- *
- * Parsing a postgres array can be a tricky problem, he's my
- * take on this, it handles multi-dimensional arrays plus
- * escaping using a nasty regexp to determine the limits of each
- * data-item.
- *
- * This should really be handled by PHP PostgreSQL module
- *
- * @since 1.19
- * @param string $text Postgreql array returned in a text form like {a,b}
- * @param string $output
- * @param int $limit
- * @param int $offset
- * @return string
- */
- function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
- if ( false === $limit ) {
- $limit = strlen( $text ) - 1;
- $output = [];
- }
- if ( '{}' == $text ) {
- return $output;
- }
- do {
- if ( '{' != $text[$offset] ) {
- preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/",
- $text, $match, 0, $offset );
- $offset += strlen( $match[0] );
- $output[] = ( '"' != $match[1][0]
- ? $match[1]
- : stripcslashes( substr( $match[1], 1, -1 ) ) );
- if ( '},' == $match[3] ) {
- return $output;
- }
- } else {
- $offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 );
- }
- } while ( $limit > $offset );
-
- return $output;
- }
-
- /**
- * Return aggregated value function call
- * @param array $valuedata
- * @param string $valuename
- * @return array
- */
- public function aggregateValue( $valuedata, $valuename = 'value' ) {
- return $valuedata;
- }
-
- /**
- * @return string Wikitext of a link to the server software's web site
- */
- public function getSoftwareLink() {
- return '[{{int:version-db-postgres-url}} PostgreSQL]';
- }
-
- /**
- * Return current schema (executes SELECT current_schema())
- * Needs transaction
- *
- * @since 1.19
- * @return string Default schema for the current session
- */
- function getCurrentSchema() {
- $res = $this->query( "SELECT current_schema()", __METHOD__ );
- $row = $this->fetchRow( $res );
-
- return $row[0];
- }
-
- /**
- * Return list of schemas which are accessible without schema name
- * This is list does not contain magic keywords like "$user"
- * Needs transaction
- *
- * @see getSearchPath()
- * @see setSearchPath()
- * @since 1.19
- * @return array List of actual schemas for the current sesson
- */
- function getSchemas() {
- $res = $this->query( "SELECT current_schemas(false)", __METHOD__ );
- $row = $this->fetchRow( $res );
- $schemas = [];
-
- /* PHP pgsql support does not support array type, "{a,b}" string is returned */
-
- return $this->pg_array_parse( $row[0], $schemas );
- }
-
- /**
- * Return search patch for schemas
- * This is different from getSchemas() since it contain magic keywords
- * (like "$user").
- * Needs transaction
- *
- * @since 1.19
- * @return array How to search for table names schemas for the current user
- */
- function getSearchPath() {
- $res = $this->query( "SHOW search_path", __METHOD__ );
- $row = $this->fetchRow( $res );
-
- /* PostgreSQL returns SHOW values as strings */
-
- return explode( ",", $row[0] );
- }
-
- /**
- * Update search_path, values should already be sanitized
- * Values may contain magic keywords like "$user"
- * @since 1.19
- *
- * @param array $search_path List of schemas to be searched by default
- */
- function setSearchPath( $search_path ) {
- $this->query( "SET search_path = " . implode( ", ", $search_path ) );
- }
-
- /**
- * Determine default schema for MediaWiki core
- * Adjust this session schema search path if desired schema exists
- * and is not alread there.
- *
- * We need to have name of the core schema stored to be able
- * to query database metadata.
- *
- * This will be also called by the installer after the schema is created
- *
- * @since 1.19
- *
- * @param string $desiredSchema
- */
- function determineCoreSchema( $desiredSchema ) {
- $this->begin( __METHOD__ );
- if ( $this->schemaExists( $desiredSchema ) ) {
- if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
- $this->mCoreSchema = $desiredSchema;
- wfDebug( "Schema \"" . $desiredSchema . "\" already in the search path\n" );
- } else {
- /**
- * Prepend our schema (e.g. 'mediawiki') in front
- * of the search path
- * Fixes bug 15816
- */
- $search_path = $this->getSearchPath();
- array_unshift( $search_path,
- $this->addIdentifierQuotes( $desiredSchema ) );
- $this->setSearchPath( $search_path );
- $this->mCoreSchema = $desiredSchema;
- wfDebug( "Schema \"" . $desiredSchema . "\" added to the search path\n" );
- }
- } else {
- $this->mCoreSchema = $this->getCurrentSchema();
- wfDebug( "Schema \"" . $desiredSchema . "\" not found, using current \"" .
- $this->mCoreSchema . "\"\n" );
- }
- /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
- $this->commit( __METHOD__ );
- }
-
- /**
- * Return schema name fore core MediaWiki tables
- *
- * @since 1.19
- * @return string Core schema name
- */
- function getCoreSchema() {
- return $this->mCoreSchema;
- }
-
- /**
- * @return string Version information from the database
- */
- function getServerVersion() {
- if ( !isset( $this->numericVersion ) ) {
- $versionInfo = pg_version( $this->mConn );
- if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) {
- // Old client, abort install
- $this->numericVersion = '7.3 or earlier';
- } elseif ( isset( $versionInfo['server'] ) ) {
- // Normal client
- $this->numericVersion = $versionInfo['server'];
- } else {
- // Bug 16937: broken pgsql extension from PHP<5.3
- $this->numericVersion = pg_parameter_status( $this->mConn, 'server_version' );
- }
- }
-
- return $this->numericVersion;
- }
-
- /**
- * Query whether a given relation exists (in the given schema, or the
- * default mw one if not given)
- * @param string $table
- * @param array|string $types
- * @param bool|string $schema
- * @return bool
- */
- function relationExists( $table, $types, $schema = false ) {
- if ( !is_array( $types ) ) {
- $types = [ $types ];
- }
- if ( !$schema ) {
- $schema = $this->getCoreSchema();
- }
- $table = $this->realTableName( $table, 'raw' );
- $etable = $this->addQuotes( $table );
- $eschema = $this->addQuotes( $schema );
- $sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
- . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
- . "AND c.relkind IN ('" . implode( "','", $types ) . "')";
- $res = $this->query( $sql );
- $count = $res ? $res->numRows() : 0;
-
- return (bool)$count;
- }
-
- /**
- * For backward compatibility, this function checks both tables and
- * views.
- * @param string $table
- * @param string $fname
- * @param bool|string $schema
- * @return bool
- */
- function tableExists( $table, $fname = __METHOD__, $schema = false ) {
- return $this->relationExists( $table, [ 'r', 'v' ], $schema );
- }
-
- function sequenceExists( $sequence, $schema = false ) {
- return $this->relationExists( $sequence, 'S', $schema );
- }
-
- function triggerExists( $table, $trigger ) {
- $q = <<<SQL
- SELECT 1 FROM pg_class, pg_namespace, pg_trigger
- WHERE relnamespace=pg_namespace.oid AND relkind='r'
- AND tgrelid=pg_class.oid
- AND nspname=%s AND relname=%s AND tgname=%s
-SQL;
- $res = $this->query(
- sprintf(
- $q,
- $this->addQuotes( $this->getCoreSchema() ),
- $this->addQuotes( $table ),
- $this->addQuotes( $trigger )
- )
- );
- if ( !$res ) {
- return null;
- }
- $rows = $res->numRows();
-
- return $rows;
- }
-
- function ruleExists( $table, $rule ) {
- $exists = $this->selectField( 'pg_rules', 'rulename',
- [
- 'rulename' => $rule,
- 'tablename' => $table,
- 'schemaname' => $this->getCoreSchema()
- ]
- );
-
- return $exists === $rule;
- }
-
- function constraintExists( $table, $constraint ) {
- $sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
- "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
- $this->addQuotes( $this->getCoreSchema() ),
- $this->addQuotes( $table ),
- $this->addQuotes( $constraint )
- );
- $res = $this->query( $sql );
- if ( !$res ) {
- return null;
- }
- $rows = $res->numRows();
-
- return $rows;
- }
-
- /**
- * Query whether a given schema exists. Returns true if it does, false if it doesn't.
- * @param string $schema
- * @return bool
- */
- function schemaExists( $schema ) {
- $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1,
- [ 'nspname' => $schema ], __METHOD__ );
-
- return (bool)$exists;
- }
-
- /**
- * Returns true if a given role (i.e. user) exists, false otherwise.
- * @param string $roleName
- * @return bool
- */
- function roleExists( $roleName ) {
- $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1,
- [ 'rolname' => $roleName ], __METHOD__ );
-
- return (bool)$exists;
- }
-
- function fieldInfo( $table, $field ) {
- return PostgresField::fromText( $this, $table, $field );
- }
-
- /**
- * pg_field_type() wrapper
- * @param ResultWrapper|resource $res ResultWrapper or PostgreSQL query result resource
- * @param int $index Field number, starting from 0
- * @return string
- */
- function fieldType( $res, $index ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
-
- return pg_field_type( $res, $index );
- }
-
- /**
- * @param string $b
- * @return Blob
- */
- function encodeBlob( $b ) {
- return new PostgresBlob( pg_escape_bytea( $b ) );
- }
-
- function decodeBlob( $b ) {
- if ( $b instanceof PostgresBlob ) {
- $b = $b->fetch();
- } elseif ( $b instanceof Blob ) {
- return $b->fetch();
- }
-
- return pg_unescape_bytea( $b );
- }
-
- function strencode( $s ) {
- // Should not be called by us
-
- return pg_escape_string( $this->mConn, $s );
- }
-
- /**
- * @param null|bool|Blob $s
- * @return int|string
- */
- function addQuotes( $s ) {
- if ( is_null( $s ) ) {
- return 'NULL';
- } elseif ( is_bool( $s ) ) {
- return intval( $s );
- } elseif ( $s instanceof Blob ) {
- if ( $s instanceof PostgresBlob ) {
- $s = $s->fetch();
- } else {
- $s = pg_escape_bytea( $this->mConn, $s->fetch() );
- }
- return "'$s'";
- }
-
- return "'" . pg_escape_string( $this->mConn, $s ) . "'";
- }
-
- /**
- * Postgres specific version of replaceVars.
- * Calls the parent version in Database.php
- *
- * @param string $ins SQL string, read from a stream (usually tables.sql)
- * @return string SQL string
- */
- protected function replaceVars( $ins ) {
- $ins = parent::replaceVars( $ins );
-
- if ( $this->numericVersion >= 8.3 ) {
- // Thanks for not providing backwards-compatibility, 8.3
- $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
- }
-
- if ( $this->numericVersion <= 8.1 ) { // Our minimum version
- $ins = str_replace( 'USING gin', 'USING gist', $ins );
- }
-
- return $ins;
- }
-
- /**
- * Various select options
- *
- * @param array $options An associative array of options to be turned into
- * an SQL query, valid keys are listed in the function.
- * @return array
- */
- function makeSelectOptions( $options ) {
- $preLimitTail = $postLimitTail = '';
- $startOpts = $useIndex = '';
-
- $noKeyOptions = [];
- foreach ( $options as $key => $option ) {
- if ( is_numeric( $key ) ) {
- $noKeyOptions[$option] = true;
- }
- }
-
- $preLimitTail .= $this->makeGroupByWithHaving( $options );
-
- $preLimitTail .= $this->makeOrderBy( $options );
-
- // if ( isset( $options['LIMIT'] ) ) {
- // $tailOpts .= $this->limitResult( '', $options['LIMIT'],
- // isset( $options['OFFSET'] ) ? $options['OFFSET']
- // : false );
- // }
-
- if ( isset( $options['FOR UPDATE'] ) ) {
- $postLimitTail .= ' FOR UPDATE OF ' .
- implode( ', ', array_map( [ $this, 'tableName' ], $options['FOR UPDATE'] ) );
- } elseif ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
- $postLimitTail .= ' FOR UPDATE';
- }
-
- if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
- $startOpts .= 'DISTINCT';
- }
-
- return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
- }
-
- function getDBname() {
- return $this->mDBname;
- }
-
- function getServer() {
- return $this->mServer;
- }
-
- function buildConcat( $stringList ) {
- return implode( ' || ', $stringList );
- }
-
- public function buildGroupConcatField(
- $delimiter, $table, $field, $conds = '', $options = [], $join_conds = []
- ) {
- $fld = "array_to_string(array_agg($field)," . $this->addQuotes( $delimiter ) . ')';
-
- return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
- }
-
- public function getSearchEngine() {
- return 'SearchPostgres';
- }
-
- public function streamStatementEnd( &$sql, &$newLine ) {
- # Allow dollar quoting for function declarations
- if ( substr( $newLine, 0, 4 ) == '$mw$' ) {
- if ( $this->delimiter ) {
- $this->delimiter = false;
- } else {
- $this->delimiter = ';';
- }
- }
-
- return parent::streamStatementEnd( $sql, $newLine );
- }
-
- /**
- * Check to see if a named lock is available. This is non-blocking.
- * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
- *
- * @param string $lockName Name of lock to poll
- * @param string $method Name of method calling us
- * @return bool
- * @since 1.20
- */
- public function lockIsFree( $lockName, $method ) {
- $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
- $result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key))
- WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method );
- $row = $this->fetchObject( $result );
-
- return ( $row->lockstatus === 't' );
- }
-
- /**
- * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
- * @param string $lockName
- * @param string $method
- * @param int $timeout
- * @return bool
- */
- public function lock( $lockName, $method, $timeout = 5 ) {
- $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
- for ( $attempts = 1; $attempts <= $timeout; ++$attempts ) {
- $result = $this->query(
- "SELECT pg_try_advisory_lock($key) AS lockstatus", $method );
- $row = $this->fetchObject( $result );
- if ( $row->lockstatus === 't' ) {
- parent::lock( $lockName, $method, $timeout ); // record
- return true;
- } else {
- sleep( 1 );
- }
- }
-
- wfDebug( __METHOD__ . " failed to acquire lock\n" );
-
- return false;
- }
-
- /**
- * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM
- * PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
- * @param string $lockName
- * @param string $method
- * @return bool
- */
- public function unlock( $lockName, $method ) {
- $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
- $result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method );
- $row = $this->fetchObject( $result );
-
- if ( $row->lockstatus === 't' ) {
- parent::unlock( $lockName, $method ); // record
- return true;
- }
-
- wfDebug( __METHOD__ . " failed to release lock\n" );
-
- return false;
- }
-
- /**
- * @param string $lockName
- * @return string Integer
- */
- private function bigintFromLockName( $lockName ) {
- return Wikimedia\base_convert( substr( sha1( $lockName ), 0, 15 ), 16, 10 );
- }
-} // end DatabasePostgres class
-
-class PostgresBlob extends Blob {
-}
diff --git a/www/wiki/includes/db/DatabaseSqlite.php b/www/wiki/includes/db/DatabaseSqlite.php
deleted file mode 100644
index 9d0a0f71..00000000
--- a/www/wiki/includes/db/DatabaseSqlite.php
+++ /dev/null
@@ -1,1085 +0,0 @@
-<?php
-/**
- * This is the SQLite database abstraction layer.
- * See maintenance/sqlite/README for development notes and other specific information
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * @ingroup Database
- */
-class DatabaseSqlite extends Database {
- /** @var bool Whether full text is enabled */
- private static $fulltextEnabled = null;
-
- /** @var string Directory */
- protected $dbDir;
-
- /** @var string File name for SQLite database file */
- protected $dbPath;
-
- /** @var string Transaction mode */
- protected $trxMode;
-
- /** @var int The number of rows affected as an integer */
- protected $mAffectedRows;
-
- /** @var resource */
- protected $mLastResult;
-
- /** @var PDO */
- protected $mConn;
-
- /** @var FSLockManager (hopefully on the same server as the DB) */
- protected $lockMgr;
-
- /**
- * Additional params include:
- * - dbDirectory : directory containing the DB and the lock file directory
- * [defaults to $wgSQLiteDataDir]
- * - dbFilePath : use this to force the path of the DB file
- * - trxMode : one of (deferred, immediate, exclusive)
- * @param array $p
- */
- function __construct( array $p ) {
- global $wgSharedDB, $wgSQLiteDataDir;
-
- $this->dbDir = isset( $p['dbDirectory'] ) ? $p['dbDirectory'] : $wgSQLiteDataDir;
-
- if ( isset( $p['dbFilePath'] ) ) {
- parent::__construct( $p );
- // Standalone .sqlite file mode.
- // Super doesn't open when $user is false, but we can work with $dbName,
- // which is derived from the file path in this case.
- $this->openFile( $p['dbFilePath'] );
- } else {
- $this->mDBname = $p['dbname'];
- // Stock wiki mode using standard file names per DB.
- parent::__construct( $p );
- // Super doesn't open when $user is false, but we can work with $dbName
- if ( $p['dbname'] && !$this->isOpen() ) {
- if ( $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] ) ) {
- if ( $wgSharedDB ) {
- $this->attachDatabase( $wgSharedDB );
- }
- }
- }
- }
-
- $this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null;
- if ( $this->trxMode &&
- !in_array( $this->trxMode, [ 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ] )
- ) {
- $this->trxMode = null;
- wfWarn( "Invalid SQLite transaction mode provided." );
- }
-
- $this->lockMgr = new FSLockManager( [ 'lockDirectory' => "{$this->dbDir}/locks" ] );
- }
-
- /**
- * @param string $filename
- * @param array $p Options map; supports:
- * - flags : (same as __construct counterpart)
- * - trxMode : (same as __construct counterpart)
- * - dbDirectory : (same as __construct counterpart)
- * @return DatabaseSqlite
- * @since 1.25
- */
- public static function newStandaloneInstance( $filename, array $p = [] ) {
- $p['dbFilePath'] = $filename;
- $p['schema'] = false;
- $p['tablePrefix'] = '';
-
- return DatabaseBase::factory( 'sqlite', $p );
- }
-
- /**
- * @return string
- */
- function getType() {
- return 'sqlite';
- }
-
- /**
- * @todo Check if it should be true like parent class
- *
- * @return bool
- */
- function implicitGroupby() {
- return false;
- }
-
- /** Open an SQLite database and return a resource handle to it
- * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
- *
- * @param string $server
- * @param string $user
- * @param string $pass
- * @param string $dbName
- *
- * @throws DBConnectionError
- * @return PDO
- */
- function open( $server, $user, $pass, $dbName ) {
- $this->close();
- $fileName = self::generateFileName( $this->dbDir, $dbName );
- if ( !is_readable( $fileName ) ) {
- $this->mConn = false;
- throw new DBConnectionError( $this, "SQLite database not accessible" );
- }
- $this->openFile( $fileName );
-
- return $this->mConn;
- }
-
- /**
- * Opens a database file
- *
- * @param string $fileName
- * @throws DBConnectionError
- * @return PDO|bool SQL connection or false if failed
- */
- protected function openFile( $fileName ) {
- $err = false;
-
- $this->dbPath = $fileName;
- try {
- if ( $this->mFlags & DBO_PERSISTENT ) {
- $this->mConn = new PDO( "sqlite:$fileName", '', '',
- [ PDO::ATTR_PERSISTENT => true ] );
- } else {
- $this->mConn = new PDO( "sqlite:$fileName", '', '' );
- }
- } catch ( PDOException $e ) {
- $err = $e->getMessage();
- }
-
- if ( !$this->mConn ) {
- wfDebug( "DB connection error: $err\n" );
- throw new DBConnectionError( $this, $err );
- }
-
- $this->mOpened = !!$this->mConn;
- if ( $this->mOpened ) {
- # Set error codes only, don't raise exceptions
- $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
- # Enforce LIKE to be case sensitive, just like MySQL
- $this->query( 'PRAGMA case_sensitive_like = 1' );
-
- return $this->mConn;
- }
-
- return false;
- }
-
- /**
- * @return string SQLite DB file path
- * @since 1.25
- */
- public function getDbFilePath() {
- return $this->dbPath;
- }
-
- /**
- * Does not actually close the connection, just destroys the reference for GC to do its work
- * @return bool
- */
- protected function closeConnection() {
- $this->mConn = null;
-
- return true;
- }
-
- /**
- * Generates a database file name. Explicitly public for installer.
- * @param string $dir Directory where database resides
- * @param string $dbName Database name
- * @return string
- */
- public static function generateFileName( $dir, $dbName ) {
- return "$dir/$dbName.sqlite";
- }
-
- /**
- * Check if the searchindext table is FTS enabled.
- * @return bool False if not enabled.
- */
- function checkForEnabledSearch() {
- if ( self::$fulltextEnabled === null ) {
- self::$fulltextEnabled = false;
- $table = $this->tableName( 'searchindex' );
- $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
- if ( $res ) {
- $row = $res->fetchRow();
- self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
- }
- }
-
- return self::$fulltextEnabled;
- }
-
- /**
- * Returns version of currently supported SQLite fulltext search module or false if none present.
- * @return string
- */
- static function getFulltextSearchModule() {
- static $cachedResult = null;
- if ( $cachedResult !== null ) {
- return $cachedResult;
- }
- $cachedResult = false;
- $table = 'dummy_search_test';
-
- $db = self::newStandaloneInstance( ':memory:' );
- if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
- $cachedResult = 'FTS3';
- }
- $db->close();
-
- return $cachedResult;
- }
-
- /**
- * Attaches external database to our connection, see http://sqlite.org/lang_attach.html
- * for details.
- *
- * @param string $name Database name to be used in queries like
- * SELECT foo FROM dbname.table
- * @param bool|string $file Database file name. If omitted, will be generated
- * using $name and configured data directory
- * @param string $fname Calling function name
- * @return ResultWrapper
- */
- function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
- if ( !$file ) {
- $file = self::generateFileName( $this->dbDir, $name );
- }
- $file = $this->addQuotes( $file );
-
- return $this->query( "ATTACH DATABASE $file AS $name", $fname );
- }
-
- /**
- * @see DatabaseBase::isWriteQuery()
- *
- * @param string $sql
- * @return bool
- */
- function isWriteQuery( $sql ) {
- return parent::isWriteQuery( $sql ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql );
- }
-
- /**
- * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
- *
- * @param string $sql
- * @return bool|ResultWrapper
- */
- protected function doQuery( $sql ) {
- $res = $this->mConn->query( $sql );
- if ( $res === false ) {
- return false;
- } else {
- $r = $res instanceof ResultWrapper ? $res->result : $res;
- $this->mAffectedRows = $r->rowCount();
- $res = new ResultWrapper( $this, $r->fetchAll() );
- }
-
- return $res;
- }
-
- /**
- * @param ResultWrapper|mixed $res
- */
- function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res->result = null;
- } else {
- $res = null;
- }
- }
-
- /**
- * @param ResultWrapper|array $res
- * @return stdClass|bool
- */
- function fetchObject( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $r =& $res->result;
- } else {
- $r =& $res;
- }
-
- $cur = current( $r );
- if ( is_array( $cur ) ) {
- next( $r );
- $obj = new stdClass;
- foreach ( $cur as $k => $v ) {
- if ( !is_numeric( $k ) ) {
- $obj->$k = $v;
- }
- }
-
- return $obj;
- }
-
- return false;
- }
-
- /**
- * @param ResultWrapper|mixed $res
- * @return array|bool
- */
- function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $r =& $res->result;
- } else {
- $r =& $res;
- }
- $cur = current( $r );
- if ( is_array( $cur ) ) {
- next( $r );
-
- return $cur;
- }
-
- return false;
- }
-
- /**
- * The PDO::Statement class implements the array interface so count() will work
- *
- * @param ResultWrapper|array $res
- * @return int
- */
- function numRows( $res ) {
- $r = $res instanceof ResultWrapper ? $res->result : $res;
-
- return count( $r );
- }
-
- /**
- * @param ResultWrapper $res
- * @return int
- */
- function numFields( $res ) {
- $r = $res instanceof ResultWrapper ? $res->result : $res;
- if ( is_array( $r ) && count( $r ) > 0 ) {
- // The size of the result array is twice the number of fields. (Bug: 65578)
- return count( $r[0] ) / 2;
- } else {
- // If the result is empty return 0
- return 0;
- }
- }
-
- /**
- * @param ResultWrapper $res
- * @param int $n
- * @return bool
- */
- function fieldName( $res, $n ) {
- $r = $res instanceof ResultWrapper ? $res->result : $res;
- if ( is_array( $r ) ) {
- $keys = array_keys( $r[0] );
-
- return $keys[$n];
- }
-
- return false;
- }
-
- /**
- * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
- *
- * @param string $name
- * @param string $format
- * @return string
- */
- function tableName( $name, $format = 'quoted' ) {
- // table names starting with sqlite_ are reserved
- if ( strpos( $name, 'sqlite_' ) === 0 ) {
- return $name;
- }
-
- return str_replace( '"', '', parent::tableName( $name, $format ) );
- }
-
- /**
- * Index names have DB scope
- *
- * @param string $index
- * @return string
- */
- protected function indexName( $index ) {
- return $index;
- }
-
- /**
- * This must be called after nextSequenceVal
- *
- * @return int
- */
- function insertId() {
- // PDO::lastInsertId yields a string :(
- return intval( $this->mConn->lastInsertId() );
- }
-
- /**
- * @param ResultWrapper|array $res
- * @param int $row
- */
- function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $r =& $res->result;
- } else {
- $r =& $res;
- }
- reset( $r );
- if ( $row > 0 ) {
- for ( $i = 0; $i < $row; $i++ ) {
- next( $r );
- }
- }
- }
-
- /**
- * @return string
- */
- function lastError() {
- if ( !is_object( $this->mConn ) ) {
- return "Cannot return last error, no db connection";
- }
- $e = $this->mConn->errorInfo();
-
- return isset( $e[2] ) ? $e[2] : '';
- }
-
- /**
- * @return string
- */
- function lastErrno() {
- if ( !is_object( $this->mConn ) ) {
- return "Cannot return last error, no db connection";
- } else {
- $info = $this->mConn->errorInfo();
-
- return $info[1];
- }
- }
-
- /**
- * @return int
- */
- function affectedRows() {
- return $this->mAffectedRows;
- }
-
- /**
- * Returns information about an index
- * Returns false if the index does not exist
- * - if errors are explicitly ignored, returns NULL on failure
- *
- * @param string $table
- * @param string $index
- * @param string $fname
- * @return array
- */
- function indexInfo( $table, $index, $fname = __METHOD__ ) {
- $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
- $res = $this->query( $sql, $fname );
- if ( !$res ) {
- return null;
- }
- if ( $res->numRows() == 0 ) {
- return false;
- }
- $info = [];
- foreach ( $res as $row ) {
- $info[] = $row->name;
- }
-
- return $info;
- }
-
- /**
- * @param string $table
- * @param string $index
- * @param string $fname
- * @return bool|null
- */
- function indexUnique( $table, $index, $fname = __METHOD__ ) {
- $row = $this->selectRow( 'sqlite_master', '*',
- [
- 'type' => 'index',
- 'name' => $this->indexName( $index ),
- ], $fname );
- if ( !$row || !isset( $row->sql ) ) {
- return null;
- }
-
- // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
- $indexPos = strpos( $row->sql, 'INDEX' );
- if ( $indexPos === false ) {
- return null;
- }
- $firstPart = substr( $row->sql, 0, $indexPos );
- $options = explode( ' ', $firstPart );
-
- return in_array( 'UNIQUE', $options );
- }
-
- /**
- * Filter the options used in SELECT statements
- *
- * @param array $options
- * @return array
- */
- function makeSelectOptions( $options ) {
- foreach ( $options as $k => $v ) {
- if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) {
- $options[$k] = '';
- }
- }
-
- return parent::makeSelectOptions( $options );
- }
-
- /**
- * @param array $options
- * @return string
- */
- protected function makeUpdateOptionsArray( $options ) {
- $options = parent::makeUpdateOptionsArray( $options );
- $options = self::fixIgnore( $options );
-
- return $options;
- }
-
- /**
- * @param array $options
- * @return array
- */
- static function fixIgnore( $options ) {
- # SQLite uses OR IGNORE not just IGNORE
- foreach ( $options as $k => $v ) {
- if ( $v == 'IGNORE' ) {
- $options[$k] = 'OR IGNORE';
- }
- }
-
- return $options;
- }
-
- /**
- * @param array $options
- * @return string
- */
- function makeInsertOptions( $options ) {
- $options = self::fixIgnore( $options );
-
- return parent::makeInsertOptions( $options );
- }
-
- /**
- * Based on generic method (parent) with some prior SQLite-sepcific adjustments
- * @param string $table
- * @param array $a
- * @param string $fname
- * @param array $options
- * @return bool
- */
- function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
- if ( !count( $a ) ) {
- return true;
- }
-
- # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
- if ( isset( $a[0] ) && is_array( $a[0] ) ) {
- $ret = true;
- foreach ( $a as $v ) {
- if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) {
- $ret = false;
- }
- }
- } else {
- $ret = parent::insert( $table, $a, "$fname/single-row", $options );
- }
-
- return $ret;
- }
-
- /**
- * @param string $table
- * @param array $uniqueIndexes Unused
- * @param string|array $rows
- * @param string $fname
- * @return bool|ResultWrapper
- */
- function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
- if ( !count( $rows ) ) {
- return true;
- }
-
- # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
- if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
- $ret = true;
- foreach ( $rows as $v ) {
- if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) {
- $ret = false;
- }
- }
- } else {
- $ret = $this->nativeReplace( $table, $rows, "$fname/single-row" );
- }
-
- return $ret;
- }
-
- /**
- * Returns the size of a text field, or -1 for "unlimited"
- * In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though.
- *
- * @param string $table
- * @param string $field
- * @return int
- */
- function textFieldSize( $table, $field ) {
- return -1;
- }
-
- /**
- * @return bool
- */
- function unionSupportsOrderAndLimit() {
- return false;
- }
-
- /**
- * @param string $sqls
- * @param bool $all Whether to "UNION ALL" or not
- * @return string
- */
- function unionQueries( $sqls, $all ) {
- $glue = $all ? ' UNION ALL ' : ' UNION ';
-
- return implode( $glue, $sqls );
- }
-
- /**
- * @return bool
- */
- function wasDeadlock() {
- return $this->lastErrno() == 5; // SQLITE_BUSY
- }
-
- /**
- * @return bool
- */
- function wasErrorReissuable() {
- return $this->lastErrno() == 17; // SQLITE_SCHEMA;
- }
-
- /**
- * @return bool
- */
- function wasReadOnlyError() {
- return $this->lastErrno() == 8; // SQLITE_READONLY;
- }
-
- /**
- * @return string Wikitext of a link to the server software's web site
- */
- public function getSoftwareLink() {
- return "[{{int:version-db-sqlite-url}} SQLite]";
- }
-
- /**
- * @return string Version information from the database
- */
- function getServerVersion() {
- $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
-
- return $ver;
- }
-
- /**
- * @return string User-friendly database information
- */
- public function getServerInfo() {
- return wfMessage( self::getFulltextSearchModule()
- ? 'sqlite-has-fts'
- : 'sqlite-no-fts', $this->getServerVersion() )->text();
- }
-
- /**
- * Get information about a given field
- * Returns false if the field does not exist.
- *
- * @param string $table
- * @param string $field
- * @return SQLiteField|bool False on failure
- */
- function fieldInfo( $table, $field ) {
- $tableName = $this->tableName( $table );
- $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
- $res = $this->query( $sql, __METHOD__ );
- foreach ( $res as $row ) {
- if ( $row->name == $field ) {
- return new SQLiteField( $row, $tableName );
- }
- }
-
- return false;
- }
-
- protected function doBegin( $fname = '' ) {
- if ( $this->trxMode ) {
- $this->query( "BEGIN {$this->trxMode}", $fname );
- } else {
- $this->query( 'BEGIN', $fname );
- }
- $this->mTrxLevel = 1;
- }
-
- /**
- * @param string $s
- * @return string
- */
- function strencode( $s ) {
- return substr( $this->addQuotes( $s ), 1, -1 );
- }
-
- /**
- * @param string $b
- * @return Blob
- */
- function encodeBlob( $b ) {
- return new Blob( $b );
- }
-
- /**
- * @param Blob|string $b
- * @return string
- */
- function decodeBlob( $b ) {
- if ( $b instanceof Blob ) {
- $b = $b->fetch();
- }
-
- return $b;
- }
-
- /**
- * @param Blob|string $s
- * @return string
- */
- function addQuotes( $s ) {
- if ( $s instanceof Blob ) {
- return "x'" . bin2hex( $s->fetch() ) . "'";
- } elseif ( is_bool( $s ) ) {
- return (int)$s;
- } elseif ( strpos( $s, "\0" ) !== false ) {
- // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
- // This is a known limitation of SQLite's mprintf function which PDO
- // should work around, but doesn't. I have reported this to php.net as bug #63419:
- // https://bugs.php.net/bug.php?id=63419
- // There was already a similar report for SQLite3::escapeString, bug #62361:
- // https://bugs.php.net/bug.php?id=62361
- // There is an additional bug regarding sorting this data after insert
- // on older versions of sqlite shipped with ubuntu 12.04
- // https://phabricator.wikimedia.org/T74367
- wfDebugLog(
- __CLASS__,
- __FUNCTION__ .
- ': Quoting value containing null byte. ' .
- 'For consistency all binary data should have been ' .
- 'first processed with self::encodeBlob()'
- );
- return "x'" . bin2hex( $s ) . "'";
- } else {
- return $this->mConn->quote( $s );
- }
- }
-
- /**
- * @return string
- */
- function buildLike() {
- $params = func_get_args();
- if ( count( $params ) > 0 && is_array( $params[0] ) ) {
- $params = $params[0];
- }
-
- return parent::buildLike( $params ) . "ESCAPE '\' ";
- }
-
- /**
- * @return string
- */
- public function getSearchEngine() {
- return "SearchSqlite";
- }
-
- /**
- * No-op version of deadlockLoop
- *
- * @return mixed
- */
- public function deadlockLoop( /*...*/ ) {
- $args = func_get_args();
- $function = array_shift( $args );
-
- return call_user_func_array( $function, $args );
- }
-
- /**
- * @param string $s
- * @return string
- */
- protected function replaceVars( $s ) {
- $s = parent::replaceVars( $s );
- if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
- // CREATE TABLE hacks to allow schema file sharing with MySQL
-
- // binary/varbinary column type -> blob
- $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s );
- // no such thing as unsigned
- $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
- // INT -> INTEGER
- $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s );
- // floating point types -> REAL
- $s = preg_replace(
- '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i',
- 'REAL',
- $s
- );
- // varchar -> TEXT
- $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s );
- // TEXT normalization
- $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
- // BLOB normalization
- $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
- // BOOL -> INTEGER
- $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
- // DATETIME -> TEXT
- $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
- // No ENUM type
- $s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s );
- // binary collation type -> nothing
- $s = preg_replace( '/\bbinary\b/i', '', $s );
- // auto_increment -> autoincrement
- $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
- // No explicit options
- $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s );
- // AUTOINCREMENT should immedidately follow PRIMARY KEY
- $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
- } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
- // No truncated indexes
- $s = preg_replace( '/\(\d+\)/', '', $s );
- // No FULLTEXT
- $s = preg_replace( '/\bfulltext\b/i', '', $s );
- } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) {
- // DROP INDEX is database-wide, not table-specific, so no ON <table> clause.
- $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s );
- } elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) {
- // INSERT IGNORE --> INSERT OR IGNORE
- $s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s );
- }
-
- return $s;
- }
-
- public function lock( $lockName, $method, $timeout = 5 ) {
- if ( !is_dir( "{$this->dbDir}/locks" ) ) { // create dir as needed
- if ( !is_writable( $this->dbDir ) || !mkdir( "{$this->dbDir}/locks" ) ) {
- throw new DBError( "Cannot create directory \"{$this->dbDir}/locks\"." );
- }
- }
-
- return $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout )->isOK();
- }
-
- public function unlock( $lockName, $method ) {
- return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isOK();
- }
-
- /**
- * Build a concatenation list to feed into a SQL query
- *
- * @param string[] $stringList
- * @return string
- */
- function buildConcat( $stringList ) {
- return '(' . implode( ') || (', $stringList ) . ')';
- }
-
- public function buildGroupConcatField(
- $delim, $table, $field, $conds = '', $join_conds = []
- ) {
- $fld = "group_concat($field," . $this->addQuotes( $delim ) . ')';
-
- return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
- }
-
- /**
- * @throws MWException
- * @param string $oldName
- * @param string $newName
- * @param bool $temporary
- * @param string $fname
- * @return bool|ResultWrapper
- */
- function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
- $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" .
- $this->addQuotes( $oldName ) . " AND type='table'", $fname );
- $obj = $this->fetchObject( $res );
- if ( !$obj ) {
- throw new MWException( "Couldn't retrieve structure for table $oldName" );
- }
- $sql = $obj->sql;
- $sql = preg_replace(
- '/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/',
- $this->addIdentifierQuotes( $newName ),
- $sql,
- 1
- );
- if ( $temporary ) {
- if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
- wfDebug( "Table $oldName is virtual, can't create a temporary duplicate.\n" );
- } else {
- $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
- }
- }
-
- $res = $this->query( $sql, $fname );
-
- // Take over indexes
- $indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' );
- foreach ( $indexList as $index ) {
- if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) {
- continue;
- }
-
- if ( $index->unique ) {
- $sql = 'CREATE UNIQUE INDEX';
- } else {
- $sql = 'CREATE INDEX';
- }
- // Try to come up with a new index name, given indexes have database scope in SQLite
- $indexName = $newName . '_' . $index->name;
- $sql .= ' ' . $indexName . ' ON ' . $newName;
-
- $indexInfo = $this->query( 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')' );
- $fields = [];
- foreach ( $indexInfo as $indexInfoRow ) {
- $fields[$indexInfoRow->seqno] = $indexInfoRow->name;
- }
-
- $sql .= '(' . implode( ',', $fields ) . ')';
-
- $this->query( $sql );
- }
-
- return $res;
- }
-
- /**
- * List all tables on the database
- *
- * @param string $prefix Only show tables with this prefix, e.g. mw_
- * @param string $fname Calling function name
- *
- * @return array
- */
- function listTables( $prefix = null, $fname = __METHOD__ ) {
- $result = $this->select(
- 'sqlite_master',
- 'name',
- "type='table'"
- );
-
- $endArray = [];
-
- foreach ( $result as $table ) {
- $vars = get_object_vars( $table );
- $table = array_pop( $vars );
-
- if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
- if ( strpos( $table, 'sqlite_' ) !== 0 ) {
- $endArray[] = $table;
- }
- }
- }
-
- return $endArray;
- }
-
- /**
- * @return string
- */
- public function __toString() {
- return 'SQLite ' . (string)$this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
- }
-
-} // end DatabaseSqlite class
-
-/**
- * @ingroup Database
- */
-class SQLiteField implements Field {
- private $info, $tableName;
-
- function __construct( $info, $tableName ) {
- $this->info = $info;
- $this->tableName = $tableName;
- }
-
- function name() {
- return $this->info->name;
- }
-
- function tableName() {
- return $this->tableName;
- }
-
- function defaultValue() {
- if ( is_string( $this->info->dflt_value ) ) {
- // Typically quoted
- if ( preg_match( '/^\'(.*)\'$', $this->info->dflt_value ) ) {
- return str_replace( "''", "'", $this->info->dflt_value );
- }
- }
-
- return $this->info->dflt_value;
- }
-
- /**
- * @return bool
- */
- function isNullable() {
- return !$this->info->notnull;
- }
-
- function type() {
- return $this->info->type;
- }
-} // end SQLiteField
diff --git a/www/wiki/includes/db/DatabaseUtility.php b/www/wiki/includes/db/DatabaseUtility.php
deleted file mode 100644
index b6c37ee7..00000000
--- a/www/wiki/includes/db/DatabaseUtility.php
+++ /dev/null
@@ -1,347 +0,0 @@
-<?php
-/**
- * This file contains database-related utility classes.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * Utility class
- * @ingroup Database
- *
- * This allows us to distinguish a blob from a normal string and an array of strings
- */
-class Blob {
- /** @var string */
- protected $mData;
-
- function __construct( $data ) {
- $this->mData = $data;
- }
-
- function fetch() {
- return $this->mData;
- }
-}
-
-/**
- * Base for all database-specific classes representing information about database fields
- * @ingroup Database
- */
-interface Field {
- /**
- * Field name
- * @return string
- */
- function name();
-
- /**
- * Name of table this field belongs to
- * @return string
- */
- function tableName();
-
- /**
- * Database type
- * @return string
- */
- function type();
-
- /**
- * Whether this field can store NULL values
- * @return bool
- */
- function isNullable();
-}
-
-/**
- * Result wrapper for grabbing data queried by someone else
- * @ingroup Database
- */
-class ResultWrapper implements Iterator {
- /** @var resource */
- public $result;
-
- /** @var DatabaseBase */
- protected $db;
-
- /** @var int */
- protected $pos = 0;
-
- /** @var object|null */
- protected $currentRow = null;
-
- /**
- * Create a new result object from a result resource and a Database object
- *
- * @param DatabaseBase $database
- * @param resource|ResultWrapper $result
- */
- function __construct( $database, $result ) {
- $this->db = $database;
-
- if ( $result instanceof ResultWrapper ) {
- $this->result = $result->result;
- } else {
- $this->result = $result;
- }
- }
-
- /**
- * Get the number of rows in a result object
- *
- * @return int
- */
- function numRows() {
- return $this->db->numRows( $this );
- }
-
- /**
- * Fetch the next row from the given result object, in object form. Fields can be retrieved with
- * $row->fieldname, with fields acting like member variables. If no more rows are available,
- * false is returned.
- *
- * @return stdClass|bool
- * @throws DBUnexpectedError Thrown if the database returns an error
- */
- function fetchObject() {
- return $this->db->fetchObject( $this );
- }
-
- /**
- * Fetch the next row from the given result object, in associative array form. Fields are
- * retrieved with $row['fieldname']. If no more rows are available, false is returned.
- *
- * @return array|bool
- * @throws DBUnexpectedError Thrown if the database returns an error
- */
- function fetchRow() {
- return $this->db->fetchRow( $this );
- }
-
- /**
- * Free a result object
- */
- function free() {
- $this->db->freeResult( $this );
- unset( $this->result );
- unset( $this->db );
- }
-
- /**
- * Change the position of the cursor in a result object.
- * See mysql_data_seek()
- *
- * @param int $row
- */
- function seek( $row ) {
- $this->db->dataSeek( $this, $row );
- }
-
- /*
- * ======= Iterator functions =======
- * Note that using these in combination with the non-iterator functions
- * above may cause rows to be skipped or repeated.
- */
-
- function rewind() {
- if ( $this->numRows() ) {
- $this->db->dataSeek( $this, 0 );
- }
- $this->pos = 0;
- $this->currentRow = null;
- }
-
- /**
- * @return stdClass|array|bool
- */
- function current() {
- if ( is_null( $this->currentRow ) ) {
- $this->next();
- }
-
- return $this->currentRow;
- }
-
- /**
- * @return int
- */
- function key() {
- return $this->pos;
- }
-
- /**
- * @return stdClass
- */
- function next() {
- $this->pos++;
- $this->currentRow = $this->fetchObject();
-
- return $this->currentRow;
- }
-
- /**
- * @return bool
- */
- function valid() {
- return $this->current() !== false;
- }
-}
-
-/**
- * Overloads the relevant methods of the real ResultsWrapper so it
- * doesn't go anywhere near an actual database.
- */
-class FakeResultWrapper extends ResultWrapper {
- /** @var array */
- public $result = [];
-
- /** @var null And it's going to stay that way :D */
- protected $db = null;
-
- /** @var int */
- protected $pos = 0;
-
- /** @var array|stdClass|bool */
- protected $currentRow = null;
-
- /**
- * @param array $array
- */
- function __construct( $array ) {
- $this->result = $array;
- }
-
- /**
- * @return int
- */
- function numRows() {
- return count( $this->result );
- }
-
- /**
- * @return array|bool
- */
- function fetchRow() {
- if ( $this->pos < count( $this->result ) ) {
- $this->currentRow = $this->result[$this->pos];
- } else {
- $this->currentRow = false;
- }
- $this->pos++;
- if ( is_object( $this->currentRow ) ) {
- return get_object_vars( $this->currentRow );
- } else {
- return $this->currentRow;
- }
- }
-
- function seek( $row ) {
- $this->pos = $row;
- }
-
- function free() {
- }
-
- /**
- * Callers want to be able to access fields with $this->fieldName
- * @return bool|stdClass
- */
- function fetchObject() {
- $this->fetchRow();
- if ( $this->currentRow ) {
- return (object)$this->currentRow;
- } else {
- return false;
- }
- }
-
- function rewind() {
- $this->pos = 0;
- $this->currentRow = null;
- }
-
- /**
- * @return bool|stdClass
- */
- function next() {
- return $this->fetchObject();
- }
-}
-
-/**
- * Used by DatabaseBase::buildLike() to represent characters that have special
- * meaning in SQL LIKE clauses and thus need no escaping. Don't instantiate it
- * manually, use DatabaseBase::anyChar() and anyString() instead.
- */
-class LikeMatch {
- /** @var string */
- private $str;
-
- /**
- * Store a string into a LikeMatch marker object.
- *
- * @param string $s
- */
- public function __construct( $s ) {
- $this->str = $s;
- }
-
- /**
- * Return the original stored string.
- *
- * @return string
- */
- public function toString() {
- return $this->str;
- }
-}
-
-/**
- * An object representing a master or slave position in a replicated setup.
- *
- * The implementation details of this opaque type are up to the database subclass.
- */
-interface DBMasterPos {
- /**
- * @return float UNIX timestamp
- * @since 1.25
- */
- public function asOfTime();
-
- /**
- * @param DBMasterPos $pos
- * @return bool Whether this position is at or higher than $pos
- * @since 1.27
- */
- public function hasReached( DBMasterPos $pos );
-
- /**
- * @param DBMasterPos $pos
- * @return bool Whether this position appears to be for the same channel as another
- * @since 1.27
- */
- public function channelsMatch( DBMasterPos $pos );
-
- /**
- * @return string
- * @since 1.27
- */
- public function __toString();
-}
diff --git a/www/wiki/includes/db/IDatabase.php b/www/wiki/includes/db/IDatabase.php
deleted file mode 100644
index 710efb2c..00000000
--- a/www/wiki/includes/db/IDatabase.php
+++ /dev/null
@@ -1,1596 +0,0 @@
-<?php
-
-/**
- * @defgroup Database Database
- *
- * This file deals with database interface functions
- * and query specifics/optimisations.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * Basic database interface for live and lazy-loaded DB handles
- *
- * @todo: loosen up DB classes from MWException
- * @note: IDatabase and DBConnRef should be updated to reflect any changes
- * @ingroup Database
- */
-interface IDatabase {
- /**
- * A string describing the current software version, and possibly
- * other details in a user-friendly way. Will be listed on Special:Version, etc.
- * Use getServerVersion() to get machine-friendly information.
- *
- * @return string Version information from the database server
- */
- public function getServerInfo();
-
- /**
- * Turns buffering of SQL result sets on (true) or off (false). Default is
- * "on".
- *
- * Unbuffered queries are very troublesome in MySQL:
- *
- * - If another query is executed while the first query is being read
- * out, the first query is killed. This means you can't call normal
- * MediaWiki functions while you are reading an unbuffered query result
- * from a normal wfGetDB() connection.
- *
- * - Unbuffered queries cause the MySQL server to use large amounts of
- * memory and to hold broad locks which block other queries.
- *
- * If you want to limit client-side memory, it's almost always better to
- * split up queries into batches using a LIMIT clause than to switch off
- * buffering.
- *
- * @param null|bool $buffer
- * @return null|bool The previous value of the flag
- */
- public function bufferResults( $buffer = null );
-
- /**
- * Gets the current transaction level.
- *
- * Historically, transactions were allowed to be "nested". This is no
- * longer supported, so this function really only returns a boolean.
- *
- * @return int The previous value
- */
- public function trxLevel();
-
- /**
- * Get the UNIX timestamp of the time that the transaction was established
- *
- * This can be used to reason about the staleness of SELECT data
- * in REPEATABLE-READ transaction isolation level.
- *
- * @return float|null Returns null if there is not active transaction
- * @since 1.25
- */
- public function trxTimestamp();
-
- /**
- * Get/set the table prefix.
- * @param string $prefix The table prefix to set, or omitted to leave it unchanged.
- * @return string The previous table prefix.
- */
- public function tablePrefix( $prefix = null );
-
- /**
- * Get/set the db schema.
- * @param string $schema The database schema to set, or omitted to leave it unchanged.
- * @return string The previous db schema.
- */
- public function dbSchema( $schema = null );
-
- /**
- * Get properties passed down from the server info array of the load
- * balancer.
- *
- * @param string $name The entry of the info array to get, or null to get the
- * whole array
- *
- * @return array|mixed|null
- */
- public function getLBInfo( $name = null );
-
- /**
- * Set the LB info array, or a member of it. If called with one parameter,
- * the LB info array is set to that parameter. If it is called with two
- * parameters, the member with the given name is set to the given value.
- *
- * @param string $name
- * @param array $value
- */
- public function setLBInfo( $name, $value = null );
-
- /**
- * Returns true if this database does an implicit sort when doing GROUP BY
- *
- * @return bool
- */
- public function implicitGroupby();
-
- /**
- * Returns true if this database does an implicit order by when the column has an index
- * For example: SELECT page_title FROM page LIMIT 1
- *
- * @return bool
- */
- public function implicitOrderby();
-
- /**
- * Return the last query that went through IDatabase::query()
- * @return string
- */
- public function lastQuery();
-
- /**
- * Returns true if the connection may have been used for write queries.
- * Should return true if unsure.
- *
- * @return bool
- */
- public function doneWrites();
-
- /**
- * Returns the last time the connection may have been used for write queries.
- * Should return a timestamp if unsure.
- *
- * @return int|float UNIX timestamp or false
- * @since 1.24
- */
- public function lastDoneWrites();
-
- /**
- * @return bool Whether there is a transaction open with possible write queries
- * @since 1.27
- */
- public function writesPending();
-
- /**
- * Returns true if there is a transaction open with possible write
- * queries or transaction pre-commit/idle callbacks waiting on it to finish.
- *
- * @return bool
- */
- public function writesOrCallbacksPending();
-
- /**
- * Get the time spend running write queries for this transaction
- *
- * High times could be due to scanning, updates, locking, and such
- *
- * @return float|bool Returns false if not transaction is active
- * @since 1.26
- */
- public function pendingWriteQueryDuration();
-
- /**
- * Get the list of method names that did write queries for this transaction
- *
- * @return array
- * @since 1.27
- */
- public function pendingWriteCallers();
-
- /**
- * Is a connection to the database open?
- * @return bool
- */
- public function isOpen();
-
- /**
- * Set a flag for this connection
- *
- * @param int $flag DBO_* constants from Defines.php:
- * - DBO_DEBUG: output some debug info (same as debug())
- * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults())
- * - DBO_TRX: automatically start transactions
- * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode
- * and removes it in command line mode
- * - DBO_PERSISTENT: use persistant database connection
- */
- public function setFlag( $flag );
-
- /**
- * Clear a flag for this connection
- *
- * @param int $flag DBO_* constants from Defines.php:
- * - DBO_DEBUG: output some debug info (same as debug())
- * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults())
- * - DBO_TRX: automatically start transactions
- * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode
- * and removes it in command line mode
- * - DBO_PERSISTENT: use persistant database connection
- */
- public function clearFlag( $flag );
-
- /**
- * Returns a boolean whether the flag $flag is set for this connection
- *
- * @param int $flag DBO_* constants from Defines.php:
- * - DBO_DEBUG: output some debug info (same as debug())
- * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults())
- * - DBO_TRX: automatically start transactions
- * - DBO_PERSISTENT: use persistant database connection
- * @return bool
- */
- public function getFlag( $flag );
-
- /**
- * General read-only accessor
- *
- * @param string $name
- * @return string
- */
- public function getProperty( $name );
-
- /**
- * @return string
- */
- public function getWikiID();
-
- /**
- * Get the type of the DBMS, as it appears in $wgDBtype.
- *
- * @return string
- */
- public function getType();
-
- /**
- * Open a connection to the database. Usually aborts on failure
- *
- * @param string $server Database server host
- * @param string $user Database user name
- * @param string $password Database user password
- * @param string $dbName Database name
- * @return bool
- * @throws DBConnectionError
- */
- public function open( $server, $user, $password, $dbName );
-
- /**
- * Fetch the next row from the given result object, in object form.
- * Fields can be retrieved with $row->fieldname, with fields acting like
- * member variables.
- * If no more rows are available, false is returned.
- *
- * @param ResultWrapper|stdClass $res Object as returned from IDatabase::query(), etc.
- * @return stdClass|bool
- * @throws DBUnexpectedError Thrown if the database returns an error
- */
- public function fetchObject( $res );
-
- /**
- * Fetch the next row from the given result object, in associative array
- * form. Fields are retrieved with $row['fieldname'].
- * If no more rows are available, false is returned.
- *
- * @param ResultWrapper $res Result object as returned from IDatabase::query(), etc.
- * @return array|bool
- * @throws DBUnexpectedError Thrown if the database returns an error
- */
- public function fetchRow( $res );
-
- /**
- * Get the number of rows in a result object
- *
- * @param mixed $res A SQL result
- * @return int
- */
- public function numRows( $res );
-
- /**
- * Get the number of fields in a result object
- * @see http://www.php.net/mysql_num_fields
- *
- * @param mixed $res A SQL result
- * @return int
- */
- public function numFields( $res );
-
- /**
- * Get a field name in a result object
- * @see http://www.php.net/mysql_field_name
- *
- * @param mixed $res A SQL result
- * @param int $n
- * @return string
- */
- public function fieldName( $res, $n );
-
- /**
- * Get the inserted value of an auto-increment row
- *
- * The value inserted should be fetched from nextSequenceValue()
- *
- * Example:
- * $id = $dbw->nextSequenceValue( 'page_page_id_seq' );
- * $dbw->insert( 'page', array( 'page_id' => $id ) );
- * $id = $dbw->insertId();
- *
- * @return int
- */
- public function insertId();
-
- /**
- * Change the position of the cursor in a result object
- * @see http://www.php.net/mysql_data_seek
- *
- * @param mixed $res A SQL result
- * @param int $row
- */
- public function dataSeek( $res, $row );
-
- /**
- * Get the last error number
- * @see http://www.php.net/mysql_errno
- *
- * @return int
- */
- public function lastErrno();
-
- /**
- * Get a description of the last error
- * @see http://www.php.net/mysql_error
- *
- * @return string
- */
- public function lastError();
-
- /**
- * mysql_fetch_field() wrapper
- * Returns false if the field doesn't exist
- *
- * @param string $table Table name
- * @param string $field Field name
- *
- * @return Field
- */
- public function fieldInfo( $table, $field );
-
- /**
- * Get the number of rows affected by the last write query
- * @see http://www.php.net/mysql_affected_rows
- *
- * @return int
- */
- public function affectedRows();
-
- /**
- * Returns a wikitext link to the DB's website, e.g.,
- * return "[http://www.mysql.com/ MySQL]";
- * Should at least contain plain text, if for some reason
- * your database has no website.
- *
- * @return string Wikitext of a link to the server software's web site
- */
- public function getSoftwareLink();
-
- /**
- * A string describing the current software version, like from
- * mysql_get_server_info().
- *
- * @return string Version information from the database server.
- */
- public function getServerVersion();
-
- /**
- * Closes a database connection.
- * if it is open : commits any open transactions
- *
- * @throws MWException
- * @return bool Operation success. true if already closed.
- */
- public function close();
-
- /**
- * @param string $error Fallback error message, used if none is given by DB
- * @throws DBConnectionError
- */
- public function reportConnectionError( $error = 'Unknown error' );
-
- /**
- * Run an SQL query and return the result. Normally throws a DBQueryError
- * on failure. If errors are ignored, returns false instead.
- *
- * In new code, the query wrappers select(), insert(), update(), delete(),
- * etc. should be used where possible, since they give much better DBMS
- * independence and automatically quote or validate user input in a variety
- * of contexts. This function is generally only useful for queries which are
- * explicitly DBMS-dependent and are unsupported by the query wrappers, such
- * as CREATE TABLE.
- *
- * However, the query wrappers themselves should call this function.
- *
- * @param string $sql SQL query
- * @param string $fname Name of the calling function, for profiling/SHOW PROCESSLIST
- * comment (you can use __METHOD__ or add some extra info)
- * @param bool $tempIgnore Whether to avoid throwing an exception on errors...
- * maybe best to catch the exception instead?
- * @throws MWException
- * @return bool|ResultWrapper True for a successful write query, ResultWrapper object
- * for a successful read query, or false on failure if $tempIgnore set
- */
- public function query( $sql, $fname = __METHOD__, $tempIgnore = false );
-
- /**
- * Report a query error. Log the error, and if neither the object ignore
- * flag nor the $tempIgnore flag is set, throw a DBQueryError.
- *
- * @param string $error
- * @param int $errno
- * @param string $sql
- * @param string $fname
- * @param bool $tempIgnore
- * @throws DBQueryError
- */
- public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false );
-
- /**
- * Free a result object returned by query() or select(). It's usually not
- * necessary to call this, just use unset() or let the variable holding
- * the result object go out of scope.
- *
- * @param mixed $res A SQL result
- */
- public function freeResult( $res );
-
- /**
- * A SELECT wrapper which returns a single field from a single result row.
- *
- * Usually throws a DBQueryError on failure. If errors are explicitly
- * ignored, returns false on failure.
- *
- * If no result rows are returned from the query, false is returned.
- *
- * @param string|array $table Table name. See IDatabase::select() for details.
- * @param string $var The field name to select. This must be a valid SQL
- * fragment: do not use unvalidated user input.
- * @param string|array $cond The condition array. See IDatabase::select() for details.
- * @param string $fname The function name of the caller.
- * @param string|array $options The query options. See IDatabase::select() for details.
- *
- * @return bool|mixed The value from the field, or false on failure.
- */
- public function selectField(
- $table, $var, $cond = '', $fname = __METHOD__, $options = []
- );
-
- /**
- * A SELECT wrapper which returns a list of single field values from result rows.
- *
- * Usually throws a DBQueryError on failure. If errors are explicitly
- * ignored, returns false on failure.
- *
- * If no result rows are returned from the query, false is returned.
- *
- * @param string|array $table Table name. See IDatabase::select() for details.
- * @param string $var The field name to select. This must be a valid SQL
- * fragment: do not use unvalidated user input.
- * @param string|array $cond The condition array. See IDatabase::select() for details.
- * @param string $fname The function name of the caller.
- * @param string|array $options The query options. See IDatabase::select() for details.
- *
- * @return bool|array The values from the field, or false on failure
- * @since 1.25
- */
- public function selectFieldValues(
- $table, $var, $cond = '', $fname = __METHOD__, $options = []
- );
-
- /**
- * Execute a SELECT query constructed using the various parameters provided.
- * See below for full details of the parameters.
- *
- * @param string|array $table Table name
- * @param string|array $vars Field names
- * @param string|array $conds Conditions
- * @param string $fname Caller function name
- * @param array $options Query options
- * @param array $join_conds Join conditions
- *
- *
- * @param string|array $table
- *
- * May be either an array of table names, or a single string holding a table
- * name. If an array is given, table aliases can be specified, for example:
- *
- * array( 'a' => 'user' )
- *
- * This includes the user table in the query, with the alias "a" available
- * for use in field names (e.g. a.user_name).
- *
- * All of the table names given here are automatically run through
- * DatabaseBase::tableName(), which causes the table prefix (if any) to be
- * added, and various other table name mappings to be performed.
- *
- * Do not use untrusted user input as a table name. Alias names should
- * not have characters outside of the Basic multilingual plane.
- *
- * @param string|array $vars
- *
- * May be either a field name or an array of field names. The field names
- * can be complete fragments of SQL, for direct inclusion into the SELECT
- * query. If an array is given, field aliases can be specified, for example:
- *
- * array( 'maxrev' => 'MAX(rev_id)' )
- *
- * This includes an expression with the alias "maxrev" in the query.
- *
- * If an expression is given, care must be taken to ensure that it is
- * DBMS-independent.
- *
- * Untrusted user input must not be passed to this parameter.
- *
- * @param string|array $conds
- *
- * May be either a string containing a single condition, or an array of
- * conditions. If an array is given, the conditions constructed from each
- * element are combined with AND.
- *
- * Array elements may take one of two forms:
- *
- * - Elements with a numeric key are interpreted as raw SQL fragments.
- * - Elements with a string key are interpreted as equality conditions,
- * where the key is the field name.
- * - If the value of such an array element is a scalar (such as a
- * string), it will be treated as data and thus quoted appropriately.
- * If it is null, an IS NULL clause will be added.
- * - If the value is an array, an IN (...) clause will be constructed
- * from its non-null elements, and an IS NULL clause will be added
- * if null is present, such that the field may match any of the
- * elements in the array. The non-null elements will be quoted.
- *
- * Note that expressions are often DBMS-dependent in their syntax.
- * DBMS-independent wrappers are provided for constructing several types of
- * expression commonly used in condition queries. See:
- * - IDatabase::buildLike()
- * - IDatabase::conditional()
- *
- * Untrusted user input is safe in the values of string keys, however untrusted
- * input must not be used in the array key names or in the values of numeric keys.
- * Escaping of untrusted input used in values of numeric keys should be done via
- * IDatabase::addQuotes()
- *
- * @param string|array $options
- *
- * Optional: Array of query options. Boolean options are specified by
- * including them in the array as a string value with a numeric key, for
- * example:
- *
- * array( 'FOR UPDATE' )
- *
- * The supported options are:
- *
- * - OFFSET: Skip this many rows at the start of the result set. OFFSET
- * with LIMIT can theoretically be used for paging through a result set,
- * but this is discouraged in MediaWiki for performance reasons.
- *
- * - LIMIT: Integer: return at most this many rows. The rows are sorted
- * and then the first rows are taken until the limit is reached. LIMIT
- * is applied to a result set after OFFSET.
- *
- * - FOR UPDATE: Boolean: lock the returned rows so that they can't be
- * changed until the next COMMIT.
- *
- * - DISTINCT: Boolean: return only unique result rows.
- *
- * - GROUP BY: May be either an SQL fragment string naming a field or
- * expression to group by, or an array of such SQL fragments.
- *
- * - HAVING: May be either an string containing a HAVING clause or an array of
- * conditions building the HAVING clause. If an array is given, the conditions
- * constructed from each element are combined with AND.
- *
- * - ORDER BY: May be either an SQL fragment giving a field name or
- * expression to order by, or an array of such SQL fragments.
- *
- * - USE INDEX: This may be either a string giving the index name to use
- * for the query, or an array. If it is an associative array, each key
- * gives the table name (or alias), each value gives the index name to
- * use for that table. All strings are SQL fragments and so should be
- * validated by the caller.
- *
- * - EXPLAIN: In MySQL, this causes an EXPLAIN SELECT query to be run,
- * instead of SELECT.
- *
- * And also the following boolean MySQL extensions, see the MySQL manual
- * for documentation:
- *
- * - LOCK IN SHARE MODE
- * - STRAIGHT_JOIN
- * - HIGH_PRIORITY
- * - SQL_BIG_RESULT
- * - SQL_BUFFER_RESULT
- * - SQL_SMALL_RESULT
- * - SQL_CALC_FOUND_ROWS
- * - SQL_CACHE
- * - SQL_NO_CACHE
- *
- *
- * @param string|array $join_conds
- *
- * Optional associative array of table-specific join conditions. In the
- * most common case, this is unnecessary, since the join condition can be
- * in $conds. However, it is useful for doing a LEFT JOIN.
- *
- * The key of the array contains the table name or alias. The value is an
- * array with two elements, numbered 0 and 1. The first gives the type of
- * join, the second is the same as the $conds parameter. Thus it can be
- * an SQL fragment, or an array where the string keys are equality and the
- * numeric keys are SQL fragments all AND'd together. For example:
- *
- * array( 'page' => array( 'LEFT JOIN', 'page_latest=rev_id' ) )
- *
- * @return ResultWrapper|bool If the query returned no rows, a ResultWrapper
- * with no rows in it will be returned. If there was a query error, a
- * DBQueryError exception will be thrown, except if the "ignore errors"
- * option was set, in which case false will be returned.
- */
- public function select(
- $table, $vars, $conds = '', $fname = __METHOD__,
- $options = [], $join_conds = []
- );
-
- /**
- * The equivalent of IDatabase::select() except that the constructed SQL
- * is returned, instead of being immediately executed. This can be useful for
- * doing UNION queries, where the SQL text of each query is needed. In general,
- * however, callers outside of Database classes should just use select().
- *
- * @param string|array $table Table name
- * @param string|array $vars Field names
- * @param string|array $conds Conditions
- * @param string $fname Caller function name
- * @param string|array $options Query options
- * @param string|array $join_conds Join conditions
- *
- * @return string SQL query string.
- * @see IDatabase::select()
- */
- public function selectSQLText(
- $table, $vars, $conds = '', $fname = __METHOD__,
- $options = [], $join_conds = []
- );
-
- /**
- * Single row SELECT wrapper. Equivalent to IDatabase::select(), except
- * that a single row object is returned. If the query returns no rows,
- * false is returned.
- *
- * @param string|array $table Table name
- * @param string|array $vars Field names
- * @param array $conds Conditions
- * @param string $fname Caller function name
- * @param string|array $options Query options
- * @param array|string $join_conds Join conditions
- *
- * @return stdClass|bool
- */
- public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
- $options = [], $join_conds = []
- );
-
- /**
- * Estimate the number of rows in dataset
- *
- * MySQL allows you to estimate the number of rows that would be returned
- * by a SELECT query, using EXPLAIN SELECT. The estimate is provided using
- * index cardinality statistics, and is notoriously inaccurate, especially
- * when large numbers of rows have recently been added or deleted.
- *
- * For DBMSs that don't support fast result size estimation, this function
- * will actually perform the SELECT COUNT(*).
- *
- * Takes the same arguments as IDatabase::select().
- *
- * @param string $table Table name
- * @param string $vars Unused
- * @param array|string $conds Filters on the table
- * @param string $fname Function name for profiling
- * @param array $options Options for select
- * @return int Row count
- */
- public function estimateRowCount(
- $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
- );
-
- /**
- * Get the number of rows in dataset
- *
- * This is useful when trying to do COUNT(*) but with a LIMIT for performance.
- *
- * Takes the same arguments as IDatabase::select().
- *
- * @since 1.27 Added $join_conds parameter
- *
- * @param array|string $tables Table names
- * @param string $vars Unused
- * @param array|string $conds Filters on the table
- * @param string $fname Function name for profiling
- * @param array $options Options for select
- * @param array $join_conds Join conditions (since 1.27)
- * @return int Row count
- */
- public function selectRowCount(
- $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
- );
-
- /**
- * Determines whether a field exists in a table
- *
- * @param string $table Table name
- * @param string $field Filed to check on that table
- * @param string $fname Calling function name (optional)
- * @return bool Whether $table has filed $field
- */
- public function fieldExists( $table, $field, $fname = __METHOD__ );
-
- /**
- * Determines whether an index exists
- * Usually throws a DBQueryError on failure
- * If errors are explicitly ignored, returns NULL on failure
- *
- * @param string $table
- * @param string $index
- * @param string $fname
- * @return bool|null
- */
- public function indexExists( $table, $index, $fname = __METHOD__ );
-
- /**
- * Query whether a given table exists
- *
- * @param string $table
- * @param string $fname
- * @return bool
- */
- public function tableExists( $table, $fname = __METHOD__ );
-
- /**
- * Determines if a given index is unique
- *
- * @param string $table
- * @param string $index
- *
- * @return bool
- */
- public function indexUnique( $table, $index );
-
- /**
- * INSERT wrapper, inserts an array into a table.
- *
- * $a may be either:
- *
- * - A single associative array. The array keys are the field names, and
- * the values are the values to insert. The values are treated as data
- * and will be quoted appropriately. If NULL is inserted, this will be
- * converted to a database NULL.
- * - An array with numeric keys, holding a list of associative arrays.
- * This causes a multi-row INSERT on DBMSs that support it. The keys in
- * each subarray must be identical to each other, and in the same order.
- *
- * Usually throws a DBQueryError on failure. If errors are explicitly ignored,
- * returns success.
- *
- * $options is an array of options, with boolean options encoded as values
- * with numeric keys, in the same style as $options in
- * IDatabase::select(). Supported options are:
- *
- * - IGNORE: Boolean: if present, duplicate key errors are ignored, and
- * any rows which cause duplicate key errors are not inserted. It's
- * possible to determine how many rows were successfully inserted using
- * IDatabase::affectedRows().
- *
- * @param string $table Table name. This will be passed through
- * DatabaseBase::tableName().
- * @param array $a Array of rows to insert
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @param array $options Array of options
- *
- * @return bool
- */
- public function insert( $table, $a, $fname = __METHOD__, $options = [] );
-
- /**
- * UPDATE wrapper. Takes a condition array and a SET array.
- *
- * @param string $table Name of the table to UPDATE. This will be passed through
- * DatabaseBase::tableName().
- * @param array $values An array of values to SET. For each array element,
- * the key gives the field name, and the value gives the data to set
- * that field to. The data will be quoted by IDatabase::addQuotes().
- * @param array $conds An array of conditions (WHERE). See
- * IDatabase::select() for the details of the format of condition
- * arrays. Use '*' to update all rows.
- * @param string $fname The function name of the caller (from __METHOD__),
- * for logging and profiling.
- * @param array $options An array of UPDATE options, can be:
- * - IGNORE: Ignore unique key conflicts
- * - LOW_PRIORITY: MySQL-specific, see MySQL manual.
- * @return bool
- */
- public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] );
-
- /**
- * Makes an encoded list of strings from an array
- *
- * @param array $a Containing the data
- * @param int $mode Constant
- * - LIST_COMMA: Comma separated, no field names
- * - LIST_AND: ANDed WHERE clause (without the WHERE). See the
- * documentation for $conds in IDatabase::select().
- * - LIST_OR: ORed WHERE clause (without the WHERE)
- * - LIST_SET: Comma separated with field names, like a SET clause
- * - LIST_NAMES: Comma separated field names
- * @throws MWException|DBUnexpectedError
- * @return string
- */
- public function makeList( $a, $mode = LIST_COMMA );
-
- /**
- * Build a partial where clause from a 2-d array such as used for LinkBatch.
- * The keys on each level may be either integers or strings.
- *
- * @param array $data Organized as 2-d
- * array(baseKeyVal => array(subKeyVal => [ignored], ...), ...)
- * @param string $baseKey Field name to match the base-level keys to (eg 'pl_namespace')
- * @param string $subKey Field name to match the sub-level keys to (eg 'pl_title')
- * @return string|bool SQL fragment, or false if no items in array
- */
- public function makeWhereFrom2d( $data, $baseKey, $subKey );
-
- /**
- * @param string $field
- * @return string
- */
- public function bitNot( $field );
-
- /**
- * @param string $fieldLeft
- * @param string $fieldRight
- * @return string
- */
- public function bitAnd( $fieldLeft, $fieldRight );
-
- /**
- * @param string $fieldLeft
- * @param string $fieldRight
- * @return string
- */
- public function bitOr( $fieldLeft, $fieldRight );
-
- /**
- * Build a concatenation list to feed into a SQL query
- * @param array $stringList List of raw SQL expressions; caller is
- * responsible for any quoting
- * @return string
- */
- public function buildConcat( $stringList );
-
- /**
- * Build a GROUP_CONCAT or equivalent statement for a query.
- *
- * This is useful for combining a field for several rows into a single string.
- * NULL values will not appear in the output, duplicated values will appear,
- * and the resulting delimiter-separated values have no defined sort order.
- * Code using the results may need to use the PHP unique() or sort() methods.
- *
- * @param string $delim Glue to bind the results together
- * @param string|array $table Table name
- * @param string $field Field name
- * @param string|array $conds Conditions
- * @param string|array $join_conds Join conditions
- * @return string SQL text
- * @since 1.23
- */
- public function buildGroupConcatField(
- $delim, $table, $field, $conds = '', $join_conds = []
- );
-
- /**
- * Change the current database
- *
- * @param string $db
- * @return bool Success or failure
- */
- public function selectDB( $db );
-
- /**
- * Get the current DB name
- * @return string
- */
- public function getDBname();
-
- /**
- * Get the server hostname or IP address
- * @return string
- */
- public function getServer();
-
- /**
- * Adds quotes and backslashes.
- *
- * @param string|Blob $s
- * @return string
- */
- public function addQuotes( $s );
-
- /**
- * LIKE statement wrapper, receives a variable-length argument list with
- * parts of pattern to match containing either string literals that will be
- * escaped or tokens returned by anyChar() or anyString(). Alternatively,
- * the function could be provided with an array of aforementioned
- * parameters.
- *
- * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns
- * a LIKE clause that searches for subpages of 'My page title'.
- * Alternatively:
- * $pattern = array( 'My_page_title/', $dbr->anyString() );
- * $query .= $dbr->buildLike( $pattern );
- *
- * @since 1.16
- * @return string Fully built LIKE statement
- */
- public function buildLike();
-
- /**
- * Returns a token for buildLike() that denotes a '_' to be used in a LIKE query
- *
- * @return LikeMatch
- */
- public function anyChar();
-
- /**
- * Returns a token for buildLike() that denotes a '%' to be used in a LIKE query
- *
- * @return LikeMatch
- */
- public function anyString();
-
- /**
- * Returns an appropriately quoted sequence value for inserting a new row.
- * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
- * subclass will return an integer, and save the value for insertId()
- *
- * Any implementation of this function should *not* involve reusing
- * sequence numbers created for rolled-back transactions.
- * See http://bugs.mysql.com/bug.php?id=30767 for details.
- * @param string $seqName
- * @return null|int
- */
- public function nextSequenceValue( $seqName );
-
- /**
- * REPLACE query wrapper.
- *
- * REPLACE is a very handy MySQL extension, which functions like an INSERT
- * except that when there is a duplicate key error, the old row is deleted
- * and the new row is inserted in its place.
- *
- * We simulate this with standard SQL with a DELETE followed by INSERT. To
- * perform the delete, we need to know what the unique indexes are so that
- * we know how to find the conflicting rows.
- *
- * It may be more efficient to leave off unique indexes which are unlikely
- * to collide. However if you do this, you run the risk of encountering
- * errors which wouldn't have occurred in MySQL.
- *
- * @param string $table The table to replace the row(s) in.
- * @param array $uniqueIndexes Is an array of indexes. Each element may be either
- * a field name or an array of field names
- * @param array $rows Can be either a single row to insert, or multiple rows,
- * in the same format as for IDatabase::insert()
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- */
- public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ );
-
- /**
- * INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table.
- *
- * This updates any conflicting rows (according to the unique indexes) using
- * the provided SET clause and inserts any remaining (non-conflicted) rows.
- *
- * $rows may be either:
- * - A single associative array. The array keys are the field names, and
- * the values are the values to insert. The values are treated as data
- * and will be quoted appropriately. If NULL is inserted, this will be
- * converted to a database NULL.
- * - An array with numeric keys, holding a list of associative arrays.
- * This causes a multi-row INSERT on DBMSs that support it. The keys in
- * each subarray must be identical to each other, and in the same order.
- *
- * It may be more efficient to leave off unique indexes which are unlikely
- * to collide. However if you do this, you run the risk of encountering
- * errors which wouldn't have occurred in MySQL.
- *
- * Usually throws a DBQueryError on failure. If errors are explicitly ignored,
- * returns success.
- *
- * @since 1.22
- *
- * @param string $table Table name. This will be passed through DatabaseBase::tableName().
- * @param array $rows A single row or list of rows to insert
- * @param array $uniqueIndexes List of single field names or field name tuples
- * @param array $set An array of values to SET. For each array element, the
- * key gives the field name, and the value gives the data to set that
- * field to. The data will be quoted by IDatabase::addQuotes().
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @throws Exception
- * @return bool
- */
- public function upsert(
- $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__
- );
-
- /**
- * DELETE where the condition is a join.
- *
- * MySQL overrides this to use a multi-table DELETE syntax, in other databases
- * we use sub-selects
- *
- * For safety, an empty $conds will not delete everything. If you want to
- * delete all rows where the join condition matches, set $conds='*'.
- *
- * DO NOT put the join condition in $conds.
- *
- * @param string $delTable The table to delete from.
- * @param string $joinTable The other table.
- * @param string $delVar The variable to join on, in the first table.
- * @param string $joinVar The variable to join on, in the second table.
- * @param array $conds Condition array of field names mapped to variables,
- * ANDed together in the WHERE clause
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @throws DBUnexpectedError
- */
- public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
- $fname = __METHOD__
- );
-
- /**
- * DELETE query wrapper.
- *
- * @param array $table Table name
- * @param string|array $conds Array of conditions. See $conds in IDatabase::select()
- * for the format. Use $conds == "*" to delete all rows
- * @param string $fname Name of the calling function
- * @throws DBUnexpectedError
- * @return bool|ResultWrapper
- */
- public function delete( $table, $conds, $fname = __METHOD__ );
-
- /**
- * INSERT SELECT wrapper. Takes data from a SELECT query and inserts it
- * into another table.
- *
- * @param string $destTable The table name to insert into
- * @param string|array $srcTable May be either a table name, or an array of table names
- * to include in a join.
- *
- * @param array $varMap Must be an associative array of the form
- * array( 'dest1' => 'source1', ...). Source items may be literals
- * rather than field names, but strings should be quoted with
- * IDatabase::addQuotes()
- *
- * @param array $conds Condition array. See $conds in IDatabase::select() for
- * the details of the format of condition arrays. May be "*" to copy the
- * whole table.
- *
- * @param string $fname The function name of the caller, from __METHOD__
- *
- * @param array $insertOptions Options for the INSERT part of the query, see
- * IDatabase::insert() for details.
- * @param array $selectOptions Options for the SELECT part of the query, see
- * IDatabase::select() for details.
- *
- * @return ResultWrapper
- */
- public function insertSelect( $destTable, $srcTable, $varMap, $conds,
- $fname = __METHOD__,
- $insertOptions = [], $selectOptions = []
- );
-
- /**
- * Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries
- * within the UNION construct.
- * @return bool
- */
- public function unionSupportsOrderAndLimit();
-
- /**
- * Construct a UNION query
- * This is used for providing overload point for other DB abstractions
- * not compatible with the MySQL syntax.
- * @param array $sqls SQL statements to combine
- * @param bool $all Use UNION ALL
- * @return string SQL fragment
- */
- public function unionQueries( $sqls, $all );
-
- /**
- * Returns an SQL expression for a simple conditional. This doesn't need
- * to be overridden unless CASE isn't supported in your DBMS.
- *
- * @param string|array $cond SQL expression which will result in a boolean value
- * @param string $trueVal SQL expression to return if true
- * @param string $falseVal SQL expression to return if false
- * @return string SQL fragment
- */
- public function conditional( $cond, $trueVal, $falseVal );
-
- /**
- * Returns a comand for str_replace function in SQL query.
- * Uses REPLACE() in MySQL
- *
- * @param string $orig Column to modify
- * @param string $old Column to seek
- * @param string $new Column to replace with
- *
- * @return string
- */
- public function strreplace( $orig, $old, $new );
-
- /**
- * Determines how long the server has been up
- * STUB
- *
- * @return int
- */
- public function getServerUptime();
-
- /**
- * Determines if the last failure was due to a deadlock
- * STUB
- *
- * @return bool
- */
- public function wasDeadlock();
-
- /**
- * Determines if the last failure was due to a lock timeout
- * STUB
- *
- * @return bool
- */
- public function wasLockTimeout();
-
- /**
- * Determines if the last query error was something that should be dealt
- * with by pinging the connection and reissuing the query.
- * STUB
- *
- * @return bool
- */
- public function wasErrorReissuable();
-
- /**
- * Determines if the last failure was due to the database being read-only.
- * STUB
- *
- * @return bool
- */
- public function wasReadOnlyError();
-
- /**
- * Wait for the slave to catch up to a given master position
- *
- * @param DBMasterPos $pos
- * @param int $timeout The maximum number of seconds to wait for synchronisation
- * @return int|null Zero if the slave was past that position already,
- * greater than zero if we waited for some period of time, less than
- * zero if it timed out, and null on error
- */
- public function masterPosWait( DBMasterPos $pos, $timeout );
-
- /**
- * Get the replication position of this slave
- *
- * @return DBMasterPos|bool False if this is not a slave.
- */
- public function getSlavePos();
-
- /**
- * Get the position of this master
- *
- * @return DBMasterPos|bool False if this is not a master
- */
- public function getMasterPos();
-
- /**
- * Run an anonymous function as soon as there is no transaction pending.
- * If there is a transaction and it is rolled back, then the callback is cancelled.
- * Queries in the function will run in AUTO-COMMIT mode unless there are begin() calls.
- * Callbacks must commit any transactions that they begin.
- *
- * This is useful for updates to different systems or when separate transactions are needed.
- * For example, one might want to enqueue jobs into a system outside the database, but only
- * after the database is updated so that the jobs will see the data when they actually run.
- * It can also be used for updates that easily cause deadlocks if locks are held too long.
- *
- * Updates will execute in the order they were enqueued.
- *
- * @param callable $callback
- * @since 1.20
- */
- public function onTransactionIdle( $callback );
-
- /**
- * Run an anonymous function before the current transaction commits or now if there is none.
- * If there is a transaction and it is rolled back, then the callback is cancelled.
- * Callbacks must not start nor commit any transactions.
- *
- * This is useful for updates that easily cause deadlocks if locks are held too long
- * but where atomicity is strongly desired for these updates and some related updates.
- *
- * Updates will execute in the order they were enqueued.
- *
- * @param callable $callback
- * @since 1.22
- */
- public function onTransactionPreCommitOrIdle( $callback );
-
- /**
- * Begin an atomic section of statements
- *
- * If a transaction has been started already, just keep track of the given
- * section name to make sure the transaction is not committed pre-maturely.
- * This function can be used in layers (with sub-sections), so use a stack
- * to keep track of the different atomic sections. If there is no transaction,
- * start one implicitly.
- *
- * The goal of this function is to create an atomic section of SQL queries
- * without having to start a new transaction if it already exists.
- *
- * Atomic sections are more strict than transactions. With transactions,
- * attempting to begin a new transaction when one is already running results
- * in MediaWiki issuing a brief warning and doing an implicit commit. All
- * atomic levels *must* be explicitly closed using IDatabase::endAtomic(),
- * and any database transactions cannot be began or committed until all atomic
- * levels are closed. There is no such thing as implicitly opening or closing
- * an atomic section.
- *
- * @since 1.23
- * @param string $fname
- * @throws DBError
- */
- public function startAtomic( $fname = __METHOD__ );
-
- /**
- * Ends an atomic section of SQL statements
- *
- * Ends the next section of atomic SQL statements and commits the transaction
- * if necessary.
- *
- * @since 1.23
- * @see IDatabase::startAtomic
- * @param string $fname
- * @throws DBError
- */
- public function endAtomic( $fname = __METHOD__ );
-
- /**
- * Run a callback to do an atomic set of updates for this database
- *
- * The $callback takes the following arguments:
- * - This database object
- * - The value of $fname
- *
- * If any exception occurs in the callback, then rollback() will be called and the error will
- * be re-thrown. It may also be that the rollback itself fails with an exception before then.
- * In any case, such errors are expected to terminate the request, without any outside caller
- * attempting to catch errors and commit anyway. Note that any rollback undoes all prior
- * atomic section and uncommitted updates, which trashes the current request, requiring an
- * error to be displayed.
- *
- * This can be an alternative to explicit startAtomic()/endAtomic() calls.
- *
- * @see DatabaseBase::startAtomic
- * @see DatabaseBase::endAtomic
- *
- * @param string $fname Caller name (usually __METHOD__)
- * @param callable $callback Callback that issues DB updates
- * @throws DBError
- * @throws RuntimeException
- * @throws UnexpectedValueException
- * @since 1.27
- */
- public function doAtomicSection( $fname, $callback );
-
- /**
- * Begin a transaction. If a transaction is already in progress,
- * that transaction will be committed before the new transaction is started.
- *
- * Note that when the DBO_TRX flag is set (which is usually the case for web
- * requests, but not for maintenance scripts), any previous database query
- * will have started a transaction automatically.
- *
- * Nesting of transactions is not supported. Attempts to nest transactions
- * will cause a warning, unless the current transaction was started
- * automatically because of the DBO_TRX flag.
- *
- * @param string $fname
- * @throws DBError
- */
- public function begin( $fname = __METHOD__ );
-
- /**
- * Commits a transaction previously started using begin().
- * If no transaction is in progress, a warning is issued.
- *
- * Nesting of transactions is not supported.
- *
- * @param string $fname
- * @param string $flush Flush flag, set to 'flush' to disable warnings about
- * explicitly committing implicit transactions, or calling commit when no
- * transaction is in progress.
- *
- * This will trigger an exception if there is an ongoing explicit transaction.
- *
- * Only set the flush flag if you are sure that these warnings are not applicable,
- * and no explicit transactions are open.
- *
- * @throws DBUnexpectedError
- */
- public function commit( $fname = __METHOD__, $flush = '' );
-
- /**
- * Rollback a transaction previously started using begin().
- * If no transaction is in progress, a warning is issued.
- *
- * No-op on non-transactional databases.
- *
- * @param string $fname
- * @param string $flush Flush flag, set to 'flush' to disable warnings about
- * calling rollback when no transaction is in progress. This will silently
- * break any ongoing explicit transaction. Only set the flush flag if you
- * are sure that it is safe to ignore these warnings in your context.
- * @throws DBUnexpectedError
- * @since 1.23 Added $flush parameter
- */
- public function rollback( $fname = __METHOD__, $flush = '' );
-
- /**
- * List all tables on the database
- *
- * @param string $prefix Only show tables with this prefix, e.g. mw_
- * @param string $fname Calling function name
- * @throws MWException
- * @return array
- */
- public function listTables( $prefix = null, $fname = __METHOD__ );
-
- /**
- * Convert a timestamp in one of the formats accepted by wfTimestamp()
- * to the format used for inserting into timestamp fields in this DBMS.
- *
- * The result is unquoted, and needs to be passed through addQuotes()
- * before it can be included in raw SQL.
- *
- * @param string|int $ts
- *
- * @return string
- */
- public function timestamp( $ts = 0 );
-
- /**
- * Convert a timestamp in one of the formats accepted by wfTimestamp()
- * to the format used for inserting into timestamp fields in this DBMS. If
- * NULL is input, it is passed through, allowing NULL values to be inserted
- * into timestamp fields.
- *
- * The result is unquoted, and needs to be passed through addQuotes()
- * before it can be included in raw SQL.
- *
- * @param string|int $ts
- *
- * @return string
- */
- public function timestampOrNull( $ts = null );
-
- /**
- * Ping the server and try to reconnect if it there is no connection
- *
- * @return bool Success or failure
- */
- public function ping();
-
- /**
- * Get slave lag. Currently supported only by MySQL.
- *
- * Note that this function will generate a fatal error on many
- * installations. Most callers should use LoadBalancer::safeGetLag()
- * instead.
- *
- * @return int|bool Database replication lag in seconds or false on error
- */
- public function getLag();
-
- /**
- * Get the slave lag when the current transaction started
- * or a general lag estimate if not transaction is active
- *
- * This is useful when transactions might use snapshot isolation
- * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
- * is this lag plus transaction duration. If they don't, it is still
- * safe to be pessimistic. In AUTO-COMMIT mode, this still gives an
- * indication of the staleness of subsequent reads.
- *
- * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
- * @since 1.27
- */
- public function getSessionLagStatus();
-
- /**
- * Return the maximum number of items allowed in a list, or 0 for unlimited.
- *
- * @return int
- */
- public function maxListLen();
-
- /**
- * Some DBMSs have a special format for inserting into blob fields, they
- * don't allow simple quoted strings to be inserted. To insert into such
- * a field, pass the data through this function before passing it to
- * IDatabase::insert().
- *
- * @param string $b
- * @return string
- */
- public function encodeBlob( $b );
-
- /**
- * Some DBMSs return a special placeholder object representing blob fields
- * in result objects. Pass the object through this function to return the
- * original string.
- *
- * @param string|Blob $b
- * @return string
- */
- public function decodeBlob( $b );
-
- /**
- * Override database's default behavior. $options include:
- * 'connTimeout' : Set the connection timeout value in seconds.
- * May be useful for very long batch queries such as
- * full-wiki dumps, where a single query reads out over
- * hours or days.
- *
- * @param array $options
- * @return void
- */
- public function setSessionOptions( array $options );
-
- /**
- * Set variables to be used in sourceFile/sourceStream, in preference to the
- * ones in $GLOBALS. If an array is set here, $GLOBALS will not be used at
- * all. If it's set to false, $GLOBALS will be used.
- *
- * @param bool|array $vars Mapping variable name to value.
- */
- public function setSchemaVars( $vars );
-
- /**
- * Check to see if a named lock is available (non-blocking)
- *
- * @param string $lockName Name of lock to poll
- * @param string $method Name of method calling us
- * @return bool
- * @since 1.20
- */
- public function lockIsFree( $lockName, $method );
-
- /**
- * Acquire a named lock
- *
- * Named locks are not related to transactions
- *
- * @param string $lockName Name of lock to aquire
- * @param string $method Name of the calling method
- * @param int $timeout Acquisition timeout in seconds
- * @return bool
- */
- public function lock( $lockName, $method, $timeout = 5 );
-
- /**
- * Release a lock
- *
- * Named locks are not related to transactions
- *
- * @param string $lockName Name of lock to release
- * @param string $method Name of the calling method
- *
- * @return int Returns 1 if the lock was released, 0 if the lock was not established
- * by this thread (in which case the lock is not released), and NULL if the named
- * lock did not exist
- */
- public function unlock( $lockName, $method );
-
- /**
- * Acquire a named lock, flush any transaction, and return an RAII style unlocker object
- *
- * This is suitiable for transactions that need to be serialized using cooperative locks,
- * where each transaction can see each others' changes. Any transaction is flushed to clear
- * out stale REPEATABLE-READ snapshot data. Once the returned object falls out of PHP scope,
- * any transaction will be committed and the lock will be released.
- *
- * If the lock acquisition failed, then no transaction flush happens, and null is returned.
- *
- * @param string $lockKey Name of lock to release
- * @param string $fname Name of the calling method
- * @param int $timeout Acquisition timeout in seconds
- * @return ScopedCallback|null
- * @throws DBUnexpectedError
- * @since 1.27
- */
- public function getScopedLockAndFlush( $lockKey, $fname, $timeout );
-
- /**
- * Check to see if a named lock used by lock() use blocking queues
- *
- * @return bool
- * @since 1.26
- */
- public function namedLocksEnqueue();
-
- /**
- * Find out when 'infinity' is. Most DBMSes support this. This is a special
- * keyword for timestamps in PostgreSQL, and works with CHAR(14) as well
- * because "i" sorts after all numbers.
- *
- * @return string
- */
- public function getInfinity();
-
- /**
- * Encode an expiry time into the DBMS dependent format
- *
- * @param string $expiry Timestamp for expiry, or the 'infinity' string
- * @return string
- */
- public function encodeExpiry( $expiry );
-
- /**
- * Decode an expiry time into a DBMS independent format
- *
- * @param string $expiry DB timestamp field value for expiry
- * @param int $format TS_* constant, defaults to TS_MW
- * @return string
- */
- public function decodeExpiry( $expiry, $format = TS_MW );
-
- /**
- * Allow or deny "big selects" for this session only. This is done by setting
- * the sql_big_selects session variable.
- *
- * This is a MySQL-specific feature.
- *
- * @param bool|string $value True for allow, false for deny, or "default" to
- * restore the initial value
- */
- public function setBigSelects( $value = true );
-
- /**
- * @return bool Whether this DB is read-only
- * @since 1.27
- */
- public function isReadOnly();
-}
diff --git a/www/wiki/includes/db/MWLBFactory.php b/www/wiki/includes/db/MWLBFactory.php
index 5196ac2d..79f787db 100644
--- a/www/wiki/includes/db/MWLBFactory.php
+++ b/www/wiki/includes/db/MWLBFactory.php
@@ -23,6 +23,7 @@
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\DatabaseDomain;
/**
@@ -30,6 +31,10 @@ use Wikimedia\Rdbms\DatabaseDomain;
* @ingroup Database
*/
abstract class MWLBFactory {
+
+ /** @var array Cache of already-logged deprecation messages */
+ private static $loggedDeprecations = [];
+
/**
* @param array $lbConf Config for LBFactory::__construct()
* @param Config $mainConfig Main config object from MediaWikiServices
@@ -46,7 +51,7 @@ abstract class MWLBFactory {
$lbConf += [
'localDomain' => new DatabaseDomain(
$mainConfig->get( 'DBname' ),
- null,
+ $mainConfig->get( 'DBmwschema' ),
$mainConfig->get( 'DBprefix' )
),
'profiler' => Profiler::instance(),
@@ -56,6 +61,7 @@ abstract class MWLBFactory {
'connLogger' => LoggerFactory::getInstance( 'DBConnection' ),
'perfLogger' => LoggerFactory::getInstance( 'DBPerformance' ),
'errorLogger' => [ MWExceptionHandler::class, 'logException' ],
+ 'deprecationLogger' => [ static::class, 'logDeprecation' ],
'cliMode' => $wgCommandLineMode,
'hostname' => wfHostname(),
'readOnlyReason' => $readOnlyMode->getReason(),
@@ -64,7 +70,7 @@ abstract class MWLBFactory {
// When making changes here, remember to also specify MediaWiki-specific options
// for Database classes in the relevant Installer subclass.
// Such as MysqlInstaller::openConnection and PostgresInstaller::openConnectionWithParams.
- if ( $lbConf['class'] === 'LBFactorySimple' ) {
+ if ( $lbConf['class'] === Wikimedia\Rdbms\LBFactorySimple::class ) {
if ( isset( $lbConf['servers'] ) ) {
// Server array is already explicitly configured; leave alone
} elseif ( is_array( $mainConfig->get( 'DBservers' ) ) ) {
@@ -132,7 +138,7 @@ abstract class MWLBFactory {
if ( !isset( $lbConf['externalClusters'] ) ) {
$lbConf['externalClusters'] = $mainConfig->get( 'ExternalServers' );
}
- } elseif ( $lbConf['class'] === 'LBFactoryMulti' ) {
+ } elseif ( $lbConf['class'] === Wikimedia\Rdbms\LBFactoryMulti::class ) {
if ( isset( $lbConf['serverTemplate'] ) ) {
if ( in_array( $lbConf['serverTemplate']['type'], $typesWithSchema, true ) ) {
$lbConf['serverTemplate']['schema'] = $mainConfig->get( 'DBmwschema' );
@@ -142,16 +148,18 @@ abstract class MWLBFactory {
}
}
+ $services = MediaWikiServices::getInstance();
+
// Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804)
- $sCache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
+ $sCache = $services->getLocalServerObjectCache();
if ( $sCache->getQoS( $sCache::ATTR_EMULATION ) > $sCache::QOS_EMULATION_SQL ) {
$lbConf['srvCache'] = $sCache;
}
- $cCache = ObjectCache::getLocalClusterInstance();
- if ( $cCache->getQoS( $cCache::ATTR_EMULATION ) > $cCache::QOS_EMULATION_SQL ) {
- $lbConf['memStash'] = $cCache;
+ $mStash = $services->getMainObjectStash();
+ if ( $mStash->getQoS( $mStash::ATTR_EMULATION ) > $mStash::QOS_EMULATION_SQL ) {
+ $lbConf['memStash'] = $mStash;
}
- $wCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+ $wCache = $services->getMainWANObjectCache();
if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) {
$lbConf['wanCache'] = $wCache;
}
@@ -199,4 +207,46 @@ abstract class MWLBFactory {
return $class;
}
+
+ public static function setSchemaAliases( LBFactory $lbFactory, Config $config ) {
+ if ( $config->get( 'DBtype' ) === 'mysql' ) {
+ /**
+ * When SQLite indexes were introduced in r45764, it was noted that
+ * SQLite requires index names to be unique within the whole database,
+ * not just within a schema. As discussed in CR r45819, to avoid the
+ * need for a schema change on existing installations, the indexes
+ * were implicitly mapped from the new names to the old names.
+ *
+ * This mapping can be removed if DB patches are introduced to alter
+ * the relevant tables in existing installations. Note that because
+ * this index mapping applies to table creation, even new installations
+ * of MySQL have the old names (except for installations created during
+ * a period where this mapping was inappropriately removed, see
+ * T154872).
+ */
+ $lbFactory->setIndexAliases( [
+ 'ar_usertext_timestamp' => 'usertext_timestamp',
+ 'un_user_id' => 'user_id',
+ 'un_user_ip' => 'user_ip',
+ ] );
+ }
+ }
+
+ /**
+ * Log a database deprecation warning
+ * @param string $msg Deprecation message
+ */
+ public static function logDeprecation( $msg ) {
+ global $wgDevelopmentWarnings;
+
+ if ( isset( self::$loggedDeprecations[$msg] ) ) {
+ return;
+ }
+ self::$loggedDeprecations[$msg] = true;
+
+ if ( $wgDevelopmentWarnings ) {
+ trigger_error( $msg, E_USER_DEPRECATED );
+ }
+ wfDebugLog( 'deprecated', $msg, 'private' );
+ }
}
diff --git a/www/wiki/includes/db/loadbalancer/LBFactory.php b/www/wiki/includes/db/loadbalancer/LBFactory.php
deleted file mode 100644
index f39596b7..00000000
--- a/www/wiki/includes/db/loadbalancer/LBFactory.php
+++ /dev/null
@@ -1,481 +0,0 @@
-<?php
-/**
- * Generator of database load balancing objects.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-use Psr\Log\LoggerInterface;
-use MediaWiki\Logger\LoggerFactory;
-
-/**
- * An interface for generating database load balancers
- * @ingroup Database
- */
-abstract class LBFactory {
- /** @var ChronologyProtector */
- protected $chronProt;
-
- /** @var TransactionProfiler */
- protected $trxProfiler;
-
- /** @var LoggerInterface */
- protected $logger;
-
- /** @var LBFactory */
- private static $instance;
-
- /** @var string|bool Reason all LBs are read-only or false if not */
- protected $readOnlyReason = false;
-
- const SHUTDOWN_NO_CHRONPROT = 1; // don't save ChronologyProtector positions (for async code)
-
- /**
- * Construct a factory based on a configuration array (typically from $wgLBFactoryConf)
- * @param array $conf
- */
- public function __construct( array $conf ) {
- if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
- $this->readOnlyReason = $conf['readOnlyReason'];
- }
-
- $this->chronProt = $this->newChronologyProtector();
- $this->trxProfiler = Profiler::instance()->getTransactionProfiler();
- $this->logger = LoggerFactory::getInstance( 'DBTransaction' );
- }
-
- /**
- * Disables all access to the load balancer, will cause all database access
- * to throw a DBAccessError
- */
- public static function disableBackend() {
- global $wgLBFactoryConf;
- self::$instance = new LBFactoryFake( $wgLBFactoryConf );
- }
-
- /**
- * Get an LBFactory instance
- *
- * @return LBFactory
- */
- public static function singleton() {
- global $wgLBFactoryConf;
-
- if ( is_null( self::$instance ) ) {
- $class = self::getLBFactoryClass( $wgLBFactoryConf );
- $config = $wgLBFactoryConf;
- if ( !isset( $config['readOnlyReason'] ) ) {
- $config['readOnlyReason'] = wfConfiguredReadOnlyReason();
- }
- self::$instance = new $class( $config );
- }
-
- return self::$instance;
- }
-
- /**
- * Returns the LBFactory class to use and the load balancer configuration.
- *
- * @param array $config (e.g. $wgLBFactoryConf)
- * @return string Class name
- */
- public static function getLBFactoryClass( array $config ) {
- // For configuration backward compatibility after removing
- // underscores from class names in MediaWiki 1.23.
- $bcClasses = [
- 'LBFactory_Simple' => 'LBFactorySimple',
- 'LBFactory_Single' => 'LBFactorySingle',
- 'LBFactory_Multi' => 'LBFactoryMulti',
- 'LBFactory_Fake' => 'LBFactoryFake',
- ];
-
- $class = $config['class'];
-
- if ( isset( $bcClasses[$class] ) ) {
- $class = $bcClasses[$class];
- wfDeprecated(
- '$wgLBFactoryConf must be updated. See RELEASE-NOTES for details',
- '1.23'
- );
- }
-
- return $class;
- }
-
- /**
- * Shut down, close connections and destroy the cached instance.
- */
- public static function destroyInstance() {
- if ( self::$instance ) {
- self::$instance->shutdown();
- self::$instance->forEachLBCallMethod( 'closeAll' );
- self::$instance = null;
- }
- }
-
- /**
- * Set the instance to be the given object
- *
- * @param LBFactory $instance
- */
- public static function setInstance( $instance ) {
- self::destroyInstance();
- self::$instance = $instance;
- }
-
- /**
- * Create a new load balancer object. The resulting object will be untracked,
- * not chronology-protected, and the caller is responsible for cleaning it up.
- *
- * @param bool|string $wiki Wiki ID, or false for the current wiki
- * @return LoadBalancer
- */
- abstract public function newMainLB( $wiki = false );
-
- /**
- * Get a cached (tracked) load balancer object.
- *
- * @param bool|string $wiki Wiki ID, or false for the current wiki
- * @return LoadBalancer
- */
- abstract public function getMainLB( $wiki = false );
-
- /**
- * Create a new load balancer for external storage. The resulting object will be
- * untracked, not chronology-protected, and the caller is responsible for
- * cleaning it up.
- *
- * @param string $cluster External storage cluster, or false for core
- * @param bool|string $wiki Wiki ID, or false for the current wiki
- * @return LoadBalancer
- */
- abstract protected function newExternalLB( $cluster, $wiki = false );
-
- /**
- * Get a cached (tracked) load balancer for external storage
- *
- * @param string $cluster External storage cluster, or false for core
- * @param bool|string $wiki Wiki ID, or false for the current wiki
- * @return LoadBalancer
- */
- abstract public function &getExternalLB( $cluster, $wiki = false );
-
- /**
- * Execute a function for each tracked load balancer
- * The callback is called with the load balancer as the first parameter,
- * and $params passed as the subsequent parameters.
- *
- * @param callable $callback
- * @param array $params
- */
- abstract public function forEachLB( $callback, array $params = [] );
-
- /**
- * Prepare all tracked load balancers for shutdown
- * @param integer $flags Supports SHUTDOWN_* flags
- * STUB
- */
- public function shutdown( $flags = 0 ) {
- }
-
- /**
- * Call a method of each tracked load balancer
- *
- * @param string $methodName
- * @param array $args
- */
- private function forEachLBCallMethod( $methodName, array $args = [] ) {
- $this->forEachLB(
- function ( LoadBalancer $loadBalancer, $methodName, array $args ) {
- call_user_func_array( [ $loadBalancer, $methodName ], $args );
- },
- [ $methodName, $args ]
- );
- }
-
- /**
- * Commit on all connections. Done for two reasons:
- * 1. To commit changes to the masters.
- * 2. To release the snapshot on all connections, master and slave.
- * @param string $fname Caller name
- */
- public function commitAll( $fname = __METHOD__ ) {
- $this->logMultiDbTransaction();
-
- $start = microtime( true );
- $this->forEachLBCallMethod( 'commitAll', [ $fname ] );
- $timeMs = 1000 * ( microtime( true ) - $start );
-
- RequestContext::getMain()->getStats()->timing( "db.commit-all", $timeMs );
- }
-
- /**
- * Commit changes on all master connections
- * @param string $fname Caller name
- * @param array $options Options map:
- * - maxWriteDuration: abort if more than this much time was spent in write queries
- */
- public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) {
- $limit = isset( $options['maxWriteDuration'] ) ? $options['maxWriteDuration'] : 0;
-
- $this->logMultiDbTransaction();
- $this->forEachLB( function ( LoadBalancer $lb ) use ( $limit ) {
- $lb->forEachOpenConnection( function ( IDatabase $db ) use ( $limit ) {
- $time = $db->pendingWriteQueryDuration();
- if ( $limit > 0 && $time > $limit ) {
- throw new DBTransactionError(
- $db,
- wfMessage( 'transaction-duration-limit-exceeded', $time, $limit )->text()
- );
- }
- } );
- } );
-
- $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
- }
-
- /**
- * Rollback changes on all master connections
- * @param string $fname Caller name
- * @since 1.23
- */
- public function rollbackMasterChanges( $fname = __METHOD__ ) {
- $this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] );
- }
-
- /**
- * Log query info if multi DB transactions are going to be committed now
- */
- private function logMultiDbTransaction() {
- $callersByDB = [];
- $this->forEachLB( function ( LoadBalancer $lb ) use ( &$callersByDB ) {
- $masterName = $lb->getServerName( $lb->getWriterIndex() );
- $callers = $lb->pendingMasterChangeCallers();
- if ( $callers ) {
- $callersByDB[$masterName] = $callers;
- }
- } );
-
- if ( count( $callersByDB ) >= 2 ) {
- $dbs = implode( ', ', array_keys( $callersByDB ) );
- $msg = "Multi-DB transaction [{$dbs}]:\n";
- foreach ( $callersByDB as $db => $callers ) {
- $msg .= "$db: " . implode( '; ', $callers ) . "\n";
- }
- $this->logger->info( $msg );
- }
- }
-
- /**
- * Determine if any master connection has pending changes
- * @return bool
- * @since 1.23
- */
- public function hasMasterChanges() {
- $ret = false;
- $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
- $ret = $ret || $lb->hasMasterChanges();
- } );
-
- return $ret;
- }
-
- /**
- * Detemine if any lagged slave connection was used
- * @since 1.27
- * @return bool
- */
- public function laggedSlaveUsed() {
- $ret = false;
- $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
- $ret = $ret || $lb->laggedSlaveUsed();
- } );
-
- return $ret;
- }
-
- /**
- * Determine if any master connection has pending/written changes from this request
- * @return bool
- * @since 1.27
- */
- public function hasOrMadeRecentMasterChanges() {
- $ret = false;
- $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
- $ret = $ret || $lb->hasOrMadeRecentMasterChanges();
- } );
- return $ret;
- }
-
- /**
- * Waits for the slave DBs to catch up to the current master position
- *
- * Use this when updating very large numbers of rows, as in maintenance scripts,
- * to avoid causing too much lag. Of course, this is a no-op if there are no slaves.
- *
- * By default this waits on all DB clusters actually used in this request.
- * This makes sense when lag being waiting on is caused by the code that does this check.
- * In that case, setting "ifWritesSince" can avoid the overhead of waiting for clusters
- * that were not changed since the last wait check. To forcefully wait on a specific cluster
- * for a given wiki, use the 'wiki' parameter. To forcefully wait on an "external" cluster,
- * use the "cluster" parameter.
- *
- * Never call this function after a large DB write that is *still* in a transaction.
- * It only makes sense to call this after the possible lag inducing changes were committed.
- *
- * @param array $opts Optional fields that include:
- * - wiki : wait on the load balancer DBs that handles the given wiki
- * - cluster : wait on the given external load balancer DBs
- * - timeout : Max wait time. Default: ~60 seconds
- * - ifWritesSince: Only wait if writes were done since this UNIX timestamp
- * @throws DBReplicationWaitError If a timeout or error occured waiting on a DB cluster
- * @since 1.27
- */
- public function waitForReplication( array $opts = [] ) {
- $opts += [
- 'wiki' => false,
- 'cluster' => false,
- 'timeout' => 60,
- 'ifWritesSince' => null
- ];
-
- // Figure out which clusters need to be checked
- /** @var LoadBalancer[] $lbs */
- $lbs = [];
- if ( $opts['cluster'] !== false ) {
- $lbs[] = $this->getExternalLB( $opts['cluster'] );
- } elseif ( $opts['wiki'] !== false ) {
- $lbs[] = $this->getMainLB( $opts['wiki'] );
- } else {
- $this->forEachLB( function ( LoadBalancer $lb ) use ( &$lbs ) {
- $lbs[] = $lb;
- } );
- if ( !$lbs ) {
- return; // nothing actually used
- }
- }
-
- // Get all the master positions of applicable DBs right now.
- // This can be faster since waiting on one cluster reduces the
- // time needed to wait on the next clusters.
- $masterPositions = array_fill( 0, count( $lbs ), false );
- foreach ( $lbs as $i => $lb ) {
- 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 ( $opts['ifWritesSince']
- && $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince']
- ) {
- continue; // no writes since the last wait
- }
- $masterPositions[$i] = $lb->getMasterPos();
- }
-
- $failed = [];
- foreach ( $lbs as $i => $lb ) {
- if ( $masterPositions[$i] ) {
- // The DBMS may not support getMasterPos() or the whole
- // load balancer might be fake (e.g. $wgAllDBsAreLocalhost).
- if ( !$lb->waitForAll( $masterPositions[$i], $opts['timeout'] ) ) {
- $failed[] = $lb->getServerName( $lb->getWriterIndex() );
- }
- }
- }
-
- if ( $failed ) {
- throw new DBReplicationWaitError(
- "Could not wait for slaves to catch up to " .
- implode( ', ', $failed )
- );
- }
- }
-
- /**
- * Disable the ChronologyProtector for all load balancers
- *
- * This can be called at the start of special API entry points
- *
- * @since 1.27
- */
- public function disableChronologyProtection() {
- $this->chronProt->setEnabled( false );
- }
-
- /**
- * @return ChronologyProtector
- */
- protected function newChronologyProtector() {
- $request = RequestContext::getMain()->getRequest();
- $chronProt = new ChronologyProtector(
- ObjectCache::getMainStashInstance(),
- [
- 'ip' => $request->getIP(),
- 'agent' => $request->getHeader( 'User-Agent' )
- ]
- );
- if ( PHP_SAPI === 'cli' ) {
- $chronProt->setEnabled( false );
- } elseif ( $request->getHeader( 'ChronologyProtection' ) === 'false' ) {
- // Request opted out of using position wait logic. This is useful for requests
- // done by the job queue or background ETL that do not have a meaningful session.
- $chronProt->setWaitEnabled( false );
- }
-
- return $chronProt;
- }
-
- /**
- * @param ChronologyProtector $cp
- */
- protected function shutdownChronologyProtector( ChronologyProtector $cp ) {
- // Get all the master positions needed
- $this->forEachLB( function ( LoadBalancer $lb ) use ( $cp ) {
- $cp->shutdownLB( $lb );
- } );
- // Write them to the stash
- $unsavedPositions = $cp->shutdown();
- // If the positions failed to write to the stash, at least wait on local datacenter
- // slaves to catch up before responding. Even if there are several DCs, this increases
- // the chance that the user will see their own changes immediately afterwards. As long
- // as the sticky DC cookie applies (same domain), this is not even an issue.
- $this->forEachLB( function ( LoadBalancer $lb ) use ( $unsavedPositions ) {
- $masterName = $lb->getServerName( $lb->getWriterIndex() );
- if ( isset( $unsavedPositions[$masterName] ) ) {
- $lb->waitForAll( $unsavedPositions[$masterName] );
- }
- } );
- }
-}
-
-/**
- * Exception class for attempted DB access
- */
-class DBAccessError extends MWException {
- public function __construct() {
- parent::__construct( "Mediawiki tried to access the database via wfGetDB(). " .
- "This is not allowed." );
- }
-}
-
-/**
- * Exception class for replica DB wait timeouts
- */
-class DBReplicationWaitError extends Exception {
-}
diff --git a/www/wiki/includes/db/loadbalancer/LBFactoryFake.php b/www/wiki/includes/db/loadbalancer/LBFactoryFake.php
deleted file mode 100644
index 33ee2504..00000000
--- a/www/wiki/includes/db/loadbalancer/LBFactoryFake.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-/**
- * Generator of database load balancing objects.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * LBFactory class that throws an error on any attempt to use it.
- * This will typically be done via wfGetDB().
- * Call LBFactory::disableBackend() to start using this, and
- * LBFactory::enableBackend() to return to normal behavior
- */
-class LBFactoryFake extends LBFactory {
- public function newMainLB( $wiki = false ) {
- throw new DBAccessError;
- }
-
- public function getMainLB( $wiki = false ) {
- throw new DBAccessError;
- }
-
- protected function newExternalLB( $cluster, $wiki = false ) {
- throw new DBAccessError;
- }
-
- public function &getExternalLB( $cluster, $wiki = false ) {
- throw new DBAccessError;
- }
-
- public function forEachLB( $callback, array $params = [] ) {
- }
-}
diff --git a/www/wiki/includes/db/loadbalancer/LBFactoryMulti.php b/www/wiki/includes/db/loadbalancer/LBFactoryMulti.php
deleted file mode 100644
index 3a543acc..00000000
--- a/www/wiki/includes/db/loadbalancer/LBFactoryMulti.php
+++ /dev/null
@@ -1,424 +0,0 @@
-<?php
-/**
- * Advanced generator of database load balancing objects for wiki farms.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * A multi-wiki, multi-master factory for Wikimedia and similar installations.
- * Ignores the old configuration globals.
- *
- * Template override precedence (highest => lowest):
- * - templateOverridesByServer
- * - masterTemplateOverrides
- * - templateOverridesBySection/templateOverridesByCluster
- * - externalTemplateOverrides
- * - serverTemplate
- * Overrides only work on top level keys (so nested values will not be merged).
- *
- * Configuration:
- * sectionsByDB A map of database names to section names.
- *
- * sectionLoads A 2-d map. For each section, gives a map of server names to
- * load ratios. For example:
- * array(
- * 'section1' => array(
- * 'db1' => 100,
- * 'db2' => 100
- * )
- * )
- *
- * serverTemplate A server info associative array as documented for $wgDBservers.
- * The host, hostName and load entries will be overridden.
- *
- * groupLoadsBySection A 3-d map giving server load ratios for each section and group.
- * For example:
- * array(
- * 'section1' => array(
- * 'group1' => array(
- * 'db1' => 100,
- * 'db2' => 100
- * )
- * )
- * )
- *
- * groupLoadsByDB A 3-d map giving server load ratios by DB name.
- *
- * hostsByName A map of hostname to IP address.
- *
- * externalLoads A map of external storage cluster name to server load map.
- *
- * externalTemplateOverrides A set of server info keys overriding serverTemplate for external
- * storage.
- *
- * templateOverridesByServer A 2-d map overriding serverTemplate and
- * externalTemplateOverrides on a server-by-server basis. Applies
- * to both core and external storage.
- * templateOverridesBySection A 2-d map overriding the server info by section.
- * templateOverridesByCluster A 2-d map overriding the server info by external storage cluster.
- *
- * masterTemplateOverrides An override array for all master servers.
- *
- * loadMonitorClass Name of the LoadMonitor class to always use.
- *
- * readOnlyBySection A map of section name to read-only message.
- * Missing or false for read/write.
- *
- * @ingroup Database
- */
-class LBFactoryMulti extends LBFactory {
- /** @var array A map of database names to section names */
- private $sectionsByDB;
-
- /**
- * @var array A 2-d map. For each section, gives a map of server names to
- * load ratios
- */
- private $sectionLoads;
-
- /**
- * @var array A server info associative array as documented for
- * $wgDBservers. The host, hostName and load entries will be
- * overridden
- */
- private $serverTemplate;
-
- // Optional settings
-
- /** @var array A 3-d map giving server load ratios for each section and group */
- private $groupLoadsBySection = [];
-
- /** @var array A 3-d map giving server load ratios by DB name */
- private $groupLoadsByDB = [];
-
- /** @var array A map of hostname to IP address */
- private $hostsByName = [];
-
- /** @var array A map of external storage cluster name to server load map */
- private $externalLoads = [];
-
- /**
- * @var array A set of server info keys overriding serverTemplate for
- * external storage
- */
- private $externalTemplateOverrides;
-
- /**
- * @var array A 2-d map overriding serverTemplate and
- * externalTemplateOverrides on a server-by-server basis. Applies to both
- * core and external storage
- */
- private $templateOverridesByServer;
-
- /** @var array A 2-d map overriding the server info by section */
- private $templateOverridesBySection;
-
- /** @var array A 2-d map overriding the server info by external storage cluster */
- private $templateOverridesByCluster;
-
- /** @var array An override array for all master servers */
- private $masterTemplateOverrides;
-
- /**
- * @var array|bool A map of section name to read-only message. Missing or
- * false for read/write
- */
- private $readOnlyBySection = [];
-
- // Other stuff
-
- /** @var array Load balancer factory configuration */
- private $conf;
-
- /** @var LoadBalancer[] */
- private $mainLBs = [];
-
- /** @var LoadBalancer[] */
- private $extLBs = [];
-
- /** @var string */
- private $loadMonitorClass;
-
- /** @var string */
- private $lastWiki;
-
- /** @var string */
- private $lastSection;
-
- /**
- * @param array $conf
- * @throws MWException
- */
- public function __construct( array $conf ) {
- parent::__construct( $conf );
-
- $this->conf = $conf;
- $required = [ 'sectionsByDB', 'sectionLoads', 'serverTemplate' ];
- $optional = [ 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
- 'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
- 'templateOverridesByCluster', 'templateOverridesBySection', 'masterTemplateOverrides',
- 'readOnlyBySection', 'loadMonitorClass' ];
-
- foreach ( $required as $key ) {
- if ( !isset( $conf[$key] ) ) {
- throw new MWException( __CLASS__ . ": $key is required in configuration" );
- }
- $this->$key = $conf[$key];
- }
-
- foreach ( $optional as $key ) {
- if ( isset( $conf[$key] ) ) {
- $this->$key = $conf[$key];
- }
- }
- }
-
- /**
- * @param bool|string $wiki
- * @return string
- */
- private function getSectionForWiki( $wiki = false ) {
- if ( $this->lastWiki === $wiki ) {
- return $this->lastSection;
- }
- list( $dbName, ) = $this->getDBNameAndPrefix( $wiki );
- if ( isset( $this->sectionsByDB[$dbName] ) ) {
- $section = $this->sectionsByDB[$dbName];
- } else {
- $section = 'DEFAULT';
- }
- $this->lastSection = $section;
- $this->lastWiki = $wiki;
-
- return $section;
- }
-
- /**
- * @param bool|string $wiki
- * @return LoadBalancer
- */
- public function newMainLB( $wiki = false ) {
- list( $dbName, ) = $this->getDBNameAndPrefix( $wiki );
- $section = $this->getSectionForWiki( $wiki );
- if ( isset( $this->groupLoadsByDB[$dbName] ) ) {
- $groupLoads = $this->groupLoadsByDB[$dbName];
- } else {
- $groupLoads = [];
- }
-
- if ( isset( $this->groupLoadsBySection[$section] ) ) {
- $groupLoads = array_merge_recursive( $groupLoads, $this->groupLoadsBySection[$section] );
- }
-
- $readOnlyReason = $this->readOnlyReason;
- // Use the LB-specific read-only reason if everything isn't already read-only
- if ( $readOnlyReason === false && isset( $this->readOnlyBySection[$section] ) ) {
- $readOnlyReason = $this->readOnlyBySection[$section];
- }
-
- $template = $this->serverTemplate;
- if ( isset( $this->templateOverridesBySection[$section] ) ) {
- $template = $this->templateOverridesBySection[$section] + $template;
- }
-
- return $this->newLoadBalancer(
- $template,
- $this->sectionLoads[$section],
- $groupLoads,
- $readOnlyReason
- );
- }
-
- /**
- * @param bool|string $wiki
- * @return LoadBalancer
- */
- public function getMainLB( $wiki = false ) {
- $section = $this->getSectionForWiki( $wiki );
- if ( !isset( $this->mainLBs[$section] ) ) {
- $lb = $this->newMainLB( $wiki );
- $lb->parentInfo( [ 'id' => "main-$section" ] );
- $this->chronProt->initLB( $lb );
- $this->mainLBs[$section] = $lb;
- }
-
- return $this->mainLBs[$section];
- }
-
- /**
- * @param string $cluster
- * @param bool|string $wiki
- * @throws MWException
- * @return LoadBalancer
- */
- protected function newExternalLB( $cluster, $wiki = false ) {
- if ( !isset( $this->externalLoads[$cluster] ) ) {
- throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
- }
- $template = $this->serverTemplate;
- if ( isset( $this->externalTemplateOverrides ) ) {
- $template = $this->externalTemplateOverrides + $template;
- }
- if ( isset( $this->templateOverridesByCluster[$cluster] ) ) {
- $template = $this->templateOverridesByCluster[$cluster] + $template;
- }
-
- return $this->newLoadBalancer(
- $template,
- $this->externalLoads[$cluster],
- [],
- $this->readOnlyReason
- );
- }
-
- /**
- * @param string $cluster External storage cluster, or false for core
- * @param bool|string $wiki Wiki ID, or false for the current wiki
- * @return LoadBalancer
- */
- public function &getExternalLB( $cluster, $wiki = false ) {
- if ( !isset( $this->extLBs[$cluster] ) ) {
- $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
- $this->extLBs[$cluster]->parentInfo( [ 'id' => "ext-$cluster" ] );
- $this->chronProt->initLB( $this->extLBs[$cluster] );
- }
-
- return $this->extLBs[$cluster];
- }
-
- /**
- * Make a new load balancer object based on template and load array
- *
- * @param array $template
- * @param array $loads
- * @param array $groupLoads
- * @param string|bool $readOnlyReason
- * @return LoadBalancer
- */
- private function newLoadBalancer( $template, $loads, $groupLoads, $readOnlyReason ) {
- return new LoadBalancer( [
- 'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
- 'loadMonitor' => $this->loadMonitorClass,
- 'readOnlyReason' => $readOnlyReason,
- 'trxProfiler' => $this->trxProfiler
- ] );
- }
-
- /**
- * Make a server array as expected by LoadBalancer::__construct, using a template and load array
- *
- * @param array $template
- * @param array $loads
- * @param array $groupLoads
- * @return array
- */
- private function makeServerArray( $template, $loads, $groupLoads ) {
- $servers = [];
- $master = true;
- $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads );
- foreach ( $groupLoadsByServer as $server => $stuff ) {
- if ( !isset( $loads[$server] ) ) {
- $loads[$server] = 0;
- }
- }
- foreach ( $loads as $serverName => $load ) {
- $serverInfo = $template;
- if ( $master ) {
- $serverInfo['master'] = true;
- if ( isset( $this->masterTemplateOverrides ) ) {
- $serverInfo = $this->masterTemplateOverrides + $serverInfo;
- }
- $master = false;
- } else {
- $serverInfo['slave'] = true;
- }
- if ( isset( $this->templateOverridesByServer[$serverName] ) ) {
- $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo;
- }
- if ( isset( $groupLoadsByServer[$serverName] ) ) {
- $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
- }
- if ( isset( $this->hostsByName[$serverName] ) ) {
- $serverInfo['host'] = $this->hostsByName[$serverName];
- } else {
- $serverInfo['host'] = $serverName;
- }
- $serverInfo['hostName'] = $serverName;
- $serverInfo['load'] = $load;
- $servers[] = $serverInfo;
- }
-
- return $servers;
- }
-
- /**
- * Take a group load array indexed by group then server, and reindex it by server then group
- * @param array $groupLoads
- * @return array
- */
- private function reindexGroupLoads( $groupLoads ) {
- $reindexed = [];
- foreach ( $groupLoads as $group => $loads ) {
- foreach ( $loads as $server => $load ) {
- $reindexed[$server][$group] = $load;
- }
- }
-
- return $reindexed;
- }
-
- /**
- * Get the database name and prefix based on the wiki ID
- * @param bool|string $wiki
- * @return array
- */
- private function getDBNameAndPrefix( $wiki = false ) {
- if ( $wiki === false ) {
- global $wgDBname, $wgDBprefix;
-
- return [ $wgDBname, $wgDBprefix ];
- } else {
- return wfSplitWikiID( $wiki );
- }
- }
-
- /**
- * Execute a function for each tracked load balancer
- * The callback is called with the load balancer as the first parameter,
- * and $params passed as the subsequent parameters.
- * @param callable $callback
- * @param array $params
- */
- public function forEachLB( $callback, array $params = [] ) {
- foreach ( $this->mainLBs as $lb ) {
- call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
- }
- foreach ( $this->extLBs as $lb ) {
- call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
- }
- }
-
- public function shutdown( $flags = 0 ) {
- if ( !( $flags & self::SHUTDOWN_NO_CHRONPROT ) ) {
- $this->shutdownChronologyProtector( $this->chronProt );
- }
- $this->commitMasterChanges( __METHOD__ ); // sanity
- }
-}
diff --git a/www/wiki/includes/db/loadbalancer/LBFactorySimple.php b/www/wiki/includes/db/loadbalancer/LBFactorySimple.php
deleted file mode 100644
index 1b0a1f3f..00000000
--- a/www/wiki/includes/db/loadbalancer/LBFactorySimple.php
+++ /dev/null
@@ -1,167 +0,0 @@
-<?php
-/**
- * Generator of database load balancing objects.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * A simple single-master LBFactory that gets its configuration from the b/c globals
- */
-class LBFactorySimple extends LBFactory {
- /** @var LoadBalancer */
- private $mainLB;
- /** @var LoadBalancer[] */
- private $extLBs = [];
-
- /** @var string */
- private $loadMonitorClass;
-
- public function __construct( array $conf ) {
- parent::__construct( $conf );
-
- $this->loadMonitorClass = isset( $conf['loadMonitorClass'] )
- ? $conf['loadMonitorClass']
- : null;
- }
-
- /**
- * @param bool|string $wiki
- * @return LoadBalancer
- */
- public function newMainLB( $wiki = false ) {
- global $wgDBservers;
-
- if ( is_array( $wgDBservers ) ) {
- $servers = $wgDBservers;
- foreach ( $servers as $i => &$server ) {
- if ( $i == 0 ) {
- $server['master'] = true;
- } else {
- $server['slave'] = true;
- }
- }
- } else {
- global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
- global $wgDBssl, $wgDBcompress;
-
- $flags = DBO_DEFAULT;
- if ( $wgDebugDumpSql ) {
- $flags |= DBO_DEBUG;
- }
- if ( $wgDBssl ) {
- $flags |= DBO_SSL;
- }
- if ( $wgDBcompress ) {
- $flags |= DBO_COMPRESS;
- }
-
- $servers = [ [
- 'host' => $wgDBserver,
- 'user' => $wgDBuser,
- 'password' => $wgDBpassword,
- 'dbname' => $wgDBname,
- 'type' => $wgDBtype,
- 'load' => 1,
- 'flags' => $flags,
- 'master' => true
- ] ];
- }
-
- return new LoadBalancer( [
- 'servers' => $servers,
- 'loadMonitor' => $this->loadMonitorClass,
- 'readOnlyReason' => $this->readOnlyReason,
- 'trxProfiler' => $this->trxProfiler
- ] );
- }
-
- /**
- * @param bool|string $wiki
- * @return LoadBalancer
- */
- public function getMainLB( $wiki = false ) {
- if ( !isset( $this->mainLB ) ) {
- $this->mainLB = $this->newMainLB( $wiki );
- $this->mainLB->parentInfo( [ 'id' => 'main' ] );
- $this->chronProt->initLB( $this->mainLB );
- }
-
- return $this->mainLB;
- }
-
- /**
- * @throws MWException
- * @param string $cluster
- * @param bool|string $wiki
- * @return LoadBalancer
- */
- protected function newExternalLB( $cluster, $wiki = false ) {
- global $wgExternalServers;
- if ( !isset( $wgExternalServers[$cluster] ) ) {
- throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
- }
-
- return new LoadBalancer( [
- 'servers' => $wgExternalServers[$cluster],
- 'loadMonitor' => $this->loadMonitorClass,
- 'readOnlyReason' => $this->readOnlyReason,
- 'trxProfiler' => $this->trxProfiler
- ] );
- }
-
- /**
- * @param string $cluster
- * @param bool|string $wiki
- * @return array
- */
- public function &getExternalLB( $cluster, $wiki = false ) {
- if ( !isset( $this->extLBs[$cluster] ) ) {
- $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
- $this->extLBs[$cluster]->parentInfo( [ 'id' => "ext-$cluster" ] );
- $this->chronProt->initLB( $this->extLBs[$cluster] );
- }
-
- return $this->extLBs[$cluster];
- }
-
- /**
- * Execute a function for each tracked load balancer
- * The callback is called with the load balancer as the first parameter,
- * and $params passed as the subsequent parameters.
- *
- * @param callable $callback
- * @param array $params
- */
- public function forEachLB( $callback, array $params = [] ) {
- if ( isset( $this->mainLB ) ) {
- call_user_func_array( $callback, array_merge( [ $this->mainLB ], $params ) );
- }
- foreach ( $this->extLBs as $lb ) {
- call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
- }
- }
-
- public function shutdown( $flags = 0 ) {
- if ( !( $flags & self::SHUTDOWN_NO_CHRONPROT ) ) {
- $this->shutdownChronologyProtector( $this->chronProt );
- }
- $this->commitMasterChanges( __METHOD__ ); // sanity
- }
-}
diff --git a/www/wiki/includes/db/loadbalancer/LBFactorySingle.php b/www/wiki/includes/db/loadbalancer/LBFactorySingle.php
deleted file mode 100644
index 79ca3a70..00000000
--- a/www/wiki/includes/db/loadbalancer/LBFactorySingle.php
+++ /dev/null
@@ -1,127 +0,0 @@
-<?php
-/**
- * Simple generator of database connections that always returns the same object.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * An LBFactory class that always returns a single database object.
- */
-class LBFactorySingle extends LBFactory {
- /** @var LoadBalancerSingle */
- private $lb;
-
- /**
- * @param array $conf An associative array with one member:
- * - connection: The DatabaseBase connection object
- */
- public function __construct( array $conf ) {
- parent::__construct( $conf );
-
- $this->lb = new LoadBalancerSingle( [
- 'readOnlyReason' => $this->readOnlyReason,
- 'trxProfiler' => $this->trxProfiler
- ] + $conf );
- }
-
- /**
- * @param bool|string $wiki
- * @return LoadBalancerSingle
- */
- public function newMainLB( $wiki = false ) {
- return $this->lb;
- }
-
- /**
- * @param bool|string $wiki
- * @return LoadBalancerSingle
- */
- public function getMainLB( $wiki = false ) {
- return $this->lb;
- }
-
- /**
- * @param string $cluster External storage cluster, or false for core
- * @param bool|string $wiki Wiki ID, or false for the current wiki
- * @return LoadBalancerSingle
- */
- protected function newExternalLB( $cluster, $wiki = false ) {
- return $this->lb;
- }
-
- /**
- * @param string $cluster External storage cluster, or false for core
- * @param bool|string $wiki Wiki ID, or false for the current wiki
- * @return LoadBalancerSingle
- */
- public function &getExternalLB( $cluster, $wiki = false ) {
- return $this->lb;
- }
-
- /**
- * @param string|callable $callback
- * @param array $params
- */
- public function forEachLB( $callback, array $params = [] ) {
- call_user_func_array( $callback, array_merge( [ $this->lb ], $params ) );
- }
-}
-
-/**
- * Helper class for LBFactorySingle.
- */
-class LoadBalancerSingle extends LoadBalancer {
- /** @var DatabaseBase */
- private $db;
-
- /**
- * @param array $params
- */
- public function __construct( array $params ) {
- $this->db = $params['connection'];
-
- parent::__construct( [
- 'servers' => [
- [
- 'type' => $this->db->getType(),
- 'host' => $this->db->getServer(),
- 'dbname' => $this->db->getDBname(),
- 'load' => 1,
- ]
- ],
- 'trxProfiler' => $this->trxProfiler
- ] );
-
- if ( isset( $params['readOnlyReason'] ) ) {
- $this->db->setLBInfo( 'readOnlyReason', $params['readOnlyReason'] );
- }
- }
-
- /**
- *
- * @param string $server
- * @param bool $dbNameOverride
- *
- * @return DatabaseBase
- */
- protected function reallyOpenConnection( $server, $dbNameOverride = false ) {
- return $this->db;
- }
-}
diff --git a/www/wiki/includes/db/loadbalancer/LoadBalancer.php b/www/wiki/includes/db/loadbalancer/LoadBalancer.php
deleted file mode 100644
index 741999c5..00000000
--- a/www/wiki/includes/db/loadbalancer/LoadBalancer.php
+++ /dev/null
@@ -1,1407 +0,0 @@
-<?php
-/**
- * Database load balancing.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * Database load balancing object
- *
- * @todo document
- * @ingroup Database
- */
-class LoadBalancer {
- /** @var array[] Map of (server index => server config array) */
- private $mServers;
- /** @var array[] Map of (local/foreignUsed/foreignFree => server index => DatabaseBase array) */
- private $mConns;
- /** @var array Map of (server index => weight) */
- private $mLoads;
- /** @var array[] Map of (group => server index => weight) */
- private $mGroupLoads;
- /** @var bool Whether to disregard slave lag as a factor in slave selection */
- private $mAllowLagged;
- /** @var integer Seconds to spend waiting on slave lag to resolve */
- private $mWaitTimeout;
- /** @var array LBFactory information */
- private $mParentInfo;
-
- /** @var string The LoadMonitor subclass name */
- private $mLoadMonitorClass;
- /** @var LoadMonitor */
- private $mLoadMonitor;
- /** @var BagOStuff */
- private $srvCache;
-
- /** @var bool|DatabaseBase Database connection that caused a problem */
- private $mErrorConnection;
- /** @var integer The generic (not query grouped) slave index (of $mServers) */
- private $mReadIndex;
- /** @var bool|DBMasterPos False if not set */
- private $mWaitForPos;
- /** @var bool Whether the generic reader fell back to a lagged slave */
- private $laggedSlaveMode = false;
- /** @var bool Whether the generic reader fell back to a lagged slave */
- private $slavesDownMode = false;
- /** @var string The last DB selection or connection error */
- private $mLastError = 'Unknown error';
- /** @var string|bool Reason the LB is read-only or false if not */
- private $readOnlyReason = false;
- /** @var integer Total connections opened */
- private $connsOpened = 0;
-
- /** @var TransactionProfiler */
- protected $trxProfiler;
-
- /** @var integer Warn when this many connection are held */
- const CONN_HELD_WARN_THRESHOLD = 10;
- /** @var integer Default 'max lag' when unspecified */
- const MAX_LAG = 10;
- /** @var integer Max time to wait for a slave to catch up (e.g. ChronologyProtector) */
- const POS_WAIT_TIMEOUT = 10;
-
- /**
- * @param array $params Array with keys:
- * - servers : Required. Array of server info structures.
- * - loadMonitor : Name of a class used to fetch server lag and load.
- * - readOnlyReason : Reason the master DB is read-only if so [optional]
- * @throws MWException
- */
- public function __construct( array $params ) {
- if ( !isset( $params['servers'] ) ) {
- throw new MWException( __CLASS__ . ': missing servers parameter' );
- }
- $this->mServers = $params['servers'];
- $this->mWaitTimeout = self::POS_WAIT_TIMEOUT;
-
- $this->mReadIndex = -1;
- $this->mWriteIndex = -1;
- $this->mConns = [
- 'local' => [],
- 'foreignUsed' => [],
- 'foreignFree' => [] ];
- $this->mLoads = [];
- $this->mWaitForPos = false;
- $this->mErrorConnection = false;
- $this->mAllowLagged = false;
-
- if ( isset( $params['readOnlyReason'] ) && is_string( $params['readOnlyReason'] ) ) {
- $this->readOnlyReason = $params['readOnlyReason'];
- }
-
- if ( isset( $params['loadMonitor'] ) ) {
- $this->mLoadMonitorClass = $params['loadMonitor'];
- } else {
- $master = reset( $params['servers'] );
- if ( isset( $master['type'] ) && $master['type'] === 'mysql' ) {
- $this->mLoadMonitorClass = 'LoadMonitorMySQL';
- } else {
- $this->mLoadMonitorClass = 'LoadMonitorNull';
- }
- }
-
- foreach ( $params['servers'] as $i => $server ) {
- $this->mLoads[$i] = $server['load'];
- if ( isset( $server['groupLoads'] ) ) {
- foreach ( $server['groupLoads'] as $group => $ratio ) {
- if ( !isset( $this->mGroupLoads[$group] ) ) {
- $this->mGroupLoads[$group] = [];
- }
- $this->mGroupLoads[$group][$i] = $ratio;
- }
- }
- }
-
- $this->srvCache = ObjectCache::getLocalServerInstance();
-
- if ( isset( $params['trxProfiler'] ) ) {
- $this->trxProfiler = $params['trxProfiler'];
- } else {
- $this->trxProfiler = new TransactionProfiler();
- }
- }
-
- /**
- * Get a LoadMonitor instance
- *
- * @return LoadMonitor
- */
- private function getLoadMonitor() {
- if ( !isset( $this->mLoadMonitor ) ) {
- $class = $this->mLoadMonitorClass;
- $this->mLoadMonitor = new $class( $this );
- }
-
- return $this->mLoadMonitor;
- }
-
- /**
- * Get or set arbitrary data used by the parent object, usually an LBFactory
- * @param mixed $x
- * @return mixed
- */
- public function parentInfo( $x = null ) {
- return wfSetVar( $this->mParentInfo, $x );
- }
-
- /**
- * @param array $loads
- * @param bool|string $wiki Wiki to get non-lagged for
- * @param int $maxLag Restrict the maximum allowed lag to this many seconds
- * @return bool|int|string
- */
- private function getRandomNonLagged( array $loads, $wiki = false, $maxLag = self::MAX_LAG ) {
- $lags = $this->getLagTimes( $wiki );
-
- # Unset excessively lagged servers
- foreach ( $lags as $i => $lag ) {
- if ( $i != 0 ) {
- $maxServerLag = $maxLag;
- if ( isset( $this->mServers[$i]['max lag'] ) ) {
- $maxServerLag = min( $maxServerLag, $this->mServers[$i]['max lag'] );
- }
-
- $host = $this->getServerName( $i );
- if ( $lag === false ) {
- wfDebugLog( 'replication', "Server $host (#$i) is not replicating?" );
- unset( $loads[$i] );
- } elseif ( $lag > $maxServerLag ) {
- wfDebugLog( 'replication', "Server $host (#$i) has >= $lag seconds of lag" );
- unset( $loads[$i] );
- }
- }
- }
-
- # Find out if all the slaves with non-zero load are lagged
- $sum = 0;
- foreach ( $loads as $load ) {
- $sum += $load;
- }
- if ( $sum == 0 ) {
- # No appropriate DB servers except maybe the master and some slaves with zero load
- # Do NOT use the master
- # Instead, this function will return false, triggering read-only mode,
- # and a lagged slave will be used instead.
- return false;
- }
-
- if ( count( $loads ) == 0 ) {
- return false;
- }
-
- # Return a random representative of the remainder
- return ArrayUtils::pickRandom( $loads );
- }
-
- /**
- * Get the index of the reader connection, which may be a slave
- * This takes into account load ratios and lag times. It should
- * always return a consistent index during a given invocation
- *
- * Side effect: opens connections to databases
- * @param string|bool $group Query group, or false for the generic reader
- * @param string|bool $wiki Wiki ID, or false for the current wiki
- * @throws MWException
- * @return bool|int|string
- */
- public function getReaderIndex( $group = false, $wiki = false ) {
- global $wgDBtype;
-
- # @todo FIXME: For now, only go through all this for mysql databases
- if ( $wgDBtype != 'mysql' ) {
- return $this->getWriterIndex();
- }
-
- if ( count( $this->mServers ) == 1 ) {
- # Skip the load balancing if there's only one server
- return 0;
- } elseif ( $group === false && $this->mReadIndex >= 0 ) {
- # Shortcut if generic reader exists already
- return $this->mReadIndex;
- }
-
- # Find the relevant load array
- if ( $group !== false ) {
- if ( isset( $this->mGroupLoads[$group] ) ) {
- $nonErrorLoads = $this->mGroupLoads[$group];
- } else {
- # No loads for this group, return false and the caller can use some other group
- wfDebugLog( 'connect', __METHOD__ . ": no loads for group $group\n" );
-
- return false;
- }
- } else {
- $nonErrorLoads = $this->mLoads;
- }
-
- if ( !count( $nonErrorLoads ) ) {
- throw new MWException( "Empty server array given to LoadBalancer" );
- }
-
- # Scale the configured load ratios according to the dynamic load (if the load monitor supports it)
- $this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $group, $wiki );
-
- $laggedSlaveMode = false;
-
- # No server found yet
- $i = false;
- $conn = false;
- # First try quickly looking through the available servers for a server that
- # meets our criteria
- $currentLoads = $nonErrorLoads;
- while ( count( $currentLoads ) ) {
- if ( $this->mAllowLagged || $laggedSlaveMode ) {
- $i = ArrayUtils::pickRandom( $currentLoads );
- } else {
- $i = false;
- if ( $this->mWaitForPos && $this->mWaitForPos->asOfTime() ) {
- # ChronologyProtecter causes mWaitForPos to be set via sessions.
- # This triggers doWait() after connect, so it's especially good to
- # avoid lagged servers so as to avoid just blocking in that method.
- $ago = microtime( true ) - $this->mWaitForPos->asOfTime();
- # Aim for <= 1 second of waiting (being too picky can backfire)
- $i = $this->getRandomNonLagged( $currentLoads, $wiki, $ago + 1 );
- }
- if ( $i === false ) {
- # Any server with less lag than it's 'max lag' param is preferable
- $i = $this->getRandomNonLagged( $currentLoads, $wiki );
- }
- if ( $i === false && count( $currentLoads ) != 0 ) {
- # All slaves lagged. Switch to read-only mode
- wfDebugLog( 'replication', "All slaves lagged. Switch to read-only mode" );
- $i = ArrayUtils::pickRandom( $currentLoads );
- $laggedSlaveMode = true;
- }
- }
-
- if ( $i === false ) {
- # pickRandom() returned false
- # This is permanent and means the configuration or the load monitor
- # wants us to return false.
- wfDebugLog( 'connect', __METHOD__ . ": pickRandom() returned false" );
-
- return false;
- }
-
- $serverName = $this->getServerName( $i );
- wfDebugLog( 'connect', __METHOD__ . ": Using reader #$i: $serverName..." );
-
- $conn = $this->openConnection( $i, $wiki );
- if ( !$conn ) {
- wfDebugLog( 'connect', __METHOD__ . ": Failed connecting to $i/$wiki" );
- unset( $nonErrorLoads[$i] );
- unset( $currentLoads[$i] );
- $i = false;
- continue;
- }
-
- // Decrement reference counter, we are finished with this connection.
- // It will be incremented for the caller later.
- if ( $wiki !== false ) {
- $this->reuseConnection( $conn );
- }
-
- # Return this server
- break;
- }
-
- # If all servers were down, quit now
- if ( !count( $nonErrorLoads ) ) {
- wfDebugLog( 'connect', "All servers down" );
- }
-
- if ( $i !== false ) {
- # Slave connection successful
- # Wait for the session master pos for a short time
- if ( $this->mWaitForPos && $i > 0 ) {
- if ( !$this->doWait( $i ) ) {
- $this->mServers[$i]['slave pos'] = $conn->getSlavePos();
- }
- }
- if ( $this->mReadIndex <= 0 && $this->mLoads[$i] > 0 && $group === false ) {
- $this->mReadIndex = $i;
- # Record if the generic reader index is in "lagged slave" mode
- if ( $laggedSlaveMode ) {
- $this->laggedSlaveMode = true;
- }
- }
- $serverName = $this->getServerName( $i );
- wfDebugLog( 'connect', __METHOD__ .
- ": using server $serverName for group '$group'\n" );
- }
-
- return $i;
- }
-
- /**
- * Set the master wait position
- * If a DB_SLAVE connection has been opened already, waits
- * Otherwise sets a variable telling it to wait if such a connection is opened
- * @param DBMasterPos $pos
- */
- public function waitFor( $pos ) {
- $this->mWaitForPos = $pos;
- $i = $this->mReadIndex;
-
- if ( $i > 0 ) {
- if ( !$this->doWait( $i ) ) {
- $this->mServers[$i]['slave pos'] = $this->getAnyOpenConnection( $i )->getSlavePos();
- $this->laggedSlaveMode = true;
- }
- }
- }
-
- /**
- * Set the master wait position and wait for a "generic" slave to catch up to it
- *
- * This can be used a faster proxy for waitForAll()
- *
- * @param DBMasterPos $pos
- * @param int $timeout Max seconds to wait; default is mWaitTimeout
- * @return bool Success (able to connect and no timeouts reached)
- * @since 1.26
- */
- public function waitForOne( $pos, $timeout = null ) {
- $this->mWaitForPos = $pos;
-
- $i = $this->mReadIndex;
- if ( $i <= 0 ) {
- // Pick a generic slave if there isn't one yet
- $readLoads = $this->mLoads;
- unset( $readLoads[$this->getWriterIndex()] ); // slaves only
- $readLoads = array_filter( $readLoads ); // with non-zero load
- $i = ArrayUtils::pickRandom( $readLoads );
- }
-
- if ( $i > 0 ) {
- $ok = $this->doWait( $i, true, $timeout );
- } else {
- $ok = true; // no applicable loads
- }
-
- return $ok;
- }
-
- /**
- * Set the master wait position and wait for ALL slaves to catch up to it
- * @param DBMasterPos $pos
- * @param int $timeout Max seconds to wait; default is mWaitTimeout
- * @return bool Success (able to connect and no timeouts reached)
- */
- public function waitForAll( $pos, $timeout = null ) {
- $this->mWaitForPos = $pos;
- $serverCount = count( $this->mServers );
-
- $ok = true;
- for ( $i = 1; $i < $serverCount; $i++ ) {
- if ( $this->mLoads[$i] > 0 ) {
- $ok = $this->doWait( $i, true, $timeout ) && $ok;
- }
- }
-
- return $ok;
- }
-
- /**
- * Get any open connection to a given server index, local or foreign
- * Returns false if there is no connection open
- *
- * @param int $i
- * @return DatabaseBase|bool False on failure
- */
- public function getAnyOpenConnection( $i ) {
- foreach ( $this->mConns as $conns ) {
- if ( !empty( $conns[$i] ) ) {
- return reset( $conns[$i] );
- }
- }
-
- return false;
- }
-
- /**
- * Wait for a given slave to catch up to the master pos stored in $this
- * @param int $index Server index
- * @param bool $open Check the server even if a new connection has to be made
- * @param int $timeout Max seconds to wait; default is mWaitTimeout
- * @return bool
- */
- protected function doWait( $index, $open = false, $timeout = null ) {
- $close = false; // close the connection afterwards
-
- // Check if we already know that the DB has reached this point
- $server = $this->getServerName( $index );
- $key = $this->srvCache->makeGlobalKey( __CLASS__, 'last-known-pos', $server );
- /** @var DBMasterPos $knownReachedPos */
- $knownReachedPos = $this->srvCache->get( $key );
- if ( $knownReachedPos && $knownReachedPos->hasReached( $this->mWaitForPos ) ) {
- wfDebugLog( 'replication', __METHOD__ .
- ": slave $server known to be caught up (pos >= $knownReachedPos).\n" );
- return true;
- }
-
- // Find a connection to wait on, creating one if needed and allowed
- $conn = $this->getAnyOpenConnection( $index );
- if ( !$conn ) {
- if ( !$open ) {
- wfDebugLog( 'replication', __METHOD__ . ": no connection open for $server\n" );
-
- return false;
- } else {
- $conn = $this->openConnection( $index, '' );
- if ( !$conn ) {
- wfDebugLog( 'replication', __METHOD__ . ": failed to connect to $server\n" );
-
- return false;
- }
- // Avoid connection spam in waitForAll() when connections
- // are made just for the sake of doing this lag check.
- $close = true;
- }
- }
-
- wfDebugLog( 'replication', __METHOD__ . ": Waiting for slave $server to catch up...\n" );
- $timeout = $timeout ?: $this->mWaitTimeout;
- $result = $conn->masterPosWait( $this->mWaitForPos, $timeout );
-
- if ( $result == -1 || is_null( $result ) ) {
- // Timed out waiting for slave, use master instead
- $msg = __METHOD__ . ": Timed out waiting on $server pos {$this->mWaitForPos}";
- wfDebugLog( 'replication', "$msg\n" );
- wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
- $ok = false;
- } else {
- wfDebugLog( 'replication', __METHOD__ . ": Done\n" );
- $ok = true;
- // Remember that the DB reached this point
- $this->srvCache->set( $key, $this->mWaitForPos, BagOStuff::TTL_DAY );
- }
-
- if ( $close ) {
- $this->closeConnection( $conn );
- }
-
- return $ok;
- }
-
- /**
- * Get a connection by index
- * This is the main entry point for this class.
- *
- * @param int $i Server index
- * @param array|string|bool $groups Query group(s), or false for the generic reader
- * @param string|bool $wiki Wiki ID, or false for the current wiki
- *
- * @throws MWException
- * @return DatabaseBase
- */
- public function getConnection( $i, $groups = [], $wiki = false ) {
- if ( $i === null || $i === false ) {
- throw new MWException( 'Attempt to call ' . __METHOD__ .
- ' with invalid server index' );
- }
-
- if ( $wiki === wfWikiID() ) {
- $wiki = false;
- }
-
- $groups = ( $groups === false || $groups === [] )
- ? [ false ] // check one "group": the generic pool
- : (array)$groups;
-
- $masterOnly = ( $i == DB_MASTER || $i == $this->getWriterIndex() );
- $oldConnsOpened = $this->connsOpened; // connections open now
-
- if ( $i == DB_MASTER ) {
- $i = $this->getWriterIndex();
- } else {
- # Try to find an available server in any the query groups (in order)
- foreach ( $groups as $group ) {
- $groupIndex = $this->getReaderIndex( $group, $wiki );
- if ( $groupIndex !== false ) {
- $i = $groupIndex;
- break;
- }
- }
- }
-
- # Operation-based index
- if ( $i == DB_SLAVE ) {
- $this->mLastError = 'Unknown error'; // reset error string
- # Try the general server pool if $groups are unavailable.
- $i = in_array( false, $groups, true )
- ? false // don't bother with this if that is what was tried above
- : $this->getReaderIndex( false, $wiki );
- # Couldn't find a working server in getReaderIndex()?
- if ( $i === false ) {
- $this->mLastError = 'No working slave server: ' . $this->mLastError;
-
- return $this->reportConnectionError();
- }
- }
-
- # Now we have an explicit index into the servers array
- $conn = $this->openConnection( $i, $wiki );
- if ( !$conn ) {
- return $this->reportConnectionError();
- }
-
- # Profile any new connections that happen
- if ( $this->connsOpened > $oldConnsOpened ) {
- $host = $conn->getServer();
- $dbname = $conn->getDBname();
- $trxProf = Profiler::instance()->getTransactionProfiler();
- $trxProf->recordConnection( $host, $dbname, $masterOnly );
- }
-
- if ( $masterOnly ) {
- # Make master-requested DB handles inherit any read-only mode setting
- $conn->setLBInfo( 'readOnlyReason', $this->getReadOnlyReason( $wiki ) );
- }
-
- return $conn;
- }
-
- /**
- * Mark a foreign connection as being available for reuse under a different
- * DB name or prefix. This mechanism is reference-counted, and must be called
- * the same number of times as getConnection() to work.
- *
- * @param DatabaseBase $conn
- * @throws MWException
- */
- public function reuseConnection( $conn ) {
- $serverIndex = $conn->getLBInfo( 'serverIndex' );
- $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
- if ( $serverIndex === null || $refCount === null ) {
- wfDebug( __METHOD__ . ": this connection was not opened as a foreign connection\n" );
- /**
- * This can happen in code like:
- * foreach ( $dbs as $db ) {
- * $conn = $lb->getConnection( DB_SLAVE, array(), $db );
- * ...
- * $lb->reuseConnection( $conn );
- * }
- * When a connection to the local DB is opened in this way, reuseConnection()
- * should be ignored
- */
- return;
- }
-
- $dbName = $conn->getDBname();
- $prefix = $conn->tablePrefix();
- if ( strval( $prefix ) !== '' ) {
- $wiki = "$dbName-$prefix";
- } else {
- $wiki = $dbName;
- }
- if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
- throw new MWException( __METHOD__ . ": connection not found, has " .
- "the connection been freed already?" );
- }
- $conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
- if ( $refCount <= 0 ) {
- $this->mConns['foreignFree'][$serverIndex][$wiki] = $conn;
- unset( $this->mConns['foreignUsed'][$serverIndex][$wiki] );
- wfDebug( __METHOD__ . ": freed connection $serverIndex/$wiki\n" );
- } else {
- wfDebug( __METHOD__ . ": reference count for $serverIndex/$wiki reduced to $refCount\n" );
- }
- }
-
- /**
- * Get a database connection handle reference
- *
- * The handle's methods wrap simply wrap those of a DatabaseBase handle
- *
- * @see LoadBalancer::getConnection() for parameter information
- *
- * @param int $db
- * @param array|string|bool $groups Query group(s), or false for the generic reader
- * @param string|bool $wiki Wiki ID, or false for the current wiki
- * @return DBConnRef
- */
- public function getConnectionRef( $db, $groups = [], $wiki = false ) {
- return new DBConnRef( $this, $this->getConnection( $db, $groups, $wiki ) );
- }
-
- /**
- * Get a database connection handle reference without connecting yet
- *
- * The handle's methods wrap simply wrap those of a DatabaseBase handle
- *
- * @see LoadBalancer::getConnection() for parameter information
- *
- * @param int $db
- * @param array|string|bool $groups Query group(s), or false for the generic reader
- * @param string|bool $wiki Wiki ID, or false for the current wiki
- * @return DBConnRef
- */
- public function getLazyConnectionRef( $db, $groups = [], $wiki = false ) {
- return new DBConnRef( $this, [ $db, $groups, $wiki ] );
- }
-
- /**
- * Open a connection to the server given by the specified index
- * Index must be an actual index into the array.
- * If the server is already open, returns it.
- *
- * On error, returns false, and the connection which caused the
- * error will be available via $this->mErrorConnection.
- *
- * @param int $i Server index
- * @param string|bool $wiki Wiki ID, or false for the current wiki
- * @return DatabaseBase|bool Returns false on errors
- */
- public function openConnection( $i, $wiki = false ) {
- if ( $wiki !== false ) {
- $conn = $this->openForeignConnection( $i, $wiki );
- } elseif ( isset( $this->mConns['local'][$i][0] ) ) {
- $conn = $this->mConns['local'][$i][0];
- } else {
- $server = $this->mServers[$i];
- $server['serverIndex'] = $i;
- $conn = $this->reallyOpenConnection( $server, false );
- $serverName = $this->getServerName( $i );
- if ( $conn->isOpen() ) {
- wfDebugLog( 'connect', "Connected to database $i at $serverName\n" );
- $this->mConns['local'][$i][0] = $conn;
- } else {
- wfDebugLog( 'connect', "Failed to connect to database $i at $serverName\n" );
- $this->mErrorConnection = $conn;
- $conn = false;
- }
- }
-
- if ( $conn && !$conn->isOpen() ) {
- // Connection was made but later unrecoverably lost for some reason.
- // Do not return a handle that will just throw exceptions on use,
- // but let the calling code (e.g. getReaderIndex) try another server.
- // See DatabaseMyslBase::ping() for how this can happen.
- $this->mErrorConnection = $conn;
- $conn = false;
- }
-
- return $conn;
- }
-
- /**
- * Open a connection to a foreign DB, or return one if it is already open.
- *
- * Increments a reference count on the returned connection which locks the
- * connection to the requested wiki. This reference count can be
- * decremented by calling reuseConnection().
- *
- * If a connection is open to the appropriate server already, but with the wrong
- * database, it will be switched to the right database and returned, as long as
- * it has been freed first with reuseConnection().
- *
- * On error, returns false, and the connection which caused the
- * error will be available via $this->mErrorConnection.
- *
- * @param int $i Server index
- * @param string $wiki Wiki ID to open
- * @return DatabaseBase
- */
- private function openForeignConnection( $i, $wiki ) {
- list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
- if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
- // Reuse an already-used connection
- $conn = $this->mConns['foreignUsed'][$i][$wiki];
- wfDebug( __METHOD__ . ": reusing connection $i/$wiki\n" );
- } elseif ( isset( $this->mConns['foreignFree'][$i][$wiki] ) ) {
- // Reuse a free connection for the same wiki
- $conn = $this->mConns['foreignFree'][$i][$wiki];
- unset( $this->mConns['foreignFree'][$i][$wiki] );
- $this->mConns['foreignUsed'][$i][$wiki] = $conn;
- wfDebug( __METHOD__ . ": reusing free connection $i/$wiki\n" );
- } elseif ( !empty( $this->mConns['foreignFree'][$i] ) ) {
- // Reuse a connection from another wiki
- $conn = reset( $this->mConns['foreignFree'][$i] );
- $oldWiki = key( $this->mConns['foreignFree'][$i] );
-
- // The empty string as a DB name means "don't care".
- // DatabaseMysqlBase::open() already handle this on connection.
- if ( $dbName !== '' && !$conn->selectDB( $dbName ) ) {
- $this->mLastError = "Error selecting database $dbName on server " .
- $conn->getServer() . " from client host " . wfHostname() . "\n";
- $this->mErrorConnection = $conn;
- $conn = false;
- } else {
- $conn->tablePrefix( $prefix );
- unset( $this->mConns['foreignFree'][$i][$oldWiki] );
- $this->mConns['foreignUsed'][$i][$wiki] = $conn;
- wfDebug( __METHOD__ . ": reusing free connection from $oldWiki for $wiki\n" );
- }
- } else {
- // Open a new connection
- $server = $this->mServers[$i];
- $server['serverIndex'] = $i;
- $server['foreignPoolRefCount'] = 0;
- $server['foreign'] = true;
- $conn = $this->reallyOpenConnection( $server, $dbName );
- if ( !$conn->isOpen() ) {
- wfDebug( __METHOD__ . ": error opening connection for $i/$wiki\n" );
- $this->mErrorConnection = $conn;
- $conn = false;
- } else {
- $conn->tablePrefix( $prefix );
- $this->mConns['foreignUsed'][$i][$wiki] = $conn;
- wfDebug( __METHOD__ . ": opened new connection for $i/$wiki\n" );
- }
- }
-
- // Increment reference count
- if ( $conn ) {
- $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
- $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
- }
-
- return $conn;
- }
-
- /**
- * Test if the specified index represents an open connection
- *
- * @param int $index Server index
- * @access private
- * @return bool
- */
- private function isOpen( $index ) {
- if ( !is_integer( $index ) ) {
- return false;
- }
-
- return (bool)$this->getAnyOpenConnection( $index );
- }
-
- /**
- * Really opens a connection. Uncached.
- * Returns a Database object whether or not the connection was successful.
- * @access private
- *
- * @param array $server
- * @param bool $dbNameOverride
- * @throws MWException
- * @return DatabaseBase
- */
- protected function reallyOpenConnection( $server, $dbNameOverride = false ) {
- if ( !is_array( $server ) ) {
- throw new MWException( 'You must update your load-balancing configuration. ' .
- 'See DefaultSettings.php entry for $wgDBservers.' );
- }
-
- if ( $dbNameOverride !== false ) {
- $server['dbname'] = $dbNameOverride;
- }
-
- // Let the handle know what the cluster master is (e.g. "db1052")
- $masterName = $this->getServerName( 0 );
- $server['clusterMasterHost'] = $masterName;
-
- // Log when many connection are made on requests
- if ( ++$this->connsOpened >= self::CONN_HELD_WARN_THRESHOLD ) {
- wfDebugLog( 'DBPerformance', __METHOD__ . ": " .
- "{$this->connsOpened}+ connections made (master=$masterName)\n" .
- wfBacktrace( true ) );
- }
-
- # Create object
- try {
- $db = DatabaseBase::factory( $server['type'], $server );
- } catch ( DBConnectionError $e ) {
- // FIXME: This is probably the ugliest thing I have ever done to
- // PHP. I'm half-expecting it to segfault, just out of disgust. -- TS
- $db = $e->db;
- }
-
- $db->setLBInfo( $server );
- $db->setLazyMasterHandle(
- $this->getLazyConnectionRef( DB_MASTER, [], $db->getWikiID() )
- );
- $db->setTransactionProfiler( $this->trxProfiler );
-
- return $db;
- }
-
- /**
- * @throws DBConnectionError
- * @return bool
- */
- private function reportConnectionError() {
- $conn = $this->mErrorConnection; // The connection which caused the error
- $context = [
- 'method' => __METHOD__,
- 'last_error' => $this->mLastError,
- ];
-
- if ( !is_object( $conn ) ) {
- // No last connection, probably due to all servers being too busy
- wfLogDBError(
- "LB failure with no last connection. Connection error: {last_error}",
- $context
- );
-
- // If all servers were busy, mLastError will contain something sensible
- throw new DBConnectionError( null, $this->mLastError );
- } else {
- $context['db_server'] = $conn->getProperty( 'mServer' );
- wfLogDBError(
- "Connection error: {last_error} ({db_server})",
- $context
- );
-
- // throws DBConnectionError
- $conn->reportConnectionError( "{$this->mLastError} ({$context['db_server']})" );
- }
-
- return false; /* not reached */
- }
-
- /**
- * @return int
- * @since 1.26
- */
- public function getWriterIndex() {
- return 0;
- }
-
- /**
- * Returns true if the specified index is a valid server index
- *
- * @param string $i
- * @return bool
- */
- public function haveIndex( $i ) {
- return array_key_exists( $i, $this->mServers );
- }
-
- /**
- * Returns true if the specified index is valid and has non-zero load
- *
- * @param string $i
- * @return bool
- */
- public function isNonZeroLoad( $i ) {
- return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
- }
-
- /**
- * Get the number of defined servers (not the number of open connections)
- *
- * @return int
- */
- public function getServerCount() {
- return count( $this->mServers );
- }
-
- /**
- * Get the host name or IP address of the server with the specified index
- * Prefer a readable name if available.
- * @param string $i
- * @return string
- */
- public function getServerName( $i ) {
- if ( isset( $this->mServers[$i]['hostName'] ) ) {
- $name = $this->mServers[$i]['hostName'];
- } elseif ( isset( $this->mServers[$i]['host'] ) ) {
- $name = $this->mServers[$i]['host'];
- } else {
- $name = '';
- }
-
- return ( $name != '' ) ? $name : 'localhost';
- }
-
- /**
- * Return the server info structure for a given index, or false if the index is invalid.
- * @param int $i
- * @return array|bool
- */
- public function getServerInfo( $i ) {
- if ( isset( $this->mServers[$i] ) ) {
- return $this->mServers[$i];
- } else {
- return false;
- }
- }
-
- /**
- * Sets the server info structure for the given index. Entry at index $i
- * is created if it doesn't exist
- * @param int $i
- * @param array $serverInfo
- */
- public function setServerInfo( $i, array $serverInfo ) {
- $this->mServers[$i] = $serverInfo;
- }
-
- /**
- * Get the current master position for chronology control purposes
- * @return mixed
- */
- public function getMasterPos() {
- # If this entire request was served from a slave without opening a connection to the
- # master (however unlikely that may be), then we can fetch the position from the slave.
- $masterConn = $this->getAnyOpenConnection( 0 );
- if ( !$masterConn ) {
- $serverCount = count( $this->mServers );
- for ( $i = 1; $i < $serverCount; $i++ ) {
- $conn = $this->getAnyOpenConnection( $i );
- if ( $conn ) {
- return $conn->getSlavePos();
- }
- }
- } else {
- return $masterConn->getMasterPos();
- }
-
- return false;
- }
-
- /**
- * Close all open connections
- */
- public function closeAll() {
- foreach ( $this->mConns as $conns2 ) {
- foreach ( $conns2 as $conns3 ) {
- /** @var DatabaseBase $conn */
- foreach ( $conns3 as $conn ) {
- $conn->close();
- }
- }
- }
- $this->mConns = [
- 'local' => [],
- 'foreignFree' => [],
- 'foreignUsed' => [],
- ];
- $this->connsOpened = 0;
- }
-
- /**
- * Close a connection
- * Using this function makes sure the LoadBalancer knows the connection is closed.
- * If you use $conn->close() directly, the load balancer won't update its state.
- * @param DatabaseBase $conn
- */
- public function closeConnection( $conn ) {
- $done = false;
- foreach ( $this->mConns as $i1 => $conns2 ) {
- foreach ( $conns2 as $i2 => $conns3 ) {
- foreach ( $conns3 as $i3 => $candidateConn ) {
- if ( $conn === $candidateConn ) {
- $conn->close();
- unset( $this->mConns[$i1][$i2][$i3] );
- --$this->connsOpened;
- $done = true;
- break;
- }
- }
- }
- }
- if ( !$done ) {
- $conn->close();
- }
- }
-
- /**
- * Commit transactions on all open connections
- * @param string $fname Caller name
- */
- public function commitAll( $fname = __METHOD__ ) {
- foreach ( $this->mConns as $conns2 ) {
- foreach ( $conns2 as $conns3 ) {
- /** @var DatabaseBase[] $conns3 */
- foreach ( $conns3 as $conn ) {
- if ( $conn->trxLevel() ) {
- $conn->commit( $fname, 'flush' );
- }
- }
- }
- }
- }
-
- /**
- * Issue COMMIT only on master, only if queries were done on connection
- * @param string $fname Caller name
- */
- public function commitMasterChanges( $fname = __METHOD__ ) {
- $masterIndex = $this->getWriterIndex();
- foreach ( $this->mConns as $conns2 ) {
- if ( empty( $conns2[$masterIndex] ) ) {
- continue;
- }
- /** @var DatabaseBase $conn */
- foreach ( $conns2[$masterIndex] as $conn ) {
- if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
- $conn->commit( $fname, 'flush' );
- }
- }
- }
- }
-
- /**
- * Issue ROLLBACK only on master, only if queries were done on connection
- * @param string $fname Caller name
- * @throws DBExpectedError
- * @since 1.23
- */
- public function rollbackMasterChanges( $fname = __METHOD__ ) {
- $failedServers = [];
-
- $masterIndex = $this->getWriterIndex();
- foreach ( $this->mConns as $conns2 ) {
- if ( empty( $conns2[$masterIndex] ) ) {
- continue;
- }
- /** @var DatabaseBase $conn */
- foreach ( $conns2[$masterIndex] as $conn ) {
- if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
- try {
- $conn->rollback( $fname, 'flush' );
- } catch ( DBError $e ) {
- MWExceptionHandler::logException( $e );
- $failedServers[] = $conn->getServer();
- }
- }
- }
- }
-
- if ( $failedServers ) {
- throw new DBExpectedError( null, "Rollback failed on server(s) " .
- implode( ', ', array_unique( $failedServers ) ) );
- }
- }
-
- /**
- * @return bool Whether a master connection is already open
- * @since 1.24
- */
- public function hasMasterConnection() {
- return $this->isOpen( $this->getWriterIndex() );
- }
-
- /**
- * Determine if there are pending changes in a transaction by this thread
- * @since 1.23
- * @return bool
- */
- public function hasMasterChanges() {
- $masterIndex = $this->getWriterIndex();
- foreach ( $this->mConns as $conns2 ) {
- if ( empty( $conns2[$masterIndex] ) ) {
- continue;
- }
- /** @var DatabaseBase $conn */
- foreach ( $conns2[$masterIndex] as $conn ) {
- if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
- return true;
- }
- }
- }
- return false;
- }
-
- /**
- * Get the timestamp of the latest write query done by this thread
- * @since 1.25
- * @return float|bool UNIX timestamp or false
- */
- public function lastMasterChangeTimestamp() {
- $lastTime = false;
- $masterIndex = $this->getWriterIndex();
- foreach ( $this->mConns as $conns2 ) {
- if ( empty( $conns2[$masterIndex] ) ) {
- continue;
- }
- /** @var DatabaseBase $conn */
- foreach ( $conns2[$masterIndex] as $conn ) {
- $lastTime = max( $lastTime, $conn->lastDoneWrites() );
- }
- }
- return $lastTime;
- }
-
- /**
- * Check if this load balancer object had any recent or still
- * pending writes issued against it by this PHP thread
- *
- * @param float $age How many seconds ago is "recent" [defaults to mWaitTimeout]
- * @return bool
- * @since 1.25
- */
- public function hasOrMadeRecentMasterChanges( $age = null ) {
- $age = ( $age === null ) ? $this->mWaitTimeout : $age;
-
- return ( $this->hasMasterChanges()
- || $this->lastMasterChangeTimestamp() > microtime( true ) - $age );
- }
-
- /**
- * Get the list of callers that have pending master changes
- *
- * @return array
- * @since 1.27
- */
- public function pendingMasterChangeCallers() {
- $fnames = [];
-
- $masterIndex = $this->getWriterIndex();
- foreach ( $this->mConns as $conns2 ) {
- if ( empty( $conns2[$masterIndex] ) ) {
- continue;
- }
- /** @var DatabaseBase $conn */
- foreach ( $conns2[$masterIndex] as $conn ) {
- $fnames = array_merge( $fnames, $conn->pendingWriteCallers() );
- }
- }
-
- return $fnames;
- }
-
- /**
- * @param mixed $value
- * @return mixed
- */
- public function waitTimeout( $value = null ) {
- return wfSetVar( $this->mWaitTimeout, $value );
- }
-
- /**
- * @note This method will trigger a DB connection if not yet done
- *
- * @param string|bool $wiki Wiki ID, or false for the current wiki
- * @return bool Whether the generic connection for reads is highly "lagged"
- */
- public function getLaggedSlaveMode( $wiki = false ) {
- // No-op if there is only one DB (also avoids recursion)
- if ( !$this->laggedSlaveMode && $this->getServerCount() > 1 ) {
- try {
- // See if laggedSlaveMode gets set
- $conn = $this->getConnection( DB_SLAVE, false, $wiki );
- $this->reuseConnection( $conn );
- } catch ( DBConnectionError $e ) {
- // Avoid expensive re-connect attempts and failures
- $this->slavesDownMode = true;
- $this->laggedSlaveMode = true;
- }
- }
-
- return $this->laggedSlaveMode;
- }
-
- /**
- * @note This method will never cause a new DB connection
- * @return bool Whether any generic connection used for reads was highly "lagged"
- * @since 1.27
- */
- public function laggedSlaveUsed() {
- return $this->laggedSlaveMode;
- }
-
- /**
- * @note This method may trigger a DB connection if not yet done
- * @param string|bool $wiki Wiki ID, or false for the current wiki
- * @return string|bool Reason the master is read-only or false if it is not
- * @since 1.27
- */
- public function getReadOnlyReason( $wiki = false ) {
- if ( $this->readOnlyReason !== false ) {
- return $this->readOnlyReason;
- } elseif ( $this->getLaggedSlaveMode( $wiki ) ) {
- if ( $this->slavesDownMode ) {
- return 'The database has been automatically locked ' .
- 'until the slave database servers become available';
- } else {
- return 'The database has been automatically locked ' .
- 'while the slave database servers catch up to the master.';
- }
- }
-
- return false;
- }
-
- /**
- * Disables/enables lag checks
- * @param null|bool $mode
- * @return bool
- */
- public function allowLagged( $mode = null ) {
- if ( $mode === null ) {
- return $this->mAllowLagged;
- }
- $this->mAllowLagged = $mode;
-
- return $this->mAllowLagged;
- }
-
- /**
- * @return bool
- */
- public function pingAll() {
- $success = true;
- foreach ( $this->mConns as $conns2 ) {
- foreach ( $conns2 as $conns3 ) {
- /** @var DatabaseBase[] $conns3 */
- foreach ( $conns3 as $conn ) {
- if ( !$conn->ping() ) {
- $success = false;
- }
- }
- }
- }
-
- return $success;
- }
-
- /**
- * Call a function with each open connection object
- * @param callable $callback
- * @param array $params
- */
- public function forEachOpenConnection( $callback, array $params = [] ) {
- foreach ( $this->mConns as $conns2 ) {
- foreach ( $conns2 as $conns3 ) {
- foreach ( $conns3 as $conn ) {
- $mergedParams = array_merge( [ $conn ], $params );
- call_user_func_array( $callback, $mergedParams );
- }
- }
- }
- }
-
- /**
- * Get the hostname and lag time of the most-lagged slave
- *
- * This is useful for maintenance scripts that need to throttle their updates.
- * May attempt to open connections to slaves on the default DB. If there is
- * no lag, the maximum lag will be reported as -1.
- *
- * @param bool|string $wiki Wiki ID, or false for the default database
- * @return array ( host, max lag, index of max lagged host )
- */
- public function getMaxLag( $wiki = false ) {
- $maxLag = -1;
- $host = '';
- $maxIndex = 0;
-
- if ( $this->getServerCount() <= 1 ) {
- return [ $host, $maxLag, $maxIndex ]; // no replication = no lag
- }
-
- $lagTimes = $this->getLagTimes( $wiki );
- foreach ( $lagTimes as $i => $lag ) {
- if ( $lag > $maxLag ) {
- $maxLag = $lag;
- $host = $this->mServers[$i]['host'];
- $maxIndex = $i;
- }
- }
-
- return [ $host, $maxLag, $maxIndex ];
- }
-
- /**
- * Get an estimate of replication lag (in seconds) for each server
- *
- * Results are cached for a short time in memcached/process cache
- *
- * Values may be "false" if replication is too broken to estimate
- *
- * @param string|bool $wiki
- * @return int[] Map of (server index => float|int|bool)
- */
- public function getLagTimes( $wiki = false ) {
- if ( $this->getServerCount() <= 1 ) {
- return [ 0 => 0 ]; // no replication = no lag
- }
-
- # Send the request to the load monitor
- return $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki );
- }
-
- /**
- * Get the lag in seconds for a given connection, or zero if this load
- * balancer does not have replication enabled.
- *
- * This should be used in preference to Database::getLag() in cases where
- * replication may not be in use, since there is no way to determine if
- * replication is in use at the connection level without running
- * potentially restricted queries such as SHOW SLAVE STATUS. Using this
- * function instead of Database::getLag() avoids a fatal error in this
- * case on many installations.
- *
- * @param IDatabase $conn
- * @return int|bool Returns false on error
- */
- public function safeGetLag( IDatabase $conn ) {
- if ( $this->getServerCount() == 1 ) {
- return 0;
- } else {
- return $conn->getLag();
- }
- }
-
- /**
- * Wait for a slave DB to reach a specified master position
- *
- * This will connect to the master to get an accurate position if $pos is not given
- *
- * @param IDatabase $conn Slave DB
- * @param DBMasterPos|bool $pos Master position; default: current position
- * @param integer $timeout Timeout in seconds
- * @return bool Success
- * @since 1.27
- */
- public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ) {
- if ( $this->getServerCount() == 1 || !$conn->getLBInfo( 'slave' ) ) {
- return true; // server is not a slave DB
- }
-
- $pos = $pos ?: $this->getConnection( DB_MASTER )->getMasterPos();
- if ( !$pos ) {
- return false; // something is misconfigured
- }
-
- $result = $conn->masterPosWait( $pos, $timeout );
- if ( $result == -1 || is_null( $result ) ) {
- $msg = __METHOD__ . ": Timed out waiting on {$conn->getServer()} pos {$pos}";
- wfDebugLog( 'replication', "$msg\n" );
- wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
- $ok = false;
- } else {
- wfDebugLog( 'replication', __METHOD__ . ": Done\n" );
- $ok = true;
- }
-
- return $ok;
- }
-
- /**
- * Clear the cache for slag lag delay times
- *
- * This is only used for testing
- */
- public function clearLagTimeCache() {
- $this->getLoadMonitor()->clearCaches();
- }
-}
diff --git a/www/wiki/includes/db/loadbalancer/LoadMonitor.php b/www/wiki/includes/db/loadbalancer/LoadMonitor.php
deleted file mode 100644
index e68cf1a5..00000000
--- a/www/wiki/includes/db/loadbalancer/LoadMonitor.php
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-/**
- * Database load monitoring.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * An interface for database load monitoring
- *
- * @ingroup Database
- */
-interface LoadMonitor {
- /**
- * Construct a new LoadMonitor with a given LoadBalancer parent
- *
- * @param LoadBalancer $parent
- */
- public function __construct( $parent );
-
- /**
- * Perform pre-connection load ratio adjustment.
- * @param array &$loads
- * @param string|bool $group The selected query group. Default: false
- * @param string|bool $wiki Default: false
- */
- public function scaleLoads( &$loads, $group = false, $wiki = false );
-
- /**
- * Get an estimate of replication lag (in seconds) for each server
- *
- * Values may be "false" if replication is too broken to estimate
- *
- * @param array $serverIndexes
- * @param string $wiki
- *
- * @return array Map of (server index => float|int|bool)
- */
- public function getLagTimes( $serverIndexes, $wiki );
-
- /**
- * Clear any process and persistent cache of lag times
- * @since 1.27
- */
- public function clearCaches();
-}
-
-class LoadMonitorNull implements LoadMonitor {
- public function __construct( $parent ) {
- }
-
- public function scaleLoads( &$loads, $group = false, $wiki = false ) {
- }
-
- public function getLagTimes( $serverIndexes, $wiki ) {
- return array_fill_keys( $serverIndexes, 0 );
- }
-
- public function clearCaches() {
-
- }
-}
diff --git a/www/wiki/includes/db/loadbalancer/LoadMonitorMySQL.php b/www/wiki/includes/db/loadbalancer/LoadMonitorMySQL.php
deleted file mode 100644
index 444c4b4e..00000000
--- a/www/wiki/includes/db/loadbalancer/LoadMonitorMySQL.php
+++ /dev/null
@@ -1,148 +0,0 @@
-<?php
-/**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * Basic MySQL load monitor with no external dependencies
- * Uses memcached to cache the replication lag for a short time
- *
- * @ingroup Database
- */
-class LoadMonitorMySQL implements LoadMonitor {
- /** @var LoadBalancer */
- public $parent;
- /** @var BagOStuff */
- protected $srvCache;
- /** @var BagOStuff */
- protected $mainCache;
-
- public function __construct( $parent ) {
- $this->parent = $parent;
-
- $this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
- $this->mainCache = ObjectCache::getLocalClusterInstance();
- }
-
- public function scaleLoads( &$loads, $group = false, $wiki = false ) {
- }
-
- public function getLagTimes( $serverIndexes, $wiki ) {
- if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) {
- # Single server only, just return zero without caching
- return [ 0 => 0 ];
- }
-
- $key = $this->getLagTimeCacheKey();
- # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
- $ttl = mt_rand( 4e6, 5e6 ) / 1e6;
- # Keep keys around longer as fallbacks
- $staleTTL = 60;
-
- # (a) Check the local APC cache
- $value = $this->srvCache->get( $key );
- if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
- wfDebugLog( 'replication', __METHOD__ . ": got lag times ($key) from local cache" );
- return $value['lagTimes']; // cache hit
- }
- $staleValue = $value ?: false;
-
- # (b) Check the shared cache and backfill APC
- $value = $this->mainCache->get( $key );
- if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
- $this->srvCache->set( $key, $value, $staleTTL );
- wfDebugLog( 'replication', __METHOD__ . ": got lag times ($key) from main cache" );
-
- return $value['lagTimes']; // cache hit
- }
- $staleValue = $value ?: $staleValue;
-
- # (c) Cache key missing or expired; regenerate and backfill
- if ( $this->mainCache->lock( $key, 0, 10 ) ) {
- # Let this process alone update the cache value
- $cache = $this->mainCache;
- /** @noinspection PhpUnusedLocalVariableInspection */
- $unlocker = new ScopedCallback( function () use ( $cache, $key ) {
- $cache->unlock( $key );
- } );
- } elseif ( $staleValue ) {
- # Could not acquire lock but an old cache exists, so use it
- return $staleValue['lagTimes'];
- }
-
- $lagTimes = [];
- foreach ( $serverIndexes as $i ) {
- if ( $i == $this->parent->getWriterIndex() ) {
- $lagTimes[$i] = 0; // master always has no lag
- continue;
- }
-
- $conn = $this->parent->getAnyOpenConnection( $i );
- if ( $conn ) {
- $close = false; // already open
- } else {
- $conn = $this->parent->openConnection( $i, $wiki );
- $close = true; // new connection
- }
-
- if ( !$conn ) {
- $lagTimes[$i] = false;
- $host = $this->parent->getServerName( $i );
- wfDebugLog( 'replication', __METHOD__ . ": host $host (#$i) is unreachable" );
- continue;
- }
-
- $lagTimes[$i] = $conn->getLag();
- if ( $lagTimes[$i] === false ) {
- $host = $this->parent->getServerName( $i );
- wfDebugLog( 'replication', __METHOD__ . ": host $host (#$i) is not replicating?" );
- }
-
- if ( $close ) {
- # Close the connection to avoid sleeper connections piling up.
- # Note that the caller will pick one of these DBs and reconnect,
- # which is slightly inefficient, but this only matters for the lag
- # time cache miss cache, which is far less common that cache hits.
- $this->parent->closeConnection( $conn );
- }
- }
-
- # Add a timestamp key so we know when it was cached
- $value = [ 'lagTimes' => $lagTimes, 'timestamp' => microtime( true ) ];
- $this->mainCache->set( $key, $value, $staleTTL );
- $this->srvCache->set( $key, $value, $staleTTL );
- wfDebugLog( 'replication', __METHOD__ . ": re-calculated lag times ($key)" );
-
- return $value['lagTimes'];
- }
-
- public function clearCaches() {
- $key = $this->getLagTimeCacheKey();
- $this->srvCache->delete( $key );
- $this->mainCache->delete( $key );
- }
-
- private function getLagTimeCacheKey() {
- $writerIndex = $this->parent->getWriterIndex();
- // Lag is per-server, not per-DB, so key on the master DB name
- return $this->srvCache->makeGlobalKey(
- 'lag-times', $this->parent->getServerName( $writerIndex )
- );
- }
-}
diff --git a/www/wiki/includes/debug/MWDebug.php b/www/wiki/includes/debug/MWDebug.php
index 012837fd..74798414 100644
--- a/www/wiki/includes/debug/MWDebug.php
+++ b/www/wiki/includes/debug/MWDebug.php
@@ -517,7 +517,7 @@ class MWDebug {
return [];
}
- global $wgVersion, $wgRequestTime;
+ global $wgVersion;
$request = $context->getRequest();
// HHVM's reported memory usage from memory_get_peak_usage()
@@ -540,7 +540,7 @@ class MWDebug {
'gitRevision' => GitInfo::headSHA1(),
'gitBranch' => $branch,
'gitViewUrl' => GitInfo::headViewUrl(),
- 'time' => microtime( true ) - $wgRequestTime,
+ 'time' => $request->getElapsedTime(),
'log' => self::$log,
'debugLog' => self::$debug,
'queries' => self::$query,
diff --git a/www/wiki/includes/debug/logger/LegacyLogger.php b/www/wiki/includes/debug/logger/LegacyLogger.php
index 06ec5743..7bd505d0 100644
--- a/www/wiki/includes/debug/logger/LegacyLogger.php
+++ b/www/wiki/includes/debug/logger/LegacyLogger.php
@@ -467,7 +467,7 @@ class LegacyLogger extends AbstractLogger {
$transport = UDPTransport::newFromString( $file );
$transport->emit( $text );
} else {
- \MediaWiki\suppressWarnings();
+ \Wikimedia\suppressWarnings();
$exists = file_exists( $file );
$size = $exists ? filesize( $file ) : false;
if ( !$exists ||
@@ -475,7 +475,7 @@ class LegacyLogger extends AbstractLogger {
) {
file_put_contents( $file, $text, FILE_APPEND );
}
- \MediaWiki\restoreWarnings();
+ \Wikimedia\restoreWarnings();
}
}
diff --git a/www/wiki/includes/debug/logger/LegacySpi.php b/www/wiki/includes/debug/logger/LegacySpi.php
index 8e750cab..cb0e066c 100644
--- a/www/wiki/includes/debug/logger/LegacySpi.php
+++ b/www/wiki/includes/debug/logger/LegacySpi.php
@@ -26,7 +26,7 @@ namespace MediaWiki\Logger;
* Usage:
* @code
* $wgMWLoggerDefaultSpi = [
- * 'class' => '\\MediaWiki\\Logger\\LegacySpi',
+ * 'class' => \MediaWiki\Logger\LegacySpi::class,
* ];
* @endcode
*
diff --git a/www/wiki/includes/debug/logger/LoggerFactory.php b/www/wiki/includes/debug/logger/LoggerFactory.php
index c183ff15..d6931942 100644
--- a/www/wiki/includes/debug/logger/LoggerFactory.php
+++ b/www/wiki/includes/debug/logger/LoggerFactory.php
@@ -20,7 +20,7 @@
namespace MediaWiki\Logger;
-use ObjectFactory;
+use Wikimedia\ObjectFactory;
/**
* PSR-3 logger instance factory.
diff --git a/www/wiki/includes/debug/logger/MonologSpi.php b/www/wiki/includes/debug/logger/MonologSpi.php
index 197b269b..ec27ad1c 100644
--- a/www/wiki/includes/debug/logger/MonologSpi.php
+++ b/www/wiki/includes/debug/logger/MonologSpi.php
@@ -22,7 +22,7 @@ namespace MediaWiki\Logger;
use MediaWiki\Logger\Monolog\BufferHandler;
use Monolog\Logger;
-use ObjectFactory;
+use Wikimedia\ObjectFactory;
/**
* LoggerFactory service provider that creates loggers implemented by
@@ -40,7 +40,7 @@ use ObjectFactory;
* default SPI provider:
* @code
* $wgMWLoggerDefaultSpi = [
- * 'class' => '\\MediaWiki\\Logger\\MonologSpi',
+ * 'class' => \MediaWiki\Logger\MonologSpi::class,
* 'args' => [ [
* 'loggers' => [
* '@default' => [
@@ -54,29 +54,29 @@ use ObjectFactory;
* ],
* 'processors' => [
* 'wiki' => [
- * 'class' => '\\MediaWiki\\Logger\\Monolog\\WikiProcessor',
+ * 'class' => \MediaWiki\Logger\Monolog\WikiProcessor::class,
* ],
* 'psr' => [
- * 'class' => '\\Monolog\\Processor\\PsrLogMessageProcessor',
+ * 'class' => \Monolog\Processor\PsrLogMessageProcessor::class,
* ],
* 'pid' => [
- * 'class' => '\\Monolog\\Processor\\ProcessIdProcessor',
+ * 'class' => \Monolog\Processor\ProcessIdProcessor::class,
* ],
* 'uid' => [
- * 'class' => '\\Monolog\\Processor\\UidProcessor',
+ * 'class' => \Monolog\Processor\UidProcessor::class,
* ],
* 'web' => [
- * 'class' => '\\Monolog\\Processor\\WebProcessor',
+ * 'class' => \Monolog\Processor\WebProcessor::class,
* ],
* ],
* 'handlers' => [
* 'stream' => [
- * 'class' => '\\Monolog\\Handler\\StreamHandler',
+ * 'class' => \Monolog\Handler\StreamHandler::class,
* 'args' => [ 'path/to/your.log' ],
* 'formatter' => 'line',
* ],
* 'redis' => [
- * 'class' => '\\Monolog\\Handler\\RedisHandler',
+ * 'class' => \Monolog\Handler\RedisHandler::class,
* 'args' => [ function() {
* $redis = new Redis();
* $redis->connect( '127.0.0.1', 6379 );
@@ -88,7 +88,7 @@ use ObjectFactory;
* 'buffer' => true,
* ],
* 'udp2log' => [
- * 'class' => '\\MediaWiki\\Logger\\Monolog\\LegacyHandler',
+ * 'class' => \MediaWiki\Logger\Monolog\LegacyHandler::class,
* 'args' => [
* 'udp://127.0.0.1:8420/mediawiki
* ],
@@ -97,10 +97,10 @@ use ObjectFactory;
* ],
* 'formatters' => [
* 'line' => [
- * 'class' => '\\Monolog\\Formatter\\LineFormatter',
+ * 'class' => \Monolog\Formatter\LineFormatter::class,
* ],
* 'logstash' => [
- * 'class' => '\\Monolog\\Formatter\\LogstashFormatter',
+ * 'class' => \Monolog\Formatter\LogstashFormatter::class,
* 'args' => [ 'mediawiki', php_uname( 'n' ), null, '', 1 ],
* ],
* ],
diff --git a/www/wiki/includes/debug/logger/NullSpi.php b/www/wiki/includes/debug/logger/NullSpi.php
index 4862157d..d65c1851 100644
--- a/www/wiki/includes/debug/logger/NullSpi.php
+++ b/www/wiki/includes/debug/logger/NullSpi.php
@@ -29,7 +29,7 @@ use Psr\Log\NullLogger;
* Usage:
*
* $wgMWLoggerDefaultSpi = [
- * 'class' => '\\MediaWiki\\Logger\\NullSpi',
+ * 'class' => \MediaWiki\Logger\NullSpi::class,
* ];
*
* @see \MediaWiki\Logger\LoggerFactory
diff --git a/www/wiki/includes/debug/logger/monolog/WikiProcessor.php b/www/wiki/includes/debug/logger/monolog/WikiProcessor.php
index e39a2c30..db5b9bf6 100644
--- a/www/wiki/includes/debug/logger/monolog/WikiProcessor.php
+++ b/www/wiki/includes/debug/logger/monolog/WikiProcessor.php
@@ -39,7 +39,7 @@ class WikiProcessor {
$record['extra']['wiki'] = wfWikiID();
$record['extra']['mwversion'] = $wgVersion;
$record['extra']['reqId'] = \WebRequest::getRequestId();
- if ( PHP_SAPI === 'cli' && isset( $_SERVER['argv'] ) ) {
+ if ( wfIsCLI() && isset( $_SERVER['argv'] ) ) {
$record['extra']['cli_argv'] = implode( ' ', $_SERVER['argv'] );
}
return $record;
diff --git a/www/wiki/includes/deferred/CallableUpdate.php b/www/wiki/includes/deferred/CallableUpdate.php
deleted file mode 100644
index 4b19c200..00000000
--- a/www/wiki/includes/deferred/CallableUpdate.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-/**
- * Deferrable Update for closure/callback
- */
-class MWCallableUpdate implements DeferrableUpdate {
- /** @var Closure|callable */
- private $callback;
-
- /**
- * @param callable $callback
- * @throws InvalidArgumentException
- */
- public function __construct( $callback ) {
- if ( !is_callable( $callback ) ) {
- throw new InvalidArgumentException( 'Not a valid callback/closure!' );
- }
- $this->callback = $callback;
- }
-
- public function doUpdate() {
- call_user_func( $this->callback );
- }
-}
diff --git a/www/wiki/includes/deferred/CdnCacheUpdate.php b/www/wiki/includes/deferred/CdnCacheUpdate.php
index 7fafc0eb..301c4f3b 100644
--- a/www/wiki/includes/deferred/CdnCacheUpdate.php
+++ b/www/wiki/includes/deferred/CdnCacheUpdate.php
@@ -18,7 +18,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup Cache
*/
use Wikimedia\Assert\Assert;
diff --git a/www/wiki/includes/deferred/DataUpdate.php b/www/wiki/includes/deferred/DataUpdate.php
index d2d8bd7a..ed9a7462 100644
--- a/www/wiki/includes/deferred/DataUpdate.php
+++ b/www/wiki/includes/deferred/DataUpdate.php
@@ -28,6 +28,10 @@
abstract class DataUpdate implements DeferrableUpdate {
/** @var mixed Result from LBFactory::getEmptyTransactionTicket() */
protected $ticket;
+ /** @var string Short update cause action description */
+ protected $causeAction = 'unknown';
+ /** @var string Short update cause user description */
+ protected $causeAgent = 'unknown';
public function __construct() {
// noop
@@ -42,6 +46,29 @@ abstract class DataUpdate implements DeferrableUpdate {
}
/**
+ * @param string $action Action type
+ * @param string $user User name
+ */
+ public function setCause( $action, $user ) {
+ $this->causeAction = $action;
+ $this->causeAgent = $user;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCauseAction() {
+ return $this->causeAction;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCauseAgent() {
+ return $this->causeAgent;
+ }
+
+ /**
* Convenience method, calls doUpdate() on every DataUpdate in the array.
*
* @param DataUpdate[] $updates A list of DataUpdate instances
diff --git a/www/wiki/includes/deferred/DeferredUpdates.php b/www/wiki/includes/deferred/DeferredUpdates.php
index e8e250b5..9b25d538 100644
--- a/www/wiki/includes/deferred/DeferredUpdates.php
+++ b/www/wiki/includes/deferred/DeferredUpdates.php
@@ -106,10 +106,10 @@ class DeferredUpdates {
*
* @param callable $callable
* @param int $stage DeferredUpdates constant (PRESEND or POSTSEND) (since 1.27)
- * @param IDatabase|null $dbw Abort if this DB is rolled back [optional] (since 1.28)
+ * @param IDatabase|IDatabase[]|null $dbw Abort if this DB is rolled back [optional] (since 1.28)
*/
public static function addCallableUpdate(
- $callable, $stage = self::POSTSEND, IDatabase $dbw = null
+ $callable, $stage = self::POSTSEND, $dbw = null
) {
self::addUpdate( new MWCallableUpdate( $callable, wfGetCaller(), $dbw ), $stage );
}
@@ -250,6 +250,8 @@ class DeferredUpdates {
// Run only the job enqueue logic to complete the update later
$spec = $update->getAsJobSpecification();
JobQueueGroup::singleton( $spec['wiki'] )->push( $spec['job'] );
+ } elseif ( $update instanceof TransactionRoundDefiningUpdate ) {
+ $update->doUpdate();
} else {
// Run the bulk of the update now
$fnameTrxOwner = get_class( $update ) . '::doUpdate';
diff --git a/www/wiki/includes/deferred/HTMLCacheUpdate.php b/www/wiki/includes/deferred/HTMLCacheUpdate.php
index db3790f7..29846bfb 100644
--- a/www/wiki/includes/deferred/HTMLCacheUpdate.php
+++ b/www/wiki/includes/deferred/HTMLCacheUpdate.php
@@ -26,7 +26,7 @@
*
* @ingroup Cache
*/
-class HTMLCacheUpdate implements DeferrableUpdate {
+class HTMLCacheUpdate extends DataUpdate {
/** @var Title */
public $mTitle;
@@ -36,14 +36,24 @@ class HTMLCacheUpdate implements DeferrableUpdate {
/**
* @param Title $titleTo
* @param string $table
+ * @param string $causeAction Triggering action
+ * @param string $causeAgent Triggering user
*/
- function __construct( Title $titleTo, $table ) {
+ function __construct(
+ Title $titleTo, $table, $causeAction = 'unknown', $causeAgent = 'unknown'
+ ) {
$this->mTitle = $titleTo;
$this->mTable = $table;
+ $this->causeAction = $causeAction;
+ $this->causeAgent = $causeAgent;
}
public function doUpdate() {
- $job = HTMLCacheUpdateJob::newForBacklinks( $this->mTitle, $this->mTable );
+ $job = HTMLCacheUpdateJob::newForBacklinks(
+ $this->mTitle,
+ $this->mTable,
+ [ 'causeAction' => $this->getCauseAction(), 'causeAgent' => $this->getCauseAgent() ]
+ );
JobQueueGroup::singleton()->lazyPush( $job );
}
diff --git a/www/wiki/includes/deferred/LinksUpdate.php b/www/wiki/includes/deferred/LinksUpdate.php
index dfe89ba3..89136428 100644
--- a/www/wiki/includes/deferred/LinksUpdate.php
+++ b/www/wiki/includes/deferred/LinksUpdate.php
@@ -306,10 +306,13 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
* using the job queue.
*/
protected function queueRecursiveJobs() {
- self::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
+ $action = $this->getCauseAction();
+ $agent = $this->getCauseAgent();
+
+ self::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks', $action, $agent );
if ( $this->mTitle->getNamespace() == NS_FILE ) {
// Process imagelinks in case the title is or was a redirect
- self::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
+ self::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks', $action, $agent );
}
$bc = $this->mTitle->getBacklinkCache();
@@ -320,7 +323,13 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
// Which ever runs first generally no-ops the other one.
$jobs = [];
foreach ( $bc->getCascadeProtectedLinks() as $title ) {
- $jobs[] = RefreshLinksJob::newPrioritized( $title, [] );
+ $jobs[] = RefreshLinksJob::newPrioritized(
+ $title,
+ [
+ 'causeAction' => $action,
+ 'causeAgent' => $agent
+ ]
+ );
}
JobQueueGroup::singleton()->push( $jobs );
}
@@ -330,8 +339,12 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
*
* @param Title $title Title to do job for
* @param string $table Table to use (e.g. 'templatelinks')
+ * @param string $action Triggering action
+ * @param string $userName Triggering user name
*/
- public static function queueRecursiveJobsForTable( Title $title, $table ) {
+ public static function queueRecursiveJobsForTable(
+ Title $title, $table, $action = 'unknown', $userName = 'unknown'
+ ) {
if ( $title->getBacklinkCache()->hasLinks( $table ) ) {
$job = new RefreshLinksJob(
$title,
@@ -340,7 +353,7 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
'recursive' => true,
] + Job::newRootJobParams( // "overall" refresh links job info
"refreshlinks:{$table}:{$title->getPrefixedText()}"
- )
+ ) + [ 'causeAction' => $action, 'causeAgent' => $userName ]
);
JobQueueGroup::singleton()->push( $job );
@@ -1042,7 +1055,9 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
$inv = [ $inv ];
}
foreach ( $inv as $table ) {
- DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->mTitle, $table ) );
+ DeferredUpdates::addUpdate(
+ new HTMLCacheUpdate( $this->mTitle, $table, 'page-props' )
+ );
}
}
}
@@ -1156,6 +1171,8 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
'useRecursiveLinksUpdate' => $this->mRecursive,
'triggeringUser' => $userInfo,
'triggeringRevisionId' => $triggeringRevisionId,
+ 'causeAction' => $this->getCauseAction(),
+ 'causeAgent' => $this->getCauseAgent()
],
[ 'removeDuplicates' => true ],
$this->getTitle()
diff --git a/www/wiki/includes/deferred/MWCallableUpdate.php b/www/wiki/includes/deferred/MWCallableUpdate.php
index 5b822af4..9803b7a4 100644
--- a/www/wiki/includes/deferred/MWCallableUpdate.php
+++ b/www/wiki/includes/deferred/MWCallableUpdate.php
@@ -14,14 +14,18 @@ class MWCallableUpdate implements DeferrableUpdate, DeferrableCallback {
/**
* @param callable $callback
* @param string $fname Calling method
- * @param IDatabase|null $dbw Abort if this DB is rolled back [optional] (since 1.28)
+ * @param IDatabase|IDatabase[]|null $dbws Abort if any of the specified DB handles have
+ * a currently pending transaction which later gets rolled back [optional] (since 1.28)
*/
- public function __construct( callable $callback, $fname = 'unknown', IDatabase $dbw = null ) {
+ public function __construct( callable $callback, $fname = 'unknown', $dbws = [] ) {
$this->callback = $callback;
$this->fname = $fname;
- if ( $dbw && $dbw->trxLevel() ) {
- $dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ], $fname );
+ $dbws = is_array( $dbws ) ? $dbws : [ $dbws ];
+ foreach ( $dbws as $dbw ) {
+ if ( $dbw && $dbw->trxLevel() ) {
+ $dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ], $fname );
+ }
}
}
diff --git a/www/wiki/includes/deferred/MergeableUpdate.php b/www/wiki/includes/deferred/MergeableUpdate.php
index 70760ce4..8eeef13b 100644
--- a/www/wiki/includes/deferred/MergeableUpdate.php
+++ b/www/wiki/includes/deferred/MergeableUpdate.php
@@ -6,7 +6,7 @@
*
* @since 1.27
*/
-interface MergeableUpdate {
+interface MergeableUpdate extends DeferrableUpdate {
/**
* Merge this update with $update
*
diff --git a/www/wiki/includes/deferred/SiteStatsUpdate.php b/www/wiki/includes/deferred/SiteStatsUpdate.php
index 2f074ba2..7cb29509 100644
--- a/www/wiki/includes/deferred/SiteStatsUpdate.php
+++ b/www/wiki/includes/deferred/SiteStatsUpdate.php
@@ -25,6 +25,8 @@ use Wikimedia\Rdbms\IDatabase;
* Class for handling updates to the site_stats table
*/
class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
+ /** @var BagOStuff */
+ protected $stash;
/** @var int */
protected $edits = 0;
/** @var int */
@@ -44,6 +46,8 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
$this->articles = $good;
$this->pages = $pages;
$this->users = $users;
+
+ $this->stash = MediaWikiServices::getInstance()->getMainObjectStash();
}
public function merge( MergeableUpdate $update ) {
@@ -62,6 +66,12 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
public static function factory( array $deltas ) {
$update = new self( 0, 0, 0 );
+ foreach ( $deltas as $name => $unused ) {
+ if ( !in_array( $name, self::$counters ) ) { // T187585
+ throw new UnexpectedValueException( __METHOD__ . ": no field called '$name'" );
+ }
+ }
+
foreach ( self::$counters as $field ) {
if ( isset( $deltas[$field] ) && $deltas[$field] ) {
$update->$field = $deltas[$field];
@@ -72,11 +82,9 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
}
public function doUpdate() {
- global $wgSiteStatsAsyncFactor;
-
$this->doUpdateContextStats();
- $rate = $wgSiteStatsAsyncFactor; // convenience
+ $rate = MediaWikiServices::getInstance()->getMainConfig()->get( 'SiteStatsAsyncFactor' );
// If set to do so, only do actual DB updates 1 every $rate times.
// The other times, just update "pending delta" values in memcached.
if ( $rate && ( $rate < 0 || mt_rand( 0, $rate - 1 ) != 0 ) ) {
@@ -91,16 +99,15 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
* Do not call this outside of SiteStatsUpdate
*/
public function tryDBUpdateInternal() {
- global $wgSiteStatsAsyncFactor;
+ $services = MediaWikiServices::getInstance();
+ $config = $services->getMainConfig();
- $dbw = wfGetDB( DB_MASTER );
- $lockKey = wfWikiID() . ':site_stats'; // prepend wiki ID
+ $dbw = $services->getDBLoadBalancer()->getConnection( DB_MASTER );
+ $lockKey = $dbw->getDomainID() . ':site_stats'; // prepend wiki ID
$pd = [];
- if ( $wgSiteStatsAsyncFactor ) {
+ if ( $config->get( 'SiteStatsAsyncFactor' ) ) {
// Lock the table so we don't have double DB/memcached updates
- if ( !$dbw->lockIsFree( $lockKey, __METHOD__ )
- || !$dbw->lock( $lockKey, __METHOD__, 1 ) // 1 sec timeout
- ) {
+ if ( !$dbw->lock( $lockKey, __METHOD__, 0 ) ) {
$this->doUpdatePendingDeltas();
return;
@@ -125,7 +132,7 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
$dbw->update( 'site_stats', [ $updates ], [], __METHOD__ );
}
- if ( $wgSiteStatsAsyncFactor ) {
+ if ( $config->get( 'SiteStatsAsyncFactor' ) ) {
// Decrement the async deltas now that we applied them
$this->removePendingDeltas( $pd );
// Commit the updates and unlock the table
@@ -140,22 +147,28 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
* @param IDatabase $dbw
* @return bool|mixed
*/
- public static function cacheUpdate( $dbw ) {
- global $wgActiveUserDays;
- $dbr = wfGetDB( DB_REPLICA, 'vslow' );
+ public static function cacheUpdate( IDatabase $dbw ) {
+ $services = MediaWikiServices::getInstance();
+ $config = $services->getMainConfig();
+
+ $dbr = $services->getDBLoadBalancer()->getConnection( DB_REPLICA, 'vslow' );
# Get non-bot users than did some recent action other than making accounts.
# If account creation is included, the number gets inflated ~20+ fold on enwiki.
+ $rcQuery = RecentChange::getQueryInfo();
$activeUsers = $dbr->selectField(
- 'recentchanges',
- 'COUNT( DISTINCT rc_user_text )',
+ $rcQuery['tables'],
+ 'COUNT( DISTINCT ' . $rcQuery['fields']['rc_user_text'] . ' )',
[
- 'rc_user != 0',
+ 'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Exclude external (Wikidata)
+ ActorMigration::newMigration()->isNotAnon( $rcQuery['fields']['rc_user'] ),
'rc_bot' => 0,
'rc_log_type != ' . $dbr->addQuotes( 'newusers' ) . ' OR rc_log_type IS NULL',
- 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( wfTimestamp( TS_UNIX )
- - $wgActiveUserDays * 24 * 3600 ) ),
+ 'rc_timestamp >= ' . $dbr->addQuotes(
+ $dbr->timestamp( time() - $config->get( 'ActiveUserDays' ) * 24 * 3600 ) ),
],
- __METHOD__
+ __METHOD__,
+ [],
+ $rcQuery['joins']
);
$dbw->update(
'site_stats',
@@ -207,13 +220,13 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
}
/**
- * @param BagOStuff $cache
+ * @param BagOStuff $stash
* @param string $type
* @param string $sign ('+' or '-')
* @return string
*/
- private function getTypeCacheKey( BagOStuff $cache, $type, $sign ) {
- return $cache->makeKey( 'sitestatsupdate', 'pendingdelta', $type, $sign );
+ private function getTypeCacheKey( BagOStuff $stash, $type, $sign ) {
+ return $stash->makeKey( 'sitestatsupdate', 'pendingdelta', $type, $sign );
}
/**
@@ -223,15 +236,14 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
* @param int $delta Delta (positive or negative)
*/
protected function adjustPending( $type, $delta ) {
- $cache = MediaWikiServices::getInstance()->getMainObjectStash();
if ( $delta < 0 ) { // decrement
- $key = $this->getTypeCacheKey( $cache, $type, '-' );
+ $key = $this->getTypeCacheKey( $this->stash, $type, '-' );
} else { // increment
- $key = $this->getTypeCacheKey( $cache, $type, '+' );
+ $key = $this->getTypeCacheKey( $this->stash, $type, '+' );
}
$magnitude = abs( $delta );
- $cache->incrWithInit( $key, 0, $magnitude, $magnitude );
+ $this->stash->incrWithInit( $key, 0, $magnitude, $magnitude );
}
/**
@@ -239,16 +251,20 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
* @return array Positive and negative deltas for each type
*/
protected function getPendingDeltas() {
- $cache = MediaWikiServices::getInstance()->getMainObjectStash();
-
$pending = [];
foreach ( [ 'ss_total_edits',
'ss_good_articles', 'ss_total_pages', 'ss_users', 'ss_images' ] as $type
) {
// Get pending increments and pending decrements
$flg = BagOStuff::READ_LATEST;
- $pending[$type]['+'] = (int)$cache->get( $this->getTypeCacheKey( $cache, $type, '+' ), $flg );
- $pending[$type]['-'] = (int)$cache->get( $this->getTypeCacheKey( $cache, $type, '-' ), $flg );
+ $pending[$type]['+'] = (int)$this->stash->get(
+ $this->getTypeCacheKey( $this->stash, $type, '+' ),
+ $flg
+ );
+ $pending[$type]['-'] = (int)$this->stash->get(
+ $this->getTypeCacheKey( $this->stash, $type, '-' ),
+ $flg
+ );
}
return $pending;
@@ -259,12 +275,11 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
* @param array $pd Result of getPendingDeltas(), used for DB update
*/
protected function removePendingDeltas( array $pd ) {
- $cache = MediaWikiServices::getInstance()->getMainObjectStash();
-
foreach ( $pd as $type => $deltas ) {
foreach ( $deltas as $sign => $magnitude ) {
// Lower the pending counter now that we applied these changes
- $cache->decr( $this->getTypeCacheKey( $cache, $type, $sign ), $magnitude );
+ $key = $this->getTypeCacheKey( $this->stash, $type, $sign );
+ $this->stash->decr( $key, $magnitude );
}
}
}
diff --git a/www/wiki/includes/deferred/TransactionRoundDefiningUpdate.php b/www/wiki/includes/deferred/TransactionRoundDefiningUpdate.php
new file mode 100644
index 00000000..a32d4a07
--- /dev/null
+++ b/www/wiki/includes/deferred/TransactionRoundDefiningUpdate.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * Deferrable update that must run outside of any explicit LBFactory transaction round
+ *
+ * @since 1.31
+ */
+class TransactionRoundDefiningUpdate implements DeferrableUpdate, DeferrableCallback {
+ /** @var callable|null */
+ private $callback;
+ /** @var string */
+ private $fname;
+
+ /**
+ * @param callable $callback
+ * @param string $fname Calling method
+ */
+ public function __construct( callable $callback, $fname = 'unknown' ) {
+ $this->callback = $callback;
+ $this->fname = $fname;
+ }
+
+ public function doUpdate() {
+ call_user_func( $this->callback );
+ }
+
+ public function getOrigin() {
+ return $this->fname;
+ }
+}
diff --git a/www/wiki/includes/diff/DifferenceEngine.php b/www/wiki/includes/diff/DifferenceEngine.php
index a893fe88..8f57c578 100644
--- a/www/wiki/includes/diff/DifferenceEngine.php
+++ b/www/wiki/includes/diff/DifferenceEngine.php
@@ -21,9 +21,7 @@
* @ingroup DifferenceEngine
*/
use MediaWiki\MediaWikiServices;
-
-/** @deprecated use class constant instead */
-define( 'MW_DIFF_VERSION', '1.11a' );
+use MediaWiki\Shell\Shell;
/**
* @todo document
@@ -36,7 +34,7 @@ class DifferenceEngine extends ContextSource {
* fixes important bugs or such to force cached diff views to
* clear.
*/
- const DIFF_VERSION = MW_DIFF_VERSION;
+ const DIFF_VERSION = '1.12';
/** @var int */
public $mOldid;
@@ -181,13 +179,15 @@ class DifferenceEngine extends ContextSource {
public function deletedLink( $id ) {
if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
$dbr = wfGetDB( DB_REPLICA );
- $row = $dbr->selectRow( 'archive',
- array_merge(
- Revision::selectArchiveFields(),
- [ 'ar_namespace', 'ar_title' ]
- ),
+ $arQuery = Revision::getArchiveQueryInfo();
+ $row = $dbr->selectRow(
+ $arQuery['tables'],
+ array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
[ 'ar_rev_id' => $id ],
- __METHOD__ );
+ __METHOD__,
+ [],
+ $arQuery['joins']
+ );
if ( $row ) {
$rev = Revision::newFromArchiveRow( $row );
$title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
@@ -542,7 +542,7 @@ class DifferenceEngine extends ContextSource {
[
'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
'rc_this_oldid' => $this->mNewid,
- 'rc_patrolled' => 0
+ 'rc_patrolled' => RecentChange::PRC_UNPATROLLED
],
__METHOD__
);
@@ -604,14 +604,15 @@ class DifferenceEngine extends ContextSource {
$out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
<h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
# Page content may be handled by a hooked call instead...
- # @codingStandardsIgnoreStart Ignoring long lines.
if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
$this->loadNewText();
$out->setRevisionId( $this->mNewid );
$out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
$out->setArticleFlag( true );
- if ( !Hooks::run( 'ArticleContentViewCustom', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
+ if ( !Hooks::run( 'ArticleContentViewCustom',
+ [ $this->mNewContent, $this->mNewPage, $out ] )
+ ) {
// Handled by extension
} else {
// Normal page
@@ -630,13 +631,17 @@ class DifferenceEngine extends ContextSource {
# WikiPage::getParserOutput() should not return false, but just in case
if ( $parserOutput ) {
// Allow extensions to change parser output here
- if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput', [ $this, $out, $parserOutput, $wikiPage ] ) ) {
- $out->addParserOutput( $parserOutput );
+ if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput',
+ [ $this, $out, $parserOutput, $wikiPage ] )
+ ) {
+ $out->addParserOutput( $parserOutput, [
+ 'enableSectionEditLinks' => $this->mNewRev->isCurrent()
+ && $this->mNewRev->getTitle()->quickUserCan( 'edit', $this->getUser() ),
+ ] );
}
}
}
}
- # @codingStandardsIgnoreEnd
// Allow extensions to optionally not show the final patrolled link
if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
@@ -645,13 +650,14 @@ class DifferenceEngine extends ContextSource {
}
}
+ /**
+ * @param WikiPage $page
+ * @param Revision $rev
+ *
+ * @return ParserOutput|bool False if the revision was not found
+ */
protected function getParserOutput( WikiPage $page, Revision $rev ) {
$parserOptions = $page->makeParserOptions( $this->getContext() );
-
- if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( 'edit', $this->getUser() ) ) {
- $parserOptions->setEditSection( false );
- }
-
$parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
return $parserOutput;
@@ -748,14 +754,22 @@ class DifferenceEngine extends ContextSource {
$key = false;
$cache = ObjectCache::getMainWANInstance();
if ( $this->mOldid && $this->mNewid ) {
+ // Check if subclass is still using the old way
+ // for backwards-compatibility
$key = $this->getDiffBodyCacheKey();
+ if ( $key === null ) {
+ $key = call_user_func_array(
+ [ $cache, 'makeKey' ],
+ $this->getDiffBodyCacheKeyParams()
+ );
+ }
// Try cache
if ( !$this->mRefreshCache ) {
$difftext = $cache->get( $key );
if ( $difftext ) {
wfIncrStats( 'diff_cache.hit' );
- $difftext = $this->localiseLineNumbers( $difftext );
+ $difftext = $this->localiseDiff( $difftext );
$difftext .= "\n<!-- diff cache key $key -->\n";
return $difftext;
@@ -783,9 +797,9 @@ class DifferenceEngine extends ContextSource {
} else {
wfIncrStats( 'diff_cache.uncacheable' );
}
- // Replace line numbers with the text in the user's language
+ // localise line numbers and title attribute text
if ( $difftext !== false ) {
- $difftext = $this->localiseLineNumbers( $difftext );
+ $difftext = $this->localiseDiff( $difftext );
}
return $difftext;
@@ -794,18 +808,49 @@ class DifferenceEngine extends ContextSource {
/**
* Returns the cache key for diff body text or content.
*
+ * @deprecated since 1.31, use getDiffBodyCacheKeyParams() instead
* @since 1.23
*
* @throws MWException
- * @return string
+ * @return string|null
*/
protected function getDiffBodyCacheKey() {
+ return null;
+ }
+
+ /**
+ * Get the cache key parameters
+ *
+ * Subclasses can replace the first element in the array to something
+ * more specific to the type of diff (e.g. "inline-diff"), or append
+ * if the cache should vary on more things. Overriding entirely should
+ * be avoided.
+ *
+ * @since 1.31
+ *
+ * @return array
+ * @throws MWException
+ */
+ protected function getDiffBodyCacheKeyParams() {
if ( !$this->mOldid || !$this->mNewid ) {
throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
}
- return wfMemcKey( 'diff', 'version', self::DIFF_VERSION,
- 'oldid', $this->mOldid, 'newid', $this->mNewid );
+ $engine = $this->getEngine();
+ $params = [
+ 'diff',
+ $engine,
+ self::DIFF_VERSION,
+ "old-{$this->mOldid}",
+ "rev-{$this->mNewid}"
+ ];
+
+ if ( $engine === 'wikidiff2' ) {
+ $params[] = phpversion( 'wikidiff2' );
+ $params[] = $this->getConfig()->get( 'WikiDiff2MovedParagraphDetectionCutoff' );
+ }
+
+ return $params;
}
/**
@@ -892,18 +937,14 @@ class DifferenceEngine extends ContextSource {
}
/**
- * Generates diff, to be wrapped internally in a logging/instrumentation
+ * Process $wgExternalDiffEngine and get a sane, usable engine
*
- * @param string $otext Old text, must be already segmented
- * @param string $ntext New text, must be already segmented
- * @return bool|string
+ * @return bool|string 'wikidiff2', path to an executable, or false
*/
- protected function textDiff( $otext, $ntext ) {
- global $wgExternalDiffEngine, $wgContLang;
-
- $otext = str_replace( "\r\n", "\n", $otext );
- $ntext = str_replace( "\r\n", "\n", $ntext );
-
+ private function getEngine() {
+ global $wgExternalDiffEngine;
+ // We use the global here instead of Config because we write to the value,
+ // and Config is not mutable.
if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
$wgExternalDiffEngine = false;
@@ -916,9 +957,34 @@ class DifferenceEngine extends ContextSource {
$wgExternalDiffEngine = false;
}
+ if ( is_string( $wgExternalDiffEngine ) && is_executable( $wgExternalDiffEngine ) ) {
+ return $wgExternalDiffEngine;
+ } elseif ( $wgExternalDiffEngine === false && function_exists( 'wikidiff2_do_diff' ) ) {
+ return 'wikidiff2';
+ } else {
+ // Native PHP
+ return false;
+ }
+ }
+
+ /**
+ * Generates diff, to be wrapped internally in a logging/instrumentation
+ *
+ * @param string $otext Old text, must be already segmented
+ * @param string $ntext New text, must be already segmented
+ * @return bool|string
+ */
+ protected function textDiff( $otext, $ntext ) {
+ global $wgContLang;
+
+ $otext = str_replace( "\r\n", "\n", $otext );
+ $ntext = str_replace( "\r\n", "\n", $ntext );
+
+ $engine = $this->getEngine();
+
// Better external diff engine, the 2 may some day be dropped
// This one does the escaping and segmenting itself
- if ( function_exists( 'wikidiff2_do_diff' ) && $wgExternalDiffEngine === false ) {
+ if ( $engine === 'wikidiff2' ) {
$wikidiff2Version = phpversion( 'wikidiff2' );
if (
$wikidiff2Version !== false &&
@@ -948,7 +1014,7 @@ class DifferenceEngine extends ContextSource {
$text .= $this->debug( 'wikidiff2' );
return $text;
- } elseif ( $wgExternalDiffEngine !== false && is_executable( $wgExternalDiffEngine ) ) {
+ } elseif ( $engine !== false ) {
# Diff via the shell
$tmpDir = wfTempDir();
$tempName1 = tempnam( $tmpDir, 'diff_' );
@@ -966,9 +1032,17 @@ class DifferenceEngine extends ContextSource {
fwrite( $tempFile2, $ntext );
fclose( $tempFile1 );
fclose( $tempFile2 );
- $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
- $difftext = wfShellExec( $cmd );
- $difftext .= $this->debug( "external $wgExternalDiffEngine" );
+ $cmd = [ $engine, $tempName1, $tempName2 ];
+ $result = Shell::command( $cmd )
+ ->execute();
+ $exitCode = $result->getExitCode();
+ if ( $exitCode !== 0 ) {
+ throw new Exception( "External diff command returned code {$exitCode}. Stderr: "
+ . wfEscapeWikiText( $result->getStderr() )
+ );
+ }
+ $difftext = $result->getStdout();
+ $difftext .= $this->debug( "external $engine" );
unlink( $tempName1 );
unlink( $tempName2 );
@@ -981,6 +1055,7 @@ class DifferenceEngine extends ContextSource {
$diffs = new Diff( $ota, $nta );
$formatter = new TableDiffFormatter();
$difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) );
+ $difftext .= $this->debug( 'native PHP' );
return $difftext;
}
@@ -1010,6 +1085,22 @@ class DifferenceEngine extends ContextSource {
}
/**
+ * Localise diff output
+ *
+ * @param string $text
+ * @return string
+ */
+ private function localiseDiff( $text ) {
+ $text = $this->localiseLineNumbers( $text );
+ if ( $this->getEngine() === 'wikidiff2' &&
+ version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
+ ) {
+ $text = $this->addLocalisedTitleTooltips( $text );
+ }
+ return $text;
+ }
+
+ /**
* Replace line numbers with the text in the user's language
*
* @param string $text
@@ -1033,6 +1124,31 @@ class DifferenceEngine extends ContextSource {
}
/**
+ * Add title attributes for tooltips on moved paragraph indicators
+ *
+ * @param string $text
+ * @return string
+ */
+ private function addLocalisedTitleTooltips( $text ) {
+ return preg_replace_callback(
+ '/class="mw-diff-movedpara-(left|right)"/',
+ [ $this, 'addLocalisedTitleTooltipsCb' ],
+ $text
+ );
+ }
+
+ /**
+ * @param array $matches
+ * @return string
+ */
+ private function addLocalisedTitleTooltipsCb( array $matches ) {
+ $key = $matches[1] === 'right' ?
+ 'diff-paragraph-moved-toold' :
+ 'diff-paragraph-moved-tonew';
+ return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
+ }
+
+ /**
* If there are revisions between the ones being compared, return a note saying so.
*
* @return string
@@ -1178,7 +1294,7 @@ class DifferenceEngine extends ContextSource {
if ( !$diff && !$otitle ) {
$header .= "
- <tr style=\"vertical-align: top;\" lang=\"{$userLang}\">
+ <tr class=\"diff-title\" lang=\"{$userLang}\">
<td class=\"diff-ntitle\">{$ntitle}</td>
</tr>";
$multiColspan = 1;
@@ -1197,7 +1313,7 @@ class DifferenceEngine extends ContextSource {
}
if ( $otitle || $ntitle ) {
$header .= "
- <tr style=\"vertical-align: top;\" lang=\"{$userLang}\">
+ <tr class=\"diff-title\" lang=\"{$userLang}\">
<td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
<td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
</tr>";
@@ -1205,12 +1321,12 @@ class DifferenceEngine extends ContextSource {
}
if ( $multi != '' ) {
- $header .= "<tr><td colspan=\"{$multiColspan}\" style=\"text-align: center;\" " .
+ $header .= "<tr><td colspan=\"{$multiColspan}\" " .
"class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
}
if ( $notice != '' ) {
- $header .= "<tr><td colspan=\"{$multiColspan}\" style=\"text-align: center;\" " .
- "lang=\"{$userLang}\">{$notice}</td></tr>";
+ $header .= "<tr><td colspan=\"{$multiColspan}\" " .
+ "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
}
return $header . $diff . "</table>";
diff --git a/www/wiki/includes/diff/TableDiffFormatter.php b/www/wiki/includes/diff/TableDiffFormatter.php
index 14307b58..67f9a79b 100644
--- a/www/wiki/includes/diff/TableDiffFormatter.php
+++ b/www/wiki/includes/diff/TableDiffFormatter.php
@@ -38,7 +38,6 @@ class TableDiffFormatter extends DiffFormatter {
}
/**
- * @static
* @param string $msg
*
* @return mixed
diff --git a/www/wiki/includes/diff/WikiDiff3.php b/www/wiki/includes/diff/WikiDiff3.php
deleted file mode 100644
index f35e30f3..00000000
--- a/www/wiki/includes/diff/WikiDiff3.php
+++ /dev/null
@@ -1,621 +0,0 @@
-<?php
-/**
- * New version of the difference engine
- *
- * Copyright © 2008 Guy Van den Broeck <guy@guyvdb.eu>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup DifferenceEngine
- */
-
-/**
- * This diff implementation is mainly lifted from the LCS algorithm of the Eclipse project which
- * in turn is based on Myers' "An O(ND) difference algorithm and its variations"
- * (http://citeseer.ist.psu.edu/myers86ond.html) with range compression (see Wu et al.'s
- * "An O(NP) Sequence Comparison Algorithm").
- *
- * This implementation supports an upper bound on the execution time.
- *
- * Complexity: O((M + N)D) worst case time, O(M + N + D^2) expected time, O(M + N) space
- *
- * @author Guy Van den Broeck
- * @ingroup DifferenceEngine
- */
-class WikiDiff3 {
-
- // Input variables
- private $from;
- private $to;
- private $m;
- private $n;
-
- private $tooLong;
- private $powLimit;
-
- // State variables
- private $maxDifferences;
- private $lcsLengthCorrectedForHeuristic = false;
-
- // Output variables
- public $length;
- public $removed;
- public $added;
- public $heuristicUsed;
-
- function __construct( $tooLong = 2000000, $powLimit = 1.45 ) {
- $this->tooLong = $tooLong;
- $this->powLimit = $powLimit;
- }
-
- public function diff( /*array*/ $from, /*array*/ $to ) {
- // remember initial lengths
- $m = count( $from );
- $n = count( $to );
-
- $this->heuristicUsed = false;
-
- // output
- $removed = $m > 0 ? array_fill( 0, $m, true ) : [];
- $added = $n > 0 ? array_fill( 0, $n, true ) : [];
-
- // reduce the complexity for the next step (intentionally done twice)
- // remove common tokens at the start
- $i = 0;
- while ( $i < $m && $i < $n && $from[$i] === $to[$i] ) {
- $removed[$i] = $added[$i] = false;
- unset( $from[$i], $to[$i] );
- ++$i;
- }
-
- // remove common tokens at the end
- $j = 1;
- while ( $i + $j <= $m && $i + $j <= $n && $from[$m - $j] === $to[$n - $j] ) {
- $removed[$m - $j] = $added[$n - $j] = false;
- unset( $from[$m - $j], $to[$n - $j] );
- ++$j;
- }
-
- $this->from = $newFromIndex = $this->to = $newToIndex = [];
-
- // remove tokens not in both sequences
- $shared = [];
- foreach ( $from as $key ) {
- $shared[$key] = false;
- }
-
- foreach ( $to as $index => &$el ) {
- if ( array_key_exists( $el, $shared ) ) {
- // keep it
- $this->to[] = $el;
- $shared[$el] = true;
- $newToIndex[] = $index;
- }
- }
- foreach ( $from as $index => &$el ) {
- if ( $shared[$el] ) {
- // keep it
- $this->from[] = $el;
- $newFromIndex[] = $index;
- }
- }
-
- unset( $shared, $from, $to );
-
- $this->m = count( $this->from );
- $this->n = count( $this->to );
-
- $this->removed = $this->m > 0 ? array_fill( 0, $this->m, true ) : [];
- $this->added = $this->n > 0 ? array_fill( 0, $this->n, true ) : [];
-
- if ( $this->m == 0 || $this->n == 0 ) {
- $this->length = 0;
- } else {
- $this->maxDifferences = ceil( ( $this->m + $this->n ) / 2.0 );
- if ( $this->m * $this->n > $this->tooLong ) {
- // limit complexity to D^POW_LIMIT for long sequences
- $this->maxDifferences = floor( pow( $this->maxDifferences, $this->powLimit - 1.0 ) );
- wfDebug( "Limiting max number of differences to $this->maxDifferences\n" );
- }
-
- /*
- * The common prefixes and suffixes are always part of some LCS, include
- * them now to reduce our search space
- */
- $max = min( $this->m, $this->n );
- for ( $forwardBound = 0; $forwardBound < $max
- && $this->from[$forwardBound] === $this->to[$forwardBound];
- ++$forwardBound
- ) {
- $this->removed[$forwardBound] = $this->added[$forwardBound] = false;
- }
-
- $backBoundL1 = $this->m - 1;
- $backBoundL2 = $this->n - 1;
-
- while ( $backBoundL1 >= $forwardBound && $backBoundL2 >= $forwardBound
- && $this->from[$backBoundL1] === $this->to[$backBoundL2]
- ) {
- $this->removed[$backBoundL1--] = $this->added[$backBoundL2--] = false;
- }
-
- $temp = array_fill( 0, $this->m + $this->n + 1, 0 );
- $V = [ $temp, $temp ];
- $snake = [ 0, 0, 0 ];
-
- $this->length = $forwardBound + $this->m - $backBoundL1 - 1
- + $this->lcs_rec(
- $forwardBound,
- $backBoundL1,
- $forwardBound,
- $backBoundL2,
- $V,
- $snake
- );
- }
-
- $this->m = $m;
- $this->n = $n;
-
- $this->length += $i + $j - 1;
-
- foreach ( $this->removed as $key => &$removed_elem ) {
- if ( !$removed_elem ) {
- $removed[$newFromIndex[$key]] = false;
- }
- }
- foreach ( $this->added as $key => &$added_elem ) {
- if ( !$added_elem ) {
- $added[$newToIndex[$key]] = false;
- }
- }
- $this->removed = $removed;
- $this->added = $added;
- }
-
- function diff_range( $from_lines, $to_lines ) {
- // Diff and store locally
- $this->diff( $from_lines, $to_lines );
- unset( $from_lines, $to_lines );
-
- $ranges = [];
- $xi = $yi = 0;
- while ( $xi < $this->m || $yi < $this->n ) {
- // Matching "snake".
- while ( $xi < $this->m && $yi < $this->n
- && !$this->removed[$xi]
- && !$this->added[$yi]
- ) {
- ++$xi;
- ++$yi;
- }
- // Find deletes & adds.
- $xstart = $xi;
- while ( $xi < $this->m && $this->removed[$xi] ) {
- ++$xi;
- }
-
- $ystart = $yi;
- while ( $yi < $this->n && $this->added[$yi] ) {
- ++$yi;
- }
-
- if ( $xi > $xstart || $yi > $ystart ) {
- $ranges[] = new RangeDifference( $xstart, $xi, $ystart, $yi );
- }
- }
-
- return $ranges;
- }
-
- private function lcs_rec( $bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake ) {
- // check that both sequences are non-empty
- if ( $bottoml1 > $topl1 || $bottoml2 > $topl2 ) {
- return 0;
- }
-
- $d = $this->find_middle_snake( $bottoml1, $topl1, $bottoml2,
- $topl2, $V, $snake );
-
- // need to store these so we don't lose them when they're
- // overwritten by the recursion
- $len = $snake[2];
- $startx = $snake[0];
- $starty = $snake[1];
-
- // the middle snake is part of the LCS, store it
- for ( $i = 0; $i < $len; ++$i ) {
- $this->removed[$startx + $i] = $this->added[$starty + $i] = false;
- }
-
- if ( $d > 1 ) {
- return $len
- + $this->lcs_rec( $bottoml1, $startx - 1, $bottoml2,
- $starty - 1, $V, $snake )
- + $this->lcs_rec( $startx + $len, $topl1, $starty + $len,
- $topl2, $V, $snake );
- } elseif ( $d == 1 ) {
- /*
- * In this case the sequences differ by exactly 1 line. We have
- * already saved all the lines after the difference in the for loop
- * above, now we need to save all the lines before the difference.
- */
- $max = min( $startx - $bottoml1, $starty - $bottoml2 );
- for ( $i = 0; $i < $max; ++$i ) {
- $this->removed[$bottoml1 + $i] =
- $this->added[$bottoml2 + $i] = false;
- }
-
- return $max + $len;
- }
-
- return $len;
- }
-
- private function find_middle_snake( $bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake ) {
- $from = &$this->from;
- $to = &$this->to;
- $V0 = &$V[0];
- $V1 = &$V[1];
- $snake0 = &$snake[0];
- $snake1 = &$snake[1];
- $snake2 = &$snake[2];
- $bottoml1_min_1 = $bottoml1 - 1;
- $bottoml2_min_1 = $bottoml2 - 1;
- $N = $topl1 - $bottoml1_min_1;
- $M = $topl2 - $bottoml2_min_1;
- $delta = $N - $M;
- $maxabsx = $N + $bottoml1;
- $maxabsy = $M + $bottoml2;
- $limit = min( $this->maxDifferences, ceil( ( $N + $M ) / 2 ) );
-
- // value_to_add_forward: a 0 or 1 that we add to the start
- // offset to make it odd/even
- if ( ( $M & 1 ) == 1 ) {
- $value_to_add_forward = 1;
- } else {
- $value_to_add_forward = 0;
- }
-
- if ( ( $N & 1 ) == 1 ) {
- $value_to_add_backward = 1;
- } else {
- $value_to_add_backward = 0;
- }
-
- $start_forward = -$M;
- $end_forward = $N;
- $start_backward = -$N;
- $end_backward = $M;
-
- $limit_min_1 = $limit - 1;
- $limit_plus_1 = $limit + 1;
-
- $V0[$limit_plus_1] = 0;
- $V1[$limit_min_1] = $N;
- $limit = min( $this->maxDifferences, ceil( ( $N + $M ) / 2 ) );
-
- if ( ( $delta & 1 ) == 1 ) {
- for ( $d = 0; $d <= $limit; ++$d ) {
- $start_diag = max( $value_to_add_forward + $start_forward, -$d );
- $end_diag = min( $end_forward, $d );
- $value_to_add_forward = 1 - $value_to_add_forward;
-
- // compute forward furthest reaching paths
- for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) {
- if ( $k == -$d || ( $k < $d
- && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] )
- ) {
- $x = $V0[$limit_plus_1 + $k];
- } else {
- $x = $V0[$limit_min_1 + $k] + 1;
- }
-
- $absx = $snake0 = $x + $bottoml1;
- $absy = $snake1 = $x - $k + $bottoml2;
-
- while ( $absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy] ) {
- ++$absx;
- ++$absy;
- }
- $x = $absx - $bottoml1;
-
- $snake2 = $absx - $snake0;
- $V0[$limit + $k] = $x;
- if ( $k >= $delta - $d + 1 && $k <= $delta + $d - 1
- && $x >= $V1[$limit + $k - $delta]
- ) {
- return 2 * $d - 1;
- }
-
- // check to see if we can cut down the diagonal range
- if ( $x >= $N && $end_forward > $k - 1 ) {
- $end_forward = $k - 1;
- } elseif ( $absy - $bottoml2 >= $M ) {
- $start_forward = $k + 1;
- $value_to_add_forward = 0;
- }
- }
-
- $start_diag = max( $value_to_add_backward + $start_backward, -$d );
- $end_diag = min( $end_backward, $d );
- $value_to_add_backward = 1 - $value_to_add_backward;
-
- // compute backward furthest reaching paths
- for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) {
- if ( $k == $d
- || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] )
- ) {
- $x = $V1[$limit_min_1 + $k];
- } else {
- $x = $V1[$limit_plus_1 + $k] - 1;
- }
-
- $y = $x - $k - $delta;
-
- $snake2 = 0;
- while ( $x > 0 && $y > 0
- && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1]
- ) {
- --$x;
- --$y;
- ++$snake2;
- }
- $V1[$limit + $k] = $x;
-
- // check to see if we can cut down our diagonal range
- if ( $x <= 0 ) {
- $start_backward = $k + 1;
- $value_to_add_backward = 0;
- } elseif ( $y <= 0 && $end_backward > $k - 1 ) {
- $end_backward = $k - 1;
- }
- }
- }
- } else {
- for ( $d = 0; $d <= $limit; ++$d ) {
- $start_diag = max( $value_to_add_forward + $start_forward, -$d );
- $end_diag = min( $end_forward, $d );
- $value_to_add_forward = 1 - $value_to_add_forward;
-
- // compute forward furthest reaching paths
- for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) {
- if ( $k == -$d
- || ( $k < $d && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] )
- ) {
- $x = $V0[$limit_plus_1 + $k];
- } else {
- $x = $V0[$limit_min_1 + $k] + 1;
- }
-
- $absx = $snake0 = $x + $bottoml1;
- $absy = $snake1 = $x - $k + $bottoml2;
-
- while ( $absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy] ) {
- ++$absx;
- ++$absy;
- }
- $x = $absx - $bottoml1;
- $snake2 = $absx - $snake0;
- $V0[$limit + $k] = $x;
-
- // check to see if we can cut down the diagonal range
- if ( $x >= $N && $end_forward > $k - 1 ) {
- $end_forward = $k - 1;
- } elseif ( $absy - $bottoml2 >= $M ) {
- $start_forward = $k + 1;
- $value_to_add_forward = 0;
- }
- }
-
- $start_diag = max( $value_to_add_backward + $start_backward, -$d );
- $end_diag = min( $end_backward, $d );
- $value_to_add_backward = 1 - $value_to_add_backward;
-
- // compute backward furthest reaching paths
- for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) {
- if ( $k == $d
- || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] )
- ) {
- $x = $V1[$limit_min_1 + $k];
- } else {
- $x = $V1[$limit_plus_1 + $k] - 1;
- }
-
- $y = $x - $k - $delta;
-
- $snake2 = 0;
- while ( $x > 0 && $y > 0
- && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1]
- ) {
- --$x;
- --$y;
- ++$snake2;
- }
- $V1[$limit + $k] = $x;
-
- if ( $k >= -$delta - $d && $k <= $d - $delta
- && $x <= $V0[$limit + $k + $delta]
- ) {
- $snake0 = $bottoml1 + $x;
- $snake1 = $bottoml2 + $y;
-
- return 2 * $d;
- }
-
- // check to see if we can cut down our diagonal range
- if ( $x <= 0 ) {
- $start_backward = $k + 1;
- $value_to_add_backward = 0;
- } elseif ( $y <= 0 && $end_backward > $k - 1 ) {
- $end_backward = $k - 1;
- }
- }
- }
- }
- /*
- * computing the true LCS is too expensive, instead find the diagonal
- * with the most progress and pretend a midle snake of length 0 occurs
- * there.
- */
-
- $most_progress = self::findMostProgress( $M, $N, $limit, $V );
-
- $snake0 = $bottoml1 + $most_progress[0];
- $snake1 = $bottoml2 + $most_progress[1];
- $snake2 = 0;
- wfDebug( "Computing the LCS is too expensive. Using a heuristic.\n" );
- $this->heuristicUsed = true;
-
- return 5; /*
- * HACK: since we didn't really finish the LCS computation
- * we don't really know the length of the SES. We don't do
- * anything with the result anyway, unless it's <=1. We know
- * for a fact SES > 1 so 5 is as good a number as any to
- * return here
- */
- }
-
- private static function findMostProgress( $M, $N, $limit, $V ) {
- $delta = $N - $M;
-
- if ( ( $M & 1 ) == ( $limit & 1 ) ) {
- $forward_start_diag = max( -$M, -$limit );
- } else {
- $forward_start_diag = max( 1 - $M, -$limit );
- }
-
- $forward_end_diag = min( $N, $limit );
-
- if ( ( $N & 1 ) == ( $limit & 1 ) ) {
- $backward_start_diag = max( -$N, -$limit );
- } else {
- $backward_start_diag = max( 1 - $N, -$limit );
- }
-
- $backward_end_diag = -min( $M, $limit );
-
- $temp = [ 0, 0, 0 ];
-
- $max_progress = array_fill( 0, ceil( max( $forward_end_diag - $forward_start_diag,
- $backward_end_diag - $backward_start_diag ) / 2 ), $temp );
- $num_progress = 0; // the 1st entry is current, it is initialized
- // with 0s
-
- // first search the forward diagonals
- for ( $k = $forward_start_diag; $k <= $forward_end_diag; $k += 2 ) {
- $x = $V[0][$limit + $k];
- $y = $x - $k;
- if ( $x > $N || $y > $M ) {
- continue;
- }
-
- $progress = $x + $y;
- if ( $progress > $max_progress[0][2] ) {
- $num_progress = 0;
- $max_progress[0][0] = $x;
- $max_progress[0][1] = $y;
- $max_progress[0][2] = $progress;
- } elseif ( $progress == $max_progress[0][2] ) {
- ++$num_progress;
- $max_progress[$num_progress][0] = $x;
- $max_progress[$num_progress][1] = $y;
- $max_progress[$num_progress][2] = $progress;
- }
- }
-
- $max_progress_forward = true; // initially the maximum
- // progress is in the forward
- // direction
-
- // now search the backward diagonals
- for ( $k = $backward_start_diag; $k <= $backward_end_diag; $k += 2 ) {
- $x = $V[1][$limit + $k];
- $y = $x - $k - $delta;
- if ( $x < 0 || $y < 0 ) {
- continue;
- }
-
- $progress = $N - $x + $M - $y;
- if ( $progress > $max_progress[0][2] ) {
- $num_progress = 0;
- $max_progress_forward = false;
- $max_progress[0][0] = $x;
- $max_progress[0][1] = $y;
- $max_progress[0][2] = $progress;
- } elseif ( $progress == $max_progress[0][2] && !$max_progress_forward ) {
- ++$num_progress;
- $max_progress[$num_progress][0] = $x;
- $max_progress[$num_progress][1] = $y;
- $max_progress[$num_progress][2] = $progress;
- }
- }
-
- // return the middle diagonal with maximal progress.
- return $max_progress[(int)floor( $num_progress / 2 )];
- }
-
- /**
- * @return mixed
- */
- public function getLcsLength() {
- if ( $this->heuristicUsed && !$this->lcsLengthCorrectedForHeuristic ) {
- $this->lcsLengthCorrectedForHeuristic = true;
- $this->length = $this->m - array_sum( $this->added );
- }
-
- return $this->length;
- }
-
-}
-
-/**
- * Alternative representation of a set of changes, by the index
- * ranges that are changed.
- *
- * @ingroup DifferenceEngine
- */
-class RangeDifference {
-
- /** @var int */
- public $leftstart;
-
- /** @var int */
- public $leftend;
-
- /** @var int */
- public $leftlength;
-
- /** @var int */
- public $rightstart;
-
- /** @var int */
- public $rightend;
-
- /** @var int */
- public $rightlength;
-
- function __construct( $leftstart, $leftend, $rightstart, $rightend ) {
- $this->leftstart = $leftstart;
- $this->leftend = $leftend;
- $this->leftlength = $leftend - $leftstart;
- $this->rightstart = $rightstart;
- $this->rightend = $rightend;
- $this->rightlength = $rightend - $rightstart;
- }
-
-}
diff --git a/www/wiki/includes/edit/PreparedEdit.php b/www/wiki/includes/edit/PreparedEdit.php
index 62624f4d..910d221a 100644
--- a/www/wiki/includes/edit/PreparedEdit.php
+++ b/www/wiki/includes/edit/PreparedEdit.php
@@ -87,27 +87,4 @@ class PreparedEdit {
*/
public $oldContent;
- /**
- * $newContent in text form
- *
- * @var string
- * @deprecated since 1.21
- */
- public $newText;
-
- /**
- * $oldContent in text from
- *
- * @var string
- * @deprecated since 1.21
- */
- public $oldText;
-
- /**
- * $pstContent in text form
- *
- * @var string
- * @deprecated since 1.21
- */
- public $pst;
}
diff --git a/www/wiki/includes/editpage/TextConflictHelper.php b/www/wiki/includes/editpage/TextConflictHelper.php
new file mode 100644
index 00000000..b447b18f
--- /dev/null
+++ b/www/wiki/includes/editpage/TextConflictHelper.php
@@ -0,0 +1,257 @@
+<?php
+/**
+ * Helper for displaying edit conflicts to users
+ *
+ * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\EditPage;
+
+use Content;
+use ContentHandler;
+use Html;
+use IBufferingStatsdDataFactory;
+use OutputPage;
+use Title;
+
+/**
+ * Helper for displaying edit conflicts in text content
+ * models to users
+ *
+ * @since 1.31
+ */
+class TextConflictHelper {
+
+ /**
+ * @var Title
+ */
+ protected $title;
+
+ /**
+ * @var null|string
+ */
+ public $contentModel = null;
+
+ /**
+ * @var null|string
+ */
+ public $contentFormat = null;
+
+ /**
+ * @var OutputPage
+ */
+ protected $out;
+
+ /**
+ * @var IBufferingStatsdDataFactory
+ */
+ protected $stats;
+
+ /**
+ * @var string Message key for submit button's label
+ */
+ protected $submitLabel;
+
+ /**
+ * @var string
+ */
+ protected $yourtext = '';
+
+ /**
+ * @var string
+ */
+ protected $storedversion = '';
+
+ /**
+ * @param Title $title
+ * @param OutputPage $out
+ * @param IBufferingStatsdDataFactory $stats
+ * @param string $submitLabel
+ */
+ public function __construct( Title $title, OutputPage $out, IBufferingStatsdDataFactory $stats,
+ $submitLabel
+ ) {
+ $this->title = $title;
+ $this->out = $out;
+ $this->stats = $stats;
+ $this->submitLabel = $submitLabel;
+ $this->contentModel = $title->getContentModel();
+ $this->contentFormat = ContentHandler::getForModelID( $this->contentModel )->getDefaultFormat();
+ }
+
+ /**
+ * @param string $yourtext
+ * @param string $storedversion
+ */
+ public function setTextboxes( $yourtext, $storedversion ) {
+ $this->yourtext = $yourtext;
+ $this->storedversion = $storedversion;
+ }
+
+ /**
+ * @param string $contentModel
+ */
+ public function setContentModel( $contentModel ) {
+ $this->contentModel = $contentModel;
+ }
+
+ /**
+ * @param string $contentFormat
+ */
+ public function setContentFormat( $contentFormat ) {
+ $this->contentFormat = $contentFormat;
+ }
+
+ /**
+ * Record a user encountering an edit conflict
+ */
+ public function incrementConflictStats() {
+ $this->stats->increment( 'edit.failures.conflict' );
+ // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
+ if (
+ $this->title->getNamespace() >= NS_MAIN &&
+ $this->title->getNamespace() <= NS_CATEGORY_TALK
+ ) {
+ $this->stats->increment(
+ 'edit.failures.conflict.byNamespaceId.' . $this->title->getNamespace()
+ );
+ }
+ }
+
+ /**
+ * Record when a user has resolved an edit conflict
+ */
+ public function incrementResolvedStats() {
+ $this->stats->increment( 'edit.failures.conflict.resolved' );
+ // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
+ if (
+ $this->title->getNamespace() >= NS_MAIN &&
+ $this->title->getNamespace() <= NS_CATEGORY_TALK
+ ) {
+ $this->stats->increment(
+ 'edit.failures.conflict.resolved.byNamespaceId.' . $this->title->getNamespace()
+ );
+ }
+ }
+
+ /**
+ * @return string HTML
+ */
+ public function getExplainHeader() {
+ return Html::rawElement(
+ 'div',
+ [ 'class' => 'mw-explainconflict' ],
+ $this->out->msg( 'explainconflict', $this->out->msg( $this->submitLabel )->text() )->parse()
+ );
+ }
+
+ /**
+ * HTML to build the textbox1 on edit conflicts
+ *
+ * @param mixed[]|null $customAttribs
+ * @return string HTML
+ */
+ public function getEditConflictMainTextBox( $customAttribs = [] ) {
+ $builder = new TextboxBuilder();
+ $classes = $builder->getTextboxProtectionCSSClasses( $this->title );
+
+ $attribs = [ 'tabindex' => 1 ];
+ $attribs += $customAttribs;
+
+ $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
+
+ $attribs = $builder->buildTextboxAttribs(
+ 'wpTextbox1',
+ $attribs,
+ $this->out->getUser(),
+ $this->title
+ );
+
+ $this->out->addHTML(
+ Html::textarea( 'wpTextbox1', $builder->addNewLineAtEnd( $this->storedversion ), $attribs )
+ );
+ }
+
+ /**
+ * Content to go in the edit form before textbox1
+ *
+ * @see EditPage::$editFormTextBeforeContent
+ * @return string HTML
+ */
+ public function getEditFormHtmlBeforeContent() {
+ return '';
+ }
+
+ /**
+ * Content to go in the edit form after textbox1
+ *
+ * @see EditPage::$editFormTextAfterContent
+ * @return string HTML
+ */
+ public function getEditFormHtmlAfterContent() {
+ return '';
+ }
+
+ /**
+ * Content to go in the edit form after the footers
+ * (templates on this page, hidden categories, limit report)
+ */
+ public function showEditFormTextAfterFooters() {
+ $this->out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
+
+ $yourContent = $this->toEditContent( $this->yourtext );
+ $storedContent = $this->toEditContent( $this->storedversion );
+ $handler = ContentHandler::getForModelID( $this->contentModel );
+ $diffEngine = $handler->createDifferenceEngine( $this->out );
+
+ $diffEngine->setContent( $yourContent, $storedContent );
+ $diffEngine->showDiff(
+ $this->out->msg( 'yourtext' )->parse(),
+ $this->out->msg( 'storedversion' )->text()
+ );
+
+ $this->out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
+
+ $builder = new TextboxBuilder();
+ $attribs = $builder->buildTextboxAttribs(
+ 'wpTextbox2',
+ [ 'tabindex' => 6, 'readonly' ],
+ $this->out->getUser(),
+ $this->title
+ );
+
+ $this->out->addHTML(
+ Html::textarea( 'wpTextbox2', $builder->addNewLineAtEnd( $this->yourtext ), $attribs )
+ );
+ }
+
+ /**
+ * @param string $text
+ * @return Content
+ */
+ public function toEditContent( $text ) {
+ return ContentHandler::makeContent(
+ $text,
+ $this->title,
+ $this->contentModel,
+ $this->contentFormat
+ );
+ }
+}
diff --git a/www/wiki/includes/editpage/TextboxBuilder.php b/www/wiki/includes/editpage/TextboxBuilder.php
new file mode 100644
index 00000000..81dc78d6
--- /dev/null
+++ b/www/wiki/includes/editpage/TextboxBuilder.php
@@ -0,0 +1,137 @@
+<?php
+/**
+ * Helps EditPage build textboxes
+ *
+ * (C) Copyright 2017 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\EditPage;
+
+use MWNamespace;
+use Sanitizer;
+use Title;
+use User;
+
+/**
+ * Helps EditPage build textboxes
+ *
+ * @since 1.31
+ */
+class TextboxBuilder {
+
+ /**
+ * @param string $wikitext
+ * @return string
+ */
+ public function addNewLineAtEnd( $wikitext ) {
+ if ( strval( $wikitext ) !== '' ) {
+ // Ensure there's a newline at the end, otherwise adding lines
+ // is awkward.
+ // But don't add a newline if the text is empty, or Firefox in XHTML
+ // mode will show an extra newline. A bit annoying.
+ $wikitext .= "\n";
+ return $wikitext;
+ }
+ return $wikitext;
+ }
+
+ /**
+ * @param string[] $classes
+ * @param mixed[] $attribs
+ * @return mixed[]
+ */
+ public function mergeClassesIntoAttributes( array $classes, array $attribs ) {
+ if ( !count( $classes ) ) {
+ return $attribs;
+ }
+
+ return Sanitizer::mergeAttributes(
+ $attribs,
+ [ 'class' => implode( ' ', $classes ) ]
+ );
+ }
+
+ /**
+ * @param Title $title
+ * @return string[]
+ */
+ public function getTextboxProtectionCSSClasses( Title $title ) {
+ $classes = []; // Textarea CSS
+ if ( $title->isProtected( 'edit' ) &&
+ MWNamespace::getRestrictionLevels( $title->getNamespace() ) !== [ '' ]
+ ) {
+ # Is the title semi-protected?
+ if ( $title->isSemiProtected() ) {
+ $classes[] = 'mw-textarea-sprotected';
+ } else {
+ # Then it must be protected based on static groups (regular)
+ $classes[] = 'mw-textarea-protected';
+ }
+ # Is the title cascade-protected?
+ if ( $title->isCascadeProtected() ) {
+ $classes[] = 'mw-textarea-cprotected';
+ }
+ }
+
+ return $classes;
+ }
+
+ /**
+ * @param string $name
+ * @param mixed[] $customAttribs
+ * @param User $user
+ * @param Title $title
+ * @return mixed[]
+ */
+ public function buildTextboxAttribs( $name, array $customAttribs, User $user, Title $title ) {
+ $attribs = $customAttribs + [
+ 'accesskey' => ',',
+ 'id' => $name,
+ 'cols' => 80,
+ 'rows' => 25,
+ // Avoid PHP notices when appending preferences
+ // (appending allows customAttribs['style'] to still work).
+ 'style' => ''
+ ];
+
+ // The following classes can be used here:
+ // * mw-editfont-monospace
+ // * mw-editfont-sans-serif
+ // * mw-editfont-serif
+ $class = 'mw-editfont-' . $user->getOption( 'editfont' );
+
+ if ( isset( $attribs['class'] ) ) {
+ if ( is_string( $attribs['class'] ) ) {
+ $attribs['class'] .= ' ' . $class;
+ } elseif ( is_array( $attribs['class'] ) ) {
+ $attribs['class'][] = $class;
+ }
+ } else {
+ $attribs['class'] = $class;
+ }
+
+ $pageLang = $title->getPageLanguage();
+ $attribs['lang'] = $pageLang->getHtmlCode();
+ $attribs['dir'] = $pageLang->getDir();
+
+ return $attribs;
+ }
+
+}
diff --git a/www/wiki/includes/compat/MemcachedClientCompat.php b/www/wiki/includes/exception/CannotCreateActorException.php
index 23047339..7c7ccfc4 100644
--- a/www/wiki/includes/compat/MemcachedClientCompat.php
+++ b/www/wiki/includes/exception/CannotCreateActorException.php
@@ -1,6 +1,6 @@
<?php
/**
- * Backward-compatibility alias for MemcachedClient
+ * Exception thrown when some operation failed
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,18 +17,13 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
- * @since 1.27
* @file
+ *
+ * @since 1.31
*/
/**
- * @deprecated since 1.27
- */
-class MWMemcached extends MemcachedClient {
-}
-
-/**
- * @deprecated since 1.27
+ * Exception thrown when an actor can't be created.
*/
-class MemCachedClientforWiki extends MWMemcached {
+class CannotCreateActorException extends RuntimeException {
}
diff --git a/www/wiki/includes/exception/LocalizedException.php b/www/wiki/includes/exception/LocalizedException.php
index d2cb5d17..f5f8c84e 100644
--- a/www/wiki/includes/exception/LocalizedException.php
+++ b/www/wiki/includes/exception/LocalizedException.php
@@ -45,7 +45,7 @@ class LocalizedException extends Exception implements ILocalizedException {
/**
* @param string|array|MessageSpecifier $messageSpec See Message::newFromSpecifier
- * @param int $code Exception code
+ * @param int $code
* @param Exception|Throwable $previous The previous exception used for the exception chaining.
*/
public function __construct( $messageSpec, $code = 0, $previous = null ) {
diff --git a/www/wiki/includes/exception/MWException.php b/www/wiki/includes/exception/MWException.php
index c3f09a6f..b3e9422b 100644
--- a/www/wiki/includes/exception/MWException.php
+++ b/www/wiki/includes/exception/MWException.php
@@ -55,7 +55,7 @@ class MWException extends Exception {
global $wgLang;
foreach ( $this->getTrace() as $frame ) {
- if ( isset( $frame['class'] ) && $frame['class'] === 'LocalisationCache' ) {
+ if ( isset( $frame['class'] ) && $frame['class'] === LocalisationCache::class ) {
return false;
}
}
@@ -102,7 +102,7 @@ class MWException extends Exception {
} else {
$logId = WebRequest::getRequestId();
$type = static::class;
- return "<div class=\"errorbox\">" .
+ return Html::errorBox(
htmlspecialchars(
'[' . $logId . '] ' .
gmdate( 'Y-m-d H:i:s' ) . ": " .
@@ -112,7 +112,7 @@ class MWException extends Exception {
$logId,
MWExceptionHandler::getURL( $this )
)
- ) . "</div>\n" .
+ ) ) .
"<!-- Set \$wgShowExceptionDetails = true; " .
"at the bottom of LocalSettings.php to show detailed " .
"debugging information. -->";
@@ -189,7 +189,7 @@ class MWException extends Exception {
} elseif ( self::isCommandLine() ) {
$message = $this->getText();
// T17602: STDERR may not be available
- if ( defined( 'STDERR' ) ) {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) && defined( 'STDERR' ) ) {
fwrite( STDERR, $message );
} else {
echo $message;
diff --git a/www/wiki/includes/exception/MWExceptionHandler.php b/www/wiki/includes/exception/MWExceptionHandler.php
index a2ec391d..79f0a233 100644
--- a/www/wiki/includes/exception/MWExceptionHandler.php
+++ b/www/wiki/includes/exception/MWExceptionHandler.php
@@ -51,7 +51,7 @@ class MWExceptionHandler {
* Install handlers with PHP.
*/
public static function installHandler() {
- set_exception_handler( 'MWExceptionHandler::handleException' );
+ set_exception_handler( 'MWExceptionHandler::handleUncaughtException' );
set_error_handler( 'MWExceptionHandler::handleError' );
// Reserve 16k of memory so we can report OOM fatals
@@ -112,6 +112,25 @@ class MWExceptionHandler {
}
/**
+ * Callback to use with PHP's set_exception_handler.
+ *
+ * @since 1.31
+ * @param Exception|Throwable $e
+ */
+ public static function handleUncaughtException( $e ) {
+ self::handleException( $e );
+
+ // Make sure we don't claim success on exit for CLI scripts (T177414)
+ if ( wfIsCLI() ) {
+ register_shutdown_function(
+ function () {
+ exit( 255 );
+ }
+ );
+ }
+ }
+
+ /**
* Exception handler which simulates the appropriate catch() handling:
*
* try {
@@ -151,6 +170,8 @@ class MWExceptionHandler {
public static function handleError(
$level, $message, $file = null, $line = null
) {
+ global $wgPropagateErrors;
+
if ( in_array( $level, self::$fatalErrorTypes ) ) {
return call_user_func_array(
'MWExceptionHandler::handleFatalError', func_get_args()
@@ -194,9 +215,10 @@ class MWExceptionHandler {
$e = new ErrorException( "PHP $levelName: $message", 0, $level, $file, $line );
self::logError( $e, 'error', $severity );
- // This handler is for logging only. Return false will instruct PHP
- // to continue regular handling.
- return false;
+ // If $wgPropagateErrors is true return false so PHP shows/logs the error normally.
+ // Ignore $wgPropagateErrors if the error should break execution, or track_errors is set
+ // (which means someone is counting on regular PHP error handling behavior).
+ return !( $wgPropagateErrors || $level == E_RECOVERABLE_ERROR || ini_get( 'track_errors' ) );
}
/**
@@ -254,13 +276,22 @@ class MWExceptionHandler {
return false;
}
- $msg = "[{exception_id}] PHP Fatal Error: {$message}";
+ $url = WebRequest::getGlobalRequestURL();
+ $msgParts = [
+ '[{exception_id}] {exception_url} PHP Fatal Error',
+ ( $line || $file ) ? ' from' : '',
+ $line ? " line $line" : '',
+ ( $line && $file ) ? ' of' : '',
+ $file ? " $file" : '',
+ ": $message",
+ ];
+ $msg = implode( '', $msgParts );
// Look at message to see if this is a class not found failure
// HHVM: Class undefined: foo
// PHP5: Class 'foo' not found
- if ( preg_match( "/Class (undefined: \w+|'\w+' not found)/", $msg ) ) {
- // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong
+ if ( preg_match( "/Class (undefined: \w+|'\w+' not found)/", $message ) ) {
+ // phpcs:disable Generic.Files.LineLength
$msg = <<<TXT
{$msg}
@@ -268,7 +299,7 @@ MediaWiki or an installed extension requires this class but it is not embedded d
Please see <a href="https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries">mediawiki.org</a> for help on installing the required components.
TXT;
- // @codingStandardsIgnoreEnd
+ // phpcs:enable
}
// We can't just create an exception and log it as it is likely that
@@ -281,14 +312,15 @@ TXT;
$logger = LoggerFactory::getInstance( 'fatal' );
$logger->error( $msg, [
'fatal_exception' => [
- 'class' => 'ErrorException',
+ 'class' => ErrorException::class,
'message' => "PHP Fatal Error: {$message}",
'code' => $level,
'file' => $file,
'line' => $line,
- 'trace' => static::redactTrace( $trace ),
+ 'trace' => self::prettyPrintTrace( self::redactTrace( $trace ) ),
],
- 'exception_id' => wfRandomString( 8 ),
+ 'exception_id' => WebRequest::getRequestId(),
+ 'exception_url' => $url,
'caught_by' => self::CAUGHT_BY_HANDLER
] );
@@ -642,7 +674,7 @@ TXT;
$catcher = self::CAUGHT_BY_HANDLER;
// The set_error_handler callback is independent from error_reporting.
// Filter out unwanted errors manually (e.g. when
- // MediaWiki\suppressWarnings is active).
+ // Wikimedia\suppressWarnings is active).
$suppressed = ( error_reporting() & $e->getSeverity() ) === 0;
if ( !$suppressed ) {
$logger = LoggerFactory::getInstance( $channel );
diff --git a/www/wiki/includes/exception/MWExceptionRenderer.php b/www/wiki/includes/exception/MWExceptionRenderer.php
index 1ba65aa4..5d750365 100644
--- a/www/wiki/includes/exception/MWExceptionRenderer.php
+++ b/www/wiki/includes/exception/MWExceptionRenderer.php
@@ -47,13 +47,15 @@ class MWExceptionRenderer {
self::printError( self::getText( $e ) );
} elseif ( $mode === self::AS_PRETTY ) {
self::statusHeader( 500 );
+ self::header( "Content-Type: $wgMimeType; charset=utf-8" );
if ( $e instanceof DBConnectionError ) {
self::reportOutageHTML( $e );
} else {
- self::header( "Content-Type: $wgMimeType; charset=utf-8" );
self::reportHTML( $e );
}
} else {
+ self::statusHeader( 500 );
+ self::header( "Content-Type: $wgMimeType; charset=utf-8" );
if ( $eNew ) {
$message = "MediaWiki internal error.\n\n";
if ( self::showBackTrace( $e ) ) {
@@ -90,7 +92,7 @@ class MWExceptionRenderer {
private static function useOutputPage( $e ) {
// Can the extension use the Message class/wfMessage to get i18n-ed messages?
foreach ( $e->getTrace() as $frame ) {
- if ( isset( $frame['class'] ) && $frame['class'] === 'LocalisationCache' ) {
+ if ( isset( $frame['class'] ) && $frame['class'] === LocalisationCache::class ) {
return false;
}
}
@@ -128,7 +130,7 @@ class MWExceptionRenderer {
// Show any custom GUI message before the details
if ( $e instanceof MessageSpecifier ) {
- $wgOut->addHTML( Message::newFromSpecifier( $e )->escaped() );
+ $wgOut->addHTML( Html::element( 'p', [], Message::newFromSpecifier( $e )->text() ) );
}
$wgOut->addHTML( self::getHTML( $e ) );
@@ -177,8 +179,7 @@ class MWExceptionRenderer {
get_class( $e ),
$logId,
MWExceptionHandler::getURL()
- )
- ) . "</div>\n" .
+ ) ) . "</div>\n" .
"<!-- " . wordwrap( self::getShowBacktraceError( $e ), 50 ) . " -->";
}
@@ -293,7 +294,7 @@ class MWExceptionRenderer {
* @param Exception|Throwable $e
*/
private static function reportOutageHTML( $e ) {
- global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors;
+ global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors, $wgSitename;
$sorry = htmlspecialchars( self::msg(
'dberr-problems',
@@ -318,55 +319,20 @@ class MWExceptionRenderer {
}
MessageCache::singleton()->disable(); // no DB access
-
- $html = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
+ $html = "<!DOCTYPE html>\n" .
+ '<html><head>' .
+ '<title>' .
+ htmlspecialchars( $wgSitename ) .
+ '</title>' .
+ '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
+ "</head><body><h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
if ( $wgShowDBErrorBacktrace ) {
$html .= '<p>Backtrace:</p><pre>' .
htmlspecialchars( $e->getTraceAsString() ) . '</pre>';
}
- $html .= '<hr />';
- $html .= self::googleSearchForm();
-
+ $html .= '</body></html>';
echo $html;
}
-
- /**
- * @return string
- */
- private static function googleSearchForm() {
- global $wgSitename, $wgCanonicalServer, $wgRequest;
-
- $usegoogle = htmlspecialchars( self::msg(
- 'dberr-usegoogle',
- 'You can try searching via Google in the meantime.'
- ) );
- $outofdate = htmlspecialchars( self::msg(
- 'dberr-outofdate',
- 'Note that their indexes of our content may be out of date.'
- ) );
- $googlesearch = htmlspecialchars( self::msg( 'searchbutton', 'Search' ) );
- $search = htmlspecialchars( $wgRequest->getVal( 'search' ) );
- $server = htmlspecialchars( $wgCanonicalServer );
- $sitename = htmlspecialchars( $wgSitename );
- $trygoogle = <<<EOT
-<div style="margin: 1.5em">$usegoogle<br />
-<small>$outofdate</small>
-</div>
-<form method="get" action="//www.google.com/search" id="googlesearch">
- <input type="hidden" name="domains" value="$server" />
- <input type="hidden" name="num" value="50" />
- <input type="hidden" name="ie" value="UTF-8" />
- <input type="hidden" name="oe" value="UTF-8" />
- <input type="text" name="q" size="31" maxlength="255" value="$search" />
- <input type="submit" name="btnG" value="$googlesearch" />
- <p>
- <label><input type="radio" name="sitesearch" value="$server" checked="checked" />$sitename</label>
- <label><input type="radio" name="sitesearch" value="" />WWW</label>
- </p>
-</form>
-EOT;
- return $trygoogle;
- }
}
diff --git a/www/wiki/includes/exception/TimestampException.php b/www/wiki/includes/exception/TimestampException.php
deleted file mode 100644
index b9c0c35c..00000000
--- a/www/wiki/includes/exception/TimestampException.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-/**
- * @since 1.20
- */
-class TimestampException extends MWException {
-}
diff --git a/www/wiki/includes/export/BaseDump.php b/www/wiki/includes/export/BaseDump.php
new file mode 100644
index 00000000..6a2d3bf6
--- /dev/null
+++ b/www/wiki/includes/export/BaseDump.php
@@ -0,0 +1,219 @@
+<?php
+/**
+ * Helper class for the --prefetch option of dumpTextPass.php
+ *
+ * Copyright © 2005 Brion Vibber <brion@pobox.com>
+ * https://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Maintenance
+ */
+
+/**
+ * Readahead helper for making large MediaWiki data dumps;
+ * reads in a previous XML dump to sequentially prefetch text
+ * records already normalized and decompressed.
+ *
+ * This can save load on the external database servers, hopefully.
+ *
+ * Assumes that dumps will be recorded in the canonical order:
+ * - ascending by page_id
+ * - ascending by rev_id within each page
+ * - text contents are immutable and should not change once
+ * recorded, so the previous dump is a reliable source
+ *
+ * @ingroup Maintenance
+ */
+class BaseDump {
+ /** @var XMLReader */
+ protected $reader = null;
+ protected $atEnd = false;
+ protected $atPageEnd = false;
+ protected $lastPage = 0;
+ protected $lastRev = 0;
+ protected $infiles = null;
+
+ public function __construct( $infile ) {
+ $this->infiles = explode( ';', $infile );
+ $this->reader = new XMLReader();
+ $infile = array_shift( $this->infiles );
+ if ( defined( 'LIBXML_PARSEHUGE' ) ) {
+ $this->reader->open( $infile, null, LIBXML_PARSEHUGE );
+ } else {
+ $this->reader->open( $infile );
+ }
+ }
+
+ /**
+ * Attempts to fetch the text of a particular page revision
+ * from the dump stream. May return null if the page is
+ * unavailable.
+ *
+ * @param int $page ID number of page to read
+ * @param int $rev ID number of revision to read
+ * @return string|null
+ */
+ function prefetch( $page, $rev ) {
+ $page = intval( $page );
+ $rev = intval( $rev );
+ while ( $this->lastPage < $page && !$this->atEnd ) {
+ $this->debug( "BaseDump::prefetch at page $this->lastPage, looking for $page" );
+ $this->nextPage();
+ }
+ if ( $this->lastPage > $page || $this->atEnd ) {
+ $this->debug( "BaseDump::prefetch already past page $page "
+ . "looking for rev $rev [$this->lastPage, $this->lastRev]" );
+
+ return null;
+ }
+ while ( $this->lastRev < $rev && !$this->atEnd && !$this->atPageEnd ) {
+ $this->debug( "BaseDump::prefetch at page $this->lastPage, rev $this->lastRev, "
+ . "looking for $page, $rev" );
+ $this->nextRev();
+ }
+ if ( $this->lastRev == $rev && !$this->atEnd ) {
+ $this->debug( "BaseDump::prefetch hit on $page, $rev [$this->lastPage, $this->lastRev]" );
+
+ return $this->nextText();
+ } else {
+ $this->debug( "BaseDump::prefetch already past rev $rev on page $page "
+ . "[$this->lastPage, $this->lastRev]" );
+
+ return null;
+ }
+ }
+
+ function debug( $str ) {
+ wfDebug( $str . "\n" );
+ // global $dumper;
+ // $dumper->progress( $str );
+ }
+
+ /**
+ * @access private
+ */
+ function nextPage() {
+ if ( $this->skipTo( 'page', 'mediawiki' ) ) {
+ if ( $this->skipTo( 'id' ) ) {
+ $this->lastPage = intval( $this->nodeContents() );
+ $this->lastRev = 0;
+ $this->atPageEnd = false;
+ }
+ } else {
+ $this->close();
+ if ( count( $this->infiles ) ) {
+ $infile = array_shift( $this->infiles );
+ $this->reader->open( $infile );
+ $this->atEnd = false;
+ }
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function nextRev() {
+ if ( $this->skipTo( 'revision' ) ) {
+ if ( $this->skipTo( 'id' ) ) {
+ $this->lastRev = intval( $this->nodeContents() );
+ }
+ } else {
+ $this->atPageEnd = true;
+ }
+ }
+
+ /**
+ * @access private
+ * @return string
+ */
+ function nextText() {
+ $this->skipTo( 'text' );
+
+ return strval( $this->nodeContents() );
+ }
+
+ /**
+ * @access private
+ * @param string $name
+ * @param string $parent
+ * @return bool|null
+ */
+ function skipTo( $name, $parent = 'page' ) {
+ if ( $this->atEnd ) {
+ return false;
+ }
+ while ( $this->reader->read() ) {
+ if ( $this->reader->nodeType == XMLReader::ELEMENT
+ && $this->reader->name == $name
+ ) {
+ return true;
+ }
+ if ( $this->reader->nodeType == XMLReader::END_ELEMENT
+ && $this->reader->name == $parent
+ ) {
+ $this->debug( "BaseDump::skipTo found </$parent> searching for <$name>" );
+
+ return false;
+ }
+ }
+
+ return $this->close();
+ }
+
+ /**
+ * Shouldn't something like this be built-in to XMLReader?
+ * Fetches text contents of the current element, assuming
+ * no sub-elements or such scary things.
+ *
+ * @return string
+ * @access private
+ */
+ function nodeContents() {
+ if ( $this->atEnd ) {
+ return null;
+ }
+ if ( $this->reader->isEmptyElement ) {
+ return "";
+ }
+ $buffer = "";
+ while ( $this->reader->read() ) {
+ switch ( $this->reader->nodeType ) {
+ case XMLReader::TEXT:
+ // case XMLReader::WHITESPACE:
+ case XMLReader::SIGNIFICANT_WHITESPACE:
+ $buffer .= $this->reader->value;
+ break;
+ case XMLReader::END_ELEMENT:
+ return $buffer;
+ }
+ }
+
+ return $this->close();
+ }
+
+ /**
+ * @access private
+ * @return null
+ */
+ function close() {
+ $this->reader->close();
+ $this->atEnd = true;
+
+ return null;
+ }
+}
diff --git a/www/wiki/includes/export/DumpNamespaceFilter.php b/www/wiki/includes/export/DumpNamespaceFilter.php
index 2b71db00..12b9b55e 100644
--- a/www/wiki/includes/export/DumpNamespaceFilter.php
+++ b/www/wiki/includes/export/DumpNamespaceFilter.php
@@ -50,8 +50,8 @@ class DumpNamespaceFilter extends DumpFilter {
"NS_PROJECT_TALK" => NS_PROJECT_TALK,
"NS_FILE" => NS_FILE,
"NS_FILE_TALK" => NS_FILE_TALK,
- "NS_IMAGE" => NS_IMAGE, // NS_IMAGE is an alias for NS_FILE
- "NS_IMAGE_TALK" => NS_IMAGE_TALK,
+ "NS_IMAGE" => NS_FILE, // NS_IMAGE is an alias for NS_FILE
+ "NS_IMAGE_TALK" => NS_FILE_TALK,
"NS_MEDIAWIKI" => NS_MEDIAWIKI,
"NS_MEDIAWIKI_TALK" => NS_MEDIAWIKI_TALK,
"NS_TEMPLATE" => NS_TEMPLATE,
diff --git a/www/wiki/includes/export/ExportProgressFilter.php b/www/wiki/includes/export/ExportProgressFilter.php
new file mode 100644
index 00000000..9b1571f7
--- /dev/null
+++ b/www/wiki/includes/export/ExportProgressFilter.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * Copyright © 2005 Brion Vibber <brion@pobox.com>
+ * https://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup Dump
+ */
+class ExportProgressFilter extends DumpFilter {
+ /**
+ * @var BackupDumper
+ */
+ private $progress;
+
+ function __construct( &$sink, &$progress ) {
+ parent::__construct( $sink );
+ $this->progress = $progress;
+ }
+
+ function writeClosePage( $string ) {
+ parent::writeClosePage( $string );
+ $this->progress->reportPage();
+ }
+
+ function writeRevision( $rev, $string ) {
+ parent::writeRevision( $rev, $string );
+ $this->progress->revCount();
+ }
+}
diff --git a/www/wiki/includes/export/WikiExporter.php b/www/wiki/includes/export/WikiExporter.php
index 6e2a5a4f..6c7a4493 100644
--- a/www/wiki/includes/export/WikiExporter.php
+++ b/www/wiki/includes/export/WikiExporter.php
@@ -227,15 +227,20 @@ class WikiExporter {
$this->author_list = "<contributors>";
// rev_deleted
+ $revQuery = Revision::getQueryInfo( [ 'page' ] );
$res = $this->db->select(
- [ 'page', 'revision' ],
- [ 'DISTINCT rev_user_text', 'rev_user' ],
+ $revQuery['tables'],
+ [
+ 'rev_user_text' => $revQuery['fields']['rev_user_text'],
+ 'rev_user' => $revQuery['fields']['rev_user'],
+ ],
[
$this->db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0',
$cond,
- 'page_id = rev_id',
],
- __METHOD__
+ __METHOD__,
+ [ 'DISTINCT' ],
+ $revQuery['joins']
);
foreach ( $res as $row ) {
@@ -278,15 +283,19 @@ class WikiExporter {
}
$result = null; // Assuring $result is not undefined, if exception occurs early
- $commentQuery = CommentStore::newKey( 'log_comment' )->getJoin();
+ $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
try {
- $result = $this->db->select( [ 'logging', 'user' ] + $commentQuery['tables'],
- [ "{$logging}.*", 'user_name' ] + $commentQuery['fields'], // grab the user name
+ $result = $this->db->select(
+ array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
+ [ "{$logging}.*", 'user_name' ] + $commentQuery['fields'] + $actorQuery['fields'],
$where,
__METHOD__,
[ 'ORDER BY' => 'log_id', 'USE INDEX' => [ 'logging' => 'PRIMARY' ] ],
- [ 'user' => [ 'JOIN', 'user_id = log_user' ] ] + $commentQuery['joins']
+ [
+ 'user' => [ 'JOIN', 'user_id = ' . $actorQuery['fields']['log_user'] ]
+ ] + $commentQuery['joins'] + $actorQuery['joins']
);
$this->outputLogStream( $result );
if ( $this->buffer == self::STREAM ) {
@@ -321,13 +330,29 @@ class WikiExporter {
}
# For page dumps...
} else {
- $tables = [ 'page', 'revision' ];
+ $revOpts = [ 'page' ];
+ if ( $this->text != self::STUB ) {
+ $revOpts[] = 'text';
+ }
+ $revQuery = Revision::getQueryInfo( $revOpts );
+
+ // We want page primary rather than revision
+ $tables = array_merge( [ 'page' ], array_diff( $revQuery['tables'], [ 'page' ] ) );
+ $join = $revQuery['joins'] + [
+ 'revision' => $revQuery['joins']['page']
+ ];
+ unset( $join['page'] );
+
+ $fields = array_merge( $revQuery['fields'], [ 'page_restrictions' ] );
+
+ $conds = [];
+ if ( $cond !== '' ) {
+ $conds[] = $cond;
+ }
$opts = [ 'ORDER BY' => 'page_id ASC' ];
$opts['USE INDEX'] = [];
- $join = [];
if ( is_array( $this->history ) ) {
# Time offset/limit for all pages/history...
- $revJoin = 'page_id=rev_page';
# Set time order
if ( $this->history['dir'] == 'asc' ) {
$op = '>';
@@ -338,10 +363,9 @@ class WikiExporter {
}
# Set offset
if ( !empty( $this->history['offset'] ) ) {
- $revJoin .= " AND rev_timestamp $op " .
+ $conds[] = "rev_timestamp $op " .
$this->db->addQuotes( $this->db->timestamp( $this->history['offset'] ) );
}
- $join['revision'] = [ 'INNER JOIN', $revJoin ];
# Set query limit
if ( !empty( $this->history['limit'] ) ) {
$opts['LIMIT'] = intval( $this->history['limit'] );
@@ -350,13 +374,11 @@ class WikiExporter {
# Full history dumps...
# query optimization for history stub dumps
if ( $this->text == self::STUB && $orderRevs ) {
- $tables = [ 'revision', 'page' ];
- $opts[] = 'STRAIGHT_JOIN';
+ $tables = $revQuery['tables'];
$opts['ORDER BY'] = [ 'rev_page ASC', 'rev_id ASC' ];
$opts['USE INDEX']['revision'] = 'rev_page_id';
+ unset( $join['revision'] );
$join['page'] = [ 'INNER JOIN', 'rev_page=page_id' ];
- } else {
- $join['revision'] = [ 'INNER JOIN', 'page_id=rev_page' ];
}
} elseif ( $this->history & self::CURRENT ) {
# Latest revision dumps...
@@ -374,22 +396,11 @@ class WikiExporter {
}
} elseif ( $this->history & self::RANGE ) {
# Dump of revisions within a specified range
- $join['revision'] = [ 'INNER JOIN', 'page_id=rev_page' ];
$opts['ORDER BY'] = [ 'rev_page ASC', 'rev_id ASC' ];
} else {
# Unknown history specification parameter?
throw new MWException( __METHOD__ . " given invalid history dump type." );
}
- # Query optimization hacks
- if ( $cond == '' ) {
- $opts[] = 'STRAIGHT_JOIN';
- $opts['USE INDEX']['page'] = 'PRIMARY';
- }
- # Build text join options
- if ( $this->text != self::STUB ) { // 1-pass
- $tables[] = 'text';
- $join['text'] = [ 'INNER JOIN', 'rev_text_id=old_id' ];
- }
if ( $this->buffer == self::STREAM ) {
$prev = $this->db->bufferResults( false );
@@ -399,16 +410,14 @@ class WikiExporter {
Hooks::run( 'ModifyExportQuery',
[ $this->db, &$tables, &$cond, &$opts, &$join ] );
- $commentQuery = CommentStore::newKey( 'rev_comment' )->getJoin();
-
# Do the query!
$result = $this->db->select(
- $tables + $commentQuery['tables'],
- [ '*' ] + $commentQuery['fields'],
- $cond,
+ $tables,
+ $fields,
+ $conds,
__METHOD__,
$opts,
- $join + $commentQuery['joins']
+ $join
);
# Output dump results
$this->outputPageStream( $result );
diff --git a/www/wiki/includes/export/XmlDumpWriter.php b/www/wiki/includes/export/XmlDumpWriter.php
index c46eb61c..e1c12de1 100644
--- a/www/wiki/includes/export/XmlDumpWriter.php
+++ b/www/wiki/includes/export/XmlDumpWriter.php
@@ -219,7 +219,7 @@ class XmlDumpWriter {
if ( isset( $row->rev_deleted ) && ( $row->rev_deleted & Revision::DELETED_COMMENT ) ) {
$out .= " " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n";
} else {
- $comment = CommentStore::newKey( 'rev_comment' )->getComment( $row )->text;
+ $comment = CommentStore::getStore()->getComment( 'rev_comment', $row )->text;
if ( $comment != '' ) {
$out .= " " . Xml::elementClean( 'comment', [], strval( $comment ) ) . "\n";
}
@@ -303,7 +303,7 @@ class XmlDumpWriter {
if ( $row->log_deleted & LogPage::DELETED_COMMENT ) {
$out .= " " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n";
} else {
- $comment = CommentStore::newKey( 'log_comment' )->getComment( $row )->text;
+ $comment = CommentStore::getStore()->getComment( 'log_comment', $row )->text;
if ( $comment != '' ) {
$out .= " " . Xml::elementClean( 'comment', null, strval( $comment ) ) . "\n";
}
diff --git a/www/wiki/includes/externalstore/ExternalStore.php b/www/wiki/includes/externalstore/ExternalStore.php
index 1563baf8..de7d1a4c 100644
--- a/www/wiki/includes/externalstore/ExternalStore.php
+++ b/www/wiki/includes/externalstore/ExternalStore.php
@@ -3,6 +3,8 @@
* @defgroup ExternalStorage ExternalStorage
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Interface for data storage in external repositories.
*
@@ -52,16 +54,9 @@ class ExternalStore {
* @return ExternalStoreMedium|bool The store class or false on error
*/
public static function getStoreObject( $proto, array $params = [] ) {
- global $wgExternalStores;
-
- if ( !$wgExternalStores || !in_array( $proto, $wgExternalStores ) ) {
- return false; // protocol not enabled
- }
-
- $class = 'ExternalStore' . ucfirst( $proto );
-
- // Any custom modules should be added to $wgAutoLoadClasses for on-demand loading
- return class_exists( $class ) ? new $class( $params ) : false;
+ return MediaWikiServices::getInstance()
+ ->getExternalStoreFactory()
+ ->getStoreObject( $proto, $params );
}
/**
@@ -97,6 +92,7 @@ class ExternalStore {
* @param array $urls The URLs of the text to get
* @return array Map from url to its data. Data is either string when found
* or false on failure.
+ * @throws MWException
*/
public static function batchFetchFromURLs( array $urls ) {
$batches = [];
@@ -162,7 +158,7 @@ class ExternalStore {
* provided by $wgDefaultExternalStore.
*
* @param string $data
- * @param array $params Associative array of ExternalStoreMedium parameters
+ * @param array $params Map of ExternalStoreMedium::__construct context parameters
* @return string|bool The URL of the stored data item, or false on error
* @throws MWException
*/
@@ -180,7 +176,7 @@ class ExternalStore {
*
* @param array $tryStores Refer to $wgDefaultExternalStore
* @param string $data
- * @param array $params Associative array of ExternalStoreMedium parameters
+ * @param array $params Map of ExternalStoreMedium::__construct context parameters
* @return string|bool The URL of the stored data item, or false on error
* @throws MWException
*/
@@ -195,19 +191,25 @@ class ExternalStore {
if ( $store === false ) {
throw new MWException( "Invalid external storage protocol - $storeUrl" );
}
+
try {
- $url = $store->store( $path, $data ); // Try to save the object
+ if ( $store->isReadOnly( $path ) ) {
+ $msg = 'read only';
+ } else {
+ $url = $store->store( $path, $data );
+ if ( strlen( $url ) ) {
+ return $url; // a store accepted the write; done!
+ }
+ $msg = 'operation failed';
+ }
} catch ( Exception $error ) {
- $url = false;
- }
- if ( strlen( $url ) ) {
- return $url; // Done!
- } else {
- unset( $tryStores[$index] ); // Don't try this one again!
- $tryStores = array_values( $tryStores ); // Must have consecutive keys
- wfDebugLog( 'ExternalStorage',
- "Unable to store text to external storage $storeUrl" );
+ $msg = 'caught exception';
}
+
+ unset( $tryStores[$index] ); // Don't try this one again!
+ $tryStores = array_values( $tryStores ); // Must have consecutive keys
+ wfDebugLog( 'ExternalStorage',
+ "Unable to store text to external storage $storeUrl ($msg)" );
}
// All stores failed
if ( $error ) {
@@ -218,6 +220,29 @@ class ExternalStore {
}
/**
+ * @return bool Whether all the default insertion stores are marked as read-only
+ * @since 1.31
+ */
+ public static function defaultStoresAreReadOnly() {
+ global $wgDefaultExternalStore;
+
+ $tryStores = (array)$wgDefaultExternalStore;
+ if ( !$tryStores ) {
+ return false; // no stores exists which can be "read only"
+ }
+
+ foreach ( $tryStores as $storeUrl ) {
+ list( $proto, $path ) = explode( '://', $storeUrl, 2 );
+ $store = self::getStoreObject( $proto, [] );
+ if ( !$store->isReadOnly( $path ) ) {
+ return false; // at least one store is not read-only
+ }
+ }
+
+ return true; // all stores are read-only
+ }
+
+ /**
* @param string $data
* @param string $wiki
* @return string|bool The URL of the stored data item, or false on error
diff --git a/www/wiki/includes/externalstore/ExternalStoreDB.php b/www/wiki/includes/externalstore/ExternalStoreDB.php
index e5d36e10..22a2d2bc 100644
--- a/www/wiki/includes/externalstore/ExternalStoreDB.php
+++ b/www/wiki/includes/externalstore/ExternalStoreDB.php
@@ -20,13 +20,15 @@
* @file
*/
-use Wikimedia\Rdbms\LoadBalancer;
+use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\ILoadBalancer;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\DBConnRef;
use Wikimedia\Rdbms\MaintainableDBConnRef;
+use Wikimedia\Rdbms\DatabaseDomain;
/**
- * DB accessable external objects.
+ * DB accessible external objects.
*
* In this system, each store "location" maps to a database "cluster".
* The clusters must be defined in the normal LBFactory configuration.
@@ -103,14 +105,19 @@ class ExternalStoreDB extends ExternalStoreMedium {
return "DB://$location/$id";
}
+ public function isReadOnly( $location ) {
+ return ( $this->getLoadBalancer( $location )->getReadOnlyReason() !== false );
+ }
+
/**
* Get a LoadBalancer for the specified cluster
*
* @param string $cluster Cluster name
- * @return LoadBalancer
+ * @return ILoadBalancer
*/
private function getLoadBalancer( $cluster ) {
- return wfGetLBFactory()->getExternalLB( $cluster );
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ return $lbFactory->getExternalLB( $cluster );
}
/**
@@ -122,8 +129,8 @@ class ExternalStoreDB extends ExternalStoreMedium {
public function getSlave( $cluster ) {
global $wgDefaultExternalStore;
- $wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false;
$lb = $this->getLoadBalancer( $cluster );
+ $domainId = $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) );
if ( !in_array( "DB://" . $cluster, (array)$wgDefaultExternalStore ) ) {
wfDebug( "read only external store\n" );
@@ -132,7 +139,7 @@ class ExternalStoreDB extends ExternalStoreMedium {
wfDebug( "writable external store\n" );
}
- $db = $lb->getConnectionRef( DB_REPLICA, [], $wiki );
+ $db = $lb->getConnectionRef( DB_REPLICA, [], $domainId );
$db->clearFlag( DBO_TRX ); // sanity
return $db;
@@ -145,16 +152,43 @@ class ExternalStoreDB extends ExternalStoreMedium {
* @return MaintainableDBConnRef
*/
public function getMaster( $cluster ) {
- $wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false;
$lb = $this->getLoadBalancer( $cluster );
+ $domainId = $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) );
- $db = $lb->getMaintenanceConnectionRef( DB_MASTER, [], $wiki );
+ $db = $lb->getMaintenanceConnectionRef( DB_MASTER, [], $domainId );
$db->clearFlag( DBO_TRX ); // sanity
return $db;
}
/**
+ * @param array $server Master DB server configuration array for LoadBalancer
+ * @return string|bool Database domain ID or false
+ */
+ private function getDomainId( array $server ) {
+ if ( isset( $this->params['wiki'] ) ) {
+ return $this->params['wiki']; // explicit domain
+ }
+
+ if ( isset( $server['dbname'] ) ) {
+ // T200471: for b/c, treat any "dbname" field as forcing which database to use.
+ // MediaWiki/LoadBalancer previously did not enforce any concept of a local DB
+ // domain, but rather assumed that the LB server configuration matched $wgDBname.
+ // This check is useful when the external storage DB for this cluster does not use
+ // the same name as the corresponding "main" DB(s) for wikis.
+ $domain = new DatabaseDomain(
+ $server['dbname'],
+ $server['schema'] ?? null,
+ $server['tablePrefix'] ?? ''
+ );
+
+ return $domain->getId();
+ }
+
+ return false; // local LB domain
+ }
+
+ /**
* Get the 'blobs' table name for this database
*
* @param IDatabase $db
diff --git a/www/wiki/includes/externalstore/ExternalStoreFactory.php b/www/wiki/includes/externalstore/ExternalStoreFactory.php
new file mode 100644
index 00000000..940fb2e2
--- /dev/null
+++ b/www/wiki/includes/externalstore/ExternalStoreFactory.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @defgroup ExternalStorage ExternalStorage
+ */
+
+/**
+ * @ingroup ExternalStorage
+ */
+class ExternalStoreFactory {
+
+ /**
+ * @var array
+ */
+ private $externalStores;
+
+ /**
+ * @param array $externalStores See $wgExternalStores
+ */
+ public function __construct( array $externalStores ) {
+ $this->externalStores = array_map( 'strtolower', $externalStores );
+ }
+
+ /**
+ * Get an external store object of the given type, with the given parameters
+ *
+ * @param string $proto Type of external storage, should be a value in $wgExternalStores
+ * @param array $params Associative array of ExternalStoreMedium parameters
+ * @return ExternalStoreMedium|bool The store class or false on error
+ */
+ public function getStoreObject( $proto, array $params = [] ) {
+ if ( !$this->externalStores || !in_array( strtolower( $proto ), $this->externalStores ) ) {
+ // Protocol not enabled
+ return false;
+ }
+
+ $class = 'ExternalStore' . ucfirst( $proto );
+
+ // Any custom modules should be added to $wgAutoLoadClasses for on-demand loading
+ return class_exists( $class ) ? new $class( $params ) : false;
+ }
+
+}
diff --git a/www/wiki/includes/externalstore/ExternalStoreHttp.php b/www/wiki/includes/externalstore/ExternalStoreHttp.php
index 8e1e49fa..879686f7 100644
--- a/www/wiki/includes/externalstore/ExternalStoreHttp.php
+++ b/www/wiki/includes/externalstore/ExternalStoreHttp.php
@@ -21,30 +21,21 @@
*/
/**
- * Example class for HTTP accessable external objects.
+ * Example class for HTTP accessible external objects.
* Only supports reading, not storing.
*
* @ingroup ExternalStorage
*/
class ExternalStoreHttp extends ExternalStoreMedium {
- /**
- * @see ExternalStoreMedium::fetchFromURL()
- * @param string $url
- * @return string|bool
- * @throws MWException
- */
public function fetchFromURL( $url ) {
return Http::get( $url, [], __METHOD__ );
}
- /**
- * @see ExternalStoreMedium::store()
- * @param string $cluster
- * @param string $data
- * @return string|bool
- * @throws MWException
- */
- public function store( $cluster, $data ) {
+ public function store( $location, $data ) {
throw new MWException( "ExternalStoreHttp is read-only and does not support store()." );
}
+
+ public function isReadOnly( $location ) {
+ return true;
+ }
}
diff --git a/www/wiki/includes/externalstore/ExternalStoreMedium.php b/www/wiki/includes/externalstore/ExternalStoreMedium.php
index 6cfa0838..da7752b7 100644
--- a/www/wiki/includes/externalstore/ExternalStoreMedium.php
+++ b/www/wiki/includes/externalstore/ExternalStoreMedium.php
@@ -32,7 +32,8 @@ abstract class ExternalStoreMedium {
protected $params = [];
/**
- * @param array $params Options
+ * @param array $params Usage context options:
+ * - wiki: the domain ID of the wiki this is being used for [optional]
*/
public function __construct( array $params = [] ) {
$this->params = $params;
@@ -76,4 +77,15 @@ abstract class ExternalStoreMedium {
* @throws MWException
*/
abstract public function store( $location, $data );
+
+ /**
+ * Check if a given location is read-only
+ *
+ * @param string $location The location name
+ * @return bool Whether this location is read-only
+ * @since 1.31
+ */
+ public function isReadOnly( $location ) {
+ return false;
+ }
}
diff --git a/www/wiki/includes/externalstore/ExternalStoreMwstore.php b/www/wiki/includes/externalstore/ExternalStoreMwstore.php
index 5395f562..5d7155e8 100644
--- a/www/wiki/includes/externalstore/ExternalStoreMwstore.php
+++ b/www/wiki/includes/externalstore/ExternalStoreMwstore.php
@@ -21,7 +21,7 @@
*/
/**
- * File backend accessable external objects.
+ * File backend accessible external objects.
*
* In this system, each store "location" maps to the name of a file backend.
* The file backends must be defined in $wgFileBackends and must be global
@@ -73,13 +73,6 @@ class ExternalStoreMwstore extends ExternalStoreMedium {
return $blobs;
}
- /**
- * @see ExternalStoreMedium::store()
- * @param string $backend
- * @param string $data
- * @return string|bool
- * @throws MWException
- */
public function store( $backend, $data ) {
$be = FileBackendGroup::singleton()->get( $backend );
if ( $be instanceof FileBackend ) {
@@ -103,4 +96,10 @@ class ExternalStoreMwstore extends ExternalStoreMedium {
return false;
}
+
+ public function isReadOnly( $backend ) {
+ $be = FileBackendGroup::singleton()->get( $backend );
+
+ return $be ? $be->isReadOnly() : false;
+ }
}
diff --git a/www/wiki/includes/filebackend/FSFile.php b/www/wiki/includes/filebackend/FSFile.php
deleted file mode 100644
index 8aa11b65..00000000
--- a/www/wiki/includes/filebackend/FSFile.php
+++ /dev/null
@@ -1,280 +0,0 @@
-<?php
-/**
- * Non-directory file on the file system.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileBackend
- */
-
-/**
- * Class representing a non-directory file on the file system
- *
- * @ingroup FileBackend
- */
-class FSFile {
- /** @var string Path to file */
- protected $path;
-
- /** @var string File SHA-1 in base 36 */
- protected $sha1Base36;
-
- /**
- * Sets up the file object
- *
- * @param string $path Path to temporary file on local disk
- */
- public function __construct( $path ) {
- $this->path = $path;
- }
-
- /**
- * Returns the file system path
- *
- * @return string
- */
- public function getPath() {
- return $this->path;
- }
-
- /**
- * Checks if the file exists
- *
- * @return bool
- */
- public function exists() {
- return is_file( $this->path );
- }
-
- /**
- * Get the file size in bytes
- *
- * @return int|bool
- */
- public function getSize() {
- return filesize( $this->path );
- }
-
- /**
- * Get the file's last-modified timestamp
- *
- * @return string|bool TS_MW timestamp or false on failure
- */
- public function getTimestamp() {
- MediaWiki\suppressWarnings();
- $timestamp = filemtime( $this->path );
- MediaWiki\restoreWarnings();
- if ( $timestamp !== false ) {
- $timestamp = wfTimestamp( TS_MW, $timestamp );
- }
-
- return $timestamp;
- }
-
- /**
- * Guess the MIME type from the file contents alone
- *
- * @return string
- */
- public function getMimeType() {
- return MimeMagic::singleton()->guessMimeType( $this->path, false );
- }
-
- /**
- * Get an associative array containing information about
- * a file with the given storage path.
- *
- * Resulting array fields include:
- * - fileExists
- * - size (filesize in bytes)
- * - mime (as major/minor)
- * - media_type (value to be used with the MEDIATYPE_xxx constants)
- * - metadata (handler specific)
- * - sha1 (in base 36)
- * - width
- * - height
- * - bits (bitrate)
- * - file-mime
- * - major_mime
- * - minor_mime
- *
- * @param string|bool $ext The file extension, or true to extract it from the filename.
- * Set it to false to ignore the extension.
- * @return array
- */
- public function getProps( $ext = true ) {
- wfDebug( __METHOD__ . ": Getting file info for $this->path\n" );
-
- $info = self::placeholderProps();
- $info['fileExists'] = $this->exists();
-
- if ( $info['fileExists'] ) {
- $magic = MimeMagic::singleton();
-
- # get the file extension
- if ( $ext === true ) {
- $ext = self::extensionFromPath( $this->path );
- }
-
- # MIME type according to file contents
- $info['file-mime'] = $this->getMimeType();
- # logical MIME type
- $info['mime'] = $magic->improveTypeFromExtension( $info['file-mime'], $ext );
-
- list( $info['major_mime'], $info['minor_mime'] ) = File::splitMime( $info['mime'] );
- $info['media_type'] = $magic->getMediaType( $this->path, $info['mime'] );
-
- # Get size in bytes
- $info['size'] = $this->getSize();
-
- # Height, width and metadata
- $handler = MediaHandler::getHandler( $info['mime'] );
- if ( $handler ) {
- $tempImage = (object)[]; // XXX (hack for File object)
- $info['metadata'] = $handler->getMetadata( $tempImage, $this->path );
- $gis = $handler->getImageSize( $tempImage, $this->path, $info['metadata'] );
- if ( is_array( $gis ) ) {
- $info = $this->extractImageSizeInfo( $gis ) + $info;
- }
- }
- $info['sha1'] = $this->getSha1Base36();
-
- wfDebug( __METHOD__ . ": $this->path loaded, {$info['size']} bytes, {$info['mime']}.\n" );
- } else {
- wfDebug( __METHOD__ . ": $this->path NOT FOUND!\n" );
- }
-
- return $info;
- }
-
- /**
- * Placeholder file properties to use for files that don't exist
- *
- * Resulting array fields include:
- * - fileExists
- * - mime (as major/minor)
- * - media_type (value to be used with the MEDIATYPE_xxx constants)
- * - metadata (handler specific)
- * - sha1 (in base 36)
- * - width
- * - height
- * - bits (bitrate)
- *
- * @return array
- */
- public static function placeholderProps() {
- $info = [];
- $info['fileExists'] = false;
- $info['mime'] = null;
- $info['media_type'] = MEDIATYPE_UNKNOWN;
- $info['metadata'] = '';
- $info['sha1'] = '';
- $info['width'] = 0;
- $info['height'] = 0;
- $info['bits'] = 0;
-
- return $info;
- }
-
- /**
- * Exract image size information
- *
- * @param array $gis
- * @return array
- */
- protected function extractImageSizeInfo( array $gis ) {
- $info = [];
- # NOTE: $gis[2] contains a code for the image type. This is no longer used.
- $info['width'] = $gis[0];
- $info['height'] = $gis[1];
- if ( isset( $gis['bits'] ) ) {
- $info['bits'] = $gis['bits'];
- } else {
- $info['bits'] = 0;
- }
-
- return $info;
- }
-
- /**
- * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
- * encoding, zero padded to 31 digits.
- *
- * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
- * fairly neatly.
- *
- * @param bool $recache
- * @return bool|string False on failure
- */
- public function getSha1Base36( $recache = false ) {
- if ( $this->sha1Base36 !== null && !$recache ) {
- return $this->sha1Base36;
- }
-
- MediaWiki\suppressWarnings();
- $this->sha1Base36 = sha1_file( $this->path );
- MediaWiki\restoreWarnings();
-
- if ( $this->sha1Base36 !== false ) {
- $this->sha1Base36 = Wikimedia\base_convert( $this->sha1Base36, 16, 36, 31 );
- }
-
- return $this->sha1Base36;
- }
-
- /**
- * Get the final file extension from a file system path
- *
- * @param string $path
- * @return string
- */
- public static function extensionFromPath( $path ) {
- $i = strrpos( $path, '.' );
-
- return strtolower( $i ? substr( $path, $i + 1 ) : '' );
- }
-
- /**
- * Get an associative array containing information about a file in the local filesystem.
- *
- * @param string $path Absolute local filesystem path
- * @param string|bool $ext The file extension, or true to extract it from the filename.
- * Set it to false to ignore the extension.
- * @return array
- */
- public static function getPropsFromPath( $path, $ext = true ) {
- $fsFile = new self( $path );
-
- return $fsFile->getProps( $ext );
- }
-
- /**
- * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
- * encoding, zero padded to 31 digits.
- *
- * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
- * fairly neatly.
- *
- * @param string $path
- * @return bool|string False on failure
- */
- public static function getSha1Base36FromPath( $path ) {
- $fsFile = new self( $path );
-
- return $fsFile->getSha1Base36();
- }
-}
diff --git a/www/wiki/includes/filebackend/FSFileBackend.php b/www/wiki/includes/filebackend/FSFileBackend.php
deleted file mode 100644
index efe78ee2..00000000
--- a/www/wiki/includes/filebackend/FSFileBackend.php
+++ /dev/null
@@ -1,975 +0,0 @@
-<?php
-/**
- * File system based backend.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * @brief Class for a file system (FS) based file backend.
- *
- * All "containers" each map to a directory under the backend's base directory.
- * For backwards-compatibility, some container paths can be set to custom paths.
- * The wiki ID will not be used in any custom paths, so this should be avoided.
- *
- * Having directories with thousands of files will diminish performance.
- * Sharding can be accomplished by using FileRepo-style hash paths.
- *
- * Status messages should avoid mentioning the internal FS paths.
- * PHP warnings are assumed to be logged rather than output.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-class FSFileBackend extends FileBackendStore {
- /** @var string Directory holding the container directories */
- protected $basePath;
-
- /** @var array Map of container names to root paths for custom container paths */
- protected $containerPaths = [];
-
- /** @var int File permission mode */
- protected $fileMode;
-
- /** @var string Required OS username to own files */
- protected $fileOwner;
-
- /** @var string OS username running this script */
- protected $currentUser;
-
- /** @var array */
- protected $hadWarningErrors = [];
-
- /**
- * @see FileBackendStore::__construct()
- * Additional $config params include:
- * - basePath : File system directory that holds containers.
- * - containerPaths : Map of container names to custom file system directories.
- * This should only be used for backwards-compatibility.
- * - fileMode : Octal UNIX file permissions to use on files stored.
- * @param array $config
- */
- public function __construct( array $config ) {
- parent::__construct( $config );
-
- // Remove any possible trailing slash from directories
- if ( isset( $config['basePath'] ) ) {
- $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash
- } else {
- $this->basePath = null; // none; containers must have explicit paths
- }
-
- if ( isset( $config['containerPaths'] ) ) {
- $this->containerPaths = (array)$config['containerPaths'];
- foreach ( $this->containerPaths as &$path ) {
- $path = rtrim( $path, '/' ); // remove trailing slash
- }
- }
-
- $this->fileMode = isset( $config['fileMode'] ) ? $config['fileMode'] : 0644;
- if ( isset( $config['fileOwner'] ) && function_exists( 'posix_getuid' ) ) {
- $this->fileOwner = $config['fileOwner'];
- // cache this, assuming it doesn't change
- $this->currentUser = posix_getpwuid( posix_getuid() )['name'];
- }
- }
-
- public function getFeatures() {
- return !wfIsWindows() ? FileBackend::ATTR_UNICODE_PATHS : 0;
- }
-
- protected function resolveContainerPath( $container, $relStoragePath ) {
- // Check that container has a root directory
- if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
- // Check for sane relative paths (assume the base paths are OK)
- if ( $this->isLegalRelPath( $relStoragePath ) ) {
- return $relStoragePath;
- }
- }
-
- return null;
- }
-
- /**
- * Sanity check a relative file system path for validity
- *
- * @param string $path Normalized relative path
- * @return bool
- */
- protected function isLegalRelPath( $path ) {
- // Check for file names longer than 255 chars
- if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS
- return false;
- }
- if ( wfIsWindows() ) { // NTFS
- return !preg_match( '![:*?"<>|]!', $path );
- } else {
- return true;
- }
- }
-
- /**
- * Given the short (unresolved) and full (resolved) name of
- * a container, return the file system path of the container.
- *
- * @param string $shortCont
- * @param string $fullCont
- * @return string|null
- */
- protected function containerFSRoot( $shortCont, $fullCont ) {
- if ( isset( $this->containerPaths[$shortCont] ) ) {
- return $this->containerPaths[$shortCont];
- } elseif ( isset( $this->basePath ) ) {
- return "{$this->basePath}/{$fullCont}";
- }
-
- return null; // no container base path defined
- }
-
- /**
- * Get the absolute file system path for a storage path
- *
- * @param string $storagePath Storage path
- * @return string|null
- */
- protected function resolveToFSPath( $storagePath ) {
- list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
- if ( $relPath === null ) {
- return null; // invalid
- }
- list( , $shortCont, ) = FileBackend::splitStoragePath( $storagePath );
- $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
- if ( $relPath != '' ) {
- $fsPath .= "/{$relPath}";
- }
-
- return $fsPath;
- }
-
- public function isPathUsableInternal( $storagePath ) {
- $fsPath = $this->resolveToFSPath( $storagePath );
- if ( $fsPath === null ) {
- return false; // invalid
- }
- $parentDir = dirname( $fsPath );
-
- if ( file_exists( $fsPath ) ) {
- $ok = is_file( $fsPath ) && is_writable( $fsPath );
- } else {
- $ok = is_dir( $parentDir ) && is_writable( $parentDir );
- }
-
- if ( $this->fileOwner !== null && $this->currentUser !== $this->fileOwner ) {
- $ok = false;
- trigger_error( __METHOD__ . ": PHP process owner is not '{$this->fileOwner}'." );
- }
-
- return $ok;
- }
-
- protected function doCreateInternal( array $params ) {
- $status = Status::newGood();
-
- $dest = $this->resolveToFSPath( $params['dst'] );
- if ( $dest === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
- return $status;
- }
-
- if ( !empty( $params['async'] ) ) { // deferred
- $tempFile = TempFSFile::factory( 'create_', 'tmp' );
- if ( !$tempFile ) {
- $status->fatal( 'backend-fail-create', $params['dst'] );
-
- return $status;
- }
- $this->trapWarnings();
- $bytes = file_put_contents( $tempFile->getPath(), $params['content'] );
- $this->untrapWarnings();
- if ( $bytes === false ) {
- $status->fatal( 'backend-fail-create', $params['dst'] );
-
- return $status;
- }
- $cmd = implode( ' ', [
- wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
- wfEscapeShellArg( $this->cleanPathSlashes( $tempFile->getPath() ) ),
- wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
- ] );
- $handler = function ( $errors, Status $status, array $params, $cmd ) {
- if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
- $status->fatal( 'backend-fail-create', $params['dst'] );
- trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
- }
- };
- $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
- $tempFile->bind( $status->value );
- } else { // immediate write
- $this->trapWarnings();
- $bytes = file_put_contents( $dest, $params['content'] );
- $this->untrapWarnings();
- if ( $bytes === false ) {
- $status->fatal( 'backend-fail-create', $params['dst'] );
-
- return $status;
- }
- $this->chmod( $dest );
- }
-
- return $status;
- }
-
- protected function doStoreInternal( array $params ) {
- $status = Status::newGood();
-
- $dest = $this->resolveToFSPath( $params['dst'] );
- if ( $dest === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
- return $status;
- }
-
- if ( !empty( $params['async'] ) ) { // deferred
- $cmd = implode( ' ', [
- wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
- wfEscapeShellArg( $this->cleanPathSlashes( $params['src'] ) ),
- wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
- ] );
- $handler = function ( $errors, Status $status, array $params, $cmd ) {
- if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
- $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
- trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
- }
- };
- $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
- } else { // immediate write
- $this->trapWarnings();
- $ok = copy( $params['src'], $dest );
- $this->untrapWarnings();
- // In some cases (at least over NFS), copy() returns true when it fails
- if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) {
- if ( $ok ) { // PHP bug
- unlink( $dest ); // remove broken file
- trigger_error( __METHOD__ . ": copy() failed but returned true." );
- }
- $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
-
- return $status;
- }
- $this->chmod( $dest );
- }
-
- return $status;
- }
-
- protected function doCopyInternal( array $params ) {
- $status = Status::newGood();
-
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
- return $status;
- }
-
- $dest = $this->resolveToFSPath( $params['dst'] );
- if ( $dest === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
- return $status;
- }
-
- if ( !is_file( $source ) ) {
- if ( empty( $params['ignoreMissingSource'] ) ) {
- $status->fatal( 'backend-fail-copy', $params['src'] );
- }
-
- return $status; // do nothing; either OK or bad status
- }
-
- if ( !empty( $params['async'] ) ) { // deferred
- $cmd = implode( ' ', [
- wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
- wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
- wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
- ] );
- $handler = function ( $errors, Status $status, array $params, $cmd ) {
- if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
- $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
- trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
- }
- };
- $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
- } else { // immediate write
- $this->trapWarnings();
- $ok = ( $source === $dest ) ? true : copy( $source, $dest );
- $this->untrapWarnings();
- // In some cases (at least over NFS), copy() returns true when it fails
- if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) {
- if ( $ok ) { // PHP bug
- $this->trapWarnings();
- unlink( $dest ); // remove broken file
- $this->untrapWarnings();
- trigger_error( __METHOD__ . ": copy() failed but returned true." );
- }
- $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
-
- return $status;
- }
- $this->chmod( $dest );
- }
-
- return $status;
- }
-
- protected function doMoveInternal( array $params ) {
- $status = Status::newGood();
-
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
- return $status;
- }
-
- $dest = $this->resolveToFSPath( $params['dst'] );
- if ( $dest === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
- return $status;
- }
-
- if ( !is_file( $source ) ) {
- if ( empty( $params['ignoreMissingSource'] ) ) {
- $status->fatal( 'backend-fail-move', $params['src'] );
- }
-
- return $status; // do nothing; either OK or bad status
- }
-
- if ( !empty( $params['async'] ) ) { // deferred
- $cmd = implode( ' ', [
- wfIsWindows() ? 'MOVE /Y' : 'mv', // (overwrite)
- wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
- wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
- ] );
- $handler = function ( $errors, Status $status, array $params, $cmd ) {
- if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
- $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
- trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
- }
- };
- $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
- } else { // immediate write
- $this->trapWarnings();
- $ok = ( $source === $dest ) ? true : rename( $source, $dest );
- $this->untrapWarnings();
- clearstatcache(); // file no longer at source
- if ( !$ok ) {
- $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
-
- return $status;
- }
- }
-
- return $status;
- }
-
- protected function doDeleteInternal( array $params ) {
- $status = Status::newGood();
-
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
- return $status;
- }
-
- if ( !is_file( $source ) ) {
- if ( empty( $params['ignoreMissingSource'] ) ) {
- $status->fatal( 'backend-fail-delete', $params['src'] );
- }
-
- return $status; // do nothing; either OK or bad status
- }
-
- if ( !empty( $params['async'] ) ) { // deferred
- $cmd = implode( ' ', [
- wfIsWindows() ? 'DEL' : 'unlink',
- wfEscapeShellArg( $this->cleanPathSlashes( $source ) )
- ] );
- $handler = function ( $errors, Status $status, array $params, $cmd ) {
- if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
- $status->fatal( 'backend-fail-delete', $params['src'] );
- trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
- }
- };
- $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
- } else { // immediate write
- $this->trapWarnings();
- $ok = unlink( $source );
- $this->untrapWarnings();
- if ( !$ok ) {
- $status->fatal( 'backend-fail-delete', $params['src'] );
-
- return $status;
- }
- }
-
- return $status;
- }
-
- /**
- * @param string $fullCont
- * @param string $dirRel
- * @param array $params
- * @return Status
- */
- protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
- $status = Status::newGood();
- list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
- $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
- $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
- $existed = is_dir( $dir ); // already there?
- // Create the directory and its parents as needed...
- $this->trapWarnings();
- if ( !wfMkdirParents( $dir ) ) {
- wfDebugLog( 'FSFileBackend', __METHOD__ . ": cannot create directory $dir" );
- $status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races
- } elseif ( !is_writable( $dir ) ) {
- wfDebugLog( 'FSFileBackend', __METHOD__ . ": directory $dir is read-only" );
- $status->fatal( 'directoryreadonlyerror', $params['dir'] );
- } elseif ( !is_readable( $dir ) ) {
- wfDebugLog( 'FSFileBackend', __METHOD__ . ": directory $dir is not readable" );
- $status->fatal( 'directorynotreadableerror', $params['dir'] );
- }
- $this->untrapWarnings();
- // Respect any 'noAccess' or 'noListing' flags...
- if ( is_dir( $dir ) && !$existed ) {
- $status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) );
- }
-
- return $status;
- }
-
- protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
- $status = Status::newGood();
- list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
- $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
- $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
- // Seed new directories with a blank index.html, to prevent crawling...
- if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
- $this->trapWarnings();
- $bytes = file_put_contents( "{$dir}/index.html", $this->indexHtmlPrivate() );
- $this->untrapWarnings();
- if ( $bytes === false ) {
- $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
- }
- }
- // Add a .htaccess file to the root of the container...
- if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) {
- $this->trapWarnings();
- $bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() );
- $this->untrapWarnings();
- if ( $bytes === false ) {
- $storeDir = "mwstore://{$this->name}/{$shortCont}";
- $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
- }
- }
-
- return $status;
- }
-
- protected function doPublishInternal( $fullCont, $dirRel, array $params ) {
- $status = Status::newGood();
- list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
- $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
- $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
- // Unseed new directories with a blank index.html, to allow crawling...
- if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) {
- $exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() );
- $this->trapWarnings();
- if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure()
- $status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' );
- }
- $this->untrapWarnings();
- }
- // Remove the .htaccess file from the root of the container...
- if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) {
- $exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() );
- $this->trapWarnings();
- if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure()
- $storeDir = "mwstore://{$this->name}/{$shortCont}";
- $status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" );
- }
- $this->untrapWarnings();
- }
-
- return $status;
- }
-
- protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
- $status = Status::newGood();
- list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
- $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
- $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
- $this->trapWarnings();
- if ( is_dir( $dir ) ) {
- rmdir( $dir ); // remove directory if empty
- }
- $this->untrapWarnings();
-
- return $status;
- }
-
- protected function doGetFileStat( array $params ) {
- $source = $this->resolveToFSPath( $params['src'] );
- if ( $source === null ) {
- return false; // invalid storage path
- }
-
- $this->trapWarnings(); // don't trust 'false' if there were errors
- $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
- $hadError = $this->untrapWarnings();
-
- if ( $stat ) {
- return [
- 'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ),
- 'size' => $stat['size']
- ];
- } elseif ( !$hadError ) {
- return false; // file does not exist
- } else {
- return null; // failure
- }
- }
-
- protected function doClearCache( array $paths = null ) {
- clearstatcache(); // clear the PHP file stat cache
- }
-
- protected function doDirectoryExists( $fullCont, $dirRel, array $params ) {
- list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
- $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
- $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
-
- $this->trapWarnings(); // don't trust 'false' if there were errors
- $exists = is_dir( $dir );
- $hadError = $this->untrapWarnings();
-
- return $hadError ? null : $exists;
- }
-
- /**
- * @see FileBackendStore::getDirectoryListInternal()
- * @param string $fullCont
- * @param string $dirRel
- * @param array $params
- * @return array|null
- */
- public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
- list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
- $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
- $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
- $exists = is_dir( $dir );
- if ( !$exists ) {
- wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
-
- return []; // nothing under this dir
- } elseif ( !is_readable( $dir ) ) {
- wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
-
- return null; // bad permissions?
- }
-
- return new FSFileBackendDirList( $dir, $params );
- }
-
- /**
- * @see FileBackendStore::getFileListInternal()
- * @param string $fullCont
- * @param string $dirRel
- * @param array $params
- * @return array|FSFileBackendFileList|null
- */
- public function getFileListInternal( $fullCont, $dirRel, array $params ) {
- list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
- $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
- $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
- $exists = is_dir( $dir );
- if ( !$exists ) {
- wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
-
- return []; // nothing under this dir
- } elseif ( !is_readable( $dir ) ) {
- wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
-
- return null; // bad permissions?
- }
-
- return new FSFileBackendFileList( $dir, $params );
- }
-
- protected function doGetLocalReferenceMulti( array $params ) {
- $fsFiles = []; // (path => FSFile)
-
- foreach ( $params['srcs'] as $src ) {
- $source = $this->resolveToFSPath( $src );
- if ( $source === null || !is_file( $source ) ) {
- $fsFiles[$src] = null; // invalid path or file does not exist
- } else {
- $fsFiles[$src] = new FSFile( $source );
- }
- }
-
- return $fsFiles;
- }
-
- protected function doGetLocalCopyMulti( array $params ) {
- $tmpFiles = []; // (path => TempFSFile)
-
- foreach ( $params['srcs'] as $src ) {
- $source = $this->resolveToFSPath( $src );
- if ( $source === null ) {
- $tmpFiles[$src] = null; // invalid path
- } else {
- // Create a new temporary file with the same extension...
- $ext = FileBackend::extensionFromPath( $src );
- $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
- if ( !$tmpFile ) {
- $tmpFiles[$src] = null;
- } else {
- $tmpPath = $tmpFile->getPath();
- // Copy the source file over the temp file
- $this->trapWarnings();
- $ok = copy( $source, $tmpPath );
- $this->untrapWarnings();
- if ( !$ok ) {
- $tmpFiles[$src] = null;
- } else {
- $this->chmod( $tmpPath );
- $tmpFiles[$src] = $tmpFile;
- }
- }
- }
- }
-
- return $tmpFiles;
- }
-
- protected function directoriesAreVirtual() {
- return false;
- }
-
- /**
- * @param FSFileOpHandle[] $fileOpHandles
- *
- * @return Status[]
- */
- protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
- $statuses = [];
-
- $pipes = [];
- foreach ( $fileOpHandles as $index => $fileOpHandle ) {
- $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' );
- }
-
- $errs = [];
- foreach ( $pipes as $index => $pipe ) {
- // Result will be empty on success in *NIX. On Windows,
- // it may be something like " 1 file(s) [copied|moved].".
- $errs[$index] = stream_get_contents( $pipe );
- fclose( $pipe );
- }
-
- foreach ( $fileOpHandles as $index => $fileOpHandle ) {
- $status = Status::newGood();
- $function = $fileOpHandle->call;
- $function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
- $statuses[$index] = $status;
- if ( $status->isOK() && $fileOpHandle->chmodPath ) {
- $this->chmod( $fileOpHandle->chmodPath );
- }
- }
-
- clearstatcache(); // files changed
- return $statuses;
- }
-
- /**
- * Chmod a file, suppressing the warnings
- *
- * @param string $path Absolute file system path
- * @return bool Success
- */
- protected function chmod( $path ) {
- $this->trapWarnings();
- $ok = chmod( $path, $this->fileMode );
- $this->untrapWarnings();
-
- return $ok;
- }
-
- /**
- * Return the text of an index.html file to hide directory listings
- *
- * @return string
- */
- protected function indexHtmlPrivate() {
- return '';
- }
-
- /**
- * Return the text of a .htaccess file to make a directory private
- *
- * @return string
- */
- protected function htaccessPrivate() {
- return "Deny from all\n";
- }
-
- /**
- * Clean up directory separators for the given OS
- *
- * @param string $path FS path
- * @return string
- */
- protected function cleanPathSlashes( $path ) {
- return wfIsWindows() ? strtr( $path, '/', '\\' ) : $path;
- }
-
- /**
- * Listen for E_WARNING errors and track whether any happen
- */
- protected function trapWarnings() {
- $this->hadWarningErrors[] = false; // push to stack
- set_error_handler( [ $this, 'handleWarning' ], E_WARNING );
- }
-
- /**
- * Stop listening for E_WARNING errors and return true if any happened
- *
- * @return bool
- */
- protected function untrapWarnings() {
- restore_error_handler(); // restore previous handler
- return array_pop( $this->hadWarningErrors ); // pop from stack
- }
-
- /**
- * @param int $errno
- * @param string $errstr
- * @return bool
- * @access private
- */
- public function handleWarning( $errno, $errstr ) {
- wfDebugLog( 'FSFileBackend', $errstr ); // more detailed error logging
- $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
-
- return true; // suppress from PHP handler
- }
-}
-
-/**
- * @see FileBackendStoreOpHandle
- */
-class FSFileOpHandle extends FileBackendStoreOpHandle {
- public $cmd; // string; shell command
- public $chmodPath; // string; file to chmod
-
- /**
- * @param FSFileBackend $backend
- * @param array $params
- * @param callable $call
- * @param string $cmd
- * @param int|null $chmodPath
- */
- public function __construct(
- FSFileBackend $backend, array $params, $call, $cmd, $chmodPath = null
- ) {
- $this->backend = $backend;
- $this->params = $params;
- $this->call = $call;
- $this->cmd = $cmd;
- $this->chmodPath = $chmodPath;
- }
-}
-
-/**
- * Wrapper around RecursiveDirectoryIterator/DirectoryIterator that
- * catches exception or does any custom behavoir that we may want.
- * Do not use this class from places outside FSFileBackend.
- *
- * @ingroup FileBackend
- */
-abstract class FSFileBackendList implements Iterator {
- /** @var Iterator */
- protected $iter;
-
- /** @var int */
- protected $suffixStart;
-
- /** @var int */
- protected $pos = 0;
-
- /** @var array */
- protected $params = [];
-
- /**
- * @param string $dir File system directory
- * @param array $params
- */
- public function __construct( $dir, array $params ) {
- $path = realpath( $dir ); // normalize
- if ( $path === false ) {
- $path = $dir;
- }
- $this->suffixStart = strlen( $path ) + 1; // size of "path/to/dir/"
- $this->params = $params;
-
- try {
- $this->iter = $this->initIterator( $path );
- } catch ( UnexpectedValueException $e ) {
- $this->iter = null; // bad permissions? deleted?
- }
- }
-
- /**
- * Return an appropriate iterator object to wrap
- *
- * @param string $dir File system directory
- * @return Iterator
- */
- protected function initIterator( $dir ) {
- if ( !empty( $this->params['topOnly'] ) ) { // non-recursive
- # Get an iterator that will get direct sub-nodes
- return new DirectoryIterator( $dir );
- } else { // recursive
- # Get an iterator that will return leaf nodes (non-directories)
- # RecursiveDirectoryIterator extends FilesystemIterator.
- # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
- $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS;
-
- return new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator( $dir, $flags ),
- RecursiveIteratorIterator::CHILD_FIRST // include dirs
- );
- }
- }
-
- /**
- * @see Iterator::key()
- * @return int
- */
- public function key() {
- return $this->pos;
- }
-
- /**
- * @see Iterator::current()
- * @return string|bool String or false
- */
- public function current() {
- return $this->getRelPath( $this->iter->current()->getPathname() );
- }
-
- /**
- * @see Iterator::next()
- * @throws FileBackendError
- */
- public function next() {
- try {
- $this->iter->next();
- $this->filterViaNext();
- } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
- throw new FileBackendError( "File iterator gave UnexpectedValueException." );
- }
- ++$this->pos;
- }
-
- /**
- * @see Iterator::rewind()
- * @throws FileBackendError
- */
- public function rewind() {
- $this->pos = 0;
- try {
- $this->iter->rewind();
- $this->filterViaNext();
- } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
- throw new FileBackendError( "File iterator gave UnexpectedValueException." );
- }
- }
-
- /**
- * @see Iterator::valid()
- * @return bool
- */
- public function valid() {
- return $this->iter && $this->iter->valid();
- }
-
- /**
- * Filter out items by advancing to the next ones
- */
- protected function filterViaNext() {
- }
-
- /**
- * Return only the relative path and normalize slashes to FileBackend-style.
- * Uses the "real path" since the suffix is based upon that.
- *
- * @param string $dir
- * @return string
- */
- protected function getRelPath( $dir ) {
- $path = realpath( $dir );
- if ( $path === false ) {
- $path = $dir;
- }
-
- return strtr( substr( $path, $this->suffixStart ), '\\', '/' );
- }
-}
-
-class FSFileBackendDirList extends FSFileBackendList {
- protected function filterViaNext() {
- while ( $this->iter->valid() ) {
- if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) {
- $this->iter->next(); // skip non-directories and dot files
- } else {
- break;
- }
- }
- }
-}
-
-class FSFileBackendFileList extends FSFileBackendList {
- protected function filterViaNext() {
- while ( $this->iter->valid() ) {
- if ( !$this->iter->current()->isFile() ) {
- $this->iter->next(); // skip non-files and dot files
- } else {
- break;
- }
- }
- }
-}
diff --git a/www/wiki/includes/filebackend/FileBackend.php b/www/wiki/includes/filebackend/FileBackend.php
deleted file mode 100644
index 03974f75..00000000
--- a/www/wiki/includes/filebackend/FileBackend.php
+++ /dev/null
@@ -1,1545 +0,0 @@
-<?php
-/**
- * @defgroup FileBackend File backend
- *
- * File backend is used to interact with file storage systems,
- * such as the local file system, NFS, or cloud storage systems.
- */
-
-/**
- * Base class for all file backends.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * @brief Base class for all file backend classes (including multi-write backends).
- *
- * This class defines the methods as abstract that subclasses must implement.
- * Outside callers can assume that all backends will have these functions.
- *
- * All "storage paths" are of the format "mwstore://<backend>/<container>/<path>".
- * The "backend" portion is unique name for MediaWiki to refer to a backend, while
- * the "container" portion is a top-level directory of the backend. The "path" portion
- * is a relative path that uses UNIX file system (FS) notation, though any particular
- * backend may not actually be using a local filesystem. Therefore, the relative paths
- * are only virtual.
- *
- * Backend contents are stored under wiki-specific container names by default.
- * Global (qualified) backends are achieved by configuring the "wiki ID" to a constant.
- * For legacy reasons, the FSFileBackend class allows manually setting the paths of
- * containers to ones that do not respect the "wiki ID".
- *
- * In key/value (object) stores, containers are the only hierarchy (the rest is emulated).
- * FS-based backends are somewhat more restrictive due to the existence of real
- * directory files; a regular file cannot have the same name as a directory. Other
- * backends with virtual directories may not have this limitation. Callers should
- * store files in such a way that no files and directories are under the same path.
- *
- * In general, this class allows for callers to access storage through the same
- * interface, without regard to the underlying storage system. However, calling code
- * must follow certain patterns and be aware of certain things to ensure compatibility:
- * - a) Always call prepare() on the parent directory before trying to put a file there;
- * key/value stores only need the container to exist first, but filesystems need
- * all the parent directories to exist first (prepare() is aware of all this)
- * - b) Always call clean() on a directory when it might become empty to avoid empty
- * directory buildup on filesystems; key/value stores never have empty directories,
- * so doing this helps preserve consistency in both cases
- * - c) Likewise, do not rely on the existence of empty directories for anything;
- * calling directoryExists() on a path that prepare() was previously called on
- * will return false for key/value stores if there are no files under that path
- * - d) Never alter the resulting FSFile returned from getLocalReference(), as it could
- * either be a copy of the source file in /tmp or the original source file itself
- * - e) Use a file layout that results in never attempting to store files over directories
- * or directories over files; key/value stores allow this but filesystems do not
- * - f) Use ASCII file names (e.g. base32, IDs, hashes) to avoid Unicode issues in Windows
- * - g) Do not assume that move operations are atomic (difficult with key/value stores)
- * - h) Do not assume that file stat or read operations always have immediate consistency;
- * various methods have a "latest" flag that should always be used if up-to-date
- * information is required (this trades performance for correctness as needed)
- * - i) Do not assume that directory listings have immediate consistency
- *
- * Methods of subclasses should avoid throwing exceptions at all costs.
- * As a corollary, external dependencies should be kept to a minimum.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-abstract class FileBackend {
- /** @var string Unique backend name */
- protected $name;
-
- /** @var string Unique wiki name */
- protected $wikiId;
-
- /** @var string Read-only explanation message */
- protected $readOnly;
-
- /** @var string When to do operations in parallel */
- protected $parallelize;
-
- /** @var int How many operations can be done in parallel */
- protected $concurrency;
-
- /** @var LockManager */
- protected $lockManager;
-
- /** @var FileJournal */
- protected $fileJournal;
-
- /** Bitfield flags for supported features */
- const ATTR_HEADERS = 1; // files can be tagged with standard HTTP headers
- const ATTR_METADATA = 2; // files can be stored with metadata key/values
- const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII)
-
- /**
- * Create a new backend instance from configuration.
- * This should only be called from within FileBackendGroup.
- *
- * @param array $config Parameters include:
- * - name : The unique name of this backend.
- * This should consist of alphanumberic, '-', and '_' characters.
- * This name should not be changed after use (e.g. with journaling).
- * Note that the name is *not* used in actual container names.
- * - wikiId : Prefix to container names that is unique to this backend.
- * It should only consist of alphanumberic, '-', and '_' characters.
- * This ID is what avoids collisions if multiple logical backends
- * use the same storage system, so this should be set carefully.
- * - lockManager : LockManager object to use for any file locking.
- * If not provided, then no file locking will be enforced.
- * - fileJournal : FileJournal object to use for logging changes to files.
- * If not provided, then change journaling will be disabled.
- * - readOnly : Write operations are disallowed if this is a non-empty string.
- * It should be an explanation for the backend being read-only.
- * - parallelize : When to do file operations in parallel (when possible).
- * Allowed values are "implicit", "explicit" and "off".
- * - concurrency : How many file operations can be done in parallel.
- * @throws FileBackendException
- */
- public function __construct( array $config ) {
- $this->name = $config['name'];
- $this->wikiId = $config['wikiId']; // e.g. "my_wiki-en_"
- if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
- throw new FileBackendException( "Backend name '{$this->name}' is invalid." );
- } elseif ( !is_string( $this->wikiId ) ) {
- throw new FileBackendException( "Backend wiki ID not provided for '{$this->name}'." );
- }
- $this->lockManager = isset( $config['lockManager'] )
- ? $config['lockManager']
- : new NullLockManager( [] );
- $this->fileJournal = isset( $config['fileJournal'] )
- ? $config['fileJournal']
- : FileJournal::factory( [ 'class' => 'NullFileJournal' ], $this->name );
- $this->readOnly = isset( $config['readOnly'] )
- ? (string)$config['readOnly']
- : '';
- $this->parallelize = isset( $config['parallelize'] )
- ? (string)$config['parallelize']
- : 'off';
- $this->concurrency = isset( $config['concurrency'] )
- ? (int)$config['concurrency']
- : 50;
- }
-
- /**
- * Get the unique backend name.
- * We may have multiple different backends of the same type.
- * For example, we can have two Swift backends using different proxies.
- *
- * @return string
- */
- final public function getName() {
- return $this->name;
- }
-
- /**
- * Get the wiki identifier used for this backend (possibly empty).
- * Note that this might *not* be in the same format as wfWikiID().
- *
- * @return string
- * @since 1.20
- */
- final public function getWikiId() {
- return $this->wikiId;
- }
-
- /**
- * Check if this backend is read-only
- *
- * @return bool
- */
- final public function isReadOnly() {
- return ( $this->readOnly != '' );
- }
-
- /**
- * Get an explanatory message if this backend is read-only
- *
- * @return string|bool Returns false if the backend is not read-only
- */
- final public function getReadOnlyReason() {
- return ( $this->readOnly != '' ) ? $this->readOnly : false;
- }
-
- /**
- * Get the a bitfield of extra features supported by the backend medium
- *
- * @return int Bitfield of FileBackend::ATTR_* flags
- * @since 1.23
- */
- public function getFeatures() {
- return self::ATTR_UNICODE_PATHS;
- }
-
- /**
- * Check if the backend medium supports a field of extra features
- *
- * @param int $bitfield Bitfield of FileBackend::ATTR_* flags
- * @return bool
- * @since 1.23
- */
- final public function hasFeatures( $bitfield ) {
- return ( $this->getFeatures() & $bitfield ) === $bitfield;
- }
-
- /**
- * This is the main entry point into the backend for write operations.
- * Callers supply an ordered list of operations to perform as a transaction.
- * Files will be locked, the stat cache cleared, and then the operations attempted.
- * If any serious errors occur, all attempted operations will be rolled back.
- *
- * $ops is an array of arrays. The outer array holds a list of operations.
- * Each inner array is a set of key value pairs that specify an operation.
- *
- * Supported operations and their parameters. The supported actions are:
- * - create
- * - store
- * - copy
- * - move
- * - delete
- * - describe (since 1.21)
- * - null
- *
- * FSFile/TempFSFile object support was added in 1.27.
- *
- * a) Create a new file in storage with the contents of a string
- * @code
- * array(
- * 'op' => 'create',
- * 'dst' => <storage path>,
- * 'content' => <string of new file contents>,
- * 'overwrite' => <boolean>,
- * 'overwriteSame' => <boolean>,
- * 'headers' => <HTTP header name/value map> # since 1.21
- * );
- * @endcode
- *
- * b) Copy a file system file into storage
- * @code
- * array(
- * 'op' => 'store',
- * 'src' => <file system path, FSFile, or TempFSFile>,
- * 'dst' => <storage path>,
- * 'overwrite' => <boolean>,
- * 'overwriteSame' => <boolean>,
- * 'headers' => <HTTP header name/value map> # since 1.21
- * )
- * @endcode
- *
- * c) Copy a file within storage
- * @code
- * array(
- * 'op' => 'copy',
- * 'src' => <storage path>,
- * 'dst' => <storage path>,
- * 'overwrite' => <boolean>,
- * 'overwriteSame' => <boolean>,
- * 'ignoreMissingSource' => <boolean>, # since 1.21
- * 'headers' => <HTTP header name/value map> # since 1.21
- * )
- * @endcode
- *
- * d) Move a file within storage
- * @code
- * array(
- * 'op' => 'move',
- * 'src' => <storage path>,
- * 'dst' => <storage path>,
- * 'overwrite' => <boolean>,
- * 'overwriteSame' => <boolean>,
- * 'ignoreMissingSource' => <boolean>, # since 1.21
- * 'headers' => <HTTP header name/value map> # since 1.21
- * )
- * @endcode
- *
- * e) Delete a file within storage
- * @code
- * array(
- * 'op' => 'delete',
- * 'src' => <storage path>,
- * 'ignoreMissingSource' => <boolean>
- * )
- * @endcode
- *
- * f) Update metadata for a file within storage
- * @code
- * array(
- * 'op' => 'describe',
- * 'src' => <storage path>,
- * 'headers' => <HTTP header name/value map>
- * )
- * @endcode
- *
- * g) Do nothing (no-op)
- * @code
- * array(
- * 'op' => 'null',
- * )
- * @endcode
- *
- * Boolean flags for operations (operation-specific):
- * - ignoreMissingSource : The operation will simply succeed and do
- * nothing if the source file does not exist.
- * - overwrite : Any destination file will be overwritten.
- * - overwriteSame : If a file already exists at the destination with the
- * same contents, then do nothing to the destination file
- * instead of giving an error. This does not compare headers.
- * This option is ignored if 'overwrite' is already provided.
- * - headers : If supplied, the result of merging these headers with any
- * existing source file headers (replacing conflicting ones)
- * will be set as the destination file headers. Headers are
- * deleted if their value is set to the empty string. When a
- * file has headers they are included in responses to GET and
- * HEAD requests to the backing store for that file.
- * Header values should be no larger than 255 bytes, except for
- * Content-Disposition. The system might ignore or truncate any
- * headers that are too long to store (exact limits will vary).
- * Backends that don't support metadata ignore this. (since 1.21)
- *
- * $opts is an associative of boolean flags, including:
- * - force : Operation precondition errors no longer trigger an abort.
- * Any remaining operations are still attempted. Unexpected
- * failures may still cause remaining operations to be aborted.
- * - nonLocking : No locks are acquired for the operations.
- * This can increase performance for non-critical writes.
- * This has no effect unless the 'force' flag is set.
- * - nonJournaled : Don't log this operation batch in the file journal.
- * This limits the ability of recovery scripts.
- * - parallelize : Try to do operations in parallel when possible.
- * - bypassReadOnly : Allow writes in read-only mode. (since 1.20)
- * - preserveCache : Don't clear the process cache before checking files.
- * This should only be used if all entries in the process
- * cache were added after the files were already locked. (since 1.20)
- *
- * @remarks Remarks on locking:
- * File system paths given to operations should refer to files that are
- * already locked or otherwise safe from modification from other processes.
- * Normally these files will be new temp files, which should be adequate.
- *
- * @par Return value:
- *
- * This returns a Status, which contains all warnings and fatals that occurred
- * during the operation. The 'failCount', 'successCount', and 'success' members
- * will reflect each operation attempted.
- *
- * The status will be "OK" unless:
- * - a) unexpected operation errors occurred (network partitions, disk full...)
- * - b) significant operation errors occurred and 'force' was not set
- *
- * @param array $ops List of operations to execute in order
- * @param array $opts Batch operation options
- * @return Status
- */
- final public function doOperations( array $ops, array $opts = [] ) {
- if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
- return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
- }
- if ( !count( $ops ) ) {
- return Status::newGood(); // nothing to do
- }
-
- $ops = $this->resolveFSFileObjects( $ops );
- if ( empty( $opts['force'] ) ) { // sanity
- unset( $opts['nonLocking'] );
- }
-
- /** @noinspection PhpUnusedLocalVariableInspection */
- $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
-
- return $this->doOperationsInternal( $ops, $opts );
- }
-
- /**
- * @see FileBackend::doOperations()
- * @param array $ops
- * @param array $opts
- */
- abstract protected function doOperationsInternal( array $ops, array $opts );
-
- /**
- * Same as doOperations() except it takes a single operation.
- * If you are doing a batch of operations that should either
- * all succeed or all fail, then use that function instead.
- *
- * @see FileBackend::doOperations()
- *
- * @param array $op Operation
- * @param array $opts Operation options
- * @return Status
- */
- final public function doOperation( array $op, array $opts = [] ) {
- return $this->doOperations( [ $op ], $opts );
- }
-
- /**
- * Performs a single create operation.
- * This sets $params['op'] to 'create' and passes it to doOperation().
- *
- * @see FileBackend::doOperation()
- *
- * @param array $params Operation parameters
- * @param array $opts Operation options
- * @return Status
- */
- final public function create( array $params, array $opts = [] ) {
- return $this->doOperation( [ 'op' => 'create' ] + $params, $opts );
- }
-
- /**
- * Performs a single store operation.
- * This sets $params['op'] to 'store' and passes it to doOperation().
- *
- * @see FileBackend::doOperation()
- *
- * @param array $params Operation parameters
- * @param array $opts Operation options
- * @return Status
- */
- final public function store( array $params, array $opts = [] ) {
- return $this->doOperation( [ 'op' => 'store' ] + $params, $opts );
- }
-
- /**
- * Performs a single copy operation.
- * This sets $params['op'] to 'copy' and passes it to doOperation().
- *
- * @see FileBackend::doOperation()
- *
- * @param array $params Operation parameters
- * @param array $opts Operation options
- * @return Status
- */
- final public function copy( array $params, array $opts = [] ) {
- return $this->doOperation( [ 'op' => 'copy' ] + $params, $opts );
- }
-
- /**
- * Performs a single move operation.
- * This sets $params['op'] to 'move' and passes it to doOperation().
- *
- * @see FileBackend::doOperation()
- *
- * @param array $params Operation parameters
- * @param array $opts Operation options
- * @return Status
- */
- final public function move( array $params, array $opts = [] ) {
- return $this->doOperation( [ 'op' => 'move' ] + $params, $opts );
- }
-
- /**
- * Performs a single delete operation.
- * This sets $params['op'] to 'delete' and passes it to doOperation().
- *
- * @see FileBackend::doOperation()
- *
- * @param array $params Operation parameters
- * @param array $opts Operation options
- * @return Status
- */
- final public function delete( array $params, array $opts = [] ) {
- return $this->doOperation( [ 'op' => 'delete' ] + $params, $opts );
- }
-
- /**
- * Performs a single describe operation.
- * This sets $params['op'] to 'describe' and passes it to doOperation().
- *
- * @see FileBackend::doOperation()
- *
- * @param array $params Operation parameters
- * @param array $opts Operation options
- * @return Status
- * @since 1.21
- */
- final public function describe( array $params, array $opts = [] ) {
- return $this->doOperation( [ 'op' => 'describe' ] + $params, $opts );
- }
-
- /**
- * Perform a set of independent file operations on some files.
- *
- * This does no locking, nor journaling, and possibly no stat calls.
- * Any destination files that already exist will be overwritten.
- * This should *only* be used on non-original files, like cache files.
- *
- * Supported operations and their parameters:
- * - create
- * - store
- * - copy
- * - move
- * - delete
- * - describe (since 1.21)
- * - null
- *
- * FSFile/TempFSFile object support was added in 1.27.
- *
- * a) Create a new file in storage with the contents of a string
- * @code
- * array(
- * 'op' => 'create',
- * 'dst' => <storage path>,
- * 'content' => <string of new file contents>,
- * 'headers' => <HTTP header name/value map> # since 1.21
- * )
- * @endcode
- *
- * b) Copy a file system file into storage
- * @code
- * array(
- * 'op' => 'store',
- * 'src' => <file system path, FSFile, or TempFSFile>,
- * 'dst' => <storage path>,
- * 'headers' => <HTTP header name/value map> # since 1.21
- * )
- * @endcode
- *
- * c) Copy a file within storage
- * @code
- * array(
- * 'op' => 'copy',
- * 'src' => <storage path>,
- * 'dst' => <storage path>,
- * 'ignoreMissingSource' => <boolean>, # since 1.21
- * 'headers' => <HTTP header name/value map> # since 1.21
- * )
- * @endcode
- *
- * d) Move a file within storage
- * @code
- * array(
- * 'op' => 'move',
- * 'src' => <storage path>,
- * 'dst' => <storage path>,
- * 'ignoreMissingSource' => <boolean>, # since 1.21
- * 'headers' => <HTTP header name/value map> # since 1.21
- * )
- * @endcode
- *
- * e) Delete a file within storage
- * @code
- * array(
- * 'op' => 'delete',
- * 'src' => <storage path>,
- * 'ignoreMissingSource' => <boolean>
- * )
- * @endcode
- *
- * f) Update metadata for a file within storage
- * @code
- * array(
- * 'op' => 'describe',
- * 'src' => <storage path>,
- * 'headers' => <HTTP header name/value map>
- * )
- * @endcode
- *
- * g) Do nothing (no-op)
- * @code
- * array(
- * 'op' => 'null',
- * )
- * @endcode
- *
- * @par Boolean flags for operations (operation-specific):
- * - ignoreMissingSource : The operation will simply succeed and do
- * nothing if the source file does not exist.
- * - headers : If supplied with a header name/value map, the backend will
- * reply with these headers when GETs/HEADs of the destination
- * file are made. Header values should be smaller than 256 bytes.
- * Content-Disposition headers can be longer, though the system
- * might ignore or truncate ones that are too long to store.
- * Existing headers will remain, but these will replace any
- * conflicting previous headers, and headers will be removed
- * if they are set to an empty string.
- * Backends that don't support metadata ignore this. (since 1.21)
- *
- * $opts is an associative of boolean flags, including:
- * - bypassReadOnly : Allow writes in read-only mode (since 1.20)
- *
- * @par Return value:
- * This returns a Status, which contains all warnings and fatals that occurred
- * during the operation. The 'failCount', 'successCount', and 'success' members
- * will reflect each operation attempted for the given files. The status will be
- * considered "OK" as long as no fatal errors occurred.
- *
- * @param array $ops Set of operations to execute
- * @param array $opts Batch operation options
- * @return Status
- * @since 1.20
- */
- final public function doQuickOperations( array $ops, array $opts = [] ) {
- if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
- return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
- }
- if ( !count( $ops ) ) {
- return Status::newGood(); // nothing to do
- }
-
- $ops = $this->resolveFSFileObjects( $ops );
- foreach ( $ops as &$op ) {
- $op['overwrite'] = true; // avoids RTTs in key/value stores
- }
-
- /** @noinspection PhpUnusedLocalVariableInspection */
- $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
-
- return $this->doQuickOperationsInternal( $ops );
- }
-
- /**
- * @see FileBackend::doQuickOperations()
- * @param array $ops
- * @since 1.20
- */
- abstract protected function doQuickOperationsInternal( array $ops );
-
- /**
- * Same as doQuickOperations() except it takes a single operation.
- * If you are doing a batch of operations, then use that function instead.
- *
- * @see FileBackend::doQuickOperations()
- *
- * @param array $op Operation
- * @return Status
- * @since 1.20
- */
- final public function doQuickOperation( array $op ) {
- return $this->doQuickOperations( [ $op ] );
- }
-
- /**
- * Performs a single quick create operation.
- * This sets $params['op'] to 'create' and passes it to doQuickOperation().
- *
- * @see FileBackend::doQuickOperation()
- *
- * @param array $params Operation parameters
- * @return Status
- * @since 1.20
- */
- final public function quickCreate( array $params ) {
- return $this->doQuickOperation( [ 'op' => 'create' ] + $params );
- }
-
- /**
- * Performs a single quick store operation.
- * This sets $params['op'] to 'store' and passes it to doQuickOperation().
- *
- * @see FileBackend::doQuickOperation()
- *
- * @param array $params Operation parameters
- * @return Status
- * @since 1.20
- */
- final public function quickStore( array $params ) {
- return $this->doQuickOperation( [ 'op' => 'store' ] + $params );
- }
-
- /**
- * Performs a single quick copy operation.
- * This sets $params['op'] to 'copy' and passes it to doQuickOperation().
- *
- * @see FileBackend::doQuickOperation()
- *
- * @param array $params Operation parameters
- * @return Status
- * @since 1.20
- */
- final public function quickCopy( array $params ) {
- return $this->doQuickOperation( [ 'op' => 'copy' ] + $params );
- }
-
- /**
- * Performs a single quick move operation.
- * This sets $params['op'] to 'move' and passes it to doQuickOperation().
- *
- * @see FileBackend::doQuickOperation()
- *
- * @param array $params Operation parameters
- * @return Status
- * @since 1.20
- */
- final public function quickMove( array $params ) {
- return $this->doQuickOperation( [ 'op' => 'move' ] + $params );
- }
-
- /**
- * Performs a single quick delete operation.
- * This sets $params['op'] to 'delete' and passes it to doQuickOperation().
- *
- * @see FileBackend::doQuickOperation()
- *
- * @param array $params Operation parameters
- * @return Status
- * @since 1.20
- */
- final public function quickDelete( array $params ) {
- return $this->doQuickOperation( [ 'op' => 'delete' ] + $params );
- }
-
- /**
- * Performs a single quick describe operation.
- * This sets $params['op'] to 'describe' and passes it to doQuickOperation().
- *
- * @see FileBackend::doQuickOperation()
- *
- * @param array $params Operation parameters
- * @return Status
- * @since 1.21
- */
- final public function quickDescribe( array $params ) {
- return $this->doQuickOperation( [ 'op' => 'describe' ] + $params );
- }
-
- /**
- * Concatenate a list of storage files into a single file system file.
- * The target path should refer to a file that is already locked or
- * otherwise safe from modification from other processes. Normally,
- * the file will be a new temp file, which should be adequate.
- *
- * @param array $params Operation parameters, include:
- * - srcs : ordered source storage paths (e.g. chunk1, chunk2, ...)
- * - dst : file system path to 0-byte temp file
- * - parallelize : try to do operations in parallel when possible
- * @return Status
- */
- abstract public function concatenate( array $params );
-
- /**
- * Prepare a storage directory for usage.
- * This will create any required containers and parent directories.
- * Backends using key/value stores only need to create the container.
- *
- * The 'noAccess' and 'noListing' parameters works the same as in secure(),
- * except they are only applied *if* the directory/container had to be created.
- * These flags should always be set for directories that have private files.
- * However, setting them is not guaranteed to actually do anything.
- * Additional server configuration may be needed to achieve the desired effect.
- *
- * @param array $params Parameters include:
- * - dir : storage directory
- * - noAccess : try to deny file access (since 1.20)
- * - noListing : try to deny file listing (since 1.20)
- * - bypassReadOnly : allow writes in read-only mode (since 1.20)
- * @return Status
- */
- final public function prepare( array $params ) {
- if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
- return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
- }
- /** @noinspection PhpUnusedLocalVariableInspection */
- $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
- return $this->doPrepare( $params );
- }
-
- /**
- * @see FileBackend::prepare()
- * @param array $params
- */
- abstract protected function doPrepare( array $params );
-
- /**
- * Take measures to block web access to a storage directory and
- * the container it belongs to. FS backends might add .htaccess
- * files whereas key/value store backends might revoke container
- * access to the storage user representing end-users in web requests.
- *
- * This is not guaranteed to actually make files or listings publically hidden.
- * Additional server configuration may be needed to achieve the desired effect.
- *
- * @param array $params Parameters include:
- * - dir : storage directory
- * - noAccess : try to deny file access
- * - noListing : try to deny file listing
- * - bypassReadOnly : allow writes in read-only mode (since 1.20)
- * @return Status
- */
- final public function secure( array $params ) {
- if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
- return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
- }
- /** @noinspection PhpUnusedLocalVariableInspection */
- $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
- return $this->doSecure( $params );
- }
-
- /**
- * @see FileBackend::secure()
- * @param array $params
- */
- abstract protected function doSecure( array $params );
-
- /**
- * Remove measures to block web access to a storage directory and
- * the container it belongs to. FS backends might remove .htaccess
- * files whereas key/value store backends might grant container
- * access to the storage user representing end-users in web requests.
- * This essentially can undo the result of secure() calls.
- *
- * This is not guaranteed to actually make files or listings publically viewable.
- * Additional server configuration may be needed to achieve the desired effect.
- *
- * @param array $params Parameters include:
- * - dir : storage directory
- * - access : try to allow file access
- * - listing : try to allow file listing
- * - bypassReadOnly : allow writes in read-only mode (since 1.20)
- * @return Status
- * @since 1.20
- */
- final public function publish( array $params ) {
- if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
- return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
- }
- /** @noinspection PhpUnusedLocalVariableInspection */
- $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
- return $this->doPublish( $params );
- }
-
- /**
- * @see FileBackend::publish()
- * @param array $params
- */
- abstract protected function doPublish( array $params );
-
- /**
- * Delete a storage directory if it is empty.
- * Backends using key/value stores may do nothing unless the directory
- * is that of an empty container, in which case it will be deleted.
- *
- * @param array $params Parameters include:
- * - dir : storage directory
- * - recursive : recursively delete empty subdirectories first (since 1.20)
- * - bypassReadOnly : allow writes in read-only mode (since 1.20)
- * @return Status
- */
- final public function clean( array $params ) {
- if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
- return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
- }
- /** @noinspection PhpUnusedLocalVariableInspection */
- $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
- return $this->doClean( $params );
- }
-
- /**
- * @see FileBackend::clean()
- * @param array $params
- */
- abstract protected function doClean( array $params );
-
- /**
- * Enter file operation scope.
- * This just makes PHP ignore user aborts/disconnects until the return
- * value leaves scope. This returns null and does nothing in CLI mode.
- *
- * @return ScopedCallback|null
- */
- final protected function getScopedPHPBehaviorForOps() {
- if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
- $old = ignore_user_abort( true ); // avoid half-finished operations
- return new ScopedCallback( function () use ( $old ) {
- ignore_user_abort( $old );
- } );
- }
-
- return null;
- }
-
- /**
- * Check if a file exists at a storage path in the backend.
- * This returns false if only a directory exists at the path.
- *
- * @param array $params Parameters include:
- * - src : source storage path
- * - latest : use the latest available data
- * @return bool|null Returns null on failure
- */
- abstract public function fileExists( array $params );
-
- /**
- * Get the last-modified timestamp of the file at a storage path.
- *
- * @param array $params Parameters include:
- * - src : source storage path
- * - latest : use the latest available data
- * @return string|bool TS_MW timestamp or false on failure
- */
- abstract public function getFileTimestamp( array $params );
-
- /**
- * Get the contents of a file at a storage path in the backend.
- * This should be avoided for potentially large files.
- *
- * @param array $params Parameters include:
- * - src : source storage path
- * - latest : use the latest available data
- * @return string|bool Returns false on failure
- */
- final public function getFileContents( array $params ) {
- $contents = $this->getFileContentsMulti(
- [ 'srcs' => [ $params['src'] ] ] + $params );
-
- return $contents[$params['src']];
- }
-
- /**
- * Like getFileContents() except it takes an array of storage paths
- * and returns a map of storage paths to strings (or null on failure).
- * The map keys (paths) are in the same order as the provided list of paths.
- *
- * @see FileBackend::getFileContents()
- *
- * @param array $params Parameters include:
- * - srcs : list of source storage paths
- * - latest : use the latest available data
- * - parallelize : try to do operations in parallel when possible
- * @return array Map of (path name => string or false on failure)
- * @since 1.20
- */
- abstract public function getFileContentsMulti( array $params );
-
- /**
- * Get metadata about a file at a storage path in the backend.
- * If the file does not exist, then this returns false.
- * Otherwise, the result is an associative array that includes:
- * - headers : map of HTTP headers used for GET/HEAD requests (name => value)
- * - metadata : map of file metadata (name => value)
- * Metadata keys and headers names will be returned in all lower-case.
- * Additional values may be included for internal use only.
- *
- * Use FileBackend::hasFeatures() to check how well this is supported.
- *
- * @param array $params
- * $params include:
- * - src : source storage path
- * - latest : use the latest available data
- * @return array|bool Returns false on failure
- * @since 1.23
- */
- abstract public function getFileXAttributes( array $params );
-
- /**
- * Get the size (bytes) of a file at a storage path in the backend.
- *
- * @param array $params Parameters include:
- * - src : source storage path
- * - latest : use the latest available data
- * @return int|bool Returns false on failure
- */
- abstract public function getFileSize( array $params );
-
- /**
- * Get quick information about a file at a storage path in the backend.
- * If the file does not exist, then this returns false.
- * Otherwise, the result is an associative array that includes:
- * - mtime : the last-modified timestamp (TS_MW)
- * - size : the file size (bytes)
- * Additional values may be included for internal use only.
- *
- * @param array $params Parameters include:
- * - src : source storage path
- * - latest : use the latest available data
- * @return array|bool|null Returns null on failure
- */
- abstract public function getFileStat( array $params );
-
- /**
- * Get a SHA-1 hash of the file at a storage path in the backend.
- *
- * @param array $params Parameters include:
- * - src : source storage path
- * - latest : use the latest available data
- * @return string|bool Hash string or false on failure
- */
- abstract public function getFileSha1Base36( array $params );
-
- /**
- * Get the properties of the file at a storage path in the backend.
- * This gives the result of FSFile::getProps() on a local copy of the file.
- *
- * @param array $params Parameters include:
- * - src : source storage path
- * - latest : use the latest available data
- * @return array Returns FSFile::placeholderProps() on failure
- */
- abstract public function getFileProps( array $params );
-
- /**
- * Stream the file at a storage path in the backend.
- * If the file does not exists, an HTTP 404 error will be given.
- * Appropriate HTTP headers (Status, Content-Type, Content-Length)
- * will be sent if streaming began, while none will be sent otherwise.
- * Implementations should flush the output buffer before sending data.
- *
- * @param array $params Parameters include:
- * - src : source storage path
- * - headers : list of additional HTTP headers to send on success
- * - latest : use the latest available data
- * @return Status
- */
- abstract public function streamFile( array $params );
-
- /**
- * Returns a file system file, identical to the file at a storage path.
- * The file returned is either:
- * - a) A local copy of the file at a storage path in the backend.
- * The temporary copy will have the same extension as the source.
- * - b) An original of the file at a storage path in the backend.
- * Temporary files may be purged when the file object falls out of scope.
- *
- * Write operations should *never* be done on this file as some backends
- * may do internal tracking or may be instances of FileBackendMultiWrite.
- * In that later case, there are copies of the file that must stay in sync.
- * Additionally, further calls to this function may return the same file.
- *
- * @param array $params Parameters include:
- * - src : source storage path
- * - latest : use the latest available data
- * @return FSFile|null Returns null on failure
- */
- final public function getLocalReference( array $params ) {
- $fsFiles = $this->getLocalReferenceMulti(
- [ 'srcs' => [ $params['src'] ] ] + $params );
-
- return $fsFiles[$params['src']];
- }
-
- /**
- * Like getLocalReference() except it takes an array of storage paths
- * and returns a map of storage paths to FSFile objects (or null on failure).
- * The map keys (paths) are in the same order as the provided list of paths.
- *
- * @see FileBackend::getLocalReference()
- *
- * @param array $params Parameters include:
- * - srcs : list of source storage paths
- * - latest : use the latest available data
- * - parallelize : try to do operations in parallel when possible
- * @return array Map of (path name => FSFile or null on failure)
- * @since 1.20
- */
- abstract public function getLocalReferenceMulti( array $params );
-
- /**
- * Get a local copy on disk of the file at a storage path in the backend.
- * The temporary copy will have the same file extension as the source.
- * Temporary files may be purged when the file object falls out of scope.
- *
- * @param array $params Parameters include:
- * - src : source storage path
- * - latest : use the latest available data
- * @return TempFSFile|null Returns null on failure
- */
- final public function getLocalCopy( array $params ) {
- $tmpFiles = $this->getLocalCopyMulti(
- [ 'srcs' => [ $params['src'] ] ] + $params );
-
- return $tmpFiles[$params['src']];
- }
-
- /**
- * Like getLocalCopy() except it takes an array of storage paths and
- * returns a map of storage paths to TempFSFile objects (or null on failure).
- * The map keys (paths) are in the same order as the provided list of paths.
- *
- * @see FileBackend::getLocalCopy()
- *
- * @param array $params Parameters include:
- * - srcs : list of source storage paths
- * - latest : use the latest available data
- * - parallelize : try to do operations in parallel when possible
- * @return array Map of (path name => TempFSFile or null on failure)
- * @since 1.20
- */
- abstract public function getLocalCopyMulti( array $params );
-
- /**
- * Return an HTTP URL to a given file that requires no authentication to use.
- * The URL may be pre-authenticated (via some token in the URL) and temporary.
- * This will return null if the backend cannot make an HTTP URL for the file.
- *
- * This is useful for key/value stores when using scripts that seek around
- * large files and those scripts (and the backend) support HTTP Range headers.
- * Otherwise, one would need to use getLocalReference(), which involves loading
- * the entire file on to local disk.
- *
- * @param array $params Parameters include:
- * - src : source storage path
- * - ttl : lifetime (seconds) if pre-authenticated; default is 1 day
- * @return string|null
- * @since 1.21
- */
- abstract public function getFileHttpUrl( array $params );
-
- /**
- * Check if a directory exists at a given storage path.
- * Backends using key/value stores will check if the path is a
- * virtual directory, meaning there are files under the given directory.
- *
- * Storage backends with eventual consistency might return stale data.
- *
- * @param array $params Parameters include:
- * - dir : storage directory
- * @return bool|null Returns null on failure
- * @since 1.20
- */
- abstract public function directoryExists( array $params );
-
- /**
- * Get an iterator to list *all* directories under a storage directory.
- * If the directory is of the form "mwstore://backend/container",
- * then all directories in the container will be listed.
- * If the directory is of form "mwstore://backend/container/dir",
- * then all directories directly under that directory will be listed.
- * Results will be storage directories relative to the given directory.
- *
- * Storage backends with eventual consistency might return stale data.
- *
- * Failures during iteration can result in FileBackendError exceptions (since 1.22).
- *
- * @param array $params Parameters include:
- * - dir : storage directory
- * - topOnly : only return direct child dirs of the directory
- * @return Traversable|array|null Returns null on failure
- * @since 1.20
- */
- abstract public function getDirectoryList( array $params );
-
- /**
- * Same as FileBackend::getDirectoryList() except only lists
- * directories that are immediately under the given directory.
- *
- * Storage backends with eventual consistency might return stale data.
- *
- * Failures during iteration can result in FileBackendError exceptions (since 1.22).
- *
- * @param array $params Parameters include:
- * - dir : storage directory
- * @return Traversable|array|null Returns null on failure
- * @since 1.20
- */
- final public function getTopDirectoryList( array $params ) {
- return $this->getDirectoryList( [ 'topOnly' => true ] + $params );
- }
-
- /**
- * Get an iterator to list *all* stored files under a storage directory.
- * If the directory is of the form "mwstore://backend/container",
- * then all files in the container will be listed.
- * If the directory is of form "mwstore://backend/container/dir",
- * then all files under that directory will be listed.
- * Results will be storage paths relative to the given directory.
- *
- * Storage backends with eventual consistency might return stale data.
- *
- * Failures during iteration can result in FileBackendError exceptions (since 1.22).
- *
- * @param array $params Parameters include:
- * - dir : storage directory
- * - topOnly : only return direct child files of the directory (since 1.20)
- * - adviseStat : set to true if stat requests will be made on the files (since 1.22)
- * @return Traversable|array|null Returns null on failure
- */
- abstract public function getFileList( array $params );
-
- /**
- * Same as FileBackend::getFileList() except only lists
- * files that are immediately under the given directory.
- *
- * Storage backends with eventual consistency might return stale data.
- *
- * Failures during iteration can result in FileBackendError exceptions (since 1.22).
- *
- * @param array $params Parameters include:
- * - dir : storage directory
- * - adviseStat : set to true if stat requests will be made on the files (since 1.22)
- * @return Traversable|array|null Returns null on failure
- * @since 1.20
- */
- final public function getTopFileList( array $params ) {
- return $this->getFileList( [ 'topOnly' => true ] + $params );
- }
-
- /**
- * Preload persistent file stat cache and property cache into in-process cache.
- * This should be used when stat calls will be made on a known list of a many files.
- *
- * @see FileBackend::getFileStat()
- *
- * @param array $paths Storage paths
- */
- abstract public function preloadCache( array $paths );
-
- /**
- * Invalidate any in-process file stat and property cache.
- * If $paths is given, then only the cache for those files will be cleared.
- *
- * @see FileBackend::getFileStat()
- *
- * @param array $paths Storage paths (optional)
- */
- abstract public function clearCache( array $paths = null );
-
- /**
- * Preload file stat information (concurrently if possible) into in-process cache.
- *
- * This should be used when stat calls will be made on a known list of a many files.
- * This does not make use of the persistent file stat cache.
- *
- * @see FileBackend::getFileStat()
- *
- * @param array $params Parameters include:
- * - srcs : list of source storage paths
- * - latest : use the latest available data
- * @return bool All requests proceeded without I/O errors (since 1.24)
- * @since 1.23
- */
- abstract public function preloadFileStat( array $params );
-
- /**
- * Lock the files at the given storage paths in the backend.
- * This will either lock all the files or none (on failure).
- *
- * Callers should consider using getScopedFileLocks() instead.
- *
- * @param array $paths Storage paths
- * @param int $type LockManager::LOCK_* constant
- * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24)
- * @return Status
- */
- final public function lockFiles( array $paths, $type, $timeout = 0 ) {
- $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
-
- return $this->lockManager->lock( $paths, $type, $timeout );
- }
-
- /**
- * Unlock the files at the given storage paths in the backend.
- *
- * @param array $paths Storage paths
- * @param int $type LockManager::LOCK_* constant
- * @return Status
- */
- final public function unlockFiles( array $paths, $type ) {
- $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
-
- return $this->lockManager->unlock( $paths, $type );
- }
-
- /**
- * Lock the files at the given storage paths in the backend.
- * This will either lock all the files or none (on failure).
- * On failure, the status object will be updated with errors.
- *
- * Once the return value goes out scope, the locks will be released and
- * the status updated. Unlock fatals will not change the status "OK" value.
- *
- * @see ScopedLock::factory()
- *
- * @param array $paths List of storage paths or map of lock types to path lists
- * @param int|string $type LockManager::LOCK_* constant or "mixed"
- * @param Status $status Status to update on lock/unlock
- * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24)
- * @return ScopedLock|null Returns null on failure
- */
- final public function getScopedFileLocks( array $paths, $type, Status $status, $timeout = 0 ) {
- if ( $type === 'mixed' ) {
- foreach ( $paths as &$typePaths ) {
- $typePaths = array_map( 'FileBackend::normalizeStoragePath', $typePaths );
- }
- } else {
- $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
- }
-
- return ScopedLock::factory( $this->lockManager, $paths, $type, $status, $timeout );
- }
-
- /**
- * Get an array of scoped locks needed for a batch of file operations.
- *
- * Normally, FileBackend::doOperations() handles locking, unless
- * the 'nonLocking' param is passed in. This function is useful if you
- * want the files to be locked for a broader scope than just when the
- * files are changing. For example, if you need to update DB metadata,
- * you may want to keep the files locked until finished.
- *
- * @see FileBackend::doOperations()
- *
- * @param array $ops List of file operations to FileBackend::doOperations()
- * @param Status $status Status to update on lock/unlock
- * @return ScopedLock|null
- * @since 1.20
- */
- abstract public function getScopedLocksForOps( array $ops, Status $status );
-
- /**
- * Get the root storage path of this backend.
- * All container paths are "subdirectories" of this path.
- *
- * @return string Storage path
- * @since 1.20
- */
- final public function getRootStoragePath() {
- return "mwstore://{$this->name}";
- }
-
- /**
- * Get the storage path for the given container for this backend
- *
- * @param string $container Container name
- * @return string Storage path
- * @since 1.21
- */
- final public function getContainerStoragePath( $container ) {
- return $this->getRootStoragePath() . "/{$container}";
- }
-
- /**
- * Get the file journal object for this backend
- *
- * @return FileJournal
- */
- final public function getJournal() {
- return $this->fileJournal;
- }
-
- /**
- * Convert FSFile 'src' paths to string paths (with an 'srcRef' field set to the FSFile)
- *
- * The 'srcRef' field keeps any TempFSFile objects in scope for the backend to have it
- * around as long it needs (which may vary greatly depending on configuration)
- *
- * @param array $ops File operation batch for FileBaclend::doOperations()
- * @return array File operation batch
- */
- protected function resolveFSFileObjects( array $ops ) {
- foreach ( $ops as &$op ) {
- $src = isset( $op['src'] ) ? $op['src'] : null;
- if ( $src instanceof FSFile ) {
- $op['srcRef'] = $src;
- $op['src'] = $src->getPath();
- }
- }
- unset( $op );
-
- return $ops;
- }
-
- /**
- * Check if a given path is a "mwstore://" path.
- * This does not do any further validation or any existence checks.
- *
- * @param string $path
- * @return bool
- */
- final public static function isStoragePath( $path ) {
- return ( strpos( $path, 'mwstore://' ) === 0 );
- }
-
- /**
- * Split a storage path into a backend name, a container name,
- * and a relative file path. The relative path may be the empty string.
- * This does not do any path normalization or traversal checks.
- *
- * @param string $storagePath
- * @return array (backend, container, rel object) or (null, null, null)
- */
- final public static function splitStoragePath( $storagePath ) {
- if ( self::isStoragePath( $storagePath ) ) {
- // Remove the "mwstore://" prefix and split the path
- $parts = explode( '/', substr( $storagePath, 10 ), 3 );
- if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
- if ( count( $parts ) == 3 ) {
- return $parts; // e.g. "backend/container/path"
- } else {
- return [ $parts[0], $parts[1], '' ]; // e.g. "backend/container"
- }
- }
- }
-
- return [ null, null, null ];
- }
-
- /**
- * Normalize a storage path by cleaning up directory separators.
- * Returns null if the path is not of the format of a valid storage path.
- *
- * @param string $storagePath
- * @return string|null
- */
- final public static function normalizeStoragePath( $storagePath ) {
- list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
- if ( $relPath !== null ) { // must be for this backend
- $relPath = self::normalizeContainerPath( $relPath );
- if ( $relPath !== null ) {
- return ( $relPath != '' )
- ? "mwstore://{$backend}/{$container}/{$relPath}"
- : "mwstore://{$backend}/{$container}";
- }
- }
-
- return null;
- }
-
- /**
- * Get the parent storage directory of a storage path.
- * This returns a path like "mwstore://backend/container",
- * "mwstore://backend/container/...", or null if there is no parent.
- *
- * @param string $storagePath
- * @return string|null
- */
- final public static function parentStoragePath( $storagePath ) {
- $storagePath = dirname( $storagePath );
- list( , , $rel ) = self::splitStoragePath( $storagePath );
-
- return ( $rel === null ) ? null : $storagePath;
- }
-
- /**
- * Get the final extension from a storage or FS path
- *
- * @param string $path
- * @param string $case One of (rawcase, uppercase, lowercase) (since 1.24)
- * @return string
- */
- final public static function extensionFromPath( $path, $case = 'lowercase' ) {
- $i = strrpos( $path, '.' );
- $ext = $i ? substr( $path, $i + 1 ) : '';
-
- if ( $case === 'lowercase' ) {
- $ext = strtolower( $ext );
- } elseif ( $case === 'uppercase' ) {
- $ext = strtoupper( $ext );
- }
-
- return $ext;
- }
-
- /**
- * Check if a relative path has no directory traversals
- *
- * @param string $path
- * @return bool
- * @since 1.20
- */
- final public static function isPathTraversalFree( $path ) {
- return ( self::normalizeContainerPath( $path ) !== null );
- }
-
- /**
- * Build a Content-Disposition header value per RFC 6266.
- *
- * @param string $type One of (attachment, inline)
- * @param string $filename Suggested file name (should not contain slashes)
- * @throws FileBackendError
- * @return string
- * @since 1.20
- */
- final public static function makeContentDisposition( $type, $filename = '' ) {
- $parts = [];
-
- $type = strtolower( $type );
- if ( !in_array( $type, [ 'inline', 'attachment' ] ) ) {
- throw new FileBackendError( "Invalid Content-Disposition type '$type'." );
- }
- $parts[] = $type;
-
- if ( strlen( $filename ) ) {
- $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) );
- }
-
- return implode( ';', $parts );
- }
-
- /**
- * Validate and normalize a relative storage path.
- * Null is returned if the path involves directory traversal.
- * Traversal is insecure for FS backends and broken for others.
- *
- * This uses the same traversal protection as Title::secureAndSplit().
- *
- * @param string $path Storage path relative to a container
- * @return string|null
- */
- final protected static function normalizeContainerPath( $path ) {
- // Normalize directory separators
- $path = strtr( $path, '\\', '/' );
- // Collapse any consecutive directory separators
- $path = preg_replace( '![/]{2,}!', '/', $path );
- // Remove any leading directory separator
- $path = ltrim( $path, '/' );
- // Use the same traversal protection as Title::secureAndSplit()
- if ( strpos( $path, '.' ) !== false ) {
- if (
- $path === '.' ||
- $path === '..' ||
- strpos( $path, './' ) === 0 ||
- strpos( $path, '../' ) === 0 ||
- strpos( $path, '/./' ) !== false ||
- strpos( $path, '/../' ) !== false
- ) {
- return null;
- }
- }
-
- return $path;
- }
-}
-
-/**
- * Generic file backend exception for checked and unexpected (e.g. config) exceptions
- *
- * @ingroup FileBackend
- * @since 1.23
- */
-class FileBackendException extends Exception {
-}
-
-/**
- * File backend exception for checked exceptions (e.g. I/O errors)
- *
- * @ingroup FileBackend
- * @since 1.22
- */
-class FileBackendError extends FileBackendException {
-}
diff --git a/www/wiki/includes/filebackend/FileBackendGroup.php b/www/wiki/includes/filebackend/FileBackendGroup.php
index 5d0da6d3..454b6332 100644
--- a/www/wiki/includes/filebackend/FileBackendGroup.php
+++ b/www/wiki/includes/filebackend/FileBackendGroup.php
@@ -20,7 +20,8 @@
* @file
* @ingroup FileBackend
*/
-use \MediaWiki\Logger\LoggerFactory;
+
+use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
/**
@@ -90,7 +91,7 @@ class FileBackendGroup {
// Get the FS backend configuration
$autoBackends[] = [
'name' => $backendName,
- 'class' => 'FSFileBackend',
+ 'class' => FSFileBackend::class,
'lockManager' => 'fsLockManager',
'containerPaths' => [
"{$repoName}-public" => "{$directory}",
@@ -111,7 +112,7 @@ class FileBackendGroup {
/**
* Register an array of file backend configurations
*
- * @param array $configs
+ * @param array[] $configs
* @param string|null $readOnlyReason
* @throws InvalidArgumentException
*/
@@ -154,7 +155,7 @@ class FileBackendGroup {
$config = $this->config( $name );
$class = $config['class'];
- if ( $class === 'FileBackendMultiWrite' ) {
+ if ( $class === FileBackendMultiWrite::class ) {
foreach ( $config['backends'] as $index => $beConfig ) {
if ( isset( $beConfig['template'] ) ) {
// Config is just a modified version of a registered backend's.
@@ -189,9 +190,9 @@ class FileBackendGroup {
'wikiId' => wfWikiID(), // e.g. "my_wiki-en_"
'mimeCallback' => [ $this, 'guessMimeInternal' ],
'obResetFunc' => 'wfResetOutputBuffers',
- 'streamMimeFunc' => [ 'StreamFile', 'contentTypeFromPath' ],
+ 'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ],
'tmpDirectory' => wfTempDir(),
- 'statusWrapper' => [ 'Status', 'wrap' ],
+ 'statusWrapper' => [ Status::class, 'wrap' ],
'wanCache' => MediaWikiServices::getInstance()->getMainWANObjectCache(),
'srvCache' => ObjectCache::getLocalServerInstance( 'hash' ),
'logger' => LoggerFactory::getInstance( 'FileOperation' ),
@@ -201,7 +202,7 @@ class FileBackendGroup {
LockManagerGroup::singleton( $config['wikiId'] )->get( $config['lockManager'] );
$config['fileJournal'] = isset( $config['fileJournal'] )
? FileJournal::factory( $config['fileJournal'], $name )
- : FileJournal::factory( [ 'class' => 'NullFileJournal' ], $name );
+ : FileJournal::factory( [ 'class' => NullFileJournal::class ], $name );
return $config;
}
@@ -229,7 +230,7 @@ class FileBackendGroup {
* @since 1.27
*/
public function guessMimeInternal( $storagePath, $content, $fsPath ) {
- $magic = MimeMagic::singleton();
+ $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
// Trust the extension of the storage path (caller must validate)
$ext = FileBackend::extensionFromPath( $storagePath );
$type = $magic->guessTypesForExtension( $ext );
diff --git a/www/wiki/includes/filebackend/FileBackendMultiWrite.php b/www/wiki/includes/filebackend/FileBackendMultiWrite.php
deleted file mode 100644
index 3b200482..00000000
--- a/www/wiki/includes/filebackend/FileBackendMultiWrite.php
+++ /dev/null
@@ -1,761 +0,0 @@
-<?php
-/**
- * Proxy backend that mirrors writes to several internal backends.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * @brief Proxy backend that mirrors writes to several internal backends.
- *
- * This class defines a multi-write backend. Multiple backends can be
- * registered to this proxy backend and it will act as a single backend.
- * Use this when all access to those backends is through this proxy backend.
- * At least one of the backends must be declared the "master" backend.
- *
- * Only use this class when transitioning from one storage system to another.
- *
- * Read operations are only done on the 'master' backend for consistency.
- * Write operations are performed on all backends, starting with the master.
- * This makes a best-effort to have transactional semantics, but since requests
- * may sometimes fail, the use of "autoResync" or background scripts to fix
- * inconsistencies is important.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-class FileBackendMultiWrite extends FileBackend {
- /** @var FileBackendStore[] Prioritized list of FileBackendStore objects */
- protected $backends = [];
-
- /** @var int Index of master backend */
- protected $masterIndex = -1;
- /** @var int Index of read affinity backend */
- protected $readIndex = -1;
-
- /** @var int Bitfield */
- protected $syncChecks = 0;
- /** @var string|bool */
- protected $autoResync = false;
-
- /** @var bool */
- protected $asyncWrites = false;
-
- /* Possible internal backend consistency checks */
- const CHECK_SIZE = 1;
- const CHECK_TIME = 2;
- const CHECK_SHA1 = 4;
-
- /**
- * Construct a proxy backend that consists of several internal backends.
- * Locking, journaling, and read-only checks are handled by the proxy backend.
- *
- * Additional $config params include:
- * - backends : Array of backend config and multi-backend settings.
- * Each value is the config used in the constructor of a
- * FileBackendStore class, but with these additional settings:
- * - class : The name of the backend class
- * - isMultiMaster : This must be set for one backend.
- * - readAffinity : Use this for reads without 'latest' set.
- * - template: : If given a backend name, this will use
- * the config of that backend as a template.
- * Values specified here take precedence.
- * - syncChecks : Integer bitfield of internal backend sync checks to perform.
- * Possible bits include the FileBackendMultiWrite::CHECK_* constants.
- * There are constants for SIZE, TIME, and SHA1.
- * The checks are done before allowing any file operations.
- * - autoResync : Automatically resync the clone backends to the master backend
- * when pre-operation sync checks fail. This should only be used
- * if the master backend is stable and not missing any files.
- * Use "conservative" to limit resyncing to copying newer master
- * backend files over older (or non-existing) clone backend files.
- * Cases that cannot be handled will result in operation abortion.
- * - replication : Set to 'async' to defer file operations on the non-master backends.
- * This will apply such updates post-send for web requests. Note that
- * any checks from "syncChecks" are still synchronous.
- *
- * @param array $config
- * @throws FileBackendError
- */
- public function __construct( array $config ) {
- parent::__construct( $config );
- $this->syncChecks = isset( $config['syncChecks'] )
- ? $config['syncChecks']
- : self::CHECK_SIZE;
- $this->autoResync = isset( $config['autoResync'] )
- ? $config['autoResync']
- : false;
- $this->asyncWrites = isset( $config['replication'] ) && $config['replication'] === 'async';
- // Construct backends here rather than via registration
- // to keep these backends hidden from outside the proxy.
- $namesUsed = [];
- foreach ( $config['backends'] as $index => $config ) {
- if ( isset( $config['template'] ) ) {
- // Config is just a modified version of a registered backend's.
- // This should only be used when that config is used only by this backend.
- $config = $config + FileBackendGroup::singleton()->config( $config['template'] );
- }
- $name = $config['name'];
- if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
- throw new FileBackendError( "Two or more backends defined with the name $name." );
- }
- $namesUsed[$name] = 1;
- // Alter certain sub-backend settings for sanity
- unset( $config['readOnly'] ); // use proxy backend setting
- unset( $config['fileJournal'] ); // use proxy backend journal
- unset( $config['lockManager'] ); // lock under proxy backend
- $config['wikiId'] = $this->wikiId; // use the proxy backend wiki ID
- if ( !empty( $config['isMultiMaster'] ) ) {
- if ( $this->masterIndex >= 0 ) {
- throw new FileBackendError( 'More than one master backend defined.' );
- }
- $this->masterIndex = $index; // this is the "master"
- $config['fileJournal'] = $this->fileJournal; // log under proxy backend
- }
- if ( !empty( $config['readAffinity'] ) ) {
- $this->readIndex = $index; // prefer this for reads
- }
- // Create sub-backend object
- if ( !isset( $config['class'] ) ) {
- throw new FileBackendError( 'No class given for a backend config.' );
- }
- $class = $config['class'];
- $this->backends[$index] = new $class( $config );
- }
- if ( $this->masterIndex < 0 ) { // need backends and must have a master
- throw new FileBackendError( 'No master backend defined.' );
- }
- if ( $this->readIndex < 0 ) {
- $this->readIndex = $this->masterIndex; // default
- }
- }
-
- final protected function doOperationsInternal( array $ops, array $opts ) {
- $status = Status::newGood();
-
- $mbe = $this->backends[$this->masterIndex]; // convenience
-
- // Try to lock those files for the scope of this function...
- $scopeLock = null;
- if ( empty( $opts['nonLocking'] ) ) {
- // Try to lock those files for the scope of this function...
- /** @noinspection PhpUnusedLocalVariableInspection */
- $scopeLock = $this->getScopedLocksForOps( $ops, $status );
- if ( !$status->isOK() ) {
- return $status; // abort
- }
- }
- // Clear any cache entries (after locks acquired)
- $this->clearCache();
- $opts['preserveCache'] = true; // only locked files are cached
- // Get the list of paths to read/write...
- $relevantPaths = $this->fileStoragePathsForOps( $ops );
- // Check if the paths are valid and accessible on all backends...
- $status->merge( $this->accessibilityCheck( $relevantPaths ) );
- if ( !$status->isOK() ) {
- return $status; // abort
- }
- // Do a consistency check to see if the backends are consistent...
- $syncStatus = $this->consistencyCheck( $relevantPaths );
- if ( !$syncStatus->isOK() ) {
- wfDebugLog( 'FileOperation', get_class( $this ) .
- " failed sync check: " . FormatJson::encode( $relevantPaths ) );
- // Try to resync the clone backends to the master on the spot...
- if ( $this->autoResync === false
- || !$this->resyncFiles( $relevantPaths, $this->autoResync )->isOK()
- ) {
- $status->merge( $syncStatus );
-
- return $status; // abort
- }
- }
- // Actually attempt the operation batch on the master backend...
- $realOps = $this->substOpBatchPaths( $ops, $mbe );
- $masterStatus = $mbe->doOperations( $realOps, $opts );
- $status->merge( $masterStatus );
- // Propagate the operations to the clone backends if there were no unexpected errors
- // and if there were either no expected errors or if the 'force' option was used.
- // However, if nothing succeeded at all, then don't replicate any of the operations.
- // If $ops only had one operation, this might avoid backend sync inconsistencies.
- if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) {
- foreach ( $this->backends as $index => $backend ) {
- if ( $index === $this->masterIndex ) {
- continue; // done already
- }
-
- $realOps = $this->substOpBatchPaths( $ops, $backend );
- if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) {
- // Bind $scopeLock to the callback to preserve locks
- DeferredUpdates::addCallableUpdate(
- function() use ( $backend, $realOps, $opts, $scopeLock, $relevantPaths ) {
- wfDebugLog( 'FileOperationReplication',
- "'{$backend->getName()}' async replication; paths: " .
- FormatJson::encode( $relevantPaths ) );
- $backend->doOperations( $realOps, $opts );
- }
- );
- } else {
- wfDebugLog( 'FileOperationReplication',
- "'{$backend->getName()}' sync replication; paths: " .
- FormatJson::encode( $relevantPaths ) );
- $status->merge( $backend->doOperations( $realOps, $opts ) );
- }
- }
- }
- // Make 'success', 'successCount', and 'failCount' fields reflect
- // the overall operation, rather than all the batches for each backend.
- // Do this by only using success values from the master backend's batch.
- $status->success = $masterStatus->success;
- $status->successCount = $masterStatus->successCount;
- $status->failCount = $masterStatus->failCount;
-
- return $status;
- }
-
- /**
- * Check that a set of files are consistent across all internal backends
- *
- * @param array $paths List of storage paths
- * @return Status
- */
- public function consistencyCheck( array $paths ) {
- $status = Status::newGood();
- if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
- return $status; // skip checks
- }
-
- // Preload all of the stat info in as few round trips as possible...
- foreach ( $this->backends as $backend ) {
- $realPaths = $this->substPaths( $paths, $backend );
- $backend->preloadFileStat( [ 'srcs' => $realPaths, 'latest' => true ] );
- }
-
- $mBackend = $this->backends[$this->masterIndex];
- foreach ( $paths as $path ) {
- $params = [ 'src' => $path, 'latest' => true ];
- $mParams = $this->substOpPaths( $params, $mBackend );
- // Stat the file on the 'master' backend
- $mStat = $mBackend->getFileStat( $mParams );
- if ( $this->syncChecks & self::CHECK_SHA1 ) {
- $mSha1 = $mBackend->getFileSha1Base36( $mParams );
- } else {
- $mSha1 = false;
- }
- // Check if all clone backends agree with the master...
- foreach ( $this->backends as $index => $cBackend ) {
- if ( $index === $this->masterIndex ) {
- continue; // master
- }
- $cParams = $this->substOpPaths( $params, $cBackend );
- $cStat = $cBackend->getFileStat( $cParams );
- if ( $mStat ) { // file is in master
- if ( !$cStat ) { // file should exist
- $status->fatal( 'backend-fail-synced', $path );
- continue;
- }
- if ( $this->syncChecks & self::CHECK_SIZE ) {
- if ( $cStat['size'] != $mStat['size'] ) { // wrong size
- $status->fatal( 'backend-fail-synced', $path );
- continue;
- }
- }
- if ( $this->syncChecks & self::CHECK_TIME ) {
- $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] );
- $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] );
- if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere
- $status->fatal( 'backend-fail-synced', $path );
- continue;
- }
- }
- if ( $this->syncChecks & self::CHECK_SHA1 ) {
- if ( $cBackend->getFileSha1Base36( $cParams ) !== $mSha1 ) { // wrong SHA1
- $status->fatal( 'backend-fail-synced', $path );
- continue;
- }
- }
- } else { // file is not in master
- if ( $cStat ) { // file should not exist
- $status->fatal( 'backend-fail-synced', $path );
- }
- }
- }
- }
-
- return $status;
- }
-
- /**
- * Check that a set of file paths are usable across all internal backends
- *
- * @param array $paths List of storage paths
- * @return Status
- */
- public function accessibilityCheck( array $paths ) {
- $status = Status::newGood();
- if ( count( $this->backends ) <= 1 ) {
- return $status; // skip checks
- }
-
- foreach ( $paths as $path ) {
- foreach ( $this->backends as $backend ) {
- $realPath = $this->substPaths( $path, $backend );
- if ( !$backend->isPathUsableInternal( $realPath ) ) {
- $status->fatal( 'backend-fail-usable', $path );
- }
- }
- }
-
- return $status;
- }
-
- /**
- * Check that a set of files are consistent across all internal backends
- * and re-synchronize those files against the "multi master" if needed.
- *
- * @param array $paths List of storage paths
- * @param string|bool $resyncMode False, True, or "conservative"; see __construct()
- * @return Status
- */
- public function resyncFiles( array $paths, $resyncMode = true ) {
- $status = Status::newGood();
-
- $mBackend = $this->backends[$this->masterIndex];
- foreach ( $paths as $path ) {
- $mPath = $this->substPaths( $path, $mBackend );
- $mSha1 = $mBackend->getFileSha1Base36( [ 'src' => $mPath, 'latest' => true ] );
- $mStat = $mBackend->getFileStat( [ 'src' => $mPath, 'latest' => true ] );
- if ( $mStat === null || ( $mSha1 !== false && !$mStat ) ) { // sanity
- $status->fatal( 'backend-fail-internal', $this->name );
- wfDebugLog( 'FileOperation', __METHOD__
- . ': File is not available on the master backend' );
- continue; // file is not available on the master backend...
- }
- // Check of all clone backends agree with the master...
- foreach ( $this->backends as $index => $cBackend ) {
- if ( $index === $this->masterIndex ) {
- continue; // master
- }
- $cPath = $this->substPaths( $path, $cBackend );
- $cSha1 = $cBackend->getFileSha1Base36( [ 'src' => $cPath, 'latest' => true ] );
- $cStat = $cBackend->getFileStat( [ 'src' => $cPath, 'latest' => true ] );
- if ( $cStat === null || ( $cSha1 !== false && !$cStat ) ) { // sanity
- $status->fatal( 'backend-fail-internal', $cBackend->getName() );
- wfDebugLog( 'FileOperation', __METHOD__ .
- ': File is not available on the clone backend' );
- continue; // file is not available on the clone backend...
- }
- if ( $mSha1 === $cSha1 ) {
- // already synced; nothing to do
- } elseif ( $mSha1 !== false ) { // file is in master
- if ( $resyncMode === 'conservative'
- && $cStat && $cStat['mtime'] > $mStat['mtime']
- ) {
- $status->fatal( 'backend-fail-synced', $path );
- continue; // don't rollback data
- }
- $fsFile = $mBackend->getLocalReference(
- [ 'src' => $mPath, 'latest' => true ] );
- $status->merge( $cBackend->quickStore(
- [ 'src' => $fsFile->getPath(), 'dst' => $cPath ]
- ) );
- } elseif ( $mStat === false ) { // file is not in master
- if ( $resyncMode === 'conservative' ) {
- $status->fatal( 'backend-fail-synced', $path );
- continue; // don't delete data
- }
- $status->merge( $cBackend->quickDelete( [ 'src' => $cPath ] ) );
- }
- }
- }
-
- if ( !$status->isOK() ) {
- wfDebugLog( 'FileOperation', get_class( $this ) .
- " failed to resync: " . FormatJson::encode( $paths ) );
- }
-
- return $status;
- }
-
- /**
- * Get a list of file storage paths to read or write for a list of operations
- *
- * @param array $ops Same format as doOperations()
- * @return array List of storage paths to files (does not include directories)
- */
- protected function fileStoragePathsForOps( array $ops ) {
- $paths = [];
- foreach ( $ops as $op ) {
- if ( isset( $op['src'] ) ) {
- // For things like copy/move/delete with "ignoreMissingSource" and there
- // is no source file, nothing should happen and there should be no errors.
- if ( empty( $op['ignoreMissingSource'] )
- || $this->fileExists( [ 'src' => $op['src'] ] )
- ) {
- $paths[] = $op['src'];
- }
- }
- if ( isset( $op['srcs'] ) ) {
- $paths = array_merge( $paths, $op['srcs'] );
- }
- if ( isset( $op['dst'] ) ) {
- $paths[] = $op['dst'];
- }
- }
-
- return array_values( array_unique( array_filter( $paths, 'FileBackend::isStoragePath' ) ) );
- }
-
- /**
- * Substitute the backend name in storage path parameters
- * for a set of operations with that of a given internal backend.
- *
- * @param array $ops List of file operation arrays
- * @param FileBackendStore $backend
- * @return array
- */
- protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
- $newOps = []; // operations
- foreach ( $ops as $op ) {
- $newOp = $op; // operation
- foreach ( [ 'src', 'srcs', 'dst', 'dir' ] as $par ) {
- if ( isset( $newOp[$par] ) ) { // string or array
- $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
- }
- }
- $newOps[] = $newOp;
- }
-
- return $newOps;
- }
-
- /**
- * Same as substOpBatchPaths() but for a single operation
- *
- * @param array $ops File operation array
- * @param FileBackendStore $backend
- * @return array
- */
- protected function substOpPaths( array $ops, FileBackendStore $backend ) {
- $newOps = $this->substOpBatchPaths( [ $ops ], $backend );
-
- return $newOps[0];
- }
-
- /**
- * Substitute the backend of storage paths with an internal backend's name
- *
- * @param array|string $paths List of paths or single string path
- * @param FileBackendStore $backend
- * @return array|string
- */
- protected function substPaths( $paths, FileBackendStore $backend ) {
- return preg_replace(
- '!^mwstore://' . preg_quote( $this->name, '!' ) . '/!',
- StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
- $paths // string or array
- );
- }
-
- /**
- * Substitute the backend of internal storage paths with the proxy backend's name
- *
- * @param array|string $paths List of paths or single string path
- * @return array|string
- */
- protected function unsubstPaths( $paths ) {
- return preg_replace(
- '!^mwstore://([^/]+)!',
- StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ),
- $paths // string or array
- );
- }
-
- /**
- * @param array $ops File operations for FileBackend::doOperations()
- * @return bool Whether there are file path sources with outside lifetime/ownership
- */
- protected function hasVolatileSources( array $ops ) {
- foreach ( $ops as $op ) {
- if ( $op['op'] === 'store' && !isset( $op['srcRef'] ) ) {
- return true; // source file might be deleted anytime after do*Operations()
- }
- }
-
- return false;
- }
-
- protected function doQuickOperationsInternal( array $ops ) {
- $status = Status::newGood();
- // Do the operations on the master backend; setting Status fields...
- $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
- $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
- $status->merge( $masterStatus );
- // Propagate the operations to the clone backends...
- foreach ( $this->backends as $index => $backend ) {
- if ( $index === $this->masterIndex ) {
- continue; // done already
- }
-
- $realOps = $this->substOpBatchPaths( $ops, $backend );
- if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) {
- DeferredUpdates::addCallableUpdate(
- function() use ( $backend, $realOps ) {
- $backend->doQuickOperations( $realOps );
- }
- );
- } else {
- $status->merge( $backend->doQuickOperations( $realOps ) );
- }
- }
- // Make 'success', 'successCount', and 'failCount' fields reflect
- // the overall operation, rather than all the batches for each backend.
- // Do this by only using success values from the master backend's batch.
- $status->success = $masterStatus->success;
- $status->successCount = $masterStatus->successCount;
- $status->failCount = $masterStatus->failCount;
-
- return $status;
- }
-
- protected function doPrepare( array $params ) {
- return $this->doDirectoryOp( 'prepare', $params );
- }
-
- protected function doSecure( array $params ) {
- return $this->doDirectoryOp( 'secure', $params );
- }
-
- protected function doPublish( array $params ) {
- return $this->doDirectoryOp( 'publish', $params );
- }
-
- protected function doClean( array $params ) {
- return $this->doDirectoryOp( 'clean', $params );
- }
-
- /**
- * @param string $method One of (doPrepare,doSecure,doPublish,doClean)
- * @param array $params Method arguments
- * @return Status
- */
- protected function doDirectoryOp( $method, array $params ) {
- $status = Status::newGood();
-
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
- $masterStatus = $this->backends[$this->masterIndex]->$method( $realParams );
- $status->merge( $masterStatus );
-
- foreach ( $this->backends as $index => $backend ) {
- if ( $index === $this->masterIndex ) {
- continue; // already done
- }
-
- $realParams = $this->substOpPaths( $params, $backend );
- if ( $this->asyncWrites ) {
- DeferredUpdates::addCallableUpdate(
- function() use ( $backend, $method, $realParams ) {
- $backend->$method( $realParams );
- }
- );
- } else {
- $status->merge( $backend->$method( $realParams ) );
- }
- }
-
- return $status;
- }
-
- public function concatenate( array $params ) {
- // We are writing to an FS file, so we don't need to do this per-backend
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- return $this->backends[$index]->concatenate( $realParams );
- }
-
- public function fileExists( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- return $this->backends[$index]->fileExists( $realParams );
- }
-
- public function getFileTimestamp( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- return $this->backends[$index]->getFileTimestamp( $realParams );
- }
-
- public function getFileSize( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- return $this->backends[$index]->getFileSize( $realParams );
- }
-
- public function getFileStat( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- return $this->backends[$index]->getFileStat( $realParams );
- }
-
- public function getFileXAttributes( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- return $this->backends[$index]->getFileXAttributes( $realParams );
- }
-
- public function getFileContentsMulti( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- $contentsM = $this->backends[$index]->getFileContentsMulti( $realParams );
-
- $contents = []; // (path => FSFile) mapping using the proxy backend's name
- foreach ( $contentsM as $path => $data ) {
- $contents[$this->unsubstPaths( $path )] = $data;
- }
-
- return $contents;
- }
-
- public function getFileSha1Base36( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- return $this->backends[$index]->getFileSha1Base36( $realParams );
- }
-
- public function getFileProps( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- return $this->backends[$index]->getFileProps( $realParams );
- }
-
- public function streamFile( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- return $this->backends[$index]->streamFile( $realParams );
- }
-
- public function getLocalReferenceMulti( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- $fsFilesM = $this->backends[$index]->getLocalReferenceMulti( $realParams );
-
- $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name
- foreach ( $fsFilesM as $path => $fsFile ) {
- $fsFiles[$this->unsubstPaths( $path )] = $fsFile;
- }
-
- return $fsFiles;
- }
-
- public function getLocalCopyMulti( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- $tempFilesM = $this->backends[$index]->getLocalCopyMulti( $realParams );
-
- $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name
- foreach ( $tempFilesM as $path => $tempFile ) {
- $tempFiles[$this->unsubstPaths( $path )] = $tempFile;
- }
-
- return $tempFiles;
- }
-
- public function getFileHttpUrl( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- return $this->backends[$index]->getFileHttpUrl( $realParams );
- }
-
- public function directoryExists( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
-
- return $this->backends[$this->masterIndex]->directoryExists( $realParams );
- }
-
- public function getDirectoryList( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
-
- return $this->backends[$this->masterIndex]->getDirectoryList( $realParams );
- }
-
- public function getFileList( array $params ) {
- $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
-
- return $this->backends[$this->masterIndex]->getFileList( $realParams );
- }
-
- public function getFeatures() {
- return $this->backends[$this->masterIndex]->getFeatures();
- }
-
- public function clearCache( array $paths = null ) {
- foreach ( $this->backends as $backend ) {
- $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null;
- $backend->clearCache( $realPaths );
- }
- }
-
- public function preloadCache( array $paths ) {
- $realPaths = $this->substPaths( $paths, $this->backends[$this->readIndex] );
- $this->backends[$this->readIndex]->preloadCache( $realPaths );
- }
-
- public function preloadFileStat( array $params ) {
- $index = $this->getReadIndexFromParams( $params );
- $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
- return $this->backends[$index]->preloadFileStat( $realParams );
- }
-
- public function getScopedLocksForOps( array $ops, Status $status ) {
- $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
- $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $realOps );
- // Get the paths to lock from the master backend
- $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
- // Get the paths under the proxy backend's name
- $pbPaths = [
- LockManager::LOCK_UW => $this->unsubstPaths( $paths[LockManager::LOCK_UW] ),
- LockManager::LOCK_EX => $this->unsubstPaths( $paths[LockManager::LOCK_EX] )
- ];
-
- // Actually acquire the locks
- return $this->getScopedFileLocks( $pbPaths, 'mixed', $status );
- }
-
- /**
- * @param array $params
- * @return int The master or read affinity backend index, based on $params['latest']
- */
- protected function getReadIndexFromParams( array $params ) {
- return !empty( $params['latest'] ) ? $this->masterIndex : $this->readIndex;
- }
-}
diff --git a/www/wiki/includes/filebackend/FileBackendStore.php b/www/wiki/includes/filebackend/FileBackendStore.php
deleted file mode 100644
index 4d9587ef..00000000
--- a/www/wiki/includes/filebackend/FileBackendStore.php
+++ /dev/null
@@ -1,1971 +0,0 @@
-<?php
-/**
- * Base class for all backends using particular storage medium.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * @brief Base class for all backends using particular storage medium.
- *
- * This class defines the methods as abstract that subclasses must implement.
- * Outside callers should *not* use functions with "Internal" in the name.
- *
- * The FileBackend operations are implemented using basic functions
- * such as storeInternal(), copyInternal(), deleteInternal() and the like.
- * This class is also responsible for path resolution and sanitization.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-abstract class FileBackendStore extends FileBackend {
- /** @var WANObjectCache */
- protected $memCache;
- /** @var ProcessCacheLRU Map of paths to small (RAM/disk) cache items */
- protected $cheapCache;
- /** @var ProcessCacheLRU Map of paths to large (RAM/disk) cache items */
- protected $expensiveCache;
-
- /** @var array Map of container names to sharding config */
- protected $shardViaHashLevels = [];
-
- /** @var callable Method to get the MIME type of files */
- protected $mimeCallback;
-
- protected $maxFileSize = 4294967296; // integer bytes (4GiB)
-
- const CACHE_TTL = 10; // integer; TTL in seconds for process cache entries
- const CACHE_CHEAP_SIZE = 500; // integer; max entries in "cheap cache"
- const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache"
-
- /**
- * @see FileBackend::__construct()
- * Additional $config params include:
- * - wanCache : WANObjectCache object to use for persistent caching.
- * - mimeCallback : Callback that takes (storage path, content, file system path) and
- * returns the MIME type of the file or 'unknown/unknown'. The file
- * system path parameter should be used if the content one is null.
- *
- * @param array $config
- */
- public function __construct( array $config ) {
- parent::__construct( $config );
- $this->mimeCallback = isset( $config['mimeCallback'] )
- ? $config['mimeCallback']
- : null;
- $this->memCache = WANObjectCache::newEmpty(); // disabled by default
- $this->cheapCache = new ProcessCacheLRU( self::CACHE_CHEAP_SIZE );
- $this->expensiveCache = new ProcessCacheLRU( self::CACHE_EXPENSIVE_SIZE );
- }
-
- /**
- * Get the maximum allowable file size given backend
- * medium restrictions and basic performance constraints.
- * Do not call this function from places outside FileBackend and FileOp.
- *
- * @return int Bytes
- */
- final public function maxFileSizeInternal() {
- return $this->maxFileSize;
- }
-
- /**
- * Check if a file can be created or changed at a given storage path.
- * FS backends should check if the parent directory exists, files can be
- * written under it, and that any file already there is writable.
- * Backends using key/value stores should check if the container exists.
- *
- * @param string $storagePath
- * @return bool
- */
- abstract public function isPathUsableInternal( $storagePath );
-
- /**
- * Create a file in the backend with the given contents.
- * This will overwrite any file that exists at the destination.
- * Do not call this function from places outside FileBackend and FileOp.
- *
- * $params include:
- * - content : the raw file contents
- * - dst : destination storage path
- * - headers : HTTP header name/value map
- * - async : Status will be returned immediately if supported.
- * If the status is OK, then its value field will be
- * set to a FileBackendStoreOpHandle object.
- * - dstExists : Whether a file exists at the destination (optimization).
- * Callers can use "false" if no existing file is being changed.
- *
- * @param array $params
- * @return Status
- */
- final public function createInternal( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
- $status = Status::newFatal( 'backend-fail-maxsize',
- $params['dst'], $this->maxFileSizeInternal() );
- } else {
- $status = $this->doCreateInternal( $params );
- $this->clearCache( [ $params['dst'] ] );
- if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
- $this->deleteFileCache( $params['dst'] ); // persistent cache
- }
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::createInternal()
- * @param array $params
- * @return Status
- */
- abstract protected function doCreateInternal( array $params );
-
- /**
- * Store a file into the backend from a file on disk.
- * This will overwrite any file that exists at the destination.
- * Do not call this function from places outside FileBackend and FileOp.
- *
- * $params include:
- * - src : source path on disk
- * - dst : destination storage path
- * - headers : HTTP header name/value map
- * - async : Status will be returned immediately if supported.
- * If the status is OK, then its value field will be
- * set to a FileBackendStoreOpHandle object.
- * - dstExists : Whether a file exists at the destination (optimization).
- * Callers can use "false" if no existing file is being changed.
- *
- * @param array $params
- * @return Status
- */
- final public function storeInternal( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
- $status = Status::newFatal( 'backend-fail-maxsize',
- $params['dst'], $this->maxFileSizeInternal() );
- } else {
- $status = $this->doStoreInternal( $params );
- $this->clearCache( [ $params['dst'] ] );
- if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
- $this->deleteFileCache( $params['dst'] ); // persistent cache
- }
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::storeInternal()
- * @param array $params
- * @return Status
- */
- abstract protected function doStoreInternal( array $params );
-
- /**
- * Copy a file from one storage path to another in the backend.
- * This will overwrite any file that exists at the destination.
- * Do not call this function from places outside FileBackend and FileOp.
- *
- * $params include:
- * - src : source storage path
- * - dst : destination storage path
- * - ignoreMissingSource : do nothing if the source file does not exist
- * - headers : HTTP header name/value map
- * - async : Status will be returned immediately if supported.
- * If the status is OK, then its value field will be
- * set to a FileBackendStoreOpHandle object.
- * - dstExists : Whether a file exists at the destination (optimization).
- * Callers can use "false" if no existing file is being changed.
- *
- * @param array $params
- * @return Status
- */
- final public function copyInternal( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $status = $this->doCopyInternal( $params );
- $this->clearCache( [ $params['dst'] ] );
- if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
- $this->deleteFileCache( $params['dst'] ); // persistent cache
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::copyInternal()
- * @param array $params
- * @return Status
- */
- abstract protected function doCopyInternal( array $params );
-
- /**
- * Delete a file at the storage path.
- * Do not call this function from places outside FileBackend and FileOp.
- *
- * $params include:
- * - src : source storage path
- * - ignoreMissingSource : do nothing if the source file does not exist
- * - async : Status will be returned immediately if supported.
- * If the status is OK, then its value field will be
- * set to a FileBackendStoreOpHandle object.
- *
- * @param array $params
- * @return Status
- */
- final public function deleteInternal( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $status = $this->doDeleteInternal( $params );
- $this->clearCache( [ $params['src'] ] );
- $this->deleteFileCache( $params['src'] ); // persistent cache
- return $status;
- }
-
- /**
- * @see FileBackendStore::deleteInternal()
- * @param array $params
- * @return Status
- */
- abstract protected function doDeleteInternal( array $params );
-
- /**
- * Move a file from one storage path to another in the backend.
- * This will overwrite any file that exists at the destination.
- * Do not call this function from places outside FileBackend and FileOp.
- *
- * $params include:
- * - src : source storage path
- * - dst : destination storage path
- * - ignoreMissingSource : do nothing if the source file does not exist
- * - headers : HTTP header name/value map
- * - async : Status will be returned immediately if supported.
- * If the status is OK, then its value field will be
- * set to a FileBackendStoreOpHandle object.
- * - dstExists : Whether a file exists at the destination (optimization).
- * Callers can use "false" if no existing file is being changed.
- *
- * @param array $params
- * @return Status
- */
- final public function moveInternal( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $status = $this->doMoveInternal( $params );
- $this->clearCache( [ $params['src'], $params['dst'] ] );
- $this->deleteFileCache( $params['src'] ); // persistent cache
- if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
- $this->deleteFileCache( $params['dst'] ); // persistent cache
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::moveInternal()
- * @param array $params
- * @return Status
- */
- protected function doMoveInternal( array $params ) {
- unset( $params['async'] ); // two steps, won't work here :)
- $nsrc = FileBackend::normalizeStoragePath( $params['src'] );
- $ndst = FileBackend::normalizeStoragePath( $params['dst'] );
- // Copy source to dest
- $status = $this->copyInternal( $params );
- if ( $nsrc !== $ndst && $status->isOK() ) {
- // Delete source (only fails due to races or network problems)
- $status->merge( $this->deleteInternal( [ 'src' => $params['src'] ] ) );
- $status->setResult( true, $status->value ); // ignore delete() errors
- }
-
- return $status;
- }
-
- /**
- * Alter metadata for a file at the storage path.
- * Do not call this function from places outside FileBackend and FileOp.
- *
- * $params include:
- * - src : source storage path
- * - headers : HTTP header name/value map
- * - async : Status will be returned immediately if supported.
- * If the status is OK, then its value field will be
- * set to a FileBackendStoreOpHandle object.
- *
- * @param array $params
- * @return Status
- */
- final public function describeInternal( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- if ( count( $params['headers'] ) ) {
- $status = $this->doDescribeInternal( $params );
- $this->clearCache( [ $params['src'] ] );
- $this->deleteFileCache( $params['src'] ); // persistent cache
- } else {
- $status = Status::newGood(); // nothing to do
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::describeInternal()
- * @param array $params
- * @return Status
- */
- protected function doDescribeInternal( array $params ) {
- return Status::newGood();
- }
-
- /**
- * No-op file operation that does nothing.
- * Do not call this function from places outside FileBackend and FileOp.
- *
- * @param array $params
- * @return Status
- */
- final public function nullInternal( array $params ) {
- return Status::newGood();
- }
-
- final public function concatenate( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $status = Status::newGood();
-
- // Try to lock the source files for the scope of this function
- $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
- if ( $status->isOK() ) {
- // Actually do the file concatenation...
- $start_time = microtime( true );
- $status->merge( $this->doConcatenate( $params ) );
- $sec = microtime( true ) - $start_time;
- if ( !$status->isOK() ) {
- wfDebugLog( 'FileOperation', get_class( $this ) . "-{$this->name}" .
- " failed to concatenate " . count( $params['srcs'] ) . " file(s) [$sec sec]" );
- }
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::concatenate()
- * @param array $params
- * @return Status
- */
- protected function doConcatenate( array $params ) {
- $status = Status::newGood();
- $tmpPath = $params['dst']; // convenience
- unset( $params['latest'] ); // sanity
-
- // Check that the specified temp file is valid...
- MediaWiki\suppressWarnings();
- $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
- MediaWiki\restoreWarnings();
- if ( !$ok ) { // not present or not empty
- $status->fatal( 'backend-fail-opentemp', $tmpPath );
-
- return $status;
- }
-
- // Get local FS versions of the chunks needed for the concatenation...
- $fsFiles = $this->getLocalReferenceMulti( $params );
- foreach ( $fsFiles as $path => &$fsFile ) {
- if ( !$fsFile ) { // chunk failed to download?
- $fsFile = $this->getLocalReference( [ 'src' => $path ] );
- if ( !$fsFile ) { // retry failed?
- $status->fatal( 'backend-fail-read', $path );
-
- return $status;
- }
- }
- }
- unset( $fsFile ); // unset reference so we can reuse $fsFile
-
- // Get a handle for the destination temp file
- $tmpHandle = fopen( $tmpPath, 'ab' );
- if ( $tmpHandle === false ) {
- $status->fatal( 'backend-fail-opentemp', $tmpPath );
-
- return $status;
- }
-
- // Build up the temp file using the source chunks (in order)...
- foreach ( $fsFiles as $virtualSource => $fsFile ) {
- // Get a handle to the local FS version
- $sourceHandle = fopen( $fsFile->getPath(), 'rb' );
- if ( $sourceHandle === false ) {
- fclose( $tmpHandle );
- $status->fatal( 'backend-fail-read', $virtualSource );
-
- return $status;
- }
- // Append chunk to file (pass chunk size to avoid magic quotes)
- if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
- fclose( $sourceHandle );
- fclose( $tmpHandle );
- $status->fatal( 'backend-fail-writetemp', $tmpPath );
-
- return $status;
- }
- fclose( $sourceHandle );
- }
- if ( !fclose( $tmpHandle ) ) {
- $status->fatal( 'backend-fail-closetemp', $tmpPath );
-
- return $status;
- }
-
- clearstatcache(); // temp file changed
-
- return $status;
- }
-
- final protected function doPrepare( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $status = Status::newGood();
-
- list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
-
- return $status; // invalid storage path
- }
-
- if ( $shard !== null ) { // confined to a single container/shard
- $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
- } else { // directory is on several shards
- wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
- foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
- $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
- }
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doPrepare()
- * @param string $container
- * @param string $dir
- * @param array $params
- * @return Status
- */
- protected function doPrepareInternal( $container, $dir, array $params ) {
- return Status::newGood();
- }
-
- final protected function doSecure( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $status = Status::newGood();
-
- list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
-
- return $status; // invalid storage path
- }
-
- if ( $shard !== null ) { // confined to a single container/shard
- $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
- } else { // directory is on several shards
- wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
- foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
- $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
- }
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doSecure()
- * @param string $container
- * @param string $dir
- * @param array $params
- * @return Status
- */
- protected function doSecureInternal( $container, $dir, array $params ) {
- return Status::newGood();
- }
-
- final protected function doPublish( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $status = Status::newGood();
-
- list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
-
- return $status; // invalid storage path
- }
-
- if ( $shard !== null ) { // confined to a single container/shard
- $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
- } else { // directory is on several shards
- wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
- foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
- $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) );
- }
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doPublish()
- * @param string $container
- * @param string $dir
- * @param array $params
- * @return Status
- */
- protected function doPublishInternal( $container, $dir, array $params ) {
- return Status::newGood();
- }
-
- final protected function doClean( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $status = Status::newGood();
-
- // Recursive: first delete all empty subdirs recursively
- if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
- $subDirsRel = $this->getTopDirectoryList( [ 'dir' => $params['dir'] ] );
- if ( $subDirsRel !== null ) { // no errors
- foreach ( $subDirsRel as $subDirRel ) {
- $subDir = $params['dir'] . "/{$subDirRel}"; // full path
- $status->merge( $this->doClean( [ 'dir' => $subDir ] + $params ) );
- }
- unset( $subDirsRel ); // free directory for rmdir() on Windows (for FS backends)
- }
- }
-
- list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
-
- return $status; // invalid storage path
- }
-
- // Attempt to lock this directory...
- $filesLockEx = [ $params['dir'] ];
- $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
- if ( !$status->isOK() ) {
- return $status; // abort
- }
-
- if ( $shard !== null ) { // confined to a single container/shard
- $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
- $this->deleteContainerCache( $fullCont ); // purge cache
- } else { // directory is on several shards
- wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
- foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
- $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
- $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
- }
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::doClean()
- * @param string $container
- * @param string $dir
- * @param array $params
- * @return Status
- */
- protected function doCleanInternal( $container, $dir, array $params ) {
- return Status::newGood();
- }
-
- final public function fileExists( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $stat = $this->getFileStat( $params );
-
- return ( $stat === null ) ? null : (bool)$stat; // null => failure
- }
-
- final public function getFileTimestamp( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $stat = $this->getFileStat( $params );
-
- return $stat ? $stat['mtime'] : false;
- }
-
- final public function getFileSize( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $stat = $this->getFileStat( $params );
-
- return $stat ? $stat['size'] : false;
- }
-
- final public function getFileStat( array $params ) {
- $path = self::normalizeStoragePath( $params['src'] );
- if ( $path === null ) {
- return false; // invalid storage path
- }
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $latest = !empty( $params['latest'] ); // use latest data?
- if ( !$latest && !$this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
- $this->primeFileCache( [ $path ] ); // check persistent cache
- }
- if ( $this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
- $stat = $this->cheapCache->get( $path, 'stat' );
- // If we want the latest data, check that this cached
- // value was in fact fetched with the latest available data.
- if ( is_array( $stat ) ) {
- if ( !$latest || $stat['latest'] ) {
- return $stat;
- }
- } elseif ( in_array( $stat, [ 'NOT_EXIST', 'NOT_EXIST_LATEST' ] ) ) {
- if ( !$latest || $stat === 'NOT_EXIST_LATEST' ) {
- return false;
- }
- }
- }
- $stat = $this->doGetFileStat( $params );
- if ( is_array( $stat ) ) { // file exists
- // Strongly consistent backends can automatically set "latest"
- $stat['latest'] = isset( $stat['latest'] ) ? $stat['latest'] : $latest;
- $this->cheapCache->set( $path, 'stat', $stat );
- $this->setFileCache( $path, $stat ); // update persistent cache
- if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
- $this->cheapCache->set( $path, 'sha1',
- [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
- }
- if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
- $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
- $this->cheapCache->set( $path, 'xattr',
- [ 'map' => $stat['xattr'], 'latest' => $latest ] );
- }
- } elseif ( $stat === false ) { // file does not exist
- $this->cheapCache->set( $path, 'stat', $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
- $this->cheapCache->set( $path, 'xattr', [ 'map' => false, 'latest' => $latest ] );
- $this->cheapCache->set( $path, 'sha1', [ 'hash' => false, 'latest' => $latest ] );
- wfDebug( __METHOD__ . ": File $path does not exist.\n" );
- } else { // an error occurred
- wfDebug( __METHOD__ . ": Could not stat file $path.\n" );
- }
-
- return $stat;
- }
-
- /**
- * @see FileBackendStore::getFileStat()
- */
- abstract protected function doGetFileStat( array $params );
-
- public function getFileContentsMulti( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
- $params = $this->setConcurrencyFlags( $params );
- $contents = $this->doGetFileContentsMulti( $params );
-
- return $contents;
- }
-
- /**
- * @see FileBackendStore::getFileContentsMulti()
- * @param array $params
- * @return array
- */
- protected function doGetFileContentsMulti( array $params ) {
- $contents = [];
- foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
- MediaWiki\suppressWarnings();
- $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
- MediaWiki\restoreWarnings();
- }
-
- return $contents;
- }
-
- final public function getFileXAttributes( array $params ) {
- $path = self::normalizeStoragePath( $params['src'] );
- if ( $path === null ) {
- return false; // invalid storage path
- }
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $latest = !empty( $params['latest'] ); // use latest data?
- if ( $this->cheapCache->has( $path, 'xattr', self::CACHE_TTL ) ) {
- $stat = $this->cheapCache->get( $path, 'xattr' );
- // If we want the latest data, check that this cached
- // value was in fact fetched with the latest available data.
- if ( !$latest || $stat['latest'] ) {
- return $stat['map'];
- }
- }
- $fields = $this->doGetFileXAttributes( $params );
- $fields = is_array( $fields ) ? self::normalizeXAttributes( $fields ) : false;
- $this->cheapCache->set( $path, 'xattr', [ 'map' => $fields, 'latest' => $latest ] );
-
- return $fields;
- }
-
- /**
- * @see FileBackendStore::getFileXAttributes()
- * @return bool|string
- */
- protected function doGetFileXAttributes( array $params ) {
- return [ 'headers' => [], 'metadata' => [] ]; // not supported
- }
-
- final public function getFileSha1Base36( array $params ) {
- $path = self::normalizeStoragePath( $params['src'] );
- if ( $path === null ) {
- return false; // invalid storage path
- }
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $latest = !empty( $params['latest'] ); // use latest data?
- if ( $this->cheapCache->has( $path, 'sha1', self::CACHE_TTL ) ) {
- $stat = $this->cheapCache->get( $path, 'sha1' );
- // If we want the latest data, check that this cached
- // value was in fact fetched with the latest available data.
- if ( !$latest || $stat['latest'] ) {
- return $stat['hash'];
- }
- }
- $hash = $this->doGetFileSha1Base36( $params );
- $this->cheapCache->set( $path, 'sha1', [ 'hash' => $hash, 'latest' => $latest ] );
-
- return $hash;
- }
-
- /**
- * @see FileBackendStore::getFileSha1Base36()
- * @param array $params
- * @return bool|string
- */
- protected function doGetFileSha1Base36( array $params ) {
- $fsFile = $this->getLocalReference( $params );
- if ( !$fsFile ) {
- return false;
- } else {
- return $fsFile->getSha1Base36();
- }
- }
-
- final public function getFileProps( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $fsFile = $this->getLocalReference( $params );
- $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
-
- return $props;
- }
-
- final public function getLocalReferenceMulti( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
- $params = $this->setConcurrencyFlags( $params );
-
- $fsFiles = []; // (path => FSFile)
- $latest = !empty( $params['latest'] ); // use latest data?
- // Reuse any files already in process cache...
- foreach ( $params['srcs'] as $src ) {
- $path = self::normalizeStoragePath( $src );
- if ( $path === null ) {
- $fsFiles[$src] = null; // invalid storage path
- } elseif ( $this->expensiveCache->has( $path, 'localRef' ) ) {
- $val = $this->expensiveCache->get( $path, 'localRef' );
- // If we want the latest data, check that this cached
- // value was in fact fetched with the latest available data.
- if ( !$latest || $val['latest'] ) {
- $fsFiles[$src] = $val['object'];
- }
- }
- }
- // Fetch local references of any remaning files...
- $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
- foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
- $fsFiles[$path] = $fsFile;
- if ( $fsFile ) { // update the process cache...
- $this->expensiveCache->set( $path, 'localRef',
- [ 'object' => $fsFile, 'latest' => $latest ] );
- }
- }
-
- return $fsFiles;
- }
-
- /**
- * @see FileBackendStore::getLocalReferenceMulti()
- * @param array $params
- * @return array
- */
- protected function doGetLocalReferenceMulti( array $params ) {
- return $this->doGetLocalCopyMulti( $params );
- }
-
- final public function getLocalCopyMulti( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
- $params = $this->setConcurrencyFlags( $params );
- $tmpFiles = $this->doGetLocalCopyMulti( $params );
-
- return $tmpFiles;
- }
-
- /**
- * @see FileBackendStore::getLocalCopyMulti()
- * @param array $params
- * @return array
- */
- abstract protected function doGetLocalCopyMulti( array $params );
-
- /**
- * @see FileBackend::getFileHttpUrl()
- * @param array $params
- * @return string|null
- */
- public function getFileHttpUrl( array $params ) {
- return null; // not supported
- }
-
- final public function streamFile( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $status = Status::newGood();
-
- $info = $this->getFileStat( $params );
- if ( !$info ) { // let StreamFile handle the 404
- $status->fatal( 'backend-fail-notexists', $params['src'] );
- }
-
- // Set output buffer and HTTP headers for stream
- $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : [];
- $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders );
- if ( $res == StreamFile::NOT_MODIFIED ) {
- // do nothing; client cache is up to date
- } elseif ( $res == StreamFile::READY_STREAM ) {
- $status = $this->doStreamFile( $params );
- if ( !$status->isOK() ) {
- // Per bug 41113, nasty things can happen if bad cache entries get
- // stuck in cache. It's also possible that this error can come up
- // with simple race conditions. Clear out the stat cache to be safe.
- $this->clearCache( [ $params['src'] ] );
- $this->deleteFileCache( $params['src'] );
- trigger_error( "Bad stat cache or race condition for file {$params['src']}." );
- }
- } else {
- $status->fatal( 'backend-fail-stream', $params['src'] );
- }
-
- return $status;
- }
-
- /**
- * @see FileBackendStore::streamFile()
- * @param array $params
- * @return Status
- */
- protected function doStreamFile( array $params ) {
- $status = Status::newGood();
-
- $fsFile = $this->getLocalReference( $params );
- if ( !$fsFile ) {
- $status->fatal( 'backend-fail-stream', $params['src'] );
- } elseif ( !readfile( $fsFile->getPath() ) ) {
- $status->fatal( 'backend-fail-stream', $params['src'] );
- }
-
- return $status;
- }
-
- final public function directoryExists( array $params ) {
- list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) {
- return false; // invalid storage path
- }
- if ( $shard !== null ) { // confined to a single container/shard
- return $this->doDirectoryExists( $fullCont, $dir, $params );
- } else { // directory is on several shards
- wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
- $res = false; // response
- foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
- $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
- if ( $exists ) {
- $res = true;
- break; // found one!
- } elseif ( $exists === null ) { // error?
- $res = null; // if we don't find anything, it is indeterminate
- }
- }
-
- return $res;
- }
- }
-
- /**
- * @see FileBackendStore::directoryExists()
- *
- * @param string $container Resolved container name
- * @param string $dir Resolved path relative to container
- * @param array $params
- * @return bool|null
- */
- abstract protected function doDirectoryExists( $container, $dir, array $params );
-
- final public function getDirectoryList( array $params ) {
- list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) { // invalid storage path
- return null;
- }
- if ( $shard !== null ) {
- // File listing is confined to a single container/shard
- return $this->getDirectoryListInternal( $fullCont, $dir, $params );
- } else {
- wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- // File listing spans multiple containers/shards
- list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
-
- return new FileBackendStoreShardDirIterator( $this,
- $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
- }
- }
-
- /**
- * Do not call this function from places outside FileBackend
- *
- * @see FileBackendStore::getDirectoryList()
- *
- * @param string $container Resolved container name
- * @param string $dir Resolved path relative to container
- * @param array $params
- * @return Traversable|array|null Returns null on failure
- */
- abstract public function getDirectoryListInternal( $container, $dir, array $params );
-
- final public function getFileList( array $params ) {
- list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) { // invalid storage path
- return null;
- }
- if ( $shard !== null ) {
- // File listing is confined to a single container/shard
- return $this->getFileListInternal( $fullCont, $dir, $params );
- } else {
- wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
- // File listing spans multiple containers/shards
- list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
-
- return new FileBackendStoreShardFileIterator( $this,
- $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
- }
- }
-
- /**
- * Do not call this function from places outside FileBackend
- *
- * @see FileBackendStore::getFileList()
- *
- * @param string $container Resolved container name
- * @param string $dir Resolved path relative to container
- * @param array $params
- * @return Traversable|array|null Returns null on failure
- */
- abstract public function getFileListInternal( $container, $dir, array $params );
-
- /**
- * Return a list of FileOp objects from a list of operations.
- * Do not call this function from places outside FileBackend.
- *
- * The result must have the same number of items as the input.
- * An exception is thrown if an unsupported operation is requested.
- *
- * @param array $ops Same format as doOperations()
- * @return array List of FileOp objects
- * @throws FileBackendError
- */
- final public function getOperationsInternal( array $ops ) {
- $supportedOps = [
- 'store' => 'StoreFileOp',
- 'copy' => 'CopyFileOp',
- 'move' => 'MoveFileOp',
- 'delete' => 'DeleteFileOp',
- 'create' => 'CreateFileOp',
- 'describe' => 'DescribeFileOp',
- 'null' => 'NullFileOp'
- ];
-
- $performOps = []; // array of FileOp objects
- // Build up ordered array of FileOps...
- foreach ( $ops as $operation ) {
- $opName = $operation['op'];
- if ( isset( $supportedOps[$opName] ) ) {
- $class = $supportedOps[$opName];
- // Get params for this operation
- $params = $operation;
- // Append the FileOp class
- $performOps[] = new $class( $this, $params );
- } else {
- throw new FileBackendError( "Operation '$opName' is not supported." );
- }
- }
-
- return $performOps;
- }
-
- /**
- * Get a list of storage paths to lock for a list of operations
- * Returns an array with LockManager::LOCK_UW (shared locks) and
- * LockManager::LOCK_EX (exclusive locks) keys, each corresponding
- * to a list of storage paths to be locked. All returned paths are
- * normalized.
- *
- * @param array $performOps List of FileOp objects
- * @return array (LockManager::LOCK_UW => path list, LockManager::LOCK_EX => path list)
- */
- final public function getPathsToLockForOpsInternal( array $performOps ) {
- // Build up a list of files to lock...
- $paths = [ 'sh' => [], 'ex' => [] ];
- foreach ( $performOps as $fileOp ) {
- $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
- $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
- }
- // Optimization: if doing an EX lock anyway, don't also set an SH one
- $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
- // Get a shared lock on the parent directory of each path changed
- $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
-
- return [
- LockManager::LOCK_UW => $paths['sh'],
- LockManager::LOCK_EX => $paths['ex']
- ];
- }
-
- public function getScopedLocksForOps( array $ops, Status $status ) {
- $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
-
- return $this->getScopedFileLocks( $paths, 'mixed', $status );
- }
-
- final protected function doOperationsInternal( array $ops, array $opts ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $status = Status::newGood();
-
- // Fix up custom header name/value pairs...
- $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
-
- // Build up a list of FileOps...
- $performOps = $this->getOperationsInternal( $ops );
-
- // Acquire any locks as needed...
- if ( empty( $opts['nonLocking'] ) ) {
- // Build up a list of files to lock...
- $paths = $this->getPathsToLockForOpsInternal( $performOps );
- // Try to lock those files for the scope of this function...
-
- $scopeLock = $this->getScopedFileLocks( $paths, 'mixed', $status );
- if ( !$status->isOK() ) {
- return $status; // abort
- }
- }
-
- // Clear any file cache entries (after locks acquired)
- if ( empty( $opts['preserveCache'] ) ) {
- $this->clearCache();
- }
-
- // Build the list of paths involved
- $paths = [];
- foreach ( $performOps as $op ) {
- $paths = array_merge( $paths, $op->storagePathsRead() );
- $paths = array_merge( $paths, $op->storagePathsChanged() );
- }
-
- // Enlarge the cache to fit the stat entries of these files
- $this->cheapCache->resize( max( 2 * count( $paths ), self::CACHE_CHEAP_SIZE ) );
-
- // Load from the persistent container caches
- $this->primeContainerCache( $paths );
- // Get the latest stat info for all the files (having locked them)
- $ok = $this->preloadFileStat( [ 'srcs' => $paths, 'latest' => true ] );
-
- if ( $ok ) {
- // Actually attempt the operation batch...
- $opts = $this->setConcurrencyFlags( $opts );
- $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
- } else {
- // If we could not even stat some files, then bail out...
- $subStatus = Status::newFatal( 'backend-fail-internal', $this->name );
- foreach ( $ops as $i => $op ) { // mark each op as failed
- $subStatus->success[$i] = false;
- ++$subStatus->failCount;
- }
- wfDebugLog( 'FileOperation', get_class( $this ) . "-{$this->name} " .
- " stat failure; aborted operations: " . FormatJson::encode( $ops ) );
- }
-
- // Merge errors into status fields
- $status->merge( $subStatus );
- $status->success = $subStatus->success; // not done in merge()
-
- // Shrink the stat cache back to normal size
- $this->cheapCache->resize( self::CACHE_CHEAP_SIZE );
-
- return $status;
- }
-
- final protected function doQuickOperationsInternal( array $ops ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $status = Status::newGood();
-
- // Fix up custom header name/value pairs...
- $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
-
- // Clear any file cache entries
- $this->clearCache();
-
- $supportedOps = [ 'create', 'store', 'copy', 'move', 'delete', 'describe', 'null' ];
- // Parallel ops may be disabled in config due to dependencies (e.g. needing popen())
- $async = ( $this->parallelize === 'implicit' && count( $ops ) > 1 );
- $maxConcurrency = $this->concurrency; // throttle
-
- $statuses = []; // array of (index => Status)
- $fileOpHandles = []; // list of (index => handle) arrays
- $curFileOpHandles = []; // current handle batch
- // Perform the sync-only ops and build up op handles for the async ops...
- foreach ( $ops as $index => $params ) {
- if ( !in_array( $params['op'], $supportedOps ) ) {
- throw new FileBackendError( "Operation '{$params['op']}' is not supported." );
- }
- $method = $params['op'] . 'Internal'; // e.g. "storeInternal"
- $subStatus = $this->$method( [ 'async' => $async ] + $params );
- if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
- if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
- $fileOpHandles[] = $curFileOpHandles; // push this batch
- $curFileOpHandles = [];
- }
- $curFileOpHandles[$index] = $subStatus->value; // keep index
- } else { // error or completed
- $statuses[$index] = $subStatus; // keep index
- }
- }
- if ( count( $curFileOpHandles ) ) {
- $fileOpHandles[] = $curFileOpHandles; // last batch
- }
- // Do all the async ops that can be done concurrently...
- foreach ( $fileOpHandles as $fileHandleBatch ) {
- $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch );
- }
- // Marshall and merge all the responses...
- foreach ( $statuses as $index => $subStatus ) {
- $status->merge( $subStatus );
- if ( $subStatus->isOK() ) {
- $status->success[$index] = true;
- ++$status->successCount;
- } else {
- $status->success[$index] = false;
- ++$status->failCount;
- }
- }
-
- return $status;
- }
-
- /**
- * Execute a list of FileBackendStoreOpHandle handles in parallel.
- * The resulting Status object fields will correspond
- * to the order in which the handles where given.
- *
- * @param FileBackendStoreOpHandle[] $fileOpHandles
- *
- * @throws FileBackendError
- * @return array Map of Status objects
- */
- final public function executeOpHandlesInternal( array $fileOpHandles ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
- foreach ( $fileOpHandles as $fileOpHandle ) {
- if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
- throw new FileBackendError( "Given a non-FileBackendStoreOpHandle object." );
- } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
- throw new FileBackendError( "Given a FileBackendStoreOpHandle for the wrong backend." );
- }
- }
- $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
- foreach ( $fileOpHandles as $fileOpHandle ) {
- $fileOpHandle->closeResources();
- }
-
- return $res;
- }
-
- /**
- * @see FileBackendStore::executeOpHandlesInternal()
- *
- * @param FileBackendStoreOpHandle[] $fileOpHandles
- *
- * @throws FileBackendError
- * @return Status[] List of corresponding Status objects
- */
- protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
- if ( count( $fileOpHandles ) ) {
- throw new FileBackendError( "This backend supports no asynchronous operations." );
- }
-
- return [];
- }
-
- /**
- * Normalize and filter HTTP headers from a file operation
- *
- * This normalizes and strips long HTTP headers from a file operation.
- * Most headers are just numbers, but some are allowed to be long.
- * This function is useful for cleaning up headers and avoiding backend
- * specific errors, especially in the middle of batch file operations.
- *
- * @param array $op Same format as doOperation()
- * @return array
- */
- protected function sanitizeOpHeaders( array $op ) {
- static $longs = [ 'content-disposition' ];
-
- if ( isset( $op['headers'] ) ) { // op sets HTTP headers
- $newHeaders = [];
- foreach ( $op['headers'] as $name => $value ) {
- $name = strtolower( $name );
- $maxHVLen = in_array( $name, $longs ) ? INF : 255;
- if ( strlen( $name ) > 255 || strlen( $value ) > $maxHVLen ) {
- trigger_error( "Header '$name: $value' is too long." );
- } else {
- $newHeaders[$name] = strlen( $value ) ? $value : ''; // null/false => ""
- }
- }
- $op['headers'] = $newHeaders;
- }
-
- return $op;
- }
-
- final public function preloadCache( array $paths ) {
- $fullConts = []; // full container names
- foreach ( $paths as $path ) {
- list( $fullCont, , ) = $this->resolveStoragePath( $path );
- $fullConts[] = $fullCont;
- }
- // Load from the persistent file and container caches
- $this->primeContainerCache( $fullConts );
- $this->primeFileCache( $paths );
- }
-
- final public function clearCache( array $paths = null ) {
- if ( is_array( $paths ) ) {
- $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
- $paths = array_filter( $paths, 'strlen' ); // remove nulls
- }
- if ( $paths === null ) {
- $this->cheapCache->clear();
- $this->expensiveCache->clear();
- } else {
- foreach ( $paths as $path ) {
- $this->cheapCache->clear( $path );
- $this->expensiveCache->clear( $path );
- }
- }
- $this->doClearCache( $paths );
- }
-
- /**
- * Clears any additional stat caches for storage paths
- *
- * @see FileBackend::clearCache()
- *
- * @param array $paths Storage paths (optional)
- */
- protected function doClearCache( array $paths = null ) {
- }
-
- final public function preloadFileStat( array $params ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- $success = true; // no network errors
-
- $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1;
- $stats = $this->doGetFileStatMulti( $params );
- if ( $stats === null ) {
- return true; // not supported
- }
-
- $latest = !empty( $params['latest'] ); // use latest data?
- foreach ( $stats as $path => $stat ) {
- $path = FileBackend::normalizeStoragePath( $path );
- if ( $path === null ) {
- continue; // this shouldn't happen
- }
- if ( is_array( $stat ) ) { // file exists
- // Strongly consistent backends can automatically set "latest"
- $stat['latest'] = isset( $stat['latest'] ) ? $stat['latest'] : $latest;
- $this->cheapCache->set( $path, 'stat', $stat );
- $this->setFileCache( $path, $stat ); // update persistent cache
- if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
- $this->cheapCache->set( $path, 'sha1',
- [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
- }
- if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
- $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
- $this->cheapCache->set( $path, 'xattr',
- [ 'map' => $stat['xattr'], 'latest' => $latest ] );
- }
- } elseif ( $stat === false ) { // file does not exist
- $this->cheapCache->set( $path, 'stat',
- $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
- $this->cheapCache->set( $path, 'xattr',
- [ 'map' => false, 'latest' => $latest ] );
- $this->cheapCache->set( $path, 'sha1',
- [ 'hash' => false, 'latest' => $latest ] );
- wfDebug( __METHOD__ . ": File $path does not exist.\n" );
- } else { // an error occurred
- $success = false;
- wfDebug( __METHOD__ . ": Could not stat file $path.\n" );
- }
- }
-
- return $success;
- }
-
- /**
- * Get file stat information (concurrently if possible) for several files
- *
- * @see FileBackend::getFileStat()
- *
- * @param array $params Parameters include:
- * - srcs : list of source storage paths
- * - latest : use the latest available data
- * @return array|null Map of storage paths to array|bool|null (returns null if not supported)
- * @since 1.23
- */
- protected function doGetFileStatMulti( array $params ) {
- return null; // not supported
- }
-
- /**
- * Is this a key/value store where directories are just virtual?
- * Virtual directories exists in so much as files exists that are
- * prefixed with the directory path followed by a forward slash.
- *
- * @return bool
- */
- abstract protected function directoriesAreVirtual();
-
- /**
- * Check if a short container name is valid
- *
- * This checks for length and illegal characters.
- * This may disallow certain characters that can appear
- * in the prefix used to make the full container name.
- *
- * @param string $container
- * @return bool
- */
- final protected static function isValidShortContainerName( $container ) {
- // Suffixes like '.xxx' (hex shard chars) or '.seg' (file segments)
- // might be used by subclasses. Reserve the dot character for sanity.
- // The only way dots end up in containers (e.g. resolveStoragePath)
- // is due to the wikiId container prefix or the above suffixes.
- return self::isValidContainerName( $container ) && !preg_match( '/[.]/', $container );
- }
-
- /**
- * Check if a full container name is valid
- *
- * This checks for length and illegal characters.
- * Limiting the characters makes migrations to other stores easier.
- *
- * @param string $container
- * @return bool
- */
- final protected static function isValidContainerName( $container ) {
- // This accounts for NTFS, Swift, and Ceph restrictions
- // and disallows directory separators or traversal characters.
- // Note that matching strings URL encode to the same string;
- // in Swift/Ceph, the length restriction is *after* URL encoding.
- return (bool)preg_match( '/^[a-z0-9][a-z0-9-_.]{0,199}$/i', $container );
- }
-
- /**
- * Splits a storage path into an internal container name,
- * an internal relative file name, and a container shard suffix.
- * Any shard suffix is already appended to the internal container name.
- * This also checks that the storage path is valid and within this backend.
- *
- * If the container is sharded but a suffix could not be determined,
- * this means that the path can only refer to a directory and can only
- * be scanned by looking in all the container shards.
- *
- * @param string $storagePath
- * @return array (container, path, container suffix) or (null, null, null) if invalid
- */
- final protected function resolveStoragePath( $storagePath ) {
- list( $backend, $shortCont, $relPath ) = self::splitStoragePath( $storagePath );
- if ( $backend === $this->name ) { // must be for this backend
- $relPath = self::normalizeContainerPath( $relPath );
- if ( $relPath !== null && self::isValidShortContainerName( $shortCont ) ) {
- // Get shard for the normalized path if this container is sharded
- $cShard = $this->getContainerShard( $shortCont, $relPath );
- // Validate and sanitize the relative path (backend-specific)
- $relPath = $this->resolveContainerPath( $shortCont, $relPath );
- if ( $relPath !== null ) {
- // Prepend any wiki ID prefix to the container name
- $container = $this->fullContainerName( $shortCont );
- if ( self::isValidContainerName( $container ) ) {
- // Validate and sanitize the container name (backend-specific)
- $container = $this->resolveContainerName( "{$container}{$cShard}" );
- if ( $container !== null ) {
- return [ $container, $relPath, $cShard ];
- }
- }
- }
- }
- }
-
- return [ null, null, null ];
- }
-
- /**
- * Like resolveStoragePath() except null values are returned if
- * the container is sharded and the shard could not be determined
- * or if the path ends with '/'. The later case is illegal for FS
- * backends and can confuse listings for object store backends.
- *
- * This function is used when resolving paths that must be valid
- * locations for files. Directory and listing functions should
- * generally just use resolveStoragePath() instead.
- *
- * @see FileBackendStore::resolveStoragePath()
- *
- * @param string $storagePath
- * @return array (container, path) or (null, null) if invalid
- */
- final protected function resolveStoragePathReal( $storagePath ) {
- list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
- if ( $cShard !== null && substr( $relPath, -1 ) !== '/' ) {
- return [ $container, $relPath ];
- }
-
- return [ null, null ];
- }
-
- /**
- * Get the container name shard suffix for a given path.
- * Any empty suffix means the container is not sharded.
- *
- * @param string $container Container name
- * @param string $relPath Storage path relative to the container
- * @return string|null Returns null if shard could not be determined
- */
- final protected function getContainerShard( $container, $relPath ) {
- list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
- if ( $levels == 1 || $levels == 2 ) {
- // Hash characters are either base 16 or 36
- $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
- // Get a regex that represents the shard portion of paths.
- // The concatenation of the captures gives us the shard.
- if ( $levels === 1 ) { // 16 or 36 shards per container
- $hashDirRegex = '(' . $char . ')';
- } else { // 256 or 1296 shards per container
- if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
- $hashDirRegex = $char . '/(' . $char . '{2})';
- } else { // short hash dir format (e.g. "a/b/c")
- $hashDirRegex = '(' . $char . ')/(' . $char . ')';
- }
- }
- // Allow certain directories to be above the hash dirs so as
- // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
- // They must be 2+ chars to avoid any hash directory ambiguity.
- $m = [];
- if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
- return '.' . implode( '', array_slice( $m, 1 ) );
- }
-
- return null; // failed to match
- }
-
- return ''; // no sharding
- }
-
- /**
- * Check if a storage path maps to a single shard.
- * Container dirs like "a", where the container shards on "x/xy",
- * can reside on several shards. Such paths are tricky to handle.
- *
- * @param string $storagePath Storage path
- * @return bool
- */
- final public function isSingleShardPathInternal( $storagePath ) {
- list( , , $shard ) = $this->resolveStoragePath( $storagePath );
-
- return ( $shard !== null );
- }
-
- /**
- * Get the sharding config for a container.
- * If greater than 0, then all file storage paths within
- * the container are required to be hashed accordingly.
- *
- * @param string $container
- * @return array (integer levels, integer base, repeat flag) or (0, 0, false)
- */
- final protected function getContainerHashLevels( $container ) {
- if ( isset( $this->shardViaHashLevels[$container] ) ) {
- $config = $this->shardViaHashLevels[$container];
- $hashLevels = (int)$config['levels'];
- if ( $hashLevels == 1 || $hashLevels == 2 ) {
- $hashBase = (int)$config['base'];
- if ( $hashBase == 16 || $hashBase == 36 ) {
- return [ $hashLevels, $hashBase, $config['repeat'] ];
- }
- }
- }
-
- return [ 0, 0, false ]; // no sharding
- }
-
- /**
- * Get a list of full container shard suffixes for a container
- *
- * @param string $container
- * @return array
- */
- final protected function getContainerSuffixes( $container ) {
- $shards = [];
- list( $digits, $base ) = $this->getContainerHashLevels( $container );
- if ( $digits > 0 ) {
- $numShards = pow( $base, $digits );
- for ( $index = 0; $index < $numShards; $index++ ) {
- $shards[] = '.' . Wikimedia\base_convert( $index, 10, $base, $digits );
- }
- }
-
- return $shards;
- }
-
- /**
- * Get the full container name, including the wiki ID prefix
- *
- * @param string $container
- * @return string
- */
- final protected function fullContainerName( $container ) {
- if ( $this->wikiId != '' ) {
- return "{$this->wikiId}-$container";
- } else {
- return $container;
- }
- }
-
- /**
- * Resolve a container name, checking if it's allowed by the backend.
- * This is intended for internal use, such as encoding illegal chars.
- * Subclasses can override this to be more restrictive.
- *
- * @param string $container
- * @return string|null
- */
- protected function resolveContainerName( $container ) {
- return $container;
- }
-
- /**
- * Resolve a relative storage path, checking if it's allowed by the backend.
- * This is intended for internal use, such as encoding illegal chars or perhaps
- * getting absolute paths (e.g. FS based backends). Note that the relative path
- * may be the empty string (e.g. the path is simply to the container).
- *
- * @param string $container Container name
- * @param string $relStoragePath Storage path relative to the container
- * @return string|null Path or null if not valid
- */
- protected function resolveContainerPath( $container, $relStoragePath ) {
- return $relStoragePath;
- }
-
- /**
- * Get the cache key for a container
- *
- * @param string $container Resolved container name
- * @return string
- */
- private function containerCacheKey( $container ) {
- return "filebackend:{$this->name}:{$this->wikiId}:container:{$container}";
- }
-
- /**
- * Set the cached info for a container
- *
- * @param string $container Resolved container name
- * @param array $val Information to cache
- */
- final protected function setContainerCache( $container, array $val ) {
- $this->memCache->set( $this->containerCacheKey( $container ), $val, 14 * 86400 );
- }
-
- /**
- * Delete the cached info for a container.
- * The cache key is salted for a while to prevent race conditions.
- *
- * @param string $container Resolved container name
- */
- final protected function deleteContainerCache( $container ) {
- if ( !$this->memCache->delete( $this->containerCacheKey( $container ), 300 ) ) {
- trigger_error( "Unable to delete stat cache for container $container." );
- }
- }
-
- /**
- * Do a batch lookup from cache for container stats for all containers
- * used in a list of container names or storage paths objects.
- * This loads the persistent cache values into the process cache.
- *
- * @param array $items
- */
- final protected function primeContainerCache( array $items ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
- $paths = []; // list of storage paths
- $contNames = []; // (cache key => resolved container name)
- // Get all the paths/containers from the items...
- foreach ( $items as $item ) {
- if ( self::isStoragePath( $item ) ) {
- $paths[] = $item;
- } elseif ( is_string( $item ) ) { // full container name
- $contNames[$this->containerCacheKey( $item )] = $item;
- }
- }
- // Get all the corresponding cache keys for paths...
- foreach ( $paths as $path ) {
- list( $fullCont, , ) = $this->resolveStoragePath( $path );
- if ( $fullCont !== null ) { // valid path for this backend
- $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
- }
- }
-
- $contInfo = []; // (resolved container name => cache value)
- // Get all cache entries for these container cache keys...
- $values = $this->memCache->getMulti( array_keys( $contNames ) );
- foreach ( $values as $cacheKey => $val ) {
- $contInfo[$contNames[$cacheKey]] = $val;
- }
-
- // Populate the container process cache for the backend...
- $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
- }
-
- /**
- * Fill the backend-specific process cache given an array of
- * resolved container names and their corresponding cached info.
- * Only containers that actually exist should appear in the map.
- *
- * @param array $containerInfo Map of resolved container names to cached info
- */
- protected function doPrimeContainerCache( array $containerInfo ) {
- }
-
- /**
- * Get the cache key for a file path
- *
- * @param string $path Normalized storage path
- * @return string
- */
- private function fileCacheKey( $path ) {
- return "filebackend:{$this->name}:{$this->wikiId}:file:" . sha1( $path );
- }
-
- /**
- * Set the cached stat info for a file path.
- * Negatives (404s) are not cached. By not caching negatives, we can skip cache
- * salting for the case when a file is created at a path were there was none before.
- *
- * @param string $path Storage path
- * @param array $val Stat information to cache
- */
- final protected function setFileCache( $path, array $val ) {
- $path = FileBackend::normalizeStoragePath( $path );
- if ( $path === null ) {
- return; // invalid storage path
- }
- $age = time() - wfTimestamp( TS_UNIX, $val['mtime'] );
- $ttl = min( 7 * 86400, max( 300, floor( .1 * $age ) ) );
- $key = $this->fileCacheKey( $path );
- // Set the cache unless it is currently salted.
- $this->memCache->set( $key, $val, $ttl );
- }
-
- /**
- * Delete the cached stat info for a file path.
- * The cache key is salted for a while to prevent race conditions.
- * Since negatives (404s) are not cached, this does not need to be called when
- * a file is created at a path were there was none before.
- *
- * @param string $path Storage path
- */
- final protected function deleteFileCache( $path ) {
- $path = FileBackend::normalizeStoragePath( $path );
- if ( $path === null ) {
- return; // invalid storage path
- }
- if ( !$this->memCache->delete( $this->fileCacheKey( $path ), 300 ) ) {
- trigger_error( "Unable to delete stat cache for file $path." );
- }
- }
-
- /**
- * Do a batch lookup from cache for file stats for all paths
- * used in a list of storage paths or FileOp objects.
- * This loads the persistent cache values into the process cache.
- *
- * @param array $items List of storage paths
- */
- final protected function primeFileCache( array $items ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
- $paths = []; // list of storage paths
- $pathNames = []; // (cache key => storage path)
- // Get all the paths/containers from the items...
- foreach ( $items as $item ) {
- if ( self::isStoragePath( $item ) ) {
- $paths[] = FileBackend::normalizeStoragePath( $item );
- }
- }
- // Get rid of any paths that failed normalization...
- $paths = array_filter( $paths, 'strlen' ); // remove nulls
- // Get all the corresponding cache keys for paths...
- foreach ( $paths as $path ) {
- list( , $rel, ) = $this->resolveStoragePath( $path );
- if ( $rel !== null ) { // valid path for this backend
- $pathNames[$this->fileCacheKey( $path )] = $path;
- }
- }
- // Get all cache entries for these file cache keys...
- $values = $this->memCache->getMulti( array_keys( $pathNames ) );
- foreach ( $values as $cacheKey => $val ) {
- $path = $pathNames[$cacheKey];
- if ( is_array( $val ) ) {
- $val['latest'] = false; // never completely trust cache
- $this->cheapCache->set( $path, 'stat', $val );
- if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
- $this->cheapCache->set( $path, 'sha1',
- [ 'hash' => $val['sha1'], 'latest' => false ] );
- }
- if ( isset( $val['xattr'] ) ) { // some backends store headers/metadata
- $val['xattr'] = self::normalizeXAttributes( $val['xattr'] );
- $this->cheapCache->set( $path, 'xattr',
- [ 'map' => $val['xattr'], 'latest' => false ] );
- }
- }
- }
- }
-
- /**
- * Normalize file headers/metadata to the FileBackend::getFileXAttributes() format
- *
- * @param array $xattr
- * @return array
- * @since 1.22
- */
- final protected static function normalizeXAttributes( array $xattr ) {
- $newXAttr = [ 'headers' => [], 'metadata' => [] ];
-
- foreach ( $xattr['headers'] as $name => $value ) {
- $newXAttr['headers'][strtolower( $name )] = $value;
- }
-
- foreach ( $xattr['metadata'] as $name => $value ) {
- $newXAttr['metadata'][strtolower( $name )] = $value;
- }
-
- return $newXAttr;
- }
-
- /**
- * Set the 'concurrency' option from a list of operation options
- *
- * @param array $opts Map of operation options
- * @return array
- */
- final protected function setConcurrencyFlags( array $opts ) {
- $opts['concurrency'] = 1; // off
- if ( $this->parallelize === 'implicit' ) {
- if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
- $opts['concurrency'] = $this->concurrency;
- }
- } elseif ( $this->parallelize === 'explicit' ) {
- if ( !empty( $opts['parallelize'] ) ) {
- $opts['concurrency'] = $this->concurrency;
- }
- }
-
- return $opts;
- }
-
- /**
- * Get the content type to use in HEAD/GET requests for a file
- *
- * @param string $storagePath
- * @param string|null $content File data
- * @param string|null $fsPath File system path
- * @return string MIME type
- */
- protected function getContentType( $storagePath, $content, $fsPath ) {
- if ( $this->mimeCallback ) {
- return call_user_func_array( $this->mimeCallback, func_get_args() );
- }
-
- $mime = null;
- if ( $fsPath !== null && function_exists( 'finfo_file' ) ) {
- $finfo = finfo_open( FILEINFO_MIME_TYPE );
- $mime = finfo_file( $finfo, $fsPath );
- finfo_close( $finfo );
- }
-
- return is_string( $mime ) ? $mime : 'unknown/unknown';
- }
-}
-
-/**
- * FileBackendStore helper class for performing asynchronous file operations.
- *
- * For example, calling FileBackendStore::createInternal() with the "async"
- * param flag may result in a Status that contains this object as a value.
- * This class is largely backend-specific and is mostly just "magic" to be
- * passed to FileBackendStore::executeOpHandlesInternal().
- */
-abstract class FileBackendStoreOpHandle {
- /** @var array */
- public $params = []; // params to caller functions
- /** @var FileBackendStore */
- public $backend;
- /** @var array */
- public $resourcesToClose = [];
-
- public $call; // string; name that identifies the function called
-
- /**
- * Close all open file handles
- */
- public function closeResources() {
- array_map( 'fclose', $this->resourcesToClose );
- }
-}
-
-/**
- * FileBackendStore helper function to handle listings that span container shards.
- * Do not use this class from places outside of FileBackendStore.
- *
- * @ingroup FileBackend
- */
-abstract class FileBackendStoreShardListIterator extends FilterIterator {
- /** @var FileBackendStore */
- protected $backend;
-
- /** @var array */
- protected $params;
-
- /** @var string Full container name */
- protected $container;
-
- /** @var string Resolved relative path */
- protected $directory;
-
- /** @var array */
- protected $multiShardPaths = []; // (rel path => 1)
-
- /**
- * @param FileBackendStore $backend
- * @param string $container Full storage container name
- * @param string $dir Storage directory relative to container
- * @param array $suffixes List of container shard suffixes
- * @param array $params
- */
- public function __construct(
- FileBackendStore $backend, $container, $dir, array $suffixes, array $params
- ) {
- $this->backend = $backend;
- $this->container = $container;
- $this->directory = $dir;
- $this->params = $params;
-
- $iter = new AppendIterator();
- foreach ( $suffixes as $suffix ) {
- $iter->append( $this->listFromShard( $this->container . $suffix ) );
- }
-
- parent::__construct( $iter );
- }
-
- public function accept() {
- $rel = $this->getInnerIterator()->current(); // path relative to given directory
- $path = $this->params['dir'] . "/{$rel}"; // full storage path
- if ( $this->backend->isSingleShardPathInternal( $path ) ) {
- return true; // path is only on one shard; no issue with duplicates
- } elseif ( isset( $this->multiShardPaths[$rel] ) ) {
- // Don't keep listing paths that are on multiple shards
- return false;
- } else {
- $this->multiShardPaths[$rel] = 1;
-
- return true;
- }
- }
-
- public function rewind() {
- parent::rewind();
- $this->multiShardPaths = [];
- }
-
- /**
- * Get the list for a given container shard
- *
- * @param string $container Resolved container name
- * @return Iterator
- */
- abstract protected function listFromShard( $container );
-}
-
-/**
- * Iterator for listing directories
- */
-class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
- protected function listFromShard( $container ) {
- $list = $this->backend->getDirectoryListInternal(
- $container, $this->directory, $this->params );
- if ( $list === null ) {
- return new ArrayIterator( [] );
- } else {
- return is_array( $list ) ? new ArrayIterator( $list ) : $list;
- }
- }
-}
-
-/**
- * Iterator for listing regular files
- */
-class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
- protected function listFromShard( $container ) {
- $list = $this->backend->getFileListInternal(
- $container, $this->directory, $this->params );
- if ( $list === null ) {
- return new ArrayIterator( [] );
- } else {
- return is_array( $list ) ? new ArrayIterator( $list ) : $list;
- }
- }
-}
diff --git a/www/wiki/includes/filebackend/FileOp.php b/www/wiki/includes/filebackend/FileOp.php
deleted file mode 100644
index 56a40738..00000000
--- a/www/wiki/includes/filebackend/FileOp.php
+++ /dev/null
@@ -1,848 +0,0 @@
-<?php
-/**
- * Helper class for representing operations with transaction support.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * FileBackend helper class for representing operations.
- * Do not use this class from places outside FileBackend.
- *
- * Methods called from FileOpBatch::attempt() should avoid throwing
- * exceptions at all costs. FileOp objects should be lightweight in order
- * to support large arrays in memory and serialization.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-abstract class FileOp {
- /** @var array */
- protected $params = [];
-
- /** @var FileBackendStore */
- protected $backend;
-
- /** @var int */
- protected $state = self::STATE_NEW;
-
- /** @var bool */
- protected $failed = false;
-
- /** @var bool */
- protected $async = false;
-
- /** @var string */
- protected $batchId;
-
- /** @var bool Operation is not a no-op */
- protected $doOperation = true;
-
- /** @var string */
- protected $sourceSha1;
-
- /** @var bool */
- protected $overwriteSameCase;
-
- /** @var bool */
- protected $destExists;
-
- /* Object life-cycle */
- const STATE_NEW = 1;
- const STATE_CHECKED = 2;
- const STATE_ATTEMPTED = 3;
-
- /**
- * Build a new batch file operation transaction
- *
- * @param FileBackendStore $backend
- * @param array $params
- * @throws FileBackendError
- */
- final public function __construct( FileBackendStore $backend, array $params ) {
- $this->backend = $backend;
- list( $required, $optional, $paths ) = $this->allowedParams();
- foreach ( $required as $name ) {
- if ( isset( $params[$name] ) ) {
- $this->params[$name] = $params[$name];
- } else {
- throw new FileBackendError( "File operation missing parameter '$name'." );
- }
- }
- foreach ( $optional as $name ) {
- if ( isset( $params[$name] ) ) {
- $this->params[$name] = $params[$name];
- }
- }
- foreach ( $paths as $name ) {
- if ( isset( $this->params[$name] ) ) {
- // Normalize paths so the paths to the same file have the same string
- $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] );
- }
- }
- }
-
- /**
- * Normalize a string if it is a valid storage path
- *
- * @param string $path
- * @return string
- */
- protected static function normalizeIfValidStoragePath( $path ) {
- if ( FileBackend::isStoragePath( $path ) ) {
- $res = FileBackend::normalizeStoragePath( $path );
-
- return ( $res !== null ) ? $res : $path;
- }
-
- return $path;
- }
-
- /**
- * Set the batch UUID this operation belongs to
- *
- * @param string $batchId
- */
- final public function setBatchId( $batchId ) {
- $this->batchId = $batchId;
- }
-
- /**
- * Get the value of the parameter with the given name
- *
- * @param string $name
- * @return mixed Returns null if the parameter is not set
- */
- final public function getParam( $name ) {
- return isset( $this->params[$name] ) ? $this->params[$name] : null;
- }
-
- /**
- * Check if this operation failed precheck() or attempt()
- *
- * @return bool
- */
- final public function failed() {
- return $this->failed;
- }
-
- /**
- * Get a new empty predicates array for precheck()
- *
- * @return array
- */
- final public static function newPredicates() {
- return [ 'exists' => [], 'sha1' => [] ];
- }
-
- /**
- * Get a new empty dependency tracking array for paths read/written to
- *
- * @return array
- */
- final public static function newDependencies() {
- return [ 'read' => [], 'write' => [] ];
- }
-
- /**
- * Update a dependency tracking array to account for this operation
- *
- * @param array $deps Prior path reads/writes; format of FileOp::newPredicates()
- * @return array
- */
- final public function applyDependencies( array $deps ) {
- $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
- $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
-
- return $deps;
- }
-
- /**
- * Check if this operation changes files listed in $paths
- *
- * @param array $deps Prior path reads/writes; format of FileOp::newPredicates()
- * @return bool
- */
- final public function dependsOn( array $deps ) {
- foreach ( $this->storagePathsChanged() as $path ) {
- if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
- return true; // "output" or "anti" dependency
- }
- }
- foreach ( $this->storagePathsRead() as $path ) {
- if ( isset( $deps['write'][$path] ) ) {
- return true; // "flow" dependency
- }
- }
-
- return false;
- }
-
- /**
- * Get the file journal entries for this file operation
- *
- * @param array $oPredicates Pre-op info about files (format of FileOp::newPredicates)
- * @param array $nPredicates Post-op info about files (format of FileOp::newPredicates)
- * @return array
- */
- final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
- if ( !$this->doOperation ) {
- return []; // this is a no-op
- }
- $nullEntries = [];
- $updateEntries = [];
- $deleteEntries = [];
- $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
- foreach ( array_unique( $pathsUsed ) as $path ) {
- $nullEntries[] = [ // assertion for recovery
- 'op' => 'null',
- 'path' => $path,
- 'newSha1' => $this->fileSha1( $path, $oPredicates )
- ];
- }
- foreach ( $this->storagePathsChanged() as $path ) {
- if ( $nPredicates['sha1'][$path] === false ) { // deleted
- $deleteEntries[] = [
- 'op' => 'delete',
- 'path' => $path,
- 'newSha1' => ''
- ];
- } else { // created/updated
- $updateEntries[] = [
- 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
- 'path' => $path,
- 'newSha1' => $nPredicates['sha1'][$path]
- ];
- }
- }
-
- return array_merge( $nullEntries, $updateEntries, $deleteEntries );
- }
-
- /**
- * Check preconditions of the operation without writing anything.
- * This must update $predicates for each path that the op can change
- * except when a failing status object is returned.
- *
- * @param array $predicates
- * @return Status
- */
- final public function precheck( array &$predicates ) {
- if ( $this->state !== self::STATE_NEW ) {
- return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
- }
- $this->state = self::STATE_CHECKED;
- $status = $this->doPrecheck( $predicates );
- if ( !$status->isOK() ) {
- $this->failed = true;
- }
-
- return $status;
- }
-
- /**
- * @param array $predicates
- * @return Status
- */
- protected function doPrecheck( array &$predicates ) {
- return Status::newGood();
- }
-
- /**
- * Attempt the operation
- *
- * @return Status
- */
- final public function attempt() {
- if ( $this->state !== self::STATE_CHECKED ) {
- return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
- } elseif ( $this->failed ) { // failed precheck
- return Status::newFatal( 'fileop-fail-attempt-precheck' );
- }
- $this->state = self::STATE_ATTEMPTED;
- if ( $this->doOperation ) {
- $status = $this->doAttempt();
- if ( !$status->isOK() ) {
- $this->failed = true;
- $this->logFailure( 'attempt' );
- }
- } else { // no-op
- $status = Status::newGood();
- }
-
- return $status;
- }
-
- /**
- * @return Status
- */
- protected function doAttempt() {
- return Status::newGood();
- }
-
- /**
- * Attempt the operation in the background
- *
- * @return Status
- */
- final public function attemptAsync() {
- $this->async = true;
- $result = $this->attempt();
- $this->async = false;
-
- return $result;
- }
-
- /**
- * Get the file operation parameters
- *
- * @return array (required params list, optional params list, list of params that are paths)
- */
- protected function allowedParams() {
- return [ [], [], [] ];
- }
-
- /**
- * Adjust params to FileBackendStore internal file calls
- *
- * @param array $params
- * @return array (required params list, optional params list)
- */
- protected function setFlags( array $params ) {
- return [ 'async' => $this->async ] + $params;
- }
-
- /**
- * Get a list of storage paths read from for this operation
- *
- * @return array
- */
- public function storagePathsRead() {
- return [];
- }
-
- /**
- * Get a list of storage paths written to for this operation
- *
- * @return array
- */
- public function storagePathsChanged() {
- return [];
- }
-
- /**
- * Check for errors with regards to the destination file already existing.
- * Also set the destExists, overwriteSameCase and sourceSha1 member variables.
- * A bad status will be returned if there is no chance it can be overwritten.
- *
- * @param array $predicates
- * @return Status
- */
- protected function precheckDestExistence( array $predicates ) {
- $status = Status::newGood();
- // Get hash of source file/string and the destination file
- $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
- if ( $this->sourceSha1 === null ) { // file in storage?
- $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
- }
- $this->overwriteSameCase = false;
- $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
- if ( $this->destExists ) {
- if ( $this->getParam( 'overwrite' ) ) {
- return $status; // OK
- } elseif ( $this->getParam( 'overwriteSame' ) ) {
- $dhash = $this->fileSha1( $this->params['dst'], $predicates );
- // Check if hashes are valid and match each other...
- if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
- $status->fatal( 'backend-fail-hashes' );
- } elseif ( $this->sourceSha1 !== $dhash ) {
- // Give an error if the files are not identical
- $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
- } else {
- $this->overwriteSameCase = true; // OK
- }
-
- return $status; // do nothing; either OK or bad status
- } else {
- $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
-
- return $status;
- }
- }
-
- return $status;
- }
-
- /**
- * precheckDestExistence() helper function to get the source file SHA-1.
- * Subclasses should overwride this if the source is not in storage.
- *
- * @return string|bool Returns false on failure
- */
- protected function getSourceSha1Base36() {
- return null; // N/A
- }
-
- /**
- * Check if a file will exist in storage when this operation is attempted
- *
- * @param string $source Storage path
- * @param array $predicates
- * @return bool
- */
- final protected function fileExists( $source, array $predicates ) {
- if ( isset( $predicates['exists'][$source] ) ) {
- return $predicates['exists'][$source]; // previous op assures this
- } else {
- $params = [ 'src' => $source, 'latest' => true ];
-
- return $this->backend->fileExists( $params );
- }
- }
-
- /**
- * Get the SHA-1 of a file in storage when this operation is attempted
- *
- * @param string $source Storage path
- * @param array $predicates
- * @return string|bool False on failure
- */
- final protected function fileSha1( $source, array $predicates ) {
- if ( isset( $predicates['sha1'][$source] ) ) {
- return $predicates['sha1'][$source]; // previous op assures this
- } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) {
- return false; // previous op assures this
- } else {
- $params = [ 'src' => $source, 'latest' => true ];
-
- return $this->backend->getFileSha1Base36( $params );
- }
- }
-
- /**
- * Get the backend this operation is for
- *
- * @return FileBackendStore
- */
- public function getBackend() {
- return $this->backend;
- }
-
- /**
- * Log a file operation failure and preserve any temp files
- *
- * @param string $action
- */
- final public function logFailure( $action ) {
- $params = $this->params;
- $params['failedAction'] = $action;
- try {
- wfDebugLog( 'FileOperation', get_class( $this ) .
- " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
- } catch ( Exception $e ) {
- // bad config? debug log error?
- }
- }
-}
-
-/**
- * Create a file in the backend with the given content.
- * Parameters for this operation are outlined in FileBackend::doOperations().
- */
-class CreateFileOp extends FileOp {
- protected function allowedParams() {
- return [
- [ 'content', 'dst' ],
- [ 'overwrite', 'overwriteSame', 'headers' ],
- [ 'dst' ]
- ];
- }
-
- protected function doPrecheck( array &$predicates ) {
- $status = Status::newGood();
- // Check if the source data is too big
- if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
- $status->fatal( 'backend-fail-maxsize',
- $this->params['dst'], $this->backend->maxFileSizeInternal() );
- $status->fatal( 'backend-fail-create', $this->params['dst'] );
-
- return $status;
- // Check if a file can be placed/changed at the destination
- } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
- $status->fatal( 'backend-fail-usable', $this->params['dst'] );
- $status->fatal( 'backend-fail-create', $this->params['dst'] );
-
- return $status;
- }
- // Check if destination file exists
- $status->merge( $this->precheckDestExistence( $predicates ) );
- $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
- if ( $status->isOK() ) {
- // Update file existence predicates
- $predicates['exists'][$this->params['dst']] = true;
- $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
- }
-
- return $status; // safe to call attempt()
- }
-
- protected function doAttempt() {
- if ( !$this->overwriteSameCase ) {
- // Create the file at the destination
- return $this->backend->createInternal( $this->setFlags( $this->params ) );
- }
-
- return Status::newGood();
- }
-
- protected function getSourceSha1Base36() {
- return Wikimedia\base_convert( sha1( $this->params['content'] ), 16, 36, 31 );
- }
-
- public function storagePathsChanged() {
- return [ $this->params['dst'] ];
- }
-}
-
-/**
- * Store a file into the backend from a file on the file system.
- * Parameters for this operation are outlined in FileBackend::doOperations().
- */
-class StoreFileOp extends FileOp {
- protected function allowedParams() {
- return [
- [ 'src', 'dst' ],
- [ 'overwrite', 'overwriteSame', 'headers' ],
- [ 'src', 'dst' ]
- ];
- }
-
- protected function doPrecheck( array &$predicates ) {
- $status = Status::newGood();
- // Check if the source file exists on the file system
- if ( !is_file( $this->params['src'] ) ) {
- $status->fatal( 'backend-fail-notexists', $this->params['src'] );
-
- return $status;
- // Check if the source file is too big
- } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
- $status->fatal( 'backend-fail-maxsize',
- $this->params['dst'], $this->backend->maxFileSizeInternal() );
- $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
-
- return $status;
- // Check if a file can be placed/changed at the destination
- } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
- $status->fatal( 'backend-fail-usable', $this->params['dst'] );
- $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
-
- return $status;
- }
- // Check if destination file exists
- $status->merge( $this->precheckDestExistence( $predicates ) );
- $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
- if ( $status->isOK() ) {
- // Update file existence predicates
- $predicates['exists'][$this->params['dst']] = true;
- $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
- }
-
- return $status; // safe to call attempt()
- }
-
- protected function doAttempt() {
- if ( !$this->overwriteSameCase ) {
- // Store the file at the destination
- return $this->backend->storeInternal( $this->setFlags( $this->params ) );
- }
-
- return Status::newGood();
- }
-
- protected function getSourceSha1Base36() {
- MediaWiki\suppressWarnings();
- $hash = sha1_file( $this->params['src'] );
- MediaWiki\restoreWarnings();
- if ( $hash !== false ) {
- $hash = Wikimedia\base_convert( $hash, 16, 36, 31 );
- }
-
- return $hash;
- }
-
- public function storagePathsChanged() {
- return [ $this->params['dst'] ];
- }
-}
-
-/**
- * Copy a file from one storage path to another in the backend.
- * Parameters for this operation are outlined in FileBackend::doOperations().
- */
-class CopyFileOp extends FileOp {
- protected function allowedParams() {
- return [
- [ 'src', 'dst' ],
- [ 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ],
- [ 'src', 'dst' ]
- ];
- }
-
- protected function doPrecheck( array &$predicates ) {
- $status = Status::newGood();
- // Check if the source file exists
- if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
- if ( $this->getParam( 'ignoreMissingSource' ) ) {
- $this->doOperation = false; // no-op
- // Update file existence predicates (cache 404s)
- $predicates['exists'][$this->params['src']] = false;
- $predicates['sha1'][$this->params['src']] = false;
-
- return $status; // nothing to do
- } else {
- $status->fatal( 'backend-fail-notexists', $this->params['src'] );
-
- return $status;
- }
- // Check if a file can be placed/changed at the destination
- } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
- $status->fatal( 'backend-fail-usable', $this->params['dst'] );
- $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
-
- return $status;
- }
- // Check if destination file exists
- $status->merge( $this->precheckDestExistence( $predicates ) );
- $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
- if ( $status->isOK() ) {
- // Update file existence predicates
- $predicates['exists'][$this->params['dst']] = true;
- $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
- }
-
- return $status; // safe to call attempt()
- }
-
- protected function doAttempt() {
- if ( $this->overwriteSameCase ) {
- $status = Status::newGood(); // nothing to do
- } elseif ( $this->params['src'] === $this->params['dst'] ) {
- // Just update the destination file headers
- $headers = $this->getParam( 'headers' ) ?: [];
- $status = $this->backend->describeInternal( $this->setFlags( [
- 'src' => $this->params['dst'], 'headers' => $headers
- ] ) );
- } else {
- // Copy the file to the destination
- $status = $this->backend->copyInternal( $this->setFlags( $this->params ) );
- }
-
- return $status;
- }
-
- public function storagePathsRead() {
- return [ $this->params['src'] ];
- }
-
- public function storagePathsChanged() {
- return [ $this->params['dst'] ];
- }
-}
-
-/**
- * Move a file from one storage path to another in the backend.
- * Parameters for this operation are outlined in FileBackend::doOperations().
- */
-class MoveFileOp extends FileOp {
- protected function allowedParams() {
- return [
- [ 'src', 'dst' ],
- [ 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ],
- [ 'src', 'dst' ]
- ];
- }
-
- protected function doPrecheck( array &$predicates ) {
- $status = Status::newGood();
- // Check if the source file exists
- if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
- if ( $this->getParam( 'ignoreMissingSource' ) ) {
- $this->doOperation = false; // no-op
- // Update file existence predicates (cache 404s)
- $predicates['exists'][$this->params['src']] = false;
- $predicates['sha1'][$this->params['src']] = false;
-
- return $status; // nothing to do
- } else {
- $status->fatal( 'backend-fail-notexists', $this->params['src'] );
-
- return $status;
- }
- // Check if a file can be placed/changed at the destination
- } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
- $status->fatal( 'backend-fail-usable', $this->params['dst'] );
- $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
-
- return $status;
- }
- // Check if destination file exists
- $status->merge( $this->precheckDestExistence( $predicates ) );
- $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
- if ( $status->isOK() ) {
- // Update file existence predicates
- $predicates['exists'][$this->params['src']] = false;
- $predicates['sha1'][$this->params['src']] = false;
- $predicates['exists'][$this->params['dst']] = true;
- $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
- }
-
- return $status; // safe to call attempt()
- }
-
- protected function doAttempt() {
- if ( $this->overwriteSameCase ) {
- if ( $this->params['src'] === $this->params['dst'] ) {
- // Do nothing to the destination (which is also the source)
- $status = Status::newGood();
- } else {
- // Just delete the source as the destination file needs no changes
- $status = $this->backend->deleteInternal( $this->setFlags(
- [ 'src' => $this->params['src'] ]
- ) );
- }
- } elseif ( $this->params['src'] === $this->params['dst'] ) {
- // Just update the destination file headers
- $headers = $this->getParam( 'headers' ) ?: [];
- $status = $this->backend->describeInternal( $this->setFlags(
- [ 'src' => $this->params['dst'], 'headers' => $headers ]
- ) );
- } else {
- // Move the file to the destination
- $status = $this->backend->moveInternal( $this->setFlags( $this->params ) );
- }
-
- return $status;
- }
-
- public function storagePathsRead() {
- return [ $this->params['src'] ];
- }
-
- public function storagePathsChanged() {
- return [ $this->params['src'], $this->params['dst'] ];
- }
-}
-
-/**
- * Delete a file at the given storage path from the backend.
- * Parameters for this operation are outlined in FileBackend::doOperations().
- */
-class DeleteFileOp extends FileOp {
- protected function allowedParams() {
- return [ [ 'src' ], [ 'ignoreMissingSource' ], [ 'src' ] ];
- }
-
- protected function doPrecheck( array &$predicates ) {
- $status = Status::newGood();
- // Check if the source file exists
- if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
- if ( $this->getParam( 'ignoreMissingSource' ) ) {
- $this->doOperation = false; // no-op
- // Update file existence predicates (cache 404s)
- $predicates['exists'][$this->params['src']] = false;
- $predicates['sha1'][$this->params['src']] = false;
-
- return $status; // nothing to do
- } else {
- $status->fatal( 'backend-fail-notexists', $this->params['src'] );
-
- return $status;
- }
- // Check if a file can be placed/changed at the source
- } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
- $status->fatal( 'backend-fail-usable', $this->params['src'] );
- $status->fatal( 'backend-fail-delete', $this->params['src'] );
-
- return $status;
- }
- // Update file existence predicates
- $predicates['exists'][$this->params['src']] = false;
- $predicates['sha1'][$this->params['src']] = false;
-
- return $status; // safe to call attempt()
- }
-
- protected function doAttempt() {
- // Delete the source file
- return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
- }
-
- public function storagePathsChanged() {
- return [ $this->params['src'] ];
- }
-}
-
-/**
- * Change metadata for a file at the given storage path in the backend.
- * Parameters for this operation are outlined in FileBackend::doOperations().
- */
-class DescribeFileOp extends FileOp {
- protected function allowedParams() {
- return [ [ 'src' ], [ 'headers' ], [ 'src' ] ];
- }
-
- protected function doPrecheck( array &$predicates ) {
- $status = Status::newGood();
- // Check if the source file exists
- if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
- $status->fatal( 'backend-fail-notexists', $this->params['src'] );
-
- return $status;
- // Check if a file can be placed/changed at the source
- } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
- $status->fatal( 'backend-fail-usable', $this->params['src'] );
- $status->fatal( 'backend-fail-describe', $this->params['src'] );
-
- return $status;
- }
- // Update file existence predicates
- $predicates['exists'][$this->params['src']] =
- $this->fileExists( $this->params['src'], $predicates );
- $predicates['sha1'][$this->params['src']] =
- $this->fileSha1( $this->params['src'], $predicates );
-
- return $status; // safe to call attempt()
- }
-
- protected function doAttempt() {
- // Update the source file's metadata
- return $this->backend->describeInternal( $this->setFlags( $this->params ) );
- }
-
- public function storagePathsChanged() {
- return [ $this->params['src'] ];
- }
-}
-
-/**
- * Placeholder operation that has no params and does nothing
- */
-class NullFileOp extends FileOp {
-}
diff --git a/www/wiki/includes/filebackend/FileOpBatch.php b/www/wiki/includes/filebackend/FileOpBatch.php
deleted file mode 100644
index 78209d8b..00000000
--- a/www/wiki/includes/filebackend/FileOpBatch.php
+++ /dev/null
@@ -1,202 +0,0 @@
-<?php
-/**
- * Helper class for representing batch file operations.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * Helper class for representing batch file operations.
- * Do not use this class from places outside FileBackend.
- *
- * Methods should avoid throwing exceptions at all costs.
- *
- * @ingroup FileBackend
- * @since 1.20
- */
-class FileOpBatch {
- /* Timeout related parameters */
- const MAX_BATCH_SIZE = 1000; // integer
-
- /**
- * Attempt to perform a series of file operations.
- * Callers are responsible for handling file locking.
- *
- * $opts is an array of options, including:
- * - force : Errors that would normally cause a rollback do not.
- * The remaining operations are still attempted if any fail.
- * - nonJournaled : Don't log this operation batch in the file journal.
- * - concurrency : Try to do this many operations in parallel when possible.
- *
- * The resulting Status will be "OK" unless:
- * - a) unexpected operation errors occurred (network partitions, disk full...)
- * - b) significant operation errors occurred and 'force' was not set
- *
- * @param FileOp[] $performOps List of FileOp operations
- * @param array $opts Batch operation options
- * @param FileJournal $journal Journal to log operations to
- * @return Status
- */
- public static function attempt( array $performOps, array $opts, FileJournal $journal ) {
- $status = Status::newGood();
-
- $n = count( $performOps );
- if ( $n > self::MAX_BATCH_SIZE ) {
- $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE );
-
- return $status;
- }
-
- $batchId = $journal->getTimestampedUUID();
- $ignoreErrors = !empty( $opts['force'] );
- $journaled = empty( $opts['nonJournaled'] );
- $maxConcurrency = isset( $opts['concurrency'] ) ? $opts['concurrency'] : 1;
-
- $entries = []; // file journal entry list
- $predicates = FileOp::newPredicates(); // account for previous ops in prechecks
- $curBatch = []; // concurrent FileOp sub-batch accumulation
- $curBatchDeps = FileOp::newDependencies(); // paths used in FileOp sub-batch
- $pPerformOps = []; // ordered list of concurrent FileOp sub-batches
- $lastBackend = null; // last op backend name
- // Do pre-checks for each operation; abort on failure...
- foreach ( $performOps as $index => $fileOp ) {
- $backendName = $fileOp->getBackend()->getName();
- $fileOp->setBatchId( $batchId ); // transaction ID
- // Decide if this op can be done concurrently within this sub-batch
- // or if a new concurrent sub-batch must be started after this one...
- if ( $fileOp->dependsOn( $curBatchDeps )
- || count( $curBatch ) >= $maxConcurrency
- || ( $backendName !== $lastBackend && count( $curBatch ) )
- ) {
- $pPerformOps[] = $curBatch; // push this batch
- $curBatch = []; // start a new sub-batch
- $curBatchDeps = FileOp::newDependencies();
- }
- $lastBackend = $backendName;
- $curBatch[$index] = $fileOp; // keep index
- // Update list of affected paths in this batch
- $curBatchDeps = $fileOp->applyDependencies( $curBatchDeps );
- // Simulate performing the operation...
- $oldPredicates = $predicates;
- $subStatus = $fileOp->precheck( $predicates ); // updates $predicates
- $status->merge( $subStatus );
- if ( $subStatus->isOK() ) {
- if ( $journaled ) { // journal log entries
- $entries = array_merge( $entries,
- $fileOp->getJournalEntries( $oldPredicates, $predicates ) );
- }
- } else { // operation failed?
- $status->success[$index] = false;
- ++$status->failCount;
- if ( !$ignoreErrors ) {
- return $status; // abort
- }
- }
- }
- // Push the last sub-batch
- if ( count( $curBatch ) ) {
- $pPerformOps[] = $curBatch;
- }
-
- // Log the operations in the file journal...
- if ( count( $entries ) ) {
- $subStatus = $journal->logChangeBatch( $entries, $batchId );
- if ( !$subStatus->isOK() ) {
- return $subStatus; // abort
- }
- }
-
- if ( $ignoreErrors ) { // treat precheck() fatals as mere warnings
- $status->setResult( true, $status->value );
- }
-
- // Attempt each operation (in parallel if allowed and possible)...
- self::runParallelBatches( $pPerformOps, $status );
-
- return $status;
- }
-
- /**
- * Attempt a list of file operations sub-batches in series.
- *
- * The operations *in* each sub-batch will be done in parallel.
- * The caller is responsible for making sure the operations
- * within any given sub-batch do not depend on each other.
- * This will abort remaining ops on failure.
- *
- * @param array $pPerformOps Batches of file ops (batches use original indexes)
- * @param Status $status
- */
- protected static function runParallelBatches( array $pPerformOps, Status $status ) {
- $aborted = false; // set to true on unexpected errors
- foreach ( $pPerformOps as $performOpsBatch ) {
- /** @var FileOp[] $performOpsBatch */
- if ( $aborted ) { // check batch op abort flag...
- // We can't continue (even with $ignoreErrors) as $predicates is wrong.
- // Log the remaining ops as failed for recovery...
- foreach ( $performOpsBatch as $i => $fileOp ) {
- $status->success[$i] = false;
- ++$status->failCount;
- $performOpsBatch[$i]->logFailure( 'attempt_aborted' );
- }
- continue;
- }
- /** @var Status[] $statuses */
- $statuses = [];
- $opHandles = [];
- // Get the backend; all sub-batch ops belong to a single backend
- $backend = reset( $performOpsBatch )->getBackend();
- // Get the operation handles or actually do it if there is just one.
- // If attemptAsync() returns a Status, it was either due to an error
- // or the backend does not support async ops and did it synchronously.
- foreach ( $performOpsBatch as $i => $fileOp ) {
- if ( !isset( $status->success[$i] ) ) { // didn't already fail in precheck()
- // Parallel ops may be disabled in config due to missing dependencies,
- // (e.g. needing popen()). When they are, $performOpsBatch has size 1.
- $subStatus = ( count( $performOpsBatch ) > 1 )
- ? $fileOp->attemptAsync()
- : $fileOp->attempt();
- if ( $subStatus->value instanceof FileBackendStoreOpHandle ) {
- $opHandles[$i] = $subStatus->value; // deferred
- } else {
- $statuses[$i] = $subStatus; // done already
- }
- }
- }
- // Try to do all the operations concurrently...
- $statuses = $statuses + $backend->executeOpHandlesInternal( $opHandles );
- // Marshall and merge all the responses (blocking)...
- foreach ( $performOpsBatch as $i => $fileOp ) {
- if ( !isset( $status->success[$i] ) ) { // didn't already fail in precheck()
- $subStatus = $statuses[$i];
- $status->merge( $subStatus );
- if ( $subStatus->isOK() ) {
- $status->success[$i] = true;
- ++$status->successCount;
- } else {
- $status->success[$i] = false;
- ++$status->failCount;
- $aborted = true; // set abort flag; we can't continue
- }
- }
- }
- }
- }
-}
diff --git a/www/wiki/includes/filebackend/MemoryFileBackend.php b/www/wiki/includes/filebackend/MemoryFileBackend.php
deleted file mode 100644
index 6e32c629..00000000
--- a/www/wiki/includes/filebackend/MemoryFileBackend.php
+++ /dev/null
@@ -1,278 +0,0 @@
-<?php
-/**
- * Simulation of a backend storage in memory.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * Simulation of a backend storage in memory.
- *
- * All data in the backend is automatically deleted at the end of PHP execution.
- * Since the data stored here is volatile, this is only useful for staging or testing.
- *
- * @ingroup FileBackend
- * @since 1.23
- */
-class MemoryFileBackend extends FileBackendStore {
- /** @var array Map of (file path => (data,mtime) */
- protected $files = [];
-
- public function getFeatures() {
- return self::ATTR_UNICODE_PATHS;
- }
-
- public function isPathUsableInternal( $storagePath ) {
- return true;
- }
-
- protected function doCreateInternal( array $params ) {
- $status = Status::newGood();
-
- $dst = $this->resolveHashKey( $params['dst'] );
- if ( $dst === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
- return $status;
- }
-
- $this->files[$dst] = [
- 'data' => $params['content'],
- 'mtime' => wfTimestamp( TS_MW, time() )
- ];
-
- return $status;
- }
-
- protected function doStoreInternal( array $params ) {
- $status = Status::newGood();
-
- $dst = $this->resolveHashKey( $params['dst'] );
- if ( $dst === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
- return $status;
- }
-
- MediaWiki\suppressWarnings();
- $data = file_get_contents( $params['src'] );
- MediaWiki\restoreWarnings();
- if ( $data === false ) { // source doesn't exist?
- $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
-
- return $status;
- }
-
- $this->files[$dst] = [
- 'data' => $data,
- 'mtime' => wfTimestamp( TS_MW, time() )
- ];
-
- return $status;
- }
-
- protected function doCopyInternal( array $params ) {
- $status = Status::newGood();
-
- $src = $this->resolveHashKey( $params['src'] );
- if ( $src === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
- return $status;
- }
-
- $dst = $this->resolveHashKey( $params['dst'] );
- if ( $dst === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
- return $status;
- }
-
- if ( !isset( $this->files[$src] ) ) {
- if ( empty( $params['ignoreMissingSource'] ) ) {
- $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
- }
-
- return $status;
- }
-
- $this->files[$dst] = [
- 'data' => $this->files[$src]['data'],
- 'mtime' => wfTimestamp( TS_MW, time() )
- ];
-
- return $status;
- }
-
- protected function doDeleteInternal( array $params ) {
- $status = Status::newGood();
-
- $src = $this->resolveHashKey( $params['src'] );
- if ( $src === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
- return $status;
- }
-
- if ( !isset( $this->files[$src] ) ) {
- if ( empty( $params['ignoreMissingSource'] ) ) {
- $status->fatal( 'backend-fail-delete', $params['src'] );
- }
-
- return $status;
- }
-
- unset( $this->files[$src] );
-
- return $status;
- }
-
- protected function doGetFileStat( array $params ) {
- $src = $this->resolveHashKey( $params['src'] );
- if ( $src === null ) {
- return null;
- }
-
- if ( isset( $this->files[$src] ) ) {
- return [
- 'mtime' => $this->files[$src]['mtime'],
- 'size' => strlen( $this->files[$src]['data'] ),
- ];
- }
-
- return false;
- }
-
- protected function doGetLocalCopyMulti( array $params ) {
- $tmpFiles = []; // (path => TempFSFile)
- foreach ( $params['srcs'] as $srcPath ) {
- $src = $this->resolveHashKey( $srcPath );
- if ( $src === null || !isset( $this->files[$src] ) ) {
- $fsFile = null;
- } else {
- // Create a new temporary file with the same extension...
- $ext = FileBackend::extensionFromPath( $src );
- $fsFile = TempFSFile::factory( 'localcopy_', $ext );
- if ( $fsFile ) {
- $bytes = file_put_contents( $fsFile->getPath(), $this->files[$src]['data'] );
- if ( $bytes !== strlen( $this->files[$src]['data'] ) ) {
- $fsFile = null;
- }
- }
- }
- $tmpFiles[$srcPath] = $fsFile;
- }
-
- return $tmpFiles;
- }
-
- protected function doStreamFile( array $params ) {
- $status = Status::newGood();
-
- $src = $this->resolveHashKey( $params['src'] );
- if ( $src === null || !isset( $this->files[$src] ) ) {
- $status->fatal( 'backend-fail-stream', $params['src'] );
-
- return $status;
- }
-
- print $this->files[$src]['data'];
-
- return $status;
- }
-
- protected function doDirectoryExists( $container, $dir, array $params ) {
- $prefix = rtrim( "$container/$dir", '/' ) . '/';
- foreach ( $this->files as $path => $data ) {
- if ( strpos( $path, $prefix ) === 0 ) {
- return true;
- }
- }
-
- return false;
- }
-
- public function getDirectoryListInternal( $container, $dir, array $params ) {
- $dirs = [];
- $prefix = rtrim( "$container/$dir", '/' ) . '/';
- $prefixLen = strlen( $prefix );
- foreach ( $this->files as $path => $data ) {
- if ( strpos( $path, $prefix ) === 0 ) {
- $relPath = substr( $path, $prefixLen );
- if ( $relPath === false ) {
- continue;
- } elseif ( strpos( $relPath, '/' ) === false ) {
- continue; // just a file
- }
- $parts = array_slice( explode( '/', $relPath ), 0, -1 ); // last part is file name
- if ( !empty( $params['topOnly'] ) ) {
- $dirs[$parts[0]] = 1; // top directory
- } else {
- $current = '';
- foreach ( $parts as $part ) { // all directories
- $dir = ( $current === '' ) ? $part : "$current/$part";
- $dirs[$dir] = 1;
- $current = $dir;
- }
- }
- }
- }
-
- return array_keys( $dirs );
- }
-
- public function getFileListInternal( $container, $dir, array $params ) {
- $files = [];
- $prefix = rtrim( "$container/$dir", '/' ) . '/';
- $prefixLen = strlen( $prefix );
- foreach ( $this->files as $path => $data ) {
- if ( strpos( $path, $prefix ) === 0 ) {
- $relPath = substr( $path, $prefixLen );
- if ( $relPath === false ) {
- continue;
- } elseif ( !empty( $params['topOnly'] ) && strpos( $relPath, '/' ) !== false ) {
- continue;
- }
- $files[] = $relPath;
- }
- }
-
- return $files;
- }
-
- protected function directoriesAreVirtual() {
- return true;
- }
-
- /**
- * Get the absolute file system path for a storage path
- *
- * @param string $storagePath Storage path
- * @return string|null
- */
- protected function resolveHashKey( $storagePath ) {
- list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
- if ( $relPath === null ) {
- return null; // invalid
- }
-
- return ( $relPath !== '' ) ? "$fullCont/$relPath" : $fullCont;
- }
-}
diff --git a/www/wiki/includes/filebackend/SwiftFileBackend.php b/www/wiki/includes/filebackend/SwiftFileBackend.php
deleted file mode 100644
index 0f7e4b56..00000000
--- a/www/wiki/includes/filebackend/SwiftFileBackend.php
+++ /dev/null
@@ -1,1910 +0,0 @@
-<?php
-/**
- * OpenStack Swift based file backend.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileBackend
- * @author Russ Nelson
- * @author Aaron Schulz
- */
-
-/**
- * @brief Class for an OpenStack Swift (or Ceph RGW) based file backend.
- *
- * Status messages should avoid mentioning the Swift account name.
- * Likewise, error suppression should be used to avoid path disclosure.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-class SwiftFileBackend extends FileBackendStore {
- /** @var MultiHttpClient */
- protected $http;
-
- /** @var int TTL in seconds */
- protected $authTTL;
-
- /** @var string Authentication base URL (without version) */
- protected $swiftAuthUrl;
-
- /** @var string Swift user (account:user) to authenticate as */
- protected $swiftUser;
-
- /** @var string Secret key for user */
- protected $swiftKey;
-
- /** @var string Shared secret value for making temp URLs */
- protected $swiftTempUrlKey;
-
- /** @var string S3 access key (RADOS Gateway) */
- protected $rgwS3AccessKey;
-
- /** @var string S3 authentication key (RADOS Gateway) */
- protected $rgwS3SecretKey;
-
- /** @var BagOStuff */
- protected $srvCache;
-
- /** @var ProcessCacheLRU Container stat cache */
- protected $containerStatCache;
-
- /** @var array */
- protected $authCreds;
-
- /** @var int UNIX timestamp */
- protected $authSessionTimestamp = 0;
-
- /** @var int UNIX timestamp */
- protected $authErrorTimestamp = null;
-
- /** @var bool Whether the server is an Ceph RGW */
- protected $isRGW = false;
-
- /**
- * @see FileBackendStore::__construct()
- * Additional $config params include:
- * - swiftAuthUrl : Swift authentication server URL
- * - swiftUser : Swift user used by MediaWiki (account:username)
- * - swiftKey : Swift authentication key for the above user
- * - swiftAuthTTL : Swift authentication TTL (seconds)
- * - swiftTempUrlKey : Swift "X-Account-Meta-Temp-URL-Key" value on the account.
- * Do not set this until it has been set in the backend.
- * - shardViaHashLevels : Map of container names to sharding config with:
- * - base : base of hash characters, 16 or 36
- * - levels : the number of hash levels (and digits)
- * - repeat : hash subdirectories are prefixed with all the
- * parent hash directory names (e.g. "a/ab/abc")
- * - cacheAuthInfo : Whether to cache authentication tokens in APC, XCache, ect.
- * If those are not available, then the main cache will be used.
- * This is probably insecure in shared hosting environments.
- * - rgwS3AccessKey : Rados Gateway S3 "access key" value on the account.
- * Do not set this until it has been set in the backend.
- * This is used for generating expiring pre-authenticated URLs.
- * Only use this when using rgw and to work around
- * http://tracker.newdream.net/issues/3454.
- * - rgwS3SecretKey : Rados Gateway S3 "secret key" value on the account.
- * Do not set this until it has been set in the backend.
- * This is used for generating expiring pre-authenticated URLs.
- * Only use this when using rgw and to work around
- * http://tracker.newdream.net/issues/3454.
- */
- public function __construct( array $config ) {
- parent::__construct( $config );
- // Required settings
- $this->swiftAuthUrl = $config['swiftAuthUrl'];
- $this->swiftUser = $config['swiftUser'];
- $this->swiftKey = $config['swiftKey'];
- // Optional settings
- $this->authTTL = isset( $config['swiftAuthTTL'] )
- ? $config['swiftAuthTTL']
- : 15 * 60; // some sane number
- $this->swiftTempUrlKey = isset( $config['swiftTempUrlKey'] )
- ? $config['swiftTempUrlKey']
- : '';
- $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
- ? $config['shardViaHashLevels']
- : '';
- $this->rgwS3AccessKey = isset( $config['rgwS3AccessKey'] )
- ? $config['rgwS3AccessKey']
- : '';
- $this->rgwS3SecretKey = isset( $config['rgwS3SecretKey'] )
- ? $config['rgwS3SecretKey']
- : '';
- // HTTP helper client
- $this->http = new MultiHttpClient( [] );
- // Cache container information to mask latency
- if ( isset( $config['wanCache'] ) && $config['wanCache'] instanceof WANObjectCache ) {
- $this->memCache = $config['wanCache'];
- }
- // Process cache for container info
- $this->containerStatCache = new ProcessCacheLRU( 300 );
- // Cache auth token information to avoid RTTs
- if ( !empty( $config['cacheAuthInfo'] ) ) {
- if ( PHP_SAPI === 'cli' ) {
- // Preferrably memcached
- $this->srvCache = ObjectCache::getLocalClusterInstance();
- } else {
- // Look for APC, XCache, WinCache, ect...
- $this->srvCache = ObjectCache::getLocalServerInstance( CACHE_NONE );
- }
- } else {
- $this->srvCache = new EmptyBagOStuff();
- }
- }
-
- public function getFeatures() {
- return ( FileBackend::ATTR_UNICODE_PATHS |
- FileBackend::ATTR_HEADERS | FileBackend::ATTR_METADATA );
- }
-
- protected function resolveContainerPath( $container, $relStoragePath ) {
- if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) {
- return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
- } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
- return null; // too long for Swift
- }
-
- return $relStoragePath;
- }
-
- public function isPathUsableInternal( $storagePath ) {
- list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
- if ( $rel === null ) {
- return false; // invalid
- }
-
- return is_array( $this->getContainerStat( $container ) );
- }
-
- /**
- * Sanitize and filter the custom headers from a $params array.
- * Only allows certain "standard" Content- and X-Content- headers.
- *
- * @param array $params
- * @return array Sanitized value of 'headers' field in $params
- */
- protected function sanitizeHdrs( array $params ) {
- return isset( $params['headers'] )
- ? $this->getCustomHeaders( $params['headers'] )
- : [];
-
- }
-
- /**
- * @param array $rawHeaders
- * @return array Custom non-metadata HTTP headers
- */
- protected function getCustomHeaders( array $rawHeaders ) {
- $headers = [];
-
- // Normalize casing, and strip out illegal headers
- foreach ( $rawHeaders as $name => $value ) {
- $name = strtolower( $name );
- if ( preg_match( '/^content-(type|length)$/', $name ) ) {
- continue; // blacklisted
- } elseif ( preg_match( '/^(x-)?content-/', $name ) ) {
- $headers[$name] = $value; // allowed
- } elseif ( preg_match( '/^content-(disposition)/', $name ) ) {
- $headers[$name] = $value; // allowed
- }
- }
- // By default, Swift has annoyingly low maximum header value limits
- if ( isset( $headers['content-disposition'] ) ) {
- $disposition = '';
- // @note: assume FileBackend::makeContentDisposition() already used
- foreach ( explode( ';', $headers['content-disposition'] ) as $part ) {
- $part = trim( $part );
- $new = ( $disposition === '' ) ? $part : "{$disposition};{$part}";
- if ( strlen( $new ) <= 255 ) {
- $disposition = $new;
- } else {
- break; // too long; sigh
- }
- }
- $headers['content-disposition'] = $disposition;
- }
-
- return $headers;
- }
-
- /**
- * @param array $rawHeaders
- * @return array Custom metadata headers
- */
- protected function getMetadataHeaders( array $rawHeaders ) {
- $headers = [];
- foreach ( $rawHeaders as $name => $value ) {
- $name = strtolower( $name );
- if ( strpos( $name, 'x-object-meta-' ) === 0 ) {
- $headers[$name] = $value;
- }
- }
-
- return $headers;
- }
-
- /**
- * @param array $rawHeaders
- * @return array Custom metadata headers with prefix removed
- */
- protected function getMetadata( array $rawHeaders ) {
- $metadata = [];
- foreach ( $this->getMetadataHeaders( $rawHeaders ) as $name => $value ) {
- $metadata[substr( $name, strlen( 'x-object-meta-' ) )] = $value;
- }
-
- return $metadata;
- }
-
- protected function doCreateInternal( array $params ) {
- $status = Status::newGood();
-
- list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
- if ( $dstRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
- return $status;
- }
-
- $sha1Hash = Wikimedia\base_convert( sha1( $params['content'] ), 16, 36, 31 );
- $contentType = isset( $params['headers']['content-type'] )
- ? $params['headers']['content-type']
- : $this->getContentType( $params['dst'], $params['content'], null );
-
- $reqs = [ [
- 'method' => 'PUT',
- 'url' => [ $dstCont, $dstRel ],
- 'headers' => [
- 'content-length' => strlen( $params['content'] ),
- 'etag' => md5( $params['content'] ),
- 'content-type' => $contentType,
- 'x-object-meta-sha1base36' => $sha1Hash
- ] + $this->sanitizeHdrs( $params ),
- 'body' => $params['content']
- ] ];
-
- $method = __METHOD__;
- $handler = function ( array $request, Status $status ) use ( $method, $params ) {
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
- if ( $rcode === 201 ) {
- // good
- } elseif ( $rcode === 412 ) {
- $status->fatal( 'backend-fail-contenttype', $params['dst'] );
- } else {
- $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
- }
- };
-
- $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
- if ( !empty( $params['async'] ) ) { // deferred
- $status->value = $opHandle;
- } else { // actually write the object in Swift
- $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
- }
-
- return $status;
- }
-
- protected function doStoreInternal( array $params ) {
- $status = Status::newGood();
-
- list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
- if ( $dstRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
- return $status;
- }
-
- MediaWiki\suppressWarnings();
- $sha1Hash = sha1_file( $params['src'] );
- MediaWiki\restoreWarnings();
- if ( $sha1Hash === false ) { // source doesn't exist?
- $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
-
- return $status;
- }
- $sha1Hash = Wikimedia\base_convert( $sha1Hash, 16, 36, 31 );
- $contentType = isset( $params['headers']['content-type'] )
- ? $params['headers']['content-type']
- : $this->getContentType( $params['dst'], null, $params['src'] );
-
- $handle = fopen( $params['src'], 'rb' );
- if ( $handle === false ) { // source doesn't exist?
- $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
-
- return $status;
- }
-
- $reqs = [ [
- 'method' => 'PUT',
- 'url' => [ $dstCont, $dstRel ],
- 'headers' => [
- 'content-length' => filesize( $params['src'] ),
- 'etag' => md5_file( $params['src'] ),
- 'content-type' => $contentType,
- 'x-object-meta-sha1base36' => $sha1Hash
- ] + $this->sanitizeHdrs( $params ),
- 'body' => $handle // resource
- ] ];
-
- $method = __METHOD__;
- $handler = function ( array $request, Status $status ) use ( $method, $params ) {
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
- if ( $rcode === 201 ) {
- // good
- } elseif ( $rcode === 412 ) {
- $status->fatal( 'backend-fail-contenttype', $params['dst'] );
- } else {
- $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
- }
- };
-
- $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
- if ( !empty( $params['async'] ) ) { // deferred
- $status->value = $opHandle;
- } else { // actually write the object in Swift
- $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
- }
-
- return $status;
- }
-
- protected function doCopyInternal( array $params ) {
- $status = Status::newGood();
-
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
- return $status;
- }
-
- list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
- if ( $dstRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
- return $status;
- }
-
- $reqs = [ [
- 'method' => 'PUT',
- 'url' => [ $dstCont, $dstRel ],
- 'headers' => [
- 'x-copy-from' => '/' . rawurlencode( $srcCont ) .
- '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
- ] + $this->sanitizeHdrs( $params ), // extra headers merged into object
- ] ];
-
- $method = __METHOD__;
- $handler = function ( array $request, Status $status ) use ( $method, $params ) {
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
- if ( $rcode === 201 ) {
- // good
- } elseif ( $rcode === 404 ) {
- $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
- } else {
- $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
- }
- };
-
- $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
- if ( !empty( $params['async'] ) ) { // deferred
- $status->value = $opHandle;
- } else { // actually write the object in Swift
- $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
- }
-
- return $status;
- }
-
- protected function doMoveInternal( array $params ) {
- $status = Status::newGood();
-
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
- return $status;
- }
-
- list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
- if ( $dstRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
- return $status;
- }
-
- $reqs = [
- [
- 'method' => 'PUT',
- 'url' => [ $dstCont, $dstRel ],
- 'headers' => [
- 'x-copy-from' => '/' . rawurlencode( $srcCont ) .
- '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
- ] + $this->sanitizeHdrs( $params ) // extra headers merged into object
- ]
- ];
- if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) {
- $reqs[] = [
- 'method' => 'DELETE',
- 'url' => [ $srcCont, $srcRel ],
- 'headers' => []
- ];
- }
-
- $method = __METHOD__;
- $handler = function ( array $request, Status $status ) use ( $method, $params ) {
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
- if ( $request['method'] === 'PUT' && $rcode === 201 ) {
- // good
- } elseif ( $request['method'] === 'DELETE' && $rcode === 204 ) {
- // good
- } elseif ( $rcode === 404 ) {
- $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
- } else {
- $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
- }
- };
-
- $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
- if ( !empty( $params['async'] ) ) { // deferred
- $status->value = $opHandle;
- } else { // actually move the object in Swift
- $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
- }
-
- return $status;
- }
-
- protected function doDeleteInternal( array $params ) {
- $status = Status::newGood();
-
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
- return $status;
- }
-
- $reqs = [ [
- 'method' => 'DELETE',
- 'url' => [ $srcCont, $srcRel ],
- 'headers' => []
- ] ];
-
- $method = __METHOD__;
- $handler = function ( array $request, Status $status ) use ( $method, $params ) {
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
- if ( $rcode === 204 ) {
- // good
- } elseif ( $rcode === 404 ) {
- if ( empty( $params['ignoreMissingSource'] ) ) {
- $status->fatal( 'backend-fail-delete', $params['src'] );
- }
- } else {
- $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
- }
- };
-
- $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
- if ( !empty( $params['async'] ) ) { // deferred
- $status->value = $opHandle;
- } else { // actually delete the object in Swift
- $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
- }
-
- return $status;
- }
-
- protected function doDescribeInternal( array $params ) {
- $status = Status::newGood();
-
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
- return $status;
- }
-
- // Fetch the old object headers/metadata...this should be in stat cache by now
- $stat = $this->getFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
- if ( $stat && !isset( $stat['xattr'] ) ) { // older cache entry
- $stat = $this->doGetFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
- }
- if ( !$stat ) {
- $status->fatal( 'backend-fail-describe', $params['src'] );
-
- return $status;
- }
-
- // POST clears prior headers, so we need to merge the changes in to the old ones
- $metaHdrs = [];
- foreach ( $stat['xattr']['metadata'] as $name => $value ) {
- $metaHdrs["x-object-meta-$name"] = $value;
- }
- $customHdrs = $this->sanitizeHdrs( $params ) + $stat['xattr']['headers'];
-
- $reqs = [ [
- 'method' => 'POST',
- 'url' => [ $srcCont, $srcRel ],
- 'headers' => $metaHdrs + $customHdrs
- ] ];
-
- $method = __METHOD__;
- $handler = function ( array $request, Status $status ) use ( $method, $params ) {
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
- if ( $rcode === 202 ) {
- // good
- } elseif ( $rcode === 404 ) {
- $status->fatal( 'backend-fail-describe', $params['src'] );
- } else {
- $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
- }
- };
-
- $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
- if ( !empty( $params['async'] ) ) { // deferred
- $status->value = $opHandle;
- } else { // actually change the object in Swift
- $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
- }
-
- return $status;
- }
-
- protected function doPrepareInternal( $fullCont, $dir, array $params ) {
- $status = Status::newGood();
-
- // (a) Check if container already exists
- $stat = $this->getContainerStat( $fullCont );
- if ( is_array( $stat ) ) {
- return $status; // already there
- } elseif ( $stat === null ) {
- $status->fatal( 'backend-fail-internal', $this->name );
- wfDebugLog( 'SwiftBackend', __METHOD__ . ': cannot get container stat' );
-
- return $status;
- }
-
- // (b) Create container as needed with proper ACLs
- if ( $stat === false ) {
- $params['op'] = 'prepare';
- $status->merge( $this->createContainer( $fullCont, $params ) );
- }
-
- return $status;
- }
-
- protected function doSecureInternal( $fullCont, $dir, array $params ) {
- $status = Status::newGood();
- if ( empty( $params['noAccess'] ) ) {
- return $status; // nothing to do
- }
-
- $stat = $this->getContainerStat( $fullCont );
- if ( is_array( $stat ) ) {
- // Make container private to end-users...
- $status->merge( $this->setContainerAccess(
- $fullCont,
- [ $this->swiftUser ], // read
- [ $this->swiftUser ] // write
- ) );
- } elseif ( $stat === false ) {
- $status->fatal( 'backend-fail-usable', $params['dir'] );
- } else {
- $status->fatal( 'backend-fail-internal', $this->name );
- wfDebugLog( 'SwiftBackend', __METHOD__ . ': cannot get container stat' );
- }
-
- return $status;
- }
-
- protected function doPublishInternal( $fullCont, $dir, array $params ) {
- $status = Status::newGood();
-
- $stat = $this->getContainerStat( $fullCont );
- if ( is_array( $stat ) ) {
- // Make container public to end-users...
- $status->merge( $this->setContainerAccess(
- $fullCont,
- [ $this->swiftUser, '.r:*' ], // read
- [ $this->swiftUser ] // write
- ) );
- } elseif ( $stat === false ) {
- $status->fatal( 'backend-fail-usable', $params['dir'] );
- } else {
- $status->fatal( 'backend-fail-internal', $this->name );
- wfDebugLog( 'SwiftBackend', __METHOD__ . ': cannot get container stat' );
- }
-
- return $status;
- }
-
- protected function doCleanInternal( $fullCont, $dir, array $params ) {
- $status = Status::newGood();
-
- // Only containers themselves can be removed, all else is virtual
- if ( $dir != '' ) {
- return $status; // nothing to do
- }
-
- // (a) Check the container
- $stat = $this->getContainerStat( $fullCont, true );
- if ( $stat === false ) {
- return $status; // ok, nothing to do
- } elseif ( !is_array( $stat ) ) {
- $status->fatal( 'backend-fail-internal', $this->name );
- wfDebugLog( 'SwiftBackend', __METHOD__ . ': cannot get container stat' );
-
- return $status;
- }
-
- // (b) Delete the container if empty
- if ( $stat['count'] == 0 ) {
- $params['op'] = 'clean';
- $status->merge( $this->deleteContainer( $fullCont, $params ) );
- }
-
- return $status;
- }
-
- protected function doGetFileStat( array $params ) {
- $params = [ 'srcs' => [ $params['src'] ], 'concurrency' => 1 ] + $params;
- unset( $params['src'] );
- $stats = $this->doGetFileStatMulti( $params );
-
- return reset( $stats );
- }
-
- /**
- * Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT"/"2013-05-11T07:37:27.678360Z".
- * Dates might also come in like "2013-05-11T07:37:27.678360" from Swift listings,
- * missing the timezone suffix (though Ceph RGW does not appear to have this bug).
- *
- * @param string $ts
- * @param int $format Output format (TS_* constant)
- * @return string
- * @throws FileBackendError
- */
- protected function convertSwiftDate( $ts, $format = TS_MW ) {
- try {
- $timestamp = new MWTimestamp( $ts );
-
- return $timestamp->getTimestamp( $format );
- } catch ( Exception $e ) {
- throw new FileBackendError( $e->getMessage() );
- }
- }
-
- /**
- * Fill in any missing object metadata and save it to Swift
- *
- * @param array $objHdrs Object response headers
- * @param string $path Storage path to object
- * @return array New headers
- */
- protected function addMissingMetadata( array $objHdrs, $path ) {
- if ( isset( $objHdrs['x-object-meta-sha1base36'] ) ) {
- return $objHdrs; // nothing to do
- }
-
- /** @noinspection PhpUnusedLocalVariableInspection */
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
- wfDebugLog( 'SwiftBackend', __METHOD__ . ": $path was not stored with SHA-1 metadata." );
-
- $objHdrs['x-object-meta-sha1base36'] = false;
-
- $auth = $this->getAuthentication();
- if ( !$auth ) {
- return $objHdrs; // failed
- }
-
- // Find prior custom HTTP headers
- $postHeaders = $this->getCustomHeaders( $objHdrs );
- // Find prior metadata headers
- $postHeaders += $this->getMetadataHeaders( $objHdrs );
-
- $status = Status::newGood();
- /** @noinspection PhpUnusedLocalVariableInspection */
- $scopeLockS = $this->getScopedFileLocks( [ $path ], LockManager::LOCK_UW, $status );
- if ( $status->isOK() ) {
- $tmpFile = $this->getLocalCopy( [ 'src' => $path, 'latest' => 1 ] );
- if ( $tmpFile ) {
- $hash = $tmpFile->getSha1Base36();
- if ( $hash !== false ) {
- $objHdrs['x-object-meta-sha1base36'] = $hash;
- // Merge new SHA1 header into the old ones
- $postHeaders['x-object-meta-sha1base36'] = $hash;
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
- list( $rcode ) = $this->http->run( [
- 'method' => 'POST',
- 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
- 'headers' => $this->authTokenHeaders( $auth ) + $postHeaders
- ] );
- if ( $rcode >= 200 && $rcode <= 299 ) {
- $this->deleteFileCache( $path );
-
- return $objHdrs; // success
- }
- }
- }
- }
-
- wfDebugLog( 'SwiftBackend', __METHOD__ . ": unable to set SHA-1 metadata for $path" );
-
- return $objHdrs; // failed
- }
-
- protected function doGetFileContentsMulti( array $params ) {
- $contents = [];
-
- $auth = $this->getAuthentication();
-
- $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
- // Blindly create tmp files and stream to them, catching any exception if the file does
- // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata().
- $reqs = []; // (path => op)
-
- foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
- if ( $srcRel === null || !$auth ) {
- $contents[$path] = false;
- continue;
- }
- // Create a new temporary memory file...
- $handle = fopen( 'php://temp', 'wb' );
- if ( $handle ) {
- $reqs[$path] = [
- 'method' => 'GET',
- 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
- 'headers' => $this->authTokenHeaders( $auth )
- + $this->headersFromParams( $params ),
- 'stream' => $handle,
- ];
- }
- $contents[$path] = false;
- }
-
- $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
- $reqs = $this->http->runMulti( $reqs, $opts );
- foreach ( $reqs as $path => $op ) {
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
- if ( $rcode >= 200 && $rcode <= 299 ) {
- rewind( $op['stream'] ); // start from the beginning
- $contents[$path] = stream_get_contents( $op['stream'] );
- } elseif ( $rcode === 404 ) {
- $contents[$path] = false;
- } else {
- $this->onError( null, __METHOD__,
- [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
- }
- fclose( $op['stream'] ); // close open handle
- }
-
- return $contents;
- }
-
- protected function doDirectoryExists( $fullCont, $dir, array $params ) {
- $prefix = ( $dir == '' ) ? null : "{$dir}/";
- $status = $this->objectListing( $fullCont, 'names', 1, null, $prefix );
- if ( $status->isOK() ) {
- return ( count( $status->value ) ) > 0;
- }
-
- return null; // error
- }
-
- /**
- * @see FileBackendStore::getDirectoryListInternal()
- * @param string $fullCont
- * @param string $dir
- * @param array $params
- * @return SwiftFileBackendDirList
- */
- public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
- return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
- }
-
- /**
- * @see FileBackendStore::getFileListInternal()
- * @param string $fullCont
- * @param string $dir
- * @param array $params
- * @return SwiftFileBackendFileList
- */
- public function getFileListInternal( $fullCont, $dir, array $params ) {
- return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
- }
-
- /**
- * Do not call this function outside of SwiftFileBackendFileList
- *
- * @param string $fullCont Resolved container name
- * @param string $dir Resolved storage directory with no trailing slash
- * @param string|null $after Resolved container relative path to list items after
- * @param int $limit Max number of items to list
- * @param array $params Parameters for getDirectoryList()
- * @return array List of container relative resolved paths of directories directly under $dir
- * @throws FileBackendError
- */
- public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
- $dirs = [];
- if ( $after === INF ) {
- return $dirs; // nothing more
- }
-
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
- $prefix = ( $dir == '' ) ? null : "{$dir}/";
- // Non-recursive: only list dirs right under $dir
- if ( !empty( $params['topOnly'] ) ) {
- $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
- if ( !$status->isOK() ) {
- throw new FileBackendError( "Iterator page I/O error: {$status->getMessage()}" );
- }
- $objects = $status->value;
- foreach ( $objects as $object ) { // files and directories
- if ( substr( $object, -1 ) === '/' ) {
- $dirs[] = $object; // directories end in '/'
- }
- }
- } else {
- // Recursive: list all dirs under $dir and its subdirs
- $getParentDir = function ( $path ) {
- return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
- };
-
- // Get directory from last item of prior page
- $lastDir = $getParentDir( $after ); // must be first page
- $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
-
- if ( !$status->isOK() ) {
- throw new FileBackendError( "Iterator page I/O error: {$status->getMessage()}" );
- }
-
- $objects = $status->value;
-
- foreach ( $objects as $object ) { // files
- $objectDir = $getParentDir( $object ); // directory of object
-
- if ( $objectDir !== false && $objectDir !== $dir ) {
- // Swift stores paths in UTF-8, using binary sorting.
- // See function "create_container_table" in common/db.py.
- // If a directory is not "greater" than the last one,
- // then it was already listed by the calling iterator.
- if ( strcmp( $objectDir, $lastDir ) > 0 ) {
- $pDir = $objectDir;
- do { // add dir and all its parent dirs
- $dirs[] = "{$pDir}/";
- $pDir = $getParentDir( $pDir );
- } while ( $pDir !== false // sanity
- && strcmp( $pDir, $lastDir ) > 0 // not done already
- && strlen( $pDir ) > strlen( $dir ) // within $dir
- );
- }
- $lastDir = $objectDir;
- }
- }
- }
- // Page on the unfiltered directory listing (what is returned may be filtered)
- if ( count( $objects ) < $limit ) {
- $after = INF; // avoid a second RTT
- } else {
- $after = end( $objects ); // update last item
- }
-
- return $dirs;
- }
-
- /**
- * Do not call this function outside of SwiftFileBackendFileList
- *
- * @param string $fullCont Resolved container name
- * @param string $dir Resolved storage directory with no trailing slash
- * @param string|null $after Resolved container relative path of file to list items after
- * @param int $limit Max number of items to list
- * @param array $params Parameters for getDirectoryList()
- * @return array List of resolved container relative paths of files under $dir
- * @throws FileBackendError
- */
- public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
- $files = []; // list of (path, stat array or null) entries
- if ( $after === INF ) {
- return $files; // nothing more
- }
-
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
- $prefix = ( $dir == '' ) ? null : "{$dir}/";
- // $objects will contain a list of unfiltered names or CF_Object items
- // Non-recursive: only list files right under $dir
- if ( !empty( $params['topOnly'] ) ) {
- if ( !empty( $params['adviseStat'] ) ) {
- $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix, '/' );
- } else {
- $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
- }
- } else {
- // Recursive: list all files under $dir and its subdirs
- if ( !empty( $params['adviseStat'] ) ) {
- $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix );
- } else {
- $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
- }
- }
-
- // Reformat this list into a list of (name, stat array or null) entries
- if ( !$status->isOK() ) {
- throw new FileBackendError( "Iterator page I/O error: {$status->getMessage()}" );
- }
-
- $objects = $status->value;
- $files = $this->buildFileObjectListing( $params, $dir, $objects );
-
- // Page on the unfiltered object listing (what is returned may be filtered)
- if ( count( $objects ) < $limit ) {
- $after = INF; // avoid a second RTT
- } else {
- $after = end( $objects ); // update last item
- $after = is_object( $after ) ? $after->name : $after;
- }
-
- return $files;
- }
-
- /**
- * Build a list of file objects, filtering out any directories
- * and extracting any stat info if provided in $objects (for CF_Objects)
- *
- * @param array $params Parameters for getDirectoryList()
- * @param string $dir Resolved container directory path
- * @param array $objects List of CF_Object items or object names
- * @return array List of (names,stat array or null) entries
- */
- private function buildFileObjectListing( array $params, $dir, array $objects ) {
- $names = [];
- foreach ( $objects as $object ) {
- if ( is_object( $object ) ) {
- if ( isset( $object->subdir ) || !isset( $object->name ) ) {
- continue; // virtual directory entry; ignore
- }
- $stat = [
- // Convert various random Swift dates to TS_MW
- 'mtime' => $this->convertSwiftDate( $object->last_modified, TS_MW ),
- 'size' => (int)$object->bytes,
- 'sha1' => null,
- // Note: manifiest ETags are not an MD5 of the file
- 'md5' => ctype_xdigit( $object->hash ) ? $object->hash : null,
- 'latest' => false // eventually consistent
- ];
- $names[] = [ $object->name, $stat ];
- } elseif ( substr( $object, -1 ) !== '/' ) {
- // Omit directories, which end in '/' in listings
- $names[] = [ $object, null ];
- }
- }
-
- return $names;
- }
-
- /**
- * Do not call this function outside of SwiftFileBackendFileList
- *
- * @param string $path Storage path
- * @param array $val Stat value
- */
- public function loadListingStatInternal( $path, array $val ) {
- $this->cheapCache->set( $path, 'stat', $val );
- }
-
- protected function doGetFileXAttributes( array $params ) {
- $stat = $this->getFileStat( $params );
- if ( $stat ) {
- if ( !isset( $stat['xattr'] ) ) {
- // Stat entries filled by file listings don't include metadata/headers
- $this->clearCache( [ $params['src'] ] );
- $stat = $this->getFileStat( $params );
- }
-
- return $stat['xattr'];
- } else {
- return false;
- }
- }
-
- protected function doGetFileSha1base36( array $params ) {
- $stat = $this->getFileStat( $params );
- if ( $stat ) {
- if ( !isset( $stat['sha1'] ) ) {
- // Stat entries filled by file listings don't include SHA1
- $this->clearCache( [ $params['src'] ] );
- $stat = $this->getFileStat( $params );
- }
-
- return $stat['sha1'];
- } else {
- return false;
- }
- }
-
- protected function doStreamFile( array $params ) {
- $status = Status::newGood();
-
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['src'] );
- }
-
- $auth = $this->getAuthentication();
- if ( !$auth || !is_array( $this->getContainerStat( $srcCont ) ) ) {
- $status->fatal( 'backend-fail-stream', $params['src'] );
-
- return $status;
- }
-
- $handle = fopen( 'php://output', 'wb' );
-
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
- 'method' => 'GET',
- 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
- 'headers' => $this->authTokenHeaders( $auth )
- + $this->headersFromParams( $params ),
- 'stream' => $handle,
- ] );
-
- if ( $rcode >= 200 && $rcode <= 299 ) {
- // good
- } elseif ( $rcode === 404 ) {
- $status->fatal( 'backend-fail-stream', $params['src'] );
- } else {
- $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
- }
-
- return $status;
- }
-
- protected function doGetLocalCopyMulti( array $params ) {
- $tmpFiles = [];
-
- $auth = $this->getAuthentication();
-
- $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
- // Blindly create tmp files and stream to them, catching any exception if the file does
- // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata().
- $reqs = []; // (path => op)
-
- foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
- if ( $srcRel === null || !$auth ) {
- $tmpFiles[$path] = null;
- continue;
- }
- // Get source file extension
- $ext = FileBackend::extensionFromPath( $path );
- // Create a new temporary file...
- $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
- if ( $tmpFile ) {
- $handle = fopen( $tmpFile->getPath(), 'wb' );
- if ( $handle ) {
- $reqs[$path] = [
- 'method' => 'GET',
- 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
- 'headers' => $this->authTokenHeaders( $auth )
- + $this->headersFromParams( $params ),
- 'stream' => $handle,
- ];
- } else {
- $tmpFile = null;
- }
- }
- $tmpFiles[$path] = $tmpFile;
- }
-
- $isLatest = ( $this->isRGW || !empty( $params['latest'] ) );
- $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
- $reqs = $this->http->runMulti( $reqs, $opts );
- foreach ( $reqs as $path => $op ) {
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
- fclose( $op['stream'] ); // close open handle
- if ( $rcode >= 200 && $rcode <= 299 ) {
- $size = $tmpFiles[$path] ? $tmpFiles[$path]->getSize() : 0;
- // Double check that the disk is not full/broken
- if ( $size != $rhdrs['content-length'] ) {
- $tmpFiles[$path] = null;
- $rerr = "Got {$size}/{$rhdrs['content-length']} bytes";
- $this->onError( null, __METHOD__,
- [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
- }
- // Set the file stat process cache in passing
- $stat = $this->getStatFromHeaders( $rhdrs );
- $stat['latest'] = $isLatest;
- $this->cheapCache->set( $path, 'stat', $stat );
- } elseif ( $rcode === 404 ) {
- $tmpFiles[$path] = false;
- } else {
- $tmpFiles[$path] = null;
- $this->onError( null, __METHOD__,
- [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
- }
- }
-
- return $tmpFiles;
- }
-
- public function getFileHttpUrl( array $params ) {
- if ( $this->swiftTempUrlKey != '' ||
- ( $this->rgwS3AccessKey != '' && $this->rgwS3SecretKey != '' )
- ) {
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
- if ( $srcRel === null ) {
- return null; // invalid path
- }
-
- $auth = $this->getAuthentication();
- if ( !$auth ) {
- return null;
- }
-
- $ttl = isset( $params['ttl'] ) ? $params['ttl'] : 86400;
- $expires = time() + $ttl;
-
- if ( $this->swiftTempUrlKey != '' ) {
- $url = $this->storageUrl( $auth, $srcCont, $srcRel );
- // Swift wants the signature based on the unencoded object name
- $contPath = parse_url( $this->storageUrl( $auth, $srcCont ), PHP_URL_PATH );
- $signature = hash_hmac( 'sha1',
- "GET\n{$expires}\n{$contPath}/{$srcRel}",
- $this->swiftTempUrlKey
- );
-
- return "{$url}?temp_url_sig={$signature}&temp_url_expires={$expires}";
- } else { // give S3 API URL for rgw
- // Path for signature starts with the bucket
- $spath = '/' . rawurlencode( $srcCont ) . '/' .
- str_replace( '%2F', '/', rawurlencode( $srcRel ) );
- // Calculate the hash
- $signature = base64_encode( hash_hmac(
- 'sha1',
- "GET\n\n\n{$expires}\n{$spath}",
- $this->rgwS3SecretKey,
- true // raw
- ) );
- // See http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
- // Note: adding a newline for empty CanonicalizedAmzHeaders does not work.
- return wfAppendQuery(
- str_replace( '/swift/v1', '', // S3 API is the rgw default
- $this->storageUrl( $auth ) . $spath ),
- [
- 'Signature' => $signature,
- 'Expires' => $expires,
- 'AWSAccessKeyId' => $this->rgwS3AccessKey ]
- );
- }
- }
-
- return null;
- }
-
- protected function directoriesAreVirtual() {
- return true;
- }
-
- /**
- * Get headers to send to Swift when reading a file based
- * on a FileBackend params array, e.g. that of getLocalCopy().
- * $params is currently only checked for a 'latest' flag.
- *
- * @param array $params
- * @return array
- */
- protected function headersFromParams( array $params ) {
- $hdrs = [];
- if ( !empty( $params['latest'] ) ) {
- $hdrs['x-newest'] = 'true';
- }
-
- return $hdrs;
- }
-
- /**
- * @param FileBackendStoreOpHandle[] $fileOpHandles
- *
- * @return Status[]
- */
- protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
- $statuses = [];
-
- $auth = $this->getAuthentication();
- if ( !$auth ) {
- foreach ( $fileOpHandles as $index => $fileOpHandle ) {
- $statuses[$index] = Status::newFatal( 'backend-fail-connect', $this->name );
- }
-
- return $statuses;
- }
-
- // Split the HTTP requests into stages that can be done concurrently
- $httpReqsByStage = []; // map of (stage => index => HTTP request)
- foreach ( $fileOpHandles as $index => $fileOpHandle ) {
- $reqs = $fileOpHandle->httpOp;
- // Convert the 'url' parameter to an actual URL using $auth
- foreach ( $reqs as $stage => &$req ) {
- list( $container, $relPath ) = $req['url'];
- $req['url'] = $this->storageUrl( $auth, $container, $relPath );
- $req['headers'] = isset( $req['headers'] ) ? $req['headers'] : [];
- $req['headers'] = $this->authTokenHeaders( $auth ) + $req['headers'];
- $httpReqsByStage[$stage][$index] = $req;
- }
- $statuses[$index] = Status::newGood();
- }
-
- // Run all requests for the first stage, then the next, and so on
- $reqCount = count( $httpReqsByStage );
- for ( $stage = 0; $stage < $reqCount; ++$stage ) {
- $httpReqs = $this->http->runMulti( $httpReqsByStage[$stage] );
- foreach ( $httpReqs as $index => $httpReq ) {
- // Run the callback for each request of this operation
- $callback = $fileOpHandles[$index]->callback;
- call_user_func_array( $callback, [ $httpReq, $statuses[$index] ] );
- // On failure, abort all remaining requests for this operation
- // (e.g. abort the DELETE request if the COPY request fails for a move)
- if ( !$statuses[$index]->isOK() ) {
- $stages = count( $fileOpHandles[$index]->httpOp );
- for ( $s = ( $stage + 1 ); $s < $stages; ++$s ) {
- unset( $httpReqsByStage[$s][$index] );
- }
- }
- }
- }
-
- return $statuses;
- }
-
- /**
- * Set read/write permissions for a Swift container.
- *
- * @see http://swift.openstack.org/misc.html#acls
- *
- * In general, we don't allow listings to end-users. It's not useful, isn't well-defined
- * (lists are truncated to 10000 item with no way to page), and is just a performance risk.
- *
- * @param string $container Resolved Swift container
- * @param array $readGrps List of the possible criteria for a request to have
- * access to read a container. Each item is one of the following formats:
- * - account:user : Grants access if the request is by the given user
- * - ".r:<regex>" : Grants access if the request is from a referrer host that
- * matches the expression and the request is not for a listing.
- * Setting this to '*' effectively makes a container public.
- * -".rlistings:<regex>" : Grants access if the request is from a referrer host that
- * matches the expression and the request is for a listing.
- * @param array $writeGrps A list of the possible criteria for a request to have
- * access to write to a container. Each item is of the following format:
- * - account:user : Grants access if the request is by the given user
- * @return Status
- */
- protected function setContainerAccess( $container, array $readGrps, array $writeGrps ) {
- $status = Status::newGood();
- $auth = $this->getAuthentication();
-
- if ( !$auth ) {
- $status->fatal( 'backend-fail-connect', $this->name );
-
- return $status;
- }
-
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
- 'method' => 'POST',
- 'url' => $this->storageUrl( $auth, $container ),
- 'headers' => $this->authTokenHeaders( $auth ) + [
- 'x-container-read' => implode( ',', $readGrps ),
- 'x-container-write' => implode( ',', $writeGrps )
- ]
- ] );
-
- if ( $rcode != 204 && $rcode !== 202 ) {
- $status->fatal( 'backend-fail-internal', $this->name );
- wfDebugLog( 'SwiftBackend', __METHOD__ . ': unexpected rcode value (' . $rcode . ')' );
- }
-
- return $status;
- }
-
- /**
- * Get a Swift container stat array, possibly from process cache.
- * Use $reCache if the file count or byte count is needed.
- *
- * @param string $container Container name
- * @param bool $bypassCache Bypass all caches and load from Swift
- * @return array|bool|null False on 404, null on failure
- */
- protected function getContainerStat( $container, $bypassCache = false ) {
- $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
- if ( $bypassCache ) { // purge cache
- $this->containerStatCache->clear( $container );
- } elseif ( !$this->containerStatCache->has( $container, 'stat' ) ) {
- $this->primeContainerCache( [ $container ] ); // check persistent cache
- }
- if ( !$this->containerStatCache->has( $container, 'stat' ) ) {
- $auth = $this->getAuthentication();
- if ( !$auth ) {
- return null;
- }
-
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
- 'method' => 'HEAD',
- 'url' => $this->storageUrl( $auth, $container ),
- 'headers' => $this->authTokenHeaders( $auth )
- ] );
-
- if ( $rcode === 204 ) {
- $stat = [
- 'count' => $rhdrs['x-container-object-count'],
- 'bytes' => $rhdrs['x-container-bytes-used']
- ];
- if ( $bypassCache ) {
- return $stat;
- } else {
- $this->containerStatCache->set( $container, 'stat', $stat ); // cache it
- $this->setContainerCache( $container, $stat ); // update persistent cache
- }
- } elseif ( $rcode === 404 ) {
- return false;
- } else {
- $this->onError( null, __METHOD__,
- [ 'cont' => $container ], $rerr, $rcode, $rdesc );
-
- return null;
- }
- }
-
- return $this->containerStatCache->get( $container, 'stat' );
- }
-
- /**
- * Create a Swift container
- *
- * @param string $container Container name
- * @param array $params
- * @return Status
- */
- protected function createContainer( $container, array $params ) {
- $status = Status::newGood();
-
- $auth = $this->getAuthentication();
- if ( !$auth ) {
- $status->fatal( 'backend-fail-connect', $this->name );
-
- return $status;
- }
-
- // @see SwiftFileBackend::setContainerAccess()
- if ( empty( $params['noAccess'] ) ) {
- $readGrps = [ '.r:*', $this->swiftUser ]; // public
- } else {
- $readGrps = [ $this->swiftUser ]; // private
- }
- $writeGrps = [ $this->swiftUser ]; // sanity
-
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
- 'method' => 'PUT',
- 'url' => $this->storageUrl( $auth, $container ),
- 'headers' => $this->authTokenHeaders( $auth ) + [
- 'x-container-read' => implode( ',', $readGrps ),
- 'x-container-write' => implode( ',', $writeGrps )
- ]
- ] );
-
- if ( $rcode === 201 ) { // new
- // good
- } elseif ( $rcode === 202 ) { // already there
- // this shouldn't really happen, but is OK
- } else {
- $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
- }
-
- return $status;
- }
-
- /**
- * Delete a Swift container
- *
- * @param string $container Container name
- * @param array $params
- * @return Status
- */
- protected function deleteContainer( $container, array $params ) {
- $status = Status::newGood();
-
- $auth = $this->getAuthentication();
- if ( !$auth ) {
- $status->fatal( 'backend-fail-connect', $this->name );
-
- return $status;
- }
-
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
- 'method' => 'DELETE',
- 'url' => $this->storageUrl( $auth, $container ),
- 'headers' => $this->authTokenHeaders( $auth )
- ] );
-
- if ( $rcode >= 200 && $rcode <= 299 ) { // deleted
- $this->containerStatCache->clear( $container ); // purge
- } elseif ( $rcode === 404 ) { // not there
- // this shouldn't really happen, but is OK
- } elseif ( $rcode === 409 ) { // not empty
- $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc ); // race?
- } else {
- $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
- }
-
- return $status;
- }
-
- /**
- * Get a list of objects under a container.
- * Either just the names or a list of stdClass objects with details can be returned.
- *
- * @param string $fullCont
- * @param string $type ('info' for a list of object detail maps, 'names' for names only)
- * @param int $limit
- * @param string|null $after
- * @param string|null $prefix
- * @param string|null $delim
- * @return Status With the list as value
- */
- private function objectListing(
- $fullCont, $type, $limit, $after = null, $prefix = null, $delim = null
- ) {
- $status = Status::newGood();
-
- $auth = $this->getAuthentication();
- if ( !$auth ) {
- $status->fatal( 'backend-fail-connect', $this->name );
-
- return $status;
- }
-
- $query = [ 'limit' => $limit ];
- if ( $type === 'info' ) {
- $query['format'] = 'json';
- }
- if ( $after !== null ) {
- $query['marker'] = $after;
- }
- if ( $prefix !== null ) {
- $query['prefix'] = $prefix;
- }
- if ( $delim !== null ) {
- $query['delimiter'] = $delim;
- }
-
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
- 'method' => 'GET',
- 'url' => $this->storageUrl( $auth, $fullCont ),
- 'query' => $query,
- 'headers' => $this->authTokenHeaders( $auth )
- ] );
-
- $params = [ 'cont' => $fullCont, 'prefix' => $prefix, 'delim' => $delim ];
- if ( $rcode === 200 ) { // good
- if ( $type === 'info' ) {
- $status->value = FormatJson::decode( trim( $rbody ) );
- } else {
- $status->value = explode( "\n", trim( $rbody ) );
- }
- } elseif ( $rcode === 204 ) {
- $status->value = []; // empty container
- } elseif ( $rcode === 404 ) {
- $status->value = []; // no container
- } else {
- $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
- }
-
- return $status;
- }
-
- protected function doPrimeContainerCache( array $containerInfo ) {
- foreach ( $containerInfo as $container => $info ) {
- $this->containerStatCache->set( $container, 'stat', $info );
- }
- }
-
- protected function doGetFileStatMulti( array $params ) {
- $stats = [];
-
- $auth = $this->getAuthentication();
-
- $reqs = [];
- foreach ( $params['srcs'] as $path ) {
- list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
- if ( $srcRel === null ) {
- $stats[$path] = false;
- continue; // invalid storage path
- } elseif ( !$auth ) {
- $stats[$path] = null;
- continue;
- }
-
- // (a) Check the container
- $cstat = $this->getContainerStat( $srcCont );
- if ( $cstat === false ) {
- $stats[$path] = false;
- continue; // ok, nothing to do
- } elseif ( !is_array( $cstat ) ) {
- $stats[$path] = null;
- continue;
- }
-
- $reqs[$path] = [
- 'method' => 'HEAD',
- 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
- 'headers' => $this->authTokenHeaders( $auth ) + $this->headersFromParams( $params )
- ];
- }
-
- $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
- $reqs = $this->http->runMulti( $reqs, $opts );
-
- foreach ( $params['srcs'] as $path ) {
- if ( array_key_exists( $path, $stats ) ) {
- continue; // some sort of failure above
- }
- // (b) Check the file
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $reqs[$path]['response'];
- if ( $rcode === 200 || $rcode === 204 ) {
- // Update the object if it is missing some headers
- $rhdrs = $this->addMissingMetadata( $rhdrs, $path );
- // Load the stat array from the headers
- $stat = $this->getStatFromHeaders( $rhdrs );
- if ( $this->isRGW ) {
- $stat['latest'] = true; // strong consistency
- }
- } elseif ( $rcode === 404 ) {
- $stat = false;
- } else {
- $stat = null;
- $this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc );
- }
- $stats[$path] = $stat;
- }
-
- return $stats;
- }
-
- /**
- * @param array $rhdrs
- * @return array
- */
- protected function getStatFromHeaders( array $rhdrs ) {
- // Fetch all of the custom metadata headers
- $metadata = $this->getMetadata( $rhdrs );
- // Fetch all of the custom raw HTTP headers
- $headers = $this->sanitizeHdrs( [ 'headers' => $rhdrs ] );
-
- return [
- // Convert various random Swift dates to TS_MW
- 'mtime' => $this->convertSwiftDate( $rhdrs['last-modified'], TS_MW ),
- // Empty objects actually return no content-length header in Ceph
- 'size' => isset( $rhdrs['content-length'] ) ? (int)$rhdrs['content-length'] : 0,
- 'sha1' => isset( $metadata['sha1base36'] ) ? $metadata['sha1base36'] : null,
- // Note: manifiest ETags are not an MD5 of the file
- 'md5' => ctype_xdigit( $rhdrs['etag'] ) ? $rhdrs['etag'] : null,
- 'xattr' => [ 'metadata' => $metadata, 'headers' => $headers ]
- ];
- }
-
- /**
- * @return array|null Credential map
- */
- protected function getAuthentication() {
- if ( $this->authErrorTimestamp !== null ) {
- if ( ( time() - $this->authErrorTimestamp ) < 60 ) {
- return null; // failed last attempt; don't bother
- } else { // actually retry this time
- $this->authErrorTimestamp = null;
- }
- }
- // Session keys expire after a while, so we renew them periodically
- $reAuth = ( ( time() - $this->authSessionTimestamp ) > $this->authTTL );
- // Authenticate with proxy and get a session key...
- if ( !$this->authCreds || $reAuth ) {
- $this->authSessionTimestamp = 0;
- $cacheKey = $this->getCredsCacheKey( $this->swiftUser );
- $creds = $this->srvCache->get( $cacheKey ); // credentials
- // Try to use the credential cache
- if ( isset( $creds['auth_token'] ) && isset( $creds['storage_url'] ) ) {
- $this->authCreds = $creds;
- // Skew the timestamp for worst case to avoid using stale credentials
- $this->authSessionTimestamp = time() - ceil( $this->authTTL / 2 );
- } else { // cache miss
- list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
- 'method' => 'GET',
- 'url' => "{$this->swiftAuthUrl}/v1.0",
- 'headers' => [
- 'x-auth-user' => $this->swiftUser,
- 'x-auth-key' => $this->swiftKey
- ]
- ] );
-
- if ( $rcode >= 200 && $rcode <= 299 ) { // OK
- $this->authCreds = [
- 'auth_token' => $rhdrs['x-auth-token'],
- 'storage_url' => $rhdrs['x-storage-url']
- ];
- $this->srvCache->set( $cacheKey, $this->authCreds, ceil( $this->authTTL / 2 ) );
- $this->authSessionTimestamp = time();
- } elseif ( $rcode === 401 ) {
- $this->onError( null, __METHOD__, [], "Authentication failed.", $rcode );
- $this->authErrorTimestamp = time();
-
- return null;
- } else {
- $this->onError( null, __METHOD__, [], "HTTP return code: $rcode", $rcode );
- $this->authErrorTimestamp = time();
-
- return null;
- }
- }
- // Ceph RGW does not use <account> in URLs (OpenStack Swift uses "/v1/<account>")
- if ( substr( $this->authCreds['storage_url'], -3 ) === '/v1' ) {
- $this->isRGW = true; // take advantage of strong consistency in Ceph
- }
- }
-
- return $this->authCreds;
- }
-
- /**
- * @param array $creds From getAuthentication()
- * @param string $container
- * @param string $object
- * @return array
- */
- protected function storageUrl( array $creds, $container = null, $object = null ) {
- $parts = [ $creds['storage_url'] ];
- if ( strlen( $container ) ) {
- $parts[] = rawurlencode( $container );
- }
- if ( strlen( $object ) ) {
- $parts[] = str_replace( "%2F", "/", rawurlencode( $object ) );
- }
-
- return implode( '/', $parts );
- }
-
- /**
- * @param array $creds From getAuthentication()
- * @return array
- */
- protected function authTokenHeaders( array $creds ) {
- return [ 'x-auth-token' => $creds['auth_token'] ];
- }
-
- /**
- * Get the cache key for a container
- *
- * @param string $username
- * @return string
- */
- private function getCredsCacheKey( $username ) {
- return 'swiftcredentials:' . md5( $username . ':' . $this->swiftAuthUrl );
- }
-
- /**
- * Log an unexpected exception for this backend.
- * This also sets the Status object to have a fatal error.
- *
- * @param Status|null $status
- * @param string $func
- * @param array $params
- * @param string $err Error string
- * @param int $code HTTP status
- * @param string $desc HTTP status description
- */
- public function onError( $status, $func, array $params, $err = '', $code = 0, $desc = '' ) {
- if ( $status instanceof Status ) {
- $status->fatal( 'backend-fail-internal', $this->name );
- }
- if ( $code == 401 ) { // possibly a stale token
- $this->srvCache->delete( $this->getCredsCacheKey( $this->swiftUser ) );
- }
- wfDebugLog( 'SwiftBackend',
- "HTTP $code ($desc) in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
- ( $err ? ": $err" : "" )
- );
- }
-}
-
-/**
- * @see FileBackendStoreOpHandle
- */
-class SwiftFileOpHandle extends FileBackendStoreOpHandle {
- /** @var array List of Requests for MultiHttpClient */
- public $httpOp;
- /** @var Closure */
- public $callback;
-
- /**
- * @param SwiftFileBackend $backend
- * @param Closure $callback Function that takes (HTTP request array, status)
- * @param array $httpOp MultiHttpClient op
- */
- public function __construct( SwiftFileBackend $backend, Closure $callback, array $httpOp ) {
- $this->backend = $backend;
- $this->callback = $callback;
- $this->httpOp = $httpOp;
- }
-}
-
-/**
- * SwiftFileBackend helper class to page through listings.
- * Swift also has a listing limit of 10,000 objects for sanity.
- * Do not use this class from places outside SwiftFileBackend.
- *
- * @ingroup FileBackend
- */
-abstract class SwiftFileBackendList implements Iterator {
- /** @var array List of path or (path,stat array) entries */
- protected $bufferIter = [];
-
- /** @var string List items *after* this path */
- protected $bufferAfter = null;
-
- /** @var int */
- protected $pos = 0;
-
- /** @var array */
- protected $params = [];
-
- /** @var SwiftFileBackend */
- protected $backend;
-
- /** @var string Container name */
- protected $container;
-
- /** @var string Storage directory */
- protected $dir;
-
- /** @var int */
- protected $suffixStart;
-
- const PAGE_SIZE = 9000; // file listing buffer size
-
- /**
- * @param SwiftFileBackend $backend
- * @param string $fullCont Resolved container name
- * @param string $dir Resolved directory relative to container
- * @param array $params
- */
- public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
- $this->backend = $backend;
- $this->container = $fullCont;
- $this->dir = $dir;
- if ( substr( $this->dir, -1 ) === '/' ) {
- $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
- }
- if ( $this->dir == '' ) { // whole container
- $this->suffixStart = 0;
- } else { // dir within container
- $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
- }
- $this->params = $params;
- }
-
- /**
- * @see Iterator::key()
- * @return int
- */
- public function key() {
- return $this->pos;
- }
-
- /**
- * @see Iterator::next()
- */
- public function next() {
- // Advance to the next file in the page
- next( $this->bufferIter );
- ++$this->pos;
- // Check if there are no files left in this page and
- // advance to the next page if this page was not empty.
- if ( !$this->valid() && count( $this->bufferIter ) ) {
- $this->bufferIter = $this->pageFromList(
- $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
- ); // updates $this->bufferAfter
- }
- }
-
- /**
- * @see Iterator::rewind()
- */
- public function rewind() {
- $this->pos = 0;
- $this->bufferAfter = null;
- $this->bufferIter = $this->pageFromList(
- $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
- ); // updates $this->bufferAfter
- }
-
- /**
- * @see Iterator::valid()
- * @return bool
- */
- public function valid() {
- if ( $this->bufferIter === null ) {
- return false; // some failure?
- } else {
- return ( current( $this->bufferIter ) !== false ); // no paths can have this value
- }
- }
-
- /**
- * Get the given list portion (page)
- *
- * @param string $container Resolved container name
- * @param string $dir Resolved path relative to container
- * @param string $after
- * @param int $limit
- * @param array $params
- * @return Traversable|array
- */
- abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
-}
-
-/**
- * Iterator for listing directories
- */
-class SwiftFileBackendDirList extends SwiftFileBackendList {
- /**
- * @see Iterator::current()
- * @return string|bool String (relative path) or false
- */
- public function current() {
- return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
- }
-
- protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
- return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
- }
-}
-
-/**
- * Iterator for listing regular files
- */
-class SwiftFileBackendFileList extends SwiftFileBackendList {
- /**
- * @see Iterator::current()
- * @return string|bool String (relative path) or false
- */
- public function current() {
- list( $path, $stat ) = current( $this->bufferIter );
- $relPath = substr( $path, $this->suffixStart );
- if ( is_array( $stat ) ) {
- $storageDir = rtrim( $this->params['dir'], '/' );
- $this->backend->loadListingStatInternal( "$storageDir/$relPath", $stat );
- }
-
- return $relPath;
- }
-
- protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
- return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
- }
-}
diff --git a/www/wiki/includes/filebackend/TempFSFile.php b/www/wiki/includes/filebackend/TempFSFile.php
deleted file mode 100644
index f5728408..00000000
--- a/www/wiki/includes/filebackend/TempFSFile.php
+++ /dev/null
@@ -1,157 +0,0 @@
-<?php
-/**
- * Location holder of files stored temporarily
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileBackend
- */
-
-/**
- * This class is used to hold the location and do limited manipulation
- * of files stored temporarily (this will be whatever wfTempDir() returns)
- *
- * @ingroup FileBackend
- */
-class TempFSFile extends FSFile {
- /** @var bool Garbage collect the temp file */
- protected $canDelete = false;
-
- /** @var array Map of (path => 1) for paths to delete on shutdown */
- protected static $pathsCollect = null;
-
- public function __construct( $path ) {
- parent::__construct( $path );
-
- if ( self::$pathsCollect === null ) {
- self::$pathsCollect = [];
- register_shutdown_function( [ __CLASS__, 'purgeAllOnShutdown' ] );
- }
- }
-
- /**
- * Make a new temporary file on the file system.
- * Temporary files may be purged when the file object falls out of scope.
- *
- * @param string $prefix
- * @param string $extension
- * @return TempFSFile|null
- */
- public static function factory( $prefix, $extension = '' ) {
- $ext = ( $extension != '' ) ? ".{$extension}" : '';
-
- $attempts = 5;
- while ( $attempts-- ) {
- $path = wfTempDir() . '/' . $prefix . wfRandomString( 12 ) . $ext;
- MediaWiki\suppressWarnings();
- $newFileHandle = fopen( $path, 'x' );
- MediaWiki\restoreWarnings();
- if ( $newFileHandle ) {
- fclose( $newFileHandle );
- $tmpFile = new self( $path );
- $tmpFile->autocollect();
- // Safely instantiated, end loop.
- return $tmpFile;
- }
- }
-
- // Give up
- return null;
- }
-
- /**
- * Purge this file off the file system
- *
- * @return bool Success
- */
- public function purge() {
- $this->canDelete = false; // done
- MediaWiki\suppressWarnings();
- $ok = unlink( $this->path );
- MediaWiki\restoreWarnings();
-
- unset( self::$pathsCollect[$this->path] );
-
- return $ok;
- }
-
- /**
- * Clean up the temporary file only after an object goes out of scope
- *
- * @param object $object
- * @return TempFSFile This object
- */
- public function bind( $object ) {
- if ( is_object( $object ) ) {
- if ( !isset( $object->tempFSFileReferences ) ) {
- // Init first since $object might use __get() and return only a copy variable
- $object->tempFSFileReferences = [];
- }
- $object->tempFSFileReferences[] = $this;
- }
-
- return $this;
- }
-
- /**
- * Set flag to not clean up after the temporary file
- *
- * @return TempFSFile This object
- */
- public function preserve() {
- $this->canDelete = false;
-
- unset( self::$pathsCollect[$this->path] );
-
- return $this;
- }
-
- /**
- * Set flag clean up after the temporary file
- *
- * @return TempFSFile This object
- */
- public function autocollect() {
- $this->canDelete = true;
-
- self::$pathsCollect[$this->path] = 1;
-
- return $this;
- }
-
- /**
- * Try to make sure that all files are purged on error
- *
- * This method should only be called internally
- */
- public static function purgeAllOnShutdown() {
- foreach ( self::$pathsCollect as $path ) {
- MediaWiki\suppressWarnings();
- unlink( $path );
- MediaWiki\restoreWarnings();
- }
- }
-
- /**
- * Cleans up after the temporary file by deleting it
- */
- function __destruct() {
- if ( $this->canDelete ) {
- $this->purge();
- }
- }
-}
diff --git a/www/wiki/includes/filebackend/filejournal/DBFileJournal.php b/www/wiki/includes/filebackend/filejournal/DBFileJournal.php
index 4269f91e..3dc9f18e 100644
--- a/www/wiki/includes/filebackend/filejournal/DBFileJournal.php
+++ b/www/wiki/includes/filebackend/filejournal/DBFileJournal.php
@@ -124,9 +124,9 @@ class DBFileJournal extends FileJournal {
/**
* @see FileJournal::doGetChangeEntries()
- * @param int $start
+ * @param int|null $start
* @param int $limit
- * @return array
+ * @return array[]
*/
protected function doGetChangeEntries( $start, $limit ) {
$dbw = $this->getMasterDB();
diff --git a/www/wiki/includes/filebackend/filejournal/FileJournal.php b/www/wiki/includes/filebackend/filejournal/FileJournal.php
deleted file mode 100644
index b84e1959..00000000
--- a/www/wiki/includes/filebackend/filejournal/FileJournal.php
+++ /dev/null
@@ -1,251 +0,0 @@
-<?php
-/**
- * @defgroup FileJournal File journal
- * @ingroup FileBackend
- */
-
-/**
- * File operation journaling.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileJournal
- * @author Aaron Schulz
- */
-
-/**
- * @brief Class for handling file operation journaling.
- *
- * Subclasses should avoid throwing exceptions at all costs.
- *
- * @ingroup FileJournal
- * @since 1.20
- */
-abstract class FileJournal {
- /** @var string */
- protected $backend;
-
- /** @var int */
- protected $ttlDays;
-
- /**
- * Construct a new instance from configuration.
- *
- * @param array $config Includes:
- * 'ttlDays' : days to keep log entries around (false means "forever")
- */
- protected function __construct( array $config ) {
- $this->ttlDays = isset( $config['ttlDays'] ) ? $config['ttlDays'] : false;
- }
-
- /**
- * Create an appropriate FileJournal object from config
- *
- * @param array $config
- * @param string $backend A registered file backend name
- * @throws Exception
- * @return FileJournal
- */
- final public static function factory( array $config, $backend ) {
- $class = $config['class'];
- $jrn = new $class( $config );
- if ( !$jrn instanceof self ) {
- throw new Exception( "Class given is not an instance of FileJournal." );
- }
- $jrn->backend = $backend;
-
- return $jrn;
- }
-
- /**
- * Get a statistically unique ID string
- *
- * @return string <9 char TS_MW timestamp in base 36><22 random base 36 chars>
- */
- final public function getTimestampedUUID() {
- $s = '';
- for ( $i = 0; $i < 5; $i++ ) {
- $s .= mt_rand( 0, 2147483647 );
- }
- $s = Wikimedia\base_convert( sha1( $s ), 16, 36, 31 );
-
- return substr( Wikimedia\base_convert( wfTimestamp( TS_MW ), 10, 36, 9 ) . $s, 0, 31 );
- }
-
- /**
- * Log changes made by a batch file operation.
- *
- * @param array $entries List of file operations (each an array of parameters) which contain:
- * op : Basic operation name (create, update, delete)
- * path : The storage path of the file
- * newSha1 : The final base 36 SHA-1 of the file
- * Note that 'false' should be used as the SHA-1 for non-existing files.
- * @param string $batchId UUID string that identifies the operation batch
- * @return Status
- */
- final public function logChangeBatch( array $entries, $batchId ) {
- if ( !count( $entries ) ) {
- return Status::newGood();
- }
-
- return $this->doLogChangeBatch( $entries, $batchId );
- }
-
- /**
- * @see FileJournal::logChangeBatch()
- *
- * @param array $entries List of file operations (each an array of parameters)
- * @param string $batchId UUID string that identifies the operation batch
- * @return Status
- */
- abstract protected function doLogChangeBatch( array $entries, $batchId );
-
- /**
- * Get the position ID of the latest journal entry
- *
- * @return int|bool
- */
- final public function getCurrentPosition() {
- return $this->doGetCurrentPosition();
- }
-
- /**
- * @see FileJournal::getCurrentPosition()
- * @return int|bool
- */
- abstract protected function doGetCurrentPosition();
-
- /**
- * Get the position ID of the latest journal entry at some point in time
- *
- * @param int|string $time Timestamp
- * @return int|bool
- */
- final public function getPositionAtTime( $time ) {
- return $this->doGetPositionAtTime( $time );
- }
-
- /**
- * @see FileJournal::getPositionAtTime()
- * @param int|string $time Timestamp
- * @return int|bool
- */
- abstract protected function doGetPositionAtTime( $time );
-
- /**
- * Get an array of file change log entries.
- * A starting change ID and/or limit can be specified.
- *
- * @param int $start Starting change ID or null
- * @param int $limit Maximum number of items to return
- * @param string &$next Updated to the ID of the next entry.
- * @return array List of associative arrays, each having:
- * id : unique, monotonic, ID for this change
- * batch_uuid : UUID for an operation batch
- * backend : the backend name
- * op : primitive operation (create,update,delete,null)
- * path : affected storage path
- * new_sha1 : base 36 sha1 of the new file had the operation succeeded
- * timestamp : TS_MW timestamp of the batch change
- * Also, $next is updated to the ID of the next entry.
- */
- final public function getChangeEntries( $start = null, $limit = 0, &$next = null ) {
- $entries = $this->doGetChangeEntries( $start, $limit ? $limit + 1 : 0 );
- if ( $limit && count( $entries ) > $limit ) {
- $last = array_pop( $entries ); // remove the extra entry
- $next = $last['id']; // update for next call
- } else {
- $next = null; // end of list
- }
-
- return $entries;
- }
-
- /**
- * @see FileJournal::getChangeEntries()
- * @param int $start
- * @param int $limit
- * @return array
- */
- abstract protected function doGetChangeEntries( $start, $limit );
-
- /**
- * Purge any old log entries
- *
- * @return Status
- */
- final public function purgeOldLogs() {
- return $this->doPurgeOldLogs();
- }
-
- /**
- * @see FileJournal::purgeOldLogs()
- * @return Status
- */
- abstract protected function doPurgeOldLogs();
-}
-
-/**
- * Simple version of FileJournal that does nothing
- * @since 1.20
- */
-class NullFileJournal extends FileJournal {
- /**
- * @see FileJournal::doLogChangeBatch()
- * @param array $entries
- * @param string $batchId
- * @return Status
- */
- protected function doLogChangeBatch( array $entries, $batchId ) {
- return Status::newGood();
- }
-
- /**
- * @see FileJournal::doGetCurrentPosition()
- * @return int|bool
- */
- protected function doGetCurrentPosition() {
- return false;
- }
-
- /**
- * @see FileJournal::doGetPositionAtTime()
- * @param int|string $time Timestamp
- * @return int|bool
- */
- protected function doGetPositionAtTime( $time ) {
- return false;
- }
-
- /**
- * @see FileJournal::doGetChangeEntries()
- * @param int $start
- * @param int $limit
- * @return array
- */
- protected function doGetChangeEntries( $start, $limit ) {
- return [];
- }
-
- /**
- * @see FileJournal::doPurgeOldLogs()
- * @return Status
- */
- protected function doPurgeOldLogs() {
- return Status::newGood();
- }
-}
diff --git a/www/wiki/includes/filebackend/lockmanager/DBLockManager.php b/www/wiki/includes/filebackend/lockmanager/DBLockManager.php
deleted file mode 100644
index f4410cad..00000000
--- a/www/wiki/includes/filebackend/lockmanager/DBLockManager.php
+++ /dev/null
@@ -1,433 +0,0 @@
-<?php
-/**
- * Version of LockManager based on using DB table locks.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup LockManager
- */
-
-/**
- * Version of LockManager based on using named/row DB locks.
- *
- * This is meant for multi-wiki systems that may share files.
- *
- * All lock requests for a resource, identified by a hash string, will map
- * to one bucket. Each bucket maps to one or several peer DBs, each on their
- * own server, all having the filelocks.sql tables (with row-level locking).
- * A majority of peer DBs must agree for a lock to be acquired.
- *
- * Caching is used to avoid hitting servers that are down.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-abstract class DBLockManager extends QuorumLockManager {
- /** @var array Map of DB names to server config */
- protected $dbServers; // (DB name => server config array)
- /** @var BagOStuff */
- protected $statusCache;
-
- protected $lockExpiry; // integer number of seconds
- protected $safeDelay; // integer number of seconds
-
- protected $session = 0; // random integer
- /** @var array Map Database connections (DB name => Database) */
- protected $conns = [];
-
- /**
- * Construct a new instance from configuration.
- *
- * @param array $config Parameters include:
- * - dbServers : Associative array of DB names to server configuration.
- * Configuration is an associative array that includes:
- * - host : DB server name
- * - dbname : DB name
- * - type : DB type (mysql,postgres,...)
- * - user : DB user
- * - password : DB user password
- * - tablePrefix : DB table prefix
- * - flags : DB flags (see DatabaseBase)
- * - dbsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
- * each having an odd-numbered list of DB names (peers) as values.
- * Any DB named 'localDBMaster' will automatically use the DB master
- * settings for this wiki (without the need for a dbServers entry).
- * Only use 'localDBMaster' if the domain is a valid wiki ID.
- * - lockExpiry : Lock timeout (seconds) for dropped connections. [optional]
- * This tells the DB server how long to wait before assuming
- * connection failure and releasing all the locks for a session.
- */
- public function __construct( array $config ) {
- parent::__construct( $config );
-
- $this->dbServers = isset( $config['dbServers'] )
- ? $config['dbServers']
- : []; // likely just using 'localDBMaster'
- // Sanitize srvsByBucket config to prevent PHP errors
- $this->srvsByBucket = array_filter( $config['dbsByBucket'], 'is_array' );
- $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
-
- if ( isset( $config['lockExpiry'] ) ) {
- $this->lockExpiry = $config['lockExpiry'];
- } else {
- $met = ini_get( 'max_execution_time' );
- $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0
- }
- $this->safeDelay = ( $this->lockExpiry <= 0 )
- ? 60 // pick a safe-ish number to match DB timeout default
- : $this->lockExpiry; // cover worst case
-
- foreach ( $this->srvsByBucket as $bucket ) {
- if ( count( $bucket ) > 1 ) { // multiple peers
- // Tracks peers that couldn't be queried recently to avoid lengthy
- // connection timeouts. This is useless if each bucket has one peer.
- $this->statusCache = ObjectCache::getLocalServerInstance();
- break;
- }
- }
-
- $this->session = wfRandomString( 31 );
- }
-
- // @todo change this code to work in one batch
- protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
- $status = Status::newGood();
- foreach ( $pathsByType as $type => $paths ) {
- $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
- }
-
- return $status;
- }
-
- protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
- return Status::newGood();
- }
-
- /**
- * @see QuorumLockManager::isServerUp()
- * @param string $lockSrv
- * @return bool
- */
- protected function isServerUp( $lockSrv ) {
- if ( !$this->cacheCheckFailures( $lockSrv ) ) {
- return false; // recent failure to connect
- }
- try {
- $this->getConnection( $lockSrv );
- } catch ( DBError $e ) {
- $this->cacheRecordFailure( $lockSrv );
-
- return false; // failed to connect
- }
-
- return true;
- }
-
- /**
- * Get (or reuse) a connection to a lock DB
- *
- * @param string $lockDb
- * @return IDatabase
- * @throws DBError
- */
- protected function getConnection( $lockDb ) {
- if ( !isset( $this->conns[$lockDb] ) ) {
- $db = null;
- if ( $lockDb === 'localDBMaster' ) {
- $lb = wfGetLBFactory()->getMainLB( $this->domain );
- $db = $lb->getConnection( DB_MASTER, [], $this->domain );
- } elseif ( isset( $this->dbServers[$lockDb] ) ) {
- $config = $this->dbServers[$lockDb];
- $db = DatabaseBase::factory( $config['type'], $config );
- }
- if ( !$db ) {
- return null; // config error?
- }
- $this->conns[$lockDb] = $db;
- $this->conns[$lockDb]->clearFlag( DBO_TRX );
- # If the connection drops, try to avoid letting the DB rollback
- # and release the locks before the file operations are finished.
- # This won't handle the case of DB server restarts however.
- $options = [];
- if ( $this->lockExpiry > 0 ) {
- $options['connTimeout'] = $this->lockExpiry;
- }
- $this->conns[$lockDb]->setSessionOptions( $options );
- $this->initConnection( $lockDb, $this->conns[$lockDb] );
- }
- if ( !$this->conns[$lockDb]->trxLevel() ) {
- $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction
- }
-
- return $this->conns[$lockDb];
- }
-
- /**
- * Do additional initialization for new lock DB connection
- *
- * @param string $lockDb
- * @param IDatabase $db
- * @throws DBError
- */
- protected function initConnection( $lockDb, IDatabase $db ) {
- }
-
- /**
- * Checks if the DB has not recently had connection/query errors.
- * This just avoids wasting time on doomed connection attempts.
- *
- * @param string $lockDb
- * @return bool
- */
- protected function cacheCheckFailures( $lockDb ) {
- return ( $this->statusCache && $this->safeDelay > 0 )
- ? !$this->statusCache->get( $this->getMissKey( $lockDb ) )
- : true;
- }
-
- /**
- * Log a lock request failure to the cache
- *
- * @param string $lockDb
- * @return bool Success
- */
- protected function cacheRecordFailure( $lockDb ) {
- return ( $this->statusCache && $this->safeDelay > 0 )
- ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay )
- : true;
- }
-
- /**
- * Get a cache key for recent query misses for a DB
- *
- * @param string $lockDb
- * @return string
- */
- protected function getMissKey( $lockDb ) {
- $lockDb = ( $lockDb === 'localDBMaster' ) ? wfWikiID() : $lockDb; // non-relative
- return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb );
- }
-
- /**
- * Make sure remaining locks get cleared for sanity
- */
- function __destruct() {
- $this->releaseAllLocks();
- foreach ( $this->conns as $db ) {
- $db->close();
- }
- }
-}
-
-/**
- * MySQL version of DBLockManager that supports shared locks.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * @ingroup LockManager
- */
-class MySqlLockManager extends DBLockManager {
- /** @var array Mapping of lock types to the type actually used */
- protected $lockTypeMap = [
- self::LOCK_SH => self::LOCK_SH,
- self::LOCK_UW => self::LOCK_SH,
- self::LOCK_EX => self::LOCK_EX
- ];
-
- /**
- * @param string $lockDb
- * @param IDatabase $db
- */
- protected function initConnection( $lockDb, IDatabase $db ) {
- # Let this transaction see lock rows from other transactions
- $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
- }
-
- /**
- * Get a connection to a lock DB and acquire locks on $paths.
- * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118.
- *
- * @see DBLockManager::getLocksOnServer()
- * @param string $lockSrv
- * @param array $paths
- * @param string $type
- * @return Status
- */
- protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
- $status = Status::newGood();
-
- $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
-
- $keys = []; // list of hash keys for the paths
- $data = []; // list of rows to insert
- $checkEXKeys = []; // list of hash keys that this has no EX lock on
- # Build up values for INSERT clause
- foreach ( $paths as $path ) {
- $key = $this->sha1Base36Absolute( $path );
- $keys[] = $key;
- $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ];
- if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
- $checkEXKeys[] = $key;
- }
- }
-
- # Block new writers (both EX and SH locks leave entries here)...
- $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] );
- # Actually do the locking queries...
- if ( $type == self::LOCK_SH ) { // reader locks
- $blocked = false;
- # Bail if there are any existing writers...
- if ( count( $checkEXKeys ) ) {
- $blocked = $db->selectField( 'filelocks_exclusive', '1',
- [ 'fle_key' => $checkEXKeys ],
- __METHOD__
- );
- }
- # Other prospective writers that haven't yet updated filelocks_exclusive
- # will recheck filelocks_shared after doing so and bail due to this entry.
- } else { // writer locks
- $encSession = $db->addQuotes( $this->session );
- # Bail if there are any existing writers...
- # This may detect readers, but the safe check for them is below.
- # Note: if two writers come at the same time, both bail :)
- $blocked = $db->selectField( 'filelocks_shared', '1',
- [ 'fls_key' => $keys, "fls_session != $encSession" ],
- __METHOD__
- );
- if ( !$blocked ) {
- # Build up values for INSERT clause
- $data = [];
- foreach ( $keys as $key ) {
- $data[] = [ 'fle_key' => $key ];
- }
- # Block new readers/writers...
- $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
- # Bail if there are any existing readers...
- $blocked = $db->selectField( 'filelocks_shared', '1',
- [ 'fls_key' => $keys, "fls_session != $encSession" ],
- __METHOD__
- );
- }
- }
-
- if ( $blocked ) {
- foreach ( $paths as $path ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- }
-
- return $status;
- }
-
- /**
- * @see QuorumLockManager::releaseAllLocks()
- * @return Status
- */
- protected function releaseAllLocks() {
- $status = Status::newGood();
-
- foreach ( $this->conns as $lockDb => $db ) {
- if ( $db->trxLevel() ) { // in transaction
- try {
- $db->rollback( __METHOD__ ); // finish transaction and kill any rows
- } catch ( DBError $e ) {
- $status->fatal( 'lockmanager-fail-db-release', $lockDb );
- }
- }
- }
-
- return $status;
- }
-}
-
-/**
- * PostgreSQL version of DBLockManager that supports shared locks.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * @ingroup LockManager
- */
-class PostgreSqlLockManager extends DBLockManager {
- /** @var array Mapping of lock types to the type actually used */
- protected $lockTypeMap = [
- self::LOCK_SH => self::LOCK_SH,
- self::LOCK_UW => self::LOCK_SH,
- self::LOCK_EX => self::LOCK_EX
- ];
-
- protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
- $status = Status::newGood();
- if ( !count( $paths ) ) {
- return $status; // nothing to lock
- }
-
- $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
- $bigints = array_unique( array_map(
- function ( $key ) {
- return Wikimedia\base_convert( substr( $key, 0, 15 ), 16, 10 );
- },
- array_map( [ $this, 'sha1Base16Absolute' ], $paths )
- ) );
-
- // Try to acquire all the locks...
- $fields = [];
- foreach ( $bigints as $bigint ) {
- $fields[] = ( $type == self::LOCK_SH )
- ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint"
- : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint";
- }
- $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
- $row = $res->fetchRow();
-
- if ( in_array( 'f', $row ) ) {
- // Release any acquired locks if some could not be acquired...
- $fields = [];
- foreach ( $row as $kbigint => $ok ) {
- if ( $ok === 't' ) { // locked
- $bigint = substr( $kbigint, 1 ); // strip off the "K"
- $fields[] = ( $type == self::LOCK_SH )
- ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})"
- : "pg_advisory_unlock({$db->addQuotes( $bigint )})";
- }
- }
- if ( count( $fields ) ) {
- $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
- }
- foreach ( $paths as $path ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- }
-
- return $status;
- }
-
- /**
- * @see QuorumLockManager::releaseAllLocks()
- * @return Status
- */
- protected function releaseAllLocks() {
- $status = Status::newGood();
-
- foreach ( $this->conns as $lockDb => $db ) {
- try {
- $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ );
- } catch ( DBError $e ) {
- $status->fatal( 'lockmanager-fail-db-release', $lockDb );
- }
- }
-
- return $status;
- }
-}
diff --git a/www/wiki/includes/filebackend/lockmanager/FSLockManager.php b/www/wiki/includes/filebackend/lockmanager/FSLockManager.php
deleted file mode 100644
index 2b660ec7..00000000
--- a/www/wiki/includes/filebackend/lockmanager/FSLockManager.php
+++ /dev/null
@@ -1,248 +0,0 @@
-<?php
-/**
- * Simple version of LockManager based on using FS lock files.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup LockManager
- */
-
-/**
- * Simple version of LockManager based on using FS lock files.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * This should work fine for small sites running off one server.
- * Do not use this with 'lockDirectory' set to an NFS mount unless the
- * NFS client is at least version 2.6.12. Otherwise, the BSD flock()
- * locks will be ignored; see http://nfs.sourceforge.net/#section_d.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-class FSLockManager extends LockManager {
- /** @var array Mapping of lock types to the type actually used */
- protected $lockTypeMap = [
- self::LOCK_SH => self::LOCK_SH,
- self::LOCK_UW => self::LOCK_SH,
- self::LOCK_EX => self::LOCK_EX
- ];
-
- protected $lockDir; // global dir for all servers
-
- /** @var array Map of (locked key => lock file handle) */
- protected $handles = [];
-
- /**
- * Construct a new instance from configuration.
- *
- * @param array $config Includes:
- * - lockDirectory : Directory containing the lock files
- */
- function __construct( array $config ) {
- parent::__construct( $config );
-
- $this->lockDir = $config['lockDirectory'];
- }
-
- /**
- * @see LockManager::doLock()
- * @param array $paths
- * @param int $type
- * @return Status
- */
- protected function doLock( array $paths, $type ) {
- $status = Status::newGood();
-
- $lockedPaths = []; // files locked in this attempt
- foreach ( $paths as $path ) {
- $status->merge( $this->doSingleLock( $path, $type ) );
- if ( $status->isOK() ) {
- $lockedPaths[] = $path;
- } else {
- // Abort and unlock everything
- $status->merge( $this->doUnlock( $lockedPaths, $type ) );
-
- return $status;
- }
- }
-
- return $status;
- }
-
- /**
- * @see LockManager::doUnlock()
- * @param array $paths
- * @param int $type
- * @return Status
- */
- protected function doUnlock( array $paths, $type ) {
- $status = Status::newGood();
-
- foreach ( $paths as $path ) {
- $status->merge( $this->doSingleUnlock( $path, $type ) );
- }
-
- return $status;
- }
-
- /**
- * Lock a single resource key
- *
- * @param string $path
- * @param int $type
- * @return Status
- */
- protected function doSingleLock( $path, $type ) {
- $status = Status::newGood();
-
- if ( isset( $this->locksHeld[$path][$type] ) ) {
- ++$this->locksHeld[$path][$type];
- } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
- $this->locksHeld[$path][$type] = 1;
- } else {
- if ( isset( $this->handles[$path] ) ) {
- $handle = $this->handles[$path];
- } else {
- MediaWiki\suppressWarnings();
- $handle = fopen( $this->getLockPath( $path ), 'a+' );
- MediaWiki\restoreWarnings();
- if ( !$handle ) { // lock dir missing?
- wfMkdirParents( $this->lockDir );
- $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again
- }
- }
- if ( $handle ) {
- // Either a shared or exclusive lock
- $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX;
- if ( flock( $handle, $lock | LOCK_NB ) ) {
- // Record this lock as active
- $this->locksHeld[$path][$type] = 1;
- $this->handles[$path] = $handle;
- } else {
- fclose( $handle );
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- } else {
- $status->fatal( 'lockmanager-fail-openlock', $path );
- }
- }
-
- return $status;
- }
-
- /**
- * Unlock a single resource key
- *
- * @param string $path
- * @param int $type
- * @return Status
- */
- protected function doSingleUnlock( $path, $type ) {
- $status = Status::newGood();
-
- if ( !isset( $this->locksHeld[$path] ) ) {
- $status->warning( 'lockmanager-notlocked', $path );
- } elseif ( !isset( $this->locksHeld[$path][$type] ) ) {
- $status->warning( 'lockmanager-notlocked', $path );
- } else {
- $handlesToClose = [];
- --$this->locksHeld[$path][$type];
- if ( $this->locksHeld[$path][$type] <= 0 ) {
- unset( $this->locksHeld[$path][$type] );
- }
- if ( !count( $this->locksHeld[$path] ) ) {
- unset( $this->locksHeld[$path] ); // no locks on this path
- if ( isset( $this->handles[$path] ) ) {
- $handlesToClose[] = $this->handles[$path];
- unset( $this->handles[$path] );
- }
- }
- // Unlock handles to release locks and delete
- // any lock files that end up with no locks on them...
- if ( wfIsWindows() ) {
- // Windows: for any process, including this one,
- // calling unlink() on a locked file will fail
- $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
- $status->merge( $this->pruneKeyLockFiles( $path ) );
- } else {
- // Unix: unlink() can be used on files currently open by this
- // process and we must do so in order to avoid race conditions
- $status->merge( $this->pruneKeyLockFiles( $path ) );
- $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
- }
- }
-
- return $status;
- }
-
- /**
- * @param string $path
- * @param array $handlesToClose
- * @return Status
- */
- private function closeLockHandles( $path, array $handlesToClose ) {
- $status = Status::newGood();
- foreach ( $handlesToClose as $handle ) {
- if ( !flock( $handle, LOCK_UN ) ) {
- $status->fatal( 'lockmanager-fail-releaselock', $path );
- }
- if ( !fclose( $handle ) ) {
- $status->warning( 'lockmanager-fail-closelock', $path );
- }
- }
-
- return $status;
- }
-
- /**
- * @param string $path
- * @return Status
- */
- private function pruneKeyLockFiles( $path ) {
- $status = Status::newGood();
- if ( !isset( $this->locksHeld[$path] ) ) {
- # No locks are held for the lock file anymore
- if ( !unlink( $this->getLockPath( $path ) ) ) {
- $status->warning( 'lockmanager-fail-deletelock', $path );
- }
- unset( $this->handles[$path] );
- }
-
- return $status;
- }
-
- /**
- * Get the path to the lock file for a key
- * @param string $path
- * @return string
- */
- protected function getLockPath( $path ) {
- return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock";
- }
-
- /**
- * Make sure remaining locks get cleared for sanity
- */
- function __destruct() {
- while ( count( $this->locksHeld ) ) {
- foreach ( $this->locksHeld as $path => $locks ) {
- $this->doSingleUnlock( $path, self::LOCK_EX );
- $this->doSingleUnlock( $path, self::LOCK_SH );
- }
- }
- }
-}
diff --git a/www/wiki/includes/filebackend/lockmanager/LockManager.php b/www/wiki/includes/filebackend/lockmanager/LockManager.php
deleted file mode 100644
index 567a2989..00000000
--- a/www/wiki/includes/filebackend/lockmanager/LockManager.php
+++ /dev/null
@@ -1,258 +0,0 @@
-<?php
-/**
- * @defgroup LockManager Lock management
- * @ingroup FileBackend
- */
-
-/**
- * Resource locking handling.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup LockManager
- * @author Aaron Schulz
- */
-
-/**
- * @brief Class for handling resource locking.
- *
- * Locks on resource keys can either be shared or exclusive.
- *
- * Implementations must keep track of what is locked by this proccess
- * in-memory and support nested locking calls (using reference counting).
- * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op.
- * Locks should either be non-blocking or have low wait timeouts.
- *
- * Subclasses should avoid throwing exceptions at all costs.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-abstract class LockManager {
- /** @var array Mapping of lock types to the type actually used */
- protected $lockTypeMap = [
- self::LOCK_SH => self::LOCK_SH,
- self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH
- self::LOCK_EX => self::LOCK_EX
- ];
-
- /** @var array Map of (resource path => lock type => count) */
- protected $locksHeld = [];
-
- protected $domain; // string; domain (usually wiki ID)
- protected $lockTTL; // integer; maximum time locks can be held
-
- /** Lock types; stronger locks have higher values */
- const LOCK_SH = 1; // shared lock (for reads)
- const LOCK_UW = 2; // shared lock (for reads used to write elsewhere)
- const LOCK_EX = 3; // exclusive lock (for writes)
-
- /**
- * Construct a new instance from configuration
- *
- * @param array $config Parameters include:
- * - domain : Domain (usually wiki ID) that all resources are relative to [optional]
- * - lockTTL : Age (in seconds) at which resource locks should expire.
- * This only applies if locks are not tied to a connection/process.
- */
- public function __construct( array $config ) {
- $this->domain = isset( $config['domain'] ) ? $config['domain'] : wfWikiID();
- if ( isset( $config['lockTTL'] ) ) {
- $this->lockTTL = max( 5, $config['lockTTL'] );
- } elseif ( PHP_SAPI === 'cli' ) {
- $this->lockTTL = 3600;
- } else {
- $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
- $this->lockTTL = max( 5 * 60, 2 * (int)$met );
- }
- }
-
- /**
- * Lock the resources at the given abstract paths
- *
- * @param array $paths List of resource names
- * @param int $type LockManager::LOCK_* constant
- * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21)
- * @return Status
- */
- final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) {
- return $this->lockByType( [ $type => $paths ], $timeout );
- }
-
- /**
- * Lock the resources at the given abstract paths
- *
- * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
- * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21)
- * @return Status
- * @since 1.22
- */
- final public function lockByType( array $pathsByType, $timeout = 0 ) {
- $pathsByType = $this->normalizePathsByType( $pathsByType );
- $msleep = [ 0, 50, 100, 300, 500 ]; // retry backoff times
- $start = microtime( true );
- do {
- $status = $this->doLockByType( $pathsByType );
- $elapsed = microtime( true ) - $start;
- if ( $status->isOK() || $elapsed >= $timeout || $elapsed < 0 ) {
- break; // success, timeout, or clock set back
- }
- usleep( 1e3 * ( next( $msleep ) ?: 1000 ) ); // use 1 sec after enough times
- $elapsed = microtime( true ) - $start;
- } while ( $elapsed < $timeout && $elapsed >= 0 );
-
- return $status;
- }
-
- /**
- * Unlock the resources at the given abstract paths
- *
- * @param array $paths List of paths
- * @param int $type LockManager::LOCK_* constant
- * @return Status
- */
- final public function unlock( array $paths, $type = self::LOCK_EX ) {
- return $this->unlockByType( [ $type => $paths ] );
- }
-
- /**
- * Unlock the resources at the given abstract paths
- *
- * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
- * @return Status
- * @since 1.22
- */
- final public function unlockByType( array $pathsByType ) {
- $pathsByType = $this->normalizePathsByType( $pathsByType );
- $status = $this->doUnlockByType( $pathsByType );
-
- return $status;
- }
-
- /**
- * Get the base 36 SHA-1 of a string, padded to 31 digits.
- * Before hashing, the path will be prefixed with the domain ID.
- * This should be used interally for lock key or file names.
- *
- * @param string $path
- * @return string
- */
- final protected function sha1Base36Absolute( $path ) {
- return Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 );
- }
-
- /**
- * Get the base 16 SHA-1 of a string, padded to 31 digits.
- * Before hashing, the path will be prefixed with the domain ID.
- * This should be used interally for lock key or file names.
- *
- * @param string $path
- * @return string
- */
- final protected function sha1Base16Absolute( $path ) {
- return sha1( "{$this->domain}:{$path}" );
- }
-
- /**
- * Normalize the $paths array by converting LOCK_UW locks into the
- * appropriate type and removing any duplicated paths for each lock type.
- *
- * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
- * @return array
- * @since 1.22
- */
- final protected function normalizePathsByType( array $pathsByType ) {
- $res = [];
- foreach ( $pathsByType as $type => $paths ) {
- $res[$this->lockTypeMap[$type]] = array_unique( $paths );
- }
-
- return $res;
- }
-
- /**
- * @see LockManager::lockByType()
- * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
- * @return Status
- * @since 1.22
- */
- protected function doLockByType( array $pathsByType ) {
- $status = Status::newGood();
- $lockedByType = []; // map of (type => paths)
- foreach ( $pathsByType as $type => $paths ) {
- $status->merge( $this->doLock( $paths, $type ) );
- if ( $status->isOK() ) {
- $lockedByType[$type] = $paths;
- } else {
- // Release the subset of locks that were acquired
- foreach ( $lockedByType as $lType => $lPaths ) {
- $status->merge( $this->doUnlock( $lPaths, $lType ) );
- }
- break;
- }
- }
-
- return $status;
- }
-
- /**
- * Lock resources with the given keys and lock type
- *
- * @param array $paths List of paths
- * @param int $type LockManager::LOCK_* constant
- * @return Status
- */
- abstract protected function doLock( array $paths, $type );
-
- /**
- * @see LockManager::unlockByType()
- * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
- * @return Status
- * @since 1.22
- */
- protected function doUnlockByType( array $pathsByType ) {
- $status = Status::newGood();
- foreach ( $pathsByType as $type => $paths ) {
- $status->merge( $this->doUnlock( $paths, $type ) );
- }
-
- return $status;
- }
-
- /**
- * Unlock resources with the given keys and lock type
- *
- * @param array $paths List of paths
- * @param int $type LockManager::LOCK_* constant
- * @return Status
- */
- abstract protected function doUnlock( array $paths, $type );
-}
-
-/**
- * Simple version of LockManager that does nothing
- * @since 1.19
- */
-class NullLockManager extends LockManager {
- protected function doLock( array $paths, $type ) {
- return Status::newGood();
- }
-
- protected function doUnlock( array $paths, $type ) {
- return Status::newGood();
- }
-}
diff --git a/www/wiki/includes/filebackend/lockmanager/LockManagerGroup.php b/www/wiki/includes/filebackend/lockmanager/LockManagerGroup.php
index e6f992c3..5d79dac0 100644
--- a/www/wiki/includes/filebackend/lockmanager/LockManagerGroup.php
+++ b/www/wiki/includes/filebackend/lockmanager/LockManagerGroup.php
@@ -116,7 +116,7 @@ class LockManagerGroup {
if ( !isset( $this->managers[$name]['instance'] ) ) {
$class = $this->managers[$name]['class'];
$config = $this->managers[$name]['config'];
- if ( $class === 'DBLockManager' ) {
+ if ( $class === DBLockManager::class ) {
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$lb = $lbFactory->newMainLB( $config['domain'] );
$dbw = $lb->getLazyConnectionRef( DB_MASTER, [], $config['domain'] );
diff --git a/www/wiki/includes/filebackend/lockmanager/MemcLockManager.php b/www/wiki/includes/filebackend/lockmanager/MemcLockManager.php
deleted file mode 100644
index cb5266ac..00000000
--- a/www/wiki/includes/filebackend/lockmanager/MemcLockManager.php
+++ /dev/null
@@ -1,384 +0,0 @@
-<?php
-/**
- * Version of LockManager based on using memcached servers.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup LockManager
- */
-
-/**
- * Manage locks using memcached servers.
- *
- * Version of LockManager based on using memcached servers.
- * This is meant for multi-wiki systems that may share files.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * All lock requests for a resource, identified by a hash string, will map to one
- * bucket. Each bucket maps to one or several peer servers, each running memcached.
- * A majority of peers must agree for a lock to be acquired.
- *
- * @ingroup LockManager
- * @since 1.20
- */
-class MemcLockManager extends QuorumLockManager {
- /** @var array Mapping of lock types to the type actually used */
- protected $lockTypeMap = [
- self::LOCK_SH => self::LOCK_SH,
- self::LOCK_UW => self::LOCK_SH,
- self::LOCK_EX => self::LOCK_EX
- ];
-
- /** @var array Map server names to MemcachedBagOStuff objects */
- protected $bagOStuffs = [];
-
- /** @var array (server name => bool) */
- protected $serversUp = [];
-
- /** @var string Random UUID */
- protected $session = '';
-
- /**
- * Construct a new instance from configuration.
- *
- * @param array $config Parameters include:
- * - lockServers : Associative array of server names to "<IP>:<port>" strings.
- * - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
- * each having an odd-numbered list of server names (peers) as values.
- * - memcConfig : Configuration array for ObjectCache::newFromParams. [optional]
- * If set, this must use one of the memcached classes.
- * @throws Exception
- */
- public function __construct( array $config ) {
- parent::__construct( $config );
-
- // Sanitize srvsByBucket config to prevent PHP errors
- $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
- $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
-
- $memcConfig = isset( $config['memcConfig'] )
- ? $config['memcConfig']
- : [ 'class' => 'MemcachedPhpBagOStuff' ];
-
- foreach ( $config['lockServers'] as $name => $address ) {
- $params = [ 'servers' => [ $address ] ] + $memcConfig;
- $cache = ObjectCache::newFromParams( $params );
- if ( $cache instanceof MemcachedBagOStuff ) {
- $this->bagOStuffs[$name] = $cache;
- } else {
- throw new Exception(
- 'Only MemcachedBagOStuff classes are supported by MemcLockManager.' );
- }
- }
-
- $this->session = wfRandomString( 32 );
- }
-
- // @todo Change this code to work in one batch
- protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
- $status = Status::newGood();
-
- $lockedPaths = [];
- foreach ( $pathsByType as $type => $paths ) {
- $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
- if ( $status->isOK() ) {
- $lockedPaths[$type] = isset( $lockedPaths[$type] )
- ? array_merge( $lockedPaths[$type], $paths )
- : $paths;
- } else {
- foreach ( $lockedPaths as $lType => $lPaths ) {
- $status->merge( $this->doFreeLocksOnServer( $lockSrv, $lPaths, $lType ) );
- }
- break;
- }
- }
-
- return $status;
- }
-
- // @todo Change this code to work in one batch
- protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
- $status = Status::newGood();
-
- foreach ( $pathsByType as $type => $paths ) {
- $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) );
- }
-
- return $status;
- }
-
- /**
- * @see QuorumLockManager::getLocksOnServer()
- * @param string $lockSrv
- * @param array $paths
- * @param string $type
- * @return Status
- */
- protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
- $status = Status::newGood();
-
- $memc = $this->getCache( $lockSrv );
- $keys = array_map( [ $this, 'recordKeyForPath' ], $paths ); // lock records
-
- // Lock all of the active lock record keys...
- if ( !$this->acquireMutexes( $memc, $keys ) ) {
- foreach ( $paths as $path ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
-
- return $status;
- }
-
- // Fetch all the existing lock records...
- $lockRecords = $memc->getMulti( $keys );
-
- $now = time();
- // Check if the requested locks conflict with existing ones...
- foreach ( $paths as $path ) {
- $locksKey = $this->recordKeyForPath( $path );
- $locksHeld = isset( $lockRecords[$locksKey] )
- ? self::sanitizeLockArray( $lockRecords[$locksKey] )
- : self::newLockArray(); // init
- foreach ( $locksHeld[self::LOCK_EX] as $session => $expiry ) {
- if ( $expiry < $now ) { // stale?
- unset( $locksHeld[self::LOCK_EX][$session] );
- } elseif ( $session !== $this->session ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- }
- if ( $type === self::LOCK_EX ) {
- foreach ( $locksHeld[self::LOCK_SH] as $session => $expiry ) {
- if ( $expiry < $now ) { // stale?
- unset( $locksHeld[self::LOCK_SH][$session] );
- } elseif ( $session !== $this->session ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- }
- }
- if ( $status->isOK() ) {
- // Register the session in the lock record array
- $locksHeld[$type][$this->session] = $now + $this->lockTTL;
- // We will update this record if none of the other locks conflict
- $lockRecords[$locksKey] = $locksHeld;
- }
- }
-
- // If there were no lock conflicts, update all the lock records...
- if ( $status->isOK() ) {
- foreach ( $paths as $path ) {
- $locksKey = $this->recordKeyForPath( $path );
- $locksHeld = $lockRecords[$locksKey];
- $ok = $memc->set( $locksKey, $locksHeld, 7 * 86400 );
- if ( !$ok ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- } else {
- wfDebug( __METHOD__ . ": acquired lock on key $locksKey.\n" );
- }
- }
- }
-
- // Unlock all of the active lock record keys...
- $this->releaseMutexes( $memc, $keys );
-
- return $status;
- }
-
- /**
- * @see QuorumLockManager::freeLocksOnServer()
- * @param string $lockSrv
- * @param array $paths
- * @param string $type
- * @return Status
- */
- protected function doFreeLocksOnServer( $lockSrv, array $paths, $type ) {
- $status = Status::newGood();
-
- $memc = $this->getCache( $lockSrv );
- $keys = array_map( [ $this, 'recordKeyForPath' ], $paths ); // lock records
-
- // Lock all of the active lock record keys...
- if ( !$this->acquireMutexes( $memc, $keys ) ) {
- foreach ( $paths as $path ) {
- $status->fatal( 'lockmanager-fail-releaselock', $path );
- }
-
- return $status;
- }
-
- // Fetch all the existing lock records...
- $lockRecords = $memc->getMulti( $keys );
-
- // Remove the requested locks from all records...
- foreach ( $paths as $path ) {
- $locksKey = $this->recordKeyForPath( $path ); // lock record
- if ( !isset( $lockRecords[$locksKey] ) ) {
- $status->warning( 'lockmanager-fail-releaselock', $path );
- continue; // nothing to do
- }
- $locksHeld = self::sanitizeLockArray( $lockRecords[$locksKey] );
- if ( isset( $locksHeld[$type][$this->session] ) ) {
- unset( $locksHeld[$type][$this->session] ); // unregister this session
- if ( $locksHeld === self::newLockArray() ) {
- $ok = $memc->delete( $locksKey );
- } else {
- $ok = $memc->set( $locksKey, $locksHeld );
- }
- if ( !$ok ) {
- $status->fatal( 'lockmanager-fail-releaselock', $path );
- }
- } else {
- $status->warning( 'lockmanager-fail-releaselock', $path );
- }
- wfDebug( __METHOD__ . ": released lock on key $locksKey.\n" );
- }
-
- // Unlock all of the active lock record keys...
- $this->releaseMutexes( $memc, $keys );
-
- return $status;
- }
-
- /**
- * @see QuorumLockManager::releaseAllLocks()
- * @return Status
- */
- protected function releaseAllLocks() {
- return Status::newGood(); // not supported
- }
-
- /**
- * @see QuorumLockManager::isServerUp()
- * @param string $lockSrv
- * @return bool
- */
- protected function isServerUp( $lockSrv ) {
- return (bool)$this->getCache( $lockSrv );
- }
-
- /**
- * Get the MemcachedBagOStuff object for a $lockSrv
- *
- * @param string $lockSrv Server name
- * @return MemcachedBagOStuff|null
- */
- protected function getCache( $lockSrv ) {
- $memc = null;
- if ( isset( $this->bagOStuffs[$lockSrv] ) ) {
- $memc = $this->bagOStuffs[$lockSrv];
- if ( !isset( $this->serversUp[$lockSrv] ) ) {
- $this->serversUp[$lockSrv] = $memc->set( __CLASS__ . ':ping', 1, 1 );
- if ( !$this->serversUp[$lockSrv] ) {
- trigger_error( __METHOD__ . ": Could not contact $lockSrv.", E_USER_WARNING );
- }
- }
- if ( !$this->serversUp[$lockSrv] ) {
- return null; // server appears to be down
- }
- }
-
- return $memc;
- }
-
- /**
- * @param string $path
- * @return string
- */
- protected function recordKeyForPath( $path ) {
- return implode( ':', [ __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ] );
- }
-
- /**
- * @return array An empty lock structure for a key
- */
- protected static function newLockArray() {
- return [ self::LOCK_SH => [], self::LOCK_EX => [] ];
- }
-
- /**
- * @param array $a
- * @return array An empty lock structure for a key
- */
- protected static function sanitizeLockArray( $a ) {
- if ( is_array( $a ) && isset( $a[self::LOCK_EX] ) && isset( $a[self::LOCK_SH] ) ) {
- return $a;
- } else {
- trigger_error( __METHOD__ . ": reset invalid lock array.", E_USER_WARNING );
-
- return self::newLockArray();
- }
- }
-
- /**
- * @param MemcachedBagOStuff $memc
- * @param array $keys List of keys to acquire
- * @return bool
- */
- protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) {
- $lockedKeys = [];
-
- // Acquire the keys in lexicographical order, to avoid deadlock problems.
- // If P1 is waiting to acquire a key P2 has, P2 can't also be waiting for a key P1 has.
- sort( $keys );
-
- // Try to quickly loop to acquire the keys, but back off after a few rounds.
- // This reduces memcached spam, especially in the rare case where a server acquires
- // some lock keys and dies without releasing them. Lock keys expire after a few minutes.
- $rounds = 0;
- $start = microtime( true );
- do {
- if ( ( ++$rounds % 4 ) == 0 ) {
- usleep( 1000 * 50 ); // 50 ms
- }
- foreach ( array_diff( $keys, $lockedKeys ) as $key ) {
- if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record
- $lockedKeys[] = $key;
- } else {
- continue; // acquire in order
- }
- }
- } while ( count( $lockedKeys ) < count( $keys ) && ( microtime( true ) - $start ) <= 3 );
-
- if ( count( $lockedKeys ) != count( $keys ) ) {
- $this->releaseMutexes( $memc, $lockedKeys ); // failed; release what was locked
- return false;
- }
-
- return true;
- }
-
- /**
- * @param MemcachedBagOStuff $memc
- * @param array $keys List of acquired keys
- */
- protected function releaseMutexes( MemcachedBagOStuff $memc, array $keys ) {
- foreach ( $keys as $key ) {
- $memc->delete( "$key:mutex" );
- }
- }
-
- /**
- * Make sure remaining locks get cleared for sanity
- */
- function __destruct() {
- while ( count( $this->locksHeld ) ) {
- foreach ( $this->locksHeld as $path => $locks ) {
- $this->doUnlock( [ $path ], self::LOCK_EX );
- $this->doUnlock( [ $path ], self::LOCK_SH );
- }
- }
- }
-}
diff --git a/www/wiki/includes/filebackend/lockmanager/QuorumLockManager.php b/www/wiki/includes/filebackend/lockmanager/QuorumLockManager.php
deleted file mode 100644
index 108b8465..00000000
--- a/www/wiki/includes/filebackend/lockmanager/QuorumLockManager.php
+++ /dev/null
@@ -1,248 +0,0 @@
-<?php
-/**
- * Version of LockManager that uses a quorum from peer servers for locks.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup LockManager
- */
-
-/**
- * Version of LockManager that uses a quorum from peer servers for locks.
- * The resource space can also be sharded into separate peer groups.
- *
- * @ingroup LockManager
- * @since 1.20
- */
-abstract class QuorumLockManager extends LockManager {
- /** @var array Map of bucket indexes to peer server lists */
- protected $srvsByBucket = []; // (bucket index => (lsrv1, lsrv2, ...))
-
- /** @var array Map of degraded buckets */
- protected $degradedBuckets = []; // (buckey index => UNIX timestamp)
-
- final protected function doLock( array $paths, $type ) {
- return $this->doLockByType( [ $type => $paths ] );
- }
-
- final protected function doUnlock( array $paths, $type ) {
- return $this->doUnlockByType( [ $type => $paths ] );
- }
-
- protected function doLockByType( array $pathsByType ) {
- $status = Status::newGood();
-
- $pathsToLock = []; // (bucket => type => paths)
- // Get locks that need to be acquired (buckets => locks)...
- foreach ( $pathsByType as $type => $paths ) {
- foreach ( $paths as $path ) {
- if ( isset( $this->locksHeld[$path][$type] ) ) {
- ++$this->locksHeld[$path][$type];
- } else {
- $bucket = $this->getBucketFromPath( $path );
- $pathsToLock[$bucket][$type][] = $path;
- }
- }
- }
-
- $lockedPaths = []; // files locked in this attempt (type => paths)
- // Attempt to acquire these locks...
- foreach ( $pathsToLock as $bucket => $pathsToLockByType ) {
- // Try to acquire the locks for this bucket
- $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) );
- if ( !$status->isOK() ) {
- $status->merge( $this->doUnlockByType( $lockedPaths ) );
-
- return $status;
- }
- // Record these locks as active
- foreach ( $pathsToLockByType as $type => $paths ) {
- foreach ( $paths as $path ) {
- $this->locksHeld[$path][$type] = 1; // locked
- // Keep track of what locks were made in this attempt
- $lockedPaths[$type][] = $path;
- }
- }
- }
-
- return $status;
- }
-
- protected function doUnlockByType( array $pathsByType ) {
- $status = Status::newGood();
-
- $pathsToUnlock = []; // (bucket => type => paths)
- foreach ( $pathsByType as $type => $paths ) {
- foreach ( $paths as $path ) {
- if ( !isset( $this->locksHeld[$path][$type] ) ) {
- $status->warning( 'lockmanager-notlocked', $path );
- } else {
- --$this->locksHeld[$path][$type];
- // Reference count the locks held and release locks when zero
- if ( $this->locksHeld[$path][$type] <= 0 ) {
- unset( $this->locksHeld[$path][$type] );
- $bucket = $this->getBucketFromPath( $path );
- $pathsToUnlock[$bucket][$type][] = $path;
- }
- if ( !count( $this->locksHeld[$path] ) ) {
- unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
- }
- }
- }
- }
-
- // Remove these specific locks if possible, or at least release
- // all locks once this process is currently not holding any locks.
- foreach ( $pathsToUnlock as $bucket => $pathsToUnlockByType ) {
- $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) );
- }
- if ( !count( $this->locksHeld ) ) {
- $status->merge( $this->releaseAllLocks() );
- $this->degradedBuckets = []; // safe to retry the normal quorum
- }
-
- return $status;
- }
-
- /**
- * Attempt to acquire locks with the peers for a bucket.
- * This is all or nothing; if any key is locked then this totally fails.
- *
- * @param int $bucket
- * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
- * @return Status
- */
- final protected function doLockingRequestBucket( $bucket, array $pathsByType ) {
- $status = Status::newGood();
-
- $yesVotes = 0; // locks made on trustable servers
- $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
- $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
- // Get votes for each peer, in order, until we have enough...
- foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
- if ( !$this->isServerUp( $lockSrv ) ) {
- --$votesLeft;
- $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv );
- $this->degradedBuckets[$bucket] = time();
- continue; // server down?
- }
- // Attempt to acquire the lock on this peer
- $status->merge( $this->getLocksOnServer( $lockSrv, $pathsByType ) );
- if ( !$status->isOK() ) {
- return $status; // vetoed; resource locked
- }
- ++$yesVotes; // success for this peer
- if ( $yesVotes >= $quorum ) {
- return $status; // lock obtained
- }
- --$votesLeft;
- $votesNeeded = $quorum - $yesVotes;
- if ( $votesNeeded > $votesLeft ) {
- break; // short-circuit
- }
- }
- // At this point, we must not have met the quorum
- $status->setResult( false );
-
- return $status;
- }
-
- /**
- * Attempt to release locks with the peers for a bucket
- *
- * @param int $bucket
- * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
- * @return Status
- */
- final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) {
- $status = Status::newGood();
-
- $yesVotes = 0; // locks freed on trustable servers
- $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
- $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
- $isDegraded = isset( $this->degradedBuckets[$bucket] ); // not the normal quorum?
- foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
- if ( !$this->isServerUp( $lockSrv ) ) {
- $status->warning( 'lockmanager-fail-svr-release', $lockSrv );
- } else {
- // Attempt to release the lock on this peer
- $status->merge( $this->freeLocksOnServer( $lockSrv, $pathsByType ) );
- ++$yesVotes; // success for this peer
- // Normally the first peers form the quorum, and the others are ignored.
- // Ignore them in this case, but not when an alternative quorum was used.
- if ( $yesVotes >= $quorum && !$isDegraded ) {
- break; // lock released
- }
- }
- }
- // Set a bad status if the quorum was not met.
- // Assumes the same "up" servers as during the acquire step.
- $status->setResult( $yesVotes >= $quorum );
-
- return $status;
- }
-
- /**
- * Get the bucket for resource path.
- * This should avoid throwing any exceptions.
- *
- * @param string $path
- * @return int
- */
- protected function getBucketFromPath( $path ) {
- $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
- return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket );
- }
-
- /**
- * Check if a lock server is up.
- * This should process cache results to reduce RTT.
- *
- * @param string $lockSrv
- * @return bool
- */
- abstract protected function isServerUp( $lockSrv );
-
- /**
- * Get a connection to a lock server and acquire locks
- *
- * @param string $lockSrv
- * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
- * @return Status
- */
- abstract protected function getLocksOnServer( $lockSrv, array $pathsByType );
-
- /**
- * Get a connection to a lock server and release locks on $paths.
- *
- * Subclasses must effectively implement this or releaseAllLocks().
- *
- * @param string $lockSrv
- * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
- * @return Status
- */
- abstract protected function freeLocksOnServer( $lockSrv, array $pathsByType );
-
- /**
- * Release all locks that this session is holding.
- *
- * Subclasses must effectively implement this or freeLocksOnServer().
- *
- * @return Status
- */
- abstract protected function releaseAllLocks();
-}
diff --git a/www/wiki/includes/filebackend/lockmanager/RedisLockManager.php b/www/wiki/includes/filebackend/lockmanager/RedisLockManager.php
deleted file mode 100644
index 6095aeed..00000000
--- a/www/wiki/includes/filebackend/lockmanager/RedisLockManager.php
+++ /dev/null
@@ -1,272 +0,0 @@
-<?php
-/**
- * Version of LockManager based on using redis servers.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup LockManager
- */
-
-/**
- * Manage locks using redis servers.
- *
- * Version of LockManager based on using redis servers.
- * This is meant for multi-wiki systems that may share files.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * All lock requests for a resource, identified by a hash string, will map to one
- * bucket. Each bucket maps to one or several peer servers, each running redis.
- * A majority of peers must agree for a lock to be acquired.
- *
- * This class requires Redis 2.6 as it makes use Lua scripts for fast atomic operations.
- *
- * @ingroup LockManager
- * @since 1.22
- */
-class RedisLockManager extends QuorumLockManager {
- /** @var array Mapping of lock types to the type actually used */
- protected $lockTypeMap = [
- self::LOCK_SH => self::LOCK_SH,
- self::LOCK_UW => self::LOCK_SH,
- self::LOCK_EX => self::LOCK_EX
- ];
-
- /** @var RedisConnectionPool */
- protected $redisPool;
-
- /** @var array Map server names to hostname/IP and port numbers */
- protected $lockServers = [];
-
- /** @var string Random UUID */
- protected $session = '';
-
- /**
- * Construct a new instance from configuration.
- *
- * @param array $config Parameters include:
- * - lockServers : Associative array of server names to "<IP>:<port>" strings.
- * - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
- * each having an odd-numbered list of server names (peers) as values.
- * - redisConfig : Configuration for RedisConnectionPool::__construct().
- * @throws Exception
- */
- public function __construct( array $config ) {
- parent::__construct( $config );
-
- $this->lockServers = $config['lockServers'];
- // Sanitize srvsByBucket config to prevent PHP errors
- $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
- $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
-
- $config['redisConfig']['serializer'] = 'none';
- $this->redisPool = RedisConnectionPool::singleton( $config['redisConfig'] );
-
- $this->session = wfRandomString( 32 );
- }
-
- protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
- $status = Status::newGood();
-
- $server = $this->lockServers[$lockSrv];
- $conn = $this->redisPool->getConnection( $server );
- if ( !$conn ) {
- foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
-
- return $status;
- }
-
- $pathsByKey = []; // (type:hash => path) map
- foreach ( $pathsByType as $type => $paths ) {
- $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
- foreach ( $paths as $path ) {
- $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
- }
- }
-
- try {
- static $script =
-<<<LUA
- local failed = {}
- -- Load input params (e.g. session, ttl, time of request)
- local rSession, rTTL, rTime = unpack(ARGV)
- -- Check that all the locks can be acquired
- for i,requestKey in ipairs(KEYS) do
- local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
- local keyIsFree = true
- local currentLocks = redis.call('hKeys',resourceKey)
- for i,lockKey in ipairs(currentLocks) do
- -- Get the type and session of this lock
- local _, _, type, session = string.find(lockKey,"(%w+):(%w+)")
- -- Check any locks that are not owned by this session
- if session ~= rSession then
- local lockExpiry = redis.call('hGet',resourceKey,lockKey)
- if 1*lockExpiry < 1*rTime then
- -- Lock is stale, so just prune it out
- redis.call('hDel',resourceKey,lockKey)
- elseif rType == 'EX' or type == 'EX' then
- keyIsFree = false
- break
- end
- end
- end
- if not keyIsFree then
- failed[#failed+1] = requestKey
- end
- end
- -- If all locks could be acquired, then do so
- if #failed == 0 then
- for i,requestKey in ipairs(KEYS) do
- local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
- redis.call('hSet',resourceKey,rType .. ':' .. rSession,rTime + rTTL)
- -- In addition to invalidation logic, be sure to garbage collect
- redis.call('expire',resourceKey,rTTL)
- end
- end
- return failed
-LUA;
- $res = $conn->luaEval( $script,
- array_merge(
- array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
- [
- $this->session, // ARGV[1]
- $this->lockTTL, // ARGV[2]
- time() // ARGV[3]
- ]
- ),
- count( $pathsByKey ) # number of first argument(s) that are keys
- );
- } catch ( RedisException $e ) {
- $res = false;
- $this->redisPool->handleError( $conn, $e );
- }
-
- if ( $res === false ) {
- foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- } else {
- foreach ( $res as $key ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $pathsByKey[$key] );
- }
- }
-
- return $status;
- }
-
- protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
- $status = Status::newGood();
-
- $server = $this->lockServers[$lockSrv];
- $conn = $this->redisPool->getConnection( $server );
- if ( !$conn ) {
- foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
- $status->fatal( 'lockmanager-fail-releaselock', $path );
- }
-
- return $status;
- }
-
- $pathsByKey = []; // (type:hash => path) map
- foreach ( $pathsByType as $type => $paths ) {
- $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
- foreach ( $paths as $path ) {
- $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
- }
- }
-
- try {
- static $script =
-<<<LUA
- local failed = {}
- -- Load input params (e.g. session)
- local rSession = unpack(ARGV)
- for i,requestKey in ipairs(KEYS) do
- local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
- local released = redis.call('hDel',resourceKey,rType .. ':' .. rSession)
- if released > 0 then
- -- Remove the whole structure if it is now empty
- if redis.call('hLen',resourceKey) == 0 then
- redis.call('del',resourceKey)
- end
- else
- failed[#failed+1] = requestKey
- end
- end
- return failed
-LUA;
- $res = $conn->luaEval( $script,
- array_merge(
- array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
- [
- $this->session, // ARGV[1]
- ]
- ),
- count( $pathsByKey ) # number of first argument(s) that are keys
- );
- } catch ( RedisException $e ) {
- $res = false;
- $this->redisPool->handleError( $conn, $e );
- }
-
- if ( $res === false ) {
- foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
- $status->fatal( 'lockmanager-fail-releaselock', $path );
- }
- } else {
- foreach ( $res as $key ) {
- $status->fatal( 'lockmanager-fail-releaselock', $pathsByKey[$key] );
- }
- }
-
- return $status;
- }
-
- protected function releaseAllLocks() {
- return Status::newGood(); // not supported
- }
-
- protected function isServerUp( $lockSrv ) {
- return (bool)$this->redisPool->getConnection( $this->lockServers[$lockSrv] );
- }
-
- /**
- * @param string $path
- * @param string $type One of (EX,SH)
- * @return string
- */
- protected function recordKeyForPath( $path, $type ) {
- return implode( ':',
- [ __CLASS__, 'locks', "$type:" . $this->sha1Base36Absolute( $path ) ] );
- }
-
- /**
- * Make sure remaining locks get cleared for sanity
- */
- function __destruct() {
- while ( count( $this->locksHeld ) ) {
- $pathsByType = [];
- foreach ( $this->locksHeld as $path => $locks ) {
- foreach ( $locks as $type => $count ) {
- $pathsByType[$type][] = $path;
- }
- }
- $this->unlockByType( $pathsByType );
- }
- }
-}
diff --git a/www/wiki/includes/filebackend/lockmanager/ScopedLock.php b/www/wiki/includes/filebackend/lockmanager/ScopedLock.php
deleted file mode 100644
index e1a600ce..00000000
--- a/www/wiki/includes/filebackend/lockmanager/ScopedLock.php
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-/**
- * Resource locking handling.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup LockManager
- * @author Aaron Schulz
- */
-
-/**
- * Self-releasing locks
- *
- * LockManager helper class to handle scoped locks, which
- * release when an object is destroyed or goes out of scope.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-class ScopedLock {
- /** @var LockManager */
- protected $manager;
-
- /** @var Status */
- protected $status;
-
- /** @var array Map of lock types to resource paths */
- protected $pathsByType;
-
- /**
- * @param LockManager $manager
- * @param array $pathsByType Map of lock types to path lists
- * @param Status $status
- */
- protected function __construct( LockManager $manager, array $pathsByType, Status $status ) {
- $this->manager = $manager;
- $this->pathsByType = $pathsByType;
- $this->status = $status;
- }
-
- /**
- * Get a ScopedLock object representing a lock on resource paths.
- * Any locks are released once this object goes out of scope.
- * The status object is updated with any errors or warnings.
- *
- * @param LockManager $manager
- * @param array $paths List of storage paths or map of lock types to path lists
- * @param int|string $type LockManager::LOCK_* constant or "mixed" and $paths
- * can be a map of types to paths (since 1.22). Otherwise $type should be an
- * integer and $paths should be a list of paths.
- * @param Status $status
- * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.22)
- * @return ScopedLock|null Returns null on failure
- */
- public static function factory(
- LockManager $manager, array $paths, $type, Status $status, $timeout = 0
- ) {
- $pathsByType = is_integer( $type ) ? [ $type => $paths ] : $paths;
- $lockStatus = $manager->lockByType( $pathsByType, $timeout );
- $status->merge( $lockStatus );
- if ( $lockStatus->isOK() ) {
- return new self( $manager, $pathsByType, $status );
- }
-
- return null;
- }
-
- /**
- * Release a scoped lock and set any errors in the attatched Status object.
- * This is useful for early release of locks before function scope is destroyed.
- * This is the same as setting the lock object to null.
- *
- * @param ScopedLock $lock
- * @since 1.21
- */
- public static function release( ScopedLock &$lock = null ) {
- $lock = null;
- }
-
- /**
- * Release the locks when this goes out of scope
- */
- function __destruct() {
- $wasOk = $this->status->isOK();
- $this->status->merge( $this->manager->unlockByType( $this->pathsByType ) );
- if ( $wasOk ) {
- // Make sure status is OK, despite any unlockFiles() fatals
- $this->status->setResult( true, $this->status->value );
- }
- }
-}
diff --git a/www/wiki/includes/filerepo/FSRepo.php b/www/wiki/includes/filerepo/FSRepo.php
deleted file mode 100644
index b24354dc..00000000
--- a/www/wiki/includes/filerepo/FSRepo.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php
-/**
- * A repository for files accessible via the local filesystem.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileRepo
- */
-
-/**
- * A repository for files accessible via the local filesystem.
- * Does not support database access or registration.
- *
- * This is a mostly a legacy class. New uses should not be added.
- *
- * @ingroup FileRepo
- * @deprecated since 1.19
- */
-class FSRepo extends FileRepo {
- /**
- * @param array $info
- * @throws MWException
- */
- function __construct( array $info ) {
- if ( !isset( $info['backend'] ) ) {
- // B/C settings...
- $directory = $info['directory'];
- $deletedDir = isset( $info['deletedDir'] )
- ? $info['deletedDir']
- : false;
- $thumbDir = isset( $info['thumbDir'] )
- ? $info['thumbDir']
- : "{$directory}/thumb";
- $transcodedDir = isset( $info['transcodedDir'] )
- ? $info['transcodedDir']
- : "{$directory}/transcoded";
- $fileMode = isset( $info['fileMode'] )
- ? $info['fileMode']
- : 0644;
-
- $repoName = $info['name'];
- // Get the FS backend configuration
- $backend = new FSFileBackend( [
- 'name' => $info['name'] . '-backend',
- 'wikiId' => wfWikiID(),
- 'lockManager' => LockManagerGroup::singleton( wfWikiID() )->get( 'fsLockManager' ),
- 'containerPaths' => [
- "{$repoName}-public" => "{$directory}",
- "{$repoName}-temp" => "{$directory}/temp",
- "{$repoName}-thumb" => $thumbDir,
- "{$repoName}-transcoded" => $transcodedDir,
- "{$repoName}-deleted" => $deletedDir
- ],
- 'fileMode' => $fileMode,
- ] );
- // Update repo config to use this backend
- $info['backend'] = $backend;
- }
-
- parent::__construct( $info );
-
- if ( !( $this->backend instanceof FSFileBackend ) ) {
- throw new MWException( "FSRepo only supports FSFileBackend." );
- }
- }
-}
diff --git a/www/wiki/includes/filerepo/FileBackendDBRepoWrapper.php b/www/wiki/includes/filerepo/FileBackendDBRepoWrapper.php
index 21b7ac2f..dbb54217 100644
--- a/www/wiki/includes/filerepo/FileBackendDBRepoWrapper.php
+++ b/www/wiki/includes/filerepo/FileBackendDBRepoWrapper.php
@@ -92,9 +92,9 @@ class FileBackendDBRepoWrapper extends FileBackend {
* E.g. mwstore://local-backend/local-public/a/ab/<name>.jpg
* => mwstore://local-backend/local-original/x/y/z/<sha1>.jpg
*
- * @param array $paths
+ * @param string[] $paths
* @param bool $latest
- * @return array Translated paths in same order
+ * @return string[] Translated paths in same order
*/
public function getBackendPaths( array $paths, $latest = true ) {
$db = $this->getDB( $latest ? DB_MASTER : DB_REPLICA );
@@ -341,8 +341,8 @@ class FileBackendDBRepoWrapper extends FileBackend {
*
* This leaves destination paths alone since we don't want those to mutate
*
- * @param array $ops
- * @return array
+ * @param array[] $ops
+ * @return array[]
*/
protected function mungeOpPaths( array $ops ) {
// Ops that use 'src' and do not mutate core file data there
diff --git a/www/wiki/includes/filerepo/FileRepo.php b/www/wiki/includes/filerepo/FileRepo.php
index 5162a04d..b15f81fa 100644
--- a/www/wiki/includes/filerepo/FileRepo.php
+++ b/www/wiki/includes/filerepo/FileRepo.php
@@ -78,10 +78,6 @@ class FileRepo {
*/
protected $scriptDirUrl;
- /** @var string Script extension of the MediaWiki installation, equivalent
- * to the old $wgScriptExtension, e.g. .php5 defaults to .php */
- protected $scriptExtension;
-
/** @var string Equivalent to $wgArticlePath, e.g. https://en.wikipedia.org/wiki/$1 */
protected $articleUrl;
@@ -124,7 +120,7 @@ class FileRepo {
protected $isPrivate;
/** @var array callable Override these in the base class */
- protected $fileFactory = [ 'UnregisteredLocalFile', 'newFromTitle' ];
+ protected $fileFactory = [ UnregisteredLocalFile::class, 'newFromTitle' ];
/** @var array callable|bool Override these in the base class */
protected $oldFileFactory = false;
/** @var array callable|bool Override these in the base class */
@@ -132,6 +128,13 @@ class FileRepo {
/** @var array callable|bool Override these in the base class */
protected $oldFileFactoryKey = false;
+ /** @var string URL of where to proxy thumb.php requests to.
+ * Example: http://127.0.0.1:8888/wiki/dev/thumb/
+ */
+ protected $thumbProxyUrl;
+ /** @var string Secret key to pass as an X-Swift-Secret header to the proxied thumb service */
+ protected $thumbProxySecret;
+
/**
* @param array|null $info
* @throws MWException
@@ -159,7 +162,7 @@ class FileRepo {
$optionalSettings = [
'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry',
- 'scriptExtension', 'favicon'
+ 'favicon', 'thumbProxyUrl', 'thumbProxySecret',
];
foreach ( $optionalSettings as $var ) {
if ( isset( $info[$var] ) ) {
@@ -575,8 +578,8 @@ class FileRepo {
* Get an array of arrays or iterators of file objects for files that
* have the given SHA-1 content hashes.
*
- * @param array $hashes An array of hashes
- * @return array An Array of arrays or iterators of file objects and the hash as key
+ * @param string[] $hashes An array of hashes
+ * @return array[] An Array of arrays or iterators of file objects and the hash as key
*/
public function findBySha1s( array $hashes ) {
$result = [];
@@ -596,7 +599,7 @@ class FileRepo {
* STUB
* @param string $prefix The prefix to search for
* @param int $limit The maximum amount of files to return
- * @return array
+ * @return LocalFile[]
*/
public function findFilesByPrefix( $prefix, $limit ) {
return [];
@@ -612,6 +615,24 @@ class FileRepo {
}
/**
+ * Get the URL thumb.php requests are being proxied to
+ *
+ * @return string
+ */
+ public function getThumbProxyUrl() {
+ return $this->thumbProxyUrl;
+ }
+
+ /**
+ * Get the secret key for the proxied thumb service
+ *
+ * @return string
+ */
+ public function getThumbProxySecret() {
+ return $this->thumbProxySecret;
+ }
+
+ /**
* Returns true if the repository can transform files via a 404 handler
*
* @return bool
@@ -719,9 +740,7 @@ class FileRepo {
*/
public function makeUrl( $query = '', $entry = 'index' ) {
if ( isset( $this->scriptDirUrl ) ) {
- $ext = isset( $this->scriptExtension ) ? $this->scriptExtension : '.php';
-
- return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query );
+ return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}.php", $query );
}
return false;
@@ -770,7 +789,7 @@ class FileRepo {
* should use File::getDescriptionText().
*
* @param string $name Name of image to fetch
- * @param string $lang Language to fetch it in, if any.
+ * @param string|null $lang Language to fetch it in, if any.
* @return string|false
*/
public function getDescriptionRenderUrl( $name, $lang = null ) {
@@ -909,7 +928,7 @@ class FileRepo {
* Each file can be a (zone, rel) pair, virtual url, storage path.
* It will try to delete each file, but ignores any errors that may occur.
*
- * @param array $files List of files to delete
+ * @param string[] $files List of files to delete
* @param int $flags Bitwise combination of the following flags:
* self::SKIP_LOCKING Skip any file locking when doing the deletions
* @return Status
@@ -1293,9 +1312,9 @@ class FileRepo {
}
// Cleanup for disk source files...
foreach ( $sourceFSFilesToDelete as $file ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
unlink( $file ); // FS cleanup
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
return $status;
@@ -1359,7 +1378,7 @@ class FileRepo {
/**
* Checks existence of an array of files.
*
- * @param array $files Virtual URLs (or storage paths) of files to check
+ * @param string[] $files Virtual URLs (or storage paths) of files to check
* @return array Map of files and existence flags, or false
*/
public function fileExistsBatch( array $files ) {
@@ -1465,7 +1484,7 @@ class FileRepo {
* Delete files in the deleted directory if they are not referenced in the filearchive table
*
* STUB
- * @param array $storageKeys
+ * @param string[] $storageKeys
*/
public function cleanupDeletedBatch( array $storageKeys ) {
$this->assertWritableRepo();
@@ -1543,7 +1562,7 @@ class FileRepo {
*/
public function getFileProps( $virtualUrl ) {
$fsFile = $this->getLocalReference( $virtualUrl );
- $mwProps = new MWFileProps( MimeMagic::singleton() );
+ $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
if ( $fsFile ) {
$props = $mwProps->getPropsFromPath( $fsFile->getPath(), true );
} else {
@@ -1685,7 +1704,7 @@ class FileRepo {
/**
* Get a callback function to use for cleaning error message parameters
*
- * @return array
+ * @return string[]
*/
function getErrorCleanupFunction() {
switch ( $this->pathDisclosureProtection ) {
@@ -1726,7 +1745,7 @@ class FileRepo {
* @return Status
*/
public function newFatal( $message /*, parameters...*/ ) {
- $status = call_user_func_array( [ 'Status', 'newFatal' ], func_get_args() );
+ $status = call_user_func_array( [ Status::class, 'newFatal' ], func_get_args() );
$status->cleanCallback = $this->getErrorCleanupFunction();
return $status;
@@ -1874,7 +1893,7 @@ class FileRepo {
/**
* Get an UploadStash associated with this repo.
*
- * @param User $user
+ * @param User|null $user
* @return UploadStash
*/
public function getUploadStash( User $user = null ) {
@@ -1907,7 +1926,7 @@ class FileRepo {
$optionalSettings = [
'url', 'thumbUrl', 'initialCapital', 'descBaseUrl', 'scriptDirUrl', 'articleUrl',
- 'fetchDescription', 'descriptionCacheExpiry', 'scriptExtension', 'favicon'
+ 'fetchDescription', 'descriptionCacheExpiry', 'favicon'
];
foreach ( $optionalSettings as $k ) {
if ( isset( $this->$k ) ) {
diff --git a/www/wiki/includes/filerepo/ForeignAPIRepo.php b/www/wiki/includes/filerepo/ForeignAPIRepo.php
index 45a5c824..06b21a8f 100644
--- a/www/wiki/includes/filerepo/ForeignAPIRepo.php
+++ b/www/wiki/includes/filerepo/ForeignAPIRepo.php
@@ -29,7 +29,7 @@ use MediaWiki\Logger\LoggerFactory;
* Example config:
*
* $wgForeignFileRepos[] = [
- * 'class' => 'ForeignAPIRepo',
+ * 'class' => ForeignAPIRepo::class,
* 'name' => 'shared',
* 'apibase' => 'https://en.wikipedia.org/w/api.php',
* 'fetchDescription' => true, // Optional
@@ -53,7 +53,7 @@ class ForeignAPIRepo extends FileRepo {
'timestamp',
];
- protected $fileFactory = [ 'ForeignAPIFile', 'newFromTitle' ];
+ protected $fileFactory = [ ForeignAPIFile::class, 'newFromTitle' ];
/** @var int Check back with Commons after this expiry */
protected $apiThumbCacheExpiry = 86400; // 1 day (24*3600)
@@ -120,7 +120,7 @@ class ForeignAPIRepo extends FileRepo {
}
/**
- * @param array $files
+ * @param string[] $files
* @return array
*/
function fileExistsBatch( array $files ) {
@@ -143,7 +143,7 @@ class ForeignAPIRepo extends FileRepo {
}
$data = $this->fetchImageQuery( [
- 'titles' => implode( $files, '|' ),
+ 'titles' => implode( '|', $files ),
'prop' => 'imageinfo' ]
);
@@ -176,7 +176,7 @@ class ForeignAPIRepo extends FileRepo {
/**
* @param string $virtualUrl
- * @return bool
+ * @return false
*/
function getFileProps( $virtualUrl ) {
return false;
@@ -231,7 +231,7 @@ class ForeignAPIRepo extends FileRepo {
/**
* @param string $hash
- * @return array
+ * @return ForeignAPIFile[]
*/
function findBySha1( $hash ) {
$results = $this->fetchImageQuery( [
@@ -257,10 +257,10 @@ class ForeignAPIRepo extends FileRepo {
* @param string $name
* @param int $width
* @param int $height
- * @param array &$result
+ * @param array|null &$result Output-only parameter, guaranteed to become an array
* @param string $otherParams
*
- * @return bool
+ * @return string|false
*/
function getThumbUrl( $name, $width = -1, $height = -1, &$result = null, $otherParams = '' ) {
$data = $this->fetchImageQuery( [
@@ -287,7 +287,7 @@ class ForeignAPIRepo extends FileRepo {
* @param int $width
* @param int $height
* @param string $otherParams
- * @param string $lang Language code for language of error
+ * @param string|null $lang Language code for language of error
* @return bool|MediaTransformError
* @since 1.22
*/
diff --git a/www/wiki/includes/filerepo/ForeignDBRepo.php b/www/wiki/includes/filerepo/ForeignDBRepo.php
index bce3005c..7879448b 100644
--- a/www/wiki/includes/filerepo/ForeignDBRepo.php
+++ b/www/wiki/includes/filerepo/ForeignDBRepo.php
@@ -58,9 +58,9 @@ class ForeignDBRepo extends LocalRepo {
protected $dbConn;
/** @var callable */
- protected $fileFactory = [ 'ForeignDBFile', 'newFromTitle' ];
+ protected $fileFactory = [ ForeignDBFile::class, 'newFromTitle' ];
/** @var callable */
- protected $fileFromRowFactory = [ 'ForeignDBFile', 'newFromRow' ];
+ protected $fileFromRowFactory = [ ForeignDBFile::class, 'newFromRow' ];
/**
* @param array|null $info
diff --git a/www/wiki/includes/filerepo/ForeignDBViaLBRepo.php b/www/wiki/includes/filerepo/ForeignDBViaLBRepo.php
index bcd253fb..249cd27c 100644
--- a/www/wiki/includes/filerepo/ForeignDBViaLBRepo.php
+++ b/www/wiki/includes/filerepo/ForeignDBViaLBRepo.php
@@ -37,10 +37,10 @@ class ForeignDBViaLBRepo extends LocalRepo {
protected $tablePrefix;
/** @var array */
- protected $fileFactory = [ 'ForeignDBFile', 'newFromTitle' ];
+ protected $fileFactory = [ ForeignDBFile::class, 'newFromTitle' ];
/** @var array */
- protected $fileFromRowFactory = [ 'ForeignDBFile', 'newFromRow' ];
+ protected $fileFromRowFactory = [ ForeignDBFile::class, 'newFromRow' ];
/** @var bool */
protected $hasSharedCache;
diff --git a/www/wiki/includes/filerepo/LocalRepo.php b/www/wiki/includes/filerepo/LocalRepo.php
index ed007935..76043d55 100644
--- a/www/wiki/includes/filerepo/LocalRepo.php
+++ b/www/wiki/includes/filerepo/LocalRepo.php
@@ -34,17 +34,17 @@ use Wikimedia\Rdbms\IDatabase;
*/
class LocalRepo extends FileRepo {
/** @var callable */
- protected $fileFactory = [ 'LocalFile', 'newFromTitle' ];
+ protected $fileFactory = [ LocalFile::class, 'newFromTitle' ];
/** @var callable */
- protected $fileFactoryKey = [ 'LocalFile', 'newFromKey' ];
+ protected $fileFactoryKey = [ LocalFile::class, 'newFromKey' ];
/** @var callable */
- protected $fileFromRowFactory = [ 'LocalFile', 'newFromRow' ];
+ protected $fileFromRowFactory = [ LocalFile::class, 'newFromRow' ];
/** @var callable */
- protected $oldFileFromRowFactory = [ 'OldLocalFile', 'newFromRow' ];
+ protected $oldFileFromRowFactory = [ OldLocalFile::class, 'newFromRow' ];
/** @var callable */
- protected $oldFileFactory = [ 'OldLocalFile', 'newFromTitle' ];
+ protected $oldFileFactory = [ OldLocalFile::class, 'newFromTitle' ];
/** @var callable */
- protected $oldFileFactoryKey = [ 'OldLocalFile', 'newFromKey' ];
+ protected $oldFileFactoryKey = [ OldLocalFile::class, 'newFromKey' ];
function __construct( array $info = null ) {
parent::__construct( $info );
@@ -91,7 +91,7 @@ class LocalRepo extends FileRepo {
* interleave database locks with file operations, which is potentially a
* remote operation.
*
- * @param array $storageKeys
+ * @param string[] $storageKeys
*
* @return Status
*/
@@ -310,8 +310,9 @@ class LocalRepo extends FileRepo {
}
if ( count( $imgNames ) ) {
- $res = $dbr->select( 'image',
- LocalFile::selectFields(), [ 'img_name' => $imgNames ], __METHOD__ );
+ $fileQuery = LocalFile::getQueryInfo();
+ $res = $dbr->select( $fileQuery['tables'], $fileQuery['fields'], [ 'img_name' => $imgNames ],
+ __METHOD__, [], $fileQuery['joins'] );
$applyMatchingFiles( $res, $searchSet, $finalFiles );
}
@@ -330,8 +331,10 @@ class LocalRepo extends FileRepo {
}
if ( count( $oiConds ) ) {
- $res = $dbr->select( 'oldimage',
- OldLocalFile::selectFields(), $dbr->makeList( $oiConds, LIST_OR ), __METHOD__ );
+ $fileQuery = OldLocalFile::getQueryInfo();
+ $res = $dbr->select( $fileQuery['tables'], $fileQuery['fields'],
+ $dbr->makeList( $oiConds, LIST_OR ),
+ __METHOD__, [], $fileQuery['joins'] );
$applyMatchingFiles( $res, $searchSet, $finalFiles );
}
@@ -368,16 +371,18 @@ class LocalRepo extends FileRepo {
* SHA-1 content hash.
*
* @param string $hash A sha1 hash to look for
- * @return File[]
+ * @return LocalFile[]
*/
function findBySha1( $hash ) {
$dbr = $this->getReplicaDB();
+ $fileQuery = LocalFile::getQueryInfo();
$res = $dbr->select(
- 'image',
- LocalFile::selectFields(),
+ $fileQuery['tables'],
+ $fileQuery['fields'],
[ 'img_sha1' => $hash ],
__METHOD__,
- [ 'ORDER BY' => 'img_name' ]
+ [ 'ORDER BY' => 'img_name' ],
+ $fileQuery['joins']
);
$result = [];
@@ -395,8 +400,8 @@ class LocalRepo extends FileRepo {
*
* Overrides generic implementation in FileRepo for performance reason
*
- * @param array $hashes An array of hashes
- * @return array An Array of arrays or iterators of file objects and the hash as key
+ * @param string[] $hashes An array of hashes
+ * @return array[] An Array of arrays or iterators of file objects and the hash as key
*/
function findBySha1s( array $hashes ) {
if ( !count( $hashes ) ) {
@@ -404,12 +409,14 @@ class LocalRepo extends FileRepo {
}
$dbr = $this->getReplicaDB();
+ $fileQuery = LocalFile::getQueryInfo();
$res = $dbr->select(
- 'image',
- LocalFile::selectFields(),
+ $fileQuery['tables'],
+ $fileQuery['fields'],
[ 'img_sha1' => $hashes ],
__METHOD__,
- [ 'ORDER BY' => 'img_name' ]
+ [ 'ORDER BY' => 'img_name' ],
+ $fileQuery['joins']
);
$result = [];
@@ -427,19 +434,21 @@ class LocalRepo extends FileRepo {
*
* @param string $prefix The prefix to search for
* @param int $limit The maximum amount of files to return
- * @return array
+ * @return LocalFile[]
*/
public function findFilesByPrefix( $prefix, $limit ) {
$selectOptions = [ 'ORDER BY' => 'img_name', 'LIMIT' => intval( $limit ) ];
// Query database
$dbr = $this->getReplicaDB();
+ $fileQuery = LocalFile::getQueryInfo();
$res = $dbr->select(
- 'image',
- LocalFile::selectFields(),
+ $fileQuery['tables'],
+ $fileQuery['fields'],
'img_name ' . $dbr->buildLike( $prefix, $dbr->anyString() ),
__METHOD__,
- $selectOptions
+ $selectOptions,
+ $fileQuery['joins']
);
// Build file objects
diff --git a/www/wiki/includes/filerepo/RepoGroup.php b/www/wiki/includes/filerepo/RepoGroup.php
index 2edd6d09..b7977900 100644
--- a/www/wiki/includes/filerepo/RepoGroup.php
+++ b/www/wiki/includes/filerepo/RepoGroup.php
@@ -138,7 +138,7 @@ class RepoGroup {
$dbkey = $title->getDBkey();
if ( empty( $options['ignoreRedirect'] )
&& empty( $options['private'] )
- && empty( $options['bypassCache'] )
+ && empty( $options['latest'] )
) {
$time = isset( $options['time'] ) ? $options['time'] : '';
if ( $this->cache->has( $dbkey, $time, 60 ) ) {
@@ -423,7 +423,7 @@ class RepoGroup {
* Split a virtual URL into repo, zone and rel parts
* @param string $url
* @throws MWException
- * @return array Containing repo, zone and rel
+ * @return string[] Containing repo, zone and rel
*/
function splitVirtualUrl( $url ) {
if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
@@ -452,7 +452,7 @@ class RepoGroup {
return $repo->getFileProps( $fileName );
} else {
- $mwProps = new MWFileProps( MimeMagic::singleton() );
+ $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
return $mwProps->getPropsFromPath( $fileName, true );
}
diff --git a/www/wiki/includes/filerepo/file/ArchivedFile.php b/www/wiki/includes/filerepo/file/ArchivedFile.php
index 758fb4b5..c4edcd1a 100644
--- a/www/wiki/includes/filerepo/file/ArchivedFile.php
+++ b/www/wiki/includes/filerepo/file/ArchivedFile.php
@@ -63,12 +63,9 @@ class ArchivedFile {
/** @var string Upload description */
private $description;
- /** @var int User ID of uploader */
+ /** @var User|null Uploader */
private $user;
- /** @var string User name of uploader */
- private $user_text;
-
/** @var string Time of upload */
private $timestamp;
@@ -116,8 +113,7 @@ class ArchivedFile {
$this->mime = "unknown/unknown";
$this->media_type = '';
$this->description = '';
- $this->user = 0;
- $this->user_text = '';
+ $this->user = null;
$this->timestamp = null;
$this->deleted = 0;
$this->dataLoaded = false;
@@ -178,12 +174,14 @@ class ArchivedFile {
if ( !$this->title || $this->title->getNamespace() == NS_FILE ) {
$this->dataLoaded = true; // set it here, to have also true on miss
$dbr = wfGetDB( DB_REPLICA );
+ $fileQuery = self::getQueryInfo();
$row = $dbr->selectRow(
- 'filearchive',
- self::selectFields(),
+ $fileQuery['tables'],
+ $fileQuery['fields'],
$conds,
__METHOD__,
- [ 'ORDER BY' => 'fa_timestamp DESC' ]
+ [ 'ORDER BY' => 'fa_timestamp DESC' ],
+ $fileQuery['joins']
);
if ( !$row ) {
// this revision does not exist?
@@ -215,11 +213,23 @@ class ArchivedFile {
/**
* Fields in the filearchive table
- * @todo Deprecate this in favor of a method that returns tables and joins
- * as well, and use CommentStore::getJoin().
- * @return array
+ * @deprecated since 1.31, use self::getQueryInfo() instead.
+ * @return string[]
*/
static function selectFields() {
+ global $wgActorTableSchemaMigrationStage;
+
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+ // If code is using this instead of self::getQueryInfo(), there's a
+ // decent chance it's going to try to directly access
+ // $row->fa_user or $row->fa_user_text and we can't give it
+ // useful values here once those aren't being written anymore.
+ throw new BadMethodCallException(
+ 'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+ );
+ }
+
+ wfDeprecated( __METHOD__, '1.31' );
return [
'fa_id',
'fa_name',
@@ -236,11 +246,49 @@ class ArchivedFile {
'fa_minor_mime',
'fa_user',
'fa_user_text',
+ 'fa_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'fa_actor' : 'NULL',
'fa_timestamp',
'fa_deleted',
'fa_deleted_timestamp', /* Used by LocalFileRestoreBatch */
'fa_sha1',
- ] + CommentStore::newKey( 'fa_description' )->getFields();
+ ] + CommentStore::getStore()->getFields( 'fa_description' );
+ }
+
+ /**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new archivedfile object.
+ * @since 1.31
+ * @return array[] With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ */
+ public static function getQueryInfo() {
+ $commentQuery = CommentStore::getStore()->getJoin( 'fa_description' );
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'fa_user' );
+ return [
+ 'tables' => [ 'filearchive' ] + $commentQuery['tables'] + $actorQuery['tables'],
+ 'fields' => [
+ 'fa_id',
+ 'fa_name',
+ 'fa_archive_name',
+ 'fa_storage_key',
+ 'fa_storage_group',
+ 'fa_size',
+ 'fa_bits',
+ 'fa_width',
+ 'fa_height',
+ 'fa_metadata',
+ 'fa_media_type',
+ 'fa_major_mime',
+ 'fa_minor_mime',
+ 'fa_timestamp',
+ 'fa_deleted',
+ 'fa_deleted_timestamp', /* Used by LocalFileRestoreBatch */
+ 'fa_sha1',
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
+ 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
+ ];
}
/**
@@ -262,11 +310,10 @@ class ArchivedFile {
$this->metadata = $row->fa_metadata;
$this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
$this->media_type = $row->fa_media_type;
- $this->description = CommentStore::newKey( 'fa_description' )
- // Legacy because $row probably came from self::selectFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row )->text;
- $this->user = $row->fa_user;
- $this->user_text = $row->fa_user_text;
+ $this->description = CommentStore::getStore()
+ // Legacy because $row may have come from self::selectFields()
+ ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'fa_description', $row )->text;
+ $this->user = User::newFromAnyId( $row->fa_user, $row->fa_user_text, $row->fa_actor );
$this->timestamp = $row->fa_timestamp;
$this->deleted = $row->fa_deleted;
if ( isset( $row->fa_sha1 ) ) {
@@ -479,17 +526,20 @@ class ArchivedFile {
* @note Prior to MediaWiki 1.23, this method always
* returned the user id, and was inconsistent with
* the rest of the file classes.
- * @param string $type 'text' or 'id'
- * @return int|string
+ * @param string $type 'text', 'id', or 'object'
+ * @return int|string|User|null
* @throws MWException
+ * @since 1.31 added 'object'
*/
public function getUser( $type = 'text' ) {
$this->load();
- if ( $type == 'text' ) {
- return $this->user_text;
- } elseif ( $type == 'id' ) {
- return (int)$this->user;
+ if ( $type === 'object' ) {
+ return $this->user;
+ } elseif ( $type === 'text' ) {
+ return $this->user ? $this->user->getName() : '';
+ } elseif ( $type === 'id' ) {
+ return $this->user ? $this->user->getId() : 0;
}
throw new MWException( "Unknown type '$type'." );
@@ -515,9 +565,7 @@ class ArchivedFile {
* @return int
*/
public function getRawUser() {
- $this->load();
-
- return $this->user;
+ return $this->getUser( 'id' );
}
/**
@@ -526,9 +574,7 @@ class ArchivedFile {
* @return string
*/
public function getRawUserText() {
- $this->load();
-
- return $this->user_text;
+ return $this->getUser( 'text' );
}
/**
diff --git a/www/wiki/includes/filerepo/file/File.php b/www/wiki/includes/filerepo/file/File.php
index 32f4504b..cfd1cf25 100644
--- a/www/wiki/includes/filerepo/file/File.php
+++ b/www/wiki/includes/filerepo/file/File.php
@@ -148,7 +148,7 @@ abstract class File implements IDBAccessObject {
protected $isSafeFile;
/** @var string Required Repository class type */
- protected $repoClass = 'FileRepo';
+ protected $repoClass = FileRepo::class;
/** @var array Cache of tmp filepaths pointing to generated bucket thumbnails, keyed by width */
protected $tmpBucketedThumbCache = [];
@@ -250,7 +250,7 @@ abstract class File implements IDBAccessObject {
$oldMime = $old->getMimeType();
$n = strrpos( $new, '.' );
$newExt = self::normalizeExtension( $n ? substr( $new, $n + 1 ) : '' );
- $mimeMagic = MimeMagic::singleton();
+ $mimeMagic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
return $mimeMagic->isMatchingExtension( $newExt, $oldMime );
}
@@ -268,7 +268,7 @@ abstract class File implements IDBAccessObject {
* a two-part name, set the minor type to 'unknown'.
*
* @param string $mime "text/html" etc
- * @return array ("text", "html") etc
+ * @return string[] ("text", "html") etc
*/
public static function splitMime( $mime ) {
if ( strpos( $mime, '/' ) !== false ) {
@@ -569,7 +569,7 @@ abstract class File implements IDBAccessObject {
* format does not support that sort of thing, returns
* an empty array.
*
- * @return array
+ * @return string[]
* @since 1.23
*/
public function getAvailableLanguages() {
@@ -582,6 +582,25 @@ abstract class File implements IDBAccessObject {
}
/**
+ * Get the language code from the available languages for this file that matches the language
+ * requested by the user
+ *
+ * @param string $userPreferredLanguage
+ * @return string|null
+ */
+ public function getMatchedLanguage( $userPreferredLanguage ) {
+ $handler = $this->getHandler();
+ if ( $handler && method_exists( $handler, 'getMatchedLanguage' ) ) {
+ return $handler->getMatchedLanguage(
+ $userPreferredLanguage,
+ $handler->getAvailableLanguages( $this )
+ );
+ } else {
+ return null;
+ }
+ }
+
+ /**
* In files that support multiple language, what is the default language
* to use if none specified.
*
@@ -1404,7 +1423,7 @@ abstract class File implements IDBAccessObject {
* Get all thumbnail names previously generated for this file
* STUB
* Overridden by LocalFile
- * @return array
+ * @return string[]
*/
function getThumbnails() {
return [];
@@ -1445,7 +1464,9 @@ abstract class File implements IDBAccessObject {
// Purge cache of all pages using this file
$title = $this->getTitle();
if ( $title ) {
- DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
+ DeferredUpdates::addUpdate(
+ new HTMLCacheUpdate( $title, 'imagelinks', 'file-purge' )
+ );
}
}
@@ -1453,9 +1474,9 @@ abstract class File implements IDBAccessObject {
* Return a fragment of the history of file.
*
* STUB
- * @param int $limit Limit of rows to return
- * @param string $start Only revisions older than $start will be returned
- * @param string $end Only revisions newer than $end will be returned
+ * @param int|null $limit Limit of rows to return
+ * @param string|int|null $start Only revisions older than $start will be returned
+ * @param string|int|null $end Only revisions newer than $end will be returned
* @param bool $inc Include the endpoints of the time range
*
* @return File[]
@@ -2078,9 +2099,9 @@ abstract class File implements IDBAccessObject {
* File::FOR_PUBLIC to be displayed to all users
* File::FOR_THIS_USER to be displayed to the given user
* File::RAW get the description regardless of permissions
- * @param User $user User object to check for, only if FOR_THIS_USER is
+ * @param User|null $user User object to check for, only if FOR_THIS_USER is
* passed to the $audience parameter
- * @return string
+ * @return null|string
*/
function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
return null;
@@ -2140,7 +2161,7 @@ abstract class File implements IDBAccessObject {
* field of this file, if it's marked as deleted.
* STUB
* @param int $field
- * @param User $user User object to check, or null to use $wgUser
+ * @param User|null $user User object to check, or null to use $wgUser
* @return bool
*/
function userCan( $field, User $user = null ) {
@@ -2156,7 +2177,7 @@ abstract class File implements IDBAccessObject {
}
/**
- * @return array HTTP header name/value map to use for HEAD/GET request responses
+ * @return string[] HTTP header name/value map to use for HEAD/GET request responses
* @since 1.30
*/
function getContentHeaders() {
@@ -2165,7 +2186,7 @@ abstract class File implements IDBAccessObject {
$metadata = $this->getMetadata();
if ( is_string( $metadata ) ) {
- $metadata = MediaWiki\quietCall( 'unserialize', $metadata );
+ $metadata = Wikimedia\quietCall( 'unserialize', $metadata );
}
if ( !is_array( $metadata ) ) {
diff --git a/www/wiki/includes/filerepo/file/ForeignAPIFile.php b/www/wiki/includes/filerepo/file/ForeignAPIFile.php
index 43b6855f..be88b49a 100644
--- a/www/wiki/includes/filerepo/file/ForeignAPIFile.php
+++ b/www/wiki/includes/filerepo/file/ForeignAPIFile.php
@@ -33,7 +33,7 @@ class ForeignAPIFile extends File {
/** @var array */
private $mInfo = [];
- protected $repoClass = 'ForeignApiRepo';
+ protected $repoClass = ForeignApiRepo::class;
/**
* @param Title|string|bool $title
@@ -192,8 +192,8 @@ class ForeignAPIFile extends File {
}
/**
- * @param array $metadata
- * @return array
+ * @param mixed $metadata
+ * @return mixed
*/
public static function parseMetadata( $metadata ) {
if ( !is_array( $metadata ) ) {
@@ -254,7 +254,7 @@ class ForeignAPIFile extends File {
/**
* @param int $audience
- * @param User $user
+ * @param User|null $user
* @return null|string
*/
public function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
@@ -286,7 +286,7 @@ class ForeignAPIFile extends File {
*/
function getMimeType() {
if ( !isset( $this->mInfo['mime'] ) ) {
- $magic = MimeMagic::singleton();
+ $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
$this->mInfo['mime'] = $magic->guessTypesForExtension( $this->getExtension() );
}
@@ -300,7 +300,7 @@ class ForeignAPIFile extends File {
if ( isset( $this->mInfo['mediatype'] ) ) {
return $this->mInfo['mediatype'];
}
- $magic = MimeMagic::singleton();
+ $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
return $magic->getMediaType( null, $this->getMimeType() );
}
@@ -333,15 +333,17 @@ class ForeignAPIFile extends File {
}
/**
- * @return array
+ * @return string[]
*/
function getThumbnails() {
$dir = $this->getThumbPath( $this->getName() );
$iter = $this->repo->getBackend()->getFileList( [ 'dir' => $dir ] );
$files = [];
- foreach ( $iter as $file ) {
- $files[] = $file;
+ if ( $iter ) {
+ foreach ( $iter as $file ) {
+ $files[] = $file;
+ }
}
return $files;
diff --git a/www/wiki/includes/filerepo/file/ForeignDBFile.php b/www/wiki/includes/filerepo/file/ForeignDBFile.php
index cf211618..388e9503 100644
--- a/www/wiki/includes/filerepo/file/ForeignDBFile.php
+++ b/www/wiki/includes/filerepo/file/ForeignDBFile.php
@@ -74,7 +74,7 @@ class ForeignDBFile extends LocalFile {
* @param string $source
* @param bool $watch
* @param bool|string $timestamp
- * @param User $user User object or null to use $wgUser
+ * @param User|null $user User object or null to use $wgUser
* @return bool
* @throws MWException
*/
diff --git a/www/wiki/includes/filerepo/file/LocalFile.php b/www/wiki/includes/filerepo/file/LocalFile.php
index 188e2ed9..90a984af 100644
--- a/www/wiki/includes/filerepo/file/LocalFile.php
+++ b/www/wiki/includes/filerepo/file/LocalFile.php
@@ -24,6 +24,7 @@
use MediaWiki\Logger\LoggerFactory;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\MediaWikiServices;
/**
* Class to represent a local file in the wiki's own database
@@ -43,7 +44,7 @@ use Wikimedia\Rdbms\IDatabase;
* @ingroup FileAbstraction
*/
class LocalFile extends File {
- const VERSION = 10; // cache version
+ const VERSION = 11; // cache version
const CACHE_FIELD_MAX_LEN = 1000;
@@ -84,7 +85,7 @@ class LocalFile extends File {
protected $deleted;
/** @var string */
- protected $repoClass = 'LocalRepo';
+ protected $repoClass = LocalRepo::class;
/** @var int Number of line to return by nextHistoryLine() (constructor) */
private $historyLine;
@@ -101,12 +102,9 @@ class LocalFile extends File {
/** @var string Upload timestamp */
private $timestamp;
- /** @var int User ID of uploader */
+ /** @var User Uploader */
private $user;
- /** @var string User name of uploader */
- private $user_text;
-
/** @var string Description of current revision of the file */
private $description;
@@ -143,7 +141,7 @@ class LocalFile extends File {
* @param FileRepo $repo
* @param null $unused
*
- * @return LocalFile
+ * @return self
*/
static function newFromTitle( $title, $repo, $unused = null ) {
return new self( $title, $repo );
@@ -156,7 +154,7 @@ class LocalFile extends File {
* @param stdClass $row
* @param FileRepo $repo
*
- * @return LocalFile
+ * @return self
*/
static function newFromRow( $row, $repo ) {
$title = Title::makeTitle( NS_FILE, $row->img_name );
@@ -183,7 +181,10 @@ class LocalFile extends File {
$conds['img_timestamp'] = $dbr->timestamp( $timestamp );
}
- $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
+ $fileQuery = self::getQueryInfo();
+ $row = $dbr->selectRow(
+ $fileQuery['tables'], $fileQuery['fields'], $conds, __METHOD__, [], $fileQuery['joins']
+ );
if ( $row ) {
return self::newFromRow( $row, $repo );
} else {
@@ -193,11 +194,23 @@ class LocalFile extends File {
/**
* Fields in the image table
- * @todo Deprecate this in favor of a method that returns tables and joins
- * as well, and use CommentStore::getJoin().
- * @return array
+ * @deprecated since 1.31, use self::getQueryInfo() instead.
+ * @return string[]
*/
static function selectFields() {
+ global $wgActorTableSchemaMigrationStage;
+
+ wfDeprecated( __METHOD__, '1.31' );
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+ // If code is using this instead of self::getQueryInfo(), there's a
+ // decent chance it's going to try to directly access
+ // $row->img_user or $row->img_user_text and we can't give it
+ // useful values here once those aren't being written anymore.
+ throw new BadMethodCallException(
+ 'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+ );
+ }
+
return [
'img_name',
'img_size',
@@ -210,9 +223,54 @@ class LocalFile extends File {
'img_minor_mime',
'img_user',
'img_user_text',
+ 'img_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'img_actor' : 'NULL',
'img_timestamp',
'img_sha1',
- ] + CommentStore::newKey( 'img_description' )->getFields();
+ ] + CommentStore::getStore()->getFields( 'img_description' );
+ }
+
+ /**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new localfile object.
+ * @since 1.31
+ * @param string[] $options
+ * - omit-lazy: Omit fields that are lazily cached.
+ * @return array[] With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ */
+ public static function getQueryInfo( array $options = [] ) {
+ $commentQuery = CommentStore::getStore()->getJoin( 'img_description' );
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'img_user' );
+ $ret = [
+ 'tables' => [ 'image' ] + $commentQuery['tables'] + $actorQuery['tables'],
+ 'fields' => [
+ 'img_name',
+ 'img_size',
+ 'img_width',
+ 'img_height',
+ 'img_metadata',
+ 'img_bits',
+ 'img_media_type',
+ 'img_major_mime',
+ 'img_minor_mime',
+ 'img_timestamp',
+ 'img_sha1',
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
+ 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
+ ];
+
+ if ( in_array( 'omit-nonlazy', $options, true ) ) {
+ // Internal use only for getting only the lazy fields
+ $ret['fields'] = [];
+ }
+ if ( !in_array( 'omit-lazy', $options, true ) ) {
+ // Note: Keep this in sync with self::getLazyCacheFields()
+ $ret['fields'][] = 'img_metadata';
+ }
+
+ return $ret;
}
/**
@@ -281,6 +339,10 @@ class LocalFile extends File {
$cacheVal[$field] = $this->$field;
}
}
+ $cacheVal['user'] = $this->user ? $this->user->getId() : 0;
+ $cacheVal['user_text'] = $this->user ? $this->user->getName() : '';
+ $cacheVal['actor'] = $this->user ? $this->user->getActorId() : null;
+
// Strip off excessive entries from the subset of fields that can become large.
// If the cache value gets to large it will not fit in memcached and nothing will
// get cached at all, causing master queries for any file access.
@@ -341,51 +403,42 @@ class LocalFile extends File {
}
/**
- * @param string $prefix
- * @return array
+ * Returns the list of object properties that are included as-is in the cache.
+ * @param string $prefix Must be the empty string
+ * @return string[]
+ * @since 1.31 No longer accepts a non-empty $prefix
*/
- function getCacheFields( $prefix = 'img_' ) {
- static $fields = [ 'size', 'width', 'height', 'bits', 'media_type',
- 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
- 'user_text' ];
- static $results = [];
-
- if ( $prefix == '' ) {
- return array_merge( $fields, [ 'description' ] );
- }
- if ( !isset( $results[$prefix] ) ) {
- $prefixedFields = [];
- foreach ( $fields as $field ) {
- $prefixedFields[] = $prefix . $field;
- }
- $prefixedFields += CommentStore::newKey( "{$prefix}description" )->getFields();
- $results[$prefix] = $prefixedFields;
+ protected function getCacheFields( $prefix = 'img_' ) {
+ if ( $prefix !== '' ) {
+ throw new InvalidArgumentException(
+ __METHOD__ . ' with a non-empty prefix is no longer supported.'
+ );
}
- return $results[$prefix];
+ // See self::getQueryInfo() for the fetching of the data from the DB,
+ // self::loadFromRow() for the loading of the object from the DB row,
+ // and self::loadFromCache() for the caching, and self::setProps() for
+ // populating the object from an array of data.
+ return [ 'size', 'width', 'height', 'bits', 'media_type',
+ 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'description' ];
}
/**
- * @param string $prefix
- * @return array
+ * Returns the list of object properties that are included as-is in the
+ * cache, only when they're not too big, and are lazily loaded by self::loadExtraFromDB().
+ * @param string $prefix Must be the empty string
+ * @return string[]
+ * @since 1.31 No longer accepts a non-empty $prefix
*/
- function getLazyCacheFields( $prefix = 'img_' ) {
- static $fields = [ 'metadata' ];
- static $results = [];
-
- if ( $prefix == '' ) {
- return $fields;
- }
-
- if ( !isset( $results[$prefix] ) ) {
- $prefixedFields = [];
- foreach ( $fields as $field ) {
- $prefixedFields[] = $prefix . $field;
- }
- $results[$prefix] = $prefixedFields;
+ protected function getLazyCacheFields( $prefix = 'img_' ) {
+ if ( $prefix !== '' ) {
+ throw new InvalidArgumentException(
+ __METHOD__ . ' with a non-empty prefix is no longer supported.'
+ );
}
- return $results[$prefix];
+ // Keep this in sync with the omit-lazy option in self::getQueryInfo().
+ return [ 'metadata' ];
}
/**
@@ -403,8 +456,15 @@ class LocalFile extends File {
? $this->repo->getMasterDB()
: $this->repo->getReplicaDB();
- $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
- [ 'img_name' => $this->getName() ], $fname );
+ $fileQuery = static::getQueryInfo();
+ $row = $dbr->selectRow(
+ $fileQuery['tables'],
+ $fileQuery['fields'],
+ [ 'img_name' => $this->getName() ],
+ $fname,
+ [],
+ $fileQuery['joins']
+ );
if ( $row ) {
$this->loadFromRow( $row );
@@ -423,9 +483,9 @@ class LocalFile extends File {
# Unconditionally set loaded=true, we don't want the accessors constantly rechecking
$this->extraDataLoaded = true;
- $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getReplicaDB(), $fname );
+ $fieldMap = $this->loadExtraFieldsWithTimestamp( $this->repo->getReplicaDB(), $fname );
if ( !$fieldMap ) {
- $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getMasterDB(), $fname );
+ $fieldMap = $this->loadExtraFieldsWithTimestamp( $this->repo->getMasterDB(), $fname );
}
if ( $fieldMap ) {
@@ -440,28 +500,48 @@ class LocalFile extends File {
/**
* @param IDatabase $dbr
* @param string $fname
- * @return array|bool
+ * @return string[]|bool
*/
- private function loadFieldsWithTimestamp( $dbr, $fname ) {
+ private function loadExtraFieldsWithTimestamp( $dbr, $fname ) {
$fieldMap = false;
- $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ), [
+ $fileQuery = self::getQueryInfo( [ 'omit-nonlazy' ] );
+ $row = $dbr->selectRow(
+ $fileQuery['tables'],
+ $fileQuery['fields'],
+ [
'img_name' => $this->getName(),
- 'img_timestamp' => $dbr->timestamp( $this->getTimestamp() )
- ], $fname );
+ 'img_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
+ ],
+ $fname,
+ [],
+ $fileQuery['joins']
+ );
if ( $row ) {
$fieldMap = $this->unprefixRow( $row, 'img_' );
} else {
# File may have been uploaded over in the meantime; check the old versions
- $row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ), [
+ $fileQuery = OldLocalFile::getQueryInfo( [ 'omit-nonlazy' ] );
+ $row = $dbr->selectRow(
+ $fileQuery['tables'],
+ $fileQuery['fields'],
+ [
'oi_name' => $this->getName(),
- 'oi_timestamp' => $dbr->timestamp( $this->getTimestamp() )
- ], $fname );
+ 'oi_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
+ ],
+ $fname,
+ [],
+ $fileQuery['joins']
+ );
if ( $row ) {
$fieldMap = $this->unprefixRow( $row, 'oi_' );
}
}
+ if ( isset( $fieldMap['metadata'] ) ) {
+ $fieldMap['metadata'] = $this->repo->getReplicaDB()->decodeBlob( $fieldMap['metadata'] );
+ }
+
return $fieldMap;
}
@@ -499,6 +579,16 @@ class LocalFile extends File {
function decodeRow( $row, $prefix = 'img_' ) {
$decoded = $this->unprefixRow( $row, $prefix );
+ $decoded['description'] = CommentStore::getStore()
+ ->getComment( 'description', (object)$decoded )->text;
+
+ $decoded['user'] = User::newFromAnyId(
+ isset( $decoded['user'] ) ? $decoded['user'] : null,
+ isset( $decoded['user_text'] ) ? $decoded['user_text'] : null,
+ isset( $decoded['actor'] ) ? $decoded['actor'] : null
+ );
+ unset( $decoded['user_text'], $decoded['actor'] );
+
$decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
$decoded['metadata'] = $this->repo->getReplicaDB()->decodeBlob( $decoded['metadata'] );
@@ -536,10 +626,6 @@ class LocalFile extends File {
$this->dataLoaded = true;
$this->extraDataLoaded = true;
- $this->description = CommentStore::newKey( "{$prefix}description" )
- // $row is probably using getFields() from self::getCacheFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row )->text;
-
$array = $this->decodeRow( $row, $prefix );
foreach ( $array as $name => $value ) {
@@ -684,6 +770,14 @@ class LocalFile extends File {
}
}
+ if ( isset( $info['user'] ) || isset( $info['user_text'] ) || isset( $info['actor'] ) ) {
+ $this->user = User::newFromAnyId(
+ isset( $info['user'] ) ? $info['user'] : null,
+ isset( $info['user_text'] ) ? $info['user_text'] : null,
+ isset( $info['actor'] ) ? $info['actor'] : null
+ );
+ }
+
// Fix up mime fields
if ( isset( $info['major_mime'] ) ) {
$this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
@@ -778,19 +872,24 @@ class LocalFile extends File {
}
/**
- * Returns ID or name of user who uploaded the file
+ * Returns user who uploaded the file
*
- * @param string $type 'text' or 'id'
- * @return int|string
+ * @param string $type 'text', 'id', or 'object'
+ * @return int|string|User
+ * @since 1.31 Added 'object'
*/
function getUser( $type = 'text' ) {
$this->load();
- if ( $type == 'text' ) {
- return $this->user_text;
- } else { // id
- return (int)$this->user;
+ if ( $type === 'object' ) {
+ return $this->user;
+ } elseif ( $type === 'text' ) {
+ return $this->user->getName();
+ } elseif ( $type === 'id' ) {
+ return $this->user->getId();
}
+
+ throw new MWException( "Unknown type '$type'." );
}
/**
@@ -1061,17 +1160,20 @@ class LocalFile extends File {
/** purgeEverything inherited */
/**
- * @param int $limit Optional: Limit to number of results
- * @param int $start Optional: Timestamp, start from
- * @param int $end Optional: Timestamp, end at
+ * @param int|null $limit Optional: Limit to number of results
+ * @param string|int|null $start Optional: Timestamp, start from
+ * @param string|int|null $end Optional: Timestamp, end at
* @param bool $inc
* @return OldLocalFile[]
*/
function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
$dbr = $this->repo->getReplicaDB();
- $tables = [ 'oldimage' ];
- $fields = OldLocalFile::selectFields();
- $conds = $opts = $join_conds = [];
+ $oldFileQuery = OldLocalFile::getQueryInfo();
+
+ $tables = $oldFileQuery['tables'];
+ $fields = $oldFileQuery['fields'];
+ $join_conds = $oldFileQuery['joins'];
+ $conds = $opts = [];
$eq = $inc ? '=' : '';
$conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
@@ -1127,13 +1229,16 @@ class LocalFile extends File {
$dbr = $this->repo->getReplicaDB();
if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
- $this->historyRes = $dbr->select( 'image',
- self::selectFields() + [
+ $fileQuery = self::getQueryInfo();
+ $this->historyRes = $dbr->select( $fileQuery['tables'],
+ $fileQuery['fields'] + [
'oi_archive_name' => $dbr->addQuotes( '' ),
'oi_deleted' => 0,
],
[ 'img_name' => $this->title->getDBkey() ],
- $fname
+ $fname,
+ [],
+ $fileQuery['joins']
);
if ( 0 == $dbr->numRows( $this->historyRes ) ) {
@@ -1142,12 +1247,14 @@ class LocalFile extends File {
return false;
}
} elseif ( $this->historyLine == 1 ) {
+ $fileQuery = OldLocalFile::getQueryInfo();
$this->historyRes = $dbr->select(
- 'oldimage',
- OldLocalFile::selectFields(),
+ $fileQuery['tables'],
+ $fileQuery['fields'],
[ 'oi_name' => $this->title->getDBkey() ],
$fname,
- [ 'ORDER BY' => 'oi_timestamp DESC' ]
+ [ 'ORDER BY' => 'oi_timestamp DESC' ],
+ $fileQuery['joins']
);
}
$this->historyLine++;
@@ -1201,6 +1308,10 @@ class LocalFile extends File {
) {
if ( $this->getRepo()->getReadOnlyReason() !== false ) {
return $this->readOnlyFatalStatus();
+ } elseif ( MediaWikiServices::getInstance()->getRevisionStore()->isReadOnly() ) {
+ // Check this in advance to avoid writing to FileBackend and the file tables,
+ // only to fail on insert the revision due to the text store being unavailable.
+ return $this->readOnlyFatalStatus();
}
$srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
@@ -1210,7 +1321,7 @@ class LocalFile extends File {
) {
$props = $this->repo->getFileProps( $srcPath );
} else {
- $mwProps = new MWFileProps( MimeMagic::singleton() );
+ $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
$props = $mwProps->getPropsFromPath( $srcPath, true );
}
}
@@ -1218,7 +1329,7 @@ class LocalFile extends File {
$options = [];
$handler = MediaHandler::getHandler( $props['mime'] );
if ( $handler ) {
- $metadata = MediaWiki\quietCall( 'unserialize', $props['metadata'] );
+ $metadata = Wikimedia\quietCall( 'unserialize', $props['metadata'] );
if ( !is_array( $metadata ) ) {
$metadata = [];
@@ -1243,8 +1354,22 @@ class LocalFile extends File {
// Once the second operation goes through, then the current version was
// updated and we must therefore update the DB too.
$oldver = $status->value;
- if ( !$this->recordUpload2( $oldver, $comment, $pageText, $props, $timestamp, $user, $tags ) ) {
- $status->fatal( 'filenotfound', $srcPath );
+ $uploadStatus = $this->recordUpload2(
+ $oldver,
+ $comment,
+ $pageText,
+ $props,
+ $timestamp,
+ $user,
+ $tags
+ );
+ if ( !$uploadStatus->isOK() ) {
+ if ( $uploadStatus->hasMessage( 'filenotfound' ) ) {
+ // update filenotfound error with more specific path
+ $status->fatal( 'filenotfound', $srcPath );
+ } else {
+ $status->merge( $uploadStatus );
+ }
}
}
@@ -1274,7 +1399,7 @@ class LocalFile extends File {
$pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
- if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
+ if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user )->isOK() ) {
return false;
}
@@ -1294,12 +1419,12 @@ class LocalFile extends File {
* @param string|bool $timestamp
* @param null|User $user
* @param string[] $tags
- * @return bool
+ * @return Status
*/
function recordUpload2(
$oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = []
) {
- global $wgCommentTableSchemaMigrationStage;
+ global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage;
if ( is_null( $user ) ) {
global $wgUser;
@@ -1321,6 +1446,7 @@ class LocalFile extends File {
$props['description'] = $comment;
$props['user'] = $user->getId();
$props['user_text'] = $user->getName();
+ $props['actor'] = $user->getActorId( $dbw );
$props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
$this->setProps( $props );
@@ -1328,7 +1454,7 @@ class LocalFile extends File {
if ( !$this->fileExists ) {
wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
- return false;
+ return Status::newFatal( 'filenotfound', $this->getRel() );
}
$dbw->startAtomic( __METHOD__ );
@@ -1336,9 +1462,11 @@ class LocalFile extends File {
# Test to see if the row exists using INSERT IGNORE
# This avoids race conditions by locking the row until the commit, and also
# doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
- $commentStore = new CommentStore( 'img_description' );
+ $commentStore = CommentStore::getStore();
list( $commentFields, $commentCallback ) =
- $commentStore->insertWithTempTable( $dbw, $comment );
+ $commentStore->insertWithTempTable( $dbw, 'img_description', $comment );
+ $actorMigration = ActorMigration::newMigration();
+ $actorFields = $actorMigration->getInsertValues( $dbw, 'img_user', $user );
$dbw->insert( 'image',
[
'img_name' => $this->getName(),
@@ -1350,27 +1478,33 @@ class LocalFile extends File {
'img_major_mime' => $this->major_mime,
'img_minor_mime' => $this->minor_mime,
'img_timestamp' => $timestamp,
- 'img_user' => $user->getId(),
- 'img_user_text' => $user->getName(),
'img_metadata' => $dbw->encodeBlob( $this->metadata ),
'img_sha1' => $this->sha1
- ] + $commentFields,
+ ] + $commentFields + $actorFields,
__METHOD__,
'IGNORE'
);
$reupload = ( $dbw->affectedRows() == 0 );
if ( $reupload ) {
+ $row = $dbw->selectRow(
+ 'image',
+ [ 'img_timestamp', 'img_sha1' ],
+ [ 'img_name' => $this->getName() ],
+ __METHOD__,
+ [ 'LOCK IN SHARE MODE' ]
+ );
+
+ if ( $row && $row->img_sha1 === $this->sha1 ) {
+ $dbw->endAtomic( __METHOD__ );
+ wfDebug( __METHOD__ . ": File " . $this->getRel() . " already exists!\n" );
+ $title = Title::newFromText( $this->getName(), NS_FILE );
+ return Status::newFatal( 'fileexists-no-change', $title->getPrefixedText() );
+ }
+
if ( $allowTimeKludge ) {
# Use LOCK IN SHARE MODE to ignore any transaction snapshotting
- $ltimestamp = $dbw->selectField(
- 'image',
- 'img_timestamp',
- [ 'img_name' => $this->getName() ],
- __METHOD__,
- [ 'LOCK IN SHARE MODE' ]
- );
- $lUnixtime = $ltimestamp ? wfTimestamp( TS_UNIX, $ltimestamp ) : false;
+ $lUnixtime = $row ? wfTimestamp( TS_UNIX, $row->img_timestamp ) : false;
# Avoid a timestamp that is not newer than the last version
# TODO: the image/oldimage tables should be like page/revision with an ID field
if ( $lUnixtime && wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
@@ -1389,8 +1523,6 @@ class LocalFile extends File {
'oi_height' => 'img_height',
'oi_bits' => 'img_bits',
'oi_timestamp' => 'img_timestamp',
- 'oi_user' => 'img_user',
- 'oi_user_text' => 'img_user_text',
'oi_metadata' => 'img_metadata',
'oi_media_type' => 'img_media_type',
'oi_major_mime' => 'img_major_mime',
@@ -1426,11 +1558,44 @@ class LocalFile extends File {
[ 'image_comment_temp' => [ 'LEFT JOIN', [ 'imgcomment_name = img_name' ] ] ]
);
foreach ( $res as $row ) {
- list( , $callback ) = $commentStore->insertWithTempTable( $dbw, $row->img_description );
+ list( , $callback ) = $commentStore->insertWithTempTable(
+ $dbw, 'img_description', $row->img_description
+ );
$callback( $row->img_name );
}
}
+ if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+ $fields['oi_user'] = 'img_user';
+ $fields['oi_user_text'] = 'img_user_text';
+ }
+ if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+ $fields['oi_actor'] = 'img_actor';
+ }
+
+ if ( $wgActorTableSchemaMigrationStage !== MIGRATION_OLD &&
+ $wgActorTableSchemaMigrationStage !== MIGRATION_NEW
+ ) {
+ // Upgrade any rows that are still old-style. Otherwise an upgrade
+ // might be missed if a deletion happens while the migration script
+ // is running.
+ $res = $dbw->select(
+ [ 'image' ],
+ [ 'img_name', 'img_user', 'img_user_text' ],
+ [ 'img_name' => $this->getName(), 'img_actor' => 0 ],
+ __METHOD__
+ );
+ foreach ( $res as $row ) {
+ $actorId = User::newFromAnyId( $row->img_user, $row->img_user_text, null )->getActorId( $dbw );
+ $dbw->update(
+ 'image',
+ [ 'img_actor' => $actorId ],
+ [ 'img_name' => $row->img_name, 'img_actor' => 0 ],
+ __METHOD__
+ );
+ }
+ }
+
# (T36993) Note: $oldver can be empty here, if the previous
# version of the file was broken. Allow registration of the new
# version to continue anyway, because that's better than having
@@ -1451,11 +1616,9 @@ class LocalFile extends File {
'img_major_mime' => $this->major_mime,
'img_minor_mime' => $this->minor_mime,
'img_timestamp' => $timestamp,
- 'img_user' => $user->getId(),
- 'img_user_text' => $user->getName(),
'img_metadata' => $dbw->encodeBlob( $this->metadata ),
'img_sha1' => $this->sha1
- ] + $commentFields,
+ ] + $commentFields + $actorFields,
[ 'img_name' => $this->getName() ],
__METHOD__
);
@@ -1624,7 +1787,12 @@ class LocalFile extends File {
);
} else {
# Update backlink pages pointing to this title if created
- LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' );
+ LinksUpdate::queueRecursiveJobsForTable(
+ $this->getTitle(),
+ 'imagelinks',
+ 'upload-image',
+ $user->getName()
+ );
}
$this->prerenderThumbnails();
@@ -1639,9 +1807,11 @@ class LocalFile extends File {
}
# Invalidate cache for all pages using this file
- DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ) );
+ DeferredUpdates::addUpdate(
+ new HTMLCacheUpdate( $this->getTitle(), 'imagelinks', 'file-upload' )
+ );
- return true;
+ return Status::newGood();
}
/**
@@ -1935,8 +2105,8 @@ class LocalFile extends File {
* This is not used by ImagePage for local files, since (among other things)
* it skips the parser cache.
*
- * @param Language $lang What language to get description in (Optional)
- * @return bool|mixed
+ * @param Language|null $lang What language to get description in (Optional)
+ * @return string|false
*/
function getDescriptionText( $lang = null ) {
$revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
@@ -1954,7 +2124,7 @@ class LocalFile extends File {
/**
* @param int $audience
- * @param User $user
+ * @param User|null $user
* @return string
*/
function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
@@ -2199,7 +2369,7 @@ class LocalFileDeleteBatch {
/**
* Add the old versions of the image to the batch
- * @return array List of archive names from old versions
+ * @return string[] List of archive names from old versions
*/
public function addOlds() {
$archiveNames = [];
@@ -2295,15 +2465,13 @@ class LocalFileDeleteBatch {
}
protected function doDBInserts() {
- global $wgCommentTableSchemaMigrationStage;
+ global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage;
$now = time();
$dbw = $this->file->repo->getMasterDB();
- $commentStoreImgDesc = new CommentStore( 'img_description' );
- $commentStoreOiDesc = new CommentStore( 'oi_description' );
- $commentStoreFaDesc = new CommentStore( 'fa_description' );
- $commentStoreFaReason = new CommentStore( 'fa_deleted_reason' );
+ $commentStore = CommentStore::getStore();
+ $actorMigration = ActorMigration::newMigration();
$encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
$encUserId = $dbw->addQuotes( $this->user->getId() );
@@ -2342,8 +2510,6 @@ class LocalFileDeleteBatch {
'fa_media_type' => 'img_media_type',
'fa_major_mime' => 'img_major_mime',
'fa_minor_mime' => 'img_minor_mime',
- 'fa_user' => 'img_user',
- 'fa_user_text' => 'img_user_text',
'fa_timestamp' => 'img_timestamp',
'fa_sha1' => 'img_sha1'
];
@@ -2351,7 +2517,7 @@ class LocalFileDeleteBatch {
$fields += array_map(
[ $dbw, 'addQuotes' ],
- $commentStoreFaReason->insert( $dbw, $this->reason )
+ $commentStore->insert( $dbw, 'fa_deleted_reason', $this->reason )
);
if ( $wgCommentTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
@@ -2381,32 +2547,67 @@ class LocalFileDeleteBatch {
[ 'image_comment_temp' => [ 'LEFT JOIN', [ 'imgcomment_name = img_name' ] ] ]
);
foreach ( $res as $row ) {
- list( , $callback ) = $commentStoreImgDesc->insertWithTempTable( $dbw, $row->img_description );
+ list( , $callback ) = $commentStore->insertWithTempTable(
+ $dbw, 'img_description', $row->img_description
+ );
$callback( $row->img_name );
}
}
+ if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+ $fields['fa_user'] = 'img_user';
+ $fields['fa_user_text'] = 'img_user_text';
+ }
+ if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+ $fields['fa_actor'] = 'img_actor';
+ }
+
+ if ( $wgActorTableSchemaMigrationStage !== MIGRATION_OLD &&
+ $wgActorTableSchemaMigrationStage !== MIGRATION_NEW
+ ) {
+ // Upgrade any rows that are still old-style. Otherwise an upgrade
+ // might be missed if a deletion happens while the migration script
+ // is running.
+ $res = $dbw->select(
+ [ 'image' ],
+ [ 'img_name', 'img_user', 'img_user_text' ],
+ [ 'img_name' => $this->file->getName(), 'img_actor' => 0 ],
+ __METHOD__
+ );
+ foreach ( $res as $row ) {
+ $actorId = User::newFromAnyId( $row->img_user, $row->img_user_text, null )->getActorId( $dbw );
+ $dbw->update(
+ 'image',
+ [ 'img_actor' => $actorId ],
+ [ 'img_name' => $row->img_name, 'img_actor' => 0 ],
+ __METHOD__
+ );
+ }
+ }
+
$dbw->insertSelect( 'filearchive', $tables, $fields,
[ 'img_name' => $this->file->getName() ], __METHOD__, [], [], $joins );
}
if ( count( $oldRels ) ) {
+ $fileQuery = OldLocalFile::getQueryInfo();
$res = $dbw->select(
- 'oldimage',
- OldLocalFile::selectFields(),
+ $fileQuery['tables'],
+ $fileQuery['fields'],
[
'oi_name' => $this->file->getName(),
'oi_archive_name' => array_keys( $oldRels )
],
__METHOD__,
- [ 'FOR UPDATE' ]
+ [ 'FOR UPDATE' ],
+ $fileQuery['joins']
);
$rowsInsert = [];
if ( $res->numRows() ) {
- $reason = $commentStoreFaReason->createComment( $dbw, $this->reason );
+ $reason = $commentStore->createComment( $dbw, $this->reason );
foreach ( $res as $row ) {
- // Legacy from OldLocalFile::selectFields() just above
- $comment = $commentStoreOiDesc->getCommentLegacy( $dbw, $row );
+ $comment = $commentStore->getComment( 'oi_description', $row );
+ $user = User::newFromAnyId( $row->oi_user, $row->oi_user_text, $row->oi_actor );
$rowsInsert[] = [
// Deletion-specific fields
'fa_storage_group' => 'deleted',
@@ -2427,12 +2628,11 @@ class LocalFileDeleteBatch {
'fa_media_type' => $row->oi_media_type,
'fa_major_mime' => $row->oi_major_mime,
'fa_minor_mime' => $row->oi_minor_mime,
- 'fa_user' => $row->oi_user,
- 'fa_user_text' => $row->oi_user_text,
'fa_timestamp' => $row->oi_timestamp,
'fa_sha1' => $row->oi_sha1
- ] + $commentStoreFaReason->insert( $dbw, $reason )
- + $commentStoreFaDesc->insert( $dbw, $comment );
+ ] + $commentStore->insert( $dbw, 'fa_deleted_reason', $reason )
+ + $commentStore->insert( $dbw, 'fa_description', $comment )
+ + $actorMigration->getInsertValues( $dbw, 'fa_user', $user );
}
}
@@ -2562,10 +2762,10 @@ class LocalFileRestoreBatch {
/** @var LocalFile */
private $file;
- /** @var array List of file IDs to restore */
+ /** @var string[] List of file IDs to restore */
private $cleanupBatch;
- /** @var array List of file IDs to restore */
+ /** @var string[] List of file IDs to restore */
private $ids;
/** @var bool Add all revisions of the file */
@@ -2580,7 +2780,7 @@ class LocalFileRestoreBatch {
*/
function __construct( File $file, $unsuppress = false ) {
$this->file = $file;
- $this->cleanupBatch = $this->ids = [];
+ $this->cleanupBatch = [];
$this->ids = [];
$this->unsuppress = $unsuppress;
}
@@ -2630,9 +2830,8 @@ class LocalFileRestoreBatch {
$dbw = $this->file->repo->getMasterDB();
- $commentStoreImgDesc = new CommentStore( 'img_description' );
- $commentStoreOiDesc = new CommentStore( 'oi_description' );
- $commentStoreFaDesc = new CommentStore( 'fa_description' );
+ $commentStore = CommentStore::getStore();
+ $actorMigration = ActorMigration::newMigration();
$status = $this->file->repo->newGood();
@@ -2653,12 +2852,14 @@ class LocalFileRestoreBatch {
$conditions['fa_id'] = $this->ids;
}
+ $arFileQuery = ArchivedFile::getQueryInfo();
$result = $dbw->select(
- 'filearchive',
- ArchivedFile::selectFields(),
+ $arFileQuery['tables'],
+ $arFileQuery['fields'],
$conditions,
__METHOD__,
- [ 'ORDER BY' => 'fa_timestamp DESC' ]
+ [ 'ORDER BY' => 'fa_timestamp DESC' ],
+ $arFileQuery['joins']
);
$idsPresent = [];
@@ -2718,13 +2919,14 @@ class LocalFileRestoreBatch {
];
}
- // Legacy from ArchivedFile::selectFields() just above
- $comment = $commentStoreFaDesc->getCommentLegacy( $dbw, $row );
+ $comment = $commentStore->getComment( 'fa_description', $row );
+ $user = User::newFromAnyId( $row->fa_user, $row->fa_user_text, $row->fa_actor );
if ( $first && !$exists ) {
// This revision will be published as the new current version
$destRel = $this->file->getRel();
list( $commentFields, $commentCallback ) =
- $commentStoreImgDesc->insertWithTempTable( $dbw, $comment );
+ $commentStore->insertWithTempTable( $dbw, 'img_description', $comment );
+ $actorFields = $actorMigration->getInsertValues( $dbw, 'img_user', $user );
$insertCurrent = [
'img_name' => $row->fa_name,
'img_size' => $row->fa_size,
@@ -2735,11 +2937,9 @@ class LocalFileRestoreBatch {
'img_media_type' => $props['media_type'],
'img_major_mime' => $props['major_mime'],
'img_minor_mime' => $props['minor_mime'],
- 'img_user' => $row->fa_user,
- 'img_user_text' => $row->fa_user_text,
'img_timestamp' => $row->fa_timestamp,
'img_sha1' => $sha1
- ] + $commentFields;
+ ] + $commentFields + $actorFields;
// The live (current) version cannot be hidden!
if ( !$this->unsuppress && $row->fa_deleted ) {
@@ -2771,8 +2971,6 @@ class LocalFileRestoreBatch {
'oi_width' => $row->fa_width,
'oi_height' => $row->fa_height,
'oi_bits' => $row->fa_bits,
- 'oi_user' => $row->fa_user,
- 'oi_user_text' => $row->fa_user_text,
'oi_timestamp' => $row->fa_timestamp,
'oi_metadata' => $props['metadata'],
'oi_media_type' => $props['media_type'],
@@ -2780,7 +2978,8 @@ class LocalFileRestoreBatch {
'oi_minor_mime' => $props['minor_mime'],
'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
'oi_sha1' => $sha1
- ] + $commentStoreOiDesc->insert( $dbw, $comment );
+ ] + $commentStore->insert( $dbw, 'oi_description', $comment )
+ + $actorMigration->getInsertValues( $dbw, 'oi_user', $user );
}
$deleteIds[] = $row->fa_id;
@@ -2898,8 +3097,8 @@ class LocalFileRestoreBatch {
/**
* Removes non-existent files from a cleanup batch.
- * @param array $batch
- * @return array
+ * @param string[] $batch
+ * @return string[]
*/
protected function removeNonexistentFromCleanup( $batch ) {
$files = $newBatch = [];
@@ -2943,7 +3142,7 @@ class LocalFileRestoreBatch {
* rollback by removing all items that were succesfully copied.
*
* @param Status $storeStatus
- * @param array $storeBatch
+ * @param array[] $storeBatch
*/
protected function cleanupFailedBatch( $storeStatus, $storeBatch ) {
$cleanupBatch = [];
@@ -3009,7 +3208,7 @@ class LocalFileMoveBatch {
/**
* Add the old versions of the image to the batch
- * @return array List of archive names from old versions
+ * @return string[] List of archive names from old versions
*/
public function addOlds() {
$archiveBase = 'archive';
@@ -3145,9 +3344,9 @@ class LocalFileMoveBatch {
__METHOD__,
[ 'FOR UPDATE' ]
);
- $oldRowCount = $dbw->selectField(
+ $oldRowCount = $dbw->selectRowCount(
'oldimage',
- 'COUNT(*)',
+ '*',
[ 'oi_name' => $this->oldName ],
__METHOD__,
[ 'FOR UPDATE' ]
@@ -3175,6 +3374,8 @@ class LocalFileMoveBatch {
* many rows where updated.
*/
protected function doDBUpdates() {
+ global $wgCommentTableSchemaMigrationStage;
+
$dbw = $this->db;
// Update current image
@@ -3184,6 +3385,15 @@ class LocalFileMoveBatch {
[ 'img_name' => $this->oldName ],
__METHOD__
);
+ if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
+ $dbw->update(
+ 'image_comment_temp',
+ [ 'imgcomment_name' => $this->newName ],
+ [ 'imgcomment_name' => $this->oldName ],
+ __METHOD__
+ );
+ }
+
// Update old images
$dbw->update(
'oldimage',
@@ -3199,7 +3409,7 @@ class LocalFileMoveBatch {
/**
* Generate triplets for FileRepo::storeBatch().
- * @return array
+ * @return array[]
*/
protected function getMoveTriplets() {
$moves = array_merge( [ $this->cur ], $this->olds );
@@ -3251,7 +3461,7 @@ class LocalFileMoveBatch {
/**
* Cleanup a partially moved array of triplets by deleting the target
* files. Called if something went wrong half way.
- * @param array $triplets
+ * @param array[] $triplets
*/
protected function cleanupTarget( $triplets ) {
// Create dest pairs from the triplets
@@ -3267,7 +3477,7 @@ class LocalFileMoveBatch {
/**
* Cleanup a fully moved array of triplets by deleting the source files.
* Called at the end of the move process if everything else went ok.
- * @param array $triplets
+ * @param array[] $triplets
*/
protected function cleanupSource( $triplets ) {
// Create source file names from the triplets
diff --git a/www/wiki/includes/filerepo/file/OldLocalFile.php b/www/wiki/includes/filerepo/file/OldLocalFile.php
index ee172e11..1bd40a0e 100644
--- a/www/wiki/includes/filerepo/file/OldLocalFile.php
+++ b/www/wiki/includes/filerepo/file/OldLocalFile.php
@@ -27,7 +27,7 @@
* @ingroup FileAbstraction
*/
class OldLocalFile extends LocalFile {
- /** @var string Timestamp */
+ /** @var string|int Timestamp */
protected $requestedTime;
/** @var string Archive name */
@@ -39,8 +39,8 @@ class OldLocalFile extends LocalFile {
/**
* @param Title $title
* @param FileRepo $repo
- * @param null|int $time Timestamp or null
- * @return OldLocalFile
+ * @param string|int $time
+ * @return self
* @throws MWException
*/
static function newFromTitle( $title, $repo, $time = null ) {
@@ -56,7 +56,7 @@ class OldLocalFile extends LocalFile {
* @param Title $title
* @param FileRepo $repo
* @param string $archiveName
- * @return OldLocalFile
+ * @return self
*/
static function newFromArchiveName( $title, $repo, $archiveName ) {
return new self( $title, $repo, null, $archiveName );
@@ -65,7 +65,7 @@ class OldLocalFile extends LocalFile {
/**
* @param stdClass $row
* @param FileRepo $repo
- * @return OldLocalFile
+ * @return self
*/
static function newFromRow( $row, $repo ) {
$title = Title::makeTitle( NS_FILE, $row->oi_name );
@@ -93,7 +93,10 @@ class OldLocalFile extends LocalFile {
$conds['oi_timestamp'] = $dbr->timestamp( $timestamp );
}
- $row = $dbr->selectRow( 'oldimage', self::selectFields(), $conds, __METHOD__ );
+ $fileQuery = self::getQueryInfo();
+ $row = $dbr->selectRow(
+ $fileQuery['tables'], $fileQuery['fields'], $conds, __METHOD__, [], $fileQuery['joins']
+ );
if ( $row ) {
return self::newFromRow( $row, $repo );
} else {
@@ -103,11 +106,23 @@ class OldLocalFile extends LocalFile {
/**
* Fields in the oldimage table
- * @todo Deprecate this in favor of a method that returns tables and joins
- * as well, and use CommentStore::getJoin().
- * @return array
+ * @deprecated since 1.31, use self::getQueryInfo() instead.
+ * @return string[]
*/
static function selectFields() {
+ global $wgActorTableSchemaMigrationStage;
+
+ wfDeprecated( __METHOD__, '1.31' );
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+ // If code is using this instead of self::getQueryInfo(), there's a
+ // decent chance it's going to try to directly access
+ // $row->oi_user or $row->oi_user_text and we can't give it
+ // useful values here once those aren't being written anymore.
+ throw new BadMethodCallException(
+ 'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+ );
+ }
+
return [
'oi_name',
'oi_archive_name',
@@ -121,17 +136,63 @@ class OldLocalFile extends LocalFile {
'oi_minor_mime',
'oi_user',
'oi_user_text',
+ 'oi_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'oi_actor' : 'NULL',
'oi_timestamp',
'oi_deleted',
'oi_sha1',
- ] + CommentStore::newKey( 'oi_description' )->getFields();
+ ] + CommentStore::getStore()->getFields( 'oi_description' );
+ }
+
+ /**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new oldlocalfile object.
+ * @since 1.31
+ * @param string[] $options
+ * - omit-lazy: Omit fields that are lazily cached.
+ * @return array[] With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ */
+ public static function getQueryInfo( array $options = [] ) {
+ $commentQuery = CommentStore::getStore()->getJoin( 'oi_description' );
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'oi_user' );
+ $ret = [
+ 'tables' => [ 'oldimage' ] + $commentQuery['tables'] + $actorQuery['tables'],
+ 'fields' => [
+ 'oi_name',
+ 'oi_archive_name',
+ 'oi_size',
+ 'oi_width',
+ 'oi_height',
+ 'oi_bits',
+ 'oi_media_type',
+ 'oi_major_mime',
+ 'oi_minor_mime',
+ 'oi_timestamp',
+ 'oi_deleted',
+ 'oi_sha1',
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
+ 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
+ ];
+
+ if ( in_array( 'omit-nonlazy', $options, true ) ) {
+ // Internal use only for getting only the lazy fields
+ $ret['fields'] = [];
+ }
+ if ( !in_array( 'omit-lazy', $options, true ) ) {
+ // Note: Keep this in sync with self::getLazyCacheFields()
+ $ret['fields'][] = 'oi_metadata';
+ }
+
+ return $ret;
}
/**
* @param Title $title
* @param FileRepo $repo
- * @param string $time Timestamp or null to load by archive name
- * @param string $archiveName Archive name or null to load by timestamp
+ * @param string|int|null $time Timestamp or null to load by archive name
+ * @param string|null $archiveName Archive name or null to load by timestamp
* @throws MWException
*/
function __construct( $title, $repo, $time, $archiveName ) {
@@ -188,8 +249,15 @@ class OldLocalFile extends LocalFile {
} else {
$conds['oi_timestamp'] = $dbr->timestamp( $this->requestedTime );
}
- $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ),
- $conds, __METHOD__, [ 'ORDER BY' => 'oi_timestamp DESC' ] );
+ $fileQuery = static::getQueryInfo();
+ $row = $dbr->selectRow(
+ $fileQuery['tables'],
+ $fileQuery['fields'],
+ $conds,
+ __METHOD__,
+ [ 'ORDER BY' => 'oi_timestamp DESC' ],
+ $fileQuery['joins']
+ );
if ( $row ) {
$this->loadFromRow( $row, 'oi_' );
} else {
@@ -209,14 +277,27 @@ class OldLocalFile extends LocalFile {
} else {
$conds['oi_timestamp'] = $dbr->timestamp( $this->requestedTime );
}
+ $fileQuery = static::getQueryInfo( [ 'omit-nonlazy' ] );
// In theory the file could have just been renamed/deleted...oh well
- $row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ),
- $conds, __METHOD__, [ 'ORDER BY' => 'oi_timestamp DESC' ] );
+ $row = $dbr->selectRow(
+ $fileQuery['tables'],
+ $fileQuery['fields'],
+ $conds,
+ __METHOD__,
+ [ 'ORDER BY' => 'oi_timestamp DESC' ],
+ $fileQuery['joins']
+ );
if ( !$row ) { // fallback to master
$dbr = $this->repo->getMasterDB();
- $row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ),
- $conds, __METHOD__, [ 'ORDER BY' => 'oi_timestamp DESC' ] );
+ $row = $dbr->selectRow(
+ $fileQuery['tables'],
+ $fileQuery['fields'],
+ $conds,
+ __METHOD__,
+ [ 'ORDER BY' => 'oi_timestamp DESC' ],
+ $fileQuery['joins']
+ );
}
if ( $row ) {
@@ -228,11 +309,8 @@ class OldLocalFile extends LocalFile {
}
}
- /**
- * @param string $prefix
- * @return array
- */
- function getCacheFields( $prefix = 'img_' ) {
+ /** @inheritDoc */
+ protected function getCacheFields( $prefix = 'img_' ) {
$fields = parent::getCacheFields( $prefix );
$fields[] = $prefix . 'archive_name';
$fields[] = $prefix . 'deleted';
@@ -368,7 +446,8 @@ class OldLocalFile extends LocalFile {
return false;
}
- $commentFields = CommentStore::newKey( 'oi_description' )->insert( $dbw, $comment );
+ $commentFields = CommentStore::getStore()->insert( $dbw, 'oi_description', $comment );
+ $actorFields = ActorMigration::newMigration()->getInsertValues( $dbw, 'oi_user', $user );
$dbw->insert( 'oldimage',
[
'oi_name' => $this->getName(),
@@ -378,14 +457,12 @@ class OldLocalFile extends LocalFile {
'oi_height' => intval( $props['height'] ),
'oi_bits' => $props['bits'],
'oi_timestamp' => $dbw->timestamp( $timestamp ),
- 'oi_user' => $user->getId(),
- 'oi_user_text' => $user->getName(),
'oi_metadata' => $props['metadata'],
'oi_media_type' => $props['media_type'],
'oi_major_mime' => $props['major_mime'],
'oi_minor_mime' => $props['minor_mime'],
'oi_sha1' => $props['sha1'],
- ] + $commentFields, __METHOD__
+ ] + $commentFields + $actorFields, __METHOD__
);
return true;
diff --git a/www/wiki/includes/filerepo/file/UnregisteredLocalFile.php b/www/wiki/includes/filerepo/file/UnregisteredLocalFile.php
index cdad5fce..fde68bbe 100644
--- a/www/wiki/includes/filerepo/file/UnregisteredLocalFile.php
+++ b/www/wiki/includes/filerepo/file/UnregisteredLocalFile.php
@@ -151,7 +151,7 @@ class UnregisteredLocalFile extends File {
*/
function getMimeType() {
if ( !isset( $this->mime ) ) {
- $magic = MimeMagic::singleton();
+ $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
$this->mime = $magic->guessMimeType( $this->getLocalRefPath() );
}
diff --git a/www/wiki/includes/gallery/ImageGalleryBase.php b/www/wiki/includes/gallery/ImageGalleryBase.php
index eeb8a8ff..09e40a28 100644
--- a/www/wiki/includes/gallery/ImageGalleryBase.php
+++ b/www/wiki/includes/gallery/ImageGalleryBase.php
@@ -59,6 +59,15 @@ abstract class ImageGalleryBase extends ContextSource {
protected $mCaption = false;
/**
+ * Length to truncate filename to in caption when using "showfilename".
+ * A value of 'true' will truncate the filename to one line using CSS
+ * and will be the behaviour after deprecation.
+ *
+ * @var bool|int
+ */
+ protected $mCaptionLength = true;
+
+ /**
* @var bool Hide blacklisted images?
*/
protected $mHideBadImages;
@@ -113,12 +122,12 @@ abstract class ImageGalleryBase extends ContextSource {
private static function loadModes() {
if ( self::$modeMapping === false ) {
self::$modeMapping = [
- 'traditional' => 'TraditionalImageGallery',
- 'nolines' => 'NolinesImageGallery',
- 'packed' => 'PackedImageGallery',
- 'packed-hover' => 'PackedHoverImageGallery',
- 'packed-overlay' => 'PackedOverlayImageGallery',
- 'slideshow' => 'SlideshowImageGallery',
+ 'traditional' => TraditionalImageGallery::class,
+ 'nolines' => NolinesImageGallery::class,
+ 'packed' => PackedImageGallery::class,
+ 'packed-hover' => PackedHoverImageGallery::class,
+ 'packed-overlay' => PackedOverlayImageGallery::class,
+ 'slideshow' => SlideshowImageGallery::class,
];
// Allow extensions to make a new gallery format.
Hooks::run( 'GalleryGetModes', [ &self::$modeMapping ] );
@@ -177,7 +186,7 @@ abstract class ImageGalleryBase extends ContextSource {
/**
* Set the caption (as plain text)
*
- * @param string $caption Caption
+ * @param string $caption
*/
function setCaption( $caption ) {
$this->mCaption = htmlspecialchars( $caption );
@@ -186,7 +195,7 @@ abstract class ImageGalleryBase extends ContextSource {
/**
* Set the caption (as HTML)
*
- * @param string $caption Caption
+ * @param string $caption
*/
public function setCaptionHtml( $caption ) {
$this->mCaption = $caption;
@@ -207,22 +216,26 @@ abstract class ImageGalleryBase extends ContextSource {
/**
* Set how wide each image will be, in pixels.
*
- * @param int $num Integer > 0; invalid numbers will be ignored
+ * @param string $num Number. Unit other than 'px is invalid. Invalid numbers
+ * and those below 0 are ignored.
*/
public function setWidths( $num ) {
- if ( $num > 0 ) {
- $this->mWidths = (int)$num;
+ $parsed = Parser::parseWidthParam( $num, false );
+ if ( isset( $parsed['width'] ) && $parsed['width'] > 0 ) {
+ $this->mWidths = $parsed['width'];
}
}
/**
* Set how high each image will be, in pixels.
*
- * @param int $num Integer > 0; invalid numbers will be ignored
+ * @param string $num Number. Unit other than 'px is invalid. Invalid numbers
+ * and those below 0 are ignored.
*/
public function setHeights( $num ) {
- if ( $num > 0 ) {
- $this->mHeights = (int)$num;
+ $parsed = Parser::parseWidthParam( $num, false );
+ if ( isset( $parsed['width'] ) && $parsed['width'] > 0 ) {
+ $this->mHeights = $parsed['width'];
}
}
diff --git a/www/wiki/includes/gallery/PackedOverlayImageGallery.php b/www/wiki/includes/gallery/PackedOverlayImageGallery.php
index db8ce68b..0a5a457f 100644
--- a/www/wiki/includes/gallery/PackedOverlayImageGallery.php
+++ b/www/wiki/includes/gallery/PackedOverlayImageGallery.php
@@ -1,8 +1,5 @@
<?php
/**
- * Packed overlay image gallery. All images adjusted to be same height and
- * image caption being placed over top of image.
- *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
@@ -21,6 +18,10 @@
* @file
*/
+/**
+ * Packed overlay image gallery. All images adjusted to be same height and
+ * image caption being placed over top of image.
+ */
class PackedOverlayImageGallery extends PackedImageGallery {
/**
* Add the wrapper html around the thumb's caption
diff --git a/www/wiki/includes/gallery/TraditionalImageGallery.php b/www/wiki/includes/gallery/TraditionalImageGallery.php
index 7a520bcb..1cb7e6d2 100644
--- a/www/wiki/includes/gallery/TraditionalImageGallery.php
+++ b/www/wiki/includes/gallery/TraditionalImageGallery.php
@@ -195,13 +195,13 @@ class TraditionalImageGallery extends ImageGalleryBase {
Linker::linkKnown(
$nt,
htmlspecialchars(
- $this->mCaptionLength !== true ?
- $lang->truncate( $nt->getText(), $this->mCaptionLength ) :
+ is_int( $this->getCaptionLength() ) ?
+ $lang->truncate( $nt->getText(), $this->getCaptionLength() ) :
$nt->getText()
),
[
'class' => 'galleryfilename' .
- ( $this->mCaptionLength === true ? ' galleryfilename-truncate' : '' )
+ ( $this->getCaptionLength() === true ? ' galleryfilename-truncate' : '' )
]
) . "\n" :
'';
@@ -209,11 +209,15 @@ class TraditionalImageGallery extends ImageGalleryBase {
$galleryText = $textlink . $text . $meta;
$galleryText = $this->wrapGalleryText( $galleryText, $thumb );
+ $gbWidth = $this->getGBWidth( $thumb ) . 'px';
+ if ( $this->getGBWidthOverwrite( $thumb ) ) {
+ $gbWidth = $this->getGBWidthOverwrite( $thumb );
+ }
# Weird double wrapping (the extra div inside the li) needed due to FF2 bug
# Can be safely removed if FF2 falls completely out of existence
$output .= "\n\t\t" . '<li class="gallerybox" style="width: '
- . $this->getGBWidth( $thumb ) . 'px">'
- . '<div style="width: ' . $this->getGBWidth( $thumb ) . 'px">'
+ . $gbWidth . '">'
+ . '<div style="width: ' . $gbWidth . '">'
. $thumbhtml
. $galleryText
. "\n\t\t</div></li>";
@@ -273,6 +277,17 @@ class TraditionalImageGallery extends ImageGalleryBase {
}
/**
+ * Length to truncate filename to in caption when using "showfilename" (if int).
+ * A value of 'true' will truncate the filename to one line using CSS, while
+ * 'false' will disable truncating.
+ *
+ * @return int|bool
+ */
+ protected function getCaptionLength() {
+ return $this->mCaptionLength;
+ }
+
+ /**
* Get total padding.
*
* @return int Number of pixels of whitespace surrounding the thumbnail.
@@ -319,7 +334,7 @@ class TraditionalImageGallery extends ImageGalleryBase {
}
/**
- * Width of gallerybox <li>.
+ * Computed width of gallerybox <li>.
*
* Generally is the width of the image, plus padding on image
* plus padding on gallerybox.
@@ -333,6 +348,21 @@ class TraditionalImageGallery extends ImageGalleryBase {
}
/**
+ * Allows overwriting the computed width of the gallerybox <li> with a string,
+ * like '100%'.
+ *
+ * Generally is the width of the image, plus padding on image
+ * plus padding on gallerybox.
+ *
+ * @note Important: parameter will be false if no thumb used.
+ * @param MediaTransformOutput|bool $thumb MediaTransformObject object or false.
+ * @return bool|string Ignored if false.
+ */
+ protected function getGBWidthOverwrite( $thumb ) {
+ return false;
+ }
+
+ /**
* Get a list of modules to include in the page.
*
* Primarily intended for subclasses.
diff --git a/www/wiki/includes/htmlform/HTMLApiField.php b/www/wiki/includes/htmlform/HTMLApiField.php
deleted file mode 100644
index 24a253ed..00000000
--- a/www/wiki/includes/htmlform/HTMLApiField.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-class HTMLApiField extends HTMLFormField {
- public function getTableRow( $value ) {
- return '';
- }
-
- public function getDiv( $value ) {
- return $this->getTableRow( $value );
- }
-
- public function getRaw( $value ) {
- return $this->getTableRow( $value );
- }
-
- public function getInputHTML( $value ) {
- return '';
- }
-
- public function hasVisibleOutput() {
- return false;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLAutoCompleteSelectField.php b/www/wiki/includes/htmlform/HTMLAutoCompleteSelectField.php
deleted file mode 100644
index 76a88d51..00000000
--- a/www/wiki/includes/htmlform/HTMLAutoCompleteSelectField.php
+++ /dev/null
@@ -1,177 +0,0 @@
-<?php
-
-/**
- * Text field for selecting a value from a large list of possible values, with
- * auto-completion and optionally with a select dropdown for selecting common
- * options.
- *
- * HTMLComboboxField implements most of the same functionality and should be
- * used instead, if possible.
- *
- * If one of 'options-messages', 'options', or 'options-message' is provided
- * and non-empty, the select dropdown will be shown. An 'other' key will be
- * appended using message 'htmlform-selectorother-other' if not already
- * present.
- *
- * Besides the parameters recognized by HTMLTextField, the following are
- * recognized:
- * options-messages - As for HTMLSelectField
- * options - As for HTMLSelectField
- * options-message - As for HTMLSelectField
- * autocomplete - Associative array mapping display text to values.
- * autocomplete-messages - Like autocomplete, but keys are message names.
- * require-match - Boolean, if true the value must be in the options or the
- * autocomplete.
- * other-message - Message to use instead of htmlform-selectorother-other for
- * the 'other' message.
- * other - Raw text to use for the 'other' message
- */
-class HTMLAutoCompleteSelectField extends HTMLTextField {
- protected $autocomplete = [];
-
- function __construct( $params ) {
- $params += [
- 'require-match' => false,
- ];
-
- parent::__construct( $params );
-
- if ( array_key_exists( 'autocomplete-messages', $this->mParams ) ) {
- foreach ( $this->mParams['autocomplete-messages'] as $key => $value ) {
- $key = $this->msg( $key )->plain();
- $this->autocomplete[$key] = strval( $value );
- }
- } elseif ( array_key_exists( 'autocomplete', $this->mParams ) ) {
- foreach ( $this->mParams['autocomplete'] as $key => $value ) {
- $this->autocomplete[$key] = strval( $value );
- }
- }
- if ( !is_array( $this->autocomplete ) || !$this->autocomplete ) {
- throw new MWException( 'HTMLAutoCompleteSelectField called without any autocompletions' );
- }
-
- $this->getOptions();
- if ( $this->mOptions && !in_array( 'other', $this->mOptions, true ) ) {
- if ( isset( $params['other-message'] ) ) {
- $msg = $this->getMessage( $params['other-message'] )->text();
- } elseif ( isset( $params['other'] ) ) {
- $msg = $params['other'];
- } else {
- $msg = wfMessage( 'htmlform-selectorother-other' )->text();
- }
- $this->mOptions[$msg] = 'other';
- }
- }
-
- function loadDataFromRequest( $request ) {
- if ( $request->getCheck( $this->mName ) ) {
- $val = $request->getText( $this->mName . '-select', 'other' );
-
- if ( $val === 'other' ) {
- $val = $request->getText( $this->mName );
- if ( isset( $this->autocomplete[$val] ) ) {
- $val = $this->autocomplete[$val];
- }
- }
-
- return $val;
- } else {
- return $this->getDefault();
- }
- }
-
- function validate( $value, $alldata ) {
- $p = parent::validate( $value, $alldata );
-
- if ( $p !== true ) {
- return $p;
- }
-
- $validOptions = HTMLFormField::flattenOptions( $this->getOptions() );
-
- if ( in_array( strval( $value ), $validOptions, true ) ) {
- return true;
- } elseif ( in_array( strval( $value ), $this->autocomplete, true ) ) {
- return true;
- } elseif ( $this->mParams['require-match'] ) {
- return $this->msg( 'htmlform-select-badoption' )->parse();
- }
-
- return true;
- }
-
- // FIXME Ewww, this shouldn't be adding any attributes not requested in $list :(
- public function getAttributes( array $list ) {
- $attribs = [
- 'type' => 'text',
- 'data-autocomplete' => FormatJson::encode( array_keys( $this->autocomplete ) ),
- ] + parent::getAttributes( $list );
-
- if ( $this->getOptions() ) {
- $attribs['data-hide-if'] = FormatJson::encode(
- [ '!==', $this->mName . '-select', 'other' ]
- );
- }
-
- return $attribs;
- }
-
- function getInputHTML( $value ) {
- $oldClass = $this->mClass;
- $this->mClass = (array)$this->mClass;
-
- $valInSelect = false;
- $ret = '';
-
- if ( $this->getOptions() ) {
- if ( $value !== false ) {
- $value = strval( $value );
- $valInSelect = in_array(
- $value, HTMLFormField::flattenOptions( $this->getOptions() ), true
- );
- }
-
- $selected = $valInSelect ? $value : 'other';
- $select = new XmlSelect( $this->mName . '-select', $this->mID . '-select', $selected );
- $select->addOptions( $this->getOptions() );
- $select->setAttribute( 'class', 'mw-htmlform-select-or-other' );
-
- if ( !empty( $this->mParams['disabled'] ) ) {
- $select->setAttribute( 'disabled', 'disabled' );
- }
-
- if ( isset( $this->mParams['tabindex'] ) ) {
- $select->setAttribute( 'tabindex', $this->mParams['tabindex'] );
- }
-
- $ret = $select->getHTML() . "<br />\n";
-
- $this->mClass[] = 'mw-htmlform-hide-if';
- }
-
- if ( $valInSelect ) {
- $value = '';
- } else {
- $key = array_search( strval( $value ), $this->autocomplete, true );
- if ( $key !== false ) {
- $value = $key;
- }
- }
-
- $this->mClass[] = 'mw-htmlform-autocomplete';
- $ret .= parent::getInputHTML( $valInSelect ? '' : $value );
- $this->mClass = $oldClass;
-
- return $ret;
- }
-
- /**
- * Get the OOUI version of this input.
- * @param string $value
- * @return false
- */
- function getInputOOUI( $value ) {
- // To be implemented, for now override the function from HTMLTextField
- return false;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLButtonField.php b/www/wiki/includes/htmlform/HTMLButtonField.php
deleted file mode 100644
index 64fe7eda..00000000
--- a/www/wiki/includes/htmlform/HTMLButtonField.php
+++ /dev/null
@@ -1,132 +0,0 @@
-<?php
-
-/**
- * Adds a generic button inline to the form. Does not do anything, you must add
- * click handling code in JavaScript. Use a HTMLSubmitField if you merely
- * wish to add a submit button to a form.
- *
- * Additional recognized configuration parameters include:
- * - flags: OOUI flags for the button, see OOUI\FlaggedElement
- * - buttonlabel-message: Message to use for the button display text, instead
- * of the value from 'default'. Overrides 'buttonlabel' and 'buttonlabel-raw'.
- * - buttonlabel: Text to display for the button display text, instead
- * of the value from 'default'. Overrides 'buttonlabel-raw'.
- * - buttonlabel-raw: HTMLto display for the button display text, instead
- * of the value from 'default'.
- *
- * Note that the buttonlabel parameters are not supported on IE6 and IE7 due to
- * bugs in those browsers. If detected, they will be served buttons using the
- * value of 'default' as the button label.
- *
- * @since 1.22
- */
-class HTMLButtonField extends HTMLFormField {
- protected $buttonType = 'button';
- protected $buttonLabel = null;
-
- /** @var array $mFlags Flags to add to OOUI Button widget */
- protected $mFlags = [];
-
- public function __construct( $info ) {
- $info['nodata'] = true;
- if ( isset( $info['flags'] ) ) {
- $this->mFlags = $info['flags'];
- }
-
- # Generate the label from a message, if possible
- if ( isset( $info['buttonlabel-message'] ) ) {
- $this->buttonLabel = $this->getMessage( $info['buttonlabel-message'] )->parse();
- } elseif ( isset( $info['buttonlabel'] ) ) {
- if ( $info['buttonlabel'] === '&#160;' ) {
- // Apparently some things set &nbsp directly and in an odd format
- $this->buttonLabel = '&#160;';
- } else {
- $this->buttonLabel = htmlspecialchars( $info['buttonlabel'] );
- }
- } elseif ( isset( $info['buttonlabel-raw'] ) ) {
- $this->buttonLabel = $info['buttonlabel-raw'];
- }
-
- $this->setShowEmptyLabel( false );
-
- parent::__construct( $info );
- }
-
- public function getInputHTML( $value ) {
- $flags = '';
- $prefix = 'mw-htmlform-';
- if ( $this->mParent instanceof VFormHTMLForm ||
- $this->mParent->getConfig()->get( 'UseMediaWikiUIEverywhere' )
- ) {
- $prefix = 'mw-ui-';
- // add mw-ui-button separately, so the descriptor doesn't need to set it
- $flags .= ' ' . $prefix . 'button';
- }
- foreach ( $this->mFlags as $flag ) {
- $flags .= ' ' . $prefix . $flag;
- }
- $attr = [
- 'class' => 'mw-htmlform-submit ' . $this->mClass . $flags,
- 'id' => $this->mID,
- 'type' => $this->buttonType,
- 'name' => $this->mName,
- 'value' => $this->getDefault(),
- ] + $this->getAttributes( [ 'disabled', 'tabindex' ] );
-
- if ( $this->isBadIE() ) {
- return Html::element( 'input', $attr );
- } else {
- return Html::rawElement( 'button', $attr,
- $this->buttonLabel ?: htmlspecialchars( $this->getDefault() ) );
- }
- }
-
- /**
- * Get the OOUI widget for this field.
- * @param string $value
- * @return OOUI\ButtonInputWidget
- */
- public function getInputOOUI( $value ) {
- return new OOUI\ButtonInputWidget( [
- 'name' => $this->mName,
- 'value' => $this->getDefault(),
- 'label' => !$this->isBadIE() && $this->buttonLabel
- ? new OOUI\HtmlSnippet( $this->buttonLabel )
- : $this->getDefault(),
- 'type' => $this->buttonType,
- 'classes' => [ 'mw-htmlform-submit', $this->mClass ],
- 'id' => $this->mID,
- 'flags' => $this->mFlags,
- 'useInputTag' => $this->isBadIE(),
- ] + OOUI\Element::configFromHtmlAttributes(
- $this->getAttributes( [ 'disabled', 'tabindex' ] )
- ) );
- }
-
- protected function needsLabel() {
- return false;
- }
-
- /**
- * Button cannot be invalid
- *
- * @param string $value
- * @param array $alldata
- *
- * @return bool
- */
- public function validate( $value, $alldata ) {
- return true;
- }
-
- /**
- * IE<8 has bugs with <button>, so we'll need to avoid them.
- * @return bool Whether the request is from a bad version of IE
- */
- private function isBadIE() {
- $request = $this->mParent
- ? $this->mParent->getRequest()
- : RequestContext::getMain()->getRequest();
- return preg_match( '/MSIE [1-7]\./i', $request->getHeader( 'User-Agent' ) );
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLCheckField.php b/www/wiki/includes/htmlform/HTMLCheckField.php
deleted file mode 100644
index 4a6b8047..00000000
--- a/www/wiki/includes/htmlform/HTMLCheckField.php
+++ /dev/null
@@ -1,131 +0,0 @@
-<?php
-
-/**
- * A checkbox field
- */
-class HTMLCheckField extends HTMLFormField {
- function getInputHTML( $value ) {
- global $wgUseMediaWikiUIEverywhere;
-
- if ( !empty( $this->mParams['invert'] ) ) {
- $value = !$value;
- }
-
- $attr = $this->getTooltipAndAccessKey();
- $attr['id'] = $this->mID;
-
- $attr += $this->getAttributes( [ 'disabled', 'tabindex' ] );
-
- if ( $this->mClass !== '' ) {
- $attr['class'] = $this->mClass;
- }
-
- $attrLabel = [ 'for' => $this->mID ];
- if ( isset( $attr['title'] ) ) {
- // propagate tooltip to label
- $attrLabel['title'] = $attr['title'];
- }
-
- $chkLabel = Xml::check( $this->mName, $value, $attr ) .
- '&#160;' .
- Html::rawElement( 'label', $attrLabel, $this->mLabel );
-
- if ( $wgUseMediaWikiUIEverywhere || $this->mParent instanceof VFormHTMLForm ) {
- $chkLabel = Html::rawElement(
- 'div',
- [ 'class' => 'mw-ui-checkbox' ],
- $chkLabel
- );
- }
-
- return $chkLabel;
- }
-
- /**
- * Get the OOUI version of this field.
- * @since 1.26
- * @param string $value
- * @return OOUI\CheckboxInputWidget The checkbox widget.
- */
- public function getInputOOUI( $value ) {
- if ( !empty( $this->mParams['invert'] ) ) {
- $value = !$value;
- }
-
- $attr = $this->getTooltipAndAccessKey();
- $attr['id'] = $this->mID;
- $attr['name'] = $this->mName;
-
- $attr += OOUI\Element::configFromHtmlAttributes(
- $this->getAttributes( [ 'disabled', 'tabindex' ] )
- );
-
- if ( $this->mClass !== '' ) {
- $attr['classes'] = [ $this->mClass ];
- }
-
- $attr['selected'] = $value;
- $attr['value'] = '1'; // Nasty hack, but needed to make this work
-
- return new OOUI\CheckboxInputWidget( $attr );
- }
-
- /**
- * For a checkbox, the label goes on the right hand side, and is
- * added in getInputHTML(), rather than HTMLFormField::getRow()
- *
- * ...unless OOUI is being used, in which case we actually return
- * the label here.
- *
- * @return string
- */
- function getLabel() {
- if ( $this->mParent instanceof OOUIHTMLForm ) {
- return $this->mLabel;
- } elseif (
- $this->mParent instanceof HTMLForm &&
- $this->mParent->getDisplayFormat() === 'div'
- ) {
- return '';
- } else {
- return '&#160;';
- }
- }
-
- /**
- * Get label alignment when generating field for OOUI.
- * @return string 'left', 'right', 'top' or 'inline'
- */
- protected function getLabelAlignOOUI() {
- return 'inline';
- }
-
- /**
- * checkboxes don't need a label.
- * @return bool
- */
- protected function needsLabel() {
- return false;
- }
-
- /**
- * @param WebRequest $request
- *
- * @return bool
- */
- function loadDataFromRequest( $request ) {
- $invert = isset( $this->mParams['invert'] ) && $this->mParams['invert'];
-
- // GetCheck won't work like we want for checks.
- // Fetch the value in either one of the two following case:
- // - we have a valid token (form got posted or GET forged by the user)
- // - checkbox name has a value (false or true), ie is not null
- if ( $request->getCheck( 'wpEditToken' ) || $request->getVal( $this->mName ) !== null ) {
- return $invert
- ? !$request->getBool( $this->mName )
- : $request->getBool( $this->mName );
- } else {
- return (bool)$this->getDefault();
- }
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLCheckMatrix.php b/www/wiki/includes/htmlform/HTMLCheckMatrix.php
deleted file mode 100644
index 9f672336..00000000
--- a/www/wiki/includes/htmlform/HTMLCheckMatrix.php
+++ /dev/null
@@ -1,275 +0,0 @@
-<?php
-
-/**
- * A checkbox matrix
- * Operates similarly to HTMLMultiSelectField, but instead of using an array of
- * options, uses an array of rows and an array of columns to dynamically
- * construct a matrix of options. The tags used to identify a particular cell
- * are of the form "columnName-rowName"
- *
- * Options:
- * - columns
- * - Required list of columns in the matrix.
- * - rows
- * - Required list of rows in the matrix.
- * - force-options-on
- * - Accepts array of column-row tags to be displayed as enabled but unavailable to change
- * - force-options-off
- * - Accepts array of column-row tags to be displayed as disabled but unavailable to change.
- * - tooltips
- * - Optional array mapping row label to tooltip content
- * - tooltip-class
- * - Optional CSS class used on tooltip container span. Defaults to mw-icon-question.
- */
-class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
- static private $requiredParams = [
- // Required by underlying HTMLFormField
- 'fieldname',
- // Required by HTMLCheckMatrix
- 'rows',
- 'columns'
- ];
-
- public function __construct( $params ) {
- $missing = array_diff( self::$requiredParams, array_keys( $params ) );
- if ( $missing ) {
- throw new HTMLFormFieldRequiredOptionsException( $this, $missing );
- }
- parent::__construct( $params );
- }
-
- function validate( $value, $alldata ) {
- $rows = $this->mParams['rows'];
- $columns = $this->mParams['columns'];
-
- // Make sure user-defined validation callback is run
- $p = parent::validate( $value, $alldata );
- if ( $p !== true ) {
- return $p;
- }
-
- // Make sure submitted value is an array
- if ( !is_array( $value ) ) {
- return false;
- }
-
- // If all options are valid, array_intersect of the valid options
- // and the provided options will return the provided options.
- $validOptions = [];
- foreach ( $rows as $rowTag ) {
- foreach ( $columns as $columnTag ) {
- $validOptions[] = $columnTag . '-' . $rowTag;
- }
- }
- $validValues = array_intersect( $value, $validOptions );
- if ( count( $validValues ) == count( $value ) ) {
- return true;
- } else {
- return $this->msg( 'htmlform-select-badoption' )->parse();
- }
- }
-
- /**
- * Build a table containing a matrix of checkbox options.
- * The value of each option is a combination of the row tag and column tag.
- * mParams['rows'] is an array with row labels as keys and row tags as values.
- * mParams['columns'] is an array with column labels as keys and column tags as values.
- *
- * @param array $value Array of the options that should be checked
- *
- * @return string
- */
- function getInputHTML( $value ) {
- $html = '';
- $tableContents = '';
- $rows = $this->mParams['rows'];
- $columns = $this->mParams['columns'];
-
- $attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] );
-
- // Build the column headers
- $headerContents = Html::rawElement( 'td', [], '&#160;' );
- foreach ( $columns as $columnLabel => $columnTag ) {
- $headerContents .= Html::rawElement( 'td', [], $columnLabel );
- }
- $tableContents .= Html::rawElement( 'tr', [], "\n$headerContents\n" );
-
- $tooltipClass = 'mw-icon-question';
- if ( isset( $this->mParams['tooltip-class'] ) ) {
- $tooltipClass = $this->mParams['tooltip-class'];
- }
-
- // Build the options matrix
- foreach ( $rows as $rowLabel => $rowTag ) {
- // Append tooltip if configured
- if ( isset( $this->mParams['tooltips'][$rowLabel] ) ) {
- $tooltipAttribs = [
- 'class' => "mw-htmlform-tooltip $tooltipClass",
- 'title' => $this->mParams['tooltips'][$rowLabel],
- ];
- $rowLabel .= ' ' . Html::element( 'span', $tooltipAttribs, '' );
- }
- $rowContents = Html::rawElement( 'td', [], $rowLabel );
- foreach ( $columns as $columnTag ) {
- $thisTag = "$columnTag-$rowTag";
- // Construct the checkbox
- $thisAttribs = [
- 'id' => "{$this->mID}-$thisTag",
- 'value' => $thisTag,
- ];
- $checked = in_array( $thisTag, (array)$value, true );
- if ( $this->isTagForcedOff( $thisTag ) ) {
- $checked = false;
- $thisAttribs['disabled'] = 1;
- } elseif ( $this->isTagForcedOn( $thisTag ) ) {
- $checked = true;
- $thisAttribs['disabled'] = 1;
- }
-
- $checkbox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs );
-
- $rowContents .= Html::rawElement(
- 'td',
- [],
- $checkbox
- );
- }
- $tableContents .= Html::rawElement( 'tr', [], "\n$rowContents\n" );
- }
-
- // Put it all in a table
- $html .= Html::rawElement( 'table',
- [ 'class' => 'mw-htmlform-matrix' ],
- Html::rawElement( 'tbody', [], "\n$tableContents\n" ) ) . "\n";
-
- return $html;
- }
-
- protected function getOneCheckbox( $checked, $attribs ) {
- if ( $this->mParent instanceof OOUIHTMLForm ) {
- return new OOUI\CheckboxInputWidget( [
- 'name' => "{$this->mName}[]",
- 'selected' => $checked,
- ] + OOUI\Element::configFromHtmlAttributes(
- $attribs
- ) );
- } else {
- $checkbox = Xml::check( "{$this->mName}[]", $checked, $attribs );
- if ( $this->mParent->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
- $checkbox = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
- $checkbox .
- Html::element( 'label', [ 'for' => $attribs['id'] ] ) .
- Html::closeElement( 'div' );
- }
- return $checkbox;
- }
- }
-
- protected function isTagForcedOff( $tag ) {
- return isset( $this->mParams['force-options-off'] )
- && in_array( $tag, $this->mParams['force-options-off'] );
- }
-
- protected function isTagForcedOn( $tag ) {
- return isset( $this->mParams['force-options-on'] )
- && in_array( $tag, $this->mParams['force-options-on'] );
- }
-
- /**
- * Get the complete table row for the input, including help text,
- * labels, and whatever.
- * We override this function since the label should always be on a separate
- * line above the options in the case of a checkbox matrix, i.e. it's always
- * a "vertical-label".
- *
- * @param string $value The value to set the input to
- *
- * @return string Complete HTML table row
- */
- function getTableRow( $value ) {
- list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
- $inputHtml = $this->getInputHTML( $value );
- $fieldType = get_class( $this );
- $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
- $cellAttributes = [ 'colspan' => 2 ];
-
- $hideClass = '';
- $hideAttributes = [];
- if ( $this->mHideIf ) {
- $hideAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
- $hideClass = 'mw-htmlform-hide-if';
- }
-
- $label = $this->getLabelHtml( $cellAttributes );
-
- $field = Html::rawElement(
- 'td',
- [ 'class' => 'mw-input' ] + $cellAttributes,
- $inputHtml . "\n$errors"
- );
-
- $html = Html::rawElement( 'tr',
- [ 'class' => "mw-htmlform-vertical-label $hideClass" ] + $hideAttributes,
- $label );
- $html .= Html::rawElement( 'tr',
- [ 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $hideClass" ] +
- $hideAttributes,
- $field );
-
- return $html . $helptext;
- }
-
- /**
- * @param WebRequest $request
- *
- * @return array
- */
- function loadDataFromRequest( $request ) {
- if ( $this->mParent->getMethod() == 'post' ) {
- if ( $request->wasPosted() ) {
- // Checkboxes are not added to the request arrays if they're not checked,
- // so it's perfectly possible for there not to be an entry at all
- return $request->getArray( $this->mName, [] );
- } else {
- // That's ok, the user has not yet submitted the form, so show the defaults
- return $this->getDefault();
- }
- } else {
- // This is the impossible case: if we look at $_GET and see no data for our
- // field, is it because the user has not yet submitted the form, or that they
- // have submitted it with all the options unchecked. We will have to assume the
- // latter, which basically means that you can't specify 'positive' defaults
- // for GET forms.
- return $request->getArray( $this->mName, [] );
- }
- }
-
- function getDefault() {
- if ( isset( $this->mDefault ) ) {
- return $this->mDefault;
- } else {
- return [];
- }
- }
-
- function filterDataForSubmit( $data ) {
- $columns = HTMLFormField::flattenOptions( $this->mParams['columns'] );
- $rows = HTMLFormField::flattenOptions( $this->mParams['rows'] );
- $res = [];
- foreach ( $columns as $column ) {
- foreach ( $rows as $row ) {
- // Make sure option hasn't been forced
- $thisTag = "$column-$row";
- if ( $this->isTagForcedOff( $thisTag ) ) {
- $res[$thisTag] = false;
- } elseif ( $this->isTagForcedOn( $thisTag ) ) {
- $res[$thisTag] = true;
- } else {
- $res[$thisTag] = in_array( $thisTag, $data );
- }
- }
- }
-
- return $res;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLComboboxField.php b/www/wiki/includes/htmlform/HTMLComboboxField.php
deleted file mode 100644
index 778aedbc..00000000
--- a/www/wiki/includes/htmlform/HTMLComboboxField.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-
-/**
- * A combo box field.
- *
- * You can think of it as a dropdown select with the ability to add custom options,
- * or as a text field with input suggestions (autocompletion).
- *
- * When JavaScript is not supported or enabled, it uses HTML5 `<datalist>` element.
- *
- * Besides the parameters recognized by HTMLTextField, the following are
- * recognized:
- * options-messages - As for HTMLSelectField
- * options - As for HTMLSelectField
- * options-message - As for HTMLSelectField
- */
-class HTMLComboboxField extends HTMLTextField {
- // FIXME Ewww, this shouldn't be adding any attributes not requested in $list :(
- public function getAttributes( array $list ) {
- $attribs = [
- 'type' => 'text',
- 'list' => $this->mName . '-datalist',
- ] + parent::getAttributes( $list );
-
- return $attribs;
- }
-
- function getInputHTML( $value ) {
- $datalist = new XmlSelect( false, $this->mName . '-datalist' );
- $datalist->setTagName( 'datalist' );
- $datalist->addOptions( $this->getOptions() );
-
- return parent::getInputHTML( $value ) . $datalist->getHTML();
- }
-
- function getInputOOUI( $value ) {
- $disabled = false;
- $allowedParams = [ 'tabindex' ];
- $attribs = OOUI\Element::configFromHtmlAttributes(
- $this->getAttributes( $allowedParams )
- );
-
- if ( $this->mClass !== '' ) {
- $attribs['classes'] = [ $this->mClass ];
- }
-
- if ( !empty( $this->mParams['disabled'] ) ) {
- $disabled = true;
- }
-
- return new OOUI\ComboBoxInputWidget( [
- 'name' => $this->mName,
- 'id' => $this->mID,
- 'options' => $this->getOptionsOOUI(),
- 'value' => strval( $value ),
- 'disabled' => $disabled,
- ] + $attribs );
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLEditTools.php b/www/wiki/includes/htmlform/HTMLEditTools.php
deleted file mode 100644
index 1b5d1fb4..00000000
--- a/www/wiki/includes/htmlform/HTMLEditTools.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-class HTMLEditTools extends HTMLFormField {
- public function getInputHTML( $value ) {
- return '';
- }
-
- public function getTableRow( $value ) {
- $msg = $this->formatMsg();
-
- return
- '<tr><td></td><td class="mw-input">' .
- '<div class="mw-editTools">' .
- $msg->parseAsBlock() .
- "</div></td></tr>\n";
- }
-
- /**
- * @param string $value
- * @return string
- * @since 1.20
- */
- public function getDiv( $value ) {
- $msg = $this->formatMsg();
-
- return '<div class="mw-editTools">' . $msg->parseAsBlock() . '</div>';
- }
-
- /**
- * @param string $value
- * @return string
- * @since 1.20
- */
- public function getRaw( $value ) {
- return $this->getDiv( $value );
- }
-
- protected function formatMsg() {
- if ( empty( $this->mParams['message'] ) ) {
- $msg = $this->msg( 'edittools' );
- } else {
- $msg = $this->getMessage( $this->mParams['message'] );
- if ( $msg->isDisabled() ) {
- $msg = $this->msg( 'edittools' );
- }
- }
- $msg->inContentLanguage();
-
- return $msg;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLFloatField.php b/www/wiki/includes/htmlform/HTMLFloatField.php
deleted file mode 100644
index 2ef49789..00000000
--- a/www/wiki/includes/htmlform/HTMLFloatField.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-/**
- * A field that will contain a numeric value
- */
-class HTMLFloatField extends HTMLTextField {
- function getSize() {
- return isset( $this->mParams['size'] ) ? $this->mParams['size'] : 20;
- }
-
- function validate( $value, $alldata ) {
- $p = parent::validate( $value, $alldata );
-
- if ( $p !== true ) {
- return $p;
- }
-
- $value = trim( $value );
-
- # http://www.w3.org/TR/html5/infrastructure.html#floating-point-numbers
- # with the addition that a leading '+' sign is ok.
- if ( !preg_match( '/^((\+|\-)?\d+(\.\d+)?(E(\+|\-)?\d+)?)?$/i', $value ) ) {
- return $this->msg( 'htmlform-float-invalid' )->parseAsBlock();
- }
-
- # The "int" part of these message names is rather confusing.
- # They make equal sense for all numbers.
- if ( isset( $this->mParams['min'] ) ) {
- $min = $this->mParams['min'];
-
- if ( $min > $value ) {
- return $this->msg( 'htmlform-int-toolow', $min )->parseAsBlock();
- }
- }
-
- if ( isset( $this->mParams['max'] ) ) {
- $max = $this->mParams['max'];
-
- if ( $max < $value ) {
- return $this->msg( 'htmlform-int-toohigh', $max )->parseAsBlock();
- }
- }
-
- return true;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLForm.php b/www/wiki/includes/htmlform/HTMLForm.php
index 465736bb..af1743e0 100644
--- a/www/wiki/includes/htmlform/HTMLForm.php
+++ b/www/wiki/includes/htmlform/HTMLForm.php
@@ -21,6 +21,8 @@
* @file
*/
+use Wikimedia\ObjectFactory;
+
/**
* Object handling generic submission, CSRF protection, layout and
* other logic for UI forms. in a reusable manner.
@@ -128,44 +130,44 @@
class HTMLForm extends ContextSource {
// A mapping of 'type' inputs onto standard HTMLFormField subclasses
public static $typeMappings = [
- 'api' => 'HTMLApiField',
- 'text' => 'HTMLTextField',
- 'textwithbutton' => 'HTMLTextFieldWithButton',
- 'textarea' => 'HTMLTextAreaField',
- 'select' => 'HTMLSelectField',
- 'combobox' => 'HTMLComboboxField',
- 'radio' => 'HTMLRadioField',
- 'multiselect' => 'HTMLMultiSelectField',
- 'limitselect' => 'HTMLSelectLimitField',
- 'check' => 'HTMLCheckField',
- 'toggle' => 'HTMLCheckField',
- 'int' => 'HTMLIntField',
- 'float' => 'HTMLFloatField',
- 'info' => 'HTMLInfoField',
- 'selectorother' => 'HTMLSelectOrOtherField',
- 'selectandother' => 'HTMLSelectAndOtherField',
- 'namespaceselect' => 'HTMLSelectNamespace',
- 'namespaceselectwithbutton' => 'HTMLSelectNamespaceWithButton',
- 'tagfilter' => 'HTMLTagFilter',
- 'sizefilter' => 'HTMLSizeFilterField',
- 'submit' => 'HTMLSubmitField',
- 'hidden' => 'HTMLHiddenField',
- 'edittools' => 'HTMLEditTools',
- 'checkmatrix' => 'HTMLCheckMatrix',
- 'cloner' => 'HTMLFormFieldCloner',
- 'autocompleteselect' => 'HTMLAutoCompleteSelectField',
- 'date' => 'HTMLDateTimeField',
- 'time' => 'HTMLDateTimeField',
- 'datetime' => 'HTMLDateTimeField',
+ 'api' => HTMLApiField::class,
+ 'text' => HTMLTextField::class,
+ 'textwithbutton' => HTMLTextFieldWithButton::class,
+ 'textarea' => HTMLTextAreaField::class,
+ 'select' => HTMLSelectField::class,
+ 'combobox' => HTMLComboboxField::class,
+ 'radio' => HTMLRadioField::class,
+ 'multiselect' => HTMLMultiSelectField::class,
+ 'limitselect' => HTMLSelectLimitField::class,
+ 'check' => HTMLCheckField::class,
+ 'toggle' => HTMLCheckField::class,
+ 'int' => HTMLIntField::class,
+ 'float' => HTMLFloatField::class,
+ 'info' => HTMLInfoField::class,
+ 'selectorother' => HTMLSelectOrOtherField::class,
+ 'selectandother' => HTMLSelectAndOtherField::class,
+ 'namespaceselect' => HTMLSelectNamespace::class,
+ 'namespaceselectwithbutton' => HTMLSelectNamespaceWithButton::class,
+ 'tagfilter' => HTMLTagFilter::class,
+ 'sizefilter' => HTMLSizeFilterField::class,
+ 'submit' => HTMLSubmitField::class,
+ 'hidden' => HTMLHiddenField::class,
+ 'edittools' => HTMLEditTools::class,
+ 'checkmatrix' => HTMLCheckMatrix::class,
+ 'cloner' => HTMLFormFieldCloner::class,
+ 'autocompleteselect' => HTMLAutoCompleteSelectField::class,
+ 'date' => HTMLDateTimeField::class,
+ 'time' => HTMLDateTimeField::class,
+ 'datetime' => HTMLDateTimeField::class,
// HTMLTextField will output the correct type="" attribute automagically.
// There are about four zillion other HTML5 input types, like range, but
// we don't use those at the moment, so no point in adding all of them.
- 'email' => 'HTMLTextField',
- 'password' => 'HTMLTextField',
- 'url' => 'HTMLTextField',
- 'title' => 'HTMLTitleTextField',
- 'user' => 'HTMLUserTextField',
- 'usersmultiselect' => 'HTMLUsersMultiselectField',
+ 'email' => HTMLTextField::class,
+ 'password' => HTMLTextField::class,
+ 'url' => HTMLTextField::class,
+ 'title' => HTMLTitleTextField::class,
+ 'user' => HTMLUserTextField::class,
+ 'usersmultiselect' => HTMLUsersMultiselectField::class,
];
public $mFieldData;
@@ -213,11 +215,11 @@ class HTMLForm extends ContextSource {
protected $mAction = false;
/**
- * Form attribute autocomplete. false does not set the attribute
+ * Form attribute autocomplete. A typical value is "off". null does not set the attribute
* @since 1.27
- * @var bool|string
+ * @var string|null
*/
- protected $mAutocomplete = false;
+ protected $mAutocomplete = null;
protected $mUseMultipart = false;
protected $mHiddenFields = [];
@@ -429,17 +431,6 @@ class HTMLForm extends ContextSource {
}
/**
- * Test if displayFormat is 'vform'
- * @since 1.22
- * @deprecated since 1.25
- * @return bool
- */
- public function isVForm() {
- wfDeprecated( __METHOD__, '1.25' );
- return false;
- }
-
- /**
* Get the HTMLFormField subclass for this descriptor.
*
* The descriptor can be passed either 'class' which is the name of
@@ -502,7 +493,7 @@ class HTMLForm extends ContextSource {
/**
* Prepare form for submission.
*
- * @attention When doing method chaining, that should be the very last
+ * @warning When doing method chaining, that should be the very last
* method call before displayForm().
*
* @throws MWException
@@ -1004,7 +995,7 @@ class HTMLForm extends ContextSource {
* Display the form (sending to the context's OutputPage object), with an
* appropriate error message or stack of messages, and any validation errors, etc.
*
- * @attention You should call prepareForm() before calling this function.
+ * @warning You should call prepareForm() before calling this function.
* Moreover, when doing method chaining this should be the very last method
* call just after prepareForm().
*
@@ -1062,7 +1053,7 @@ class HTMLForm extends ContextSource {
if ( $this->mId ) {
$attribs['id'] = $this->mId;
}
- if ( $this->mAutocomplete ) {
+ if ( is_string( $this->mAutocomplete ) ) {
$attribs['autocomplete'] = $this->mAutocomplete;
}
if ( $this->mName ) {
@@ -1723,6 +1714,12 @@ class HTMLForm extends ContextSource {
* @return string HTML
*/
protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
+ if ( !$fieldsHtml ) {
+ // Do not generate any wrappers for empty sections. Sections may be empty if they only have
+ // subsections, but no fields. A legend will still be added in wrapFieldSetSection().
+ return '';
+ }
+
$displayFormat = $this->getDisplayFormat();
$html = implode( '', $fieldsHtml );
@@ -1862,12 +1859,12 @@ class HTMLForm extends ContextSource {
}
/**
- * Set the value for the autocomplete attribute of the form.
- * When set to false (which is the default state), the attribute get not set.
+ * Set the value for the autocomplete attribute of the form. A typical value is "off".
+ * When set to null (which is the default state), the attribute get not set.
*
* @since 1.27
*
- * @param string|bool $autocomplete
+ * @param string|null $autocomplete
*
* @return HTMLForm $this for chaining calls
*/
diff --git a/www/wiki/includes/htmlform/HTMLFormElement.php b/www/wiki/includes/htmlform/HTMLFormElement.php
index 10db90cc..2830b9c2 100644
--- a/www/wiki/includes/htmlform/HTMLFormElement.php
+++ b/www/wiki/includes/htmlform/HTMLFormElement.php
@@ -1,7 +1,7 @@
<?php
/**
- * Allows custom data specific to HTMLFormField to be set for OOjs UI forms. A matching JS widget
+ * Allows custom data specific to HTMLFormField to be set for OOUI forms. A matching JS widget
* (defined in htmlform.Element.js) picks up the extra config when constructed using OO.ui.infuse().
*
* Currently only supports passing 'hide-if' data.
@@ -21,7 +21,7 @@ trait HTMLFormElement {
$this->addClasses( [ 'mw-htmlform-hide-if' ] );
}
if ( $this->modules ) {
- // JS code must be able to read this before infusing (before OOjs UI is even loaded),
+ // JS code must be able to read this before infusing (before OOUI is even loaded),
// so we put this in a separate attribute (not with the rest of the config).
// And it's not needed anymore after infusing, so we don't put it in JS config at all.
$this->setAttributes( [ 'data-mw-modules' => implode( ',', $this->modules ) ] );
@@ -38,8 +38,8 @@ class HTMLFormFieldLayout extends OOUI\FieldLayout {
use HTMLFormElement;
public function __construct( $fieldWidget, array $config = [] ) {
- // Parent constructor
parent::__construct( $fieldWidget, $config );
+
// Traits
$this->initializeHTMLFormElement( $config );
}
@@ -53,8 +53,8 @@ class HTMLFormActionFieldLayout extends OOUI\ActionFieldLayout {
use HTMLFormElement;
public function __construct( $fieldWidget, $buttonWidget = false, array $config = [] ) {
- // Parent constructor
parent::__construct( $fieldWidget, $buttonWidget, $config );
+
// Traits
$this->initializeHTMLFormElement( $config );
}
diff --git a/www/wiki/includes/htmlform/HTMLFormField.php b/www/wiki/includes/htmlform/HTMLFormField.php
index e642c2cd..aab88112 100644
--- a/www/wiki/includes/htmlform/HTMLFormField.php
+++ b/www/wiki/includes/htmlform/HTMLFormField.php
@@ -631,7 +631,7 @@ abstract class HTMLFormField {
// the element could specify, that the label doesn't need to be added
$label = $this->getLabel();
- if ( $label ) {
+ if ( $label && $label !== '&#160;' ) {
$config['label'] = new OOUI\HtmlSnippet( $label );
}
@@ -673,7 +673,7 @@ abstract class HTMLFormField {
}
/**
- * Whether the field should be automatically infused. Note that all OOjs UI HTMLForm fields are
+ * Whether the field should be automatically infused. Note that all OOUI HTMLForm fields are
* infusable (you can call OO.ui.infuse() on them), but not all are infused by default, since
* there is no benefit in doing it e.g. for buttons and it's a small performance hit on page load.
*
@@ -686,7 +686,7 @@ abstract class HTMLFormField {
/**
* Get the list of extra ResourceLoader modules which must be loaded client-side before it's
- * possible to infuse this field's OOjs UI widget.
+ * possible to infuse this field's OOUI widget.
*
* @return string[]
*/
diff --git a/www/wiki/includes/htmlform/HTMLFormFieldCloner.php b/www/wiki/includes/htmlform/HTMLFormFieldCloner.php
deleted file mode 100644
index ec1bd842..00000000
--- a/www/wiki/includes/htmlform/HTMLFormFieldCloner.php
+++ /dev/null
@@ -1,380 +0,0 @@
-<?php
-
-/**
- * A container for HTMLFormFields that allows for multiple copies of the set of
- * fields to be displayed to and entered by the user.
- *
- * Recognized parameters, besides the general ones, include:
- * fields - HTMLFormField descriptors for the subfields this cloner manages.
- * The format is just like for the HTMLForm. A field with key 'delete' is
- * special: it must have type = submit and will serve to delete the group
- * of fields.
- * required - If specified, at least one group of fields must be submitted.
- * format - HTMLForm display format to use when displaying the subfields:
- * 'table', 'div', or 'raw'.
- * row-legend - If non-empty, each group of subfields will be enclosed in a
- * fieldset. The value is the name of a message key to use as the legend.
- * create-button-message - Message to use as the text of the button to
- * add an additional group of fields.
- * delete-button-message - Message to use as the text of automatically-
- * generated 'delete' button. Ignored if 'delete' is included in 'fields'.
- *
- * In the generated HTML, the subfields will be named along the lines of
- * "clonerName[index][fieldname]", with ids "clonerId--index--fieldid". 'index'
- * may be a number or an arbitrary string, and may likely change when the page
- * is resubmitted. Cloners may be nested, resulting in field names along the
- * lines of "cloner1Name[index1][cloner2Name][index2][fieldname]" and
- * corresponding ids.
- *
- * Use of cloner may result in submissions of the page that are not submissions
- * of the HTMLForm, when non-JavaScript clients use the create or remove buttons.
- *
- * The result is an array, with values being arrays mapping subfield names to
- * their values. On non-HTMLForm-submission page loads, there may also be
- * additional (string) keys present with other types of values.
- *
- * @since 1.23
- */
-class HTMLFormFieldCloner extends HTMLFormField {
- private static $counter = 0;
-
- /**
- * @var string String uniquely identifying this cloner instance and
- * unlikely to exist otherwise in the generated HTML, while still being
- * valid as part of an HTML id.
- */
- protected $uniqueId;
-
- public function __construct( $params ) {
- $this->uniqueId = get_class( $this ) . ++self::$counter . 'x';
- parent::__construct( $params );
-
- if ( empty( $this->mParams['fields'] ) || !is_array( $this->mParams['fields'] ) ) {
- throw new MWException( 'HTMLFormFieldCloner called without any fields' );
- }
-
- // Make sure the delete button, if explicitly specified, is sane
- if ( isset( $this->mParams['fields']['delete'] ) ) {
- $class = 'mw-htmlform-cloner-delete-button';
- $info = $this->mParams['fields']['delete'] + [
- 'cssclass' => $class
- ];
- unset( $info['name'], $info['class'] );
-
- if ( !isset( $info['type'] ) || $info['type'] !== 'submit' ) {
- throw new MWException(
- 'HTMLFormFieldCloner delete field, if specified, must be of type "submit"'
- );
- }
-
- if ( !in_array( $class, explode( ' ', $info['cssclass'] ) ) ) {
- $info['cssclass'] .= " $class";
- }
-
- $this->mParams['fields']['delete'] = $info;
- }
- }
-
- /**
- * Create the HTMLFormFields that go inside this element, using the
- * specified key.
- *
- * @param string $key Array key under which these fields should be named
- * @return HTMLFormField[]
- */
- protected function createFieldsForKey( $key ) {
- $fields = [];
- foreach ( $this->mParams['fields'] as $fieldname => $info ) {
- $name = "{$this->mName}[$key][$fieldname]";
- if ( isset( $info['name'] ) ) {
- $info['name'] = "{$this->mName}[$key][{$info['name']}]";
- } else {
- $info['name'] = $name;
- }
- if ( isset( $info['id'] ) ) {
- $info['id'] = Sanitizer::escapeId( "{$this->mID}--$key--{$info['id']}" );
- } else {
- $info['id'] = Sanitizer::escapeId( "{$this->mID}--$key--$fieldname" );
- }
- $field = HTMLForm::loadInputFromParameters( $name, $info, $this->mParent );
- $fields[$fieldname] = $field;
- }
- return $fields;
- }
-
- /**
- * Re-key the specified values array to match the names applied by
- * createFieldsForKey().
- *
- * @param string $key Array key under which these fields should be named
- * @param array $values Values array from the request
- * @return array
- */
- protected function rekeyValuesArray( $key, $values ) {
- $data = [];
- foreach ( $values as $fieldname => $value ) {
- $name = "{$this->mName}[$key][$fieldname]";
- $data[$name] = $value;
- }
- return $data;
- }
-
- protected function needsLabel() {
- return false;
- }
-
- public function loadDataFromRequest( $request ) {
- // It's possible that this might be posted with no fields. Detect that
- // by looking for an edit token.
- if ( !$request->getCheck( 'wpEditToken' ) && $request->getArray( $this->mName ) === null ) {
- return $this->getDefault();
- }
-
- $values = $request->getArray( $this->mName );
- if ( $values === null ) {
- $values = [];
- }
-
- $ret = [];
- foreach ( $values as $key => $value ) {
- if ( $key === 'create' || isset( $value['delete'] ) ) {
- $ret['nonjs'] = 1;
- continue;
- }
-
- // Add back in $request->getValues() so things that look for e.g.
- // wpEditToken don't fail.
- $data = $this->rekeyValuesArray( $key, $value ) + $request->getValues();
-
- $fields = $this->createFieldsForKey( $key );
- $subrequest = new DerivativeRequest( $request, $data, $request->wasPosted() );
- $row = [];
- foreach ( $fields as $fieldname => $field ) {
- if ( $field->skipLoadData( $subrequest ) ) {
- continue;
- } elseif ( !empty( $field->mParams['disabled'] ) ) {
- $row[$fieldname] = $field->getDefault();
- } else {
- $row[$fieldname] = $field->loadDataFromRequest( $subrequest );
- }
- }
- $ret[] = $row;
- }
-
- if ( isset( $values['create'] ) ) {
- // Non-JS client clicked the "create" button.
- $fields = $this->createFieldsForKey( $this->uniqueId );
- $row = [];
- foreach ( $fields as $fieldname => $field ) {
- if ( !empty( $field->mParams['nodata'] ) ) {
- continue;
- } else {
- $row[$fieldname] = $field->getDefault();
- }
- }
- $ret[] = $row;
- }
-
- return $ret;
- }
-
- public function getDefault() {
- $ret = parent::getDefault();
-
- // The default default is one entry with all subfields at their
- // defaults.
- if ( $ret === null ) {
- $fields = $this->createFieldsForKey( $this->uniqueId );
- $row = [];
- foreach ( $fields as $fieldname => $field ) {
- if ( !empty( $field->mParams['nodata'] ) ) {
- continue;
- } else {
- $row[$fieldname] = $field->getDefault();
- }
- }
- $ret = [ $row ];
- }
-
- return $ret;
- }
-
- public function cancelSubmit( $values, $alldata ) {
- if ( isset( $values['nonjs'] ) ) {
- return true;
- }
-
- foreach ( $values as $key => $value ) {
- $fields = $this->createFieldsForKey( $key );
- foreach ( $fields as $fieldname => $field ) {
- if ( !array_key_exists( $fieldname, $value ) ) {
- continue;
- }
- if ( $field->cancelSubmit( $value[$fieldname], $alldata ) ) {
- return true;
- }
- }
- }
-
- return parent::cancelSubmit( $values, $alldata );
- }
-
- public function validate( $values, $alldata ) {
- if ( isset( $this->mParams['required'] )
- && $this->mParams['required'] !== false
- && !$values
- ) {
- return $this->msg( 'htmlform-cloner-required' )->parseAsBlock();
- }
-
- if ( isset( $values['nonjs'] ) ) {
- // The submission was a non-JS create/delete click, so fail
- // validation in case cancelSubmit() somehow didn't already handle
- // it.
- return false;
- }
-
- foreach ( $values as $key => $value ) {
- $fields = $this->createFieldsForKey( $key );
- foreach ( $fields as $fieldname => $field ) {
- if ( !array_key_exists( $fieldname, $value ) ) {
- continue;
- }
- $ok = $field->validate( $value[$fieldname], $alldata );
- if ( $ok !== true ) {
- return false;
- }
- }
- }
-
- return parent::validate( $values, $alldata );
- }
-
- /**
- * Get the input HTML for the specified key.
- *
- * @param string $key Array key under which the fields should be named
- * @param array $values
- * @return string
- */
- protected function getInputHTMLForKey( $key, $values ) {
- $displayFormat = isset( $this->mParams['format'] )
- ? $this->mParams['format']
- : $this->mParent->getDisplayFormat();
-
- // Conveniently, PHP method names are case-insensitive.
- $getFieldHtmlMethod = $displayFormat == 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
-
- $html = '';
- $hidden = '';
- $hasLabel = false;
-
- $fields = $this->createFieldsForKey( $key );
- foreach ( $fields as $fieldname => $field ) {
- $v = array_key_exists( $fieldname, $values )
- ? $values[$fieldname]
- : $field->getDefault();
-
- if ( $field instanceof HTMLHiddenField ) {
- // HTMLHiddenField doesn't generate its own HTML
- list( $name, $value, $params ) = $field->getHiddenFieldData( $v );
- $hidden .= Html::hidden( $name, $value, $params ) . "\n";
- } else {
- $html .= $field->$getFieldHtmlMethod( $v );
-
- $labelValue = trim( $field->getLabel() );
- if ( $labelValue != '&#160;' && $labelValue !== '' ) {
- $hasLabel = true;
- }
- }
- }
-
- if ( !isset( $fields['delete'] ) ) {
- $name = "{$this->mName}[$key][delete]";
- $label = isset( $this->mParams['delete-button-message'] )
- ? $this->mParams['delete-button-message']
- : 'htmlform-cloner-delete';
- $field = HTMLForm::loadInputFromParameters( $name, [
- 'type' => 'submit',
- 'name' => $name,
- 'id' => Sanitizer::escapeId( "{$this->mID}--$key--delete" ),
- 'cssclass' => 'mw-htmlform-cloner-delete-button',
- 'default' => $this->getMessage( $label )->text(),
- ], $this->mParent );
- $v = $field->getDefault();
-
- if ( $displayFormat === 'table' ) {
- $html .= $field->$getFieldHtmlMethod( $v );
- } else {
- $html .= $field->getInputHTML( $v );
- }
- }
-
- if ( $displayFormat !== 'raw' ) {
- $classes = [
- 'mw-htmlform-cloner-row',
- ];
-
- if ( !$hasLabel ) { // Avoid strange spacing when no labels exist
- $classes[] = 'mw-htmlform-nolabel';
- }
-
- $attribs = [
- 'class' => implode( ' ', $classes ),
- ];
-
- if ( $displayFormat === 'table' ) {
- $html = Html::rawElement( 'table',
- $attribs,
- Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
- } else {
- $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
- }
- }
-
- $html .= $hidden;
-
- if ( !empty( $this->mParams['row-legend'] ) ) {
- $legend = $this->msg( $this->mParams['row-legend'] )->text();
- $html = Xml::fieldset( $legend, $html );
- }
-
- return $html;
- }
-
- public function getInputHTML( $values ) {
- $html = '';
-
- foreach ( (array)$values as $key => $value ) {
- if ( $key === 'nonjs' ) {
- continue;
- }
- $html .= Html::rawElement( 'li', [ 'class' => 'mw-htmlform-cloner-li' ],
- $this->getInputHTMLForKey( $key, $value )
- );
- }
-
- $template = $this->getInputHTMLForKey( $this->uniqueId, null );
- $html = Html::rawElement( 'ul', [
- 'id' => "mw-htmlform-cloner-list-{$this->mID}",
- 'class' => 'mw-htmlform-cloner-ul',
- 'data-template' => $template,
- 'data-unique-id' => $this->uniqueId,
- ], $html );
-
- $name = "{$this->mName}[create]";
- $label = isset( $this->mParams['create-button-message'] )
- ? $this->mParams['create-button-message']
- : 'htmlform-cloner-create';
- $field = HTMLForm::loadInputFromParameters( $name, [
- 'type' => 'submit',
- 'name' => $name,
- 'id' => Sanitizer::escapeId( "{$this->mID}--create" ),
- 'cssclass' => 'mw-htmlform-cloner-create-button',
- 'default' => $this->getMessage( $label )->text(),
- ], $this->mParent );
- $html .= $field->getInputHTML( $field->getDefault() );
-
- return $html;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLFormFieldWithButton.php b/www/wiki/includes/htmlform/HTMLFormFieldWithButton.php
deleted file mode 100644
index bcb07bd1..00000000
--- a/www/wiki/includes/htmlform/HTMLFormFieldWithButton.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-/**
- * Enables HTMLFormField elements to be build with a button.
- */
-class HTMLFormFieldWithButton extends HTMLFormField {
- /** @var string $mButtonClass CSS class for the button in this field */
- protected $mButtonClass = '';
-
- /** @var string|integer $mButtonId Element ID for the button in this field */
- protected $mButtonId = '';
-
- /** @var string $mButtonName Name the button in this field */
- protected $mButtonName = '';
-
- /** @var string $mButtonType Type of the button in this field (e.g. button or submit) */
- protected $mButtonType = 'submit';
-
- /** @var string $mButtonType Value for the button in this field */
- protected $mButtonValue;
-
- /** @var string $mButtonType Value for the button in this field */
- protected $mButtonFlags = [ 'progressive' ];
-
- public function __construct( $info ) {
- if ( isset( $info['buttonclass'] ) ) {
- $this->mButtonClass = $info['buttonclass'];
- }
- if ( isset( $info['buttonid'] ) ) {
- $this->mButtonId = $info['buttonid'];
- }
- if ( isset( $info['buttonname'] ) ) {
- $this->mButtonName = $info['buttonname'];
- }
- if ( isset( $info['buttondefault'] ) ) {
- $this->mButtonValue = $info['buttondefault'];
- }
- if ( isset( $info['buttontype'] ) ) {
- $this->mButtonType = $info['buttontype'];
- }
- if ( isset( $info['buttonflags'] ) ) {
- $this->mButtonFlags = $info['buttonflags'];
- }
- parent::__construct( $info );
- }
-
- public function getInputHTML( $value ) {
- $attr = [
- 'class' => 'mw-htmlform-submit ' . $this->mButtonClass,
- 'id' => $this->mButtonId,
- ] + $this->getAttributes( [ 'disabled', 'tabindex' ] );
-
- return Html::input( $this->mButtonName, $this->mButtonValue, $this->mButtonType, $attr );
- }
-
- public function getInputOOUI( $value ) {
- return new OOUI\ButtonInputWidget( [
- 'name' => $this->mButtonName,
- 'value' => $this->mButtonValue,
- 'type' => $this->mButtonType,
- 'label' => $this->mButtonValue,
- 'flags' => $this->mButtonFlags,
- ] + OOUI\Element::configFromHtmlAttributes(
- $this->getAttributes( [ 'disabled', 'tabindex' ] )
- ) );
- }
-
- /**
- * Combines the passed element with a button.
- * @param String $element Element to combine the button with.
- * @return String
- */
- public function getElement( $element ) {
- return $element . '&#160;' . $this->getInputHTML( '' );
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLHiddenField.php b/www/wiki/includes/htmlform/HTMLHiddenField.php
deleted file mode 100644
index c0fce2ba..00000000
--- a/www/wiki/includes/htmlform/HTMLHiddenField.php
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-class HTMLHiddenField extends HTMLFormField {
- protected $outputAsDefault = true;
-
- public function __construct( $params ) {
- parent::__construct( $params );
-
- if ( isset( $this->mParams['output-as-default'] ) ) {
- $this->outputAsDefault = (bool)$this->mParams['output-as-default'];
- }
-
- # Per HTML5 spec, hidden fields cannot be 'required'
- # http://www.w3.org/TR/html5/forms.html#hidden-state-%28type=hidden%29
- unset( $this->mParams['required'] );
- }
-
- public function getHiddenFieldData( $value ) {
- $params = [];
- if ( $this->mID ) {
- $params['id'] = $this->mID;
- }
-
- if ( $this->outputAsDefault ) {
- $value = $this->mDefault;
- }
-
- return [ $this->mName, $value, $params ];
- }
-
- public function getTableRow( $value ) {
- list( $name, $value, $params ) = $this->getHiddenFieldData( $value );
- $this->mParent->addHiddenField( $name, $value, $params );
- return '';
- }
-
- /**
- * @param string $value
- * @return string
- * @since 1.20
- */
- public function getDiv( $value ) {
- return $this->getTableRow( $value );
- }
-
- /**
- * @param string $value
- * @return string
- * @since 1.20
- */
- public function getRaw( $value ) {
- return $this->getTableRow( $value );
- }
-
- public function getInputHTML( $value ) {
- return '';
- }
-
- public function canDisplayErrors() {
- return false;
- }
-
- public function hasVisibleOutput() {
- return false;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLInfoField.php b/www/wiki/includes/htmlform/HTMLInfoField.php
deleted file mode 100644
index ada4fb67..00000000
--- a/www/wiki/includes/htmlform/HTMLInfoField.php
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-
-/**
- * An information field (text blob), not a proper input.
- */
-class HTMLInfoField extends HTMLFormField {
- public function __construct( $info ) {
- $info['nodata'] = true;
-
- parent::__construct( $info );
- }
-
- public function getInputHTML( $value ) {
- return !empty( $this->mParams['raw'] ) ? $value : htmlspecialchars( $value );
- }
-
- public function getInputOOUI( $value ) {
- if ( !empty( $this->mParams['raw'] ) ) {
- $value = new OOUI\HtmlSnippet( $value );
- }
-
- return new OOUI\LabelWidget( [
- 'label' => $value,
- ] );
- }
-
- public function getTableRow( $value ) {
- if ( !empty( $this->mParams['rawrow'] ) ) {
- return $value;
- }
-
- return parent::getTableRow( $value );
- }
-
- /**
- * @param string $value
- * @return string
- * @since 1.20
- */
- public function getDiv( $value ) {
- if ( !empty( $this->mParams['rawrow'] ) ) {
- return $value;
- }
-
- return parent::getDiv( $value );
- }
-
- /**
- * @param string $value
- * @return string
- * @since 1.20
- */
- public function getRaw( $value ) {
- if ( !empty( $this->mParams['rawrow'] ) ) {
- return $value;
- }
-
- return parent::getRaw( $value );
- }
-
- protected function needsLabel() {
- return false;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLIntField.php b/www/wiki/includes/htmlform/HTMLIntField.php
deleted file mode 100644
index b0148d98..00000000
--- a/www/wiki/includes/htmlform/HTMLIntField.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-/**
- * A field that must contain a number
- */
-class HTMLIntField extends HTMLFloatField {
- function validate( $value, $alldata ) {
- $p = parent::validate( $value, $alldata );
-
- if ( $p !== true ) {
- return $p;
- }
-
- # http://www.w3.org/TR/html5/infrastructure.html#signed-integers
- # with the addition that a leading '+' sign is ok. Note that leading zeros
- # are fine, and will be left in the input, which is useful for things like
- # phone numbers when you know that they are integers (the HTML5 type=tel
- # input does not require its value to be numeric). If you want a tidier
- # value to, eg, save in the DB, clean it up with intval().
- if ( !preg_match( '/^((\+|\-)?\d+)?$/', trim( $value ) ) ) {
- return $this->msg( 'htmlform-int-invalid' )->parseAsBlock();
- }
-
- return true;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLMultiSelectField.php b/www/wiki/includes/htmlform/HTMLMultiSelectField.php
deleted file mode 100644
index 1aaa3e88..00000000
--- a/www/wiki/includes/htmlform/HTMLMultiSelectField.php
+++ /dev/null
@@ -1,151 +0,0 @@
-<?php
-
-/**
- * Multi-select field
- */
-class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable {
- function validate( $value, $alldata ) {
- $p = parent::validate( $value, $alldata );
-
- if ( $p !== true ) {
- return $p;
- }
-
- if ( !is_array( $value ) ) {
- return false;
- }
-
- # If all options are valid, array_intersect of the valid options
- # and the provided options will return the provided options.
- $validOptions = HTMLFormField::flattenOptions( $this->getOptions() );
-
- $validValues = array_intersect( $value, $validOptions );
- if ( count( $validValues ) == count( $value ) ) {
- return true;
- } else {
- return $this->msg( 'htmlform-select-badoption' )->parse();
- }
- }
-
- function getInputHTML( $value ) {
- $value = HTMLFormField::forceToStringRecursive( $value );
- $html = $this->formatOptions( $this->getOptions(), $value );
-
- return $html;
- }
-
- function formatOptions( $options, $value ) {
- $html = '';
-
- $attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] );
-
- foreach ( $options as $label => $info ) {
- if ( is_array( $info ) ) {
- $html .= Html::rawElement( 'h1', [], $label ) . "\n";
- $html .= $this->formatOptions( $info, $value );
- } else {
- $thisAttribs = [
- 'id' => "{$this->mID}-$info",
- 'value' => $info,
- ];
- $checked = in_array( $info, $value, true );
-
- $checkbox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs, $label );
-
- $html .= ' ' . Html::rawElement(
- 'div',
- [ 'class' => 'mw-htmlform-flatlist-item' ],
- $checkbox
- );
- }
- }
-
- return $html;
- }
-
- protected function getOneCheckbox( $checked, $attribs, $label ) {
- if ( $this->mParent instanceof OOUIHTMLForm ) {
- if ( $this->mOptionsLabelsNotFromMessage ) {
- $label = new OOUI\HtmlSnippet( $label );
- }
- return new OOUI\FieldLayout(
- new OOUI\CheckboxInputWidget( [
- 'name' => "{$this->mName}[]",
- 'selected' => $checked,
- ] + OOUI\Element::configFromHtmlAttributes(
- $attribs
- ) ),
- [
- 'label' => $label,
- 'align' => 'inline',
- ]
- );
- } else {
- $elementFunc = [ 'Html', $this->mOptionsLabelsNotFromMessage ? 'rawElement' : 'element' ];
- $checkbox =
- Xml::check( "{$this->mName}[]", $checked, $attribs ) .
- '&#160;' .
- call_user_func( $elementFunc,
- 'label',
- [ 'for' => $attribs['id'] ],
- $label
- );
- if ( $this->mParent->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
- $checkbox = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
- $checkbox .
- Html::closeElement( 'div' );
- }
- return $checkbox;
- }
- }
-
- /**
- * @param WebRequest $request
- *
- * @return string
- */
- function loadDataFromRequest( $request ) {
- if ( $this->mParent->getMethod() == 'post' ) {
- if ( $request->wasPosted() ) {
- # Checkboxes are just not added to the request arrays if they're not checked,
- # so it's perfectly possible for there not to be an entry at all
- return $request->getArray( $this->mName, [] );
- } else {
- # That's ok, the user has not yet submitted the form, so show the defaults
- return $this->getDefault();
- }
- } else {
- # This is the impossible case: if we look at $_GET and see no data for our
- # field, is it because the user has not yet submitted the form, or that they
- # have submitted it with all the options unchecked? We will have to assume the
- # latter, which basically means that you can't specify 'positive' defaults
- # for GET forms.
- # @todo FIXME...
- return $request->getArray( $this->mName, [] );
- }
- }
-
- function getDefault() {
- if ( isset( $this->mDefault ) ) {
- return $this->mDefault;
- } else {
- return [];
- }
- }
-
- function filterDataForSubmit( $data ) {
- $data = HTMLFormField::forceToStringRecursive( $data );
- $options = HTMLFormField::flattenOptions( $this->getOptions() );
-
- $res = [];
- foreach ( $options as $opt ) {
- $res["$opt"] = in_array( $opt, $data, true );
- }
-
- return $res;
- }
-
- protected function needsLabel() {
- return false;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLRadioField.php b/www/wiki/includes/htmlform/HTMLRadioField.php
deleted file mode 100644
index 12a8a1fd..00000000
--- a/www/wiki/includes/htmlform/HTMLRadioField.php
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-
-/**
- * Radio checkbox fields.
- */
-class HTMLRadioField extends HTMLFormField {
- function validate( $value, $alldata ) {
- $p = parent::validate( $value, $alldata );
-
- if ( $p !== true ) {
- return $p;
- }
-
- if ( !is_string( $value ) && !is_int( $value ) ) {
- return false;
- }
-
- $validOptions = HTMLFormField::flattenOptions( $this->getOptions() );
-
- if ( in_array( strval( $value ), $validOptions, true ) ) {
- return true;
- } else {
- return $this->msg( 'htmlform-select-badoption' )->parse();
- }
- }
-
- /**
- * This returns a block of all the radio options, in one cell.
- * @see includes/HTMLFormField#getInputHTML()
- *
- * @param string $value
- *
- * @return string
- */
- function getInputHTML( $value ) {
- $html = $this->formatOptions( $this->getOptions(), strval( $value ) );
-
- return $html;
- }
-
- function getInputOOUI( $value ) {
- $options = [];
- foreach ( $this->getOptions() as $label => $data ) {
- $options[] = [
- 'data' => $data,
- 'label' => $this->mOptionsLabelsNotFromMessage ? new OOUI\HtmlSnippet( $label ) : $label,
- ];
- }
-
- return new OOUI\RadioSelectInputWidget( [
- 'name' => $this->mName,
- 'id' => $this->mID,
- 'value' => $value,
- 'options' => $options,
- 'classes' => 'mw-htmlform-flatlist-item',
- ] + OOUI\Element::configFromHtmlAttributes(
- $this->getAttributes( [ 'disabled', 'tabindex' ] )
- ) );
- }
-
- function formatOptions( $options, $value ) {
- $html = '';
-
- $attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] );
- $elementFunc = [ 'Html', $this->mOptionsLabelsNotFromMessage ? 'rawElement' : 'element' ];
-
- # @todo Should this produce an unordered list perhaps?
- foreach ( $options as $label => $info ) {
- if ( is_array( $info ) ) {
- $html .= Html::rawElement( 'h1', [], $label ) . "\n";
- $html .= $this->formatOptions( $info, $value );
- } else {
- $id = Sanitizer::escapeId( $this->mID . "-$info" );
- $radio = Xml::radio( $this->mName, $info, $info === $value, $attribs + [ 'id' => $id ] );
- $radio .= '&#160;' . call_user_func( $elementFunc, 'label', [ 'for' => $id ], $label );
-
- $html .= ' ' . Html::rawElement(
- 'div',
- [ 'class' => 'mw-htmlform-flatlist-item mw-ui-radio' ],
- $radio
- );
- }
- }
-
- return $html;
- }
-
- protected function needsLabel() {
- return false;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLSelectAndOtherField.php b/www/wiki/includes/htmlform/HTMLSelectAndOtherField.php
deleted file mode 100644
index e75c2b25..00000000
--- a/www/wiki/includes/htmlform/HTMLSelectAndOtherField.php
+++ /dev/null
@@ -1,134 +0,0 @@
-<?php
-
-/**
- * Double field with a dropdown list constructed from a system message in the format
- * * Optgroup header
- * ** <option value>
- * * New Optgroup header
- * Plus a text field underneath for an additional reason. The 'value' of the field is
- * "<select>: <extra reason>", or "<extra reason>" if nothing has been selected in the
- * select dropdown.
- * @todo FIXME: If made 'required', only the text field should be compulsory.
- */
-class HTMLSelectAndOtherField extends HTMLSelectField {
- function __construct( $params ) {
- if ( array_key_exists( 'other', $params ) ) {
- // Do nothing
- } elseif ( array_key_exists( 'other-message', $params ) ) {
- $params['other'] = $this->getMessage( $params['other-message'] )->plain();
- } else {
- $params['other'] = $this->msg( 'htmlform-selectorother-other' )->plain();
- }
-
- parent::__construct( $params );
-
- if ( $this->getOptions() === null ) {
- // Sulk
- throw new MWException( 'HTMLSelectAndOtherField called without any options' );
- }
- if ( !in_array( 'other', $this->mOptions, true ) ) {
- // Have 'other' always as first element
- $this->mOptions = [ $params['other'] => 'other' ] + $this->mOptions;
- }
- $this->mFlatOptions = self::flattenOptions( $this->getOptions() );
-
- }
-
- function getInputHTML( $value ) {
- $select = parent::getInputHTML( $value[1] );
-
- $textAttribs = [
- 'id' => $this->mID . '-other',
- 'size' => $this->getSize(),
- 'class' => [ 'mw-htmlform-select-and-other-field' ],
- 'data-id-select' => $this->mID,
- ];
-
- if ( $this->mClass !== '' ) {
- $textAttribs['class'][] = $this->mClass;
- }
-
- $allowedParams = [
- 'required',
- 'autofocus',
- 'multiple',
- 'disabled',
- 'tabindex',
- 'maxlength', // gets dynamic with javascript, see mediawiki.htmlform.js
- ];
-
- $textAttribs += $this->getAttributes( $allowedParams );
-
- $textbox = Html::input( $this->mName . '-other', $value[2], 'text', $textAttribs );
-
- return "$select<br />\n$textbox";
- }
-
- function getInputOOUI( $value ) {
- return false;
- }
-
- /**
- * @param WebRequest $request
- *
- * @return array("<overall message>","<select value>","<text field value>")
- */
- function loadDataFromRequest( $request ) {
- if ( $request->getCheck( $this->mName ) ) {
- $list = $request->getText( $this->mName );
- $text = $request->getText( $this->mName . '-other' );
-
- // Should be built the same as in mediawiki.htmlform.js
- if ( $list == 'other' ) {
- $final = $text;
- } elseif ( !in_array( $list, $this->mFlatOptions, true ) ) {
- # User has spoofed the select form to give an option which wasn't
- # in the original offer. Sulk...
- $final = $text;
- } elseif ( $text == '' ) {
- $final = $list;
- } else {
- $final = $list . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $text;
- }
- } else {
- $final = $this->getDefault();
-
- $list = 'other';
- $text = $final;
- foreach ( $this->mFlatOptions as $option ) {
- $match = $option . $this->msg( 'colon-separator' )->inContentLanguage()->text();
- if ( strpos( $text, $match ) === 0 ) {
- $list = $option;
- $text = substr( $text, strlen( $match ) );
- break;
- }
- }
- }
-
- return [ $final, $list, $text ];
- }
-
- function getSize() {
- return isset( $this->mParams['size'] ) ? $this->mParams['size'] : 45;
- }
-
- function validate( $value, $alldata ) {
- # HTMLSelectField forces $value to be one of the options in the select
- # field, which is not useful here. But we do want the validation further up
- # the chain
- $p = parent::validate( $value[1], $alldata );
-
- if ( $p !== true ) {
- return $p;
- }
-
- if ( isset( $this->mParams['required'] )
- && $this->mParams['required'] !== false
- && $value[1] === ''
- ) {
- return $this->msg( 'htmlform-required' )->parse();
- }
-
- return true;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLSelectField.php b/www/wiki/includes/htmlform/HTMLSelectField.php
deleted file mode 100644
index b6ad46c5..00000000
--- a/www/wiki/includes/htmlform/HTMLSelectField.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-/**
- * A select dropdown field. Basically a wrapper for Xmlselect class
- */
-class HTMLSelectField extends HTMLFormField {
- function validate( $value, $alldata ) {
- $p = parent::validate( $value, $alldata );
-
- if ( $p !== true ) {
- return $p;
- }
-
- $validOptions = HTMLFormField::flattenOptions( $this->getOptions() );
-
- if ( in_array( strval( $value ), $validOptions, true ) ) {
- return true;
- } else {
- return $this->msg( 'htmlform-select-badoption' )->parse();
- }
- }
-
- function getInputHTML( $value ) {
- $select = new XmlSelect( $this->mName, $this->mID, strval( $value ) );
-
- if ( !empty( $this->mParams['disabled'] ) ) {
- $select->setAttribute( 'disabled', 'disabled' );
- }
-
- $allowedParams = [ 'tabindex', 'size' ];
- $customParams = $this->getAttributes( $allowedParams );
- foreach ( $customParams as $name => $value ) {
- $select->setAttribute( $name, $value );
- }
-
- if ( $this->mClass !== '' ) {
- $select->setAttribute( 'class', $this->mClass );
- }
-
- $select->addOptions( $this->getOptions() );
-
- return $select->getHTML();
- }
-
- function getInputOOUI( $value ) {
- $disabled = false;
- $allowedParams = [ 'tabindex' ];
- $attribs = OOUI\Element::configFromHtmlAttributes(
- $this->getAttributes( $allowedParams )
- );
-
- if ( $this->mClass !== '' ) {
- $attribs['classes'] = [ $this->mClass ];
- }
-
- if ( !empty( $this->mParams['disabled'] ) ) {
- $disabled = true;
- }
-
- return new OOUI\DropdownInputWidget( [
- 'name' => $this->mName,
- 'id' => $this->mID,
- 'options' => $this->getOptionsOOUI(),
- 'value' => strval( $value ),
- 'disabled' => $disabled,
- ] + $attribs );
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLSelectLimitField.php b/www/wiki/includes/htmlform/HTMLSelectLimitField.php
deleted file mode 100644
index e7f1c047..00000000
--- a/www/wiki/includes/htmlform/HTMLSelectLimitField.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-/**
- * A limit dropdown, which accepts any valid number
- */
-class HTMLSelectLimitField extends HTMLSelectField {
- /**
- * Basically don't do any validation. If it's a number that's fine. Also,
- * add it to the list if it's not there already
- *
- * @param string $value
- * @param array $alldata
- * @return bool
- */
- function validate( $value, $alldata ) {
- if ( $value == '' ) {
- return true;
- }
-
- // Let folks pick an explicit limit not from our list, as long as it's a real numbr.
- if ( !in_array( $value, $this->mParams['options'] )
- && $value == intval( $value )
- && $value > 0
- ) {
- // This adds the explicitly requested limit value to the drop-down,
- // then makes sure it's sorted correctly so when we output the list
- // later, the custom option doesn't just show up last.
- $this->mParams['options'][$this->mParent->getLanguage()->formatNum( $value )] =
- intval( $value );
- asort( $this->mParams['options'] );
- }
-
- return true;
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLSelectNamespace.php b/www/wiki/includes/htmlform/HTMLSelectNamespace.php
deleted file mode 100644
index ef219690..00000000
--- a/www/wiki/includes/htmlform/HTMLSelectNamespace.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-/**
- * Wrapper for Html::namespaceSelector to use in HTMLForm
- */
-class HTMLSelectNamespace extends HTMLFormField {
- public function __construct( $params ) {
- parent::__construct( $params );
-
- $this->mAllValue = array_key_exists( 'all', $params )
- ? $params['all']
- : 'all';
-
- }
-
- function getInputHTML( $value ) {
- return Html::namespaceSelector(
- [
- 'selected' => $value,
- 'all' => $this->mAllValue
- ], [
- 'name' => $this->mName,
- 'id' => $this->mID,
- 'class' => 'namespaceselector',
- ]
- );
- }
-
- public function getInputOOUI( $value ) {
- return new MediaWiki\Widget\NamespaceInputWidget( [
- 'value' => $value,
- 'name' => $this->mName,
- 'id' => $this->mID,
- 'includeAllValue' => $this->mAllValue,
- ] );
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLSelectNamespaceWithButton.php b/www/wiki/includes/htmlform/HTMLSelectNamespaceWithButton.php
deleted file mode 100644
index 24b15bd7..00000000
--- a/www/wiki/includes/htmlform/HTMLSelectNamespaceWithButton.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-/**
- * Creates a Html::namespaceSelector input field with a button assigned to the input field.
- */
-class HTMLSelectNamespaceWithButton extends HTMLSelectNamespace {
- /** @var HTMLFormClassWithButton $mClassWithButton */
- protected $mClassWithButton = null;
-
- public function __construct( $info ) {
- $this->mClassWithButton = new HTMLFormFieldWithButton( $info );
- parent::__construct( $info );
- }
-
- public function getInputHTML( $value ) {
- return $this->mClassWithButton->getElement( parent::getInputHTML( $value ) );
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLSelectOrOtherField.php b/www/wiki/includes/htmlform/HTMLSelectOrOtherField.php
deleted file mode 100644
index 8f7750c0..00000000
--- a/www/wiki/includes/htmlform/HTMLSelectOrOtherField.php
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-
-/**
- * Select dropdown field, with an additional "other" textbox.
- *
- * HTMLComboboxField implements the same functionality using a single form field
- * and should be used instead.
- */
-class HTMLSelectOrOtherField extends HTMLTextField {
- function __construct( $params ) {
- parent::__construct( $params );
- $this->getOptions();
- if ( !in_array( 'other', $this->mOptions, true ) ) {
- $msg =
- isset( $params['other'] )
- ? $params['other']
- : wfMessage( 'htmlform-selectorother-other' )->text();
- // Have 'other' always as first element
- $this->mOptions = [ $msg => 'other' ] + $this->mOptions;
- }
-
- }
-
- function getInputHTML( $value ) {
- $valInSelect = false;
-
- if ( $value !== false ) {
- $value = strval( $value );
- $valInSelect = in_array(
- $value, HTMLFormField::flattenOptions( $this->getOptions() ), true
- );
- }
-
- $selected = $valInSelect ? $value : 'other';
-
- $select = new XmlSelect( $this->mName, $this->mID, $selected );
- $select->addOptions( $this->getOptions() );
-
- $select->setAttribute( 'class', 'mw-htmlform-select-or-other' );
-
- $tbAttribs = [ 'id' => $this->mID . '-other', 'size' => $this->getSize() ];
-
- if ( !empty( $this->mParams['disabled'] ) ) {
- $select->setAttribute( 'disabled', 'disabled' );
- $tbAttribs['disabled'] = 'disabled';
- }
-
- if ( isset( $this->mParams['tabindex'] ) ) {
- $select->setAttribute( 'tabindex', $this->mParams['tabindex'] );
- $tbAttribs['tabindex'] = $this->mParams['tabindex'];
- }
-
- $select = $select->getHTML();
-
- if ( isset( $this->mParams['maxlength'] ) ) {
- $tbAttribs['maxlength'] = $this->mParams['maxlength'];
- }
-
- if ( $this->mClass !== '' ) {
- $tbAttribs['class'] = $this->mClass;
- }
-
- $textbox = Html::input( $this->mName . '-other', $valInSelect ? '' : $value, 'text', $tbAttribs );
-
- return "$select<br />\n$textbox";
- }
-
- function getInputOOUI( $value ) {
- return false;
- }
-
- /**
- * @param WebRequest $request
- *
- * @return string
- */
- function loadDataFromRequest( $request ) {
- if ( $request->getCheck( $this->mName ) ) {
- $val = $request->getText( $this->mName );
-
- if ( $val === 'other' ) {
- $val = $request->getText( $this->mName . '-other' );
- }
-
- return $val;
- } else {
- return $this->getDefault();
- }
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLSubmitField.php b/www/wiki/includes/htmlform/HTMLSubmitField.php
deleted file mode 100644
index cb98549e..00000000
--- a/www/wiki/includes/htmlform/HTMLSubmitField.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * Add a submit button inline in the form (as opposed to
- * HTMLForm::addButton(), which will add it at the end).
- */
-class HTMLSubmitField extends HTMLButtonField {
- protected $buttonType = 'submit';
-
- protected $mFlags = [ 'primary', 'constructive' ];
-
- public function skipLoadData( $request ) {
- return !$request->getCheck( $this->mName );
- }
-
- public function loadDataFromRequest( $request ) {
- return $request->getCheck( $this->mName );
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLTagFilter.php b/www/wiki/includes/htmlform/HTMLTagFilter.php
deleted file mode 100644
index 8075de5a..00000000
--- a/www/wiki/includes/htmlform/HTMLTagFilter.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-/**
- * Wrapper for ChangeTags::buildTagFilterSelector to use in HTMLForm
- */
-class HTMLTagFilter extends HTMLFormField {
- protected $tagFilter;
-
- function getTableRow( $value ) {
- $this->tagFilter = ChangeTags::buildTagFilterSelector( $value );
- if ( $this->tagFilter ) {
- return parent::getTableRow( $value );
- }
- return '';
- }
-
- function getDiv( $value ) {
- $this->tagFilter = ChangeTags::buildTagFilterSelector( $value );
- if ( $this->tagFilter ) {
- return parent::getDiv( $value );
- }
- return '';
- }
-
- function getInputHTML( $value ) {
- if ( $this->tagFilter ) {
- // we only need the select field, HTMLForm should handle the label
- return $this->tagFilter[1];
- }
- return '';
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLTextAreaField.php b/www/wiki/includes/htmlform/HTMLTextAreaField.php
deleted file mode 100644
index 8ffff438..00000000
--- a/www/wiki/includes/htmlform/HTMLTextAreaField.php
+++ /dev/null
@@ -1,103 +0,0 @@
-<?php
-
-class HTMLTextAreaField extends HTMLFormField {
- const DEFAULT_COLS = 80;
- const DEFAULT_ROWS = 25;
-
- protected $mPlaceholder = '';
-
- /**
- * @param array $params
- * - cols, rows: textarea size
- * - placeholder/placeholder-message: set HTML placeholder attribute
- * - spellcheck: set HTML spellcheck attribute
- */
- public function __construct( $params ) {
- parent::__construct( $params );
-
- if ( isset( $params['placeholder-message'] ) ) {
- $this->mPlaceholder = $this->getMessage( $params['placeholder-message'] )->parse();
- } elseif ( isset( $params['placeholder'] ) ) {
- $this->mPlaceholder = $params['placeholder'];
- }
- }
-
- function getCols() {
- return isset( $this->mParams['cols'] ) ? $this->mParams['cols'] : static::DEFAULT_COLS;
- }
-
- function getRows() {
- return isset( $this->mParams['rows'] ) ? $this->mParams['rows'] : static::DEFAULT_ROWS;
- }
-
- function getSpellCheck() {
- $val = isset( $this->mParams['spellcheck'] ) ? $this->mParams['spellcheck'] : null;
- if ( is_bool( $val ) ) {
- // "spellcheck" attribute literally requires "true" or "false" to work.
- return $val === true ? 'true' : 'false';
- }
- return null;
- }
-
- function getInputHTML( $value ) {
- $attribs = [
- 'id' => $this->mID,
- 'cols' => $this->getCols(),
- 'rows' => $this->getRows(),
- 'spellcheck' => $this->getSpellCheck(),
- ] + $this->getTooltipAndAccessKey();
-
- if ( $this->mClass !== '' ) {
- $attribs['class'] = $this->mClass;
- }
- if ( $this->mPlaceholder !== '' ) {
- $attribs['placeholder'] = $this->mPlaceholder;
- }
-
- $allowedParams = [
- 'tabindex',
- 'disabled',
- 'readonly',
- 'required',
- 'autofocus'
- ];
-
- $attribs += $this->getAttributes( $allowedParams );
- return Html::textarea( $this->mName, $value, $attribs );
- }
-
- function getInputOOUI( $value ) {
- if ( isset( $this->mParams['cols'] ) ) {
- throw new Exception( "OOUIHTMLForm does not support the 'cols' parameter for textareas" );
- }
-
- $attribs = $this->getTooltipAndAccessKey();
-
- if ( $this->mClass !== '' ) {
- $attribs['classes'] = [ $this->mClass ];
- }
- if ( $this->mPlaceholder !== '' ) {
- $attribs['placeholder'] = $this->mPlaceholder;
- }
-
- $allowedParams = [
- 'tabindex',
- 'disabled',
- 'readonly',
- 'required',
- 'autofocus',
- ];
-
- $attribs += OOUI\Element::configFromHtmlAttributes(
- $this->getAttributes( $allowedParams )
- );
-
- return new OOUI\TextInputWidget( [
- 'id' => $this->mID,
- 'name' => $this->mName,
- 'multiline' => true,
- 'value' => $value,
- 'rows' => $this->getRows(),
- ] + $attribs );
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLTextField.php b/www/wiki/includes/htmlform/HTMLTextField.php
deleted file mode 100644
index 3ab71766..00000000
--- a/www/wiki/includes/htmlform/HTMLTextField.php
+++ /dev/null
@@ -1,177 +0,0 @@
-<?php
-
-class HTMLTextField extends HTMLFormField {
- protected $mPlaceholder = '';
-
- /**
- * @param array $params
- * - type: HTML textfield type
- * - size: field size in characters (defaults to 45)
- * - placeholder/placeholder-message: set HTML placeholder attribute
- * - spellcheck: set HTML spellcheck attribute
- * - persistent: upon unsuccessful requests, retain the value (defaults to true, except
- * for password fields)
- */
- public function __construct( $params ) {
- parent::__construct( $params );
-
- if ( isset( $params['placeholder-message'] ) ) {
- $this->mPlaceholder = $this->getMessage( $params['placeholder-message'] )->parse();
- } elseif ( isset( $params['placeholder'] ) ) {
- $this->mPlaceholder = $params['placeholder'];
- }
- }
-
- function getSize() {
- return isset( $this->mParams['size'] ) ? $this->mParams['size'] : 45;
- }
-
- function getSpellCheck() {
- $val = isset( $this->mParams['spellcheck'] ) ? $this->mParams['spellcheck'] : null;
- if ( is_bool( $val ) ) {
- // "spellcheck" attribute literally requires "true" or "false" to work.
- return $val === true ? 'true' : 'false';
- }
- return null;
- }
-
- public function isPersistent() {
- if ( isset( $this->mParams['persistent'] ) ) {
- return $this->mParams['persistent'];
- }
- // don't put passwords into the HTML body, they could get cached or otherwise leaked
- return !( isset( $this->mParams['type'] ) && $this->mParams['type'] === 'password' );
- }
-
- function getInputHTML( $value ) {
- if ( !$this->isPersistent() ) {
- $value = '';
- }
-
- $attribs = [
- 'id' => $this->mID,
- 'name' => $this->mName,
- 'size' => $this->getSize(),
- 'value' => $value,
- 'dir' => $this->mDir,
- 'spellcheck' => $this->getSpellCheck(),
- ] + $this->getTooltipAndAccessKey() + $this->getDataAttribs();
-
- if ( $this->mClass !== '' ) {
- $attribs['class'] = $this->mClass;
- }
- if ( $this->mPlaceholder !== '' ) {
- $attribs['placeholder'] = $this->mPlaceholder;
- }
-
- # @todo Enforce pattern, step, required, readonly on the server side as
- # well
- $allowedParams = [
- 'type',
- 'min',
- 'max',
- 'pattern',
- 'title',
- 'step',
- 'list',
- 'maxlength',
- 'tabindex',
- 'disabled',
- 'required',
- 'autofocus',
- 'multiple',
- 'readonly'
- ];
-
- $attribs += $this->getAttributes( $allowedParams );
-
- # Extract 'type'
- $type = $this->getType( $attribs );
- return Html::input( $this->mName, $value, $type, $attribs );
- }
-
- protected function getType( &$attribs ) {
- $type = isset( $attribs['type'] ) ? $attribs['type'] : 'text';
- unset( $attribs['type'] );
-
- # Implement tiny differences between some field variants
- # here, rather than creating a new class for each one which
- # is essentially just a clone of this one.
- if ( isset( $this->mParams['type'] ) ) {
- switch ( $this->mParams['type'] ) {
- case 'int':
- $type = 'number';
- break;
- case 'float':
- $type = 'number';
- $attribs['step'] = 'any';
- break;
- # Pass through
- case 'email':
- case 'password':
- case 'file':
- case 'url':
- $type = $this->mParams['type'];
- break;
- }
- }
-
- return $type;
- }
-
- function getInputOOUI( $value ) {
- if ( !$this->isPersistent() ) {
- $value = '';
- }
-
- $attribs = $this->getTooltipAndAccessKey();
-
- if ( $this->mClass !== '' ) {
- $attribs['classes'] = [ $this->mClass ];
- }
- if ( $this->mPlaceholder !== '' ) {
- $attribs['placeholder'] = $this->mPlaceholder;
- }
-
- # @todo Enforce pattern, step, required, readonly on the server side as
- # well
- $allowedParams = [
- 'autofocus',
- 'autosize',
- 'disabled',
- 'flags',
- 'indicator',
- 'maxlength',
- 'readonly',
- 'required',
- 'tabindex',
- 'type',
- ];
-
- $attribs += OOUI\Element::configFromHtmlAttributes(
- $this->getAttributes( $allowedParams )
- );
-
- $type = $this->getType( $attribs );
-
- return $this->getInputWidget( [
- 'id' => $this->mID,
- 'name' => $this->mName,
- 'value' => $value,
- 'type' => $type,
- ] + $attribs );
- }
-
- protected function getInputWidget( $params ) {
- return new OOUI\TextInputWidget( $params );
- }
-
- /**
- * Returns an array of data-* attributes to add to the field.
- *
- * @return array
- */
- protected function getDataAttribs() {
- return [];
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLTextFieldWithButton.php b/www/wiki/includes/htmlform/HTMLTextFieldWithButton.php
deleted file mode 100644
index c6dac322..00000000
--- a/www/wiki/includes/htmlform/HTMLTextFieldWithButton.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-/**
- * Creates a text input field with a button assigned to the input field.
- */
-class HTMLTextFieldWithButton extends HTMLTextField {
- /** @var HTMLFormClassWithButton $mClassWithButton */
- protected $mClassWithButton = null;
-
- public function __construct( $info ) {
- $this->mClassWithButton = new HTMLFormFieldWithButton( $info );
- parent::__construct( $info );
- }
-
- public function getInputHTML( $value ) {
- return $this->mClassWithButton->getElement( parent::getInputHTML( $value ) );
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLTitleTextField.php b/www/wiki/includes/htmlform/HTMLTitleTextField.php
deleted file mode 100644
index fcf721a5..00000000
--- a/www/wiki/includes/htmlform/HTMLTitleTextField.php
+++ /dev/null
@@ -1,99 +0,0 @@
-<?php
-
-use MediaWiki\Widget\TitleInputWidget;
-
-/**
- * Implements a text input field for page titles.
- * Automatically does validation that the title is valid,
- * as well as autocompletion if using the OOUI display format.
- *
- * Note: Forms using GET requests will need to make sure the title value is not
- * an empty string.
- *
- * Optional parameters:
- * 'namespace' - Namespace the page must be in
- * 'relative' - If true and 'namespace' given, strip/add the namespace from/to the title as needed
- * 'creatable' - Whether to validate the title is creatable (not a special page)
- * 'exists' - Whether to validate that the title already exists
- *
- * @since 1.26
- */
-class HTMLTitleTextField extends HTMLTextField {
- public function __construct( $params ) {
- $params += [
- 'namespace' => false,
- 'relative' => false,
- 'creatable' => false,
- 'exists' => false,
- ];
-
- parent::__construct( $params );
- }
-
- public function validate( $value, $alldata ) {
- if ( $this->mParent->getMethod() === 'get' && $value === '' ) {
- // If the form is a GET form and has no value, assume it hasn't been
- // submitted yet, and skip validation
- return parent::validate( $value, $alldata );
- }
- try {
- if ( !$this->mParams['relative'] ) {
- $title = Title::newFromTextThrow( $value );
- } else {
- // Can't use Title::makeTitleSafe(), because it doesn't throw useful exceptions
- global $wgContLang;
- $namespaceName = $wgContLang->getNsText( $this->mParams['namespace'] );
- $title = Title::newFromTextThrow( $namespaceName . ':' . $value );
- }
- } catch ( MalformedTitleException $e ) {
- $msg = $this->msg( $e->getErrorMessage() );
- $params = $e->getErrorMessageParameters();
- if ( $params ) {
- $msg->params( $params );
- }
- return $msg->parse();
- }
-
- $text = $title->getPrefixedText();
- if ( $this->mParams['namespace'] !== false &&
- !$title->inNamespace( $this->mParams['namespace'] )
- ) {
- return $this->msg( 'htmlform-title-badnamespace', $this->mParams['namespace'], $text )->parse();
- }
-
- if ( $this->mParams['creatable'] && !$title->canExist() ) {
- return $this->msg( 'htmlform-title-not-creatable', $text )->escaped();
- }
-
- if ( $this->mParams['exists'] && !$title->exists() ) {
- return $this->msg( 'htmlform-title-not-exists', $text )->parse();
- }
-
- return parent::validate( $value, $alldata );
- }
-
- protected function getInputWidget( $params ) {
- $this->mParent->getOutput()->addModules( 'mediawiki.widgets' );
- if ( $this->mParams['namespace'] !== false ) {
- $params['namespace'] = $this->mParams['namespace'];
- }
- $params['relative'] = $this->mParams['relative'];
- return new TitleInputWidget( $params );
- }
-
- public function getInputHtml( $value ) {
- // add mw-searchInput class to enable search suggestions for non-OOUI, too
- $this->mClass .= 'mw-searchInput';
-
- // return the HTMLTextField html
- return parent::getInputHTML( $value );
- }
-
- protected function getDataAttribs() {
- return [
- 'data-mw-searchsuggest' => FormatJson::encode( [
- 'wrapAsLink' => false,
- ] ),
- ];
- }
-}
diff --git a/www/wiki/includes/htmlform/HTMLUserTextField.php b/www/wiki/includes/htmlform/HTMLUserTextField.php
deleted file mode 100644
index 5a7e0b9b..00000000
--- a/www/wiki/includes/htmlform/HTMLUserTextField.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-use MediaWiki\Widget\UserInputWidget;
-
-/**
- * Implements a text input field for user names.
- * Automatically auto-completes if using the OOUI display format.
- *
- * FIXME: Does not work for forms that support GET requests.
- *
- * Optional parameters:
- * 'exists' - Whether to validate that the user already exists
- *
- * @since 1.26
- */
-class HTMLUserTextField extends HTMLTextField {
- public function __construct( $params ) {
- $params += [
- 'exists' => false,
- 'ipallowed' => false,
- ];
-
- parent::__construct( $params );
- }
-
- public function validate( $value, $alldata ) {
- // check, if a user exists with the given username
- $user = User::newFromName( $value, false );
-
- if ( !$user ) {
- return $this->msg( 'htmlform-user-not-valid', $value )->parse();
- } elseif (
- ( $this->mParams['exists'] && $user->getId() === 0 ) &&
- !( $this->mParams['ipallowed'] && User::isIP( $value ) )
- ) {
- return $this->msg( 'htmlform-user-not-exists', $user->getName() )->parse();
- }
-
- return parent::validate( $value, $alldata );
- }
-
- protected function getInputWidget( $params ) {
- $this->mParent->getOutput()->addModules( 'mediawiki.widgets.UserInputWidget' );
-
- return new UserInputWidget( $params );
- }
-
- public function getInputHtml( $value ) {
- // add the required module and css class for user suggestions in non-OOUI mode
- $this->mParent->getOutput()->addModules( 'mediawiki.userSuggest' );
- $this->mClass .= ' mw-autocomplete-user';
-
- // return parent html
- return parent::getInputHTML( $value );
- }
-}
diff --git a/www/wiki/includes/htmlform/OOUIHTMLForm.php b/www/wiki/includes/htmlform/OOUIHTMLForm.php
index e47de61a..ba36888c 100644
--- a/www/wiki/includes/htmlform/OOUIHTMLForm.php
+++ b/www/wiki/includes/htmlform/OOUIHTMLForm.php
@@ -176,16 +176,22 @@ class OOUIHTMLForm extends HTMLForm {
* @return string HTML
*/
protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
- $config = [
- 'items' => $fieldsHtml,
- ];
- if ( $sectionName ) {
- $config['id'] = Sanitizer::escapeIdForAttribute( $sectionName );
+ if ( !$fieldsHtml ) {
+ // Do not generate any wrappers for empty sections. Sections may be empty if they only have
+ // subsections, but no fields. A legend will still be added in wrapFieldSetSection().
+ return '';
}
- if ( is_string( $this->mWrapperLegend ) ) {
- $config['label'] = $this->mWrapperLegend;
+
+ $html = implode( '', $fieldsHtml );
+
+ if ( $sectionName ) {
+ $html = Html::rawElement(
+ 'div',
+ [ 'id' => Sanitizer::escapeIdForAttribute( $sectionName ) ],
+ $html
+ );
}
- return new OOUI\FieldsetLayout( $config );
+ return $html;
}
/**
@@ -243,9 +249,8 @@ class OOUIHTMLForm extends HTMLForm {
}
public function getBody() {
- $fieldset = parent::getBody();
- // FIXME This only works for forms with no subsections
- if ( $fieldset instanceof OOUI\FieldsetLayout ) {
+ $html = parent::getBody();
+ if ( $this->mHeader || $this->oouiErrors || $this->oouiWarnings ) {
$classes = [ 'mw-htmlform-ooui-header' ];
if ( $this->oouiErrors ) {
$classes[] = 'mw-htmlform-ooui-header-errors';
@@ -253,33 +258,42 @@ class OOUIHTMLForm extends HTMLForm {
if ( $this->oouiWarnings ) {
$classes[] = 'mw-htmlform-ooui-header-warnings';
}
- if ( $this->mHeader || $this->oouiErrors || $this->oouiWarnings ) {
- // if there's no header, don't create an (empty) LabelWidget, simply use a placeholder
- if ( $this->mHeader ) {
- $element = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet( $this->mHeader ) ] );
- } else {
- $element = new OOUI\Widget( [] );
- }
- $fieldset->addItems( [
- new OOUI\FieldLayout(
- $element,
- [
- 'align' => 'top',
- 'errors' => $this->oouiErrors,
- 'notices' => $this->oouiWarnings,
- 'classes' => $classes,
- ]
- )
- ], 0 );
+ // if there's no header, don't create an (empty) LabelWidget, simply use a placeholder
+ if ( $this->mHeader ) {
+ $element = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet( $this->mHeader ) ] );
+ } else {
+ $element = new OOUI\Widget( [] );
}
+ $html = new OOUI\FieldLayout(
+ $element,
+ [
+ 'align' => 'top',
+ 'errors' => $this->oouiErrors,
+ 'notices' => $this->oouiWarnings,
+ 'classes' => $classes,
+ ]
+ ) . $html;
}
- return $fieldset;
+ return $html;
}
public function wrapForm( $html ) {
+ if ( is_string( $this->mWrapperLegend ) ) {
+ $content = new OOUI\FieldsetLayout( [
+ 'label' => $this->mWrapperLegend,
+ 'items' => [
+ new OOUI\Widget( [
+ 'content' => new OOUI\HtmlSnippet( $html )
+ ] ),
+ ],
+ ] );
+ } else {
+ $content = new OOUI\HtmlSnippet( $html );
+ }
+
$form = new OOUI\FormLayout( $this->getFormAttributes() + [
'classes' => [ 'mw-htmlform', 'mw-htmlform-ooui' ],
- 'content' => new OOUI\HtmlSnippet( $html ),
+ 'content' => $content,
] );
// Include a wrapper for style, if requested.
diff --git a/www/wiki/includes/htmlform/VFormHTMLForm.php b/www/wiki/includes/htmlform/VFormHTMLForm.php
index 325526ba..7bf5f9e2 100644
--- a/www/wiki/includes/htmlform/VFormHTMLForm.php
+++ b/www/wiki/includes/htmlform/VFormHTMLForm.php
@@ -37,11 +37,6 @@ class VFormHTMLForm extends HTMLForm {
*/
protected $displayFormat = 'vform';
- public function isVForm() {
- wfDeprecated( __METHOD__, '1.25' );
- return true;
- }
-
public static function loadInputFromParameters( $fieldname, $descriptor,
HTMLForm $parent = null
) {
diff --git a/www/wiki/includes/htmlform/fields/HTMLCheckMatrix.php b/www/wiki/includes/htmlform/fields/HTMLCheckMatrix.php
index fa18a3cd..df44626a 100644
--- a/www/wiki/includes/htmlform/fields/HTMLCheckMatrix.php
+++ b/www/wiki/includes/htmlform/fields/HTMLCheckMatrix.php
@@ -106,6 +106,7 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
$tooltipAttribs = [
'class' => "mw-htmlform-tooltip $tooltipClass",
'title' => $this->mParams['tooltips'][$rowLabel],
+ 'aria-label' => $this->mParams['tooltips'][$rowLabel]
];
$rowLabel .= ' ' . Html::element( 'span', $tooltipAttribs, '' );
}
@@ -121,9 +122,11 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
if ( $this->isTagForcedOff( $thisTag ) ) {
$checked = false;
$thisAttribs['disabled'] = 1;
+ $thisAttribs['class'] = 'checkmatrix-forced checkmatrix-forced-off';
} elseif ( $this->isTagForcedOn( $thisTag ) ) {
$checked = true;
$thisAttribs['disabled'] = 1;
+ $thisAttribs['class'] = 'checkmatrix-forced checkmatrix-forced-on';
}
$checkbox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs );
diff --git a/www/wiki/includes/htmlform/fields/HTMLEditTools.php b/www/wiki/includes/htmlform/fields/HTMLEditTools.php
index 1b5d1fb4..4019f813 100644
--- a/www/wiki/includes/htmlform/fields/HTMLEditTools.php
+++ b/www/wiki/includes/htmlform/fields/HTMLEditTools.php
@@ -8,8 +8,7 @@ class HTMLEditTools extends HTMLFormField {
public function getTableRow( $value ) {
$msg = $this->formatMsg();
- return
- '<tr><td></td><td class="mw-input">' .
+ return '<tr><td></td><td class="mw-input">' .
'<div class="mw-editTools">' .
$msg->parseAsBlock() .
"</div></td></tr>\n";
diff --git a/www/wiki/includes/htmlform/fields/HTMLMultiSelectField.php b/www/wiki/includes/htmlform/fields/HTMLMultiSelectField.php
index 0d5eeba9..e8a7e992 100644
--- a/www/wiki/includes/htmlform/fields/HTMLMultiSelectField.php
+++ b/www/wiki/includes/htmlform/fields/HTMLMultiSelectField.php
@@ -102,7 +102,7 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
if ( $this->mParent instanceof OOUIHTMLForm ) {
throw new MWException( 'HTMLMultiSelectField#getOneCheckbox() is not supported' );
} else {
- $elementFunc = [ 'Html', $this->mOptionsLabelsNotFromMessage ? 'rawElement' : 'element' ];
+ $elementFunc = [ Html::class, $this->mOptionsLabelsNotFromMessage ? 'rawElement' : 'element' ];
$checkbox =
Xml::check( "{$this->mName}[]", $checked, $attribs ) .
'&#160;' .
@@ -125,45 +125,87 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
* @return array Options for inclusion in a select or whatever.
*/
public function getOptionsOOUI() {
- $options = parent::getOptionsOOUI();
- foreach ( $options as &$option ) {
- $option['disabled'] = in_array( $option['data'], $this->mParams['disabled-options'], true );
- }
- return $options;
+ // Sections make this difficult. See getInputOOUI().
+ throw new MWException( 'HTMLMultiSelectField#getOptionsOOUI() is not supported' );
}
/**
* Get the OOUI version of this field.
*
+ * Returns OOUI\CheckboxMultiselectInputWidget for fields that only have one section,
+ * string otherwise.
+ *
* @since 1.28
* @param string[] $value
- * @return OOUI\CheckboxMultiselectInputWidget
+ * @return string|OOUI\CheckboxMultiselectInputWidget
*/
public function getInputOOUI( $value ) {
$this->mParent->getOutput()->addModules( 'oojs-ui-widgets' );
- $attr = [];
- $attr['id'] = $this->mID;
- $attr['name'] = "{$this->mName}[]";
+ $hasSections = false;
+ $optionsOouiSections = [];
+ $options = $this->getOptions();
+ // If the options are supposed to be split into sections, each section becomes a separate
+ // CheckboxMultiselectInputWidget.
+ foreach ( $options as $label => $section ) {
+ if ( is_array( $section ) ) {
+ $optionsOouiSections[ $label ] = Xml::listDropDownOptionsOoui( $section );
+ unset( $options[$label] );
+ $hasSections = true;
+ }
+ }
+ // If anything remains in the array, they are sectionless options. Put them in a separate widget
+ // at the beginning.
+ if ( $options ) {
+ $optionsOouiSections = array_merge(
+ [ '' => Xml::listDropDownOptionsOoui( $options ) ],
+ $optionsOouiSections
+ );
+ }
+
+ $out = [];
+ foreach ( $optionsOouiSections as $sectionLabel => $optionsOoui ) {
+ $attr = [];
+ $attr['name'] = "{$this->mName}[]";
- $attr['value'] = $value;
- $attr['options'] = $this->getOptionsOOUI();
+ $attr['value'] = $value;
+ $attr['options'] = $optionsOoui;
- if ( $this->mOptionsLabelsNotFromMessage ) {
foreach ( $attr['options'] as &$option ) {
- $option['label'] = new OOUI\HtmlSnippet( $option['label'] );
+ $option['disabled'] = in_array( $option['data'], $this->mParams['disabled-options'], true );
+ }
+ if ( $this->mOptionsLabelsNotFromMessage ) {
+ foreach ( $attr['options'] as &$option ) {
+ $option['label'] = new OOUI\HtmlSnippet( $option['label'] );
+ }
+ }
+
+ $attr += OOUI\Element::configFromHtmlAttributes(
+ $this->getAttributes( [ 'disabled', 'tabindex' ] )
+ );
+
+ if ( $this->mClass !== '' ) {
+ $attr['classes'] = [ $this->mClass ];
}
- }
- $attr += OOUI\Element::configFromHtmlAttributes(
- $this->getAttributes( [ 'disabled', 'tabindex' ] )
- );
+ $widget = new OOUI\CheckboxMultiselectInputWidget( $attr );
+ if ( $sectionLabel ) {
+ $out[] = new OOUI\FieldsetLayout( [
+ 'items' => [ $widget ],
+ 'label' => new OOUI\HtmlSnippet( $sectionLabel ),
+ ] );
+ } else {
+ $out[] = $widget;
+ }
+ }
- if ( $this->mClass !== '' ) {
- $attr['classes'] = [ $this->mClass ];
+ if ( !$hasSections ) {
+ // Directly return the only OOUI\CheckboxMultiselectInputWidget.
+ // This allows it to be made infusable and later tweaked by JS code.
+ return $out[ 0 ];
}
- return new OOUI\CheckboxMultiselectInputWidget( $attr );
+ return implode( '', $out );
}
/**
diff --git a/www/wiki/includes/htmlform/fields/HTMLRadioField.php b/www/wiki/includes/htmlform/fields/HTMLRadioField.php
index 77ea7cd2..f3bcc0e1 100644
--- a/www/wiki/includes/htmlform/fields/HTMLRadioField.php
+++ b/www/wiki/includes/htmlform/fields/HTMLRadioField.php
@@ -72,17 +72,13 @@ class HTMLRadioField extends HTMLFormField {
) );
}
- protected function shouldInfuseOOUI() {
- return true;
- }
-
public function formatOptions( $options, $value ) {
global $wgUseMediaWikiUIEverywhere;
$html = '';
$attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] );
- $elementFunc = [ 'Html', $this->mOptionsLabelsNotFromMessage ? 'rawElement' : 'element' ];
+ $elementFunc = [ Html::class, $this->mOptionsLabelsNotFromMessage ? 'rawElement' : 'element' ];
# @todo Should this produce an unordered list perhaps?
foreach ( $options as $label => $info ) {
diff --git a/www/wiki/includes/htmlform/fields/HTMLRestrictionsField.php b/www/wiki/includes/htmlform/fields/HTMLRestrictionsField.php
index dbf2c8f6..0310dd02 100644
--- a/www/wiki/includes/htmlform/fields/HTMLRestrictionsField.php
+++ b/www/wiki/includes/htmlform/fields/HTMLRestrictionsField.php
@@ -38,7 +38,7 @@ class HTMLRestrictionsField extends HTMLTextAreaField {
}
$value = rtrim( $request->getText( $this->mName ), "\r\n" );
- $ips = $value === '' ? [] : explode( PHP_EOL, $value );
+ $ips = $value === '' ? [] : explode( "\n", $value );
try {
return MWRestrictions::newFromArray( [ 'IPAddresses' => $ips ] );
} catch ( InvalidArgumentException $e ) {
@@ -79,7 +79,7 @@ class HTMLRestrictionsField extends HTMLTextAreaField {
if ( is_string( $value ) ) {
// MWRestrictions::newFromArray failed; one of the IP ranges must be invalid
$status = Status::newGood();
- foreach ( explode( PHP_EOL, $value ) as $range ) {
+ foreach ( explode( "\n", $value ) as $range ) {
if ( !\IP::isIPAddress( $range ) ) {
$status->fatal( 'restrictionsfield-badip', $range );
}
@@ -103,7 +103,7 @@ class HTMLRestrictionsField extends HTMLTextAreaField {
*/
public function getInputHTML( $value ) {
if ( $value instanceof MWRestrictions ) {
- $value = implode( PHP_EOL, $value->toArray()['IPAddresses'] );
+ $value = implode( "\n", $value->toArray()['IPAddresses'] );
}
return parent::getInputHTML( $value );
}
@@ -114,7 +114,7 @@ class HTMLRestrictionsField extends HTMLTextAreaField {
*/
public function getInputOOUI( $value ) {
if ( $value instanceof MWRestrictions ) {
- $value = implode( PHP_EOL, $value->toArray()['IPAddresses'] );
+ $value = implode( "\n", $value->toArray()['IPAddresses'] );
}
return parent::getInputOOUI( $value );
}
diff --git a/www/wiki/includes/htmlform/fields/HTMLSelectAndOtherField.php b/www/wiki/includes/htmlform/fields/HTMLSelectAndOtherField.php
index 38b487af..610b5093 100644
--- a/www/wiki/includes/htmlform/fields/HTMLSelectAndOtherField.php
+++ b/www/wiki/includes/htmlform/fields/HTMLSelectAndOtherField.php
@@ -47,6 +47,10 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
$textAttribs['class'][] = $this->mClass;
}
+ if ( isset( $this->mParams['maxlength-unit'] ) ) {
+ $textAttribs['data-mw-maxlength-unit'] = $this->mParams['maxlength-unit'];
+ }
+
$allowedParams = [
'required',
'autofocus',
@@ -54,6 +58,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
'disabled',
'tabindex',
'maxlength', // gets dynamic with javascript, see mediawiki.htmlform.js
+ 'maxlength-unit', // 'bytes' or 'codepoints', see mediawiki.htmlform.js
];
$textAttribs += $this->getAttributes( $allowedParams );
@@ -72,11 +77,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
# TextInput
$textAttribs = [
- 'id' => $this->mID . '-other',
'name' => $this->mName . '-other',
- 'size' => $this->getSize(),
- 'class' => [ 'mw-htmlform-select-and-other-field' ],
- 'data-id-select' => $this->mID,
'value' => $value[2],
];
@@ -100,7 +101,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
# DropdownInput
$dropdownInputAttribs = [
'name' => $this->mName,
- 'id' => $this->mID,
+ 'id' => $this->mID . '-select',
'options' => $this->getOptionsOOUI(),
'value' => $value[1],
];
@@ -119,14 +120,20 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
}
return $this->getInputWidget( [
+ 'id' => $this->mID,
'textinput' => $textAttribs,
'dropdowninput' => $dropdownInputAttribs,
'or' => false,
+ 'classes' => [ 'mw-htmlform-select-and-other-field' ],
+ 'data' => [
+ 'maxlengthUnit' => isset( $this->mParams['maxlength-unit'] )
+ ? $this->mParams['maxlength-unit'] : 'bytes'
+ ],
] );
}
public function getInputWidget( $params ) {
- return new Mediawiki\Widget\SelectWithInputWidget( $params );
+ return new MediaWiki\Widget\SelectWithInputWidget( $params );
}
/**
diff --git a/www/wiki/includes/htmlform/fields/HTMLSelectOrOtherField.php b/www/wiki/includes/htmlform/fields/HTMLSelectOrOtherField.php
index a009b287..fb133f20 100644
--- a/www/wiki/includes/htmlform/fields/HTMLSelectOrOtherField.php
+++ b/www/wiki/includes/htmlform/fields/HTMLSelectOrOtherField.php
@@ -85,7 +85,6 @@ class HTMLSelectOrOtherField extends HTMLTextField {
# DropdownInput
$dropdownAttribs = [
- 'id' => $this->mID,
'name' => $this->mName,
'options' => $this->getOptionsOOUI(),
'value' => $valInSelect ? $value : 'other',
@@ -103,7 +102,6 @@ class HTMLSelectOrOtherField extends HTMLTextField {
# TextInput
$textAttribs = [
- 'id' => $this->mID . '-other',
'name' => $this->mName . '-other',
'size' => $this->getSize(),
'value' => $valInSelect ? '' : $value,
@@ -130,6 +128,7 @@ class HTMLSelectOrOtherField extends HTMLTextField {
}
return $this->getInputWidget( [
+ 'id' => $this->mID,
'textinput' => $textAttribs,
'dropdowninput' => $dropdownAttribs,
'or' => true,
@@ -137,7 +136,7 @@ class HTMLSelectOrOtherField extends HTMLTextField {
}
public function getInputWidget( $params ) {
- return new Mediawiki\Widget\SelectWithInputWidget( $params );
+ return new MediaWiki\Widget\SelectWithInputWidget( $params );
}
/**
diff --git a/www/wiki/includes/htmlform/fields/HTMLSizeFilterField.php b/www/wiki/includes/htmlform/fields/HTMLSizeFilterField.php
index 5ad7ee34..145a0ecd 100644
--- a/www/wiki/includes/htmlform/fields/HTMLSizeFilterField.php
+++ b/www/wiki/includes/htmlform/fields/HTMLSizeFilterField.php
@@ -41,9 +41,26 @@ class HTMLSizeFilterField extends HTMLIntField {
return $html;
}
- // No OOUI yet
- public function getInputOOUI( $value ) {
- return false;
+ protected function getInputWidget( $params ) {
+ $this->mParent->getOutput()->addModuleStyles( 'mediawiki.widgets.SizeFilterWidget.styles' );
+
+ // negative numbers represent "max", positive numbers represent "min"
+ $value = $params['value'];
+
+ $params['value'] = $value ? abs( $value ) : '';
+
+ return new MediaWiki\Widget\SizeFilterWidget( [
+ 'selectMin' => $value >= 0,
+ 'textinput' => $params,
+ 'radioselectinput' => [
+ 'name' => $this->mName . '-mode',
+ 'disabled' => !empty( $this->mParams['disabled'] ),
+ ],
+ ] );
+ }
+
+ protected function getOOUIModules() {
+ return [ 'mediawiki.widgets.SizeFilterWidget' ];
}
/**
diff --git a/www/wiki/includes/htmlform/fields/HTMLTextAreaField.php b/www/wiki/includes/htmlform/fields/HTMLTextAreaField.php
index 480c5bb9..3370e4af 100644
--- a/www/wiki/includes/htmlform/fields/HTMLTextAreaField.php
+++ b/www/wiki/includes/htmlform/fields/HTMLTextAreaField.php
@@ -5,21 +5,27 @@ class HTMLTextAreaField extends HTMLFormField {
const DEFAULT_ROWS = 25;
protected $mPlaceholder = '';
+ protected $mUseEditFont = false;
/**
* @param array $params
* - cols, rows: textarea size
* - placeholder/placeholder-message: set HTML placeholder attribute
* - spellcheck: set HTML spellcheck attribute
+ * - useeditfont: add CSS classes to use the same font as the wikitext editor
*/
public function __construct( $params ) {
parent::__construct( $params );
if ( isset( $params['placeholder-message'] ) ) {
- $this->mPlaceholder = $this->getMessage( $params['placeholder-message'] )->parse();
+ $this->mPlaceholder = $this->getMessage( $params['placeholder-message'] )->text();
} elseif ( isset( $params['placeholder'] ) ) {
$this->mPlaceholder = $params['placeholder'];
}
+
+ if ( isset( $params['useeditfont'] ) ) {
+ $this->mUseEditFont = $params['useeditfont'];
+ }
}
public function getCols() {
@@ -40,6 +46,8 @@ class HTMLTextAreaField extends HTMLFormField {
}
public function getInputHTML( $value ) {
+ $classes = [];
+
$attribs = [
'id' => $this->mID,
'cols' => $this->getCols(),
@@ -48,11 +56,25 @@ class HTMLTextAreaField extends HTMLFormField {
] + $this->getTooltipAndAccessKey();
if ( $this->mClass !== '' ) {
- $attribs['class'] = $this->mClass;
+ array_push( $classes, $this->mClass );
+ }
+ if ( $this->mUseEditFont ) {
+ // The following classes can be used here:
+ // * mw-editfont-monospace
+ // * mw-editfont-sans-serif
+ // * mw-editfont-serif
+ array_push(
+ $classes,
+ 'mw-editfont-' . $this->mParent->getUser()->getOption( 'editfont' )
+ );
+ $this->mParent->getOutput()->addModuleStyles( 'mediawiki.editfont.styles' );
}
if ( $this->mPlaceholder !== '' ) {
$attribs['placeholder'] = $this->mPlaceholder;
}
+ if ( count( $classes ) ) {
+ $attribs['class'] = implode( ' ', $classes );
+ }
$allowedParams = [
'tabindex',
@@ -67,6 +89,8 @@ class HTMLTextAreaField extends HTMLFormField {
}
function getInputOOUI( $value ) {
+ $classes = [];
+
if ( isset( $this->mParams['cols'] ) ) {
throw new Exception( "OOUIHTMLForm does not support the 'cols' parameter for textareas" );
}
@@ -74,11 +98,25 @@ class HTMLTextAreaField extends HTMLFormField {
$attribs = $this->getTooltipAndAccessKeyOOUI();
if ( $this->mClass !== '' ) {
- $attribs['classes'] = [ $this->mClass ];
+ array_push( $classes, $this->mClass );
+ }
+ if ( $this->mUseEditFont ) {
+ // The following classes can be used here:
+ // * mw-editfont-monospace
+ // * mw-editfont-sans-serif
+ // * mw-editfont-serif
+ array_push(
+ $classes,
+ 'mw-editfont-' . $this->mParent->getUser()->getOption( 'editfont' )
+ );
+ $this->mParent->getOutput()->addModuleStyles( 'mediawiki.editfont.styles' );
}
if ( $this->mPlaceholder !== '' ) {
$attribs['placeholder'] = $this->mPlaceholder;
}
+ if ( count( $classes ) ) {
+ $attribs['classes'] = $classes;
+ }
$allowedParams = [
'tabindex',
@@ -92,10 +130,9 @@ class HTMLTextAreaField extends HTMLFormField {
$this->getAttributes( $allowedParams )
);
- return new OOUI\TextInputWidget( [
+ return new OOUI\MultilineTextInputWidget( [
'id' => $this->mID,
'name' => $this->mName,
- 'multiline' => true,
'value' => $value,
'rows' => $this->getRows(),
] + $attribs );
diff --git a/www/wiki/includes/htmlform/fields/HTMLTextField.php b/www/wiki/includes/htmlform/fields/HTMLTextField.php
index 1c5a43dd..b2e4f2a5 100644
--- a/www/wiki/includes/htmlform/fields/HTMLTextField.php
+++ b/www/wiki/includes/htmlform/fields/HTMLTextField.php
@@ -31,7 +31,7 @@ class HTMLTextField extends HTMLFormField {
parent::__construct( $params );
if ( isset( $params['placeholder-message'] ) ) {
- $this->mPlaceholder = $this->getMessage( $params['placeholder-message'] )->parse();
+ $this->mPlaceholder = $this->getMessage( $params['placeholder-message'] )->text();
} elseif ( isset( $params['placeholder'] ) ) {
$this->mPlaceholder = $params['placeholder'];
}
diff --git a/www/wiki/includes/htmlform/fields/HTMLUserTextField.php b/www/wiki/includes/htmlform/fields/HTMLUserTextField.php
index 12c09c1d..e1939705 100644
--- a/www/wiki/includes/htmlform/fields/HTMLUserTextField.php
+++ b/www/wiki/includes/htmlform/fields/HTMLUserTextField.php
@@ -10,15 +10,25 @@ use MediaWiki\Widget\UserInputWidget;
*
* Optional parameters:
* 'exists' - Whether to validate that the user already exists
+ * 'ipallowed' - Whether an IP adress is interpreted as "valid"
+ * 'iprange' - Whether an IP adress range is interpreted as "valid"
+ * 'iprangelimits' - Specifies the valid IP ranges for IPv4 and IPv6 in an array.
+ * defaults to IPv4 => 16; IPv6 => 32.
*
* @since 1.26
*/
class HTMLUserTextField extends HTMLTextField {
public function __construct( $params ) {
- $params += [
- 'exists' => false,
- 'ipallowed' => false,
- ];
+ $params = wfArrayPlus2d( $params, [
+ 'exists' => false,
+ 'ipallowed' => false,
+ 'iprange' => false,
+ 'iprangelimits' => [
+ 'IPv4' => '16',
+ 'IPv6' => '32',
+ ],
+ ]
+ );
parent::__construct( $params );
}
@@ -26,19 +36,63 @@ class HTMLUserTextField extends HTMLTextField {
public function validate( $value, $alldata ) {
// check, if a user exists with the given username
$user = User::newFromName( $value, false );
+ $rangeError = null;
if ( !$user ) {
return $this->msg( 'htmlform-user-not-valid', $value );
} elseif (
+ // check, if the user exists, if requested
( $this->mParams['exists'] && $user->getId() === 0 ) &&
- !( $this->mParams['ipallowed'] && User::isIP( $value ) )
+ // check, if the username is a valid IP address, otherweise save the error message
+ !( $this->mParams['ipallowed'] && IP::isValid( $value ) ) &&
+ // check, if the username is a valid IP range, otherwise save the error message
+ !( $this->mParams['iprange'] && ( $rangeError = $this->isValidIPRange( $value ) ) === true )
) {
+ if ( is_string( $rangeError ) ) {
+ return $rangeError;
+ }
return $this->msg( 'htmlform-user-not-exists', $user->getName() );
}
return parent::validate( $value, $alldata );
}
+ protected function isValidIPRange( $value ) {
+ $cidrIPRanges = $this->mParams['iprangelimits'];
+
+ if ( !IP::isValidBlock( $value ) ) {
+ return false;
+ }
+
+ list( $ip, $range ) = explode( '/', $value, 2 );
+
+ if (
+ ( IP::isIPv4( $ip ) && $cidrIPRanges['IPv4'] == 32 ) ||
+ ( IP::isIPv6( $ip ) && $cidrIPRanges['IPv6'] == 128 )
+ ) {
+ // Range block effectively disabled
+ return $this->msg( 'ip_range_toolow' )->parse();
+ }
+
+ if (
+ ( IP::isIPv4( $ip ) && $range > 32 ) ||
+ ( IP::isIPv6( $ip ) && $range > 128 )
+ ) {
+ // Dodgy range
+ return $this->msg( 'ip_range_invalid' )->parse();
+ }
+
+ if ( IP::isIPv4( $ip ) && $range < $cidrIPRanges['IPv4'] ) {
+ return $this->msg( 'ip_range_exceeded', $cidrIPRanges['IPv4'] )->parse();
+ }
+
+ if ( IP::isIPv6( $ip ) && $range < $cidrIPRanges['IPv6'] ) {
+ return $this->msg( 'ip_range_exceeded', $cidrIPRanges['IPv6'] )->parse();
+ }
+
+ return true;
+ }
+
protected function getInputWidget( $params ) {
return new UserInputWidget( $params );
}
diff --git a/www/wiki/includes/htmlform/fields/HTMLUsersMultiselectField.php b/www/wiki/includes/htmlform/fields/HTMLUsersMultiselectField.php
index f094745f..46cc6d31 100644
--- a/www/wiki/includes/htmlform/fields/HTMLUsersMultiselectField.php
+++ b/www/wiki/includes/htmlform/fields/HTMLUsersMultiselectField.php
@@ -56,6 +56,14 @@ class HTMLUsersMultiselectField extends HTMLUserTextField {
public function getInputOOUI( $value ) {
$params = [ 'name' => $this->mName ];
+ if ( isset( $this->mParams['id'] ) ) {
+ $params['id'] = $this->mParams['id'];
+ }
+
+ if ( isset( $this->mParams['disabled'] ) ) {
+ $params['disabled'] = $this->mParams['disabled'];
+ }
+
if ( isset( $this->mParams['default'] ) ) {
$params['default'] = $this->mParams['default'];
}
diff --git a/www/wiki/includes/http/CurlHttpRequest.php b/www/wiki/includes/http/CurlHttpRequest.php
index 3da3eb32..a8fbed09 100644
--- a/www/wiki/includes/http/CurlHttpRequest.php
+++ b/www/wiki/includes/http/CurlHttpRequest.php
@@ -82,16 +82,7 @@ class CurlHttpRequest extends MWHttpRequest {
// Don't interpret POST parameters starting with '@' as file uploads, because this
// makes it impossible to POST plain values starting with '@' (and causes security
// issues potentially exposing the contents of local files).
- // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6,
- // but we support lower versions, and the option doesn't exist in HHVM 5.6.99.
- if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) {
- $this->curlOptions[CURLOPT_SAFE_UPLOAD] = true;
- } elseif ( is_array( $postData ) ) {
- // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS
- // is an array, but not if it's a string. So convert $req['body'] to a string
- // for safety.
- $postData = wfArrayToCgi( $postData );
- }
+ $this->curlOptions[CURLOPT_SAFE_UPLOAD] = true;
$this->curlOptions[CURLOPT_POSTFIELDS] = $postData;
// Suppress 'Expect: 100-continue' header, as some servers
@@ -111,14 +102,14 @@ class CurlHttpRequest extends MWHttpRequest {
}
if ( $this->followRedirects && $this->canFollowRedirects() ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
if ( !curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) {
$this->logger->debug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " .
"Probably open_basedir is set.\n" );
// Continue the processing. If it were in curl_setopt_array,
// processing would have halted on its entry
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
if ( $this->profiler ) {
diff --git a/www/wiki/includes/http/HttpRequestFactory.php b/www/wiki/includes/http/HttpRequestFactory.php
new file mode 100644
index 00000000..80f9b688
--- /dev/null
+++ b/www/wiki/includes/http/HttpRequestFactory.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+namespace MediaWiki\Http;
+
+use CurlHttpRequest;
+use DomainException;
+use Http;
+use MediaWiki\Logger\LoggerFactory;
+use MWHttpRequest;
+use PhpHttpRequest;
+use Profiler;
+
+/**
+ * Factory creating MWHttpRequest objects.
+ */
+class HttpRequestFactory {
+
+ /**
+ * Generate a new MWHttpRequest object
+ * @param string $url Url to use
+ * @param array $options (optional) extra params to pass (see Http::request())
+ * @param string $caller The method making this request, for profiling
+ * @throws DomainException
+ * @return MWHttpRequest
+ * @see MWHttpRequest::__construct
+ */
+ public function create( $url, array $options = [], $caller = __METHOD__ ) {
+ if ( !Http::$httpEngine ) {
+ Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
+ } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
+ throw new DomainException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' .
+ ' Http::$httpEngine is set to "curl"' );
+ }
+
+ if ( !isset( $options['logger'] ) ) {
+ $options['logger'] = LoggerFactory::getInstance( 'http' );
+ }
+
+ switch ( Http::$httpEngine ) {
+ case 'curl':
+ return new CurlHttpRequest( $url, $options, $caller, Profiler::instance() );
+ case 'php':
+ if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
+ throw new DomainException( __METHOD__ . ': allow_url_fopen ' .
+ 'needs to be enabled for pure PHP http requests to ' .
+ 'work. If possible, curl should be used instead. See ' .
+ 'http://php.net/curl.'
+ );
+ }
+ return new PhpHttpRequest( $url, $options, $caller, Profiler::instance() );
+ default:
+ throw new DomainException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' );
+ }
+ }
+
+ /**
+ * Simple function to test if we can make any sort of requests at all, using
+ * cURL or fopen()
+ * @return bool
+ */
+ public function canMakeRequests() {
+ return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' );
+ }
+
+}
diff --git a/www/wiki/includes/http/MWHttpRequest.php b/www/wiki/includes/http/MWHttpRequest.php
index 0f0118ce..70691b4c 100644
--- a/www/wiki/includes/http/MWHttpRequest.php
+++ b/www/wiki/includes/http/MWHttpRequest.php
@@ -18,7 +18,6 @@
* @file
*/
-use MediaWiki\Logger\LoggerFactory;
use Psr\Log\LoggerInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\NullLogger;
@@ -30,7 +29,7 @@ use Psr\Log\NullLogger;
* Renamed from HttpRequest to MWHttpRequest to avoid conflict with
* PHP's HTTP extension.
*/
-class MWHttpRequest implements LoggerAwareInterface {
+abstract class MWHttpRequest implements LoggerAwareInterface {
const SUPPORTS_FILE_POSTS = false;
/**
@@ -50,7 +49,7 @@ class MWHttpRequest implements LoggerAwareInterface {
protected $reqHeaders = [];
protected $url;
protected $parsedUrl;
- /** @var callable */
+ /** @var callable */
protected $callback;
protected $maxRedirects = 5;
protected $followRedirects = false;
@@ -80,7 +79,7 @@ class MWHttpRequest implements LoggerAwareInterface {
protected $profileName;
/**
- * @var LoggerInterface;
+ * @var LoggerInterface
*/
protected $logger;
@@ -90,8 +89,8 @@ class MWHttpRequest implements LoggerAwareInterface {
* @param string $caller The method making this request, for profiling
* @param Profiler $profiler An instance of the profiler for profiling, or null
*/
- protected function __construct(
- $url, $options = [], $caller = __METHOD__, $profiler = null
+ public function __construct(
+ $url, array $options = [], $caller = __METHOD__, $profiler = null
) {
global $wgHTTPTimeout, $wgHTTPConnectTimeout;
@@ -174,44 +173,21 @@ class MWHttpRequest implements LoggerAwareInterface {
/**
* Generate a new request object
+ * Deprecated: @see HttpRequestFactory::create
* @param string $url Url to use
- * @param array $options (optional) extra params to pass (see Http::request())
+ * @param array|null $options (optional) extra params to pass (see Http::request())
* @param string $caller The method making this request, for profiling
* @throws DomainException
- * @return CurlHttpRequest|PhpHttpRequest
+ * @return MWHttpRequest
* @see MWHttpRequest::__construct
*/
- public static function factory( $url, $options = null, $caller = __METHOD__ ) {
- if ( !Http::$httpEngine ) {
- Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
- } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
- throw new DomainException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' .
- ' Http::$httpEngine is set to "curl"' );
- }
-
- if ( !is_array( $options ) ) {
+ public static function factory( $url, array $options = null, $caller = __METHOD__ ) {
+ if ( $options === null ) {
$options = [];
}
-
- if ( !isset( $options['logger'] ) ) {
- $options['logger'] = LoggerFactory::getInstance( 'http' );
- }
-
- switch ( Http::$httpEngine ) {
- case 'curl':
- return new CurlHttpRequest( $url, $options, $caller, Profiler::instance() );
- case 'php':
- if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
- throw new DomainException( __METHOD__ . ': allow_url_fopen ' .
- 'needs to be enabled for pure PHP http requests to ' .
- 'work. If possible, curl should be used instead. See ' .
- 'http://php.net/curl.'
- );
- }
- return new PhpHttpRequest( $url, $options, $caller, Profiler::instance() );
- default:
- throw new DomainException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' );
- }
+ return \MediaWiki\MediaWikiServices::getInstance()
+ ->getHttpRequestFactory()
+ ->create( $url, $options, $caller );
}
/**
diff --git a/www/wiki/includes/http/PhpHttpRequest.php b/www/wiki/includes/http/PhpHttpRequest.php
index 0c5d1623..0f499c23 100644
--- a/www/wiki/includes/http/PhpHttpRequest.php
+++ b/www/wiki/includes/http/PhpHttpRequest.php
@@ -46,23 +46,6 @@ class PhpHttpRequest extends MWHttpRequest {
$certLocations = [];
if ( $this->caInfo ) {
$certLocations = [ 'manual' => $this->caInfo ];
- } elseif ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) {
- // @codingStandardsIgnoreStart Generic.Files.LineLength
- // Default locations, based on
- // https://www.happyassassin.net/2015/01/12/a-note-about-ssltls-trusted-certificate-stores-and-platforms/
- // PHP 5.5 and older doesn't have any defaults, so we try to guess ourselves.
- // PHP 5.6+ gets the CA location from OpenSSL as long as it is not set manually,
- // so we should leave capath/cafile empty there.
- // @codingStandardsIgnoreEnd
- $certLocations = array_filter( [
- getenv( 'SSL_CERT_DIR' ),
- getenv( 'SSL_CERT_PATH' ),
- '/etc/pki/tls/certs/ca-bundle.crt', # Fedora et al
- '/etc/ssl/certs', # Debian et al
- '/etc/pki/tls/certs/ca-bundle.trust.crt',
- '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem',
- '/System/Library/OpenSSL', # OSX
- ] );
}
foreach ( $certLocations as $key => $cert ) {
diff --git a/www/wiki/includes/import/ImportStreamSource.php b/www/wiki/includes/import/ImportStreamSource.php
index 94a2b937..cf382e48 100644
--- a/www/wiki/includes/import/ImportStreamSource.php
+++ b/www/wiki/includes/import/ImportStreamSource.php
@@ -53,9 +53,9 @@ class ImportStreamSource implements ImportSource {
* @return Status
*/
static function newFromFile( $filename ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$file = fopen( $filename, 'rt' );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$file ) {
return Status::newFatal( "importcantopen" );
}
diff --git a/www/wiki/includes/import/ImportableOldRevision.php b/www/wiki/includes/import/ImportableOldRevision.php
new file mode 100644
index 00000000..6d1e2426
--- /dev/null
+++ b/www/wiki/includes/import/ImportableOldRevision.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @since 1.31
+ */
+interface ImportableOldRevision {
+
+ /**
+ * @since 1.31
+ * @return User
+ */
+ public function getUserObj();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getUser();
+
+ /**
+ * @since 1.31
+ * @return Title
+ */
+ public function getTitle();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getTimestamp();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getComment();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getModel();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getFormat();
+
+ /**
+ * @since 1.31
+ * @return Content
+ */
+ public function getContent();
+
+ /**
+ * @since 1.31
+ * @return bool
+ */
+ public function getMinor();
+
+ /**
+ * @since 1.31
+ * @return bool|string
+ */
+ public function getSha1Base36();
+
+}
diff --git a/www/wiki/includes/import/ImportableOldRevisionImporter.php b/www/wiki/includes/import/ImportableOldRevisionImporter.php
new file mode 100644
index 00000000..066a3eac
--- /dev/null
+++ b/www/wiki/includes/import/ImportableOldRevisionImporter.php
@@ -0,0 +1,145 @@
+<?php
+
+use Psr\Log\LoggerInterface;
+use Wikimedia\Rdbms\LoadBalancer;
+
+/**
+ * @since 1.31
+ */
+class ImportableOldRevisionImporter implements OldRevisionImporter {
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * @var bool
+ */
+ private $doUpdates;
+
+ /**
+ * @var LoadBalancer
+ */
+ private $loadBalancer;
+
+ /**
+ * @param bool $doUpdates
+ * @param LoggerInterface $logger
+ * @param LoadBalancer $loadBalancer
+ */
+ public function __construct(
+ $doUpdates,
+ LoggerInterface $logger,
+ LoadBalancer $loadBalancer
+ ) {
+ $this->doUpdates = $doUpdates;
+ $this->logger = $logger;
+ $this->loadBalancer = $loadBalancer;
+ }
+
+ public function import( ImportableOldRevision $importableRevision, $doUpdates = true ) {
+ $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
+
+ # Sneak a single revision into place
+ $user = $importableRevision->getUserObj() ?: User::newFromName( $importableRevision->getUser() );
+ if ( $user ) {
+ $userId = intval( $user->getId() );
+ $userText = $user->getName();
+ } else {
+ $userId = 0;
+ $userText = $importableRevision->getUser();
+ $user = new User;
+ }
+
+ // avoid memory leak...?
+ Title::clearCaches();
+
+ $page = WikiPage::factory( $importableRevision->getTitle() );
+ $page->loadPageData( 'fromdbmaster' );
+ if ( !$page->exists() ) {
+ // must create the page...
+ $pageId = $page->insertOn( $dbw );
+ $created = true;
+ $oldcountable = null;
+ } else {
+ $pageId = $page->getId();
+ $created = false;
+
+ // Note: sha1 has been in XML dumps since 2012. If you have an
+ // older dump, the duplicate detection here won't work.
+ if ( $importableRevision->getSha1Base36() !== false ) {
+ $prior = $dbw->selectField( 'revision', '1',
+ [ 'rev_page' => $pageId,
+ 'rev_timestamp' => $dbw->timestamp( $importableRevision->getTimestamp() ),
+ 'rev_sha1' => $importableRevision->getSha1Base36() ],
+ __METHOD__
+ );
+ if ( $prior ) {
+ // @todo FIXME: This could fail slightly for multiple matches :P
+ $this->logger->debug( __METHOD__ . ": skipping existing revision for [[" .
+ $importableRevision->getTitle()->getPrefixedText() . "]], timestamp " .
+ $importableRevision->getTimestamp() . "\n" );
+ return false;
+ }
+ }
+ }
+
+ if ( !$pageId ) {
+ // This seems to happen if two clients simultaneously try to import the
+ // same page
+ $this->logger->debug( __METHOD__ . ': got invalid $pageId when importing revision of [[' .
+ $importableRevision->getTitle()->getPrefixedText() . ']], timestamp ' .
+ $importableRevision->getTimestamp() . "\n" );
+ return false;
+ }
+
+ // Select previous version to make size diffs correct
+ // @todo This assumes that multiple revisions of the same page are imported
+ // in order from oldest to newest.
+ $prevId = $dbw->selectField( 'revision', 'rev_id',
+ [
+ 'rev_page' => $pageId,
+ 'rev_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $importableRevision->getTimestamp() ) ),
+ ],
+ __METHOD__,
+ [ 'ORDER BY' => [
+ 'rev_timestamp DESC',
+ 'rev_id DESC', // timestamp is not unique per page
+ ]
+ ]
+ );
+
+ # @todo FIXME: Use original rev_id optionally (better for backups)
+ # Insert the row
+ $revision = new Revision( [
+ 'title' => $importableRevision->getTitle(),
+ 'page' => $pageId,
+ 'content_model' => $importableRevision->getModel(),
+ 'content_format' => $importableRevision->getFormat(),
+ // XXX: just set 'content' => $wikiRevision->getContent()?
+ 'text' => $importableRevision->getContent()->serialize( $importableRevision->getFormat() ),
+ 'comment' => $importableRevision->getComment(),
+ 'user' => $userId,
+ 'user_text' => $userText,
+ 'timestamp' => $importableRevision->getTimestamp(),
+ 'minor_edit' => $importableRevision->getMinor(),
+ 'parent_id' => $prevId,
+ ] );
+ $revision->insertOn( $dbw );
+ $changed = $page->updateIfNewerOn( $dbw, $revision );
+
+ if ( $changed !== false && $this->doUpdates ) {
+ $this->logger->debug( __METHOD__ . ": running updates\n" );
+ // countable/oldcountable stuff is handled in WikiImporter::finishImportPage
+ $page->doEditUpdates(
+ $revision,
+ $user,
+ [ 'created' => $created, 'oldcountable' => 'no-change' ]
+ );
+ }
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/includes/import/ImportableUploadRevision.php b/www/wiki/includes/import/ImportableUploadRevision.php
new file mode 100644
index 00000000..3f60112a
--- /dev/null
+++ b/www/wiki/includes/import/ImportableUploadRevision.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @since 1.31
+ */
+interface ImportableUploadRevision {
+
+ /**
+ * @since 1.31
+ * @return string Archive name of a revision if archived.
+ */
+ public function getArchiveName();
+
+ /**
+ * @since 1.31
+ * @return Title
+ */
+ public function getTitle();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getTimestamp();
+
+ /**
+ * @since 1.31
+ * @return string|null HTTP source of revision to be used for downloading.
+ */
+ public function getSrc();
+
+ /**
+ * @since 1.31
+ * @return string Local file source of the revision.
+ */
+ public function getFileSrc();
+
+ /**
+ * @since 1.31
+ * @return bool Is the return of getFileSrc only temporary?
+ */
+ public function isTempSrc();
+
+ /**
+ * @since 1.31
+ * @return string|bool sha1 of the revision, false if not set or errors occour.
+ */
+ public function getSha1();
+
+ /**
+ * @since 1.31
+ * @return User
+ */
+ public function getUserObj();
+
+ /**
+ * @since 1.31
+ * @return string The username of the user that created this revision
+ */
+ public function getUser();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getComment();
+
+}
diff --git a/www/wiki/includes/import/ImportableUploadRevisionImporter.php b/www/wiki/includes/import/ImportableUploadRevisionImporter.php
new file mode 100644
index 00000000..515cdd5e
--- /dev/null
+++ b/www/wiki/includes/import/ImportableUploadRevisionImporter.php
@@ -0,0 +1,155 @@
+<?php
+
+use Psr\Log\LoggerInterface;
+
+/**
+ * @since 1.31
+ */
+class ImportableUploadRevisionImporter implements UploadRevisionImporter {
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * @var bool
+ */
+ private $enableUploads;
+
+ /**
+ * @param bool $enableUploads
+ * @param LoggerInterface $logger
+ */
+ public function __construct(
+ $enableUploads,
+ LoggerInterface $logger
+ ) {
+ $this->enableUploads = $enableUploads;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @return StatusValue
+ */
+ private function newNotOkStatus() {
+ $statusValue = new StatusValue();
+ $statusValue->setOK( false );
+ return $statusValue;
+ }
+
+ public function import( ImportableUploadRevision $importableRevision ) {
+ # Construct a file
+ $archiveName = $importableRevision->getArchiveName();
+ if ( $archiveName ) {
+ $this->logger->debug( __METHOD__ . "Importing archived file as $archiveName\n" );
+ $file = OldLocalFile::newFromArchiveName( $importableRevision->getTitle(),
+ RepoGroup::singleton()->getLocalRepo(), $archiveName );
+ } else {
+ $file = wfLocalFile( $importableRevision->getTitle() );
+ $file->load( File::READ_LATEST );
+ $this->logger->debug( __METHOD__ . 'Importing new file as ' . $file->getName() . "\n" );
+ if ( $file->exists() && $file->getTimestamp() > $importableRevision->getTimestamp() ) {
+ $archiveName = $importableRevision->getTimestamp() . '!' . $file->getName();
+ $file = OldLocalFile::newFromArchiveName( $importableRevision->getTitle(),
+ RepoGroup::singleton()->getLocalRepo(), $archiveName );
+ $this->logger->debug( __METHOD__ . "File already exists; importing as $archiveName\n" );
+ }
+ }
+ if ( !$file ) {
+ $this->logger->debug( __METHOD__ . ': Bad file for ' . $importableRevision->getTitle() . "\n" );
+ return $this->newNotOkStatus();
+ }
+
+ # Get the file source or download if necessary
+ $source = $importableRevision->getFileSrc();
+ $autoDeleteSource = $importableRevision->isTempSrc();
+ if ( !strlen( $source ) ) {
+ $source = $this->downloadSource( $importableRevision );
+ $autoDeleteSource = true;
+ }
+ if ( !strlen( $source ) ) {
+ $this->logger->debug( __METHOD__ . ": Could not fetch remote file.\n" );
+ return $this->newNotOkStatus();
+ }
+
+ $tmpFile = new TempFSFile( $source );
+ if ( $autoDeleteSource ) {
+ $tmpFile->autocollect();
+ }
+
+ $sha1File = ltrim( sha1_file( $source ), '0' );
+ $sha1 = $importableRevision->getSha1();
+ if ( $sha1 && ( $sha1 !== $sha1File ) ) {
+ $this->logger->debug( __METHOD__ . ": Corrupt file $source.\n" );
+ return $this->newNotOkStatus();
+ }
+
+ $user = $importableRevision->getUserObj()
+ ?: User::newFromName( $importableRevision->getUser(), false );
+
+ # Do the actual upload
+ if ( $archiveName ) {
+ $status = $file->uploadOld( $source, $archiveName,
+ $importableRevision->getTimestamp(), $importableRevision->getComment(), $user );
+ } else {
+ $flags = 0;
+ $status = $file->upload(
+ $source,
+ $importableRevision->getComment(),
+ $importableRevision->getComment(),
+ $flags,
+ false,
+ $importableRevision->getTimestamp(),
+ $user
+ );
+ }
+
+ if ( $status->isGood() ) {
+ $this->logger->debug( __METHOD__ . ": Successful\n" );
+ } else {
+ $this->logger->debug( __METHOD__ . ': failed: ' . $status->getHTML() . "\n" );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @deprecated DO NOT CALL ME.
+ * This method was introduced when factoring UploadImporter out of WikiRevision.
+ * It only has 1 use by the deprecated downloadSource method in WikiRevision.
+ * Do not use this in new code.
+ *
+ * @param ImportableUploadRevision $wikiRevision
+ *
+ * @return bool|string
+ */
+ public function downloadSource( ImportableUploadRevision $wikiRevision ) {
+ if ( !$this->enableUploads ) {
+ return false;
+ }
+
+ $tempo = tempnam( wfTempDir(), 'download' );
+ $f = fopen( $tempo, 'wb' );
+ if ( !$f ) {
+ $this->logger->debug( "IMPORT: couldn't write to temp file $tempo\n" );
+ return false;
+ }
+
+ // @todo FIXME!
+ $src = $wikiRevision->getSrc();
+ $data = Http::get( $src, [], __METHOD__ );
+ if ( !$data ) {
+ $this->logger->debug( "IMPORT: couldn't fetch source $src\n" );
+ fclose( $f );
+ unlink( $tempo );
+ return false;
+ }
+
+ fwrite( $f, $data );
+ fclose( $f );
+
+ return $tempo;
+ }
+
+}
diff --git a/www/wiki/includes/import/OldRevisionImporter.php b/www/wiki/includes/import/OldRevisionImporter.php
new file mode 100644
index 00000000..72af43b9
--- /dev/null
+++ b/www/wiki/includes/import/OldRevisionImporter.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @since 1.31
+ */
+interface OldRevisionImporter {
+
+ /**
+ * @since 1.31
+ *
+ * @param ImportableOldRevision $importableRevision
+ *
+ * @return bool Success
+ */
+ public function import( ImportableOldRevision $importableRevision );
+
+}
diff --git a/www/wiki/includes/import/UploadRevisionImporter.php b/www/wiki/includes/import/UploadRevisionImporter.php
new file mode 100644
index 00000000..966fc11a
--- /dev/null
+++ b/www/wiki/includes/import/UploadRevisionImporter.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @since 1.31
+ */
+interface UploadRevisionImporter {
+
+ /**
+ * @since 1.31
+ *
+ * @param ImportableUploadRevision $importableUploadRevision
+ *
+ * @return StatusValue On success, the value member contains the
+ * archive name, or an empty string if it was a new file.
+ */
+ public function import( ImportableUploadRevision $importableUploadRevision );
+
+}
diff --git a/www/wiki/includes/import/WikiImporter.php b/www/wiki/includes/import/WikiImporter.php
index 90660797..8991f5ef 100644
--- a/www/wiki/includes/import/WikiImporter.php
+++ b/www/wiki/includes/import/WikiImporter.php
@@ -23,7 +23,6 @@
* @file
* @ingroup SpecialPage
*/
-use MediaWiki\MediaWikiServices;
/**
* XML file reader for the page data importer.
@@ -48,6 +47,8 @@ class WikiImporter {
private $countableCache = [];
/** @var bool */
private $disableStatisticsUpdate = false;
+ /** @var ExternalUserNames */
+ private $externalUserNames;
/**
* Creates an ImportXMLReader drawing from the source provided
@@ -64,7 +65,7 @@ class WikiImporter {
$this->config = $config;
if ( !in_array( 'uploadsource', stream_get_wrappers() ) ) {
- stream_wrapper_register( 'uploadsource', 'UploadSourceAdapter' );
+ stream_wrapper_register( 'uploadsource', UploadSourceAdapter::class );
}
$id = UploadSourceAdapter::registerSource( $source );
@@ -92,6 +93,7 @@ class WikiImporter {
$this->setPageOutCallback( [ $this, 'finishImportPage' ] );
$this->importTitleFactory = new NaiveImportTitleFactory();
+ $this->externalUserNames = new ExternalUserNames( 'imported', false );
}
/**
@@ -123,7 +125,9 @@ class WikiImporter {
if ( is_callable( $this->mNoticeCallback ) ) {
call_user_func( $this->mNoticeCallback, $msg, $params );
} else { # No ImportReporter -> CLI
- echo wfMessage( $msg, $params )->text() . "\n";
+ // T177997: the command line importers should call setNoticeCallback()
+ // for their own custom callback to echo the notice
+ wfDebug( wfMessage( $msg, $params )->text() . "\n" );
}
}
@@ -313,6 +317,15 @@ class WikiImporter {
}
/**
+ * @since 1.31
+ * @param string $usernamePrefix Prefix to apply to unknown (and possibly also known) usernames
+ * @param bool $assignKnownUsers Whether to apply the prefix to usernames that exist locally
+ */
+ public function setUsernamePrefix( $usernamePrefix, $assignKnownUsers ) {
+ $this->externalUserNames = new ExternalUserNames( $usernamePrefix, $assignKnownUsers );
+ }
+
+ /**
* Statistics update can cause a lot of time
* @since 1.29
*/
@@ -531,13 +544,13 @@ class WikiImporter {
$buffer = "";
while ( $this->reader->read() ) {
switch ( $this->reader->nodeType ) {
- case XMLReader::TEXT:
- case XMLReader::CDATA:
- case XMLReader::SIGNIFICANT_WHITESPACE:
- $buffer .= $this->reader->value;
- break;
- case XMLReader::END_ELEMENT:
- return $buffer;
+ case XMLReader::TEXT:
+ case XMLReader::CDATA:
+ case XMLReader::SIGNIFICANT_WHITESPACE:
+ $buffer .= $this->reader->value;
+ break;
+ case XMLReader::END_ELEMENT:
+ return $buffer;
}
}
@@ -547,6 +560,7 @@ class WikiImporter {
/**
* Primary entry point
+ * @throws Exception
* @throws MWException
* @return bool
*/
@@ -717,9 +731,11 @@ class WikiImporter {
}
if ( !isset( $logInfo['contributor']['username'] ) ) {
- $revision->setUsername( 'Unknown user' );
+ $revision->setUsername( $this->externalUserNames->addPrefix( 'Unknown user' ) );
} else {
- $revision->setUsername( $logInfo['contributor']['username'] );
+ $revision->setUsername(
+ $this->externalUserNames->applyPrefix( $logInfo['contributor']['username'] )
+ );
}
return $this->logItemCallback( $revision );
@@ -848,6 +864,7 @@ class WikiImporter {
/**
* @param array $pageInfo
* @param array $revisionInfo
+ * @throws MWException
* @return bool|mixed
*/
private function processRevision( $pageInfo, $revisionInfo ) {
@@ -912,9 +929,11 @@ class WikiImporter {
if ( isset( $revisionInfo['contributor']['ip'] ) ) {
$revision->setUserIP( $revisionInfo['contributor']['ip'] );
} elseif ( isset( $revisionInfo['contributor']['username'] ) ) {
- $revision->setUsername( $revisionInfo['contributor']['username'] );
+ $revision->setUsername(
+ $this->externalUserNames->applyPrefix( $revisionInfo['contributor']['username'] )
+ );
} else {
- $revision->setUsername( 'Unknown user' );
+ $revision->setUsername( $this->externalUserNames->addPrefix( 'Unknown user' ) );
}
if ( isset( $revisionInfo['sha1'] ) ) {
$revision->setSha1Base36( $revisionInfo['sha1'] );
@@ -1021,7 +1040,9 @@ class WikiImporter {
$revision->setUserIP( $uploadInfo['contributor']['ip'] );
}
if ( isset( $uploadInfo['contributor']['username'] ) ) {
- $revision->setUsername( $uploadInfo['contributor']['username'] );
+ $revision->setUsername(
+ $this->externalUserNames->applyPrefix( $uploadInfo['contributor']['username'] )
+ );
}
$revision->setNoUpdates( $this->mNoUpdates );
diff --git a/www/wiki/includes/import/WikiRevision.php b/www/wiki/includes/import/WikiRevision.php
index edb0c9af..55a7b2d3 100644
--- a/www/wiki/includes/import/WikiRevision.php
+++ b/www/wiki/includes/import/WikiRevision.php
@@ -23,6 +23,8 @@
* @file
* @ingroup SpecialPage
*/
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
/**
* Represents a revision, log entry or upload during the import process.
@@ -32,7 +34,7 @@
*
* @ingroup SpecialPage
*/
-class WikiRevision {
+class WikiRevision implements ImportableUploadRevision, ImportableOldRevision {
/**
* @since 1.17
@@ -170,9 +172,9 @@ class WikiRevision {
/**
* @since 1.12.2
- * @var mixed
+ * @var string|null
*/
- protected $src;
+ protected $src = null;
/**
* @since 1.18
@@ -298,7 +300,7 @@ class WikiRevision {
/**
* @since 1.12.2
- * @param mixed $src
+ * @param string|null $src
*/
public function setSrc( $src ) {
$this->src = $src;
@@ -494,7 +496,7 @@ class WikiRevision {
/**
* @since 1.12.2
- * @return mixed
+ * @return string|null
*/
public function getSrc() {
return $this->src;
@@ -512,6 +514,17 @@ class WikiRevision {
}
/**
+ * @since 1.31
+ * @return bool|string
+ */
+ public function getSha1Base36() {
+ if ( $this->sha1base36 ) {
+ return $this->sha1base36;
+ }
+ return false;
+ }
+
+ /**
* @since 1.17
* @return string
*/
@@ -577,106 +590,16 @@ class WikiRevision {
/**
* @since 1.4.1
+ * @deprecated in 1.31. Use OldRevisionImporter::import
* @return bool
*/
public function importOldRevision() {
- $dbw = wfGetDB( DB_MASTER );
-
- # Sneak a single revision into place
- $user = $this->getUserObj() ?: User::newFromName( $this->getUser() );
- if ( $user ) {
- $userId = intval( $user->getId() );
- $userText = $user->getName();
- } else {
- $userId = 0;
- $userText = $this->getUser();
- $user = new User;
- }
-
- // avoid memory leak...?
- Title::clearCaches();
-
- $page = WikiPage::factory( $this->title );
- $page->loadPageData( 'fromdbmaster' );
- if ( !$page->exists() ) {
- // must create the page...
- $pageId = $page->insertOn( $dbw );
- $created = true;
- $oldcountable = null;
+ if ( $this->mNoUpdates ) {
+ $importer = MediaWikiServices::getInstance()->getWikiRevisionOldRevisionImporterNoUpdates();
} else {
- $pageId = $page->getId();
- $created = false;
-
- // Note: sha1 has been in XML dumps since 2012. If you have an
- // older dump, the duplicate detection here won't work.
- $prior = $dbw->selectField( 'revision', '1',
- [ 'rev_page' => $pageId,
- 'rev_timestamp' => $dbw->timestamp( $this->timestamp ),
- 'rev_sha1' => $this->sha1base36 ],
- __METHOD__
- );
- if ( $prior ) {
- // @todo FIXME: This could fail slightly for multiple matches :P
- wfDebug( __METHOD__ . ": skipping existing revision for [[" .
- $this->title->getPrefixedText() . "]], timestamp " . $this->timestamp . "\n" );
- return false;
- }
+ $importer = MediaWikiServices::getInstance()->getWikiRevisionOldRevisionImporter();
}
-
- if ( !$pageId ) {
- // This seems to happen if two clients simultaneously try to import the
- // same page
- wfDebug( __METHOD__ . ': got invalid $pageId when importing revision of [[' .
- $this->title->getPrefixedText() . ']], timestamp ' . $this->timestamp . "\n" );
- return false;
- }
-
- // Select previous version to make size diffs correct
- // @todo This assumes that multiple revisions of the same page are imported
- // in order from oldest to newest.
- $prevId = $dbw->selectField( 'revision', 'rev_id',
- [
- 'rev_page' => $pageId,
- 'rev_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $this->timestamp ) ),
- ],
- __METHOD__,
- [ 'ORDER BY' => [
- 'rev_timestamp DESC',
- 'rev_id DESC', // timestamp is not unique per page
- ]
- ]
- );
-
- # @todo FIXME: Use original rev_id optionally (better for backups)
- # Insert the row
- $revision = new Revision( [
- 'title' => $this->title,
- 'page' => $pageId,
- 'content_model' => $this->getModel(),
- 'content_format' => $this->getFormat(),
- // XXX: just set 'content' => $this->getContent()?
- 'text' => $this->getContent()->serialize( $this->getFormat() ),
- 'comment' => $this->getComment(),
- 'user' => $userId,
- 'user_text' => $userText,
- 'timestamp' => $this->timestamp,
- 'minor_edit' => $this->minor,
- 'parent_id' => $prevId,
- ] );
- $revision->insertOn( $dbw );
- $changed = $page->updateIfNewerOn( $dbw, $revision );
-
- if ( $changed !== false && !$this->mNoUpdates ) {
- wfDebug( __METHOD__ . ": running updates\n" );
- // countable/oldcountable stuff is handled in WikiImporter::finishImportPage
- $page->doEditUpdates(
- $revision,
- $user,
- [ 'created' => $created, 'oldcountable' => 'no-change' ]
- );
- }
-
- return true;
+ return $importer->import( $this );
}
/**
@@ -686,14 +609,7 @@ class WikiRevision {
public function importLogItem() {
$dbw = wfGetDB( DB_MASTER );
- $user = $this->getUserObj() ?: User::newFromName( $this->getUser() );
- if ( $user ) {
- $userId = intval( $user->getId() );
- $userText = $user->getName();
- } else {
- $userId = 0;
- $userText = $this->getUser();
- }
+ $user = $this->getUserObj() ?: User::newFromName( $this->getUser(), false );
# @todo FIXME: This will not record autoblocks
if ( !$this->getTitle() ) {
@@ -709,7 +625,6 @@ class WikiRevision {
'log_timestamp' => $dbw->timestamp( $this->timestamp ),
'log_namespace' => $this->getTitle()->getNamespace(),
'log_title' => $this->getTitle()->getDBkey(),
- # 'log_user_text' => $this->user_text,
'log_params' => $this->params ],
__METHOD__
);
@@ -724,12 +639,11 @@ class WikiRevision {
'log_type' => $this->type,
'log_action' => $this->action,
'log_timestamp' => $dbw->timestamp( $this->timestamp ),
- 'log_user' => $userId,
- 'log_user_text' => $userText,
'log_namespace' => $this->getTitle()->getNamespace(),
'log_title' => $this->getTitle()->getDBkey(),
'log_params' => $this->params
- ] + CommentStore::newKey( 'log_comment' )->insert( $dbw, $this->getComment() );
+ ] + CommentStore::getStore()->insert( $dbw, 'log_comment', $this->getComment() )
+ + ActorMigration::newMigration()->getInsertValues( $dbw, 'log_user', $user );
$dbw->insert( 'logging', $data, __METHOD__ );
return true;
@@ -737,106 +651,26 @@ class WikiRevision {
/**
* @since 1.12.2
+ * @deprecated in 1.31. Use UploadImporter::import
* @return bool
*/
public function importUpload() {
- # Construct a file
- $archiveName = $this->getArchiveName();
- if ( $archiveName ) {
- wfDebug( __METHOD__ . "Importing archived file as $archiveName\n" );
- $file = OldLocalFile::newFromArchiveName( $this->getTitle(),
- RepoGroup::singleton()->getLocalRepo(), $archiveName );
- } else {
- $file = wfLocalFile( $this->getTitle() );
- $file->load( File::READ_LATEST );
- wfDebug( __METHOD__ . 'Importing new file as ' . $file->getName() . "\n" );
- if ( $file->exists() && $file->getTimestamp() > $this->getTimestamp() ) {
- $archiveName = $file->getTimestamp() . '!' . $file->getName();
- $file = OldLocalFile::newFromArchiveName( $this->getTitle(),
- RepoGroup::singleton()->getLocalRepo(), $archiveName );
- wfDebug( __METHOD__ . "File already exists; importing as $archiveName\n" );
- }
- }
- if ( !$file ) {
- wfDebug( __METHOD__ . ': Bad file for ' . $this->getTitle() . "\n" );
- return false;
- }
-
- # Get the file source or download if necessary
- $source = $this->getFileSrc();
- $autoDeleteSource = $this->isTempSrc();
- if ( !strlen( $source ) ) {
- $source = $this->downloadSource();
- $autoDeleteSource = true;
- }
- if ( !strlen( $source ) ) {
- wfDebug( __METHOD__ . ": Could not fetch remote file.\n" );
- return false;
- }
-
- $tmpFile = new TempFSFile( $source );
- if ( $autoDeleteSource ) {
- $tmpFile->autocollect();
- }
-
- $sha1File = ltrim( sha1_file( $source ), '0' );
- $sha1 = $this->getSha1();
- if ( $sha1 && ( $sha1 !== $sha1File ) ) {
- wfDebug( __METHOD__ . ": Corrupt file $source.\n" );
- return false;
- }
-
- $user = $this->getUserObj() ?: User::newFromName( $this->getUser() );
-
- # Do the actual upload
- if ( $archiveName ) {
- $status = $file->uploadOld( $source, $archiveName,
- $this->getTimestamp(), $this->getComment(), $user );
- } else {
- $flags = 0;
- $status = $file->upload( $source, $this->getComment(), $this->getComment(),
- $flags, false, $this->getTimestamp(), $user );
- }
-
- if ( $status->isGood() ) {
- wfDebug( __METHOD__ . ": Successful\n" );
- return true;
- } else {
- wfDebug( __METHOD__ . ': failed: ' . $status->getHTML() . "\n" );
- return false;
- }
+ $importer = MediaWikiServices::getInstance()->getWikiRevisionUploadImporter();
+ $statusValue = $importer->import( $this );
+ return $statusValue->isGood();
}
/**
* @since 1.12.2
+ * @deprecated in 1.31. Use UploadImporter::downloadSource
* @return bool|string
*/
public function downloadSource() {
- if ( !$this->config->get( 'EnableUploads' ) ) {
- return false;
- }
-
- $tempo = tempnam( wfTempDir(), 'download' );
- $f = fopen( $tempo, 'wb' );
- if ( !$f ) {
- wfDebug( "IMPORT: couldn't write to temp file $tempo\n" );
- return false;
- }
-
- // @todo FIXME!
- $src = $this->getSrc();
- $data = Http::get( $src, [], __METHOD__ );
- if ( !$data ) {
- wfDebug( "IMPORT: couldn't fetch source $src\n" );
- fclose( $f );
- unlink( $tempo );
- return false;
- }
-
- fwrite( $f, $data );
- fclose( $f );
-
- return $tempo;
+ $importer = new ImportableUploadRevisionImporter(
+ $this->config->get( 'EnableUploads' ),
+ LoggerFactory::getInstance( 'UploadRevisionImporter' )
+ );
+ return $importer->downloadSource( $this );
}
}
diff --git a/www/wiki/includes/installer/CliInstaller.php b/www/wiki/includes/installer/CliInstaller.php
index 715f5dff..0b024c41 100644
--- a/www/wiki/includes/installer/CliInstaller.php
+++ b/www/wiki/includes/installer/CliInstaller.php
@@ -38,7 +38,6 @@ class CliInstaller extends Installer {
'dbpass' => 'wgDBpassword',
'dbprefix' => 'wgDBprefix',
'dbtableoptions' => 'wgDBTableOptions',
- 'dbmysql5' => 'wgDBmysql5',
'dbport' => 'wgDBport',
'dbschema' => 'wgDBmwschema',
'dbpath' => 'wgSQLiteDataDir',
@@ -73,6 +72,7 @@ class CliInstaller extends Installer {
$wgContLang = Language::factory( $option['lang'] );
$wgLang = Language::factory( $option['lang'] );
$wgLanguageCode = $option['lang'];
+ $this->setVar( 'wgLanguageCode', $wgLanguageCode );
RequestContext::getMain()->setLanguage( $wgLang );
}
@@ -126,6 +126,15 @@ class CliInstaller extends Installer {
* Main entry point.
*/
public function execute() {
+ // If APC is available, use that as the MainCacheType, instead of nothing.
+ // This is hacky and should be consolidated with WebInstallerOptions.
+ // This is here instead of in __construct(), because it should run run after
+ // doEnvironmentChecks(), which populates '_Caches'.
+ if ( count( $this->getVar( '_Caches' ) ) ) {
+ // We detected a CACHE_ACCEL implementation, use it.
+ $this->setVar( '_MainCacheType', 'accel' );
+ }
+
$vars = Installer::getExistingLocalSettings();
if ( $vars ) {
$this->showStatusMessage(
diff --git a/www/wiki/includes/installer/DatabaseInstaller.php b/www/wiki/includes/installer/DatabaseInstaller.php
index 925d991d..e6ee70ed 100644
--- a/www/wiki/includes/installer/DatabaseInstaller.php
+++ b/www/wiki/includes/installer/DatabaseInstaller.php
@@ -724,16 +724,16 @@ abstract class DatabaseInstaller {
}
$this->db->selectDB( $this->getVar( 'wgDBname' ) );
- if ( $this->db->selectRow( 'interwiki', '*', [], __METHOD__ ) ) {
+ if ( $this->db->selectRow( 'interwiki', '1', [], __METHOD__ ) ) {
$status->warning( 'config-install-interwiki-exists' );
return $status;
}
global $IP;
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$rows = file( "$IP/maintenance/interwiki.list",
FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
$interwikis = [];
if ( !$rows ) {
return Status::newFatal( 'config-install-interwiki-list' );
diff --git a/www/wiki/includes/installer/DatabaseUpdater.php b/www/wiki/includes/installer/DatabaseUpdater.php
index 752bc544..b56ab62d 100644
--- a/www/wiki/includes/installer/DatabaseUpdater.php
+++ b/www/wiki/includes/installer/DatabaseUpdater.php
@@ -150,7 +150,7 @@ abstract class DatabaseUpdater {
* LoadExtensionSchemaUpdates hook.
*/
private function loadExtensions() {
- if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
+ if ( !defined( 'MEDIAWIKI_INSTALL' ) || defined( 'MW_EXTENSIONS_LOADED' ) ) {
return; // already loaded
}
$vars = Installer::getExistingLocalSettings();
@@ -162,7 +162,7 @@ abstract class DatabaseUpdater {
// This will automatically add "AutoloadClasses" to $wgAutoloadClasses
$data = $registry->readFromQueue( $queue );
- $hooks = [ 'wgHooks' => [ 'LoadExtensionSchemaUpdates' => [] ] ];
+ $hooks = [];
if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
$hooks = $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'];
}
@@ -179,12 +179,12 @@ abstract class DatabaseUpdater {
/**
* @param Database $db
* @param bool $shared
- * @param Maintenance $maintenance
+ * @param Maintenance|null $maintenance
*
* @throws MWException
* @return DatabaseUpdater
*/
- public static function newForDB( Database $db, $shared = false, $maintenance = null ) {
+ public static function newForDB( Database $db, $shared = false, Maintenance $maintenance = null ) {
$type = $db->getType();
if ( in_array( $type, Installer::getDBTypes() ) ) {
$class = ucfirst( $type ) . 'Updater';
@@ -340,13 +340,23 @@ abstract class DatabaseUpdater {
*
* @param string $tableName The table name
* @param string $fieldName The field to be modified
- * @param string $sqlPath The path to the SQL change path
+ * @param string $sqlPath The path to the SQL patch
*/
public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
$this->extensionUpdates[] = [ 'modifyField', $tableName, $fieldName, $sqlPath, true ];
}
/**
+ * @since 1.31
+ *
+ * @param string $tableName The table name
+ * @param string $sqlPath The path to the SQL patch
+ */
+ public function modifyExtensionTable( $tableName, $sqlPath ) {
+ $this->extensionUpdates[] = [ 'modifyTable', $tableName, $sqlPath, true ];
+ }
+
+ /**
*
* @since 1.20
*
@@ -606,7 +616,7 @@ abstract class DatabaseUpdater {
* 1.13...) with the values being arrays of updates, identical to how
* updaters.inc did it (for now)
*
- * @return array
+ * @return array[]
*/
abstract protected function getCoreUpdateList();
@@ -988,19 +998,27 @@ abstract class DatabaseUpdater {
}
/**
- * Purge the objectcache table
+ * Purge various database caches
*/
public function purgeCache() {
global $wgLocalisationCacheConf;
- # We can't guarantee that the user will be able to use TRUNCATE,
- # but we know that DELETE is available to us
+ // We can't guarantee that the user will be able to use TRUNCATE,
+ // but we know that DELETE is available to us
$this->output( "Purging caches..." );
+
+ // ObjectCache
$this->db->delete( 'objectcache', '*', __METHOD__ );
+
+ // LocalisationCache
if ( $wgLocalisationCacheConf['manualRecache'] ) {
$this->rebuildLocalisationCache();
}
+
+ // ResourceLoader: Message cache
$blobStore = new MessageBlobStore();
$blobStore->clear();
+
+ // ResourceLoader: File-dependency cache
$this->db->delete( 'module_deps', '*', __METHOD__ );
$this->output( "done.\n" );
}
@@ -1029,7 +1047,7 @@ abstract class DatabaseUpdater {
* Sets the number of active users in the site_stats table
*/
protected function doActiveUsersInit() {
- $activeUsers = $this->db->selectField( 'site_stats', 'ss_active_users', false, __METHOD__ );
+ $activeUsers = $this->db->selectField( 'site_stats', 'ss_active_users', '', __METHOD__ );
if ( $activeUsers == -1 ) {
$activeUsers = $this->db->selectField( 'recentchanges',
'COUNT( DISTINCT rc_user_text )',
@@ -1054,7 +1072,7 @@ abstract class DatabaseUpdater {
"maintenance/populateLogUsertext.php.\n"
);
- $task = $this->maintenance->runChild( 'PopulateLogUsertext' );
+ $task = $this->maintenance->runChild( PopulateLogUsertext::class );
$task->execute();
$this->output( "done.\n" );
}
@@ -1070,7 +1088,7 @@ abstract class DatabaseUpdater {
"databases, you may want to hit Ctrl-C and do this manually with\n" .
"maintenance/populateLogSearch.php.\n" );
- $task = $this->maintenance->runChild( 'PopulateLogSearch' );
+ $task = $this->maintenance->runChild( PopulateLogSearch::class );
$task->execute();
$this->output( "done.\n" );
}
@@ -1110,7 +1128,7 @@ abstract class DatabaseUpdater {
}
$this->output( "Updating category collations..." );
- $task = $this->maintenance->runChild( 'UpdateCollation' );
+ $task = $this->maintenance->runChild( UpdateCollation::class );
$task->execute();
$this->output( "...done.\n" );
}
@@ -1121,7 +1139,7 @@ abstract class DatabaseUpdater {
*/
protected function doMigrateUserOptions() {
if ( $this->db->tableExists( 'user_properties' ) ) {
- $cl = $this->maintenance->runChild( 'ConvertUserOptions', 'convertUserOptions.php' );
+ $cl = $this->maintenance->runChild( ConvertUserOptions::class, 'convertUserOptions.php' );
$cl->execute();
$this->output( "done.\n" );
}
@@ -1159,7 +1177,9 @@ abstract class DatabaseUpdater {
/**
* @var $cl RebuildLocalisationCache
*/
- $cl = $this->maintenance->runChild( 'RebuildLocalisationCache', 'rebuildLocalisationCache.php' );
+ $cl = $this->maintenance->runChild(
+ RebuildLocalisationCache::class, 'rebuildLocalisationCache.php'
+ );
$this->output( "Rebuilding localisation cache...\n" );
$cl->setForce();
$cl->execute();
@@ -1206,10 +1226,65 @@ abstract class DatabaseUpdater {
"databases, you may want to hit Ctrl-C and do this manually with\n" .
"maintenance/migrateComments.php.\n"
);
- $task = $this->maintenance->runChild( 'MigrateComments', 'migrateComments.php' );
- $task->execute();
- $this->output( "done.\n" );
+ $task = $this->maintenance->runChild( MigrateComments::class, 'migrateComments.php' );
+ $ok = $task->execute();
+ $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
+ }
+ }
+
+ /**
+ * Migrate actors to the new 'actor' table
+ * @since 1.31
+ */
+ protected function migrateActors() {
+ global $wgActorTableSchemaMigrationStage;
+ if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_NEW &&
+ !$this->updateRowExists( 'MigrateActors' )
+ ) {
+ $this->output(
+ "Migrating actors to the 'actor' table, printing progress markers. For large\n" .
+ "databases, you may want to hit Ctrl-C and do this manually with\n" .
+ "maintenance/migrateActors.php.\n"
+ );
+ $task = $this->maintenance->runChild( 'MigrateActors', 'migrateActors.php' );
+ $ok = $task->execute();
+ $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
}
}
+ /**
+ * Migrate ar_text to modern storage
+ * @since 1.31
+ */
+ protected function migrateArchiveText() {
+ if ( $this->db->fieldExists( 'archive', 'ar_text', __METHOD__ ) ) {
+ $this->output( "Migrating archive ar_text to modern storage.\n" );
+ $task = $this->maintenance->runChild( MigrateArchiveText::class, 'migrateArchiveText.php' );
+ $task->setForce();
+ if ( $task->execute() ) {
+ $this->applyPatch( 'patch-drop-ar_text.sql', false,
+ 'Dropping ar_text and ar_flags columns' );
+ }
+ }
+ }
+
+ /**
+ * Populate ar_rev_id, then make it not nullable
+ * @since 1.31
+ */
+ protected function populateArchiveRevId() {
+ $info = $this->db->fieldInfo( 'archive', 'ar_rev_id', __METHOD__ );
+ if ( !$info ) {
+ throw new MWException( 'Missing ar_rev_id field of archive table. Should not happen.' );
+ }
+ if ( $info->isNullable() ) {
+ $this->output( "Populating ar_rev_id.\n" );
+ $task = $this->maintenance->runChild( 'PopulateArchiveRevId', 'populateArchiveRevId.php' );
+ if ( $task->execute() ) {
+ $this->applyPatch( 'patch-ar_rev_id-not-null.sql', false,
+ 'Making ar_rev_id not nullable' );
+ }
+ }
+ }
+
}
diff --git a/www/wiki/includes/installer/InstallDocFormatter.php b/www/wiki/includes/installer/InstallDocFormatter.php
index 4163e2f9..08cfd868 100644
--- a/www/wiki/includes/installer/InstallDocFormatter.php
+++ b/www/wiki/includes/installer/InstallDocFormatter.php
@@ -21,7 +21,7 @@
*/
class InstallDocFormatter {
- static function format( $text ) {
+ public static function format( $text ) {
$obj = new self( $text );
return $obj->execute();
diff --git a/www/wiki/includes/installer/Installer.php b/www/wiki/includes/installer/Installer.php
index 012b4775..abf4de4f 100644
--- a/www/wiki/includes/installer/Installer.php
+++ b/www/wiki/includes/installer/Installer.php
@@ -23,7 +23,10 @@
* @file
* @ingroup Deployment
*/
+
+use MediaWiki\Interwiki\NullInterwikiLookup;
use MediaWiki\MediaWikiServices;
+use MediaWiki\Shell\Shell;
/**
* This documentation group collects source code files with deployment functionality.
@@ -243,7 +246,6 @@ abstract class Installer {
* @var array
*/
protected $objectCaches = [
- 'xcache' => 'xcache_get',
'apc' => 'apc_fetch',
'apcu' => 'apcu_fetch',
'wincache' => 'wincache_ucache_get'
@@ -364,7 +366,7 @@ abstract class Installer {
// disable (problematic) object cache types explicitly, preserving all other (working) ones
// bug T113843
- $emptyCache = [ 'class' => 'EmptyBagOStuff' ];
+ $emptyCache = [ 'class' => EmptyBagOStuff::class ];
$objectCaches = [
CACHE_NONE => $emptyCache,
@@ -404,7 +406,7 @@ abstract class Installer {
$installerConfig = self::getInstallerConfig( $defaultConfig );
// Reset all services and inject config overrides
- MediaWiki\MediaWikiServices::resetGlobalInstance( $installerConfig );
+ MediaWikiServices::resetGlobalInstance( $installerConfig );
// Don't attempt to load user language options (T126177)
// This will be overridden in the web installer with the user-specified language
@@ -415,13 +417,19 @@ abstract class Installer {
Language::getLocalisationCache()->disableBackend();
// Disable all global services, since we don't have any configuration yet!
- MediaWiki\MediaWikiServices::disableStorageBackend();
+ MediaWikiServices::disableStorageBackend();
+ $mwServices = MediaWikiServices::getInstance();
// Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
// SqlBagOStuff will then throw since we just disabled wfGetDB)
- $wgObjectCaches = MediaWikiServices::getInstance()->getMainConfig()->get( 'ObjectCaches' );
+ $wgObjectCaches = $mwServices->getMainConfig()->get( 'ObjectCaches' );
$wgMemc = ObjectCache::getInstance( CACHE_NONE );
+ // Disable interwiki lookup, to avoid database access during parses
+ $mwServices->redefineService( 'InterwikiLookup', function () {
+ return new NullInterwikiLookup();
+ } );
+
// Having a user with id = 0 safeguards us from DB access via User::loadOptions().
$wgUser = User::newFromId( 0 );
RequestContext::getMain()->setUser( $wgUser );
@@ -446,8 +454,6 @@ abstract class Installer {
$this->parserTitle = Title::newFromText( 'Installer' );
$this->parserOptions = new ParserOptions( $wgUser ); // language will be wrong :(
- $this->parserOptions->setEditSection( false );
- $this->parserOptions->setWrapOutputClass( false );
// Don't try to access DB before user language is initialised
$this->setParserLanguage( Language::factory( 'en' ) );
}
@@ -595,15 +601,14 @@ abstract class Installer {
global $wgAutoloadClasses;
$wgAutoloadClasses = [];
- // @codingStandardsIgnoreStart
// LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
// Define the required globals here, to ensure, the functions can do it work correctly.
+ // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
global $wgExtensionDirectory, $wgStyleDirectory;
- // @codingStandardsIgnoreEnd
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$_lsExists = file_exists( "$IP/LocalSettings.php" );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$_lsExists ) {
return false;
@@ -688,13 +693,12 @@ abstract class Installer {
try {
$out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
- $html = $out->getText();
+ $html = $out->getText( [
+ 'enableSectionEditLinks' => false,
+ 'unwrap' => true,
+ ] );
} catch ( MediaWiki\Services\ServiceDisabledException $e ) {
$html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
-
- if ( !empty( $this->debug ) ) {
- $html .= "<!--\n" . $e->getTraceAsString() . "\n-->";
- }
}
return $html;
@@ -809,14 +813,14 @@ abstract class Installer {
* @return bool
*/
protected function envCheckPCRE() {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
// Need to check for \p support too, as PCRE can be compiled
// with utf8 support, but not unicode property support.
// check that \p{Zs} (space separators) matches
// U+3000 (Ideographic space)
$regexprop = preg_replace( '/\p{Zs}/u', '', "-\xE3\x80\x80-" );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $regexd != '--' || $regexprop != '--' ) {
$this->showError( 'config-pcre-no-utf8' );
@@ -860,9 +864,6 @@ abstract class Installer {
$caches = [];
foreach ( $this->objectCaches as $name => $function ) {
if ( function_exists( $function ) ) {
- if ( $name == 'xcache' && !wfIniGetBool( 'xcache.var_size' ) ) {
- continue;
- }
$caches[$name] = true;
}
}
@@ -893,10 +894,13 @@ abstract class Installer {
* @return bool
*/
protected function envCheckDiff3() {
- $names = [ "gdiff3", "diff3", "diff3.exe" ];
- $versionInfo = [ '$1 --version 2>&1', 'GNU diffutils' ];
+ $names = [ "gdiff3", "diff3" ];
+ if ( wfIsWindows() ) {
+ $names[] = 'diff3.exe';
+ }
+ $versionInfo = [ '--version', 'GNU diffutils' ];
- $diff3 = self::locateExecutableInDefaultPaths( $names, $versionInfo );
+ $diff3 = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
if ( $diff3 ) {
$this->setVar( 'wgDiff3', $diff3 );
@@ -913,9 +917,9 @@ abstract class Installer {
* @return bool
*/
protected function envCheckGraphics() {
- $names = [ wfIsWindows() ? 'convert.exe' : 'convert' ];
- $versionInfo = [ '$1 -version', 'ImageMagick' ];
- $convert = self::locateExecutableInDefaultPaths( $names, $versionInfo );
+ $names = wfIsWindows() ? 'convert.exe' : 'convert';
+ $versionInfo = [ '-version', 'ImageMagick' ];
+ $convert = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
$this->setVar( 'wgImageMagickConvertCommand', '' );
if ( $convert ) {
@@ -939,10 +943,10 @@ abstract class Installer {
* @return bool
*/
protected function envCheckGit() {
- $names = [ wfIsWindows() ? 'git.exe' : 'git' ];
- $versionInfo = [ '$1 --version', 'git version' ];
+ $names = wfIsWindows() ? 'git.exe' : 'git';
+ $versionInfo = [ '--version', 'git version' ];
- $git = self::locateExecutableInDefaultPaths( $names, $versionInfo );
+ $git = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
if ( $git ) {
$this->setVar( 'wgGitBin', $git );
@@ -994,18 +998,22 @@ abstract class Installer {
return true;
}
+ if ( Shell::isDisabled() ) {
+ return true;
+ }
+
# Get a list of available locales.
- $ret = false;
- $lines = wfShellExec( '/usr/bin/locale -a', $ret );
+ $result = Shell::command( '/usr/bin/locale', '-a' )
+ ->execute();
- if ( $ret ) {
+ if ( $result->getExitCode() != 0 ) {
return true;
}
+ $lines = $result->getStdout();
$lines = array_map( 'trim', explode( "\n", $lines ) );
$candidatesByLocale = [];
$candidatesByLang = [];
-
foreach ( $lines as $line ) {
if ( $line === '' ) {
continue;
@@ -1192,88 +1200,6 @@ abstract class Installer {
}
/**
- * Get an array of likely places we can find executables. Check a bunch
- * of known Unix-like defaults, as well as the PATH environment variable
- * (which should maybe make it work for Windows?)
- *
- * @return array
- */
- protected static function getPossibleBinPaths() {
- return array_merge(
- [ '/usr/bin', '/usr/local/bin', '/opt/csw/bin',
- '/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ],
- explode( PATH_SEPARATOR, getenv( 'PATH' ) )
- );
- }
-
- /**
- * Search a path for any of the given executable names. Returns the
- * executable name if found. Also checks the version string returned
- * by each executable.
- *
- * Used only by environment checks.
- *
- * @param string $path Path to search
- * @param array $names Array of executable names
- * @param array|bool $versionInfo False or array with two members:
- * 0 => Command to run for version check, with $1 for the full executable name
- * 1 => String to compare the output with
- *
- * If $versionInfo is not false, only executables with a version
- * matching $versionInfo[1] will be returned.
- * @return bool|string
- */
- public static function locateExecutable( $path, $names, $versionInfo = false ) {
- if ( !is_array( $names ) ) {
- $names = [ $names ];
- }
-
- foreach ( $names as $name ) {
- $command = $path . DIRECTORY_SEPARATOR . $name;
-
- MediaWiki\suppressWarnings();
- $file_exists = is_executable( $command );
- MediaWiki\restoreWarnings();
-
- if ( $file_exists ) {
- if ( !$versionInfo ) {
- return $command;
- }
-
- $file = str_replace( '$1', wfEscapeShellArg( $command ), $versionInfo[0] );
- if ( strstr( wfShellExec( $file ), $versionInfo[1] ) !== false ) {
- return $command;
- }
- }
- }
-
- return false;
- }
-
- /**
- * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
- * @see locateExecutable()
- * @param array $names Array of possible names.
- * @param array|bool $versionInfo Default: false or array with two members:
- * 0 => Command to run for version check, with $1 for the full executable name
- * 1 => String to compare the output with
- *
- * If $versionInfo is not false, only executables with a version
- * matching $versionInfo[1] will be returned.
- * @return bool|string
- */
- public static function locateExecutableInDefaultPaths( $names, $versionInfo = false ) {
- foreach ( self::getPossibleBinPaths() as $path ) {
- $exe = self::locateExecutable( $path, $names, $versionInfo );
- if ( $exe !== false ) {
- return $exe;
- }
- }
-
- return false;
- }
-
- /**
* Checks if scripts located in the given directory can be executed via the given URL.
*
* Used only by environment checks.
@@ -1285,13 +1211,13 @@ abstract class Installer {
$scriptTypes = [
'php' => [
"<?php echo 'ex' . 'ec';",
- "#!/var/env php5\n<?php echo 'ex' . 'ec';",
+ "#!/var/env php\n<?php echo 'ex' . 'ec';",
],
];
// it would be good to check other popular languages here, but it'll be slow.
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
foreach ( $scriptTypes as $ext => $contents ) {
foreach ( $contents as $source ) {
@@ -1310,14 +1236,14 @@ abstract class Installer {
unlink( $dir . $file );
if ( $text == 'exec' ) {
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return $ext;
}
}
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return false;
}
@@ -1387,7 +1313,15 @@ abstract class Installer {
if ( !is_dir( "$extDir/$file" ) ) {
continue;
}
- if ( file_exists( "$extDir/$file/$jsonFile" ) || file_exists( "$extDir/$file/$file.php" ) ) {
+ $fullJsonFile = "$extDir/$file/$jsonFile";
+ $isJson = file_exists( $fullJsonFile );
+ $isPhp = false;
+ if ( !$isJson ) {
+ // Only fallback to PHP file if JSON doesn't exist
+ $fullPhpFile = "$extDir/$file/$file.php";
+ $isPhp = file_exists( $fullPhpFile );
+ }
+ if ( $isJson || $isPhp ) {
// Extension exists. Now see if there are screenshots
$exts[$file] = [];
if ( is_dir( "$extDir/$file/screenshots" ) ) {
@@ -1398,6 +1332,13 @@ abstract class Installer {
}
}
+ if ( $isJson ) {
+ $info = $this->readExtension( $fullJsonFile );
+ if ( $info === false ) {
+ continue;
+ }
+ $exts[$file] += $info;
+ }
}
closedir( $dh );
uksort( $exts, 'strnatcasecmp' );
@@ -1406,6 +1347,82 @@ abstract class Installer {
}
/**
+ * @param string $fullJsonFile
+ * @param array $extDeps
+ * @param array $skinDeps
+ *
+ * @return array|bool False if this extension can't be loaded
+ */
+ private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
+ $load = [
+ $fullJsonFile => 1
+ ];
+ if ( $extDeps ) {
+ $extDir = $this->getVar( 'IP' ) . '/extensions';
+ foreach ( $extDeps as $dep ) {
+ $fname = "$extDir/$dep/extension.json";
+ if ( !file_exists( $fname ) ) {
+ return false;
+ }
+ $load[$fname] = 1;
+ }
+ }
+ if ( $skinDeps ) {
+ $skinDir = $this->getVar( 'IP' ) . '/skins';
+ foreach ( $skinDeps as $dep ) {
+ $fname = "$skinDir/$dep/skin.json";
+ if ( !file_exists( $fname ) ) {
+ return false;
+ }
+ $load[$fname] = 1;
+ }
+ }
+ $registry = new ExtensionRegistry();
+ try {
+ $info = $registry->readFromQueue( $load );
+ } catch ( ExtensionDependencyError $e ) {
+ if ( $e->incompatibleCore || $e->incompatibleSkins
+ || $e->incompatibleExtensions
+ ) {
+ // If something is incompatible with a dependency, we have no real
+ // option besides skipping it
+ return false;
+ } elseif ( $e->missingExtensions || $e->missingSkins ) {
+ // There's an extension missing in the dependency tree,
+ // so add those to the dependency list and try again
+ return $this->readExtension(
+ $fullJsonFile,
+ array_merge( $extDeps, $e->missingExtensions ),
+ array_merge( $skinDeps, $e->missingSkins )
+ );
+ }
+ // Some other kind of dependency error?
+ return false;
+ }
+ $ret = [];
+ // The order of credits will be the order of $load,
+ // so the first extension is the one we want to load,
+ // everything else is a dependency
+ $i = 0;
+ foreach ( $info['credits'] as $name => $credit ) {
+ $i++;
+ if ( $i == 1 ) {
+ // Extension we want to load
+ continue;
+ }
+ $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions';
+ $ret['requires'][$type][] = $credit['name'];
+ }
+ $credits = array_values( $info['credits'] )[0];
+ if ( isset( $credits['url'] ) ) {
+ $ret['url'] = $credits['url'];
+ }
+ $ret['type'] = $credits['type'];
+
+ return $ret;
+ }
+
+ /**
* Returns a default value to be used for $wgDefaultSkin: normally the one set in DefaultSettings,
* but will fall back to another if the default skin is missing and some other one is present
* instead.
@@ -1432,6 +1449,10 @@ abstract class Installer {
$exts = $this->getVar( '_Extensions' );
$IP = $this->getVar( 'IP' );
+ // Marker for DatabaseUpdater::loadExtensions so we don't
+ // double load extensions
+ define( 'MW_EXTENSIONS_LOADED', true );
+
/**
* We need to include DefaultSettings before including extensions to avoid
* warnings about unset variables. However, the only thing we really
@@ -1564,6 +1585,11 @@ abstract class Installer {
}
}
if ( $status->isOk() ) {
+ $this->showMessage(
+ 'config-install-success',
+ $this->getVar( 'wgServer' ),
+ $this->getVar( 'wgScriptPath' )
+ );
$this->setVar( '_InstallDone', true );
}
@@ -1645,7 +1671,7 @@ abstract class Installer {
$user->saveSettings();
// Update user count
- $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
+ $ssUpdate = SiteStatsUpdate::factory( [ 'users' => 1 ] );
$ssUpdate->doUpdate();
}
$status = Status::newGood();
@@ -1754,7 +1780,7 @@ abstract class Installer {
// implementation that won't stomp on PHP's cookies.
$GLOBALS['wgSessionProviders'] = [
[
- 'class' => 'InstallerSessionProvider',
+ 'class' => InstallerSessionProvider::class,
'args' => [ [
'priority' => 1,
] ]
@@ -1781,8 +1807,8 @@ abstract class Installer {
* Some long-running pages (Install, Upgrade) will want to do this
*/
protected function disableTimeLimit() {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
set_time_limit( 0 );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
}
diff --git a/www/wiki/includes/installer/InstallerOverrides.php b/www/wiki/includes/installer/InstallerOverrides.php
index eba3a20d..c9154b9a 100644
--- a/www/wiki/includes/installer/InstallerOverrides.php
+++ b/www/wiki/includes/installer/InstallerOverrides.php
@@ -30,9 +30,9 @@ class InstallerOverrides {
if ( !$overrides ) {
$overrides = [
- 'LocalSettingsGenerator' => 'LocalSettingsGenerator',
- 'WebInstaller' => 'WebInstaller',
- 'CliInstaller' => 'CliInstaller',
+ 'LocalSettingsGenerator' => LocalSettingsGenerator::class,
+ 'WebInstaller' => WebInstaller::class,
+ 'CliInstaller' => CliInstaller::class,
];
foreach ( glob( "$IP/mw-config/overrides/*.php" ) as $file ) {
require $file;
diff --git a/www/wiki/includes/installer/LocalSettingsGenerator.php b/www/wiki/includes/installer/LocalSettingsGenerator.php
index bdaeaca8..6d70338c 100644
--- a/www/wiki/includes/installer/LocalSettingsGenerator.php
+++ b/www/wiki/includes/installer/LocalSettingsGenerator.php
@@ -185,7 +185,7 @@ class LocalSettingsGenerator {
$jsonFile = 'skin.json';
$function = 'wfLoadSkin';
} else {
- throw new InvalidArgumentException( '$dir was not "extensions" or "skins' );
+ throw new InvalidArgumentException( '$dir was not "extensions" or "skins"' );
}
$encName = self::escapePhpString( $name );
@@ -299,6 +299,12 @@ class LocalSettingsGenerator {
}
$mcservers = $this->buildMemcachedServerList();
+ if ( file_exists( dirname( __DIR__ ) . '/PlatformSettings.php' ) ) {
+ $platformSettings = "\n## Include platform/distribution defaults";
+ $platformSettings .= "\nrequire_once \"\$IP/includes/PlatformSettings.php\";";
+ } else {
+ $platformSettings = '';
+ }
return "<?php
# This file was automatically generated by the MediaWiki {$GLOBALS['wgVersion']}
@@ -316,6 +322,7 @@ class LocalSettingsGenerator {
if ( !defined( 'MEDIAWIKI' ) ) {
exit;
}
+{$platformSettings}
## Uncomment this to disable output compression
# \$wgDisableOutputCompression = true;
diff --git a/www/wiki/includes/installer/MssqlUpdater.php b/www/wiki/includes/installer/MssqlUpdater.php
index 411d2c8c..2e339997 100644
--- a/www/wiki/includes/installer/MssqlUpdater.php
+++ b/www/wiki/includes/installer/MssqlUpdater.php
@@ -105,6 +105,29 @@ class MssqlUpdater extends DatabaseUpdater {
// 1.30
[ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
[ 'addIndex', 'site_stats', 'PRIMARY', 'patch-site_stats-pk.sql' ],
+
+ // Should have been in 1.30
+ [ 'addTable', 'comment', 'patch-comment-table.sql' ],
+ // This field was added in 1.31, but is put here so it can be used by 'migrateComments'
+ [ 'addField', 'image', 'img_description_id', 'patch-image-img_description_id.sql' ],
+ // Should have been in 1.30
+ [ 'migrateComments' ],
+
+ // 1.31
+ [ 'addTable', 'slots', 'patch-slots.sql' ],
+ [ 'addField', 'slots', 'slot_origin', 'patch-slot-origin.sql' ],
+ [ 'addTable', 'content', 'patch-content.sql' ],
+ [ 'addTable', 'slot_roles', 'patch-slot_roles.sql' ],
+ [ 'addTable', 'content_models', 'patch-content_models.sql' ],
+ [ 'migrateArchiveText' ],
+ [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+ [ 'migrateActors' ],
+ [ 'modifyField', 'revision', 'rev_text_id', 'patch-rev_text_id-default.sql' ],
+ [ 'modifyTable', 'site_stats', 'patch-site_stats-modify.sql' ],
+ [ 'populateArchiveRevId' ],
+ [ 'modifyField', 'recentchanges', 'rc_patrolled', 'patch-rc_patrolled_type.sql' ],
+ [ 'addIndex', 'recentchanges', 'rc_namespace_title_timestamp',
+ 'patch-recentchanges-nttindex.sql' ],
];
}
diff --git a/www/wiki/includes/installer/MysqlInstaller.php b/www/wiki/includes/installer/MysqlInstaller.php
index ab5701a8..45f932a8 100644
--- a/www/wiki/includes/installer/MysqlInstaller.php
+++ b/www/wiki/includes/installer/MysqlInstaller.php
@@ -40,7 +40,6 @@ class MysqlInstaller extends DatabaseInstaller {
'wgDBpassword',
'wgDBprefix',
'wgDBTableOptions',
- 'wgDBmysql5',
];
protected $internalDefaults = [
@@ -73,7 +72,7 @@ class MysqlInstaller extends DatabaseInstaller {
* @return bool
*/
public function isCompiled() {
- return self::checkExtension( 'mysql' ) || self::checkExtension( 'mysqli' );
+ return self::checkExtension( 'mysqli' );
}
/**
@@ -414,20 +413,6 @@ class MysqlInstaller extends DatabaseInstaller {
$this->setVar( '_MysqlCharset', reset( $charsets ) );
}
- // Do charset selector
- if ( count( $charsets ) >= 2 ) {
- // getRadioSet() builds a set of labeled radio buttons.
- // For grep: The following messages are used as the item labels:
- // config-mysql-binary, config-mysql-utf8
- $s .= $this->getRadioSet( [
- 'var' => '_MysqlCharset',
- 'label' => 'config-mysql-charset',
- 'itemLabelPrefix' => 'config-mysql-',
- 'values' => $charsets
- ] );
- $s .= $this->parent->getHelpBox( 'config-mysql-charset-help' );
- }
-
return $s;
}
@@ -671,7 +656,6 @@ class MysqlInstaller extends DatabaseInstaller {
}
public function getLocalSettings() {
- $dbmysql5 = wfBoolToStr( $this->getVar( 'wgDBmysql5', true ) );
$prefix = LocalSettingsGenerator::escapePhpString( $this->getVar( 'wgDBprefix' ) );
$tblOpts = LocalSettingsGenerator::escapePhpString( $this->getTableOptions() );
@@ -679,9 +663,6 @@ class MysqlInstaller extends DatabaseInstaller {
\$wgDBprefix = \"{$prefix}\";
# MySQL table options to use during installation or update
-\$wgDBTableOptions = \"{$tblOpts}\";
-
-# Experimental charset support for MySQL 5.0.
-\$wgDBmysql5 = {$dbmysql5};";
+\$wgDBTableOptions = \"{$tblOpts}\";";
}
}
diff --git a/www/wiki/includes/installer/MysqlUpdater.php b/www/wiki/includes/installer/MysqlUpdater.php
index e2ff9604..60bb69fd 100644
--- a/www/wiki/includes/installer/MysqlUpdater.php
+++ b/www/wiki/includes/installer/MysqlUpdater.php
@@ -20,8 +20,8 @@
* @file
* @ingroup Deployment
*/
-use Wikimedia\Rdbms\Field;
use Wikimedia\Rdbms\MySQLField;
+use Wikimedia\Rdbms\IDatabase;
use MediaWiki\MediaWikiServices;
/**
@@ -83,7 +83,7 @@ class MysqlUpdater extends DatabaseUpdater {
[ 'doUserUniqueUpdate' ],
[ 'doUserGroupsUpdate' ],
[ 'addField', 'site_stats', 'ss_total_pages', 'patch-ss_total_articles.sql' ],
- [ 'addTable', 'user_newtalk', 'patch-usernewtalk2.sql' ],
+ [ 'addTable', 'user_newtalk', 'patch-usernewtalk.sql' ],
[ 'addTable', 'transcache', 'patch-transcache.sql' ],
[ 'addField', 'interwiki', 'iw_trans', 'patch-interwiki-trans.sql' ],
@@ -325,10 +325,29 @@ class MysqlUpdater extends DatabaseUpdater {
[ 'renameIndex', 'user_properties', 'user_properties_user_property', 'PRIMARY', false,
'patch-user_properties-fix-pk.sql' ],
[ 'addTable', 'comment', 'patch-comment-table.sql' ],
+
+ // This field was added in 1.31, but is put here so it can be used by 'migrateComments'
+ [ 'addField', 'image', 'img_description_id', 'patch-image-img_description_id.sql' ],
+
[ 'migrateComments' ],
[ 'renameIndex', 'l10n_cache', 'lc_lang_key', 'PRIMARY', false,
'patch-l10n_cache-primary-key.sql' ],
[ 'doUnsignedSyncronisation' ],
+
+ // 1.31
+ [ 'addTable', 'slots', 'patch-slots.sql' ],
+ [ 'addField', 'slots', 'slot_origin', 'patch-slot-origin.sql' ],
+ [ 'addTable', 'content', 'patch-content.sql' ],
+ [ 'addTable', 'slot_roles', 'patch-slot_roles.sql' ],
+ [ 'addTable', 'content_models', 'patch-content_models.sql' ],
+ [ 'migrateArchiveText' ],
+ [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+ [ 'migrateActors' ],
+ [ 'modifyField', 'revision', 'rev_text_id', 'patch-rev_text_id-default.sql' ],
+ [ 'modifyTable', 'site_stats', 'patch-site_stats-modify.sql' ],
+ [ 'populateArchiveRevId' ],
+ [ 'addIndex', 'recentchanges', 'rc_namespace_title_timestamp',
+ 'patch-recentchanges-nttindex.sql' ],
];
}
@@ -390,7 +409,7 @@ class MysqlUpdater extends DatabaseUpdater {
global $IP;
if ( !$this->doTable( 'interwiki' ) ) {
- return true;
+ return;
}
if ( $this->db->tableExists( "interwiki", __METHOD__ ) ) {
@@ -425,7 +444,7 @@ class MysqlUpdater extends DatabaseUpdater {
}
protected function doOldLinksUpdate() {
- $cl = $this->maintenance->runChild( 'ConvertLinks' );
+ $cl = $this->maintenance->runChild( ConvertLinks::class );
$cl->execute();
}
@@ -465,6 +484,17 @@ class MysqlUpdater extends DatabaseUpdater {
return;
}
+ $insertOpts = [ 'IGNORE' ];
+ $selectOpts = [];
+
+ // If wl_id exists, make insertSelect() more replication-safe by
+ // ordering on that column. If not, hint that it should be safe anyway.
+ if ( $this->db->fieldExists( 'watchlist', 'wl_id', __METHOD__ ) ) {
+ $selectOpts['ORDER BY'] = 'wl_id';
+ } else {
+ $insertOpts[] = 'NO_AUTO_COLUMNS';
+ }
+
$this->output( "Adding missing watchlist talk page rows... " );
$this->db->insertSelect( 'watchlist', 'watchlist',
[
@@ -472,7 +502,7 @@ class MysqlUpdater extends DatabaseUpdater {
'wl_namespace' => 'wl_namespace | 1',
'wl_title' => 'wl_title',
'wl_notificationtimestamp' => 'wl_notificationtimestamp'
- ], [ 'NOT (wl_namespace & 1)' ], __METHOD__, 'IGNORE' );
+ ], [ 'NOT (wl_namespace & 1)' ], __METHOD__, $insertOpts, $selectOpts );
$this->output( "done.\n" );
$this->output( "Adding missing watchlist subject page rows... " );
@@ -482,7 +512,7 @@ class MysqlUpdater extends DatabaseUpdater {
'wl_namespace' => 'wl_namespace & ~1',
'wl_title' => 'wl_title',
'wl_notificationtimestamp' => 'wl_notificationtimestamp'
- ], [ 'wl_namespace & 1' ], __METHOD__, 'IGNORE' );
+ ], [ 'wl_namespace & 1' ], __METHOD__, $insertOpts, $selectOpts );
$this->output( "done.\n" );
}
@@ -531,25 +561,12 @@ class MysqlUpdater extends DatabaseUpdater {
) );
}
$sql = "SELECT cur_title, cur_namespace, cur_id, cur_timestamp FROM $cur WHERE ";
- $firstCond = true;
+ $dupeTitles = [];
foreach ( $duplicate as $ns => $titles ) {
- if ( $firstCond ) {
- $firstCond = false;
- } else {
- $sql .= ' OR ';
- }
- $sql .= "( cur_namespace = {$ns} AND cur_title in (";
- $first = true;
- foreach ( $titles as $t ) {
- if ( $first ) {
- $sql .= $this->db->addQuotes( $t );
- $first = false;
- } else {
- $sql .= ', ' . $this->db->addQuotes( $t );
- }
- }
- $sql .= ") ) \n";
+ $dupeTitles[] = "( cur_namespace = {$ns} AND cur_title in ("
+ . $this->db->makeList( $titles ) . ") ) \n";
}
+ $sql .= $this->db->makeList( $dupeTitles, IDatabase::LIST_OR );
# By sorting descending, the most recent entry will be the first in the list.
# All following entries will be deleted by the next while-loop.
$sql .= 'ORDER BY cur_namespace, cur_title, cur_timestamp DESC';
@@ -878,7 +895,8 @@ class MysqlUpdater extends DatabaseUpdater {
$this->applyPatch( 'patch-templatelinks.sql', false, "Creating templatelinks table" );
$this->output( "Populating...\n" );
- if ( wfGetLB()->getServerCount() > 1 ) {
+ $services = MediaWikiServices::getInstance();
+ if ( $services->getDBLoadBalancer()->getServerCount() > 1 ) {
// Slow, replication-friendly update
$res = $this->db->select( 'pagelinks', [ 'pl_from', 'pl_namespace', 'pl_title' ],
[ 'pl_namespace' => NS_TEMPLATE ], __METHOD__ );
@@ -886,7 +904,7 @@ class MysqlUpdater extends DatabaseUpdater {
foreach ( $res as $row ) {
$count = ( $count + 1 ) % 100;
if ( $count == 0 ) {
- $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $lbFactory = $services->getDBLoadBalancerFactory();
$lbFactory->waitForReplication( [ 'wiki' => wfWikiID() ] );
}
$this->db->insert( 'templatelinks',
@@ -906,7 +924,8 @@ class MysqlUpdater extends DatabaseUpdater {
'tl_title' => 'pl_title'
], [
'pl_namespace' => 10
- ], __METHOD__
+ ], __METHOD__,
+ [ 'NO_AUTO_COLUMNS' ] // There's no "tl_id" auto-increment field
);
}
$this->output( "Done. Please run maintenance/refreshLinks.php for a more " .
@@ -947,7 +966,7 @@ class MysqlUpdater extends DatabaseUpdater {
$this->output( "done.\n" );
$this->output( "Migrating old restrictions to new table...\n" );
- $task = $this->maintenance->runChild( 'UpdateRestrictions' );
+ $task = $this->maintenance->runChild( UpdateRestrictions::class );
$task->execute();
}
@@ -970,7 +989,7 @@ class MysqlUpdater extends DatabaseUpdater {
"may want to hit Ctrl-C and do this manually with maintenance/\n" .
"populateCategory.php.\n"
);
- $task = $this->maintenance->runChild( 'PopulateCategory' );
+ $task = $this->maintenance->runChild( PopulateCategory::class );
$task->execute();
$this->output( "Done populating category table.\n" );
}
@@ -982,7 +1001,7 @@ class MysqlUpdater extends DatabaseUpdater {
"databases, you may want to hit Ctrl-C and do this manually with\n" .
"maintenance/populateParentId.php.\n" );
- $task = $this->maintenance->runChild( 'PopulateParentId' );
+ $task = $this->maintenance->runChild( PopulateParentId::class );
$task->execute();
}
}
diff --git a/www/wiki/includes/installer/OracleInstaller.php b/www/wiki/includes/installer/OracleInstaller.php
index e0fbe1f9..659a1d7c 100644
--- a/www/wiki/includes/installer/OracleInstaller.php
+++ b/www/wiki/includes/installer/OracleInstaller.php
@@ -330,11 +330,11 @@ class OracleInstaller extends DatabaseInstaller {
* @return bool Whether the connection string is valid.
*/
public static function checkConnectStringFormat( $connect_string ) {
- // @@codingStandardsIgnoreStart Long lines with regular expressions.
+ // phpcs:disable Generic.Files.LineLength
// @todo Very long regular expression. Make more readable?
$isValid = preg_match( '/^[[:alpha:]][\w\-]*(?:\.[[:alpha:]][\w\-]*){0,2}$/', $connect_string ); // TNS name
$isValid |= preg_match( '/^(?:\/\/)?[\w\-\.]+(?::[\d]+)?(?:\/(?:[\w\-\.]+(?::(pooled|dedicated|shared))?)?(?:\/[\w\-\.]+)?)?$/', $connect_string ); // EZConnect
- // @@codingStandardsIgnoreEnd
+ // phpcs:enable
return (bool)$isValid;
}
}
diff --git a/www/wiki/includes/installer/OracleUpdater.php b/www/wiki/includes/installer/OracleUpdater.php
index 040b54a1..737b1728 100644
--- a/www/wiki/includes/installer/OracleUpdater.php
+++ b/www/wiki/includes/installer/OracleUpdater.php
@@ -127,6 +127,27 @@ class OracleUpdater extends DatabaseUpdater {
[ 'doAutoIncrementTriggers' ],
[ 'addIndex', 'site_stats', 'PRIMARY', 'patch-site_stats-pk.sql' ],
+ // Should have been in 1.30
+ [ 'addTable', 'comment', 'patch-comment-table.sql' ],
+ // This field was added in 1.31, but is put here so it can be used by 'migrateComments'
+ [ 'addField', 'image', 'img_description_id', 'patch-image-img_description_id.sql' ],
+ // Should have been in 1.30
+ [ 'migrateComments' ],
+
+ // 1.31
+ [ 'addTable', 'slots', 'patch-slots.sql' ],
+ [ 'addField', 'slots', 'slot_origin', 'patch-slot-origin.sql' ],
+ [ 'addTable', 'content', 'patch-content.sql' ],
+ [ 'addTable', 'slot_roles', 'patch-slot_roles.sql' ],
+ [ 'addTable', 'content_models', 'patch-content_models.sql' ],
+ [ 'migrateArchiveText' ],
+ [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+ [ 'migrateActors' ],
+ [ 'modifyTable', 'site_stats', 'patch-site_stats-modify.sql' ],
+ [ 'populateArchiveRevId' ],
+ [ 'addIndex', 'recentchanges', 'rc_namespace_title_timestamp',
+ 'patch-recentchanges-nttindex.sql' ],
+
// KEEP THIS AT THE BOTTOM!!
[ 'doRebuildDuplicateFunction' ],
diff --git a/www/wiki/includes/installer/PhpBugTests.php b/www/wiki/includes/installer/PhpBugTests.php
index d412216a..4e1e365d 100644
--- a/www/wiki/includes/installer/PhpBugTests.php
+++ b/www/wiki/includes/installer/PhpBugTests.php
@@ -18,10 +18,12 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @defgroup PHPBugTests PHP known bugs tests
*/
/**
+ * @defgroup PHPBugTests PHP known bugs tests
+ */
+/**
* Test for PHP+libxml2 bug which breaks XML input subtly with certain versions.
* Known fixed with PHP 5.2.9 + libxml2-2.7.3
* @see https://bugs.php.net/bug.php?id=45996
diff --git a/www/wiki/includes/installer/PostgresInstaller.php b/www/wiki/includes/installer/PostgresInstaller.php
index 1869689f..16b47e28 100644
--- a/www/wiki/includes/installer/PostgresInstaller.php
+++ b/www/wiki/includes/installer/PostgresInstaller.php
@@ -46,7 +46,7 @@ class PostgresInstaller extends DatabaseInstaller {
'_InstallUser' => 'postgres',
];
- public static $minimumVersion = '9.1';
+ public static $minimumVersion = '9.2';
protected static $notMiniumumVerisonMessage = 'config-postgres-old';
public $maxRoleSearchDepth = 5;
@@ -152,7 +152,7 @@ class PostgresInstaller extends DatabaseInstaller {
/**
* Open a PG connection with given parameters
* @param string $user User name
- * @param string $password Password
+ * @param string $password
* @param string $dbName Database name
* @param string $schema Database schema
* @return Status
@@ -639,7 +639,7 @@ class PostgresInstaller extends DatabaseInstaller {
public function setupPLpgSQL() {
// Connect as the install user, since it owns the database and so is
- // the user that needs to run "CREATE LANGAUGE"
+ // the user that needs to run "CREATE LANGUAGE"
$status = $this->getPgConnection( 'create-schema' );
if ( !$status->isOK() ) {
return $status;
diff --git a/www/wiki/includes/installer/PostgresUpdater.php b/www/wiki/includes/installer/PostgresUpdater.php
index c38eb6aa..5026ae92 100644
--- a/www/wiki/includes/installer/PostgresUpdater.php
+++ b/www/wiki/includes/installer/PostgresUpdater.php
@@ -294,7 +294,7 @@ class PostgresUpdater extends DatabaseUpdater {
[ 'log_timestamp', 'timestamptz_ops', 'btree', 0 ],
],
'CREATE INDEX "logging_times" ON "logging" USING "btree" ("log_timestamp")' ],
- [ 'dropIndex', 'oldimage', 'oi_name' ],
+ [ 'dropPgIndex', 'oldimage', 'oi_name' ],
[ 'checkIndex', 'oi_name_archive_name', [
[ 'oi_name', 'text_ops', 'btree', 0 ],
[ 'oi_archive_name', 'text_ops', 'btree', 0 ],
@@ -353,7 +353,7 @@ class PostgresUpdater extends DatabaseUpdater {
[ 'checkOiNameConstraint' ],
[ 'checkPageDeletedTrigger' ],
[ 'checkRevUserFkey' ],
- [ 'dropIndex', 'ipblocks', 'ipb_address' ],
+ [ 'dropPgIndex', 'ipblocks', 'ipb_address' ],
[ 'checkIndex', 'ipb_address_unique', [
[ 'ipb_address', 'text_ops', 'btree', 0 ],
[ 'ipb_user', 'int4_ops', 'btree', 0 ],
@@ -481,8 +481,94 @@ class PostgresUpdater extends DatabaseUpdater {
[ 'changeNullableField', 'protected_titles', 'pt_reason', 'NOT NULL', true ],
[ 'addPgField', 'protected_titles', 'pt_reason_id', 'INTEGER NOT NULL DEFAULT 0' ],
[ 'addTable', 'comment', 'patch-comment-table.sql' ],
+
+ // This field was added in 1.31, but is put here so it can be used by 'migrateComments'
+ [ 'addPgField', 'image', 'img_description_id', 'INTEGER NOT NULL DEFAULT 0' ],
+
+ [ 'migrateComments' ],
[ 'addIndex', 'site_stats', 'site_stats_pkey', 'patch-site_stats-pk.sql' ],
[ 'addTable', 'ip_changes', 'patch-ip_changes.sql' ],
+
+ // 1.31
+ [ 'addTable', 'slots', 'patch-slots-table.sql' ],
+ [ 'dropPgIndex', 'slots', 'slot_role_inherited' ],
+ [ 'dropPgField', 'slots', 'slot_inherited' ],
+ [ 'addPgField', 'slots', 'slot_origin', 'INTEGER NOT NULL' ],
+ [
+ 'addPgIndex',
+ 'slots',
+ 'slot_revision_origin_role',
+ '( slot_revision_id, slot_origin, slot_role_id )',
+ ],
+ [ 'addTable', 'content', 'patch-content-table.sql' ],
+ [ 'addTable', 'content_models', 'patch-content_models-table.sql' ],
+ [ 'addTable', 'slot_roles', 'patch-slot_roles-table.sql' ],
+ [ 'migrateArchiveText' ],
+ [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+ [ 'setDefault', 'revision', 'rev_user', 0 ],
+ [ 'setDefault', 'revision', 'rev_user_text', '' ],
+ [ 'setDefault', 'archive', 'ar_user', 0 ],
+ [ 'changeNullableField', 'archive', 'ar_user', 'NOT NULL', true ],
+ [ 'setDefault', 'archive', 'ar_user_text', '' ],
+ [ 'addPgField', 'archive', 'ar_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+ [ 'addPgIndex', 'archive', 'archive_actor', '( ar_actor )' ],
+ [ 'setDefault', 'ipblocks', 'ipb_by', 0 ],
+ [ 'addPgField', 'ipblocks', 'ipb_by_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+ [ 'setDefault', 'image', 'img_user', 0 ],
+ [ 'changeNullableField', 'image', 'img_user', 'NOT NULL', true ],
+ [ 'setDefault', 'image', 'img_user_text', '' ],
+ [ 'addPgField', 'image', 'img_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+ [ 'setDefault', 'oldimage', 'oi_user', 0 ],
+ [ 'changeNullableField', 'oldimage', 'oi_user', 'NOT NULL', true ],
+ [ 'setDefault', 'oldimage', 'oi_user_text', '' ],
+ [ 'addPgField', 'oldimage', 'oi_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+ [ 'setDefault', 'filearchive', 'fa_user', 0 ],
+ [ 'changeNullableField', 'filearchive', 'fa_user', 'NOT NULL', true ],
+ [ 'setDefault', 'filearchive', 'fa_user_text', '' ],
+ [ 'addPgField', 'filearchive', 'fa_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+ [ 'setDefault', 'recentchanges', 'rc_user', 0 ],
+ [ 'changeNullableField', 'recentchanges', 'rc_user', 'NOT NULL', true ],
+ [ 'setDefault', 'recentchanges', 'rc_user_text', '' ],
+ [ 'addPgField', 'recentchanges', 'rc_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+ [ 'setDefault', 'logging', 'log_user', 0 ],
+ [ 'changeNullableField', 'logging', 'log_user', 'NOT NULL', true ],
+ [ 'addPgField', 'logging', 'log_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+ [ 'addPgIndex', 'logging', 'logging_actor_time_backwards', '( log_timestamp, log_actor )' ],
+ [ 'addPgIndex', 'logging', 'logging_actor_type_time', '( log_actor, log_type, log_timestamp )' ],
+ [ 'addPgIndex', 'logging', 'logging_actor_time', '( log_actor, log_timestamp )' ],
+ [ 'migrateActors' ],
+ [ 'modifyTable', 'site_stats', 'patch-site_stats-modify.sql' ],
+ [ 'populateArchiveRevId' ],
+ [ 'dropPgIndex', 'recentchanges', 'rc_namespace_title' ],
+ [
+ 'addPgIndex',
+ 'recentchanges',
+ 'rc_namespace_title_timestamp', '( rc_namespace, rc_title, rc_timestamp )'
+ ],
+ [ 'setSequenceOwner', 'mwuser', 'user_id', 'user_user_id_seq' ],
+ [ 'setSequenceOwner', 'actor', 'actor_id', 'actor_actor_id_seq' ],
+ [ 'setSequenceOwner', 'page', 'page_id', 'page_page_id_seq' ],
+ [ 'setSequenceOwner', 'revision', 'rev_id', 'revision_rev_id_seq' ],
+ [ 'setSequenceOwner', 'ip_changes', 'ipc_rev_id', 'ip_changes_ipc_rev_id_seq' ],
+ [ 'setSequenceOwner', 'pagecontent', 'old_id', 'text_old_id_seq' ],
+ [ 'setSequenceOwner', 'comment', 'comment_id', 'comment_comment_id_seq' ],
+ [ 'setSequenceOwner', 'page_restrictions', 'pr_id', 'page_restrictions_pr_id_seq' ],
+ [ 'setSequenceOwner', 'archive', 'ar_id', 'archive_ar_id_seq' ],
+ [ 'setSequenceOwner', 'content', 'content_id', 'content_content_id_seq' ],
+ [ 'setSequenceOwner', 'slot_roles', 'role_id', 'slot_roles_role_id_seq' ],
+ [ 'setSequenceOwner', 'content_models', 'model_id', 'content_models_model_id_seq' ],
+ [ 'setSequenceOwner', 'externallinks', 'el_id', 'externallinks_el_id_seq' ],
+ [ 'setSequenceOwner', 'ipblocks', 'ipb_id', 'ipblocks_ipb_id_seq' ],
+ [ 'setSequenceOwner', 'filearchive', 'fa_id', 'filearchive_fa_id_seq' ],
+ [ 'setSequenceOwner', 'uploadstash', 'us_id', 'uploadstash_us_id_seq' ],
+ [ 'setSequenceOwner', 'recentchanges', 'rc_id', 'recentchanges_rc_id_seq' ],
+ [ 'setSequenceOwner', 'watchlist', 'wl_id', 'watchlist_wl_id_seq' ],
+ [ 'setSequenceOwner', 'logging', 'log_id', 'logging_log_id_seq' ],
+ [ 'setSequenceOwner', 'job', 'job_id', 'job_job_id_seq' ],
+ [ 'setSequenceOwner', 'category', 'cat_id', 'category_cat_id_seq' ],
+ [ 'setSequenceOwner', 'change_tag', 'ct_id', 'change_tag_ct_id_seq' ],
+ [ 'setSequenceOwner', 'tag_summary', 'ts_id', 'tag_summary_ts_id_seq' ],
+ [ 'setSequenceOwner', 'sites', 'site_id', 'sites_site_id_seq' ],
];
}
@@ -650,13 +736,22 @@ END;
protected function addSequence( $table, $pkey, $ns ) {
if ( !$this->db->sequenceExists( $ns ) ) {
$this->output( "Creating sequence $ns\n" );
- $this->db->query( "CREATE SEQUENCE $ns" );
if ( $pkey !== false ) {
+ $this->db->query( "CREATE SEQUENCE $ns OWNED BY $table.$pkey" );
$this->setDefault( $table, $pkey, '"nextval"(\'"' . $ns . '"\'::"regclass")' );
+ } else {
+ $this->db->query( "CREATE SEQUENCE $ns" );
}
}
}
+ protected function dropSequence( $table, $ns ) {
+ if ( $this->db->sequenceExists( $ns ) ) {
+ $this->output( "Dropping sequence $ns\n" );
+ $this->db->query( "DROP SEQUENCE $ns CASCADE" );
+ }
+ }
+
protected function renameSequence( $old, $new ) {
if ( $this->db->sequenceExists( $new ) ) {
$this->output( "...sequence $new already exists.\n" );
@@ -669,6 +764,13 @@ END;
}
}
+ protected function setSequenceOwner( $table, $pkey, $seq ) {
+ if ( $this->db->sequenceExists( $seq ) ) {
+ $this->output( "Setting sequence $seq owner to $table.$pkey\n" );
+ $this->db->query( "ALTER SEQUENCE $seq OWNED BY $table.$pkey" );
+ }
+ }
+
protected function renameTable( $old, $new, $patch = false ) {
if ( $this->db->tableExists( $old ) ) {
$this->output( "Renaming table $old to $new\n" );
@@ -715,6 +817,18 @@ END;
$this->db->query( "ALTER INDEX $old RENAME TO $new" );
}
+ protected function dropPgField( $table, $field ) {
+ $fi = $this->db->fieldInfo( $table, $field );
+ if ( is_null( $fi ) ) {
+ $this->output( "...$table table does not contain $field field.\n" );
+
+ return;
+ } else {
+ $this->output( "Dropping column '$table.$field'\n" );
+ $this->db->query( "ALTER TABLE $table DROP COLUMN $field" );
+ }
+ }
+
protected function addPgField( $table, $field, $type ) {
$fi = $this->db->fieldInfo( $table, $field );
if ( !is_null( $fi ) ) {
@@ -991,7 +1105,7 @@ END;
}
}
- protected function dropIndex( $table, $index, $patch = '', $fullpath = false ) {
+ protected function dropPgIndex( $table, $index ) {
if ( $this->db->indexExists( $table, $index ) ) {
$this->output( "Dropping obsolete index '$index'\n" );
$this->db->query( "DROP INDEX \"" . $index . "\"" );
diff --git a/www/wiki/includes/installer/SqliteInstaller.php b/www/wiki/includes/installer/SqliteInstaller.php
index d5909f4e..6f168720 100644
--- a/www/wiki/includes/installer/SqliteInstaller.php
+++ b/www/wiki/includes/installer/SqliteInstaller.php
@@ -160,9 +160,9 @@ class SqliteInstaller extends DatabaseInstaller {
# Called early on in the installer, later we just want to sanity check
# if it's still writable
if ( $create ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ok = wfMkdirParents( $dir, 0700, __METHOD__ );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$ok ) {
return Status::newFatal( 'config-sqlite-mkdir-error', $dir );
}
@@ -321,7 +321,7 @@ EOT;
return "# SQLite-specific settings
\$wgSQLiteDataDir = \"{$dir}\";
\$wgObjectCaches[CACHE_DB] = [
- 'class' => 'SqlBagOStuff',
+ 'class' => SqlBagOStuff::class,
'loggroup' => 'SQLBagOStuff',
'server' => [
'type' => 'sqlite',
diff --git a/www/wiki/includes/installer/SqliteUpdater.php b/www/wiki/includes/installer/SqliteUpdater.php
index 9f710014..b107fd11 100644
--- a/www/wiki/includes/installer/SqliteUpdater.php
+++ b/www/wiki/includes/installer/SqliteUpdater.php
@@ -190,9 +190,28 @@ class SqliteUpdater extends DatabaseUpdater {
[ 'renameIndex', 'user_properties', 'user_properties_user_property', 'PRIMARY', false,
'patch-user_properties-fix-pk.sql' ],
[ 'addTable', 'comment', 'patch-comment-table.sql' ],
+
+ // This field was added in 1.31, but is put here so it can be used by 'migrateComments'
+ [ 'addField', 'image', 'img_description_id', 'patch-image-img_description_id.sql' ],
+
[ 'migrateComments' ],
[ 'renameIndex', 'l10n_cache', 'lc_lang_key', 'PRIMARY', false,
'patch-l10n_cache-primary-key.sql' ],
+
+ // 1.31
+ [ 'addTable', 'content', 'patch-content.sql' ],
+ [ 'addTable', 'content_models', 'patch-content_models.sql' ],
+ [ 'addTable', 'slots', 'patch-slots.sql' ],
+ [ 'addField', 'slots', 'slot_origin', 'patch-slot-origin.sql' ],
+ [ 'addTable', 'slot_roles', 'patch-slot_roles.sql' ],
+ [ 'migrateArchiveText' ],
+ [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+ [ 'migrateActors' ],
+ [ 'modifyField', 'revision', 'rev_text_id', 'patch-rev_text_id-default.sql' ],
+ [ 'modifyTable', 'site_stats', 'patch-site_stats-modify.sql' ],
+ [ 'populateArchiveRevId' ],
+ [ 'addIndex', 'recentchanges', 'rc_namespace_title_timestamp',
+ 'patch-recentchanges-nttindex.sql' ],
];
}
diff --git a/www/wiki/includes/installer/WebInstaller.php b/www/wiki/includes/installer/WebInstaller.php
index e0e54c84..8fb98079 100644
--- a/www/wiki/includes/installer/WebInstaller.php
+++ b/www/wiki/includes/installer/WebInstaller.php
@@ -155,6 +155,10 @@ class WebInstaller extends Installer {
if ( isset( $session['settings'] ) ) {
$this->settings = $session['settings'] + $this->settings;
+ // T187586 MediaWikiServices works with globals
+ foreach ( $this->settings as $key => $val ) {
+ $GLOBALS[$key] = $val;
+ }
}
$this->setupLanguage();
@@ -911,6 +915,7 @@ class WebInstaller extends Installer {
* Parameters are:
* var: The variable to be configured (required)
* label: The message name for the label (required)
+ * labelAttribs:Additional attributes for the label element (optional)
* attribs: Additional attributes for the input element (optional)
* controlName: The name for the input element (optional)
* value: The current value of the variable (optional)
@@ -933,6 +938,9 @@ class WebInstaller extends Installer {
if ( !isset( $params['help'] ) ) {
$params['help'] = "";
}
+ if ( !isset( $params['labelAttribs'] ) ) {
+ $params['labelAttribs'] = [];
+ }
if ( isset( $params['rawtext'] ) ) {
$labelText = $params['rawtext'];
} else {
@@ -941,17 +949,19 @@ class WebInstaller extends Installer {
return "<div class=\"config-input-check\">\n" .
$params['help'] .
- "<label>\n" .
- Xml::check(
- $params['controlName'],
- $params['value'],
- $params['attribs'] + [
- 'id' => $params['controlName'],
- 'tabindex' => $this->nextTabIndex(),
- ]
- ) .
- $labelText . "\n" .
- "</label>\n" .
+ Html::rawElement(
+ 'label',
+ $params['labelAttribs'],
+ Xml::check(
+ $params['controlName'],
+ $params['value'],
+ $params['attribs'] + [
+ 'id' => $params['controlName'],
+ 'tabindex' => $this->nextTabIndex(),
+ ]
+ ) .
+ $labelText . "\n"
+ ) .
"</div>\n";
}
diff --git a/www/wiki/includes/installer/WebInstallerName.php b/www/wiki/includes/installer/WebInstallerName.php
index 81a107de..50c88aec 100644
--- a/www/wiki/includes/installer/WebInstallerName.php
+++ b/www/wiki/includes/installer/WebInstallerName.php
@@ -76,7 +76,7 @@ class WebInstallerName extends WebInstallerPage {
$this->parent->getTextBox( [
'var' => 'wgMetaNamespace',
'label' => '', // @todo Needs a label?
- 'attribs' => [ 'readonly' => 'readonly', 'class' => 'enabledByOther' ]
+ 'attribs' => [ 'class' => 'enabledByOther' ]
] ) .
$this->getFieldsetStart( 'config-admin-box' ) .
$this->parent->getTextBox( [
diff --git a/www/wiki/includes/installer/WebInstallerOptions.php b/www/wiki/includes/installer/WebInstallerOptions.php
index 07378ab3..d798ea1e 100644
--- a/www/wiki/includes/installer/WebInstallerOptions.php
+++ b/www/wiki/includes/installer/WebInstallerOptions.php
@@ -25,6 +25,8 @@ class WebInstallerOptions extends WebInstallerPage {
* @return string|null
*/
public function execute() {
+ global $wgLang;
+
if ( $this->getVar( '_SkipOptional' ) == 'skip' ) {
$this->submitSkins();
return 'skip';
@@ -145,20 +147,93 @@ class WebInstallerOptions extends WebInstallerPage {
$this->addHTML( $skinHtml );
$extensions = $this->parent->findExtensions();
+ $dependencyMap = [];
if ( $extensions ) {
$extHtml = $this->getFieldsetStart( 'config-extensions' );
+ $extByType = [];
+ $types = SpecialVersion::getExtensionTypes();
+ // Sort by type first
foreach ( $extensions as $ext => $info ) {
- $extHtml .= $this->parent->getCheckBox( [
- 'var' => "ext-$ext",
- 'rawtext' => $ext,
- ] );
+ if ( !isset( $info['type'] ) || !isset( $types[$info['type']] ) ) {
+ // We let extensions normally define custom types, but
+ // since we aren't loading extensions, we'll have to
+ // categorize them under other
+ $info['type'] = 'other';
+ }
+ $extByType[$info['type']][$ext] = $info;
+ }
+
+ foreach ( $types as $type => $message ) {
+ if ( !isset( $extByType[$type] ) ) {
+ continue;
+ }
+ $extHtml .= Html::element( 'h2', [], $message );
+ foreach ( $extByType[$type] as $ext => $info ) {
+ $urlText = '';
+ if ( isset( $info['url'] ) ) {
+ $urlText = ' ' . Html::element( 'a', [ 'href' => $info['url'] ], '(more information)' );
+ }
+ $attribs = [
+ 'data-name' => $ext,
+ 'class' => 'config-ext-input'
+ ];
+ $labelAttribs = [];
+ $fullDepList = [];
+ if ( isset( $info['requires']['extensions'] ) ) {
+ $dependencyMap[$ext]['extensions'] = $info['requires']['extensions'];
+ $labelAttribs['class'] = 'mw-ext-with-dependencies';
+ }
+ if ( isset( $info['requires']['skins'] ) ) {
+ $dependencyMap[$ext]['skins'] = $info['requires']['skins'];
+ $labelAttribs['class'] = 'mw-ext-with-dependencies';
+ }
+ if ( isset( $dependencyMap[$ext] ) ) {
+ $links = [];
+ // For each dependency, link to the checkbox for each
+ // extension/skin that is required
+ if ( isset( $dependencyMap[$ext]['extensions'] ) ) {
+ foreach ( $dependencyMap[$ext]['extensions'] as $name ) {
+ $links[] = Html::element(
+ 'a',
+ [ 'href' => "#config_ext-$name" ],
+ $name
+ );
+ }
+ }
+ if ( isset( $dependencyMap[$ext]['skins'] ) ) {
+ foreach ( $dependencyMap[$ext]['skins'] as $name ) {
+ $links[] = Html::element(
+ 'a',
+ [ 'href' => "#config_skin-$name" ],
+ $name
+ );
+ }
+ }
+
+ $text = wfMessage( 'config-extensions-requires' )
+ ->rawParams( $ext, $wgLang->commaList( $links ) )
+ ->escaped();
+ } else {
+ $text = $ext;
+ }
+ $extHtml .= $this->parent->getCheckBox( [
+ 'var' => "ext-$ext",
+ 'rawtext' => $text,
+ 'attribs' => $attribs,
+ 'labelAttribs' => $labelAttribs,
+ ] );
+ }
}
$extHtml .= $this->parent->getHelpBox( 'config-extensions-help' ) .
$this->getFieldsetEnd();
$this->addHTML( $extHtml );
+ // Push the dependency map to the client side
+ $this->addHTML( Html::inlineScript(
+ 'var extDependencyMap = ' . Xml::encodeJsVar( $dependencyMap )
+ ) );
}
// Having / in paths in Windows looks funny :)
@@ -259,7 +334,7 @@ class WebInstallerOptions extends WebInstallerPage {
foreach ( $screenshots as $shot ) {
$links[] = Html::element(
'a',
- [ 'href' => $shot ],
+ [ 'href' => $shot, 'target' => '_blank' ],
$wgLang->formatNum( $counter++ )
);
}
@@ -269,7 +344,7 @@ class WebInstallerOptions extends WebInstallerPage {
} else {
$link = Html::element(
'a',
- [ 'href' => $screenshots[0] ],
+ [ 'href' => $screenshots[0], 'target' => '_blank' ],
wfMessage( 'config-screenshot' )->text()
);
return wfMessage( 'config-skins-screenshot', $name )->rawParams( $link )->escaped();
diff --git a/www/wiki/includes/installer/WebInstallerOutput.php b/www/wiki/includes/installer/WebInstallerOutput.php
index e4eb255b..6a55d696 100644
--- a/www/wiki/includes/installer/WebInstallerOutput.php
+++ b/www/wiki/includes/installer/WebInstallerOutput.php
@@ -233,7 +233,7 @@ class WebInstallerOutput {
public function getHeadAttribs() {
return [
'dir' => $this->getDir(),
- 'lang' => wfBCP47( $this->getLanguageCode() ),
+ 'lang' => LanguageCode::bcp47( $this->getLanguageCode() ),
];
}
diff --git a/www/wiki/includes/installer/i18n/af.json b/www/wiki/includes/installer/i18n/af.json
index 6b337931..58c6b72c 100644
--- a/www/wiki/includes/installer/i18n/af.json
+++ b/www/wiki/includes/installer/i18n/af.json
@@ -41,7 +41,6 @@
"config-no-db": "Kon nie 'n geskikte databasisdrywer vind nie!",
"config-memory-raised": "PHP se <code>memory_limit</code> is $1, en is verhoog tot $2.",
"config-memory-bad": "'''Waarskuwing:''' PHP se <code>memory_limit</code> is $1.\nDit is waarskynlik te laag.\nDie installasie mag moontlik faal!",
- "config-xcache": "[Http://trac.lighttpd.net/xcache/ XCache] is geïnstalleer",
"config-apc": "[Http://www.php.net/apc APC] is geïnstalleer",
"config-wincache": "[Http://www.iis.net/download/WinCacheForPhp WinCache] is geïnstalleer",
"config-diff3-bad": "GNU diff3 nie gevind nie.",
diff --git a/www/wiki/includes/installer/i18n/ar.json b/www/wiki/includes/installer/i18n/ar.json
index 69d1fcf8..9d7d0af5 100644
--- a/www/wiki/includes/installer/i18n/ar.json
+++ b/www/wiki/includes/installer/i18n/ar.json
@@ -51,17 +51,16 @@
"config-help-restart": "هل تريد إزالة البيانات المحفوظة التي قد قمت بإدخالها وإعادة تشغيل عملية التثبيت؟",
"config-restart": "نعم، إعادة التشغيل",
"config-welcome": "=== التحقق من البيئة ===\nسوف يتم الآن التحقق من أن البيئة مناسبة لتنصيب ميديا ويكي.\nتذكر تضمين هذه المعلومات اذا اردت طلب المساعدة عن كيفية إكمال التنصيب.",
- "config-copyright": "=== حقوق النسخ والشروط ===\n\n$1\n\nهذا البرنامج هو برنامج حر؛ يمكنك إعادة توزيعه و/أو تعديله تحت شروط رخصة جنو العامة على أن هذا البرنامج قد نُشر من قِبل مؤسسة البرمجيات الحرة؛ إما النسخة 2 من الرخصة، أو أي نسخة أخرى بعدها (من إختيارك)\n\nتم توزيع هذا البرنامج على أمل ان يكون مفيدًا ولكن <strong> دون أية ضمانات</strong>؛ دون حتى أية ضمانات مفهومة ضمنيًا أو رواجات أو أية أسباب محددة.\nأنظر رخصة جنو العامة لمزيد من المعلومات.\n\nمن المفترض أنك إستملت <doclink href=Copying> نسخة عن رخصة جنو العامة </doclink> مع هذا البرنامج؛ اذا لم تقعل إكتب رسالة إلى مؤسسة البرمجيات الحرة المحدودة، شارع 51 فرانكلين الطابق الخامس، بوسطن MA 02110-1301 الولايات المتخدة أو [http://www.gnu.org/copyleft/gpl.html read it online].",
+ "config-copyright": "=== حقوق النسخ والشروط ===\n\n$1\n\nهذا البرنامج هو برنامج حر؛ يمكنك إعادة توزيعه و/أو تعديله تحت شروط رخصة جنو العامة على أن هذا البرنامج قد نُشر من قِبل مؤسسة البرمجيات الحرة؛ إما النسخة 2 من الرخصة، أو أي نسخة أخرى بعدها (من إختيارك)\n\nتم توزيع هذا البرنامج على أمل ان يكون مفيدًا ولكن <strong> دون أية ضمانات</strong>؛ دون حتى أية ضمانات مفهومة ضمنيًا أو رواجات أو أية أسباب محددة.\nأنظر رخصة جنو العامة لمزيد من المعلومات.\n\nمن المفترض أنك إستملت <doclink href=Copying> نسخة عن رخصة جنو العامة </doclink> مع هذا البرنامج؛ اذا لم تقعل إكتب رسالة إلى مؤسسة البرمجيات الحرة المحدودة، شارع 51 فرانكلين الطابق الخامس، بوسطن MA 02110-1301 الولايات المتخدة أو [https://www.gnu.org/copyleft/gpl.html read it online].",
"config-sidebar": "* [https://www.mediawiki.org موقع ميدياويكي]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents دليل المستخدم]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents دليل الإداري]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ الأسئلة المتكررة]\n----\n* <doclink href=Readme>إقراءني</doclink>\n* <doclink href=ReleaseNotes>ملاحظات الإصدار</doclink>\n* <doclink href=Copying>النسخ</doclink>\n* <doclink href=UpgradeDoc>الترقية</doclink>",
"config-env-good": "جرى التحقق من البيئة. يمكنك تنصيب ميدياويكي.",
"config-env-bad": "جرى التحقق من البيئة. لا يمكنك تنصيب ميدياويكي.",
"config-env-php": "بي إتش بي $1 مثبت.",
"config-env-hhvm": "نصبت HHVM $1.",
"config-outdated-sqlite": "<strong>تحذير:</strong> لديك SQLite $1, which وهو أقل من الحد الأدنى المطلوب للنسخة $2. SQLite سوف يكون غير متوفر.",
- "config-xcache": "تثبيت [http://xcache.lighttpd.net/ XCache]",
"config-apc": "تثبيت [http://www.php.net/apc APC]",
"config-apcu": "تثبيت [http://www.php.net/apcu APCu]",
- "config-wincache": "تثبيت [http://www.iis.net/download/WinCacheForPhp WinCache]",
+ "config-wincache": "تثبيت [https://www.iis.net/download/WinCacheForPhp WinCache]",
"config-diff3-bad": "جنو diff3 غير موجود.",
"config-imagemagick": "تم العثور على ImageMagick: <code>$1</code>.\nسيتم تمكين تصغير الصور إذا قمت بتمكين التحميل.",
"config-no-scaling": "لا يمكن أن تجد مكتبة GD أو ImageMagick; سيتم تعطيل تصغير الصور.",
@@ -250,6 +249,7 @@
"config-help-tooltip": "اضغط للتوسيع",
"config-nofile": "لا يمكن العثور على الملف \"$1\". هل حُذف؟",
"config-extension-link": "هل كنت تعلم أن الويكي الخاصة بك تدعم [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions الامتدادات]؟\n\nيمكنك تصفح [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category الامتدادات حسب التصنيف] أو [https://www.mediawiki.org/wiki/Extension_Matrix مصفوفة الامتدادت] لترى القائمة الكاملة للامتدادات.",
+ "config-screenshot": "لقطة شاشة",
"mainpagetext": "<strong>تم تثبيت ميدياويكي بنجاح.</strong>",
"mainpagedocfooter": "استشر [https://meta.wikimedia.org/wiki/Help:Contents دليل المستخدم] لمعلومات حول استخدام برنامج الويكي.\n\n== البداية ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings قائمة إعدادات الضبط]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ أسئلة متكررة حول ميدياويكي]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce القائمة البريدية الخاصة بإصدار ميدياويكي]"
}
diff --git a/www/wiki/includes/installer/i18n/ast.json b/www/wiki/includes/installer/i18n/ast.json
index 1b2831fa..c4491109 100644
--- a/www/wiki/includes/installer/i18n/ast.json
+++ b/www/wiki/includes/installer/i18n/ast.json
@@ -44,14 +44,14 @@
"config-help-restart": "¿Quier llimpiar tolos datos guardaos qu'escribió y reaniciar el procesu d'instalación?",
"config-restart": "Sí, reanicialu",
"config-welcome": "=== Comprobaciones del entornu ===\nAgora van facese unes comprobaciones básiques para ver si l'entornu ye afayadizu pa la instalación de MediaWiki.\nAlcuérdese d'incluir esta información si necesita encontu pa completar la instalación.",
- "config-copyright": "=== Drechos d'autor y condiciones d'usu ===\n\n$1\n\nEsti programa ye software llibre; pue redistribuilu y/o camudalu baxo les condiciones de la llicencia pública xeneral GNU tal como la publica la Free Software Foundation; versión 2 o (como prefiera) cualquier versión posterior.\n\nEsti programa distribúise cola esperanza de que pueda ser útil, pero '''ensin garantía denguna'''; nin siquiera la garantía implícita de '''comercialidá''' o '''adautación a un fin determináu'''.\nVea la Llicencia pública xeneral GNU pa más detalles.\n\nHabría de tener recibío <doclink href=Copying>una copia de la llicencia pública xeneral GNU</doclink> xunto con esti programa; sinón, escriba a la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, o [http://www.gnu.org/copyleft/gpl.html lléala en llinia].",
+ "config-copyright": "=== Drechos d'autor y condiciones d'usu ===\n\n$1\n\nEsti programa ye software llibre; puedes redistribuilu y/o camudalu baxo les condiciones de la llicencia pública xeneral GNU tal como la publica la Free Software Foundation; versión 2 o (como prefieras) cualquier versión posterior.\n\nEsti programa distribúese cola esperanza de que pueda ser útil, pero <strong>ensin garantía denguna</strong>; nin siquiera la garantía implícita de <strong>comercialidá</strong> o \n<strong>adautación a un fin determináu</strong>.\nVer la Llicencia pública xeneral GNU pa más detalles.\n\nHabríes de tener recibío <doclink href=Copying>una copia de la llicencia pública xeneral GNU</doclink> xunto con esti programa; sinón, escribi a la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, o [https://www.gnu.org/copyleft/gpl.html lléila en llinia].",
"config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/gl Páxina principal de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guía del usuariu]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guía del alministrador]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Entrugues frecuentes]\n----\n* <doclink href=Readme>Lléame</doclink>\n* <doclink href=ReleaseNotes>Notes de llanzamientu</doclink>\n* <doclink href=Copying>Copia</doclink>\n* <doclink href=UpgradeDoc>Anovamientu</doclink>",
"config-env-good": "Comprobóse l'entornu.\nPue instalar MediaWiki.",
"config-env-bad": "Comprobóse l'entornu.\nNun pue instalar MediaWiki.",
"config-env-php": "PHP $1 ta instaláu.",
"config-env-hhvm": "HHVM $1 ta instaláu.",
- "config-unicode-using-intl": "Usando la [http://pecl.php.net/intl estensión intl PECL] pa la normalización Unicode.",
- "config-unicode-pure-php-warning": "'''Avisu:''' La [http://pecl.php.net/intl estensión intl PECL] nun ta disponible pa xestionar la normalización Unicode; volviendo a la implementación lenta en PHP puru.\nSi xestiona un sitiu con un tráficu altu, tendría de lleer una migaya sobro la [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalización Unicode].",
+ "config-unicode-using-intl": "Usando la [https://pecl.php.net/intl estensión intl PECL] pa la normalización Unicode.",
+ "config-unicode-pure-php-warning": "'''Avisu:''' La [https://pecl.php.net/intl estensión intl PECL] nun ta disponible pa xestionar la normalización Unicode; volviendo a la implementación lenta en PHP puru.\nSi xestiona un sitiu con un tráficu altu, tendría de lleer una migaya sobro la [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalización Unicode].",
"config-unicode-update-warning": "'''Avisu:''' La versión instalada del envoltoriu de normalización Unicode usa una versión antigua de la biblioteca [http://site.icu-project.org/ de los proyeutos ICU].\nTendría [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations d'anovala] si ye importante pa vusté usar Unicode.",
"config-no-db": "¡Nun pudo alcontrase un controlador de base de datos afayadizu! Necesites instalar un controlador de base de datos pa PHP.\n{{PLURAL:$2|Tien sofitu el tipu de base de datos siguiente|Tienen sofitu los tipos de base de datos siguientes}}: $1.\n\nSi compilasti PHP tu mesmu, reconfigúralu con un cliente de base de datos activáu, por exemplu, usando <code>./configure --with-mysqli</code>.\nSi instalasti PHP dende un paquete de Debian o Ubuntu, necesites instalar tamién,por exemplu, el paquete <code>php5-mysql</code>.",
"config-outdated-sqlite": "'''Avisu:''' tien SQLite $1, que ye inferior a la versión mínima necesaria $2. SQLite nun tará disponible.",
@@ -60,12 +60,11 @@
"config-pcre-no-utf8": "<strong>Erru fatal:</strong> Paez que'l módulu PCRE de PHP foi compiláu ensin el soporte PCRE_UTF8.\nMediaWiki requier compatibilidá con UTF_8 pa furrular correutamente.",
"config-memory-raised": "El parámetru <code>memory_limit</code> de PHP ye $1. Auméntase a $2.",
"config-memory-bad": "<strong>Alvertencia:</strong>: el parámetru <code>memory_limit</code> de PHP ye $1.\nProbablemente sía demasiáu baxu.\n¡La instalación puede fallar!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] ta instaláu",
"config-apc": "[http://www.php.net/apc APC] ta instaláu",
"config-apcu": "[http://www.php.net/apcu APCu] ta instaláu",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] ta instaláu",
- "config-no-cache-apcu": "<strong>Warning:</strong> Non pudo atopase[http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] o [http://www.iis.net/download/WinCacheForPhp WinCache].\nEl caxé d'oxetos nun ta activáu.",
- "config-mod-security": "<strong>Alvertencia:</strong> El to servidor web tien activáu [http://modsecurity.org/mod_security]/mod_security2 .Munches de les sos configuraciones comunes pueden causar problemes a MediaWiki o otru software que dexe a los usuarios publicar conteníu arbitrario. De ser posible, tendríes de desactivalo. Si non, consulta la [http://modsecurity.org/documentation/ mod_security documentation] o contacta col alministrador del to servidor si atopes erros aleatorios.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] ta instaláu",
+ "config-no-cache-apcu": "<strong>Atención:</strong> Nun pudo alcontrase [http://www.php.net/apcu APCu] o [http://www.iis.net/download/WinCacheForPhp WinCache].\nLa caché d'oxetos nun ta activada.",
+ "config-mod-security": "<strong>Alvertencia:</strong> El to servidor web tien activáu [https://modsecurity.org/mod_security]/mod_security2 .Munches de les sos configuraciones comunes pueden causar problemes a MediaWiki o otru software que dexe a los usuarios publicar conteníu arbitrario. De ser posible, tendríes de desactivalo. Si non, consulta la [https://modsecurity.org/documentation/ mod_security documentation] o contacta col alministrador del to servidor si atopes erros aleatorios.",
"config-diff3-bad": "Nun s'alcontró GNU diff3.",
"config-git": "Alcontróse'l software de control de versiones Git: <code>$1</code>.",
"config-git-bad": "Nun s'alcontró el software de control de versiones Git.",
@@ -80,6 +79,7 @@
"config-no-cli-uploads-check": "<strong>Alvertencia:</strong> el to directoriu predetermináu pa cargues <code>$1</code> nun tá comprobáu contra la vulnerabilidá d'execución arbitraria de scripts mientres la instalación per llínea de comandos.",
"config-brokenlibxml": "El sistema tien una combinación de versiones de PHP y de libxml2 que ye pocu confiable y puede provocar corrupción oculta nos datos de MediaWiki y otres aplicaciones web. Actualiza a libxml2 2.7.3 o posterior ([https://bugs.php.net/bug.php?díi=45996 bug reportáu con PHP]). Instalación albortada.",
"config-suhosin-max-value-length": "Suhosin ta instaláu y llinda el parámetru <code>length</code> GET a $1 bytes.\nEl componente ResourceLoader (xestor de recursos) de MediaWiki va trabayar nesta llende, pero eso va perxudicar el rendimientu.\nSi ye posible, tendríes d'establecer <code>suhosin.get.max_value_length</code> nel valor 1024 o superior en <code>php.ini</code> y establecer <code>$wgResourceLoaderMaxQueryLength</code> nel mesmu valor en <code>LocalSettings.php</code>.",
+ "config-using-32bit": "<strong>Atención:</strong> paez que'l sistema funciona con enteros de 32 bits. Esto ta [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit desaconseyáu].",
"config-db-type": "Tipu de base de datos:",
"config-db-host": "Servidor de la base de datos:",
"config-db-host-help": "Si'l to servidor de base de datos ta n'otru servidor, escribe'l nome del equipu o la so dirección IP equí.\n\nSi tas utilizando alojamiento web compartíu, el to provisor tendría de date'l nome correctu del servidor na so documentación.\n\nSi vas instalar nun servidor Windows y a utilizar MySQL, l'usu de \"localhost\" como nome del servidor puede nun #funcionar. Si ye asina, intenta poner \"127.0.0.1\" como dirección IP local.\n\nSi utilices PostgreSQL, dexa esti campu vacío pa conectase al traviés d'un socket de Unix.",
@@ -107,11 +107,13 @@
"config-db-schema-help": "Esti esquema de vezu va tar bien.\nCamúdalos solo si sabes que lo precises.",
"config-pg-test-error": "Nun puede coneutase cola base de datos <strong>$1</strong>: $2",
"config-sqlite-dir": "Direutoriu de datos SQLite:",
+ "config-sqlite-dir-help": "SQLite almacena tolos datos nun ficheru únicu.\n\nEl direutoriu que proporciones tien de poder escribise pol servidor web mientres la instalación.\n\n<strong>Nun</strong> tendría de tener accesu pela web, por eso nun se pon nel sitiu onde tán los ficheros PHP.\n\nL'instalador escribirá un ficheru <code>.htaccess</code> xunto con él, pero si esto falla dalguién podría tener accesu a la base de datos completa.\nEso incluye los datos d'usuariu completos (direcciones de corréu electrónicu, contraseñes con hash) lo mesmo que les revisiones desaniciaes y otros datos acutaos de la wiki.\n\nConsidera poner la base de datos en dalgún otru sitiu, por casu en <code>/var/lib/mediawiki/miowiki</code>.",
"config-oracle-def-ts": "Espaciu de tables predetermináu:",
"config-oracle-temp-ts": "Espaciu de tables temporal:",
"config-type-mysql": "MySQL (o compatible)",
"config-type-mssql": "Microsoft SQL Server",
"config-support-info": "MediaWiki ye compatible colos siguientes sistemes de bases de datos:\n\n$1\n\nSi nun atopes na llista el sistema de base de datos que tas intentando utilizar, sigue les instrucciones enllazaes enriba p'activar la compatibilidá.",
+ "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] ye un sistema comercial de base de datos empresariales pa Windows. ([http://www.php.net/manual/en/sqlsrv.installation.php Cómo compilar PHP con compatibilidá pa SQLSRV])",
"config-header-mysql": "Configuración de MySQL",
"config-header-postgres": "Configuración de PostgreSQL",
"config-header-sqlite": "Configuración de SQLite",
@@ -131,8 +133,26 @@
"config-postgres-old": "Ríquese PostgreSQL $1 o posterior. Tienes la versión $2.",
"config-mssql-old": "Ríquese Microsoft SQL Server $1 o posterior. Tienes la versión $2.",
"config-sqlite-name-help": "Escueye'l nome qu'identifica la to wiki.\nNun uses espacios o guiones.\nEsti va usase como nome del ficheru de datos pa SQLite.",
+ "config-sqlite-parent-unwritable-group": "Nun puede crease el direutoriu de datos <code><nowiki>$1</nowiki></code> porque'l servidor web nun tien permisu d'escritura nel direutoriu padre <code><nowiki>$2</nowiki></code>.\n\nL'instalador determinó l'usuariu col que s'executa'l sirvidor web.\nDa-y permisos d'escritura nel direutoriu <code><nowiki>$3</nowiki></code> pa siguir.\nNun sistema Unix/Linux fai:\n\n<pre>cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3</pre>",
+ "config-sqlite-parent-unwritable-nogroup": "Nun puede crease'l direutoriu de datos <code><nowiki>$1</nowiki></code>, porque'l sirvidor web nun tien permisu d'escritura nel direutoriu padre <code><nowiki>$2</nowiki></code>.\n\nL'instalador nun pudo determinar l'usuariu col que s'executa'l sirvidor web.\nDa permisos d'escritura universal pa él (¡y pa otros!) nel direutoriu <code><nowiki>$3</nowiki></code> pa siguir.\nNun sistema Unix/Linux fai:\n\n<pre>cd $2\nmkdir $3\nchmod a+w $3</pre>",
+ "config-sqlite-mkdir-error": "Error al crear el direutoriu de datos «$1».\nComprueba la direición y tenta otra vuelta.",
+ "config-sqlite-dir-unwritable": "Nun puede escribise nel direutoriu «$1».\nCambia los sos permisos pa que'l sirvidor web pueda escribir nél, y tenta otra vuelta.",
+ "config-sqlite-connection-error": "$1.\n\nComprueba más abaxo'l direutoriu de datos ya'l nome de la base de datos y tenta otra vuelta.",
+ "config-sqlite-readonly": "El ficheru <code>$1</code> nun puede escribise.",
+ "config-sqlite-cant-create-db": "Nun pudo crease'l ficheru de la base de datos <code>$1</code>.",
+ "config-sqlite-fts3-downgrade": "PHP nun tien compatibilidá pa FTS3, baxando a una versión anterior les tables.",
+ "config-regenerate": "Rexenerar LocalSettings.php →",
+ "config-show-table-status": "¡Falló la consulta <code>SHOW TABLE STATUS</code>!",
+ "config-unknown-collation": "<strong>Avisu:</strong> La base de datos utiliza un orde alfabéticu ensin reconocer.",
+ "config-db-web-account": "Cuenta de la base de datos pal accesu web",
+ "config-db-web-help": "Escueye l'usuariu y contraseña que'l sirvidor web usará pa coneutase col sirvidor de la base de datos nel funcionamientu normal de la wiki.",
+ "config-db-web-account-same": "Utilizar la mesma cuenta que pa la instalación",
+ "config-db-web-create": "Crear la cuenta si nun esiste yá",
+ "config-db-web-no-create-privs": "La cuenta qu'especificasti pa la instalación nun tien permisos abondo pa crear una cuenta.\nLa cuenta qu'especifiques equí yá tien d'esistir.",
+ "config-mysql-engine": "Motor d'almacenamientu:",
"config-mysql-innodb": "InnoDB",
"config-mysql-myisam": "MyISAM",
+ "config-mysql-binary": "Binariu",
"config-mysql-utf8": "UTF-8",
"config-mssql-auth": "Triba d'autenticación:",
"config-site-name": "Nome de la wiki:",
@@ -147,10 +167,39 @@
"config-admin-password": "Contraseña:",
"config-optional-skip": "Yá toi aburríu, namái instala la wiki.",
"config-profile-private": "Wiki privada",
+ "config-license": "Derechos d'autor y llicencia:",
+ "config-license-none": "Ensin pie de llicencia",
+ "config-license-cc-by-sa": "Creative Commons Reconocimientu-CompartirIgual",
+ "config-license-cc-by": "Creative Commons Reconocimientu",
+ "config-license-cc-by-nc-sa": "Creative Commons Reconocimientu-NonComercial-CompartirIgual",
+ "config-license-cc-0": "Creative Commons Zero (Dominiu públicu)",
+ "config-license-gfdl": "Llicencia de documentación llibre de GNU 1.3 o posterior",
+ "config-license-pd": "Dominiu públicu",
+ "config-license-cc-choose": "Escoyer una llicencia Creative Commons personalizada",
+ "config-email-settings": "Configuración de corréu electrónicu",
+ "config-enable-email": "Activar el corréu electrónicu de salida",
+ "config-enable-email-help": "Si quies que'l corréu electrónicu funcione, les [http://www.php.net/manual/en/mail.configuration.php preferencies de corréu de PHP] tienen de tar configuraes correutamente.\nSi nun quies les funciones de corréu electrónicu, puedes desactivales equí.",
+ "config-email-user": "Activar el corréu electrónicu ente usuarios",
+ "config-logo": "URL del logo:",
+ "config-instantcommons": "Activar Instant Commons",
"config-extensions": "Estensiones",
+ "config-skins": "Apariencies",
+ "config-skins-help": "Deteutáronse les apariencies de la llista anterior nel direutoriu <code>./skins</code>. Tienes d'activar siquier una, y escoyer la predeterminada.",
+ "config-skins-use-as-default": "Utilizar esta apariencia como predeterminada",
+ "config-skins-missing": "Nun s'atopó nenguna apariencia; MediaWiki utilizará una apariencia de respaldu hasta qu'instales delles apariencies afayadices.",
+ "config-skins-must-enable-some": "Tienes d'escoyer polo meno una apariencia p'activar.",
+ "config-skins-must-enable-default": "L'apariencia escoyida como predeterminada tien de tar activada.",
+ "config-install-step-done": "fecho",
+ "config-install-step-failed": "falló",
+ "config-install-extensions": "Incluyendo estensiones",
+ "config-install-database": "Configurando la base de datos",
+ "config-install-schema": "Creando l'esquema",
+ "config-install-pg-schema-not-exist": "L'esquema PostgreSQL nun esiste.",
"config-download-localsettings": "Descargar <code>LocalSettings.php</code>",
"config-help": "Ayuda",
"config-nofile": "Nun pudo atopase'l ficheru \"$1\". ¿Desaniciose?",
+ "config-skins-screenshots": "$1 (imaxes de pantalla: $2)",
+ "config-screenshot": "imaxe de pantalla",
"mainpagetext": "<strong>Instalóse MediaWiki.</strong>",
"mainpagedocfooter": "Consulta la [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guía del usuariu] pa saber cómo usar el software wiki.\n\n== Primeros pasos ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Llista de les opciones de configuración]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ EMF de MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Llista de corréu de llanzamientos de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Llocaliza MediaWiki na to llingua]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Depriende como combatir la puxarra na to wiki]"
}
diff --git a/www/wiki/includes/installer/i18n/ba.json b/www/wiki/includes/installer/i18n/ba.json
index 909a1a2e..a9c2c7f7 100644
--- a/www/wiki/includes/installer/i18n/ba.json
+++ b/www/wiki/includes/installer/i18n/ba.json
@@ -51,14 +51,14 @@
"config-help-restart": "Һеҙ үҙегеҙ индергән һәм һаҡланған әлеге мәғлүмәттәрҙе юйып, урынлаштырыуҙың яңы процессын ебәрергә теләйһегеҙме?",
"config-restart": "Эйе, яңынан башларға",
"config-welcome": "=== Даирәне тикшереү ===",
- "config-copyright": "=== Авторлыҡ хоҡуҡтары һәм шарттар ===\n\n$1\n\nMediaWiki - ирекле программа тьәминәте. Һеҙ уны ирекле программа тьәминәте фонды баҫып сығарған GNU General Public License лицензия талаптарына ярашлы рәүештә тарата һәм/йәки үҙгәртә алаһығыҙ;икенсе версияһына йәки ниндәйҙә булһа һуңғы версияһына ярашлы рәүештә.\nMediaWiki - файҙалы булыу өмөтө менән таратыла, ләкин <strong> бер ниндәй ҙә гарантияларһыҙ</strong>, хатта күҙ уңында тотолған гарантияларһыҙ <strong> коммерция ҡимәтенән тыш </strong> йәки </strong> ниндәй ҙә булһа маҡсатҡа яраҡһыҙ </strong>. Ҡара. тулыраҡ мәғлүмәт алыу өсөн GNU General Public License лицезияһы. \nҺеҙ <doclink href=Copying> копияһын GNU General Public License</doclink>ошо программа менән бергә алырға тейеш инегеҙ, әгәр алмаһағыҙ, Free Software Foundation, Inc. ошо адрес буйынса яҙығыҙ:51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA йәки [http://www.gnu.org/copyleft/gpl.html уны онлайнда уҡығыҙ].",
+ "config-copyright": "=== Авторлыҡ хоҡуҡтары һәм шарттар ===\n\n$1\n\nMediaWiki - ирекле программа тьәминәте. Һеҙ уны ирекле программа тьәминәте фонды баҫып сығарған GNU General Public License лицензия талаптарына ярашлы рәүештә тарата һәм/йәки үҙгәртә алаһығыҙ;икенсе версияһына йәки ниндәйҙә булһа һуңғы версияһына ярашлы рәүештә.\nMediaWiki - файҙалы булыу өмөтө менән таратыла, ләкин <strong> бер ниндәй ҙә гарантияларһыҙ</strong>, хатта күҙ уңында тотолған гарантияларһыҙ <strong> коммерция ҡимәтенән тыш </strong> йәки </strong> ниндәй ҙә булһа маҡсатҡа яраҡһыҙ </strong>. Ҡара. тулыраҡ мәғлүмәт алыу өсөн GNU General Public License лицезияһы. \nҺеҙ <doclink href=Copying> копияһын GNU General Public License</doclink>ошо программа менән бергә алырға тейеш инегеҙ, әгәр алмаһағыҙ, Free Software Foundation, Inc. ошо адрес буйынса яҙығыҙ:51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA йәки [https://www.gnu.org/copyleft/gpl.html уны онлайнда уҡығыҙ].",
"config-sidebar": "* [https://www.mediawiki.org Сайт MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/ru Ҡулланыусылар өсөн белешмә]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/ru Администраторҙар өсөн белешмә]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/ru FAQ]\n----\n* <doclink href=Readme>Readme-файл</doclink>\n* <doclink href=ReleaseNotes>Сығарылыш тураһында мәғлүмәт</doclink>\n* <doclink href=Copying>Лицензия</doclink>\n* <doclink href=UpgradeDoc>Яңыртыуҙар</doclink>",
"config-env-good": "Мөхитте тикшереү уңышлы тамамланды. MediaWiki урынлаштырырға мөмкин.",
"config-env-bad": "Мөхит тикшерелде. Һеҙ MediaWiki урынлаштыра алмайһығыҙ.",
"config-env-php": "PHP: $1 өлгөһө урынлаштырылды.",
"config-env-hhvm": "HHVM $1 урынлаштырылды.",
- "config-unicode-using-intl": " [http://pecl.php.net/intl ҡушылмаһы файҙаланасаҡ, «intl» для PECL] Юникод нормаль эшләһен өсөн.",
- "config-unicode-pure-php-warning": "'''Иғтибар!''': [http://pecl.php.net/intl ҡушылмаһы intl из PECL] Юникод өсөн рөхсәт ителмәгән PHP менән бик әкрен эшләйәсәк.\nҺеҙҙең сайт бик көсөргәнешле эшләһә [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормализации Юникодты нормалләштереү] өсөн уҡығыҙ.",
+ "config-unicode-using-intl": " [https://pecl.php.net/intl ҡушылмаһы файҙаланасаҡ, «intl» для PECL] Юникод нормаль эшләһен өсөн.",
+ "config-unicode-pure-php-warning": "'''Иғтибар!''': [https://pecl.php.net/intl ҡушылмаһы intl из PECL] Юникод өсөн рөхсәт ителмәгән PHP менән бик әкрен эшләйәсәк.\nҺеҙҙең сайт бик көсөргәнешле эшләһә [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормализации Юникодты нормалләштереү] өсөн уҡығыҙ.",
"config-unicode-update-warning": "\"Иҫкәртеү\". Ҡуйылған тышлыҡ Юникодты нормаға килтереүҙең иҫке китапхана версияһын ҡуллана[http://site.icu-project.org/ проекта ICU].Әгәр Юникодты тулы мәғәнәһендә ҡулланырға теләһәгеҙ, һеҙ [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations версияны яңыртырға] тейешһегеҙ.",
"config-no-db": "Мәғлүмәттәр базаһына тура килгән драйверҙарҙы табып булманы!Һеҙгә PHP өсөн мәғлүмәттәр базаһының драйверҙарын ҡуйырға кәрәк.{{PLURAL:$2|сираттағы төр ҡулланыла|сираттағы төрҙәр ҡулланыла}}мәғлүмәттәр базалары:$1.\nӘгәр һеҙ үҙегеҙ PHP -ға компиляция яһаған булһағыҙ, мәғлүмәттәр базаһына клиентты индереп уны яңынан, мәҫәлән, <code>./configure --with-mysqli</code> ярҙамы менән көйләгеҙ. Әгәр ҙә һеҙ PHP -ны Debian йәки Ubuntu пакеттарынан ҡуйһағыҙ, һеҙгә, мәҫәлән, <code>php5-mysql</code> пакетын да ҡуйырға кәрәк булыр.",
"config-outdated-sqlite": "'''Киҫәтеү''': Һеҙҙә SQLite $1 ҡуйылған, $2 тейешле өлгөнән түбән . SQLite асылмаясаҡ.",
@@ -67,11 +67,10 @@
"config-pcre-no-utf8": "'''Фаталь хата'''. PHP өсөн PCRE модуле PCRE_UTF8 менән яраҡлыштырылмаған.\nMediaWiki дөрөҫ эшләһен өсөн UTF-8 талап ителә.",
"config-memory-raised": "Хәтер сикләнгән PHP (<code>memory_limit</code>) $1 $2 тиклем арттырылған.",
"config-memory-bad": "'''Иғтибар:''' PHP күләме <code>memory_limit</code> $1 тәшкил итә.\nБәлки, был саманан тыш аҙҙыр. \nҠуйылыштың уңышһыҙлыҡҡа осрауы бар!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] урынлаштырылды",
"config-apc": "[http://www.php.net/apc APC] урынлаштырылды",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] урынлыштырылды",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] урынлыштырылды",
"config-no-cache-apcu": "'''Иғтибар:''' [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] табылманы йәки [http://www.iis.net/download/WinCacheForPhp WinCache].\nОбъекттарҙы кэшлау һүндереләсәк..",
- "config-mod-security": "<strong>Иғтибар</strong>: һеҙҙең веб-серверығыҙҙа [http://modsecurity.org/ mod_security]/mod_security2 ҡабыҙылған. Уның күп кенә стандарт көйләүҙәре MediaWiki йәки бүтән ПО ҡулланыусыларға серверға ирекле контент ебәрегрә мөмкинлек буйынса проблемалар тыуҙырыуы мөмкин.\nКөтөлмәгән хаталарға тап булһағыҙ, ошонда [http://modsecurity.org/documentation/ документации mod_security]йәки үҙегеҙҙең хостинг-провайдерығыҙға мөрәжәғәт итегеҙ.",
+ "config-mod-security": "<strong>Иғтибар</strong>: һеҙҙең веб-серверығыҙҙа [https://modsecurity.org/ mod_security]/mod_security2 ҡабыҙылған. Уның күп кенә стандарт көйләүҙәре MediaWiki йәки бүтән ПО ҡулланыусыларға серверға ирекле контент ебәрегрә мөмкинлек буйынса проблемалар тыуҙырыуы мөмкин.\nКөтөлмәгән хаталарға тап булһағыҙ, ошонда [https://modsecurity.org/documentation/ документации mod_security]йәки үҙегеҙҙең хостинг-провайдерығыҙға мөрәжәғәт итегеҙ.",
"config-diff3-bad": "GNU diff3 табылманы.",
"config-git": "Git өлгөләрҙе контролләү системаһы табылды: <code>$1</code>.",
"config-git-bad": "Git өлгөләре менән идара итеү программаһы табылды?",
@@ -223,7 +222,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 йәки яңырағы",
"config-license-pd": "Йәмәғәт милке",
"config-license-cc-choose": "Creative Commons бер лицензияны һайлағыҙ",
- "config-license-help": "Күпселек дөйөм ҡулланыуҙағы викиҙар үҙ материалдарын [http://freedomdefined.org/Definition/Ru ирекле лицензия] шарттарында файҙаланыуға рөхсәт бирә.\nБыл берҙәмлек тойғоһон булдыррыға ярҙам итә, ҡатнашыу ваҡытын оҙайтыуға дәртләндерә. Әммә шәхси йәки корпоратив викиҙар өсөн бындай ихтыяж юҡ. \n\nӘгәр һеҙ Википедия текстарын файҙаланырға йәки Википедияға үҙ викиғыҙҙан текстар күсереү мөмкинлеге булыуын теләһәгеҙ, \n<strong>{{int:config-license-cc-by-sa}}</strong> һайлағыҙ.\nВикипедия элек GNU Free Documentation License лицензияһын файҙалана ине.\nGFDL файҙаланыла ала, әммә ул аңлау өсөн ҡатмарлы һәм материалдарҙы ҡабатлап ҡулланыуҙы ауырлаштыра.",
+ "config-license-help": "Күпселек дөйөм ҡулланыуҙағы викиҙар үҙ материалдарын [https://freedomdefined.org/Definition/Ru ирекле лицензия] шарттарында файҙаланыуға рөхсәт бирә.\nБыл берҙәмлек тойғоһон булдыррыға ярҙам итә, ҡатнашыу ваҡытын оҙайтыуға дәртләндерә. Әммә шәхси йәки корпоратив викиҙар өсөн бындай ихтыяж юҡ. \n\nӘгәр һеҙ Википедия текстарын файҙаланырға йәки Википедияға үҙ викиғыҙҙан текстар күсереү мөмкинлеге булыуын теләһәгеҙ, \n<strong>{{int:config-license-cc-by-sa}}</strong> һайлағыҙ.\nВикипедия элек GNU Free Documentation License лицензияһын файҙалана ине.\nGFDL файҙаланыла ала, әммә ул аңлау өсөн ҡатмарлы һәм материалдарҙы ҡабатлап ҡулланыуҙы ауырлаштыра.",
"config-email-settings": "Электрон почта көйләүҙәре",
"config-enable-email": "e-mail сығыусы почтаны рәхсәт итергә",
"config-enable-email-help": "Электрон почта эшләһен өсөн, [http://www.php.net/manual/ru/mail.configuration.php PHP көйләүҙәрен] башҡарырға кәрәк.\nӘгәр электрон почта мөмкинлектәре кәрәкмәһә, һүндерергә була.",
diff --git a/www/wiki/includes/installer/i18n/be-tarask.json b/www/wiki/includes/installer/i18n/be-tarask.json
index b427c195..4c4504e2 100644
--- a/www/wiki/includes/installer/i18n/be-tarask.json
+++ b/www/wiki/includes/installer/i18n/be-tarask.json
@@ -47,14 +47,14 @@
"config-help-restart": "Ці жадаеце выдаліць усе ўведзеныя зьвесткі і пачаць працэс усталяваньня зноў?",
"config-restart": "Так, пачаць зноў",
"config-welcome": "== Праверка асяродзьдзя ==\nЗараз будуць праведзеныя праверкі для запэўніваньня, што гэтае асяродзьдзе адпаведнае для ўсталяваньня MediaWiki.\nНе забудзьце далучыць гэтую інфармацыю, калі вам спатрэбіцца дапамога для завяршэньня ўсталяваньня.",
- "config-copyright": "== Аўтарскае права і ўмовы ==\n\n$1\n\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but '''without any warranty'''; without even the implied warranty of '''merchantability''' or '''fitness for a particular purpose'''.\nSee the GNU General Public License for more details.\n\nYou should have received <doclink href=Copying>a copy of the GNU General Public License</doclink> along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. or [http://www.gnu.org/copyleft/gpl.html read it online].",
+ "config-copyright": "== Аўтарскае права і ўмовы ==\n\n$1\n\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but '''without any warranty'''; without even the implied warranty of '''merchantability''' or '''fitness for a particular purpose'''.\nSee the GNU General Public License for more details.\n\nYou should have received <doclink href=Copying>a copy of the GNU General Public License</doclink> along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. or [https://www.gnu.org/copyleft/gpl.html read it online].",
"config-sidebar": "* [https://www.mediawiki.org Хатняя старонка MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Даведка для ўдзельнікаў]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Даведка для адміністратараў]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Адказы на частыя пытаньні]\n----\n* <doclink href=Readme>Прачытайце</doclink>\n* <doclink href=ReleaseNotes>Паляпшэньні ў вэрсіі</doclink>\n* <doclink href=Copying>Капіяваньне</doclink>\n* <doclink href=UpgradeDoc>Абнаўленьне</doclink>",
"config-env-good": "Асяродзьдзе было праверанае.\nВы можаце ўсталёўваць MediaWiki.",
"config-env-bad": "Асяродзьдзе было праверанае.\nУсталяваньне MediaWiki немагчымае.",
"config-env-php": "Усталяваны PHP $1.",
"config-env-hhvm": "HHVM $1 усталяваная.",
- "config-unicode-using-intl": "Выкарыстоўваецца [http://pecl.php.net/intl intl пашырэньне з PECL] для Unicode-нармалізацыі",
- "config-unicode-pure-php-warning": "'''Папярэджаньне''': [http://pecl.php.net/intl Пашырэньне intl з PECL] — ня слушнае для Unicode-нармалізацыі, цяпер выкарыстоўваецца марудная PHP-рэалізацыя.\nКалі ў Вас сайт з высокай наведвальнасьцю, раім пачытаць пра [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-нармалізацыю].",
+ "config-unicode-using-intl": "Выкарыстоўваецца [https://pecl.php.net/intl intl пашырэньне з PECL] для Unicode-нармалізацыі",
+ "config-unicode-pure-php-warning": "'''Папярэджаньне''': [https://pecl.php.net/intl Пашырэньне intl з PECL] — ня слушнае для Unicode-нармалізацыі, цяпер выкарыстоўваецца марудная PHP-рэалізацыя.\nКалі ў Вас сайт з высокай наведвальнасьцю, раім пачытаць пра [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-нармалізацыю].",
"config-unicode-update-warning": "'''Папярэджаньне''': усталяваная вэрсія бібліятэкі для Unicode-нармалізацыі выкарыстоўвае састарэлую вэрсію бібліятэкі з [http://site.icu-project.org/ праекту ICU].\nРаім [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations абнавіць], калі ваш сайт будзе працаваць з Unicode.",
"config-no-db": "Немагчыма знайсьці адпаведны драйвэр базы зьвестак. Вам неабходна ўсталяваць драйвэр базы зьвестак для PHP.\n{{PLURAL:$2|Падтрымліваецца наступны тып базы|Падтрымліваюцца наступныя тыпы базаў}} зьвестак: $1.\n\nКалі вы скампілявалі PHP самастойна, зьмяніце канфігурацыю, каб уключыць кліента базы зьвестак, напрыклад, з дапамогай <code>./configure --with-mysqli</code>.\nКалі вы ўсталявалі PHP з пакунку Debian або Ubuntu, тады вам трэба дадаткова ўсталяваць, напрыклад, пакунак <code>php5-mysql</code>.",
"config-outdated-sqlite": "'''Папярэджаньне''': усталяваны SQLite $1, у той час, калі мінімальная сумяшчальная вэрсія — $2. SQLite ня будзе даступны.",
@@ -63,12 +63,11 @@
"config-pcre-no-utf8": "'''Фатальная памылка''': модуль PCRE для PHP скампіляваны без падтрымкі PCRE_UTF8.\nMediaWiki патрабуе падтрымкі UTF-8 для слушнай працы.",
"config-memory-raised": "Абмежаваньне на даступную для PHP памяць <code>memory_limit</code> было падвышанае з $1 да $2.",
"config-memory-bad": "'''Папярэджаньне:''' памер PHP <code>memory_limit</code> складае $1.\nВерагодна, гэта вельмі мала.\nУсталяваньне можа быць няўдалым!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] усталяваны",
"config-apc": "[http://www.php.net/apc APC] усталяваны",
"config-apcu": "[http://www.php.net/apcu APCu] ўсталяваны",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] усталяваны",
- "config-no-cache-apcu": "<strong>Папярэджаньне:</strong> ня знойдзеныя [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] ці [http://www.iis.net/download/WinCacheForPhp WinCache]. Кэшаваньне аб’ектаў адключанае.",
- "config-mod-security": "'''Папярэджаньне''': на Вашым ўэб-сэрверы ўключаны [http://modsecurity.org/ mod_security]. У выпадку няслушнай наладцы, ён можа стаць прычынай праблемаў для MediaWiki ці іншага праграмнага забесьпячэньня, якое дазваляе ўдзельнікам дасылаць на сэрвэр любы зьмест.\nГлядзіце [http://modsecurity.org/documentation/ дакумэнтацыю mod_security] ці зьвярніцеся ў падтрымку Вашага хосту, калі ў Вас узьнікаюць выпадковыя праблемы.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] усталяваны",
+ "config-no-cache-apcu": "<strong>Папярэджаньне:</strong> ня знойдзеныя [http://www.php.net/apcu APCu] ці [http://www.iis.net/download/WinCacheForPhp WinCache]. Кэшаваньне аб’ектаў адключанае.",
+ "config-mod-security": "'''Папярэджаньне''': на Вашым ўэб-сэрверы ўключаны [https://modsecurity.org/ mod_security]. У выпадку няслушнай наладцы, ён можа стаць прычынай праблемаў для MediaWiki ці іншага праграмнага забесьпячэньня, якое дазваляе ўдзельнікам дасылаць на сэрвэр любы зьмест.\nГлядзіце [https://modsecurity.org/documentation/ дакумэнтацыю mod_security] ці зьвярніцеся ў падтрымку Вашага хосту, калі ў Вас узьнікаюць выпадковыя праблемы.",
"config-diff3-bad": "GNU diff3 ня знойдзены.",
"config-git": "Знойдзеная сыстэма канстролю вэрсіяў Git: <code>$1</code>",
"config-git-bad": "Сыстэма кантролю вэрсіяў Git ня знойдзеная.",
@@ -226,7 +225,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 ці болей позьняя",
"config-license-pd": "Грамадзкі набытак",
"config-license-cc-choose": "Выберыце іншую ліцэнзію Creative Commons",
- "config-license-help": "Шматлікія адкрытыя вікі публікуюць увесь унёсак у праект на ўмовах [http://freedomdefined.org/Definition вольнай ліцэнзіі].\nГэта дазваляе ствараць эфэкт супольнай уласнасьці і садзейнічае доўгатэрміноваму ўнёску.\nДля прыватных і карпаратыўных вікі гэта не зьяўляецца неабходнасьцю.\n\nКалі Вы жадаеце выкарыстоўваць тэкст зь Вікіпэдыі, і жадаеце, каб Вікіпэдыя магла прымаць тэксты, скапіяваныя з Вашай вікі, Вам неабходна выбраць ліцэнзію <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nРаней Вікіпэдыя выкарыстоўвала ліцэнзію GNU Free Documentation.\nЯна ўсё яшчэ дзейнічае, але яна ўтрымлівае некаторыя моманты, якія ўскладняюць паўторнае выкарыстаньне і інтэрпрэтацыю матэрыялаў.",
+ "config-license-help": "Шматлікія адкрытыя вікі публікуюць увесь унёсак у праект на ўмовах [https://freedomdefined.org/Definition вольнай ліцэнзіі].\nГэта дазваляе ствараць эфэкт супольнай уласнасьці і садзейнічае доўгатэрміноваму ўнёску.\nДля прыватных і карпаратыўных вікі гэта не зьяўляецца неабходнасьцю.\n\nКалі Вы жадаеце выкарыстоўваць тэкст зь Вікіпэдыі, і жадаеце, каб Вікіпэдыя магла прымаць тэксты, скапіяваныя з Вашай вікі, Вам неабходна выбраць ліцэнзію <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nРаней Вікіпэдыя выкарыстоўвала ліцэнзію GNU Free Documentation.\nЯна ўсё яшчэ дзейнічае, але яна ўтрымлівае некаторыя моманты, якія ўскладняюць паўторнае выкарыстаньне і інтэрпрэтацыю матэрыялаў.",
"config-email-settings": "Налады электроннай пошты",
"config-enable-email": "Дазволіць выходзячыя электронныя лісты",
"config-enable-email-help": "Калі Вы жадаеце, каб працавала электронная пошта, неабходна сканфігураваць PHP [http://www.php.net/manual/en/mail.configuration.php адпаведным чынам].\nКалі Вы не жадаеце выкарыстоўваць магчымасьці электроннай пошты, Вы можаце яе адключыць.",
@@ -256,7 +255,7 @@
"config-cache-options": "Налады кэшаваньня аб’ектаў:",
"config-cache-help": "Кэшаваньне аб’ектаў павялічвае хуткасьць працы MediaWiki праз кэшаваньне зьвестак, якія часта выкарыстоўваюцца.\nВельмі рэкамэндуем уключыць гэта для сярэдніх і буйных сайтаў, таксама будзе карысна для дробных сайтаў.",
"config-cache-none": "Без кэшаваньня (ніякія магчымасьці не страчваюцца, але хуткасьць працы буйных сайтаў можа зьнізіцца)",
- "config-cache-accel": "Кэшаваньне аб’ектаў PHP (APC, APCu, XCache ці WinCache)",
+ "config-cache-accel": "Кэшаваньне аб’ектаў PHP (APC, APCu ці WinCache)",
"config-cache-memcached": "Выкарыстоўваць Memcached (патрабуе дадатковай канфігурацыі)",
"config-memcached-servers": "Сэрвэры memcached:",
"config-memcached-help": "Сьпіс IP-адрасоў, якія будуць выкарыстоўвацца Memcached.\nАдрасы павінны быць у асобным радку з пазначэньнем порту, які будзе выкарыстоўвацца. Напрыклад:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -312,6 +311,7 @@
"config-install-mainpage-failed": "Немагчыма ўставіць галоўную старонку: $1",
"config-install-done": "<strong>Віншуем!</strong>\nВы ўсталявалі MediaWiki.\n\nПраграма ўсталяваньня стварыла файл <code>LocalSettings.php</code>.\nЁн утрымлівае ўсе Вашыя налады.\n\nВам неабходна загрузіць яго і захаваць у карэнную дырэкторыю Вашай вікі (у тую ж самую дырэкторыю, дзе знаходзіцца index.php). Загрузка павінна пачацца аўтаматычна.\n\nКалі загрузка не пачалася, ці Вы яе адмянілі, Вы можаце перазапусьціць яе націснуўшы на спасылку ніжэй:\n\n$3\n\n<strong>Заўвага</strong>: калі Вы гэтага ня зробіце зараз, то створаны файл ня будзе даступны Вам потым, калі Вы выйдзеце з праграмы ўсталяваньня безь яго загрузкі.\n\nКалі Вы гэта зробіце, Вы можаце <strong>[$2 ўвайсьці ў Вашую вікі]</strong>.",
"config-install-done-path": "<strong>Віншуем!</strong>\nВы ўсталявалі MediaWiki.\n\nПраграма ўсталёўкі стварыла файл <code>LocalSettings.php</code>. Ён утрымлівае ўсе вашыя налады.\n\nВам трэба спампаваць яго і пакласьці ў <code>$4</code>. Спампоўка павінна пачацца аўтаматычна.\n\nКалі спампоўка не пачалася або вы адмянілі яе, вы можаце пачаць яе наноў, калі націсьніце на наступную спасылку:\n\n$3\n\n<strong>Заўвага:</strong> Калі вы ня зробіце гэта зараз, то створаны файл ня будзе даступны вам па выхадзе з праграмы безь яго спампоўкі.\n\nКалі вы зробіце гэта, вы можаце <strong>[$2 ўвайсьці ў вашую вікі]</strong>.",
+ "config-install-success": "MediaWiki была пасьпяхова ўсталяваная. Цяпер вы можаце наведаць <$1$2>, каб пабачыць вашую вікі. Калі вы маеце пытаньні, сьпярша паглядзіце сьпіс адказаў на частыя пытаньні: <https://www.mediawiki.org/wiki/Manual:FAQ> ці скарыстайцеся адным з форумаў падтрымкі, пазначаных на гэтай старонцы.",
"config-download-localsettings": "Загрузіць <code>LocalSettings.php</code>",
"config-help": "дапамога",
"config-help-tooltip": "націсьніце, каб разгарнуць",
diff --git a/www/wiki/includes/installer/i18n/bg.json b/www/wiki/includes/installer/i18n/bg.json
index 6ecb874b..44c673b9 100644
--- a/www/wiki/includes/installer/i18n/bg.json
+++ b/www/wiki/includes/installer/i18n/bg.json
@@ -5,7 +5,8 @@
"아라",
"StanProg",
"Vodnokon4e",
- "Seb35"
+ "Seb35",
+ "ShockD"
]
},
"config-desc": "Инсталатор на МедияУики",
@@ -45,14 +46,14 @@
"config-help-restart": "Необходимо е потвърждение за изтриване на всички въведени и съхранени данни и започване отначало на процеса по инсталация.",
"config-restart": "Да, започване отначало",
"config-welcome": "=== Проверка на условията ===\nЩе бъдат извършени основни проверки, които да установят дали условията са подходящи за инсталиране на МедияУики.\nАко е необходима помощ по време на инсталацията, резултатите от направените проверки трябва също да бъдат предоставени.",
- "config-copyright": "=== Авторски права и условия ===\n\n$1\n\nТази програма е свободен софтуер, който може да се променя и/или разпространява според Общия публичен лиценз на GNU, както е публикуван от Free Software Foundation във версия на Лиценза 2 или по-късна версия.\n\nТази програма се разпространява с надеждата, че ще е полезна, но <strong>без каквито и да е гаранции</strong>; без дори косвена гаранция за <strong>продаваемост</strong> или <strong>пригодност за конкретна употреба</strong> .\nЗа повече подробности се препоръчва преглеждането на Общия публичен лиценз на GNU.\n\nКъм програмата трябва да е приложено <doclink href=Copying>копие на Общия публичен лиценз на GNU</doclink>; ако не, можете да пишете на Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, или да [http://www.gnu.org/copyleft/gpl.html го прочетете онлайн].",
+ "config-copyright": "=== Авторски права и условия ===\n\n$1\n\nТази програма е свободен софтуер, който може да се променя и/или разпространява според Общия публичен лиценз на GNU, както е публикуван от Free Software Foundation във версия на Лиценза 2 или по-късна версия.\n\nТази програма се разпространява с надеждата, че ще е полезна, но <strong>без каквито и да е гаранции</strong>; без дори косвена гаранция за <strong>продаваемост</strong> или <strong>пригодност за конкретна употреба</strong> .\nЗа повече подробности се препоръчва преглеждането на Общия публичен лиценз на GNU.\n\nКъм програмата трябва да е приложено <doclink href=Copying>копие на Общия публичен лиценз на GNU</doclink>; ако не, можете да пишете на Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, или да [https://www.gnu.org/copyleft/gpl.html го прочетете онлайн].",
"config-sidebar": "* [https://www.mediawiki.org Сайт на МедияУики]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Наръчник на потребителя]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Наръчник на администратора]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ ЧЗВ]\n----\n* <doclink href=Readme>Документация</doclink>\n* <doclink href=ReleaseNotes>Бележки за версията</doclink>\n* <doclink href=Copying>Авторски права</doclink>\n* <doclink href=UpgradeDoc>Обновяване</doclink>",
"config-env-good": "Средата беше проверена.\nИнсталирането на МедияУики е възможно.",
"config-env-bad": "Средата беше проверена.\nНе е възможна инсталация на МедияУики.",
"config-env-php": "Инсталирана е версия на PHP $1.",
"config-env-hhvm": "HHVM $1 е инсталиран.",
- "config-unicode-using-intl": "Използване на разширението [http://pecl.php.net/intl intl PECL] за нормализация на Уникод.",
- "config-unicode-pure-php-warning": "<strong>Внимание:</strong> [http://pecl.php.net/intl Разширението intl PECL] не е налично за справяне с нормализацията на Уникод, превключване към по-бавното изпълнение на чист PHP.\nАко сайтът е с голям трафик, препоръчително е запознаването с [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормализацията на Уникод].",
+ "config-unicode-using-intl": "Използване на разширението [https://pecl.php.net/intl intl PECL] за нормализация на Уникод.",
+ "config-unicode-pure-php-warning": "<strong>Внимание:</strong> [https://pecl.php.net/intl Разширението intl PECL] не е налично за справяне с нормализацията на Уникод, превключване към по-бавното изпълнение на чист PHP.\nАко сайтът е с голям трафик, препоръчително е да се запознаете с [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормализацията на Уникод].",
"config-unicode-update-warning": "<strong>Предупреждение</strong>: Инсталираната версия на Обвивката за нормализация на Unicode използва по-старата версия на библиотеката на [http://site.icu-project.org/ проекта ICU].\nНеобходимо е да [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations инсталирате по-нова версия], в случай че сте загрижени за използването на Unicode.",
"config-no-db": "Не може да бъде открит подходящ драйвер за база данни! Необходимо е да инсталирате драйвер за база данни за PHP.\n{{PLURAL:$2|Поддържа се следния тип|Поддържат се следните типове}} бази от данни: $1.\n\nАко сами сте компилирали PHP, преконфигурирайте го с включен клиент за база данни, например чрез използване на <code>./configure --with-mysqli</code>.\nАко сте инсталирали PHP от пакет за Debian или Ubuntu, необходимо е също така да инсталирате и модула <code>php5-mysql</code>.",
"config-outdated-sqlite": "<strong>Внимание:</strong> имате инсталиран SQLite $1, а минималната допустима версия е $2. SQLite ще бъде недостъпна за ползване.",
@@ -61,12 +62,11 @@
"config-pcre-no-utf8": "'''Фатално''': Модулът PCRE на PHP изглежда е компилиран без поддръжка на PCRE_UTF8.\nЗа да функционира правилно, МедияУики изисква поддръжка на UTF-8.",
"config-memory-raised": "<code>memory_limit</code> на PHP е $1, увеличаване до $2.",
"config-memory-bad": "<strong>Внимание:</strong> <code>memory_limit</code> на PHP е $1.\nСтойността вероятно е твърде ниска.\nВъзможно е инсталацията да се провали!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] е инсталиран",
"config-apc": "[http://www.php.net/apc APC] е инсталиран",
"config-apcu": "[http://www.php.net/apc APC] е инсталиран",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] е инсталиран",
- "config-no-cache-apcu": "<strong>Внимание:</strong> [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] и [http://www.iis.net/download/WinCacheForPhp WinCache] не могат да бъдат открити.\nКеширането на обекти не е активирано.",
- "config-mod-security": "<strong>Предупреждение:</strong> [http://modsecurity.org/ mod_security]/mod_security2 е включено на вашия уеб сървър. Много от обичайните му конфигурации пораждат проблеми с МедияУики и друг софтуер, който позволява публикуване на произволно съдържание.\nАко е възможно, моля изключете го. В противен случай се обърнете към [http://modsecurity.org/documentation/ документацията на mod_security] или се свържете с поддръжката на хостинга си, ако се сблъскате със случайни грешки.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] е инсталиран",
+ "config-no-cache-apcu": "<strong>Внимание:</strong> [http://www.php.net/apcu APCu] и [http://www.iis.net/download/WinCacheForPhp WinCache] не могат да бъдат открити.\nКеширането на обекти не е активирано.",
+ "config-mod-security": "<strong>Предупреждение:</strong> [https://modsecurity.org/ mod_security]/mod_security2 е включено на вашия уеб сървър. Много от обичайните му конфигурации пораждат проблеми с МедияУики и друг софтуер, който позволява публикуване на произволно съдържание.\nАко е възможно, моля изключете го. В противен случай се обърнете към [https://modsecurity.org/documentation/ документацията на mod_security] или се свържете с поддръжката на хостинга си, ако се сблъскате със случайни грешки.",
"config-diff3-bad": "GNU diff3 не беше намерен.",
"config-git": "Налична е системата за контрол на версиите Git: <code>$1</code>.",
"config-git-bad": "Не е намерен софтуер за контрол на версиите Git.",
@@ -81,6 +81,7 @@
"config-no-cli-uploads-check": "<strong>Предупреждение:</strong> Директорията по подразбиране за качване на файлове (<code>$1</code>) не е проверена за уязвимости при изпълнение на произволен скрипт по време на инсталацията от командния ред.",
"config-brokenlibxml": "Вашата система използва комбинация от версии на PHP и libxml2, които са с много грешки и могат да причинят скрити повреди на данните в МедияУики или други уеб приложения.\nНеобходимо е обновяване до libxml2 2.7.3 или по-нова версия ([https://bugs.php.net/bug.php?id=45996 докладвана грешка при PHP]).\nИнсталацията беше прекратена.",
"config-suhosin-max-value-length": "Suhosin е инсталиран и ограничава дължината GET параметъра <code>length</code> на $1 байта. Компонентът на МедияУики ResourceLoader ще може да пренебрегне частично това ограничение, но това ще намали производителността. По възможност е препоръчително да се настрои <code>suhosin.get.max_value_length</code> на 1024 или по-голяма стойност в <code>php.ini</code> и в LocalSettings.php да се настрои <code>$wgResourceLoaderMaxQueryLength</code> със същата стойност.",
+ "config-using-32bit": "<strong>Внимание:</strong> изглежда, че системата Ви работи с 32-битови числа. Това [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit не се препоръчва].",
"config-db-type": "Тип на базата от данни:",
"config-db-host": "Хост на базата от данни:",
"config-db-host-help": "Ако базата от данни е на друг сървър, в кутията се въвежда името на хоста или IP адреса.\n\nАко се използва споделен уеб хостинг, доставчикът на услугата би трябвало да е предоставил в документацията си коректния хост.\n\nАко инсталацията протича на Windows-сървър и се използва MySQL, използването на \"localhost\" може да е неприемливо. В такива случаи се използва \"127.0.0.1\" за локален IP адрес.\n\nПри използване на PostgreSQL, това поле се оставя празно, за свързване чрез Unix socket.",
@@ -166,7 +167,7 @@
"config-mysql-charset": "Набор от знаци на базата от данни:",
"config-mysql-binary": "Двоичен",
"config-mysql-utf8": "UTF-8",
- "config-mysql-charset-help": "В <strong>двоичен режим</strong> МедияУики съхранява текстовете в UTF-8 в бинарни полета в базата от данни.\nТова е по-ефективно от UTF-8 режима на MySQL и позволява използването на пълния набор от символи в Уникод.\n\nВ <strong>UTF-8 режим</strong> MySQL ще знае в кой набор от символи са данните от уикито и ще може да ги показва и променя по подходящ начин, но няма да позволява складиране на символи извън [https://en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Основния многоезичен набор].",
+ "config-mysql-charset-help": "В <strong>двоичен режим</strong> МедияУики съхранява текстовете в UTF-8 в бинарни полета в базата от данни.\nТова е по-ефективно от UTF-8 режима на MySQL и позволява използването на пълния набор от знаци в Уникод.\n\nВ <strong>UTF-8 режим</strong> MySQL ще знае в кой набор от знаци са данните от уикито и ще може да ги показва и променя по подходящ начин, но няма да позволява складиране на знаци извън [https://en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Основния многоезичен набор].",
"config-mssql-auth": "Тип на удостоверяването:",
"config-mssql-install-auth": "Изберете начин за удостоверяване, който ще бъде използван за връзка с базата от данни по време на инсталацията.\nАко изберете \"{{int:config-mssql-windowsauth}}\", ще се използват идентификационните данни на потребителя под който работи уеб сървъра.",
"config-mssql-web-auth": "Изберете начина за удостоверяване, който ще се използва от уеб сървъра за връзка със сървъра за бази от данни по време на нормалните операции на уикито.\nАко изберете \"{{int:config-mssql-windowsauth}}\", ще се използват идентификационните данни на потребителя под който работи уеб сървъра.",
@@ -180,7 +181,7 @@
"config-ns-site-name": "Същото като името на уикито: $1",
"config-ns-other": "Друго (уточняване)",
"config-ns-other-default": "МоетоУики",
- "config-project-namespace-help": "Следвайки примера на Уикипедия, много уикита съхраняват страниците си с правила в '''именно пространство на проекта''', отделно от основното съдържание.\nВсички заглавия на страниците в това именно пространство започват с определена представка, която може да бъде зададена тук.\nОбикновено представката произлиза от името на уикито, но не може да съдържа символи като \"#\" или \":\".",
+ "config-project-namespace-help": "Следвайки примера на Уикипедия, много уикита съхраняват страниците си с правила в '''именно пространство на проекта''', отделно от основното съдържание.\nВсички заглавия на страниците в това именно пространство започват с определена представка, която може да бъде зададена тук.\nОбикновено представката произлиза от името на уикито, но не може да съдържа знаци като „“# или „:“.",
"config-ns-invalid": "Посоченото именно пространство „<nowiki>$1</nowiki>“ е невалидно.\nНеобходимо е да бъде посочено друго.",
"config-ns-conflict": "Посоченото именно пространство „<nowiki>$1</nowiki>“ е в конфликт с използваното по подразбиране именно пространство MediaWiki.\nНеобходимо е да се посочи друго именно пространство.",
"config-admin-box": "Администраторска сметка",
@@ -220,7 +221,7 @@
"config-license-gfdl": "Лиценз за свободна документация на GNU 1.3 или по-нов",
"config-license-pd": "Обществено достояние",
"config-license-cc-choose": "Избиране на друг лиценз от Криейтив Комънс",
- "config-license-help": "Много публични уикита поставят всички приноси под [http://freedomdefined.org/Definition/Bg свободен лиценз].\nТова помага за създаването на усещане за общност и насърчава дългосрочните приноси. \nТова не е необходимо като цяло за частно или корпоративно уики.\n\nАко искате да използвате текстове от Уикипедия, и искате Уикипедия да може да приема текстове, копирани от вашето уики, трябва да изберете лиценз <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nЛицензът за свободна документация на GNU е старият лиценз на съдържанието на Уикипедия.\nТой все още е валиден лиценз, но някои негови условия са трудни за разбиране и правят по-сложни повторното използване и интерпретацията.",
+ "config-license-help": "Много публични уикита поставят всички приноси под [https://freedomdefined.org/Definition/Bg свободен лиценз].\nТова помага за създаването на усещане за общност и насърчава дългосрочните приноси. \nТова не е необходимо като цяло за частно или корпоративно уики.\n\nАко искате да използвате текстове от Уикипедия, и искате Уикипедия да може да приема текстове, копирани от вашето уики, трябва да изберете лиценз <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nЛицензът за свободна документация на GNU е старият лиценз на съдържанието на Уикипедия.\nТой все още е валиден лиценз, но някои негови условия са трудни за разбиране и правят по-сложни повторното използване и интерпретацията.",
"config-email-settings": "Настройки за е-поща",
"config-enable-email": "Разрешаване на изходящи е-писма",
"config-enable-email-help": "За да работят възможностите за използване на е-поща, необходимо е [http://www.php.net/manual/en/mail.configuration.php настройките за поща на PHP] да бъдат конфигурирани правилно.\nАко няма да се използват услугите за е-поща в уикито, те могат да бъдат изключени тук.",
@@ -250,7 +251,7 @@
"config-cache-options": "Настройки за обектното кеширане:",
"config-cache-help": "Обектното кеширане се използва за подобряване на скоростта на МедияУики чрез кеширане на често използваните данни.\nСилно препоръчително е на средните и големите сайтове да включат тази настройка, но малките също могат да се възползват от нея.",
"config-cache-none": "Без кеширане (не се премахва от функционалността, но това влияе на скоростта на по-големи уикита)",
- "config-cache-accel": "PHP обектно кеширане (APC, APCu, XCache или WinCache)",
+ "config-cache-accel": "PHP обектно кеширане (APC, APCu или WinCache)",
"config-cache-memcached": "Използване на Memcached (изисква допълнителни настройки и конфигуриране)",
"config-memcached-servers": "Memcached сървъри:",
"config-memcached-help": "Списък с IP адреси за използване за Memcached.\nНеобходимо е да бъдат разделени по един на ред, както и да е посочен порта. Пример:\n127.0.0.1:11211\n192.168.1.25:1234",
@@ -306,11 +307,14 @@
"config-install-mainpage-failed": "Вмъкването на Началната страница беше невъзможно: $1",
"config-install-done": "<strong>Поздравления!</strong>\nИнсталирането на МедияУики приключи успешно.\n\nИнсталаторът създаде файл <code>LocalSettings.php</code>.\nТой съдържа всичката необходима основна конфигурация на уикито.\n\nНеобходимо е той да бъде изтеглен и поставен в основната директория на уикито (директорията, в която е и index.php). Изтеглянето би трябвало да започне автоматично.\n\nАко изтеглянето не започне автоматично или е било прекратено, файлът може да бъде изтеглен чрез щракване на препратката по-долу:\n\n$3\n\n<strong>Забележка:</strong> Ако това не бъде извършено сега, генерираният конфигурационен файл няма да е достъпен на по-късен етап ако не бъде изтеглен сега или инсталацията приключи без изтеглянето му.\n\nКогато файлът вече е в основната директория, <strong>[$2 уикито ще е достъпно на този адрес]</strong>.",
"config-install-done-path": "<strong>Поздравления!</strong>\nИнсталирането на МедияУики приключи успешно.\n\nИнсталаторът създаде файл <code>LocalSettings.php</code>.\nТой съдържа всички ваши настройки.\n\nНеобходимо е той да бъде изтеглен и поставен в <code>$4</code>. Изтеглянето би трябвало да започне автоматично.\n\nАко изтеглянето не започне автоматично или е било прекратено, файлът може да бъде изтеглен чрез щракване на препратката по-долу:\n\n$3\n\n<strong>Забележка:</strong> Ако това не бъде направено сега, генерираният конфигурационен файл няма да е достъпен на по-късен етап ако не бъде изтеглен сега или инсталацията приключи без изтеглянето му.\n\nКогато файлът вече е в основната директория, <strong>[$2 уикито ще е достъпно на този адрес]</strong>.",
+ "config-install-success": "МедияУики беше успешно инсталиран. Можете да посетите <$1$2> за да видите Вашето уики.\nАко имате въпроси, вижте списъка с често задавани въпроси:\n<https://www.mediawiki.org/wiki/Manual:FAQ> или използвайте някой от форумите за поддръжка на тази страница.",
"config-download-localsettings": "Изтегляне на <code>LocalSettings.php</code>",
"config-help": "помощ",
"config-help-tooltip": "щракнете за разгръщане",
"config-nofile": "Файлът „$1“ не може да бъде открит. Да не е бил изтрит?",
"config-extension-link": "Знаете ли, че това уики поддържа [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions разширения]?\n\nМожете да разгледате [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category разширенията по категория] или [https://www.mediawiki.org/wiki/Extension_Matrix Матрицата на разширенията] за пълен списък на разширенията.",
+ "config-skins-screenshots": "$1 (снимки на екрана: $2)",
+ "config-screenshot": "снимка на екран",
"mainpagetext": "<strong>МедияУики беше успешно инсталирано.</strong>",
"mainpagedocfooter": "Разгледайте [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents ръководството] за подробна информация относно използването на уики софтуера.\n\n== Първи стъпки ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Настройки за конфигуриране]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ ЧЗВ за МедияУики]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Пощенски списък относно нови версии на МедияУики]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Локализиране на МедияУики]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Научете как да се справяте със спама във вашето уики]"
}
diff --git a/www/wiki/includes/installer/i18n/bn.json b/www/wiki/includes/installer/i18n/bn.json
index dc423a64..132a06d1 100644
--- a/www/wiki/includes/installer/i18n/bn.json
+++ b/www/wiki/includes/installer/i18n/bn.json
@@ -44,9 +44,8 @@
"config-env-php": "পিএইচপি $1 ইন্সটল করা হয়েছে।",
"config-env-hhvm": "HHVM $1 ইনস্টল করা হয়েছে।",
"config-memory-raised": "পিএইচপির <code>memory_limit</code> হচ্ছে $1, বৃদ্ধি পেয়ে $2 হয়েছে।",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] ইনস্টল করা হয়েছে",
"config-apc": "[http://www.php.net/apc এপিসি] ইনস্টল হয়েছে",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] ইনস্টল করা হয়েছে",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] ইনস্টল করা হয়েছে",
"config-diff3-bad": "GNU diff3 পাওয়া যায়নি।",
"config-git": "Git সংস্করণের নিয়ন্ত্রণ সফটওয়্যার পাওয়া গেছে: <code>$1</code>।",
"config-git-bad": "Git সংস্করণের নিয়ন্ত্রণ সফটওয়্যার পাওয়া যায়নি।",
diff --git a/www/wiki/includes/installer/i18n/br.json b/www/wiki/includes/installer/i18n/br.json
index f38c7695..f41ef232 100644
--- a/www/wiki/includes/installer/i18n/br.json
+++ b/www/wiki/includes/installer/i18n/br.json
@@ -46,14 +46,14 @@
"config-help-restart": "Ha c'hoant hoc'h eus da ziverkañ an holl roadennoù hoc'h eus ebarzhet ha da adlañsañ an argerzh staliañ ?",
"config-restart": "Ya, adloc'hañ anezhañ",
"config-welcome": "=== Gwiriadennoù a denn d'an endro ===\nRekis eo un nebeud gwiriadennoù diazez da welet hag azas eo an endro evit gallout staliañ MediaWiki.\nHo pet soñj merkañ disoc'hoù ar gwiriadennoù-se m'ho pez ezhomm skoazell e-pad ar staliadenn.",
- "config-copyright": "=== Gwiriañ aozer ha Termenoù implijout ===\n\n$1\n\nUr meziant frank eo ar programm-mañ; gallout a rit skignañ anezhañ ha/pe kemmañ anezhañ dindan termenoù ar GNU Aotre-implijout Foran Hollek evel m'emañ embannet gant Diazezadur ar Meziantoù Frank; pe diouzh stumm 2 an aotre-implijout, pe (evel mar karit) diouzh ne vern pe stumm nevesoc'h.\n\nIngalet eo ar programm gant ar spi e vo talvoudus met n'eus '''tamm gwarant ebet'''; hep zoken gwarant empleg ar '''varc'hadusted''' pe an '''azaster ouzh ur pal bennak'''. Gwelet ar GNU Aotre-Implijout Foran Hollek evit muioc'h a ditouroù.\n\nSañset oc'h bezañ resevet <doclink href=Copying>un eilskrid eus ar GNU Aotre-implijout Foran Hollek</doclink> a-gevret gant ar programm-mañ; ma n'hoc'h eus ket, skrivit da Diazezadur ar Meziantoù Frank/Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, SUA pe [http://www.gnu.org/licenses/old-licenses/gpl-2.0.html lennit anezhañ enlinenn].",
+ "config-copyright": "=== Gwiriañ aozer ha Termenoù implijout ===\n\n$1\n\nUr meziant frank eo ar programm-mañ; gallout a rit skignañ anezhañ ha/pe kemmañ anezhañ dindan termenoù ar GNU Aotre-implijout Foran Hollek evel m'emañ embannet gant Diazezadur ar Meziantoù Frank; pe diouzh stumm 2 an aotre-implijout, pe (evel mar karit) diouzh ne vern pe stumm nevesoc'h.\n\nIngalet eo ar programm gant ar spi e vo talvoudus met n'eus '''tamm gwarant ebet'''; hep zoken gwarant empleg ar '''varc'hadusted''' pe an '''azaster ouzh ur pal bennak'''. Gwelet ar GNU Aotre-Implijout Foran Hollek evit muioc'h a ditouroù.\n\nSañset oc'h bezañ resevet <doclink href=Copying>un eilskrid eus ar GNU Aotre-implijout Foran Hollek</doclink> a-gevret gant ar programm-mañ; ma n'hoc'h eus ket, skrivit da Diazezadur ar Meziantoù Frank/Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, SUA pe [https://www.gnu.org/licenses/old-licenses/gpl-2.0.html lennit anezhañ enlinenn].",
"config-sidebar": "* [https://www.mediawiki.org MediaWiki degemer]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Sturlevr an implijerien]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Sturlevr ar verourien]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAG]\n----\n* <doclink href=Readme>Lennit-me</doclink>\n* <doclink href=ReleaseNotes>Notennoù embann</doclink>\n* <doclink href=Copying>Oc'h eilañ</doclink>\n* <doclink href=UpgradeDoc>O hizivaat</doclink>",
"config-env-good": "Gwiriet eo bet an endro.\nGallout a rit staliañ MediaWiki.",
"config-env-bad": "Gwiriet eo bet an endro.\nNe c'hallit ket staliañ MediaWiki.",
"config-env-php": "Staliet eo PHP $1.",
"config-env-hhvm": "HHVM $1 zo staliet.",
- "config-unicode-using-intl": "Oc'h implijout [http://pecl.php.net/intl an astenn PECL intl] evit ar reolata Unicode.",
- "config-unicode-pure-php-warning": "<strong>Diwallit</strong> : N'haller ket ober gant an [http://pecl.php.net/intl intl astenn PECL] evit merañ reoladur Unicode; distreiñ d'ar stumm gorrek emplementet e PHP-rik.\nMa rit war-dro ul lec'hienn darempredet-stank e vo mat deoc'h lenn un tammig bihan diwar-benn se war [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations reolennadur Unicode].",
+ "config-unicode-using-intl": "Oc'h implijout [https://pecl.php.net/intl an astenn PECL intl] evit ar reolata Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Diwallit</strong> : N'haller ket ober gant an [https://pecl.php.net/intl intl astenn PECL] evit merañ reoladur Unicode; distreiñ d'ar stumm gorrek emplementet e PHP-rik.\nMa rit war-dro ul lec'hienn darempredet-stank e vo mat deoc'h lenn un tammig bihan diwar-benn se war [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations reolennadur Unicode].",
"config-unicode-update-warning": "'''Diwallit''': ober a ra stumm staliet endalc'her skoueriekaat Unicode gant ur stumm kozh eus [http://site.icu-project.org/ levraoueg meziantoù ar raktres ICU].\nDleout a rafec'h [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations hizivaat] ma seblant deoc'h bezañ pouezus ober gant Unicode.",
"config-no-db": "N'eus ket bet gallet kavout ur sturier diazoù roadennoù a zere ! Ret eo deoc'h staliañ ur sturier diazoù roadennoù evit PHP.\nSkoret eo {{PLURAL:$2|ar seurt|ar seurtoù}} diazoù roadennoù da-heul : $1.\n\nMard eo bet kempunet PHP ganeoc'h-c'hwi hoc'h-unan, adkeflugnit-eñ en ur weredekaat un arval diaz roadennoù, da skouer en ur ober gant <code>/configure --with-mysqli</code>.\nM'hoc'h eus staliet PHP adalek ur pakad Debian pe Ubuntu, eo ret deoc'h staliañ, da skouer, ar pakad <code>php5-mysql</code> ivez.",
"config-outdated-sqlite": "<strong>Taolit pled :</strong> ober a rit gant SQLite $1, hag a zo izeloc'h eget ar stumm $2 ret bihanañ. Ne vo ket posupl ober gant SQLite.",
@@ -62,12 +62,11 @@
"config-pcre-no-utf8": "'''Fazi groñs ''': evit doare eo bet kempunet modulenn PCRE PHP hep ar skor PCRE_UTF8.\nEzhomm en deus MediaWiki eus UTF-8 evit mont plaen en-dro.",
"config-memory-raised": "<code>memory_limit</code> ar PHP zo $1, kemmet e $2.",
"config-memory-bad": "'''Diwallit :''' Da $1 emañ arventenn <code>memory_limit</code> PHP.\nRe izel eo moarvat.\nMarteze e c'hwito ar staliadenn !",
- "config-xcache": "Staliet eo [http://xcache.lighttpd.net/ XCache]",
"config-apc": "Staliet eo [http://www.php.net/apc APC]",
"config-apcu": "Staliet eo [http://www.php.net/apcu APCu]",
- "config-wincache": "Staliet eo [http://www.iis.net/download/WinCacheForPhp WinCache]",
+ "config-wincache": "Staliet eo [https://www.iis.net/download/WinCacheForPhp WinCache]",
"config-no-cache-apcu": "<strong>Taolit pled :</strong> N'eus ket bet gallet kavout [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] pe [http://www.iis.net/download/WinCacheForPhp WinCache].\nN'eo ket gweredekaet ar c'hrubuilhañ traezoù.",
- "config-mod-security": "<strong>Taolit pled :</strong> Gweredekaet eo [http://modsecurity.org/ mod_security]/mod_security2 gant ho servijer web. Ma n'eo ket kfluniet mat e c'hall tegas trubuilhoù da MediaWiki ha meziantoù all a aotre implijerien da ouzhpennañ danvez evel ma karont.\nE kement ha m'eo posupl e tlefe bezañ diweredekaet. A-hend-all, sellit ouzh [http://modsecurity.org/documentation/ mod_security an teuliadur] pe kit e darempred gant skoazell ho herberc'hier m'en em gavit gant fazioù dargouezhek.",
+ "config-mod-security": "<strong>Taolit pled :</strong> Gweredekaet eo [https://modsecurity.org/ mod_security]/mod_security2 gant ho servijer web. Ma n'eo ket kfluniet mat e c'hall tegas trubuilhoù da MediaWiki ha meziantoù all a aotre implijerien da ouzhpennañ danvez evel ma karont.\nE kement ha m'eo posupl e tlefe bezañ diweredekaet. A-hend-all, sellit ouzh [https://modsecurity.org/documentation/ mod_security an teuliadur] pe kit e darempred gant skoazell ho herberc'hier m'en em gavit gant fazioù dargouezhek.",
"config-diff3-bad": "N'eo ket bet kavet GNU diff3.",
"config-git": "Kavet eo bet ar meziant kontrolliñ adstummoù Git : <code>$1</code>.",
"config-git-bad": "N'eo ket bet kavet ar meziant kontrolliñ stummoù Git.",
diff --git a/www/wiki/includes/installer/i18n/bs.json b/www/wiki/includes/installer/i18n/bs.json
index 66139728..07a0e2ee 100644
--- a/www/wiki/includes/installer/i18n/bs.json
+++ b/www/wiki/includes/installer/i18n/bs.json
@@ -50,10 +50,9 @@
"config-no-db": "Ne mogu pronaći pogodan upravljački program za bazu podataka! Morate ga instalirati za PHP-bazu.\n{{PLURAL:$2|Podržana je sljedeća vrsta|Podržane su sljedeće vrste}} baze podataka: $1.\n\nAko se sami kompajlirali PHP, omogućite klijent baze podataka u postavkama koristeći, naprimjer, <code>./configure --with-mysqli</code>.\nAko ste instalirali PHP iz paketa za Debian ili Ubuntu, onda također morate instalirati, naprimjer, paket <code>php5-mysql</code>.",
"config-memory-raised": "<code>memory_limit</code> za PHP iznosi $1, povišen na $2.",
"config-memory-bad": "<strong>Upozorenje:</strong> <code>memory_limit</code> za PHP iznosi $1.\nOvo je vjerovatno premalo.\nInstalacija možda neće uspjeti!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] je instaliran",
"config-apc": "[http://www.php.net/apc APC] je instaliran",
"config-apcu": "[http://www.php.net/apcu APCu] je instaliran",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] je instaliran",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] je instaliran",
"config-diff3-bad": "GNU diff3 nije pronađen.",
"config-git": "Pronađen je Git program za kontrolu verzija: <code>$1</code>.",
"config-git-bad": "Nije pronađen Git program za kontrolu verzija.",
diff --git a/www/wiki/includes/installer/i18n/bto.json b/www/wiki/includes/installer/i18n/bto.json
index 468bcebd..2252568e 100644
--- a/www/wiki/includes/installer/i18n/bto.json
+++ b/www/wiki/includes/installer/i18n/bto.json
@@ -25,9 +25,6 @@
"config-db-host-oracle": "Database ka TNS:",
"config-db-wiki-settings": "Mibdiron adin wiki",
"config-db-name": "Ngaran ka database:",
- "config-charset-mysql5-binary": "MySQL 4.1/5.0 binary",
- "config-charset-mysql5": "MySQL 4.1/5.0 UTF-8",
- "config-charset-mysql4": "MySQL 4.0 backwards-compatible UTF-8",
"config-db-port": "Port ka database:",
"config-db-schema": "Skema para sa MediaWiki:",
"config-sqlite-dir": "Direktoryo ka data sa SQLite:",
diff --git a/www/wiki/includes/installer/i18n/ca.json b/www/wiki/includes/installer/i18n/ca.json
index c86b8244..983c5c29 100644
--- a/www/wiki/includes/installer/i18n/ca.json
+++ b/www/wiki/includes/installer/i18n/ca.json
@@ -51,19 +51,18 @@
"config-help-restart": "Voleu esborrar totes les dades que heu introduït i tornar a començar el procés d'instal·lació?",
"config-restart": "Sí, torna a començar",
"config-welcome": "=== Comprovacions de l'entorn ===\nS'efectuaran comprovacions bàsiques per veure si l'entorn és adequat per a la instal·lació del MediaWiki.\nRecordeu d'incloure aquesta informació si heu de demanar ajuda sobre com completar la instal·lació.",
- "config-copyright": "=== Drets d'autor i condicions ===\n\n$1\n\nAquest programa és de programari lliure; podeu redistribuir-lo i/o modificar-lo sota les condicions de la Llicència Pública General GNU com es publicada per la Free Software Foundation; qualsevol versió 2 de la llicència, o (opcionalment) qualsevol versió posterior.\n\nAquest programa és distribueix amb l'esperança que serà útil, però <strong>sense cap garantia</strong>; sense ni tan sols la garantia implícita de <strong>\ncomerciabilitat</strong> o <strong>idoneïtat per a un propòsit particular</strong>.\nConsulteu la Llicència Pública General GNU, per a més detalls.\n\nHauríeu d'haver rebut <doclink href=\"Copying\">una còpia de la Llicència Pública General GNU</doclink> amb aquest programa; si no, escriviu a la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA o [http://www.gnu.org/copyleft/gpl.html per llegir-lo en línia].",
+ "config-copyright": "=== Drets d'autor i condicions ===\n\n$1\n\nAquest programa és de programari lliure; podeu redistribuir-lo i/o modificar-lo sota les condicions de la Llicència Pública General GNU com es publicada per la Free Software Foundation; qualsevol versió 2 de la llicència, o (opcionalment) qualsevol versió posterior.\n\nAquest programa és distribueix amb l'esperança que serà útil, però <strong>sense cap garantia</strong>; sense ni tan sols la garantia implícita de <strong>\ncomerciabilitat</strong> o <strong>idoneïtat per a un propòsit particular</strong>.\nConsulteu la Llicència Pública General GNU, per a més detalls.\n\nHauríeu d'haver rebut <doclink href=\"Copying\">una còpia de la Llicència Pública General GNU</doclink> amb aquest programa; si no, escriviu a la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA o [https://www.gnu.org/copyleft/gpl.html per llegir-lo en línia].",
"config-sidebar": "* [https://www.mediawiki.org la Pàgina d'inici]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guia de l'usuari]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guia de l'administrador]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ PMF]\n----\n* <doclink href=Readme>Llegeix-me</doclink>\n* <doclink href=ReleaseNotes>Notes de la versió</doclink>\n* <doclink href=Còpia>Còpia</doclink>\n* <doclink href=UpgradeDoc>Actualització</doclink>",
"config-env-good": "S'ha comprovat l'entorn.\nPodeu instal·lar el MediaWiki.",
"config-env-bad": "S'ha comprovat l'entorn.\nNo podeu instal·lar el MediaWiki.",
"config-env-php": "El PHP $1 està instal·lat.",
"config-env-hhvm": "L’HHVM $1 és instal·lat.",
- "config-unicode-using-intl": "S'utilitza l'[http://pecl.php.net/intl extensió intl PECL] per a la normalització de l'Unicode.",
+ "config-unicode-using-intl": "S'utilitza l'[https://pecl.php.net/intl extensió intl PECL] per a la normalització de l'Unicode.",
"config-memory-raised": "El <code>memory_limit</code> del PHP és $1 i s'ha aixecat a $2.",
"config-memory-bad": "<strong>Avís:</strong> El <code>memory_limit</code> del PHP és $1.\nAixò és probablement massa baix.\nLa instal·lació pot fallar!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] està instal·lat",
"config-apc": "L’[http://www.php.net/apc APC] està instal·lat",
"config-apcu": "[http://www.php.net/apcu APCu] està instal·lat",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] està instal·lat",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] està instal·lat",
"config-diff3-bad": "No s'ha trobat el GNU diff3.",
"config-git": "S'ha trobat el programari de control de versions Git: <code>$1</code>.",
"config-git-bad": "No s'ha trobat el programari de control de versions Git.",
@@ -257,6 +256,8 @@
"config-help-tooltip": "feu clic per ampliar",
"config-nofile": "No s'ha pogut trobar el fitxer «$1». S'ha suprimit?",
"config-extension-link": "Sabíeu que el vostre wiki permet l'ús d'[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensions]?\n\nPodeu navegar les [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensions per categoria] o la [https://www.mediawiki.org/wiki/Extension_Matrix matriu d'extensions] per a veure'n una llista sencera.",
+ "config-skins-screenshots": "$1 (captures de pantalla: $2)",
+ "config-screenshot": "captura de pantalla",
"mainpagetext": "<strong>MediaWiki s'ha instal·lat.</strong>",
"mainpagedocfooter": "Consulteu la [https://meta.wikimedia.org/wiki/Help:Contents Guia d'Usuari] per a més informació sobre com utilitzar aquest programari wiki.\n\n== Primers passos ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Llista de paràmetres configurables]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ PMF del MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Llista de correu per a anuncis del MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Traducció de MediaWiki en la vostra llengua]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Aprengueu com combatre la brossa que pot atacar el vostre wiki]"
}
diff --git a/www/wiki/includes/installer/i18n/ce.json b/www/wiki/includes/installer/i18n/ce.json
index e11fae23..b58ff41d 100644
--- a/www/wiki/includes/installer/i18n/ce.json
+++ b/www/wiki/includes/installer/i18n/ce.json
@@ -27,7 +27,7 @@
"config-page-copying": "Лицензи",
"config-page-upgradedoc": "Карлаяккхар",
"config-page-existingwiki": "Йолуш йолу вики",
- "config-copyright": "=== Авторан бакъонаш а хьал а ===\n\n$1\nMediaWiki ю маьрша программин латораг, шу йиш ю фондас арахецна йолу GNU General Public License лицензица и яржо я хийца а.\n\nMediaWiki яржош ю и шуна пайдане хир яц те аьлла, амма цхьа юкъарахилар доцуш. Хь. кхин. лицензи мадарра GNU General Public License .\n\nШоьга кхача езаш яра [{{SERVER}}{{SCRIPTPATH}}/COPYING копи GNU General Public License] хӀокху программица, кхаьчна яцахь язъе Free Software Foundation, Inc., адрес тӀе: 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA я [//www.gnu.org/licenses/old-licenses/gpl-2.0.html еша и онлайнехь].",
+ "config-copyright": "=== Авторан бакъонаш а хьал а ===\n\n$1\nMediaWiki ю маьрша программин латораг, шу йиш ю фондас арахецна йолу GNU General Public License лицензица и яржо я хийца а.\n\nMediaWiki яржош ю и шуна пайдане хир яц те аьлла, амма цхьа юкъарахилар доцуш. Хь. кхин. лицензи мадарра GNU General Public License .\n\nШоьга кхача езаш яра [{{SERVER}}{{SCRIPTPATH}}/COPYING копи GNU General Public License] хӀокху программица, кхаьчна яцахь язъе Free Software Foundation, Inc., адрес тӀе: 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA я [https://www.gnu.org/licenses/old-licenses/gpl-2.0.html еша и онлайнехь].",
"config-no-fts3": "'''Тергам бе''': SQLite гулйина хуттург йоцуш [//sqlite.org/fts3.html FTS3] — лахар болхбеш хир дац оцу бухца.",
"config-no-cli-uri": "'''ДӀахьедар''': <code>--scriptpath</code> параметр язйина яц, иза Ӏадйитаран кепаца лелош ю: <code>$1</code> .",
"config-db-name": "Хаамийн базан цӀе:",
@@ -65,7 +65,7 @@
"config-upload-deleted": "ДӀаяхна файлийн директори:",
"config-logo": "Логотипан URL:",
"config-cc-again": "Хьаржа кхин цӀа…",
- "config-skins": "Кечяран тема",
+ "config-skins": "Кечъяран тема",
"config-skins-use-as-default": "ХӀара тема Ӏадйитаран кепара лелае",
"config-skins-must-enable-some": "Ахьа цхьаъ мукъа тема латина йита езаш ю.",
"config-skins-must-enable-default": "Ӏадйитаран кепаца йолу тема латина хила еза.",
diff --git a/www/wiki/includes/installer/i18n/ckb.json b/www/wiki/includes/installer/i18n/ckb.json
index 3bfa8a6f..a53e4706 100644
--- a/www/wiki/includes/installer/i18n/ckb.json
+++ b/www/wiki/includes/installer/i18n/ckb.json
@@ -4,7 +4,8 @@
"Asoxor",
"Calak",
"Muhammed taha",
- "Lost Whispers"
+ "Lost Whispers",
+ "Épine"
]
},
"config-desc": "دامەزرێنەرەکە بۆ میدیاویکی",
@@ -31,7 +32,7 @@
"config-restart": "بەڵێ، دەستی پێ بکەرەوە",
"config-env-php": "PHP $1 دامەزراوە.",
"config-apc": "[http://www.php.net/apc APC] دامەزراوە",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] دامەزراوە",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] دامەزراوە",
"config-db-type": "جۆری داتابەیس:",
"config-db-host": "خانەخوێی داتابەیس:",
"config-db-name": "ناوی بنکەدراوە:",
diff --git a/www/wiki/includes/installer/i18n/cs.json b/www/wiki/includes/installer/i18n/cs.json
index 2ff49fcb..ad2b910b 100644
--- a/www/wiki/includes/installer/i18n/cs.json
+++ b/www/wiki/includes/installer/i18n/cs.json
@@ -9,7 +9,8 @@
"Paxt",
"Matěj Suchánek",
"LordMsz",
- "Seb35"
+ "Seb35",
+ "Ilimanaq29"
]
},
"config-desc": "Instalační program pro MediaWiki",
@@ -49,14 +50,14 @@
"config-help-restart": "Chcete smazat všechny údaje, které jste zadali, a spustit proces instalace znovu od začátku?",
"config-restart": "Ano, restartovat",
"config-welcome": "=== Kontrola prostředí ===\nNyní se provedou základní kontroly, aby se zjistilo, zda je toto prostředí použitelné k instalaci MediaWiki.\nPokud budete potřebovat k dokončení instalace pomoc, nezapomeňte sdělit výsledky těchto testů.",
- "config-copyright": "=== Licence a podmínky ===\n$1\n\nTento program je svobodný software; můžete jej šířit nebo modifikovat podle podmínek GNU General Public License, vydávané Free Software Foundation; buď verze 2 této licence anebo (podle vašeho uvážení) kterékoli pozdější verze.\n\nTento program je distribuován v naději, že bude užitečný, avšak '''bez jakékoli záruky'''; neposkytují se ani odvozené záruky '''prodejnosti''' anebo '''vhodnosti pro určitý účel'''.\nPodrobnosti se dočtete v textu GNU General Public License.\n\n<doclink href=Copying>Kopii GNU General Public License</doclink> jste měli obdržet spolu s tímto programem; pokud ne, napište na Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA nebo [http://www.gnu.org/copyleft/gpl.html si ji přečtěte online].",
+ "config-copyright": "=== Licence a podmínky ===\n$1\n\nTento program je svobodný software; můžete jej šířit nebo modifikovat podle podmínek GNU General Public License, vydávané Free Software Foundation; buď verze 2 této licence anebo (podle vašeho uvážení) kterékoli pozdější verze.\n\nTento program je distribuován v naději, že bude užitečný, avšak '''bez jakékoli záruky'''; neposkytují se ani odvozené záruky '''prodejnosti''' anebo '''vhodnosti pro určitý účel'''.\nPodrobnosti se dočtete v textu GNU General Public License.\n\n<doclink href=Copying>Kopii GNU General Public License</doclink> jste měli obdržet spolu s tímto programem; pokud ne, napište na Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA nebo [https://www.gnu.org/copyleft/gpl.html si ji přečtěte online].",
"config-sidebar": "* [https://www.mediawiki.org Oficiální web MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Uživatelská příručka]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administrátorská příručka]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Čti mě</doclink>\n* <doclink href=ReleaseNotes>Poznámky k vydání</doclink>\n* <doclink href=Copying>Licence</doclink>\n* <doclink href=UpgradeDoc>Upgrade</doclink>",
"config-env-good": "Prostředí bylo zkontrolováno.\nMůžete nainstalovat MediaWiki.",
"config-env-bad": "Prostředí bylo zkontrolováno.\nMediaWiki nelze nainstalovat.",
"config-env-php": "Je nainstalováno PHP $1.",
"config-env-hhvm": "Je nainstalováno HHVM $1.",
- "config-unicode-using-intl": "Pro normalizaci Unicode se používá [http://pecl.php.net/intl PECL rozšíření intl].",
- "config-unicode-pure-php-warning": "<strong>Upozornění:</strong> Není dostupné [http://pecl.php.net/intl PECL rozšíření intl] pro normalizaci Unicode, bude se využívat pomalá implementace v čistém PHP.\nPokud provozujete wiki s velkou návštěvností, měli byste si přečíst něco o [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizaci Unicode].",
+ "config-unicode-using-intl": "Pro normalizaci Unicode se používá [https://pecl.php.net/intl PECL rozšíření intl].",
+ "config-unicode-pure-php-warning": "<strong>Upozornění:</strong> Není dostupné [https://pecl.php.net/intl PECL rozšíření intl] pro normalizaci Unicode, bude se využívat pomalá implementace v čistém PHP.\nPokud provozujete wiki s velkou návštěvností, měli byste si přečíst něco o [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizaci Unicode].",
"config-unicode-update-warning": "<strong>Upozornění:</strong> Nainstalovaná verze vrstvy pro normalizaci Unicode používá starší verzi knihovny [http://site.icu-project.org/ projektu ICU].\nPokud vám aspoň trochu záleží na používání Unicode, měli byste [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ji aktualizovat].",
"config-no-db": "Nepodařilo se nalézt vhodný databázový ovladač! Musíte nainstalovat databázový ovladač pro PHP.\n{{PLURAL:$2|Je podporován následující typ databáze|Jsou podporovány následující typy databází}}: $1.\n\nPokud jste si PHP přeložili sami, překonfigurujte ho se zapnutým databázovým klientem, například pomocí <code>./configure --with-mysqli</code>.\nPokud jste PHP nainstalovali z balíčku Debian či Ubuntu, potřebujete nainstalovat také modul <code>php5-mysql</code>.",
"config-outdated-sqlite": "<strong>Upozornění:</strong> Máte SQLite $1, které je starší než minimálně vyžadovaná verze $2. SQLite nebude dostupné.",
@@ -65,12 +66,11 @@
"config-pcre-no-utf8": "<strong>Kritická chyba:</strong> PHP modul PCRE byl zřejmě přeložen bez podpory PCRE_UTF8.\nMediaWiki vyžaduje ke správné funkci podporu UTF-8.",
"config-memory-raised": "<code>memory_limit</code> v PHP byl nastaven na $1, zvýšen na $2.",
"config-memory-bad": "<strong>Upozornění:</strong> <code>memory_limit</code> je v PHP nastaven na $1.\nTo je pravděpodobně příliš málo.\nInstalace může selhat!",
- "config-xcache": "Je nainstalována [http://xcache.lighttpd.net/ XCache]",
"config-apc": "Je nainstalováno [http://www.php.net/apc APC]",
"config-apcu": "Je nainstalováno [http://www.php.net/apcu APCu]",
- "config-wincache": "Je nainstalována [http://www.iis.net/download/WinCacheForPhp WinCache]",
- "config-no-cache-apcu": "<strong>Upozornění:</strong> Nebylo nalezeno [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache], ani [http://www.iis.net/download/WinCacheForPhp WinCache].\nKešování objektů bude vypnuto.",
- "config-mod-security": "<strong>Upozornění:</strong> váš webový server má zapnuto [http://modsecurity.org/ mod_security]/mod_security2. Mnoho běžných konfigurací bude způsobovat potíže MediaWiki a dalším programům, které umožňují ukládat libovolný obsah.\nPokud je to možné, mělo by se to vypnout. Jinak se v případě, že narazíte na náhodné chyby, podívejte do [http://modsecurity.org/documentation/ dokumentace mod_security] nebo kontaktujte technickou podporu vašeho poskytovatele.",
+ "config-wincache": "Je nainstalováno [https://www.iis.net/download/WinCacheForPhp WinCache]",
+ "config-no-cache-apcu": "<strong>Upozornění:</strong> Nebylo nalezeno [http://www.php.net/apcu APCu], \nani [http://www.iis.net/download/WinCacheForPhp WinCache].\nCachování objektů není povoleno.",
+ "config-mod-security": "<strong>Upozornění:</strong> váš webový server má zapnuto [https://modsecurity.org/ mod_security]/mod_security2. Mnoho běžných konfigurací bude způsobovat potíže MediaWiki a dalším programům, které umožňují ukládat libovolný obsah.\nPokud je to možné, mělo by se to vypnout. Jinak se v případě, že narazíte na náhodné chyby, podívejte do [https://modsecurity.org/documentation/ dokumentace mod_security] nebo kontaktujte technickou podporu vašeho poskytovatele.",
"config-diff3-bad": "Nebyl nalezen GNU diff3.",
"config-git": "Nalezen software pro správu verzí Git: <code>$1</code>.",
"config-git-bad": "Software pro správu verzí Git nebyl nalezen.",
@@ -228,7 +228,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 nebo novější",
"config-license-pd": "Volné dílo",
"config-license-cc-choose": "Zvolit vlastní licenci Creative Commons",
- "config-license-help": "Mnoho veřejných wiki všechny příspěvky zveřejňuje pod některou [http://freedomdefined.org/Definition/Cs svobodnou licencí].\nTo pomáhá vytvořit duch komunitního vlastnictví a povzbuzuje dlouhodobé přispívání.\nTo obecně není potřeba u soukromé nebo firemní wiki.\n\nPokud chcete být schopni používat text z Wikipedie a chcete, aby Wikipedie byla schopna přijímat text okopírovaný z vaší wiki, měli byste zvolit <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nDříve Wikipedie používala GNU Free Documentation License.\nGFDL je platná licence, ale složité jí porozumět.\nTaké je komplikované používat obsah licencovaný pod GFDL.",
+ "config-license-help": "Mnoho veřejných wiki všechny příspěvky zveřejňuje pod některou [https://freedomdefined.org/Definition/Cs svobodnou licencí].\nTo pomáhá vytvořit duch komunitního vlastnictví a povzbuzuje dlouhodobé přispívání.\nTo obecně není potřeba u soukromé nebo firemní wiki.\n\nPokud chcete být schopni používat text z Wikipedie a chcete, aby Wikipedie byla schopna přijímat text okopírovaný z vaší wiki, měli byste zvolit <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nDříve Wikipedie používala GNU Free Documentation License.\nGFDL je platná licence, ale složité jí porozumět.\nTaké je komplikované používat obsah licencovaný pod GFDL.",
"config-email-settings": "Nastavení e-mailu",
"config-enable-email": "Zapnout odchozí e-mail",
"config-enable-email-help": "Pokud chcete, aby e-mail fungoval, je potřeba správně nakonfigurovat [http://www.php.net/manual/en/mail.configuration.php e-mailová nastavení PHP].\nPokud nechcete žádné e-mailové funkce, můžete je zde vypnout.",
@@ -258,7 +258,7 @@
"config-cache-options": "Nastavení cachování objektů:",
"config-cache-help": "Cachování objektů se používá pro vylepšení rychlosti MediaWiki tím, že se cachují často používaná data.\nStředním až velkým serverům se jeho zapnutí důrazně doporučuje, i menší servery pocítí jeho výhody.",
"config-cache-none": "Bez cachování (o žádnou funkcionalitu nepřijdete, na větších wiki však může dojít ke zhoršení rychlosti)",
- "config-cache-accel": "Cachování PHP objektů (APC, APCu, XCache nebo WinCache)",
+ "config-cache-accel": "Cachování PHP objektů (APC, APCu nebo WinCache)",
"config-cache-memcached": "Použít Memcached (vyžaduje další nastavení a konfiguraci)",
"config-memcached-servers": "Servery Memcached:",
"config-memcached-help": "Seznam IP adres, které se mají používat pro Memcached.\nUveďte jednu na řádek spolu s portem. Například:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -314,6 +314,7 @@
"config-install-mainpage-failed": "Nepodařilo se vložit hlavní stranu: $1",
"config-install-done": "<strong>Gratulujeme!</strong>\nNainstalovali jste MediaWiki.\n\nInstalátor vytvořil soubor <code>LocalSettings.php</code>.\nTen obsahuje veškerou vaši konfiguraci.\n\nBudete si ho muset stáhnout a uložit do základního adresáře vaší instalace wiki (do stejného adresáře jako soubor index.php). Stažení souboru se mělo spustit automaticky.\n\nPokud se vám stažení nenabídlo nebo jste ho zrušili, můžete ho spustit znovu kliknutím na následující odkaz:\n\n$3\n\n<strong>Poznámka</strong>: Pokud to neuděláte hned, tento vygenerovaný konfigurační soubor nebude později dostupný, pokud instalaci opustíte, aniž byste si ho stáhli.\n\nAž to dokončíte, můžete <strong>[$2 vstoupit do své wiki]</strong>.",
"config-install-done-path": "<strong>Gratulujeme!</strong>\nNainstalovali jste MediaWiki.\n\nInstalátor vytvořil soubor <code>LocalSettings.php</code>.\nTen obsahuje veškerou vaši konfiguraci.\n\nBudete si ho muset stáhnout a uložit do <code>$4</code>. Stažení souboru se mělo spustit automaticky.\n\nPokud se vám stažení nenabídlo nebo jste ho zrušili, můžete ho spustit znovu kliknutím na následující odkaz:\n\n$3\n\n<strong>Poznámka:</strong> Pokud to neuděláte hned, tento vygenerovaný konfigurační soubor nebude později dostupný, pokud instalaci opustíte, aniž byste si ho stáhli.\n\nAž to dokončíte, můžete <strong>[$2 vstoupit do své wiki]</strong>.",
+ "config-install-success": "MediaWiki byla úspěšně nainstalována. Nyní můžete\nnavštívit <$1$2>, abyste si prohlédli svou novou wiki.\nPokud máte dotazy, podívejte se do našeho seznamu často kladených otázek:\n<https://www.mediawiki.org/wiki/Manual:FAQ> nebo použijte jedno\nz tam odkazovaných diskusních fór.",
"config-download-localsettings": "Stáhnout <code>LocalSettings.php</code>",
"config-help": "nápověda",
"config-help-tooltip": "rozbalíte kliknutím",
diff --git a/www/wiki/includes/installer/i18n/csb.json b/www/wiki/includes/installer/i18n/csb.json
index defaf1bb..34cf29fe 100644
--- a/www/wiki/includes/installer/i18n/csb.json
+++ b/www/wiki/includes/installer/i18n/csb.json
@@ -34,10 +34,9 @@
"config-env-php": "PHP $1 je wjinastalowóné",
"config-env-hhvm": "HHVM $1 je wjinastalowóné",
"config-memory-raised": "Paraméter PHP <code>memory_limit</code> $1 òstôł zwikszony do $2.",
- "config-xcache": "[Http://trac.lighttpd.net/xcache/ XCache] je wjinstalowóny",
"config-apc": "[Http://www.php.net/apc APC] je wjinstalowóny",
"config-apcu": "[http://www.php.net/apcu APCu] je wjinstalowóny",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] je wjinstalowóny",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] je wjinstalowóny",
"config-diff3-bad": "Felënk GNU diff3.",
"config-mysql-innodb": "InnoDB",
"config-mysql-myisam": "MyISAM",
diff --git a/www/wiki/includes/installer/i18n/de-ch.json b/www/wiki/includes/installer/i18n/de-ch.json
index fe4aa4c1..d307c8da 100644
--- a/www/wiki/includes/installer/i18n/de-ch.json
+++ b/www/wiki/includes/installer/i18n/de-ch.json
@@ -5,8 +5,8 @@
"Das Schäfchen"
]
},
- "config-copyright": "=== Lizenz und Nutzungsbedingungen ===\n\n$1\n\nDieses Programm ist freie Software, d. h. es kann, gemäss den Bedingungen der von der Free Software Foundation veröffentlichten ''GNU General Public License'', weiterverteilt und/oder modifiziert werden. Dabei kann die Version 2, oder nach eigenem Ermessen, jede neuere Version der Lizenz verwendet werden.\n\nDieses Programm wird in der Hoffnung verteilt, dass es nützlich sein wird, allerdings '''ohne jegliche Garantie''' und sogar ohne die implizierte Garantie einer '''Marktgängigkeit''' oder '''Eignung für einen bestimmten Zweck'''. Hierzu sind weitere Hinweise in der ''GNU General Public License'' enthalten.\n\nEine <doclink href=Copying>Kopie der GNU General Public License</doclink> sollte zusammen mit diesem Programm verteilt worden sein. Sofern dies nicht der Fall war, kann eine Kopie bei der Free Software Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, schriftlich angefordert oder auf deren Website [http://www.gnu.org/copyleft/gpl.html online gelesen] werden.",
- "config-unicode-pure-php-warning": "'''Warnung:''' Die [http://pecl.php.net/intl PECL-Erweiterung intl] ist für die Unicode-Normalisierung nicht verfügbar, so dass stattdessen die langsame pure-PHP-Implementierung genutzt wird.\nSofern eine Website mit grosser Benutzeranzahl betrieben wird, sollten weitere Informationen auf der Webseite [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-Normalisierung (en)] gelesen werden.",
+ "config-copyright": "=== Lizenz und Nutzungsbedingungen ===\n\n$1\n\nDieses Programm ist freie Software, d. h. es kann, gemäss den Bedingungen der von der Free Software Foundation veröffentlichten ''GNU General Public License'', weiterverteilt und/oder modifiziert werden. Dabei kann die Version 2, oder nach eigenem Ermessen, jede neuere Version der Lizenz verwendet werden.\n\nDieses Programm wird in der Hoffnung verteilt, dass es nützlich sein wird, allerdings '''ohne jegliche Garantie''' und sogar ohne die implizierte Garantie einer '''Marktgängigkeit''' oder '''Eignung für einen bestimmten Zweck'''. Hierzu sind weitere Hinweise in der ''GNU General Public License'' enthalten.\n\nEine <doclink href=Copying>Kopie der GNU General Public License</doclink> sollte zusammen mit diesem Programm verteilt worden sein. Sofern dies nicht der Fall war, kann eine Kopie bei der Free Software Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, schriftlich angefordert oder auf deren Website [https://www.gnu.org/copyleft/gpl.html online gelesen] werden.",
+ "config-unicode-pure-php-warning": "'''Warnung:''' Die [https://pecl.php.net/intl PECL-Erweiterung intl] ist für die Unicode-Normalisierung nicht verfügbar, so dass stattdessen die langsame pure-PHP-Implementierung genutzt wird.\nSofern eine Website mit grosser Benutzeranzahl betrieben wird, sollten weitere Informationen auf der Webseite [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-Normalisierung (en)] gelesen werden.",
"config-uploads-not-safe": "'''Warnung:''' Das Standardverzeichnis für hochgeladene Dateien <code>$1</code> ist für die willkürliche Ausführung von Skripten anfällig.\nObwohl MediaWiki die hochgeladenen Dateien auf Sicherheitsrisiken überprüft, wird dennoch dringend empfohlen, diese [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security Sicherheitslücke] zu schliessen, bevor das Hochladen von Dateien aktiviert wird.",
- "config-license-help": "Viele öffentliche Wikis publizieren alle Beiträge unter einer [http://freedomdefined.org/Definition/De freien Lizenz.]\nDies trägt dazu bei, ein Gefühl von Gemeinschaft zu schaffen, und ermutigt zu längerfristiger Mitarbeit.\nHingegen ist im Allgemeinen eine freie Lizenz auf geschlossenen Wikis nicht notwendig.\n\nSofern man Texte aus der Wikipedia verwenden möchte und umgekehrt, sollte die ''Creative-Commons''-Lizenz „Namensnennung – Weitergabe unter gleichen Bedingungen“ gewählt werden.\n\nDie Wikipedia nutzte vormals die GNU-Lizenz für freie Dokumentation (GFDL).\nDie GFDL ist eine gültige Lizenz, die allerdings schwer zu verstehen ist.\nEs ist zudem schwierig, gemäss dieser Lizenz lizenzierte Inhalte wiederzuverwenden."
+ "config-license-help": "Viele öffentliche Wikis publizieren alle Beiträge unter einer [https://freedomdefined.org/Definition/De freien Lizenz.]\nDies trägt dazu bei, ein Gefühl von Gemeinschaft zu schaffen, und ermutigt zu längerfristiger Mitarbeit.\nHingegen ist im Allgemeinen eine freie Lizenz auf geschlossenen Wikis nicht notwendig.\n\nSofern man Texte aus der Wikipedia verwenden möchte und umgekehrt, sollte die ''Creative-Commons''-Lizenz „Namensnennung – Weitergabe unter gleichen Bedingungen“ gewählt werden.\n\nDie Wikipedia nutzte vormals die GNU-Lizenz für freie Dokumentation (GFDL).\nDie GFDL ist eine gültige Lizenz, die allerdings schwer zu verstehen ist.\nEs ist zudem schwierig, gemäss dieser Lizenz lizenzierte Inhalte wiederzuverwenden."
}
diff --git a/www/wiki/includes/installer/i18n/de.json b/www/wiki/includes/installer/i18n/de.json
index ca649dca..00005d33 100644
--- a/www/wiki/includes/installer/i18n/de.json
+++ b/www/wiki/includes/installer/i18n/de.json
@@ -56,14 +56,14 @@
"config-help-restart": "Sollen alle bereits eingegebenen Daten gelöscht und der Installationsvorgang erneut gestartet werden?",
"config-restart": "Ja, erneut starten",
"config-welcome": "=== Prüfung der Installationsumgebung ===\nDie Basisprüfungen werden jetzt durchgeführt, um festzustellen, ob die Installationsumgebung für MediaWiki geeignet ist.\nNotiere diese Informationen und gib sie an, sofern du Hilfe beim Installieren benötigst.",
- "config-copyright": "=== Lizenz und Nutzungsbedingungen ===\n\n$1\n\nDieses Programm ist freie Software, d. h. es kann, gemäß den Bedingungen der von der Free Software Foundation veröffentlichten ''GNU General Public License'', weiterverteilt und/oder modifiziert werden. Dabei kann die Version 2, oder nach eigenem Ermessen, jede neuere Version der Lizenz verwendet werden.\n\nDieses Programm wird in der Hoffnung verteilt, dass es nützlich sein wird, allerdings '''ohne jegliche Garantie''' und sogar ohne die implizierte Garantie einer '''Marktgängigkeit''' oder '''Eignung für einen bestimmten Zweck'''. Hierzu sind weitere Hinweise in der ''GNU General Public License'' enthalten.\n\nEine <doclink href=Copying>Kopie der GNU General Public License</doclink> sollte zusammen mit diesem Programm verteilt worden sein. Sofern dies nicht der Fall war, kann eine Kopie bei der Free Software Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, schriftlich angefordert oder auf deren Website [http://www.gnu.org/copyleft/gpl.html online gelesen] werden.",
+ "config-copyright": "=== Lizenz und Nutzungsbedingungen ===\n\n$1\n\nDieses Programm ist freie Software, d. h. es kann, gemäß den Bedingungen der von der Free Software Foundation veröffentlichten ''GNU General Public License'', weiterverteilt und/oder modifiziert werden. Dabei kann die Version 2, oder nach eigenem Ermessen, jede neuere Version der Lizenz verwendet werden.\n\nDieses Programm wird in der Hoffnung verteilt, dass es nützlich sein wird, allerdings '''ohne jegliche Garantie''' und sogar ohne die implizierte Garantie einer '''Marktgängigkeit''' oder '''Eignung für einen bestimmten Zweck'''. Hierzu sind weitere Hinweise in der ''GNU General Public License'' enthalten.\n\nEine <doclink href=Copying>Kopie der GNU General Public License</doclink> sollte zusammen mit diesem Programm verteilt worden sein. Sofern dies nicht der Fall war, kann eine Kopie bei der Free Software Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, schriftlich angefordert oder auf deren Website [https://www.gnu.org/copyleft/gpl.html online gelesen] werden.",
"config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/de Website von MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/de Benutzer­anleitung]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/de Administratoren­anleitung]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/de Häufig gestellte Fragen]\n----\n* <doclink href=Readme>Lies mich</doclink>\n* <doclink href=ReleaseNotes>Versions­informationen</doclink>\n* <doclink href=Copying>Lizenz­bestimmungen</doclink>\n* <doclink href=UpgradeDoc>Aktualisierung</doclink>",
"config-env-good": "Die Installationsumgebung wurde geprüft.\nMediaWiki kann installiert werden.",
"config-env-bad": "Die Installationsumgebung wurde geprüft.\nMediaWiki kann nicht installiert werden.",
"config-env-php": "Die Skriptsprache „PHP“ ($1) ist installiert.",
"config-env-hhvm": "HHVM $1 ist installiert.",
- "config-unicode-using-intl": "Zur Unicode-Normalisierung wird die [http://pecl.php.net/intl PECL-Erweiterung intl] eingesetzt.",
- "config-unicode-pure-php-warning": "'''Warnung:''' Die [http://pecl.php.net/intl PECL-Erweiterung intl] ist für die Unicode-Normalisierung nicht verfügbar, so dass stattdessen die langsame pure-PHP-Implementierung genutzt wird.\nSofern eine Website mit großer Benutzeranzahl betrieben wird, sollten weitere Informationen auf der Webseite [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-Normalisierung (en)] gelesen werden.",
+ "config-unicode-using-intl": "Zur Unicode-Normalisierung wird die [https://pecl.php.net/intl PECL-Erweiterung intl] eingesetzt.",
+ "config-unicode-pure-php-warning": "'''Warnung:''' Die [https://pecl.php.net/intl PECL-Erweiterung intl] ist für die Unicode-Normalisierung nicht verfügbar, so dass stattdessen die langsame pure-PHP-Implementierung genutzt wird.\nSofern eine Website mit großer Benutzeranzahl betrieben wird, sollten weitere Informationen auf der Webseite [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-Normalisierung (en)] gelesen werden.",
"config-unicode-update-warning": "'''Warnung:''' Die installierte Version des Unicode-Normalisierungswrappers nutzt einer ältere Version der Bibliothek des [http://site.icu-project.org/ ICU-Projekts].\nDiese sollte [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations aktualisiert] werden, sofern auf die Verwendung von Unicode Wert gelegt wird.",
"config-no-db": "Es konnte kein adäquater Datenbanktreiber gefunden werden. Es muss daher ein Datenbanktreiber für PHP installiert werden.\n{{PLURAL:$2|Das folgende Datenbanksystem wird|Die folgenden Datenbanksysteme werden}} unterstützt: $1\n\nWenn du PHP selbst kompiliert hast, konfiguriere es erneut mit einem aktivierten Datenbankclient, zum Beispiel durch Verwendung von <code>./configure --with-mysqli</code>.\nWenn du PHP von einem Debian- oder Ubuntu-Paket installiert hast, dann musst du auch beispielsweise das <code>php5-mysql</code>-Paket installieren.",
"config-outdated-sqlite": "'''Warnung:''' SQLite $1 ist installiert. Allerdings benötigt MediaWiki SQLite $2 oder höher. SQLite wird daher nicht verfügbar sein.",
@@ -72,12 +72,11 @@
"config-pcre-no-utf8": "'''Fataler Fehler:''' Das PHP-Modul PCRE scheint ohne PCRE_UTF8-Unterstützung kompiliert worden zu sein.\nMediaWiki benötigt die UTF-8-Unterstützung, um fehlerfrei lauffähig zu sein.",
"config-memory-raised": "Der PHP-Parameter <code>memory_limit</code> betrug $1 und wurde auf $2 erhöht.",
"config-memory-bad": "'''Warnung:''' Der PHP-Parameter <code>memory_limit</code> beträgt $1.\nDieser Wert ist wahrscheinlich zu niedrig.\nDer Installationsvorgang könnte eventuell scheitern!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] ist installiert",
"config-apc": "[http://www.php.net/apc APC] ist installiert",
"config-apcu": "[http://www.php.net/apcu APCu] ist installiert",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] ist installiert",
- "config-no-cache-apcu": "<strong>Warnung:</strong> [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] oder [http://www.iis.net/download/WinCacheForPhp WinCache] konnten nicht gefunden werden.\nDer Objektcache ist nicht aktiviert.",
- "config-mod-security": "'''Warnung:''' Auf dem Webserver wurde [http://modsecurity.org/ ModSecurity] aktiviert. Sofern falsch konfiguriert, kann dies zu Problemen mit MediaWiki sowie anderer Software auf dem Server führen und es Benutzern ermöglichen, beliebige Inhalte im Wiki einzustellen.\nFür weitere Informationen empfehlen wir die [http://modsecurity.org/documentation/ Dokumentation zu ModSecurity] oder den Kontakt zum Hoster, sofern Fehler auftreten.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] ist installiert",
+ "config-no-cache-apcu": "<strong>Warnung:</strong> [http://www.php.net/apcu APCu] oder [http://www.iis.net/download/WinCacheForPhp WinCache] konnten nicht gefunden werden.\nDer Objektcache ist nicht aktiviert.",
+ "config-mod-security": "'''Warnung:''' Auf dem Webserver wurde [https://modsecurity.org/ ModSecurity] aktiviert. Sofern falsch konfiguriert, kann dies zu Problemen mit MediaWiki sowie anderer Software auf dem Server führen und es Benutzern ermöglichen, beliebige Inhalte im Wiki einzustellen.\nFür weitere Informationen empfehlen wir die [https://modsecurity.org/documentation/ Dokumentation zu ModSecurity] oder den Kontakt zum Hoster, sofern Fehler auftreten.",
"config-diff3-bad": "GNU diff3 wurde nicht gefunden.",
"config-git": "Die Versionsverwaltungssoftware „Git“ wurde gefunden: <code>$1</code>.",
"config-git-bad": "Die Versionsverwaltungssoftware „Git“ wurde nicht gefunden.",
@@ -234,8 +233,8 @@
"config-license-cc-0": "''Creative Commons'' „Zero“ (Gemeinfreiheit)",
"config-license-gfdl": "GNU-Lizenz für freie Dokumentation 1.3 oder höher",
"config-license-pd": "Gemeinfreiheit",
- "config-license-cc-choose": "Eine benutzerdefinierte Creative-Commons-Lizenz auswählen",
- "config-license-help": "Viele öffentliche Wikis publizieren alle Beiträge unter einer [http://freedomdefined.org/Definition/De freien Lizenz.]\nDies trägt dazu bei, ein Gefühl von Gemeinschaft zu schaffen, und ermutigt zu längerfristiger Mitarbeit.\nHingegen ist im Allgemeinen eine freie Lizenz auf geschlossenen Wikis nicht notwendig.\n\nSofern man Texte aus der Wikipedia verwenden möchte und umgekehrt, sollte die Lizenz {{int:config-license-cc-by-sa}} gewählt werden.\n\nDie Wikipedia nutzte vormals die GNU-Lizenz für freie Dokumentation (GFDL).\nDie GFDL ist eine gültige Lizenz, die allerdings schwer zu verstehen ist.\nEs ist zudem schwierig, gemäß dieser Lizenz lizenzierte Inhalte wiederzuverwenden.",
+ "config-license-cc-choose": "Eine andere Creative-Commons-Lizenz auswählen",
+ "config-license-help": "Viele öffentliche Wikis publizieren alle Beiträge unter einer [https://freedomdefined.org/Definition/De freien Lizenz.]\nDies trägt dazu bei, ein Gefühl von Gemeinschaft zu schaffen, und ermutigt zu längerfristiger Mitarbeit.\nHingegen ist im Allgemeinen eine freie Lizenz auf geschlossenen Wikis nicht notwendig.\n\nSofern man Texte aus der Wikipedia verwenden möchte und umgekehrt, sollte die Lizenz {{int:config-license-cc-by-sa}} gewählt werden.\n\nDie Wikipedia nutzte vormals die GNU-Lizenz für freie Dokumentation (GFDL).\nDie GFDL ist eine gültige Lizenz, die allerdings schwer zu verstehen ist.\nEs ist zudem schwierig, gemäß dieser Lizenz lizenzierte Inhalte wiederzuverwenden.",
"config-email-settings": "E-Mail-Einstellungen",
"config-enable-email": "Ausgehende E-Mails ermöglichen",
"config-enable-email-help": "Sofern die E-Mail-Funktionen genutzt werden sollen, müssen die entsprechenden [http://www.php.net/manual/en/mail.configuration.php PHP-E-Mail-Einstellungen] richtig konfiguriert werden.\nFür den Fall, dass die E-Mail-Funktionen nicht benötigt werden, können sie hier deaktiviert werden.",
@@ -265,7 +264,7 @@
"config-cache-options": "Einstellungen für die Zwischenspeicherung von Objekten:",
"config-cache-help": "Das Objektcaching wird dazu genutzt, die Geschwindigkeit von MediaWiki zu verbessern, indem häufig genutzte Daten zwischengespeichert werden.\nEs wird sehr empfohlen, es für mittelgroße bis große Wikis zu nutzen, aber auch für kleine Wikis ergeben sich erkennbare Geschwindigkeitsverbesserungen.",
"config-cache-none": "Kein Objektcaching (es wird keine Funktion entfernt, allerdings kann dies die Leistungsfähigkeit größerer Wikis negativ beeinflussen)",
- "config-cache-accel": "Objektcaching von PHP (APC, APCu, XCache oder WinCache)",
+ "config-cache-accel": "Objektcaching von PHP (APC, APCu oder WinCache)",
"config-cache-memcached": "Memcached Cacheserver (erfordert einen zusätzlichen Installationsvorgang mitsamt Konfiguration)",
"config-memcached-servers": "Memcached Cacheserver",
"config-memcached-help": "Liste der für Memcached nutzbaren IP-Adressen.\nEs sollte eine je Zeile mitsamt des vorgesehenen Ports angegeben werden. Beispiele:\n127.0.0.1:11211 oder\n192.168.1.25:1234 usw.",
@@ -321,6 +320,7 @@
"config-install-mainpage-failed": "Die Hauptseite konnte nicht erstellt werden: $1",
"config-install-done": "'''Herzlichen Glückwunsch!'''\nMediaWiki wurde erfolgreich installiert.\n\nDas Installationsprogramm hat die Datei <code>LocalSettings.php</code> erzeugt.\nSie enthält alle vorgenommenen Konfigurationseinstellungen.\n\nDiese Datei muss nun heruntergeladen und anschließend in das Stammverzeichnis der MediaWiki-Installation hochgeladen werden. Dies ist dasselbe Verzeichnis, in dem sich auch die Datei <code>index.php</code> befindet. Das Herunterladen sollte inzwischen automatisch gestartet worden sein.\n\nSofern dies nicht der Fall war, oder das Herunterladen unterbrochen wurde, kann der Vorgang durch einen Klick auf den folgenden Link erneut gestartet werden:\n\n$3\n\n'''Hinweis:''' Die Konfigurationsdatei sollte jetzt unbedingt heruntergeladen werden. Sie wird nach Beenden des Installationsprogramms, nicht mehr zur Verfügung stehen.\n\nSobald alles erledigt wurde, kann auf das '''[$2 Wiki zugegriffen werden]'''. Wir wünschen viel Spaß und Erfolg mit dem Wiki.",
"config-install-done-path": "<strong>Herzlichen Glückwunsch!</strong>\nDu hast MediaWiki installiert.\n\nDas Installationsprogramm hat eine Datei „<code>LocalSettings.php</code>“ erzeugt.\nSie enthält deine gesamte Konfiguration.\n\nDu musst sie herunterladen und unter <code>$4</code> ablegen. Der Download sollte automatisch gestartet sein.\n\nFalls der Download nicht angeboten wird oder du ihn abgebrochen hast, kannst du ihn durch Anklicken des folgenden Links neu starten:\n\n$3\n\n<strong>Hinweis:</strong> Falls du dies jetzt nicht tust, wird die erzeugte Konfigurationsdatei später nicht verfügbar sein, wenn du die Installation ohne Herunterladen verlässt.\n\nBei Fertigstellung kannst du <strong>[$2 dein Wiki aufrufen]</strong>.",
+ "config-install-success": "MediaWiki wurde erfolgreich installiert. Du kannst jetzt\n<$1$2> aufrufen, um dein Wiki anzusehen.\nFalls du Fragen hast, lies unsere Liste der häufig gestellten Fragen:\n<https://www.mediawiki.org/wiki/Manual:FAQ> oder benutze eines der\nSupport-Foren, die auf dieser Seite verlinkt sind.",
"config-download-localsettings": "<code>LocalSettings.php</code> herunterladen",
"config-help": "Hilfe",
"config-help-tooltip": "Zum Expandieren klicken",
diff --git a/www/wiki/includes/installer/i18n/diq.json b/www/wiki/includes/installer/i18n/diq.json
index 68b47206..26cdb599 100644
--- a/www/wiki/includes/installer/i18n/diq.json
+++ b/www/wiki/includes/installer/i18n/diq.json
@@ -86,7 +86,7 @@
"config-extensions": "Olekeni",
"config-skins": "Temey",
"config-install-step-done": "qeyd ke",
- "config-install-step-failed": "nêbı",
+ "config-install-step-failed": "ebe ser nêkewt",
"config-install-schema": "Şema dek",
"config-install-pg-commit": "Vırnayışa cemaati",
"config-install-tables": "Tabloy dek",
diff --git a/www/wiki/includes/installer/i18n/dty.json b/www/wiki/includes/installer/i18n/dty.json
index a986e009..896a453e 100644
--- a/www/wiki/includes/installer/i18n/dty.json
+++ b/www/wiki/includes/installer/i18n/dty.json
@@ -41,12 +41,12 @@
"config-help-restart": "Do you want to clear all saved data that you have entered and restart the installation process?",
"config-restart": "हुन्छ, पुनः सुचारू गद्दे",
"config-welcome": "=== Environmental checks ===\nBasic checks will now be performed to see if this environment is suitable for MediaWiki installation.\nRemember to include this information if you seek support on how to complete the installation.",
- "config-copyright": "=== Copyright and Terms ===\n\n$1\n\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but <strong>without any warranty</strong>; without even the implied warranty of <strong>merchantability</strong> or <strong>fitness for a particular purpose</strong>.\nSee the GNU General Public License for more details.\n\nYou should have received <doclink href=Copying>a copy of the GNU General Public License</doclink> along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, or [http://www.gnu.org/copyleft/gpl.html read it online].",
+ "config-copyright": "=== Copyright and Terms ===\n\n$1\n\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but <strong>without any warranty</strong>; without even the implied warranty of <strong>merchantability</strong> or <strong>fitness for a particular purpose</strong>.\nSee the GNU General Public License for more details.\n\nYou should have received <doclink href=Copying>a copy of the GNU General Public License</doclink> along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, or [https://www.gnu.org/copyleft/gpl.html read it online].",
"config-sidebar": "* [https://www.mediawiki.org MediaWiki home]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents User's Guide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administrator's Guide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Read me</doclink>\n* <doclink href=ReleaseNotes>Release notes</doclink>\n* <doclink href=Copying>Copying</doclink>\n* <doclink href=UpgradeDoc>Upgrading</doclink>",
"config-env-good": "The environment has been checked.\nYou can install MediaWiki.",
"config-env-bad": "The environment has been checked.\nYou cannot install MediaWiki.",
"config-env-php": "PHP $1 is installed.",
"config-env-hhvm": "HHVM $1 स्थापना गरिएको छ ।",
- "config-unicode-using-intl": "Using the [http://pecl.php.net/intl intl PECL extension] for Unicode normalization.",
- "config-unicode-pure-php-warning": "<strong>Warning:</strong> The [http://pecl.php.net/intl intl PECL extension] is not available to handle Unicode normalization, falling back to slow pure-PHP implementation.\nIf you run a high-traffic site, you should read a little on [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization]."
+ "config-unicode-using-intl": "Using the [https://pecl.php.net/intl intl PECL extension] for Unicode normalization.",
+ "config-unicode-pure-php-warning": "<strong>Warning:</strong> The [https://pecl.php.net/intl intl PECL extension] is not available to handle Unicode normalization, falling back to slow pure-PHP implementation.\nIf you run a high-traffic site, you should read a little on [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization]."
}
diff --git a/www/wiki/includes/installer/i18n/el.json b/www/wiki/includes/installer/i18n/el.json
index f9bab3ed..39c84ceb 100644
--- a/www/wiki/includes/installer/i18n/el.json
+++ b/www/wiki/includes/installer/i18n/el.json
@@ -9,7 +9,9 @@
"Stam.nikos",
"Giorgos456",
"Badseed",
- "Macofe"
+ "Macofe",
+ "KATRINE1992",
+ "Nikosgranturismogt"
]
},
"config-desc": "Το πρόγραμμα εγκατάστασης για το MediaWiki",
@@ -48,46 +50,56 @@
"config-page-existingwiki": "Υπάρχον wiki",
"config-help-restart": "Θέλετε να καταργήσετε όλα τα αποθηκευμένα δεδομένα που έχετε εισαγάγει και να επανεκκινήσετε τη διαδικασία εγκατάστασης;",
"config-restart": "Ναι, επανεκκίνηση",
- "config-welcome": "=== Περιβαλλοντικοί έλεγχοι ===\nΤώρα θα γίνουν βασικοί έλεγχοι για να δούμε αν αυτό το περιβάλλον είναι κατάλληλο για την εγκατάσταση του MediaWiki.\nΘυμηθείτε να συμπεριλάβετε αυτές τις πληροφορίες εάν αναζητήσετε υποστήριξη για το πώς να ολοκληρώσετε την εγκατάσταση.",
- "config-copyright": "=== Πνευματικά δικαιώματα και Όροι ===\n\n$1\n\nΑυτό το πρόγραμμα είναι ελεύθερο λογισμικό• μπορείτε να το αναδιανείμετε ή και να το τροποποιήσετε υπό τους όρους της Γενικής Άδειας Δημόσιας Χρήσης GNU, όπως αυτή δημοσιεύεται από το Ίδρυμα Ελεύθερου Λογισμικού• είτε της έκδοσης 2 της Άδειας, είτε (κατά την επιλογή σας) οποιασδήποτε μεταγενέστερης έκδοσης.\n\nΑυτό το πρόγραμμα διανέμεται με την ελπίδα ότι θα είναι χρήσιμο, αλλά <strong>χωρίς καμία εγγύηση</strong>• χωρίς καν την υπονοούμενη εγγύηση της <strong>εμπορευσιμότητας</strong> ή της <strong>καταλληλοτότητας για συγκεκριμένο σκοπό</strong>.\nΔείτε την Γενική Άδεια Δημόσιας Χρήσης GNU για περισσότερες λεπτομέρειες.\n\nΘα πρέπει να έχετε λάβει <doclink href=\"Copying\">ένα αντίγραφο της Γενικής Άδειας Δημόσιας Χρήσης GNU</doclink> μαζί με αυτό το πρόγραμμα• αν όχι, γράψτε στο Free Software Foundation,\n51 Franklin Street, Fifth Floor,\nBoston, MA 02110-1335\nUSA ή [http://www.gnu.org/copyleft/gpl.html διαβάστε online].",
+ "config-welcome": "=== Έλεγχοι του περιβάλλοντος ===\nΤώρα θα γίνουν βασικοί έλεγχοι για να δούμε αν αυτό το περιβάλλον είναι κατάλληλο για την εγκατάσταση του MediaWiki.\nΘυμηθείτε να συμπεριλάβετε αυτές τις πληροφορίες εάν αναζητήσετε υποστήριξη για να ολοκληρώσετε την εγκατάσταση.",
+ "config-copyright": "=== Πνευματικά δικαιώματα και όροι ===\n\n$1\n\nΑυτό το πρόγραμμα είναι ελεύθερο λογισμικό· μπορείτε να το αναδιανείμετε ή/και να το τροποποιήσετε υπό τους όρους της Γενικής Άδειας Δημόσιας Χρήσης GNU, όπως αυτή δημοσιεύεται από το Ίδρυμα Ελεύθερου Λογισμικού· είτε της έκδοσης 2 της Άδειας, είτε (κατ' επιλογήν σας) οποιασδήποτε μεταγενέστερης έκδοσης.\n\nΑυτό το πρόγραμμα διανέμεται με την ελπίδα ότι θα είναι χρήσιμο, αλλά <strong>χωρίς καμία εγγύηση</strong>· χωρίς καν τη σιωπηρή εγγύηση της <strong>εμπορευσιμότητας</strong> ή <strong>καταλληλότητας για συγκεκριμένο σκοπό</strong>.\nΔείτε τη Γενική Άδεια Δημόσιας Χρήσης GNU για περισσότερες λεπτομέρειες.\n\nΘα πρέπει να έχετε παραλάβει <doclink href=\"Copying\">ένα αντίγραφο της Γενικής Άδειας Δημόσιας Χρήσης GNU</doclink> μαζί με αυτό το πρόγραμμα· αν όχι, στείλτε ένα γράμμα στο Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, ή [https://www.gnu.org/copyleft/gpl.html διαβάστε το διαδικτυακά].",
"config-sidebar": "* [https://www.mediawiki.org Αρχική MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Οδηγός Χρήστη]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Οδηγός Διαχειριστή]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Συχνές ερωτήσεις]\n----\n* <doclink href=\"Readme\">Διαβάστε με</doclink>\n* <doclink href=\"ReleaseNotes\">Σημειώσεις έκδοσης</doclink>\n* <doclink href=\"Copying\">Αντιγραφή</doclink>\n* <doclink href=\"UpgradeDoc\">Αναβάθμιση</doclink>",
"config-env-good": "Το περιβάλλον έχει ελεγχθεί.\nΜπορείτε να εγκαταστήσετε το MediaWiki.",
"config-env-bad": "Το περιβάλλον έχει ελεγχθεί.\nΔεν μπορείτε να εγκαταστήσετε το MediaWiki.",
"config-env-php": "H PHP $1 είναι εγκατεστημένη.",
"config-env-hhvm": "Το HHVM $1 είναι εγκατεστημένο.",
- "config-unicode-using-intl": "Χρησιμοποιώντας την [http://pecl.php.net/intl επέκταση intl PECL] για κανονικοποίηση Unicode.",
- "config-unicode-pure-php-warning": "<strong>Προειδοποίηση:</strong> Η [http://pecl.php.net/intl επέκταση intl PECL] δεν είναι διαθέσιμη για να χειριστεί την κανονικοποίηση Unicode, επιστρέφουμε στην αργή αμιγώς PHP εφαρμογή.\nΕάν λειτουργείτε έναν ιστότοπο υψηλής επισκεψιμότητας, θα πρέπει να ρίξετε μια ματιά στην [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations κανονικοποίηση Unicode].",
+ "config-unicode-using-intl": "Χρησιμοποιείται η [https://pecl.php.net/intl επέκταση intl PECL] για κανονικοποίηση Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Προειδοποίηση:</strong> Η [https://pecl.php.net/intl επέκταση intl PECL] δεν είναι διαθέσιμη για να χειριστεί την κανονικοποίηση Unicode, επιστρέφουμε στην αργή αμιγώς PHP εφαρμογή.\nΕάν λειτουργείτε έναν ιστότοπο υψηλής επισκεψιμότητας, θα πρέπει να ρίξετε μια ματιά στην [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations κανονικοποίηση Unicode].",
"config-no-db": "Δεν βρέθηκε κάποιο κατάλληλο πρόγραμμα οδήγησης βάσης δεδομένων! Θα πρέπει να εγκαταστήσετε ένα πρόγραμμα οδήγησης βάσης δεδομένων για PHP.\nΟ παρακάτω {{PLURAL:$2|τύπος βάσης δεδομένων|τύποι βάσεων δεδομένων}} υποστηρίζονται: $1.\n\nΑν κάνετε compile την PHP μόνοι σας, ρυθμίστε ξανά τις παραμέτρους με κάποιον ενεργοποιημένο εξυπηρετητή βάσεων δεδομένων, για παράδειγμα, χρησιμοποιώντας την εντολή <code>./configure --with-mysqli</code>.\nΕάν έχετε εγκαταστήσει την PHP από κάποιο πακέτο στο Debian ή στο Ubuntu, τότε θα πρέπει να εγκαταστήσετε επίσης, για παράδειγμα, το πακέτο <code>php5-mysql</code>.",
"config-outdated-sqlite": "<strong>Προειδοποίηση:</strong> έχετε την SQLite έκδοση $1, που είναι χαμηλότερη από την ελάχιστη απαιτούμενη έκδοση $2. Η SQLite δεν θα είναι διαθέσιμη.",
"config-pcre-no-utf8": "<strong>Κρίσιμο:</strong> Το PCRE module της PHP φαίνεται να έχει μεταγλωττιστεί χωρίς υποστήριξη PCRE_UTF8.\nΓια τη σωστή λειτουργία του MediaWiki απαιτείται υποστήριξη UTF-8.",
"config-memory-raised": "Το <code>memory_limit</code> της PHP είναι $1 και αυξήθηκε σε $2.",
"config-memory-bad": "<strong>Προειδοποίηση:</strong> το <code>memory_limit</code> της PHP είναι $1.\nΑυτή η τιμή είναι πιθανώς πολύ χαμηλή.\n\nΗ εγκατάσταση ενδέχεται να αποτύχει!",
- "config-xcache": "[http://xcache.lighttpd.net/ Το XCache] είναι εγκατεστημένο",
"config-apc": "Το [http://www.php.net/apc APC] είναι εγκατεστημένο",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp Το WinCache] είναι εγκατεστημένο",
+ "config-apcu": "Το [http://www.php.net/apcu APCu] είναι εγκατεστημένο",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp Το WinCache] είναι εγκατεστημένο",
+ "config-no-cache-apcu": "<strong>Προειδοποίηση:</strong> Αποτυχία εύρεσης του [http://www.php.net/apcu APCu] ή του [http://www.iis.net/download/WinCacheForPhp WinCache].\nΗ αρχειοθέτηση αντικειμένων δεν έχει ενεργοποιηθεί.",
"config-diff3-bad": "Το GNU diff3 δεν βρέθηκε.",
- "config-git": "Βρέθηκε η Git έκδοση λογισμικού ελέγχου: <code>$1</code>.",
- "config-git-bad": "Η Git έκδοση του λογισμικού ελέγχου δεν βρέθηκε.",
+ "config-git": "Βρέθηκε το λογισμικό ελέγχου εκδόσεων Git: <code>$1</code>.",
+ "config-git-bad": "Το λογισμικό ελέγχου εκδόσεων Git δεν βρέθηκε.",
+ "config-imagemagick": "Βρέθηκε το ImageMagick: <code>$1</code>.\nΟι μικρογραφίες εικόνων θα ενεργοποιηθούν αν ενεργοποιήσετε το ανέβασμα αρχείων.",
+ "config-gd": "Βρέθηκε ενσωματωμένη η βιβλιοθήκη γραφικών GD.\nΟι μικρογραφίες εικόνων θα ενεργοποιηθούν αν ενεργοποιήσετε το ανέβασμα αρχείων.",
+ "config-no-scaling": "Δεν ήταν δυνατόν να βρεθεί η βιβλιοθήκη GD ή το ImageMagick.\nΟι μικρογραφίες εικόνων θα απενεργοποιηθούν.",
"config-no-uri": "<strong>Σφάλμα:</strong> Δεν ήταν δυνατό να καθοριστεί το τρέχον URI.\nΗ εγκατάσταση ματαιώθηκε.",
- "config-using-server": "Χρησιμοποιείται το όνομα διακομιστή \"<nowiki>$1</nowiki>\".",
- "config-using-uri": "Χρησιμοποιώντας την διεύθυνση URL του διακομιστή \"<nowiki>$1$2</nowiki>\".",
+ "config-no-cli-uri": "<strong>Προειδοποίηση:</strong> Δεν καθορίστηκε <code>--scriptpath</code>, χρησιμοποιείται η προεπιλογή: <code>$1</code>.",
+ "config-using-server": "Χρησιμοποιείται το όνομα διακομιστή «<nowiki>$1</nowiki>».",
+ "config-using-uri": "Χρησιμοποιείται η διεύθυνση URL του διακομιστή «<nowiki>$1$2</nowiki>».",
+ "config-uploads-not-safe": "<strong>Προειδοποίηση:</strong> Ο προεπιλεγμένος σας κατάλογος για ανέβασμα <code>$1</code> είναι ευάλωτος σε εκτέλεση αυθαίρετων σεναρίων ενεργειών.\nΑν και το MediaWiki ελέγχει για απειλές ασφαλείας όλα τα αρχεία που ανεβαίνουν, συνιστάται ιδιαίτερα να [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security κλείσετε αυτό το κενό ασφαλείας] πριν ενεργοποιήσετε το ανέβασμα αρχείων.",
+ "config-no-cli-uploads-check": "<strong>Προειδοποίηση:</strong> Ο προεπιλεγμένος σας κατάλογος για ανέβασμα (<code>$1</code>) δεν έχει ελεγχθεί για ευπάθεια σε εκτέλεση αυθαίρετων σεναρίων ενεργειών κατά τη διάρκεια της εγκατάστασης μέσω γραμμής εντολών.",
"config-brokenlibxml": "Το σύστημά σας έχει έναν συνδυασμό εκδόσεων της PHP και της libxml2 που είναι προβληματικός και μπορεί να προκαλέσει καταστροφή κρυμμένων στοιχείων στο MediaWiki και σε άλλες εφαρμογές ιστού.\nΑναβαθμίστε σε libxml2 2.7.3 ή μεταγενέστερη έκδοση ([https://bugs.php.net/bug.php?id=45996 bug που έχει καταχωριστεί για την PHP]).\nΗ εγκατάσταση ματαιώθηκε.",
"config-db-type": "Τύπος βάσης δεδομένων:",
"config-db-host": "Φιλοξενία βάσης δεδομένων:",
+ "config-db-host-help": "Εάν ο διακομιστής βάσης δεδομένων σας βρίσκεται σε διαφορετικό διακομιστή, εισαγάγετε εδώ το όνομα του κεντρικού υπολογιστή ή τη διεύθυνση IP.\n\nΕάν χρησιμοποιείτε μοιραζόμενη φιλοξενία του ιστοτόπου σας, ο πάροχος φιλοξενίας σας θα πρέπει να σας δίνει το σωστό όνομα κεντρικού υπολογιστή στην τεκμηρίωση του.\n\nΕάν εγκαθιστάτε σε διακομιστή Windows και χρησιμοποιείτε MySQL, το «localhost» μπορεί να μην λειτουργεί ως όνομα διακομιστή. Εάν δεν λειτουργεί, δοκιμάστε «127.0.0.1» ως τοπική διεύθυνση IP.\n\nΕάν χρησιμοποιείτε PostgreSQL, αφήστε αυτό το πεδίο κενό για να συνδεθείτε μέσω υποδοχής Unix.",
"config-db-host-oracle": "Βάση δεδομένων TNS:",
"config-db-wiki-settings": "Αναγνώριση αυτού του wiki",
"config-db-name": "Όνομα βάσης δεδομένων:",
- "config-db-name-help": "Επιλέξτε ένα όνομα που ταιριάζει στο wiki σας. Δεν πρέπει να περιέχει κενά διαστήματα.\n\nΕάν χρησιμοποιείτε κοινόχρηστο web hosting, ο πάροχος φιλοξενίας είτε θα σας δώσει ένα συγκεκριμένο όνομα βάσης δεδομένων για να χρησιμοποιήσετε ή θα σας δώσει τη δυνατότητα να δημιουργήσετε βάσεις δεδομένων μέσω ενός πίνακα ελέγχου.",
+ "config-db-name-help": "Επιλέξτε όνομα που να χαρακτηρίζει το wiki σας. Δεν πρέπει να περιέχει κενά διαστήματα.\n\nΕάν χρησιμοποιείτε μοιραζόμενη φιλοξενία του ιστοτόπου σας, ο πάροχος φιλοξενίας σας είτε θα σας δίνει να χρησιμοποιήσετε ένα συγκεκριμένο όνομα βάσης δεδομένων ή θα σας δίνει τη δυνατότητα να δημιουργείτε βάσεις δεδομένων μέσω κάποιου πίνακα ελέγχου.",
"config-db-name-oracle": "Σχήμα βάσης δεδομένων:",
"config-db-install-account": "Λογαριασμός χρήστη για την εγκατάσταση",
"config-db-username": "Όνομα χρήστη βάσης δεδομένων:",
- "config-db-password": "Κωδικός πρόσβασης βάσης δεδομένων:",
+ "config-db-password": "Συνθηματικό βάσης δεδομένων:",
+ "config-db-install-username": "Εισαγάγετε το όνομα χρήστη που θα χρησιμοποιηθεί για τη σύνδεση στη βάση δεδομένων κατά τη διάρκεια της διαδικασίας εγκατάστασης.\nΑυτό δεν είναι το όνομα χρήστη του λογαριασμού του MediaWiki· αυτό είναι το όνομα χρήστη για τη βάση δεδομένων σας.",
+ "config-db-install-password": "Εισαγάγετε το συνθηματικό που θα χρησιμοποιηθεί για τη σύνδεση στη βάση δεδομένων κατά τη διάρκεια της διαδικασίας εγκατάστασης.\nΑυτό δεν είναι το συνθηματικό του λογαριασμού του MediaWiki· αυτό είναι το συνθηματικό για τη βάση δεδομένων σας.",
"config-db-install-help": "Εισαγάγετε το όνομα χρήστη και τον κωδικό πρόσβασης που θα χρησιμοποιηθεί για τη σύνδεση με τη βάση δεδομένων κατά τη διάρκεια της διαδικασίας εγκατάστασης.",
"config-db-account-lock": "Χρησιμοποιήστε το ίδιο όνομα χρήστη και συνθηματικό κατά τη διάρκεια της κανονικής λειτουργίας",
"config-db-wiki-account": "Λογαριασμός χρήστη για κανονική λειτουργία",
"config-db-wiki-help": "Πληκτρολογήστε το όνομα χρήστη και τον κωδικό πρόσβασης που θα χρησιμοποιηθεί για τη σύνδεση με τη βάση δεδομένων κατά τη διάρκεια της κανονικής λειτουργίας του wiki.\nΕάν ο λογαριασμός δεν υπάρχει και o λογαριασμός εγκατάστασης έχει επαρκή δικαιώματα, αυτός ο λογαριασμός χρήστη θα δημιουργηθεί με τα ελάχιστα δικαιώματα που απαιτούνται για τη λειτουργία του wiki.",
"config-db-prefix": "Πρόθεμα πίνακα βάσης δεδομένων:",
- "config-db-prefix-help": "Εάν χρειάζεται να μοιραστείτε μία βάση δεδομένων μεταξύ πολλαπλών wikis, ή μεταξύ του MediaWiki και μιας άλλης web εφαρμογής, μπορείτε να προσθέσετε ένα πρόθεμα σε όλα τα ονόματα πίνακα για να αποφεύγονται οι διενέξεις.\nΜην χρησιμοποιείτε κενά διαστήματα.\n\nΑυτό το πεδίο αφήνεται συνήθως κενό.",
+ "config-db-prefix-help": "Εάν χρειάζεται να μοιραστείτε μία βάση δεδομένων μεταξύ πολλαπλών wiki, ή μεταξύ του MediaWiki και μιας άλλης εφαρμογής Ιστού, μπορείτε να προσθέσετε ένα πρόθεμα σε όλα τα ονόματα πινάκων για να αποφεύγονται οι διενέξεις.\nΜην χρησιμοποιείτε κενά διαστήματα.\n\nΑυτό το πεδίο αφήνεται συνήθως κενό.",
"config-mysql-old": "Απαιτείται Microsoft SQL Server $1 ή νεότερο. Εσείς έχετε $2.",
"config-db-port": "Θύρα βάσης δεδομένων:",
"config-db-schema": "Σχήμα για MediaWiki:",
@@ -101,6 +113,11 @@
"config-type-oracle": "Oracle",
"config-type-mssql": "Microsoft SQL Server",
"config-support-info": "To MediaWiki υποστηρίζει τα ακόλουθα συστήματα βάσεων δεδομένων:\n\n$1\n\nΑν δεν εμφανίζεται παρακάτω το σύστημα βάσης δεδομένων που θέλετε να χρησιμοποιήσετε, τότε ακολουθήστε τις οδηγίες στον παραπάνω σύνδεσμο για να ενεργοποιήσετε την υποστήριξη.",
+ "config-dbsupport-mysql": "* Η [{{int:version-db-mysql-url}} MySQL] είναι ο πρωταρχικός στόχος για το MediaWiki και υποστηρίζεται καλύτερα. Το MediaWiki συνεργάζεται επίσης με τη [{{int:version-db-mariadb-url}} MariaDB] και το [{{int:version-db-percona-url}} διακομιστή Percona], που είναι όλα συμβατά με MySQL. ([http://www.php.net/manual/en/mysqli.installation.php Πώς να μεταγλωττίσετε την PHP με υποστήριξη MySQL])",
+ "config-dbsupport-postgres": "* Η [{{int:version-db-postgres-url}} PostgreSQL] είναι δημοφιλές σύστημα βάσης δεδομένων ανοικτού κώδικα ως εναλλακτική της MySQL. ([http://www.php.net/manual/en/pgsql.installation.php Πώς να μεταγλωττίσετε την PHP με υποστήριξη PostgreSQL])",
+ "config-dbsupport-sqlite": "* Η [{{int:version-db-sqlite-url}} SQLite] είναι ένα ελαφρύ σύστημα βάσης δεδομένων που υποστηρίζεται πολύ καλά. ([http://www.php.net/manual/en/pdo.installation.php Πώς να μεταγλωττίσετε την PHP με υποστήριξη SQLite], χρησιμοποιεί PDO)",
+ "config-dbsupport-oracle": "* Η [{{int:version-db-oracle-url}} Oracle] είναι εμπορική βάση δεδομένων για επιχειρήσεις. ([http://www.php.net/manual/en/oci8.installation.php Πώς να μεταγλωττίσετε την PHP με υποστήριξη OCI8])",
+ "config-dbsupport-mssql": "* Ο [{{int:version-db-mssql-url}} Microsoft SQL Server] είναι εμπορική βάση δεδομένων για επιχειρήσεις που λειτουργεί σε Windows. ([http://www.php.net/manual/en/sqlsrv.installation.php Πώς να μεταγλωττίσετε την PHP με υποστήριξη SQLSRV])",
"config-header-mysql": "Ρυθμίσεις MySQL",
"config-header-postgres": "Ρυθμίσεις PostgreSQL",
"config-header-sqlite": "Ρυθμίσεις SQLite",
@@ -118,14 +135,17 @@
"config-sqlite-cant-create-db": "Δεν ήταν δυνατή η δημιουργία του αρχείου βάσης δεδομένων <code>$1</code>.",
"config-upgrade-done-no-regenerate": "Η αναβάθμιση ολοκληρώθηκε.\n\nΜπορείτε τώρα να [$1 ξεκινήσετε να χρησιμοποιείτε το wiki σας].",
"config-regenerate": "Αναδημιουργία LocalSettings.php →",
- "config-db-web-account": "Λογαριασμός βάσης δεδομένων για πρόσβαση ιστού",
+ "config-db-web-account": "Λογαριασμός βάσης δεδομένων για πρόσβαση Ιστού",
"config-db-web-account-same": "Χρήση του ίδιου λογαριασμού για την εγκατάσταση",
+ "config-db-web-create": "Να δημιουργηθεί ο λογαριασμός αν δεν υπάρχει ήδη",
"config-mysql-engine": "Μηχανή αποθήκευσης:",
"config-mysql-innodb": "InnoDB",
"config-mysql-myisam": "MyISAM",
+ "config-mysql-engine-help": "Το <strong>InnoDB</strong> είναι σχεδόν πάντα η καλύτερη επιλογή, αφού έχει καλή υποστήριξη ταυτόχρονης λειτουργίας.\n\nΤο <strong>MyISAM</strong> μπορεί να είναι ταχύτερο σε εγκαταστάσεις του ενός χρήστη ή μόνο ανάγνωσης. \nΟι βάσεις δεδομένων MyISAM τείνουν να φθείρονται συχνότερα από τις βάσεις δεδομένων InnoDB.",
"config-mysql-charset": "Σύνολο χαρακτήρων βάσης δεδομένων:",
"config-mysql-binary": "Δυαδικό",
"config-mysql-utf8": "UTF-8",
+ "config-mysql-charset-help": "Σε <strong>λειτουργία δυαδικού</strong>, το MediaWiki αποθηκεύει κείμενο UTF-8 στη βάση δεδομένων σε δυαδικά πεδία.\nΑυτό είναι πιο αποτελεσματικό από τη λειτουργία UTF-8 της MySQL και σας επιτρέπει να χρησιμοποιείτε ολόκληρο το φάσμα των χαρακτήρων Unicode.\n\nΣε <strong>λειτουργία UTF-8</strong>, η MySQL θα γνωρίζει σε ποιο σύνολο χαρακτήρων ανήκουν τα δεδομένα σας και θα μπορεί να τα παρουσιάζει και να τα μετατρέπει κατάλληλα, αλλά δεν θα σας επιτρέπει να αποθηκεύετε χαρακτήρες πάνω από το [https://en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes βασικό πολύγλωσσο επίπεδο].",
"config-mssql-auth": "Τύπος ελέγχου ταυτότητας:",
"config-mssql-sqlauth": "Έλεγχος ταυτότητας του SQL Server",
"config-mssql-windowsauth": "Έλεγχος ταυτότητας των Windows",
@@ -137,23 +157,34 @@
"config-ns-site-name": "Ίδιο με το όνομα του wiki: $1",
"config-ns-other": "Άλλο (προσδιορίστε)",
"config-ns-other-default": "ΤοWikiμου",
+ "config-project-namespace-help": "Ακολουθώντας το παράδειγμα της Βικιπαίδειας, πολλά wiki διατηρούν τις σελίδες πολιτικής τους χωριστά από τις σελίδες περιεχομένου τους, σε έναν '''ονοματοχώρο έργου'''.\nΌλοι οι τίτλοι σελίδων σε αυτόν τον ονοματοχώρο ξεκινούν με συγκεκριμένο πρόθεμα, το οποίο μπορείτε να ορίσετε εδώ.\nΣυνήθως, αυτό το πρόθεμα παράγεται από το όνομα του wiki, αλλά δεν μπορεί να περιέχει χαρακτήρες στίξης όπως «#» ή «:».",
"config-admin-box": "Λογαριασμός διαχειριστή",
"config-admin-name": "Το όνομα χρήστη σας:",
"config-admin-password": "Κωδικός πρόσβασης:",
"config-admin-password-confirm": "Επανάληψη κωδικού πρόσβασης:",
+ "config-admin-help": "Εισαγάγετε το προτιμώμενο όνομα χρήστη εδώ, για παράδειγμα «Γιάννης Ιστολόγιος». \nΑυτό είναι το όνομα που θα χρησιμοποιείτε για να συνδέεστε στο wiki.",
"config-admin-name-blank": "Εισαγάγετε όνομα χρήστη διαχειριστή.",
"config-admin-name-invalid": "Το συγκεκριμένο όνομα χρήστη \"<nowiki>$1</nowiki>\" δεν είναι έγκυρο. Δώστε ένα διαφορετικό όνομα χρήστη.",
"config-admin-password-blank": "Εισαγάγετε κωδικό για το λογαριασμό διαχειριστή.",
"config-admin-password-mismatch": "Οι δύο κωδικοί πρόσβασης που εισηγάγατε δεν ταιριάζουν.",
"config-admin-email": "Διεύθυνση ηλεκτρονικού ταχυδρομείου:",
+ "config-admin-email-help": "Εισαγάγετε μια διεύθυνση ηλεκτρονικού ταχυδρομείου εδώ για να μπορείτε να λαμβάνετε μηνύματα ηλεκτρονικού ταχυδρομείου από άλλους χρήστες στο wiki, να επαναφέρετε το συνθηματικό σας και να ενημερώνεστε για τις αλλαγές σε σελίδες που βρίσκονται στη λίστα παρακολούθησής σας. Μπορείτε να αφήσετε αυτό το πεδίο κενό.",
+ "config-admin-error-user": "Εσωτερικό σφάλμα κατά τη δημιουργία διαχειριστή με το όνομα «<nowiki>$1</nowiki>».",
+ "config-admin-error-password": "Εσωτερικό σφάλμα κατά τον καθορισμό συνθηματικού για το διαχειριστή «<nowiki>$1</nowiki>»: <pre>$2</pre>",
"config-admin-error-bademail": "Έχετε εισαγάγει μη έγκυρη διεύθυνση ηλεκτρονικού ταχυδρομείου.",
+ "config-subscribe": "Εγγραφή στη [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce λίστα αλληλογραφίας ανακοινώσεων σχετικά με νέες κυκλοφορίες].",
+ "config-subscribe-help": "Πρόκειται για μια λίστα αλληλογραφίας χαμηλού όγκου που χρησιμοποιείται για ανακοινώσεις σχετικά με νέες κυκλοφορίες, συμπεριλαμβανομένων σημαντικών ανακοινώσεων ασφαλείας. \nΚαλό θα ήταν να εγγραφείτε σε αυτήν και να ενημερώνετε την εγκατάσταση του MediaWiki όταν βγαίνουν νέες εκδόσεις.",
+ "config-pingback": "Διαμοιρασμός δεδομένων σχετικά με αυτήν την εγκατάσταση με τους προγραμματιστές του MediaWiki.",
+ "config-pingback-help": "Αν επιλέξετε αυτή την επιλογή, το MediaWiki θα στέλνει περιοδικά στο https://www.mediawiki.org βασικά δεδομένα σχετικά με αυτήν την υλοποίηση του MediaWiki. Αυτά τα δεδομένα περιλαμβάνουν, για παράδειγμα, τον τύπο του συστήματος, την έκδοση PHP και την επιλεγμένη βάση δεδομένων. Το Ίδρυμα Wikimedia μοιράζεται αυτά τα δεδομένα με τους προγραμματιστές του MediaWiki για να καθοδηγήσει τις μελλοντικές προσπάθειες ανάπτυξης. θα αποσταλούν τα ακόλουθα δεδομένα σχετικά με το σύστημά σας:\n<pre>$1</pre>",
+ "config-almost-done": "Έχετε σχεδόν τελειώσει!\nΤώρα μπορείτε να παραλείψετε την υπόλοιπη διαμόρφωση και να εγκαταστήσετε το wiki αμέσως τώρα.",
"config-optional-continue": "Να ερωτηθώ περισσότερες ερωτήσεις.",
- "config-optional-skip": "Βαρέθηκα ήδη, απλά εγκαταστήστε το wiki.",
+ "config-optional-skip": "Βαρέθηκα ήδη, να εγκατασταθεί απλά το wiki.",
"config-profile": "Προφίλ δικαιωμάτων χρήστη:",
- "config-profile-wiki": "Ανοικτό wiki",
- "config-profile-no-anon": "Απαιτείται η δημιουργία λογαριασμού",
- "config-profile-fishbowl": "Εξουσιοδοτημένοι συντάκτες μόνο",
- "config-profile-private": "Ιδιωτικό wiki",
+ "config-profile-wiki": "ανοικτό wiki",
+ "config-profile-no-anon": "απαιτούμενη δημιουργία λογαριασμού",
+ "config-profile-fishbowl": "εξουσιοδοτημένοι συντάκτες μόνο",
+ "config-profile-private": "ιδιωτικό wiki",
+ "config-profile-help": "Τα wiki λειτουργούν καλύτερα όταν αφήνετε να τα επεξεργαστούν όσο το δυνατόν περισσότεροι άνθρωποι.\nΣτο MediaWiki, είναι εύκολο να ελέγξετε τις πρόσφατες αλλαγές και να επαναφέρετε τυχόν ζημιές που προκαλούνται από αρχάριους ή κακόβουλους χρήστες.\n\nΩστόσο, πολλοί έχουν βρει το MediaWiki χρήσιμο σε ένα ευρύ φάσμα ρόλων και μερικές φορές δεν είναι εύκολο να πείσουμε τους πάντες για τα οφέλη του τρόπου του wiki.\nΥπάρχει λοιπόν επιλογή.\n\nΤο μοντέλο <strong>{{int:config-profile-wiki}}</strong> επιτρέπει σε οποιονδήποτε να επεξεργαστεί, χωρίς καν να συνδεθεί. Ένα wiki με <strong>{{int:config-profile-no-anon}}</strong> παρέχει μεγαλύτερη λογοδοσία, αλλά μπορεί να αποτρέψει τους περιστασιακούς συνεισφέροντες.\n\nΤο σενάριο <strong>{{int:config-profile-fishbowl}}</strong> επιτρέπει στους εγκεκριμένους χρήστες να επεξεργαστούν, αλλά το κοινό μπορεί να δει τις σελίδες, συμπεριλαμβανομένου του ιστορικού. \nΈνα <strong>{{int:config-profile-private}}</strong> επιτρέπει μόνο στους εγκεκριμένους χρήστες να προβάλλουν σελίδες, και μόνο στους ίδιους να επεξεργαστούν.\n\nΠιο πολύπλοκες ρυθμίσεις δικαιωμάτων χρήστη είναι διαθέσιμες μετά την εγκατάσταση, δείτε τη [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights σχετική μη αυτόματη καταχώριση].",
"config-license": "Πνευματικά δικαιώματα και άδεια χρήσης:",
"config-license-none": "Χωρίς άδεια χρήσης στο υποσέλιδο",
"config-license-cc-by-sa": "Creative Commons Αναφορά Δημιουργού-Παρόμοια Διανομή",
@@ -163,24 +194,39 @@
"config-license-gfdl": "Αδειοδότηση Ελεύθερης Τεκμηρίωσης GNU 1.3 ή μεταγενέστερη",
"config-license-pd": "Κοινό Κτήμα",
"config-license-cc-choose": "Επιλέξτε μια προσαρμοσμένη άδεια Creative Commons",
+ "config-license-help": "Πολλά δημόσια wiki βάζουν όλες τις συνεισφορές υπό [https://freedomdefined.org/Definition ελεύθερη άδεια].\nΑυτό βοηθά να δημιουργηθεί μια αίσθηση κοινοτικής ιδιοκτησίας και ενθαρρύνει τη μακροχρόνια συνεισφορά.\nΔεν είναι γενικά απαραίτητο για ένα ιδιωτικό ή εταιρικό wiki.\n\nΑν θέλετε να μπορείτε να χρησιμοποιείτε κείμενο από τη Βικιπαίδεια και αν θέλετε να μπορεί η Βικιπαίδεια να δεχτεί κείμενο που αντιγράφεται από το wiki σας, θα πρέπει να επιλέξετε <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nΗ Βικιπαίδεια χρησιμοποιούσε παλιά την Άδεια Ελεύθερης Τεκμηρίωσης GNU (GFDL).\nΗ GFDL είναι μια έγκυρη άδεια, αλλά είναι δύσκολο να κατανοηθεί.\nΕίναι επίσης δύσκολο να επαναχρησιμοποιηθεί το περιεχόμενο που έχει χορηγηθεί βάσει της GFDL.",
"config-email-settings": "Ρυθμίσεις ηλεκτρονικού ταχυδρομείου",
+ "config-enable-email": "Ενεργοποίηση εξερχόμενου ηλεκτρονικού ταχυδρομείου",
+ "config-enable-email-help": "Αν θέλετε να λειτουργήσει το ηλεκτρονικό ταχυδρομείο, οι [http://www.php.net/manual/en/mail.configuration.php ρυθμίσεις αλληλογραφίας της PHP] πρέπει να ρυθμιστούν σωστά.\nΑν δεν θέλετε καθόλου λειτουργίες ηλεκτρονικού ταχυδρομείου, μπορείτε να τις απενεργοποιήσετε εδώ.",
"config-email-user": "Ενεργοποίηση ηλεκτρονικού ταχυδρομείου από χρήστη σε χρήστη",
+ "config-email-user-help": "Να επιτρέπεται σε όλους τους χρήστες να στέλνουν ο ένας στον άλλον μηνύματα ηλεκτρονικού ταχυδρομείου εάν το έχουν ενεργοποιήσει στις προτιμήσεις τους.",
"config-email-usertalk": "Ενεργοποίηση ειδοποίησης σελίδας συζήτησης χρήστη",
+ "config-email-usertalk-help": "Να επιτρέπεται στους χρήστες να λαμβάνουν ειδοποιήσεις σχετικά με τις αλλαγές στις σελίδες συζήτησης χρήστη, εάν το έχουν ενεργοποιήσει στις προτιμήσεις τους.",
"config-email-watchlist": "Ενεργοποίηση ειδοποίησης λίστας παρακολούθησης",
- "config-email-watchlist-help": "Επιτρέψτε στους χρήστες να λαμβάνουν ειδοποιήσεις για τις σελίδες που παρακολουθούν αν το έχουν ενεργοποιήσει στις προτιμήσεις τους.",
+ "config-email-watchlist-help": "Να επιτρέπεται στους χρήστες να λαμβάνουν ειδοποιήσεις για τις σελίδες που παρακολουθούν αν το έχουν ενεργοποιήσει στις προτιμήσεις τους.",
"config-email-auth": "Ενεργοποίηση ταυτοποίησης μέσω ηλεκτρονικού ταχυδρομείου",
+ "config-email-auth-help": "Εάν αυτή η επιλογή είναι ενεργοποιημένη, οι χρήστες πρέπει να επιβεβαιώσουν τη διεύθυνση ηλεκτρονικού ταχυδρομείου τους χρησιμοποιώντας ένα σύνδεσμο που τους αποστέλλεται όποτε την καθορίζουν ή την αλλάζουν.\nΜόνο οι ταυτοποιημένες διευθύνσεις ηλεκτρονικού ταχυδρομείου μπορούν να λαμβάνουν μηνύματα ηλεκτρονικού ταχυδρομείου από άλλους χρήστες ή να αλλάζουν μηνύματα ειδοποίησης.\nΟ καθορισμός αυτής της επιλογής <strong>συνιστάται</strong> για δημόσια wiki λόγω πιθανής κατάχρησης των λειτουργιών του ηλεκτρονικού ταχυδρομείου.",
"config-email-sender": "Διεύθυνση ηλεκτρονικού ταχυδρομείου επιστροφής:",
+ "config-email-sender-help": "Εισαγάγετε τη διεύθυνση ηλεκτρονικού ταχυδρομείου που θα χρησιμοποιηθεί ως διεύθυνση επιστροφής στο εξερχόμενο ηλεκτρονικό ταχυδρομείο.\nΕκεί είναι που θα στέλνονται οι επιστροφές.\nΠολλοί διακομιστές αλληλογραφίας απαιτούν τουλάχιστον το τμήμα ονόματος τομέα να είναι έγκυρο.",
"config-upload-settings": "Ανέβασμα εικόνων και άλλων αρχείων",
- "config-upload-enable": "Ενεργοποιήστε το ανέβασμα αρχείων",
- "config-upload-help": "Το ανέβασμα αρχείων εκθέτει πιθανώς το διακομιστή σας σε κινδύνους ασφαλείας.\nΓια περισσότερες πληροφορίες, διαβάστε την [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security ενότητα περί ασφάλειας] στο εγχειρίδιο.\n\nΓια να ενεργοποιήσετε το ανέβασμα αρχείων, αλλάξτε την κατάσταση του υποκαταλόγου <code>εικόνες</code> που βρίσκεται κάτω από τον ριζικό κατάλογο του MediaWiki έτσι ώστε ο διακομιστής ιστού να μπορεί να γράψει σε αυτόν.\nΣτη συνέχεια ενεργοποιήσετε αυτή την επιλογή.",
+ "config-upload-enable": "Ενεργοποίηση ανεβάσματος αρχείων",
+ "config-upload-help": "Το ανέβασμα αρχείων εκθέτει πιθανώς το διακομιστή σας σε κινδύνους ασφαλείας.\nΓια περισσότερες πληροφορίες, διαβάστε την [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security ενότητα περί ασφάλειας] στο εγχειρίδιο.\n\nΓια να ενεργοποιήσετε το ανέβασμα αρχείων, αλλάξτε την κατάσταση του υποκαταλόγου <code>images</code> που βρίσκεται κάτω από τον ριζικό κατάλογο του MediaWiki έτσι ώστε ο διακομιστής Ιστού να μπορεί να γράψει σε αυτόν.\nΣτη συνέχεια ενεργοποιήσετε αυτή την επιλογή.",
"config-upload-deleted": "Καταλόγος για διαγραφέντα αρχεία:",
+ "config-upload-deleted-help": "Επιλέξτε έναν κατάλογο στον οποίο να αρχειοθετείτε τα διαγραμμένα αρχεία.\nΙδανικά, αυτό δεν θα πρέπει να είναι προσβάσιμο από τον Ιστό.",
"config-logo": "Διεύθυνση URL λογότυπου:",
+ "config-logo-help": "Το προεπιλεγμένο θέμα εμφάνισης του MediaWiki περιλαμβάνει χώρο για λογότυπο 135x160 εικονοστοιχείων πάνω από το μενού της πλαϊνής μπάρας. Ανεβάστε μια εικόνα με το κατάλληλο μέγεθος και εισαγάγετε τη διεύθυνση URL εδώ.\n\nΜπορείτε να χρησιμοποιήσετε τα <code>$wgStylePath</code> ή <code>$wgScriptPath</code> εάν το λογότυπό σας είναι σχετικό με αυτές τις διαδρομές.\n\nΕάν δεν θέλετε λογότυπο, αφήστε αυτό το πλαίσιο κενό.",
"config-instantcommons": "Ενεργοποίηση Instant Commons",
+ "config-instantcommons-help": "Τα [https://www.mediawiki.org/wiki/InstantCommons Instant Commons] είναι ένα χαρακτηριστικό που επιτρέπει στα wiki να χρησιμοποιούν εικόνες, ήχους και άλλα μέσα που βρίσκονται στον ιστότοπο των Wikimedia Commons. Για να γίνει αυτό, το MediaWiki απαιτεί πρόσβαση στο Διαδίκτυο.\n\nΓια περισσότερες πληροφορίες σχετικά με αυτήν τη δυνατότητα, συμπεριλαμβανομένων των οδηγιών για τον τρόπο ρύθμισης της για άλλα wiki από τα Wikimedia Commons, συμβουλευτείτε [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos το εγχειρίδιο].",
"config-cc-error": "Ο επιλογέας αδειών Creative Commons επιλογέα δεν έδωσε κανένα αποτέλεσμα.\nΕισάγετε το όνομα της άδειας χειροκίνητα.",
"config-cc-again": "Επιλέξτε ξανά...",
"config-cc-not-chosen": "Επιλέξτε την άδεια Creative Commons που θέλετε και κάντε κλικ στο κουμπί \"proceed\".",
"config-advanced-settings": "Προηγμένες ρυθμίσεις παραμέτρων",
"config-cache-options": "Ρυθμίσεις για την προσωρινή αποθήκευση αντικειμένου:",
+ "config-cache-help": "Η προσωρινή αποθήκευση αντικειμένων χρησιμοποιείται για τη βελτίωση της ταχύτητας του MediaWiki με προσωρινή αποθήκευση των δεδομένων που χρησιμοποιούνται συχνά.\nΜεσαίοι και μεγάλοι ιστότοποι ενθαρρύνονται ιδιαίτερα να το ενεργοποιήσουν αυτό και μικροί ιστότοποι θα δουν οφέλη επίσης.",
+ "config-cache-none": "Χωρίς προσωρινή αποθήκευση (δεν καταργείται καμία λειτουργία, αλλά η ταχύτητα μπορεί να επηρεαστεί σε μεγαλύτερους ιστότοπους wiki)",
+ "config-cache-accel": "Προσωρινή αποθήκευση αντικειμένων PHP (APC, APCu ή WinCache)",
+ "config-cache-memcached": "Χρήση Memcached (απαιτεί πρόσθετη ρύθμιση και διαμόρφωση)",
+ "config-memcached-servers": "Διακομιστές Memcached:",
"config-memcache-badip": "Έχετε εισάγει μια μη έγκυρη διεύθυνση IP για το Memcached: $1.",
"config-memcache-noport": "Δεν καθορίσατε μια θύρα για να χρησιμοποιήσετε για το Memcached server: $1.\nΑν δεν ξέρετε τη θύρα, η προεπιλογή είναι 11211.",
"config-memcache-badport": "Οι Memcached αριθμοί θύρας θα πρέπει να είναι μεταξύ $1 και $2.",
@@ -226,12 +272,16 @@
"config-install-mainpage-exists": "Κύρια σελίδα ήδη υπάρχει, παρακάμπτεται",
"config-install-extension-tables": "Γίνεται δημιουργία πινάκων για τις εγκατεστημένες επεκτάσεις",
"config-install-mainpage-failed": "Δεν ήταν δυνατή η εισαγωγή της αρχικής σελίδας: $1",
- "config-install-done": "<strong>Συγχαρητήρια!</strong>\nΈχετε εγκαταστήσει με επιτυχία το MediaWiki.\n\nΤο πρόγραμμα εγκατάστασης έχει δημιουργήσει το αρχείο <code>LocalSettings.php</code>.\nΠεριέχει όλες τις ρυθμίσεις παραμέτρων σας.\n\nΘα πρέπει να το κατεβάσετε και να το βάλετε στη βάση της εγκατάστασης του wiki σας (στον ίδιο κατάλογο όπως το index.php). Η λήψη θα αρχίσει αυτόματα.\n\nΑν η λήψη δεν προσφέφθηκε, ή αν την ακυρώσατε, μπορείτε να επανεκκινήσετε τη λήψη κάνοντας κλικ στο παρακάτω link:\n\n$3\n\n<strong>Σημείωση:</strong> Εάν δεν το κάνετε αυτό τώρα, αυτό το αρχείο ρύθμισης παραμέτρων δεν θα είναι διαθέσιμο για σας αργότερα, αν βγείτε από την εγκατάσταση, χωρίς να το κατεβάσετε!\n\nΌταν γίνει αυτό, μπορείτε να <strong>[$2 μπείτε στο wiki σας]</strong>.",
+ "config-install-done": "<strong>Συγχαρητήρια!</strong>\nΈχετε εγκαταστήσει το MediaWiki.\n\nΤο πρόγραμμα εγκατάστασης έχει δημιουργήσει το αρχείο <code>LocalSettings.php</code>.\nΠεριέχει όλες τις ρυθμίσεις παραμέτρων σας.\n\nΘα πρέπει να το κατεβάσετε και να το βάλετε στη βάση της εγκατάστασης του wiki σας (στον ίδιο κατάλογο με το index.php). Η λήψη θα πρέπει να έχει ξεκινήσει αυτόματα.\n\nΕάν δεν σας προτάθηκε λήψη, ή αν την ακυρώσατε, μπορείτε να επανεκκινήσετε τη λήψη κάνοντας κλικ στο σύνδεσμο ακριβώς από κάτω:\n\n$3\n\n<strong>Σημείωση:</strong> Εάν δεν το κάνετε αυτό τώρα, αυτό το αρχείο ρύθμισης παραμέτρων δεν θα είναι διαθέσιμο για σας αργότερα αν βγείτε από την εγκατάσταση χωρίς να το κατεβάσετε!\n\nΌταν θα έχει γίνει αυτό, μπορείτε να <strong>[$2 μπείτε στο wiki σας]</strong>.",
+ "config-install-done-path": "<strong>Συγχαρητήρια!</strong>\nΈχετε εγκαταστήσει το MediaWiki.\n\nΤο πρόγραμμα εγκατάστασης έχει δημιουργήσει το αρχείο <code>LocalSettings.php</code>.\nΠεριέχει όλες τις ρύθμιση σας.\n\nΘα πρέπει να το κατεβάσετε και να το βάλετε στο <code>$4</code>. Η λήψη θα πρέπει να έχει ξεκινήσει αυτόματα.\n\nΕάν δεν σας προτάθηκε λήψη, ή αν την ακυρώσατε, μπορείτε να επανεκκινήσετε τη λήψη κάνοντας κλικ στο σύνδεσμο ακριβώς από κάτω:\n\n$3\n\n<strong>Σημείωση:</strong> Εάν δεν το κάνετε αυτό τώρα, αυτό το δημιουγημένο αρχείο ρύθμισης δεν θα είναι διαθέσιμο για σας αργότερα αν βγείτε από την εγκατάσταση χωρίς να το κατεβάσετε!\n\nΌταν θα έχει γίνει αυτό, μπορείτε να <strong>[$2 μπείτε στο wiki σας]</strong>.",
+ "config-install-success": " Το σύστημα της MediaWiki έχει εγκατασταθεί με επιτυχία. Μπορείτε τώρα να επισκεφθείτε το \n <$1$2> για να δείτε το wiki σας.\nΑν έχετε ερωτήσεις, ελέγξετε την λίστα με τις πιο συχνές ερωτήσεις:\n<https://www.mediawiki.org/wiki/Manual:FAQ> ή χρησιμοποιήστε ένα από τα φόρουμ υποστήριξης που είναι συνδεδεμένα σε αυτήν την σελίδα.",
"config-download-localsettings": "Λήψη του <code>LocalSettings.php</code>",
"config-help": "βοήθεια",
"config-help-tooltip": "κλικ για ανάπτυξη",
"config-nofile": "Το αρχείο «$1» δεν μπορεί να βρεθεί. Μήπως έχει διαγραφεί;",
- "config-extension-link": "Γνωρίζατε ότι το wiki σας υποστηρίζει [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions επεκτάσεις];\n\nΜπορείτε να περιηγηθείτε [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category επεκτάσεις ανά κατηγορία] ή το [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix] για να δείτε την πλήρη λίστα των επεκτάσεων.",
- "mainpagetext": "<strong>To MediaWiki εγκαταστάθηκε με επιτυχία.</strong>",
- "mainpagedocfooter": "Συμβουλευτείτε το [https://meta.wikimedia.org/wiki/Help:Contents Οδηγός Χρήστη] για πληροφορίες σχετικά με το λογισμικό wiki.\n\n== Ξεκινώντας ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings ρυθμίσεις Διαμόρφωσης λίστα]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ το MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce το MediaWiki απελευθέρωση mailing list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Έχουν MediaWiki για τη γλώσσα σας]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Μάθετε πώς να καταπολεμήσετε το spam στο wiki σας]"
+ "config-extension-link": "Γνωρίζατε ότι το wiki σας υποστηρίζει [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions επεκτάσεις];\n\nΜπορείτε να περιηγηθείτε στις [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category επεκτάσεις ανά κατηγορία] ή στον [https://www.mediawiki.org/wiki/Extension_Matrix πίνακα επεκτάσεων] για να δείτε την πλήρη λίστα των επεκτάσεων.",
+ "config-skins-screenshots": "$1 (στιγμιότυπα: $2)",
+ "config-screenshot": "στιγμιότυπο",
+ "mainpagetext": "<strong>To MediaWiki εγκαταστάθηκε.</strong>",
+ "mainpagedocfooter": "Συμβουλευτείτε τον [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents οδηγό χρήστη] για πληροφορίες σχετικά με το λογισμικό wiki.\n\n== Ξεκινώντας ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Λίστα ρυθμίσεων διαμόρφωσης]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ\n Συχνές ερωτήσεις για το MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Λίστα αλληλογραφίας νέων κυκλοφοριών του MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Μεταφράστε το MediaWiki στη γλώσσα σας]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Μάθετε πώς να καταπολεμήσετε το ανεπιθύμητο περιεχόμενο στο wiki σας]"
}
diff --git a/www/wiki/includes/installer/i18n/eml.json b/www/wiki/includes/installer/i18n/eml.json
index 37fefefa..1315f07b 100644
--- a/www/wiki/includes/installer/i18n/eml.json
+++ b/www/wiki/includes/installer/i18n/eml.json
@@ -7,8 +7,6 @@
"config-information": "Infurmasiòun",
"config-your-language": "La tó lengva:",
"config-page-language": "Lengva",
- "config-charset-mysql5-binary": "binàri MySQL 4.1/5.0",
- "config-charset-mysql5": "MySQL 4.1/5.0 UTF-8",
"config-admin-password-mismatch": "El dó paró li cêv 't ê pruvê i n'vàn mia bèin.",
"config-admin-email": "Indirìs e-mail:",
"config-optional-continue": "Edmànd-em de piò.",
diff --git a/www/wiki/includes/installer/i18n/en-gb.json b/www/wiki/includes/installer/i18n/en-gb.json
index 30796e3d..7476c890 100644
--- a/www/wiki/includes/installer/i18n/en-gb.json
+++ b/www/wiki/includes/installer/i18n/en-gb.json
@@ -8,9 +8,9 @@
"config-desc": "The installer for MediaWiki",
"config-title": "MediaWiki $1 installation",
"config-information": "Information",
- "config-copyright": "=== Copyright and Terms ===\n\n$1\n\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public Licence as published by the Free Software Foundation; either version 2 of the Licence, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but '''without any warranty'''; without even the implied warranty of '''merchantability''' or '''fitness for a particular purpose'''.\nSee the GNU General Public Licence for more details.\n\nYou should have received <doclink href=Copying>a copy of the GNU General Public Licence</doclink> along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. or [http://www.gnu.org/copyleft/gpl.html read it online].",
- "config-unicode-using-intl": "Using the [http://pecl.php.net/intl intl PECL extension] for Unicode normalisation.",
- "config-unicode-pure-php-warning": "'''Warning:''' The [http://pecl.php.net/intl intl PECL extension] is not available to handle Unicode normalisation, falling back to slow pure-PHP implementation.\nIf you run a high-traffic site, you should read a little on [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalisation].",
+ "config-copyright": "=== Copyright and Terms ===\n\n$1\n\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public Licence as published by the Free Software Foundation; either version 2 of the Licence, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but '''without any warranty'''; without even the implied warranty of '''merchantability''' or '''fitness for a particular purpose'''.\nSee the GNU General Public Licence for more details.\n\nYou should have received <doclink href=Copying>a copy of the GNU General Public Licence</doclink> along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. or [https://www.gnu.org/copyleft/gpl.html read it online].",
+ "config-unicode-using-intl": "Using the [https://pecl.php.net/intl intl PECL extension] for Unicode normalisation.",
+ "config-unicode-pure-php-warning": "'''Warning:''' The [https://pecl.php.net/intl intl PECL extension] is not available to handle Unicode normalisation, falling back to slow pure-PHP implementation.\nIf you run a high-traffic site, you should read a little on [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalisation].",
"config-unicode-update-warning": "'''Warning:''' The installed version of the Unicode normalisation wrapper uses an older version of [http://site.icu-project.org/ the ICU project's] library.\nYou should [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations upgrade] if you are at all concerned about using Unicode.",
"config-unknown-collation": "'''Warning:''' Database is using unrecognised collation.",
"config-profile-fishbowl": "Authorised editors only",
@@ -18,7 +18,7 @@
"config-license-none": "No licence footer",
"config-license-gfdl": "GNU Free Documentation Licence 1.3 or later",
"config-license-cc-choose": "Select a custom Creative Commons licence",
- "config-license-help": "Many public wikis put all contributions under a [http://freedomdefined.org/Definition free licence].\nThis helps to create a sense of community ownership and encourages long-term contribution.\nIt is not generally necessary for a private or corporate wiki.\n\nIf you want to be able to use text from Wikipedia, and you want Wikipedia to be able to accept text copied from your wiki, you should choose <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia previously used the GNU Free Documentation Licence.\nThe GFDL is a valid licence, but it is difficult to understand.\nIt is also difficult to reuse content licenced under the GFDL.",
+ "config-license-help": "Many public wikis put all contributions under a [https://freedomdefined.org/Definition free licence].\nThis helps to create a sense of community ownership and encourages long-term contribution.\nIt is not generally necessary for a private or corporate wiki.\n\nIf you want to be able to use text from Wikipedia, and you want Wikipedia to be able to accept text copied from your wiki, you should choose <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia previously used the GNU Free Documentation Licence.\nThe GFDL is a valid licence, but it is difficult to understand.\nIt is also difficult to reuse content licenced under the GFDL.",
"config-cc-error": "The Creative Commons licence chooser gave no result.\nEnter the licence name manually.",
"config-cc-not-chosen": "Choose which Creative Commons licence you want and click \"proceed\".",
"config-install-stats": "Initialising statistics"
diff --git a/www/wiki/includes/installer/i18n/en.json b/www/wiki/includes/installer/i18n/en.json
index 6319b76d..5509a76d 100644
--- a/www/wiki/includes/installer/i18n/en.json
+++ b/www/wiki/includes/installer/i18n/en.json
@@ -39,28 +39,27 @@
"config-help-restart": "Do you want to clear all saved data that you have entered and restart the installation process?",
"config-restart": "Yes, restart it",
"config-welcome": "=== Environmental checks ===\nBasic checks will now be performed to see if this environment is suitable for MediaWiki installation.\nRemember to include this information if you seek support on how to complete the installation.",
- "config-copyright": "=== Copyright and Terms ===\n\n$1\n\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but <strong>without any warranty</strong>; without even the implied warranty of <strong>merchantability</strong> or <strong>fitness for a particular purpose</strong>.\nSee the GNU General Public License for more details.\n\nYou should have received <doclink href=Copying>a copy of the GNU General Public License</doclink> along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, or [http://www.gnu.org/copyleft/gpl.html read it online].",
+ "config-copyright": "=== Copyright and Terms ===\n\n$1\n\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but <strong>without any warranty</strong>; without even the implied warranty of <strong>merchantability</strong> or <strong>fitness for a particular purpose</strong>.\nSee the GNU General Public License for more details.\n\nYou should have received <doclink href=Copying>a copy of the GNU General Public License</doclink> along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, or [https://www.gnu.org/copyleft/gpl.html read it online].",
"config-sidebar": "* [https://www.mediawiki.org MediaWiki home]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents User's Guide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administrator's Guide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Read me</doclink>\n* <doclink href=ReleaseNotes>Release notes</doclink>\n* <doclink href=Copying>Copying</doclink>\n* <doclink href=UpgradeDoc>Upgrading</doclink>",
"config-env-good": "The environment has been checked.\nYou can install MediaWiki.",
"config-env-bad": "The environment has been checked.\nYou cannot install MediaWiki.",
"config-env-php": "PHP $1 is installed.",
"config-env-hhvm": "HHVM $1 is installed.",
- "config-unicode-using-intl": "Using the [http://pecl.php.net/intl intl PECL extension] for Unicode normalization.",
- "config-unicode-pure-php-warning": "<strong>Warning:</strong> The [http://pecl.php.net/intl intl PECL extension] is not available to handle Unicode normalization, falling back to slow pure-PHP implementation.\nIf you run a high-traffic site, you should read a little on [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
+ "config-unicode-using-intl": "Using the [https://php.net/manual/en/book.intl.php PHP intl extension] for Unicode normalization.",
+ "config-unicode-pure-php-warning": "<strong>Warning:</strong> The [https://php.net/manual/en/book.intl.php PHP intl extension] is not available to handle Unicode normalization, falling back to slow pure-PHP implementation.\nIf you run a high-traffic site, you should read on [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
"config-unicode-update-warning": "<strong>Warning:</strong> The installed version of the Unicode normalization wrapper uses an older version of [http://site.icu-project.org/ the ICU project's] library.\nYou should [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations upgrade] if you are at all concerned about using Unicode.",
"config-no-db": "Could not find a suitable database driver! You need to install a database driver for PHP.\nThe following database {{PLURAL:$2|type is|types are}} supported: $1.\n\nIf you compiled PHP yourself, reconfigure it with a database client enabled, for example, using <code>./configure --with-mysqli</code>.\nIf you installed PHP from a Debian or Ubuntu package, then you also need to install, for example, the <code>php5-mysql</code> package.",
- "config-outdated-sqlite": "<strong>Warning:</strong> you have SQLite $1, which is lower than minimum required version $2. SQLite will be unavailable.",
+ "config-outdated-sqlite": "<strong>Warning:</strong> you have SQLite $2, which is lower than minimum required version $1. SQLite will be unavailable.",
"config-no-fts3": "<strong>Warning:</strong> SQLite is compiled without the [//sqlite.org/fts3.html FTS3 module], search features will be unavailable on this backend.",
"config-pcre-old": "<strong>Fatal:</strong> PCRE $1 or later is required.\nYour PHP binary is linked with PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE More information].",
"config-pcre-no-utf8": "<strong>Fatal:</strong> PHP's PCRE module seems to be compiled without PCRE_UTF8 support.\nMediaWiki requires UTF-8 support to function correctly.",
"config-memory-raised": "PHP's <code>memory_limit</code> is $1, raised to $2.",
"config-memory-bad": "<strong>Warning:</strong> PHP's <code>memory_limit</code> is $1.\nThis is probably too low.\nThe installation may fail!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] is installed",
"config-apc": "[http://www.php.net/apc APC] is installed",
"config-apcu": "[http://www.php.net/apcu APCu] is installed",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] is installed",
- "config-no-cache-apcu": "<strong>Warning:</strong> Could not find [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] or [http://www.iis.net/download/WinCacheForPhp WinCache].\nObject caching is not enabled.",
- "config-mod-security": "<strong>Warning:</strong> Your web server has [http://modsecurity.org/ mod_security]/mod_security2 enabled. Many common configurations of this will cause problems for MediaWiki and other software that allows users to post arbitrary content.\nIf possible, this should be disabled. Otherwise, refer to [http://modsecurity.org/documentation/ mod_security documentation] or contact your host's support if you encounter random errors.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] is installed",
+ "config-no-cache-apcu": "<strong>Warning:</strong> Could not find [http://www.php.net/apcu APCu] or [http://www.iis.net/download/WinCacheForPhp WinCache].\nObject caching is not enabled.",
+ "config-mod-security": "<strong>Warning:</strong> Your web server has [https://modsecurity.org/ mod_security]/mod_security2 enabled. Many common configurations of this will cause problems for MediaWiki and other software that allows users to post arbitrary content.\nIf possible, this should be disabled. Otherwise, refer to [https://modsecurity.org/documentation/ mod_security documentation] or contact your host's support if you encounter random errors.",
"config-diff3-bad": "GNU diff3 not found.",
"config-git": "Found the Git version control software: <code>$1</code>.",
"config-git-bad": "Git version control software not found.",
@@ -161,10 +160,6 @@
"config-mysql-myisam-dep": "<strong>Warning:</strong> You have selected MyISAM as storage engine for MySQL, which is not recommended for use with MediaWiki, because:\n* it barely supports concurrency due to table locking\n* it is more prone to corruption than other engines\n* the MediaWiki codebase does not always handle MyISAM as it should\n\nIf your MySQL installation supports InnoDB, it is highly recommended that you choose that instead.\nIf your MySQL installation does not support InnoDB, maybe it's time for an upgrade.",
"config-mysql-only-myisam-dep": "<strong>Warning:</strong> MyISAM is the only available storage engine for MySQL on this machine, and this is not recommended for use with MediaWiki, because:\n* it barely supports concurrency due to table locking\n* it is more prone to corruption than other engines\n* the MediaWiki codebase does not always handle MyISAM as it should\n\nYour MySQL installation does not support InnoDB, maybe it's time for an upgrade.",
"config-mysql-engine-help": "<strong>InnoDB</strong> is almost always the best option, since it has good concurrency support.\n\n<strong>MyISAM</strong> may be faster in single-user or read-only installations.\nMyISAM databases tend to get corrupted more often than InnoDB databases.",
- "config-mysql-charset": "Database character set:",
- "config-mysql-binary": "Binary",
- "config-mysql-utf8": "UTF-8",
- "config-mysql-charset-help": "In <strong>binary mode</strong>, MediaWiki stores UTF-8 text to the database in binary fields.\nThis is more efficient than MySQL's UTF-8 mode, and allows you to use the full range of Unicode characters.\n\nIn <strong>UTF-8 mode</strong>, MySQL will know what character set your data is in, and can present and convert it appropriately, but it will not let you store characters above the [https://en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Basic Multilingual Plane].",
"config-mssql-auth": "Authentication type:",
"config-mssql-install-auth": "Select the authentication type that will be used to connect to the database during the installation process.\nIf you select \"{{int:config-mssql-windowsauth}}\", the credentials of whatever user the webserver is running as will be used.",
"config-mssql-web-auth": "Select the authentication type that the web server will use to connect to the database server, during ordinary operation of the wiki.\nIf you select \"{{int:config-mssql-windowsauth}}\", the credentials of whatever user the webserver is running as will be used.",
@@ -218,7 +213,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 or later",
"config-license-pd": "Public Domain",
"config-license-cc-choose": "Select a custom Creative Commons license",
- "config-license-help": "Many public wikis put all contributions under a [http://freedomdefined.org/Definition free license].\nThis helps to create a sense of community ownership and encourages long-term contribution.\nIt is not generally necessary for a private or corporate wiki.\n\nIf you want to be able to use text from Wikipedia, and you want Wikipedia to be able to accept text copied from your wiki, you should choose <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia previously used the GNU Free Documentation License.\nThe GFDL is a valid license, but it is difficult to understand.\nIt is also difficult to reuse content licensed under the GFDL.",
+ "config-license-help": "Many public wikis put all contributions under a [https://freedomdefined.org/Definition free license].\nThis helps to create a sense of community ownership and encourages long-term contribution.\nIt is not generally necessary for a private or corporate wiki.\n\nIf you want to be able to use text from Wikipedia, and you want Wikipedia to be able to accept text copied from your wiki, you should choose <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia previously used the GNU Free Documentation License.\nThe GFDL is a valid license, but it is difficult to understand.\nIt is also difficult to reuse content licensed under the GFDL.",
"config-email-settings": "Email settings",
"config-enable-email": "Enable outbound email",
"config-enable-email-help": "If you want email to work, [http://www.php.net/manual/en/mail.configuration.php PHP's mail settings] need to be configured correctly.\nIf you do not want any email features, you can disable them here.",
@@ -248,7 +243,7 @@
"config-cache-options": "Settings for object caching:",
"config-cache-help": "Object caching is used to improve the speed of MediaWiki by caching frequently used data.\nMedium to large sites are highly encouraged to enable this, and small sites will see benefits as well.",
"config-cache-none": "No caching (no functionality is removed, but speed may be impacted on larger wiki sites)",
- "config-cache-accel": "PHP object caching (APC, APCu, XCache or WinCache)",
+ "config-cache-accel": "PHP object caching (APC, APCu or WinCache)",
"config-cache-memcached": "Use Memcached (requires additional setup and configuration)",
"config-memcached-servers": "Memcached servers:",
"config-memcached-help": "List of IP addresses to use for Memcached.\nShould specify one per line and specify the port to be used. For example:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -304,13 +299,15 @@
"config-install-mainpage-failed": "Could not insert main page: $1",
"config-install-done": "<strong>Congratulations!</strong>\nYou have installed MediaWiki.\n\nThe installer has generated a <code>LocalSettings.php</code> file.\nIt contains all your configuration.\n\nYou will need to download it and put it in the base of your wiki installation (the same directory as index.php). The download should have started automatically.\n\nIf the download was not offered, or if you cancelled it, you can restart the download by clicking the link below:\n\n$3\n\n<strong>Note:</strong> If you do not do this now, this generated configuration file will not be available to you later if you exit the installation without downloading it.\n\nWhen that has been done, you can <strong>[$2 enter your wiki]</strong>.",
"config-install-done-path": "<strong>Congratulations!</strong>\nYou have installed MediaWiki.\n\nThe installer has generated a <code>LocalSettings.php</code> file.\nIt contains all your configuration.\n\nYou will need to download it and put it at <code>$4</code>. The download should have started automatically.\n\nIf the download was not offered, or if you cancelled it, you can restart the download by clicking the link below:\n\n$3\n\n<strong>Note:</strong> If you do not do this now, this generated configuration file will not be available to you later if you exit the installation without downloading it.\n\nWhen that has been done, you can <strong>[$2 enter your wiki]</strong>.",
+ "config-install-success": "MediaWiki has been successfully installed. You can now\nvisit <$1$2> to view your wiki.\nIf you have questions, check out our frequently asked questions list:\n<https://www.mediawiki.org/wiki/Manual:FAQ> or use one of the\nsupport forums linked on that page.",
"config-download-localsettings": "Download <code>LocalSettings.php</code>",
"config-help": "help",
"config-help-tooltip": "click to expand",
"config-nofile": "File \"$1\" could not be found. Has it been deleted?",
- "config-extension-link": "Did you know that your wiki supports [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensions]?\n\nYou can browse [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensions by category] or the [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix] to see the full list of extensions.",
+ "config-extension-link": "Did you know that your wiki supports [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensions]?\n\nYou can browse [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensions by category].",
"config-skins-screenshots": "$1 (screenshots: $2)",
"config-skins-screenshot": "$1 ($2)",
+ "config-extensions-requires": "$1 (requires $2)",
"config-screenshot": "screenshot",
"mainpagetext": "<strong>MediaWiki has been installed.</strong>",
"mainpagedocfooter": "Consult the [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents User's Guide] for information on using the wiki software.\n\n== Getting started ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Configuration settings list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Localise MediaWiki for your language]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Learn how to combat spam on your wiki]"
diff --git a/www/wiki/includes/installer/i18n/eo.json b/www/wiki/includes/installer/i18n/eo.json
index 50c0ec04..c1c5edf9 100644
--- a/www/wiki/includes/installer/i18n/eo.json
+++ b/www/wiki/includes/installer/i18n/eo.json
@@ -37,9 +37,8 @@
"config-env-bad": "La medio estis kontrolita.\nNe eblas instali MediaWiki.",
"config-env-php": "PHP $1 estas instalita.",
"config-env-hhvm": "HHVM $1 instalatas.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] estas instalita.",
"config-apc": "[http://www.php.net/apc APC] estas instalita",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] estas instalita",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] estas instalita",
"config-diff3-bad": "GNU diff3 ne estis trovita.",
"config-db-type": "Tipo de datumbazo:",
"config-db-wiki-settings": "Identigu ĉi tiun vikion",
diff --git a/www/wiki/includes/installer/i18n/es-formal.json b/www/wiki/includes/installer/i18n/es-formal.json
index b15c7f25..da63fdcb 100644
--- a/www/wiki/includes/installer/i18n/es-formal.json
+++ b/www/wiki/includes/installer/i18n/es-formal.json
@@ -2,8 +2,18 @@
"@metadata": {
"authors": [
"Dferg",
- "Seb35"
+ "Seb35",
+ "MarcoAurelio"
]
},
- "mainpagedocfooter": "Consulte usted la [https://meta.wikimedia.org/wiki/Help:Contents/es Guía de usuario] para obtener información sobre el uso del software wiki.\n\n== Empezando ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista de ajustes de configuración]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/es FAQ de MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de correo de anuncios de distribución de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Regionalizar MediaWiki para su idioma]"
+ "config-localsettings-upgrade": "Se ha encontrado un archivo <code>LocalSettings.php</code>.\nPara actualizar esta instalación, escriba el valor de <code>$wgUpgradeKey</code> en el cuadro de abajo.\nLo encontrará en <code>LocalSettings.php</code>.",
+ "config-localsettings-cli-upgrade": "Se ha detectado un archivo <code>LocalSettings.php</code>.\nPara actualizar la instalación, en su lugar ejecute <code>update.php</code>",
+ "config-localsettings-connection-error": "Se ha producido un error al conectar con la base de datos a través de la configuración especificada en <code>LocalSettings.php</code>. Corrija estos ajustes e inténtelo de nuevo.\n\n$1",
+ "config-your-language-help": "Seleccione un idioma para usar durante el proceso de instalación.",
+ "config-page-welcome": "Le damos la bienvenida a MediaWiki.",
+ "config-page-readme": "Léame",
+ "config-help-restart": "¿Desea borrar todos los datos guardados que ha escrito y reiniciar el proceso de instalación?",
+ "config-env-good": "El entorno ha sido comprobado.\nPuede instalar MediaWiki.",
+ "config-env-bad": "El entorno ha sido comprobado.\nNo puede instalar MediaWiki.",
+ "mainpagedocfooter": "Consulte la [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents guía] para obtener información sobre el uso del software wiki.\n\n== Primeros pasos ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista de ajustes de configuración]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Preguntas frecuentes sobre MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de correo de anuncios de publicación de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Traducir MediaWiki a su idioma]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Aprenda a combatir el spam en su wiki]"
}
diff --git a/www/wiki/includes/installer/i18n/es.json b/www/wiki/includes/installer/i18n/es.json
index be2e65a5..8f9e5929 100644
--- a/www/wiki/includes/installer/i18n/es.json
+++ b/www/wiki/includes/installer/i18n/es.json
@@ -33,7 +33,8 @@
"Peter Bowman",
"Dgstranz",
"Irus",
- "Tinss"
+ "Tinss",
+ "KATRINE1992"
]
},
"config-desc": "El instalador de MediaWiki",
@@ -73,28 +74,27 @@
"config-help-restart": "¿Deseas borrar todos los datos guardados que has escrito y reiniciar el proceso de instalación?",
"config-restart": "Sí, reiniciarlo",
"config-welcome": "=== Comprobación del entorno ===\nAhora se van a realizar comprobaciones básicas para ver si el entorno es adecuado para la instalación de MediaWiki.\nRecuerda suministrar los resultados de tales comprobaciones si necesitas ayuda para completar la instalación.",
- "config-copyright": "=== Derechos de autor y Términos de uso ===\n\n$1\n\nEste programa es software libre; puedes redistribuirlo y/o modificarlo en los términos de la Licencia Pública General de GNU, tal como aparece publicada por la Fundación para el Software Libre, tanto la versión 2 de la Licencia, como cualquier versión posterior (según prefieras).\n\nEste programa es distribuido con la esperanza de que sea útil, pero <strong>sin ninguna garantía</strong>; inclusive, sin la garantía implícita de la <strong>posibilidad de ser comercializado</strong> o de <strong>idoneidad para cualquier finalidad específica</strong>.\nConsulta la Licencia Pública General de GNU para más detalles.\n\nEn conjunto con este programa debes haber recibido <doclink href=Copying>una copia de la Licencia Pública General de GNU</doclink>; caso contrario, pídela por escrito a la Fundación para el Software Libre, Inc., 51 Franklin Street, Fifth Floor, Boston, ME La 02110-1301, USA o [http://www.gnu.org/copyleft/gpl.html léela en Internet].",
+ "config-copyright": "=== Derechos de autor y Términos de uso ===\n\n$1\n\nEste programa es software libre; puedes redistribuirlo y/o modificarlo en los términos de la Licencia Pública General de GNU, tal como aparece publicada por la Fundación para el Software Libre, tanto la versión 2 de la Licencia, como cualquier versión posterior (según prefieras).\n\nEste programa es distribuido con la esperanza de que sea útil, pero <strong>sin ninguna garantía</strong>; inclusive, sin la garantía implícita de la <strong>posibilidad de ser comercializado</strong> o de <strong>idoneidad para cualquier finalidad específica</strong>.\nConsulta la Licencia Pública General de GNU para más detalles.\n\nEn conjunto con este programa debes haber recibido <doclink href=Copying>una copia de la Licencia Pública General de GNU</doclink>; caso contrario, pídela por escrito a la Fundación para el Software Libre, Inc., 51 Franklin Street, Fifth Floor, Boston, ME La 02110-1301, USA o [https://www.gnu.org/copyleft/gpl.html léela en Internet].",
"config-sidebar": "* [https://www.mediawiki.org Página principal de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guía del usuario]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guía del administrador]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Preguntas frecuentes]\n----\n* <doclink href=Readme>Léeme</doclink>\n* <doclink href=ReleaseNotes>Notas de la versión</doclink>\n* <doclink href=Copying>Copia</doclink>\n* <doclink href=UpgradeDoc>Actualización</doclink>",
"config-env-good": "El entorno ha sido comprobado.\nPuedes instalar MediaWiki.",
"config-env-bad": "El entorno ha sido comprobado.\nNo puedes instalar MediaWiki.",
"config-env-php": "PHP $1 está instalado.",
"config-env-hhvm": "HHVM $1 está instalado.",
- "config-unicode-using-intl": "Usando la [http://pecl.php.net/intl extensión intl PECL] para la normalización Unicode.",
- "config-unicode-pure-php-warning": "<strong>Advertencia:</strong> la [http://pecl.php.net/intl extensión intl] no está disponible para efectuar la normalización Unicode. Se utilizará la implementación más lenta en PHP puro.\nSi tu web tiene mucho tráfico, te recomendamos leer acerca de la [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalización Unicode].",
- "config-unicode-update-warning": "<strong>Warning:</strong> la versión instalada del contenedor de normalización Unicode usa una versión antigua de la biblioteca del [http://site.icu-project.org/ proyecto ICU].\nDeberás [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations actualizar] si realmente deseas usar Unicode.",
- "config-no-db": "No se encontró un controlador adecuado para la base de datos. Necesitas instalar un controlador de base de datos para PHP.\n{{PLURAL:$2|El siguiente gestor de bases de datos está soportado|Los siguientes gestores de bases de datos están soportados}}: $1.\n\nSi compilaste PHP tú mismo, debes reconfigurarlo habilitando un cliente de base de datos, por ejemplo, usando <code>./configure --with-mysqli</code>.\nSi instalaste PHP desde un paquete Debian o Ubuntu, entonces también necesitas instalar, por ejemplo, el paquete <code>php5-mysql</code>.",
+ "config-unicode-using-intl": "Se utiliza la [https://pecl.php.net/intl extensión «intl» de PECL] para la normalización Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Advertencia:</strong> la [https://pecl.php.net/intl extensión intl] no está disponible para efectuar la normalización Unicode. Se utilizará la implementación más lenta en PHP puro.\nSi tu web tiene mucho tráfico, te recomendamos leer acerca de la [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalización Unicode].",
+ "config-unicode-update-warning": "<strong>Atención:</strong> la versión instalada del contenedor de normalización de Unicode utiliza una versión anticuada de la biblioteca del [http://site.icu-project.org/ proyecto ICU].\nDeberías [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations modernizarla] si te interesa utilizar Unicode.",
+ "config-no-db": "No se encontró un controlador adecuado para la base de datos. Necesitas instalar un controlador de base de datos para PHP.\n{{PLURAL:$2|Se admite el siguiente gestor de bases de datos|Se admiten los siguientes gestores de bases de datos}}: $1.\n\nSi compilaste PHP por tu cuenta, debes reconfigurarlo activando un cliente de base de datos, por ejemplo, mediante <code>./configure --with-mysqli</code>.\nSi instalaste PHP desde un paquete de Debian o Ubuntu, también debes instalar, por ejemplo, el paquete <code>php5-mysql</code>.",
"config-outdated-sqlite": "<strong>Advertencia:</strong> tienes SQLite $1, que es inferior a la mínima versión requerida: $2. SQLite no estará disponible.",
"config-no-fts3": "<strong>Advertencia:</strong> SQLite está compilado sin el [//sqlite.org/fts3.html módulo FTS3]. Las funcionalidades de búsqueda no estarán disponibles en esta instalación.",
"config-pcre-old": "'''Fatal:''' Se requiere PCRE $1 o posterior.\nSu PHP binario está enlazado con PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Más información].",
"config-pcre-no-utf8": "'''Error fatal ''': Parece que el módulo PCRE de PHP fue compilado sin el soporte PCRE_UTF8.\nMediaWiki requiere compatibilidad con UTF-8 para funcionar correctamente.",
"config-memory-raised": "El parámetro <code>memory_limit</code> de PHP es $1. Se aumenta a $2.",
"config-memory-bad": "<strong>Advertencia:</strong> el parámetro <code>memory_limit</code> de PHP es $1.\nProbablemente sea demasiado bajo.\n¡La instalación puede fallar!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] está instalado",
"config-apc": "[http://www.php.net/apc APC] está instalado",
"config-apcu": "[http://www.php.net/apcu APCu] está instalado",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] está instalado",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] está instalado",
"config-no-cache-apcu": "<strong>Advertencia:</strong> No se pudo encontrar [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] o [http://www.iis.net/download/WinCacheForPhp WinCache].\nEl caché de objetos no está activado.",
- "config-mod-security": "<strong>Advertencia:</strong> tu servidor web tiene activado [http://modsecurity.org/ mod_security]/mod_security2. Muchas de sus configuraciones comunes pueden causar problemas a MediaWiki u otro software que permita a los usuarios publicar contenido arbitrario. De ser posible, deberías desactivarlo. Si no, consulta la [http://modsecurity.org/documentation/ documentación de mod_security] o contacta con el administrador de tu servidor si encuentras errores aleatorios.",
+ "config-mod-security": "<strong>Advertencia:</strong> tu servidor web tiene activado [https://modsecurity.org/ mod_security]/mod_security2. Muchas de sus configuraciones comunes pueden causar problemas a MediaWiki u otro software que permita a los usuarios publicar contenido arbitrario. De ser posible, deberías desactivarlo. Si no, consulta la [https://modsecurity.org/documentation/ documentación de mod_security] o contacta con el administrador de tu servidor si encuentras errores aleatorios.",
"config-diff3-bad": "GNU diff3 no se encuentra.",
"config-git": "Se encontró el software de control de versiones Git: <code>$1</code>.",
"config-git-bad": "No se encontró el software de control de versiones Git.",
@@ -107,7 +107,7 @@
"config-using-uri": "Utilizando la URL del servidor \"<nowiki>$1$2</nowiki>\".",
"config-uploads-not-safe": "<strong>Advertencia:</strong> tu directorio predeterminado para las cargas, <code>$1</code>, es vulnerable a la ejecución de scripts arbitrarios.\nAunque MediaWiki comprueba todos los archivos cargados por si hubiese amenazas de seguridad, es altamente recomendable [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security cerrar esta vulnerabilidad de seguridad] antes de activar las cargas.",
"config-no-cli-uploads-check": "<strong>Advertencia:</strong> tu directorio predeterminado para cargas (<code>$1</code>) no está comprobado contra la vulnerabilidad\n de ejecución arbitraria de \"scripts\" durante la instalación por línea de comandos.",
- "config-brokenlibxml": "El sistema tiene una combinación de versiones de PHP y de libxml2 que es poco confiable y puede provocar corrupción oculta en los datos de MediaWiki y otras aplicaciones web.\nActualiza a libxml2 2.7.3 o posterior ([https://bugs.php.net/bug.php?id=45996 bug reportado con PHP]).\nInstalación abortada.",
+ "config-brokenlibxml": "El sistema utiliza una combinación de versiones de PHP y de libxml2 que es poco fiable y puede provocar daños ocultos en los datos de MediaWiki y otras aplicaciones web.\nActualiza a libxml2 2.7.3 o posterior ([https://bugs.php.net/bug.php?id=45996 defecto informado a PHP]).\nSe interrumpió la instalación.",
"config-suhosin-max-value-length": "Suhosin está instalado y limita el parámetro <code>length</code> GET a $1 bytes.\nEl componente ResourceLoader (gestor de recursos) de MediaWiki trabajará en este límite, pero eso perjudicará el rendimiento.\nSi es posible, deberías establecer <code>suhosin.get.max_value_length</code> en el valor 1024 o superior en <code>php.ini</code> y establecer <code>$wgResourceLoaderMaxQueryLength</code> en el mismo valor en <code>php.ini</code>.",
"config-using-32bit": "<strong>Atención:</strong> parece que el sistema funciona con enteros de 32 bits. Esto está [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit desaconsejado].",
"config-db-type": "Tipo de base de datos:",
@@ -192,7 +192,7 @@
"config-mysql-engine": "Motor de almacenamiento:",
"config-mysql-innodb": "InnoDB",
"config-mysql-myisam": "MyISAM",
- "config-mysql-myisam-dep": "<strong>Advertencia:</strong> has seleccionado MyISAM como motor de almacenamiento de MySQL, el cual no está recomendado para usarse con MediaWiki, porque:\n* apenas soporta concurrencia debido al bloqueo de tablas\n* es más propenso a la corrupción que otros motores\n* el código MediaWiki no siempre controla MyISAM como debiera\n\nSi tu instalación de MySQL soporta InnoDB, es muy recomendable que lo elijas en su lugar.\nSi tu instalación de MySQL no soporta InnoDB, quizás es el momento de una actualización.",
+ "config-mysql-myisam-dep": "<strong>Atención:</strong> has seleccionado MyISAM como motor de almacenamiento de MySQL, el cual no está recomendado para usarse con MediaWiki, porque:\n* apenas admite la concurrencia debido al bloqueo de tablas\n* es más propenso a daños que otros motores\n* el código MediaWiki no siempre controla MyISAM como debería\n\nSi tu instalación de MySQL admite InnoDB, es muy recomendable que lo elijas en su lugar.\nSi tu instalación de MySQL no admite InnoDB, quizás es el momento de una modernización.",
"config-mysql-only-myisam-dep": "<strong>Advertencia:</strong> solo se ha encontrado el motor de almacenamiento MyISAM para MySQL en esta máquina, y no se recomienda su uso con MediaWiki, porque:\n* apenas admite la concurrencia debido al bloqueo de tablas\n* es más propenso a daños que otros motores\n* el código de MediaWiki no siempre controla MyISAM como debería\n\nTu instalación de MySQL no admite InnoDB; quizás es el momento de una actualización.",
"config-mysql-engine-help": "<strong>InnoDB</strong> es casi siempre la mejor opción, dado que soporta bien los accesos simultáneos.\n\n<strong>MyISAM</strong> puede ser más rápido en instalaciones con usuario único o de sólo lectura.\nLas bases de datos MyISAM tienden a corromperse más a menudo que las bases de datos InnoDB.",
"config-mysql-charset": "Conjunto de caracteres de la base de datos:",
@@ -221,7 +221,7 @@
"config-admin-password-confirm": "Repite la contraseña:",
"config-admin-help": "Escribe aquí el nombre de usuario que desees, como por ejemplo \"Pedro Bloggs\".\nEste es el nombre que usarás para entrar al wiki.",
"config-admin-name-blank": "Escribe un nombre de usuario de administrador.",
- "config-admin-name-invalid": "El nombre de usuario especificado \"<nowiki>$1</nowiki>\" no es válido.\nEspecifica un nombre de usuario diferente.",
+ "config-admin-name-invalid": "El nombre de usuario especificado, «<nowiki>$1</nowiki>», no es válido.\nEspecifica un nombre de usuario diferente.",
"config-admin-password-blank": "Escribe una contraseña para la cuenta de administrador.",
"config-admin-password-mismatch": "Las dos contraseñas que ingresaste no coinciden.",
"config-admin-email": "Dirección de correo electrónico:",
@@ -252,11 +252,11 @@
"config-license-gfdl": "Licencia de documentación libre de GNU 1.3 o posterior",
"config-license-pd": "Dominio público",
"config-license-cc-choose": "Selecciona una licencia personalizada de Creative Commons",
- "config-license-help": "Muchos wikis públicos ponen todas las contribuciones bajo una [http://freedomdefined.org/Definition licencia libre].\nEsto ayuda a crear un sentido de propiedad comunitaria y alienta la contribución a largo plazo.\nEsto no es generalmente necesario para un wiki privado o corporativo.\n\nSi deseas poder utilizar texto de Wikipedia, y deseas que Wikipedia pueda aceptar el texto copiado de tu wiki, debes elegir <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia utilizaba anteriormente la licencia de documentación libre de GNU (GFDL).\nLa GFDL es una licencia válida, pero es difícil de entender.\nTambién es difícil reutilizar el contenido licenciado bajo la GFDL.",
+ "config-license-help": "Muchos wikis públicos ponen todas las contribuciones bajo una [https://freedomdefined.org/Definition licencia libre].\nEsto ayuda a crear un sentido de propiedad comunitaria y alienta la contribución a largo plazo.\nEsto no es generalmente necesario para un wiki privado o corporativo.\n\nSi deseas poder utilizar texto de Wikipedia, y deseas que Wikipedia pueda aceptar el texto copiado de tu wiki, debes elegir <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia utilizaba anteriormente la licencia de documentación libre de GNU (GFDL).\nLa GFDL es una licencia válida, pero es difícil de entender.\nTambién es difícil reutilizar el contenido licenciado bajo la GFDL.",
"config-email-settings": "Configuración de correo electrónico",
"config-enable-email": "Activar el envío de correos electrónicos",
"config-enable-email-help": "Si quieres que el correo electrónico funcione, la [http://www.php.net/manual/en/mail.configuration.php configuración PHP de correo electrónico] debe ser la correcta.\nSi no quieres la funcionalidad de correo electrónico, puedes desactivarla aquí.",
- "config-email-user": "Habilitar correo electrónico entre usuarios",
+ "config-email-user": "Activar correo electrónico entre usuarios",
"config-email-user-help": "Permitir que todos los usuarios intercambien correos electrónicos si lo han activado en sus preferencias.",
"config-email-usertalk": "Activar notificaciones de páginas de discusión de usuarios",
"config-email-usertalk-help": "Permitir a los usuarios recibir notificaciones de cambios en la página de discusión de usuario, si lo han activado en sus preferencias.",
@@ -266,20 +266,20 @@
"config-email-auth-help": "Si esta opción está habilitada, los usuarios tienen que confirmar su dirección de correo electrónico mediante un enlace que se les envía a ellos cuando éstos lo establecen o lo cambian.\nSolo las direcciones de correo electrónico autenticadas pueden recibir correos electrónicos de otros usuarios o correos electrónicos de notificación de cambios.\nEsta opción está '''recomendada''' para wikis públicos debido a posibles abusos de las características del correo electrónico.",
"config-email-sender": "Dirección de correo electrónico de retorno:",
"config-email-sender-help": "Escribe la dirección de correo electrónico que se usará como dirección de retorno en los mensajes electrónicos de salida.\nAquí llegarán los correos electrónicos que no lleguen a su destino.\nMuchos servidores de correo electrónico exigen que por lo menos la parte del nombre del dominio sea válida.",
- "config-upload-settings": "Subidas de imágenes y archivos",
+ "config-upload-settings": "Cargas de imágenes y archivos",
"config-upload-enable": "Habilitar la subida de archivos",
"config-upload-help": "La subida de archivos potencialmente expone tu servidor a riesgos de seguridad.\nPara obtener más información, consulta la [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security sección de seguridad] en el manual.\n\nPara activar la subida de archivos, cambia el modo en el subdirectorio <code>images</code> bajo el directorio raíz de MediaWiki para que el servidor web pueda escribir en él.\nLuego, activa esta opción.",
"config-upload-deleted": "Directorio para los archivos eliminados:",
"config-upload-deleted-help": "Elige un directorio en el que guardar los archivos eliminados.\nLo ideal es una carpeta no accesible desde la red.",
- "config-logo": "URL del logo :",
+ "config-logo": "URL del logotipo:",
"config-logo-help": "La apariencia predeterminada de MediaWiki incluye espacio para un logotipo de 135x160 píxeles encima del menú de la barra lateral.\nCarga una imagen de tamaño adecuado y escribe la dirección URL aquí.\n\nPuedes usar <code>$wgStylePath</code> o <code>$wgScriptPath</code> si tu logotipo es relativo a esas rutas.\n\nSi no deseas un logotipo, deja esta casilla en blanco.",
- "config-instantcommons": "Habilitar Instant Commons",
+ "config-instantcommons": "Activar Instant Commons",
"config-instantcommons-help": "[https://www.mediawiki.org/wiki/InstantCommons Instant Commons] es una característica que permite que los wikis puedan utilizar imágenes, sonidos y otros archivos multimedia que se encuentran en el sitio [https://commons.wikimedia.org/ Wikimedia Commons].\nPara ello, MediaWiki requiere acceso a Internet.\n\nPara obtener más información sobre esta función, incluidas las instrucciones sobre cómo configurarlo para otras wikis distintas de Wikimedia Commons, consulta [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos el manual].",
"config-cc-error": "El selector de licencia de Creative Commons no dio resultado.\nEscribe el nombre de la licencia manualmente.",
"config-cc-again": "Elegir otra vez...",
"config-cc-not-chosen": "Elige la licencia Creative Commons que desees y haz clic en \"proceed\".",
"config-advanced-settings": "Configuración avanzada",
- "config-cache-options": "Configuración de la caché de objetos:",
+ "config-cache-options": "Configuración de la antememoria de objetos:",
"config-cache-help": "El almacenamiento en caché de objetos se utiliza para mejorar la velocidad de MediaWiki mediante el almacenamiento en caché los datos usados más frecuentemente.\nA los sitios medianos y grandes se les recomienda que permitirlo. También es beneficioso para los sitios pequeños.",
"config-cache-none": "Sin almacenamiento en caché (no se pierde ninguna funcionalidad, pero la velocidad puede resentirse en sitios grandes)",
"config-cache-accel": "Almacenamiento en caché de objetos PHP (APC, APCu, XCache o WinCache)",
@@ -313,9 +313,9 @@
"config-pg-no-create-privs": "La cuenta especificada para la instalación no tiene suficientes privilegios para crear una cuenta.",
"config-pg-not-in-role": "La cuenta especificada para el usuario web ya existe.\nLa cuenta especificada para la instalación no es de un superusuario y no es miembro del grupo de usuarios con acceso a la web, por lo que es incapaz de crear objetos pertenecientes al usuario web.\n\nMediaWiki requiere actualmente que las tablas sean propiedad del usuario web. Especifica otro nombre de cuenta web, o haz clic en \"atrás\" y especifica un usuario de instalación con los privilegios convenientes.",
"config-install-user": "Creando el usuario de la base de datos",
- "config-install-user-alreadyexists": "El usuario \"$1\" ya existe",
- "config-install-user-create-failed": "La creación del usuario \"$1\" falló: $2",
- "config-install-user-grant-failed": "La concesión de permisos al usuario \"$1\" falló: $2",
+ "config-install-user-alreadyexists": "La cuenta de usuario «$1» ya existe",
+ "config-install-user-create-failed": "Falló la creación de la cuenta de usuario «$1»: $2",
+ "config-install-user-grant-failed": "Falló la concesión de permisos a la cuenta de usuario «$1»: $2",
"config-install-user-missing": "El usuario especificado \"$1\" no existe.",
"config-install-user-missing-create": "El usuario especificado \"$1\" no existe.\nHaz clic en la casilla \"Crear cuenta\" debajo si quieres crearlo.",
"config-install-tables": "Creando tablas",
@@ -338,12 +338,14 @@
"config-install-mainpage-failed": "No se pudo insertar la página principal: $1",
"config-install-done": "<strong>¡Felicidades!</strong>\nHas instalado MediaWiki.\n\nEl instalador ha generado un archivo <code>LocalSettings.php</code>.\nEste contiene toda su configuración.\n\nDeberás descargarlo y ponerlo en la base de la instalación de wiki (el mismo directorio que index.php). La descarga debería haber comenzado automáticamente.\n\nSi no comenzó la descarga, o si se ha cancelado, puedes reiniciar la descarga haciendo clic en el siguiente enlace:\n\n$3\n\n<strong>Nota</strong>: si no haces esto ahora, este archivo de configuración generado no estará disponible más tarde si sales de la instalación sin descargarlo.\n\nCuando lo hayas hecho, podrás <strong>[$2 entrar en tu wiki]</strong>.",
"config-install-done-path": "<strong>¡Felicidades!</strong>\nHas instalado MediaWiki.\n\nEl instalador ha generado un archivo <code>LocalSettings.php</code>.\nEste contiene toda su configuración.\n\nDeberás descargarlo y ponerlo en <code>$4</code>. La descarga debería haber comenzado automáticamente.\n\nSi no comenzó la descarga, o si se ha cancelado, puedes reiniciar la descarga haciendo clic en el siguiente enlace:\n\n$3\n\n<strong>Nota</strong>: si no haces esto ahora, este archivo de configuración generado no estará disponible más tarde si sales de la instalación sin descargarlo.\n\nCuando lo hayas hecho, podrás <strong>[$2 entrar en tu wiki]</strong>.",
+ "config-install-success": "Se instaló MediaWiki correctamente. Ahora puedes visitar\n<$1$2> para ver el wiki.\nSi tienes dudas, echa un vistazo a la lista de preguntas frecuentes:\n<https://www.mediawiki.org/wiki/Manual:FAQ>, o bien, utiliza uno de\nlos foros de asistencia que se enumeran en esa página.",
"config-download-localsettings": "Descargar <code>LocalSettings.php</code>",
"config-help": "ayuda",
- "config-help-tooltip": "haz clic para ampliar",
- "config-nofile": "El archivo \"$1\" no se pudo encontrar. ¿Se ha eliminado?",
+ "config-help-tooltip": "pulsa para ampliar",
+ "config-nofile": "No se pudo encontrar el archivo «$1». Quizá se eliminó.",
"config-extension-link": "¿Sabías que tu wiki admite [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensiones]?\n\nPuedes navegar por las [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category categorías] o visitar el [https://www.mediawiki.org/wiki/Extension_Matrix centro de extensiones] para ver una lista completa.",
"config-skins-screenshots": "$1 (capturas de pantalla: $2)",
+ "config-skins-screenshot": "$1 ($2)",
"config-screenshot": "captura de pantalla",
"mainpagetext": "<strong>MediaWiki se ha instalado.</strong>",
"mainpagedocfooter": "Consulta la [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents guía] para obtener información sobre el uso del software wiki.\n\n== Primeros pasos ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista de ajustes de configuración]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Preguntas frecuentes sobre MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de correo de anuncios de publicación de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Traducir MediaWiki a tu idioma]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Aprende a combatir el spam en tu wiki]"
diff --git a/www/wiki/includes/installer/i18n/eu.json b/www/wiki/includes/installer/i18n/eu.json
index 0591cbb0..e94c71b2 100644
--- a/www/wiki/includes/installer/i18n/eu.json
+++ b/www/wiki/includes/installer/i18n/eu.json
@@ -14,10 +14,17 @@
"config-localsettings-upgrade": "<code>LocalSettings.php</code> fitxategi bat detektatu da.\nInstalazioa eguneratzeko, mesedez, sar ezazu <code>$wgUpgradeKey</code> balioa beheko koadroan.\n<code>LocalSettings.php</code> fitxategian aurkituko duzu.",
"config-localsettings-cli-upgrade": "<code>LocalSettings.php</code> fitxategi bat detektatu da.\nInstalazioa eguneratzeko, exekuta ezazu <code>update.php</code>, mesedez",
"config-localsettings-key": "Eguneratze-gakoa:",
+ "config-localsettings-badkey": "Sartu duzun eguneratze-gakoa ez da zuzena.",
+ "config-upgrade-key-missing": "Detektatu egin da dagoeneko MediaWiki instalatu dagoela.\n\nInstalazio hau gaurkotzeko, jarri hurrengo lerroa behekoaldean <code> LocalSettings.php </code>\n\n$1",
+ "config-localsettings-incomplete": "Existitzen den <code>LocalSettings.php</code> bukatu gabe dagoela ematen du.\n$1 aldagaia ez dago finkatuta.\nMesedez, aldatu <code>LocalSettings.php</code>, aldagaia aldatzeko eta gero klikatu {{int:Config-continue}}\".",
+ "config-localsettings-connection-error": "Arazo bat sortu da datu-basearekin konektatzen <code>LocalSettings.php</code>-ean zehaztutako ezarpenak erabilita. Mesedez konpondu ezarpen hauek eta berriro saiatu.\n\n$1",
"config-session-error": "Saio hasierako errorea: $1",
+ "config-session-expired": "Saioren informazio galdu egin dela ematen du.\nSaioak konfiguratutak daude $1 -eko iraupenerako.\nHau handitu ahal duzu <code>code>session.gc_maxlifetime</code> jartzen php.ini -n.\n\nBerrabiatu instalazio prozesua.",
+ "config-no-session": "Saioren informazio galdu egin da!\nEgiaztatu zure php.ini eta ziurtatu <code>session.save_path</code> egoki zaion direktorioan kokatu dagoela.",
"config-your-language": "Zure hizkuntza:",
"config-your-language-help": "Aukeratu instalazio prozesuan erabiliko den hizkuntza",
"config-wiki-language": "Wiki hizkuntza:",
+ "config-wiki-language-help": "Aukeratu nagusiki wikia zein hizkuntzan idatzita egongo da.",
"config-back": "← Atzera",
"config-continue": "Jarraitu →",
"config-page-language": "Hizkuntza",
@@ -35,97 +42,281 @@
"config-page-copying": "Kopiatzea",
"config-page-upgradedoc": "Eguneratu",
"config-page-existingwiki": "Existitzen den wikia",
+ "config-help-restart": "Ezabatu nahi duzu gorde duzun informazio guztia eta berrebiarazi instalazio prozesua?",
"config-restart": "Bai, berriz hasi",
+ "config-welcome": "=== Ingurumen-egiaztapenak ===\n\nOinarrizko kontrola burutzen ari da, ikusteko ia ingurumena aproposa da MediaWikia instalatzeko.\nLaguntza behar izanez gero instalazio prozesua amaitzeko ez ahaztu sartzea informazio hau .",
+ "config-copyright": "=== Copyright eta terminoak ===\n\n$1\n\nPrograma hau software librea da; birbana eta / edo alda dezakezu GNU Lizentzia Publiko Orokorraren baldintzapean, Free Software Foundation-ek argitaratutakoaren arabera; Lizentziaren 2. bertsioa edo (nahiago baduzu) bertsio berriago bat.\n\nPrograma hau baliagarria izango delakoan elkarbantzen da, baina <strong> bermerik gabe </ strong>; <strong> merkaturatze </ strong> edo <strong> helburu jakin baterako gaitasuna</ strong> berme inplizitua ere izan gabe.\nIkus GNU Lizentzia Publiko Orokorra xehetasun gehiagorako.\n\n<Doclink href = Kopiatzea> izan beharko zenuke GNU Lizentzia Publiko Orokorraren kopia </ doclink> programa honekin batera; bestela, idatzi Free Software Foundation-en, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, AEB, edo [https://www.gnu.org/copyleft/gpl.html irakurri ezazu online]. .",
"config-sidebar": "* [https://www.mediawiki.org MediaWiki nagusia]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Erabiltzaileentzako Gida]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administratzaileentzako Gida]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MEG]\n----\n* <doclink href=Readme>Irakur nazazu</doclink>\n* <doclink href=ReleaseNotes>Oharren argitalpena</doclink>\n* <doclink href=Copying>Kopiaketa</doclink>\n* <doclink href=UpgradeDoc>Eguneratzea</doclink>",
+ "config-env-good": "Ingurumena egiaztatu egin da. \nMediaWiki instalatu ahal duzu.",
+ "config-env-bad": "Ingurumena egiaztatu egin da.\nEzin duzu MediaWiki-a instalatu.",
"config-env-php": "PHP $1 instalatuta dago.",
"config-env-hhvm": "HHVM $1 instalatuta dago.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] instalatuta dago",
+ "config-unicode-using-intl": "[https://pecl.php.net/intl intl PECL extension] erabiltzen Unicode-ren normalizaziorako.",
+ "config-unicode-pure-php-warning": "<strong>Oharra:</strong> [https://pecl.php.net/intl intl PECL extension] ez dago prest Unicode-ren normalizazioa jasatzeko,PHP hutsaren ezarpena motelara itzultzen.\n\nTrafiko handiko gune bat exekutatzen baduzu, apur bat irakurri beharko zenuke [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization]-ri buruz.",
+ "config-unicode-update-warning": "<strong>Oharra:</strong> Unicode-ren normalizazioaren bilgarriaren bertsio instalatua [http://site.icu-project.org/ ICU proiektuaren] liburutegia bertsio zaharrago bat erabiltzen du.\n[Https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations upgrade] behar duzu Unicode erabiltzeagatik kezkatuta bazaude.",
+ "config-no-db": "Ezin izan da aurkitu datu-basearen driver egoki bat! Instalatu behar duzu PHP-ko datu-basearen driver bat. Hurrengo datu {{PLURAL:$2|basea|baseak}} onartzen {{PLURAL:$2|da|dira}}:$1\n\n\nPHP-k zuk konpilatu baduzu, berkonfiguratu datu-basearen bezeroarekin gaituta, adibidez, <code>./configure --with-mysqli</code>. erabiliz.\nDebian edo Ubuntu pakete batetik PHPa instalatu baduzu, orduan instalatu behar duzu ere bai hurrengo adibide bezalako bat <code>php5-mysql</code>",
+ "config-outdated-sqlite": "<strong>Warning:</strong> SQLite $1 daukazu, hau da, gutxieneko bertsioa $2 baino atzeratutagoa da. SQLite ez dago erabilgarri.",
+ "config-no-fts3": "<strong>Warning:</strong> SQLite konpilatu egin da [//sqlite.org/fts3.html FTS3 module] barik, \nbilaketa funtzioak ez dira erabilgarri izango \"backend\" honetan.",
+ "config-pcre-old": "<strong>Fatal:</strong> PCRE $1 edo berriagoa behar da.\nZure PHP bitarra PCRE $2 rekin lotzen da.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Informazio gehiago].",
+ "config-pcre-no-utf8": "<strong>Fatal:</strong> PHREko PCRE modulua PCRE_UTF8 ko laguntza gabe bildu da.\nMediaWiki-k UTF-8 euskarria behar du behar bezala funtziona dezan.",
+ "config-memory-raised": "PHP-ko <code>memory_limit</code> $1 da, $2-ra igota.",
+ "config-memory-bad": "<strong>Warning:</strong> PHPko <code>memory_limit</code> $1 da.\nZiurrenik hau oso baxua da.\nInstalazioa huts egin dezake!",
"config-apc": "[http://www.php.net/apc APC] instalatuta dago",
"config-apcu": "[http://www.php.net/apcu APCu] instalatuta dago",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] instalatuta dago",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] instalatuta dago",
+ "config-no-cache-apcu": "<strong>Warning:</strong> Ezin izan da [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] edo [http://www.iis.net/download/WinCacheForPhp WinCache] aurkitu.\nObjektu katxea ez dago aktibatuta.",
+ "config-mod-security": "<strong>Warning:</strong> Zure web zerbitzariak [https://modsecurity.org/mod_security] / mod_security2 aktibatu du. Honen konfigurazio komun asko sortu ahal dituzte arazoak MediaWikin eta beste software batzuetan, hautazko edukia argitaratzeko aukera ematen dutenei erabiltzaileei.\nAhal izanez gero, desgaitu egin beharko litzateke. Bestela, kontsultatu [https://modsecurity.org/documentation/ mod_security documentation] edo jarri harremanetan zure ostalariarekin ausazko akatsak aurkitzen badituzu.",
"config-diff3-bad": "GNU diff3 ez da aurkitu.",
+ "config-git": "Git bertsio-kontrol software aurkitu da: <code>$1</code>",
+ "config-git-bad": "Git bertsio-kontrol software ez da aurkitu.",
+ "config-imagemagick": "ImageMagick aurkitu da: <code>$1</code>.\nIrudi koadro txikiak gaitu egingo dira igoerak gaitzen badituzu.",
+ "config-gd": "Liburutegiko GD grafiko integratua aurkitu da.\nIrudi koadro txikiak gaitu egingo dira igoerak gaitzen badituzu.",
+ "config-no-scaling": "Ezin izan da GD liburutegia edo ImageMagick aurkitu.\nIrudiaren miniatura desgaitu egingo da.",
+ "config-no-uri": "<strong>Errore:</strong> Ezin izan da zehaztu URI. Instalazio geldiarazi egin da.",
+ "config-no-cli-uri": "<strong>Oharra</strong>. Ez da zehaztu <code>--scriptpath</code>, erabiltzen estandar <code>$1</code> .",
"config-using-server": "\"<nowiki>$1</nowiki>\" zerbitzari-izena erabiltzen.",
"config-using-uri": "\"<nowiki>$1$2</nowiki>\" zerbitzariaren URLa erabiltzen.",
+ "config-uploads-not-safe": "<strong>Oharra:</strong> Zure igoerak egiteko <code>$1</code> direktorio lehenetsia gidoi arbitrarioen exekuzioek kaltetu dezakete.\nMediaWiki-k segurtasunerako kargatutako fitxategi guztiak egiaztatzen dituen arren, oso gomendagarria da [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security segurtasun-ahultasun hau itxi] erabiltzea gaitu aurretik.",
+ "config-no-cli-uploads-check": "<strong>Oharra:</ strong> Zure kargatutako direktorio (<code>$1</code>) lehenetsia ez da hauteman ahultasunerako\nscript arbitrarioak exekutatzeko CLI instalazioan zehar.",
+ "config-brokenlibxml": "Zure sistemak dauka PHP-ko eta libxml2-ko konbinazio akastun bat eta eragin ahal du korrupzioa datarekin MediaWikin eta beste web aplikazioetan.\nAktualizatu libxml2 2.7.3-era edo berrietara ([https://bugs.php.net/bug.php?id=45996 bug filed with PHP]).\nInstalazioa geldiarazi egin da.",
+ "config-suhosin-max-value-length": "Suhosin instalatuta dago eta GET parametroaren <code>luzeera</code> $1 byte-ra mugatzen du.\nMediaWiki-ren ResourceLoader osagaia muga honen inguruan lan egingo du, baina horrek errendimendua kaltetu egingo du.\nAhal izanez gero, <code>suhosin.get.max_value_length</code> 1024 edo handiagoa ezarri beharko zenuke <code>php.ini</code>, eta <code>LocalSettings.php</code>-n <code>$wgResourceLoaderMaxQueryLength</code> balio bera ezarri.",
+ "config-using-32bit": "<strong>Oharra:</strong> zure sistemak 32 bit-ekin jarduten duela dirudi. Hau da [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit not advised].",
"config-db-type": "Datu-base mota:",
"config-db-host": "Datu-basearen zerbitzaria:",
+ "config-db-host-help": "Zure datu-basearen zerbitzaria beste zerbitzari batean badago, sartu ostalariaren izena edo IP helbidea hemen.\n\nPartekatutako web-ostatua erabiltzen ari bazara, zure ostalaritza-hornitzaileak dokumentazio-ostalariaren izen egokia eman beharko lizuke.\n\nWindows zerbitzari batean instalatzen bazara eta MySQL erabiliz, \"localhost\" agian ez du zerbitzariaren izenerako funtzionatuko. Ez badago, saiatu \"127.0.0.1\" tokiko IP helbideetarako.\n\nPostgreSQL erabiltzen ari bazara, utzi eremu hau hutsik Unix socket bidez konektatzeko.",
"config-db-host-oracle": "Datu-baseko TNS:",
+ "config-db-host-oracle-help": "Sartu baliozko [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Konekzio izan lokala]; instalazio honetarako tnsnames.ora fitxategia ikusgai egon behar da. <br/> Bezeroen 10g liburutegiak edo berriagoak erabiltzen ari bazara, [http://download.oracle.com/docs/cd/E11882_01/network.112 ere erabil dezakezu. /e10836/naming.htm Konektatzeko erraza] izendatzeko metodoa.",
"config-db-wiki-settings": "Wiki hau identifikatu",
"config-db-name": "Datu-base izena:",
+ "config-db-name-help": "Aukeratu zure Wikia identifikatzen duen izena.\nEzin dira espazioak eabili.\n\nErabiltzen ari bazara web hosting partekatua, hostin-eko hornitzaileak emango dizu datu-basearen izen espezifikoa edo kontrol panel baten bitzrtez zure datu-basea sortzea utziko dizu.",
"config-db-name-oracle": "Datu-baseko eskema:",
+ "config-db-account-oracle-warn": "Hiru euskarri onartzen dira Oracle datu-basearen euskarri gisa instalatzeko:\n\nInstalazio-prozesuaren zati gisa datu-basearen kontua sortu nahi baduzu, hornitu kontu bat SYSDBA rol datu-baseko kontu gisa instalatzeko eta webgunerako sarbide konturako nahi dituzun kredentzialak zehazteko; bestela, web-sarbideen kontua eskuz sortu eta hornitu kontu hori bakarrik (eskemaren objektuak sortzeko baimenak behar baditu) edo bi kontu ezberdin, bi pribilegio sortu eta sarbide mugatua eskaintzen dutenak.\n\nBeharrezko baimenak dituen kontu bat sortzeko gidoia instalazio honen \"mantentze/orakulu/\" direktorioan aurki daiteke. Kontuan izan kontu mugatu bat erabiliz kontu lehenetsiarekin mantentze-gaitasun guztiak desgaituko dituela.",
+ "config-db-install-account": "Instalazio prozesuan erabili erabiltzaile kontua.",
"config-db-username": "Datu-base lankide izena:",
"config-db-password": "Datu-base pasahitza:",
+ "config-db-install-username": "Sartu erabiliko duzun erabiltzaile izena datu-basearikn konektzatzeko instalazio prozesuaren bitartean.\nHau ez da MediaWikiaren erabiltzaile izanea; hau da datu-basearen erabiltzaile izena da.",
+ "config-db-install-password": "Sartu erabiliko den pasahitza datu-basea konektatzeko instalazio prozesuan zehar.\n\nHau ez da MediaWikiaren pasahitza; hau da zure datu-basearen pasahitza.",
+ "config-db-install-help": "Sartu instalazio prozesuan zehar datu-basearekin konektatzeko erabiliko den erabiltzaile izena eta pasahitza.",
+ "config-db-account-lock": "Operazio arruntentan erabili erabiltzaile izena eta pasahitza berdina",
+ "config-db-wiki-account": "Operazio arruntentaten erabili erabiltzaile kontua.",
+ "config-db-wiki-help": "Operazio arruntetan erabiliko den erabiltzaile izena eta pasahitza sartu.\n\nKontua ez bada existitzen, eta instalazio kontua pribilegio nahikoak baditu, erabiltzaile kontu hau sortuko da wikia erabiltzeko behar diren pribilegio minimoekin.",
+ "config-db-prefix": "Datu-basearen taularen aurrizkiak:",
+ "config-db-prefix-help": "Datu-base bat elkarbanatu behar baduzu hainbat wiki orrien artean, edota MediaWiki eta best web aplikazio batekin, taula guztiei aurrizki bat gehitzea aukeratu ahal dezakezu, gatazkak ez sortzeko.\nEz erabili zuriunerik.\n\nAtal hau normalean utsik uzten da.",
+ "config-mysql-old": "MySQL $1 edo berriagoa behar da. Zuk $2 badaukazu.",
"config-db-port": "Datu-basearen ataka:",
"config-db-schema": "MediaWikirako eskema:",
+ "config-db-schema-help": "Patroi hau normalean egokia da. Bakarrik aldatu beharrezkoa bada.",
+ "config-pg-test-error": "Ezin da datu-basearekin konektatu <strong>$1</strong>: $2",
+ "config-sqlite-dir": "SQLite -eko informazioaren direktorioa:",
+ "config-sqlite-dir-help": "SQLite-k datu guztiak fitxategi bakarrean gordetzen ditu.\n\nHornitu duzun direktorioa web zerbitzariaren bidez idatzia izateko aukera eman beharko duu instalazioan zehar.\n\n<Strong>Ez</strong> da webgunearen bidez eskuragarri egon behar; horregatik zure PHP fitxategiak non dauden ez dugu erakutsi.\n\nInstalatzaileak <code>.htaccess</code> fitxategi bat idatziko du bertan, baina horrek huts egiten badu zure datu base gordinera norbait sar daiteke.\nErabiltzaileen datu gordinak (helbide elektronikoak, pasahitzak), ezabatutako berrikusketa eta gainontzeko datu mugatuak ere barnean hartuz.\n\nDatu-basea beste nonbait jartzearen inguruan hausnartu, adibidez, <code>/var/lib/mediawiki/yourwiki</code>-n.",
+ "config-oracle-def-ts": "Taula-toki lehenetsia:",
+ "config-oracle-temp-ts": "Aldi baterako taula:",
"config-type-mysql": "MySQL (edo bateragarria)",
"config-type-postgres": "PostgreSQL",
"config-type-sqlite": "SQLite",
"config-type-oracle": "Oracle",
"config-type-mssql": "Microsoft SQL Server",
+ "config-support-info": "MediaWiki-k onartzen du hurrengo datu-base sistemak:\n\n$1\n\nListan ez baduzu ikusten erabili nahi duzun sistema, jarraitu goiko argibideak aktibatzeko.",
+ "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] MediaWikiren lehenengoko helburua da eta primeran babesturik dago. MediaWikik ere [{{int:version-db-mariadb-url}} MariaDB]-rekin egiten du lan baita [{{int:version-db-percona-url}} Percona Server]-kin, MySQL-rekin balio dutenak. ([http://www.php.net/manual/en/mysqli.installation.php How to compile PHP with MySQL support])",
+ "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] iturburu irekiko datu basea sistema famatua da MySQL-rako alternatiba bezala. ([http://www.php.net/manual/en/pgsql.installation.php How to compile PHP with PostgreSQL support])",
+ "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] oso ondo onartzen duen datu-basearen sistema arina da.\n ([http://www.php.net/manual/en/pdo.installation.php How to compile PHP with SQLite support], uses PDO)",
+ "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] enpresa komertzial baten datu-basea da. ([http://www.php.net/manual/en/oci8.installation.php How to compile PHP with OCI8 support])",
+ "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] Windows-entzako enpresa komertzial baten datu-basea da. ([http://www.php.net/manual/en/sqlsrv.installation.php How to compile PHP with SQLSRV support])",
"config-header-mysql": "MySQL hobespenak",
"config-header-postgres": "PostgreSQL hobespenak",
"config-header-sqlite": "SQLite hobespenak",
"config-header-oracle": "Oracle hobespenak",
"config-header-mssql": "Microsoft SQL Server-en ezarpenak",
"config-invalid-db-type": "Datu-base mota baliogabea.",
+ "config-missing-db-name": "\"{{int:config-db-name}}\"-rentzako balioa sartu behar duzu.",
+ "config-missing-db-host": "\"{{int:config-db-host}}\"-rentzako balioa sartu behar duzu.",
+ "config-missing-db-server-oracle": "\"{{int:config-db-host-oracle}}\"-rentzako balioa sartu behar duzu.",
+ "config-invalid-db-server-oracle": "\"$1\" TNS datu basea baliogabea.\nErabili \"TNS izena\" edo \"Konektagarritasun erraza\" katea ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).",
+ "config-invalid-db-name": "Datu-basearen izen okerra \"$1\"\nErabil ezazu ASCII letrak bakarrik (a-z, A-Z), zenbakiak (09), behe-gidoiak (_) eta gidoiak (-)",
+ "config-invalid-db-prefix": "Datu-basearen aurrizki okerra \"$1\"\nErabil ezazu ASCII letrak bakarrik (a-z, A-Z) behe-gidoiak (_) eta gidoiak (-)",
+ "config-connection-error": "$1\n\nHost-a, erabiltzaile izena eta pasahitza egiaztatu eta saiatu berriro.",
+ "config-invalid-schema": "MediaWikiko eskema okerra \"$1\"\nErabil ezazu ASCII letrak bakarrik (a-z, A-Z) behe-gidoiak (_).",
+ "config-db-sys-create-oracle": "Instalatzaileak bakarrik jasaten du SYSBDA kontu bat erabiltzaile kontu berri bat sortzeko.",
"config-db-sys-user-exists-oracle": "$1 erabiltzaile kontua dagoeneko existitzen da. SYSDBA kontu berri bat sortzeko erabili daiteke soilik!",
+ "config-postgres-old": "PostgreSQL $1 edo berriagoa behar da. Zuk $2 badaukazu.",
+ "config-mssql-old": "Microsoft SQL Server $1 edo berriagoa behar da. Zuk $2 badaukazu.",
+ "config-sqlite-name-help": "Aukeratu zure wikia identifikatzen duen izen bat.\nEz erabili zuriunerik edo gidoirik.\nHau erabiliko da SQLite datuen artxiborako.",
+ "config-sqlite-parent-unwritable-group": "Ezin da datu-direktorioa sortu <code><nowiki>$1</nowiki></code>, web zerbitzariak ezin baitu <code><nowiki>$2</nowiki></code> guraso direktorioan idatzi.\n\nInstalatzaileak webgunea exekutatzen ari den bitartean zure erabiltzailea zehaztu du.\nEgin <code><nowiki>$3</nowiki></code> direktorioan idazteko gai izatea jarraitzeko.\nUnix/Linux sistema batean:\n\n<pre>cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3</pre>",
+ "config-sqlite-parent-unwritable-nogroup": "Ezin da datu-direktorioa sortu <code><nowiki>$1</nowiki></code>, web zerbitzariak ezin baitu <code><nowiki>$2</nowiki></code> guraso direktorioan idatzi.\n\nInstalatzaileak webgunea exekutatzen ari den bitartean zure erabiltzailea zehaztu dezake.\nEgin <code><nowiki>$3</nowiki></code> direktorioa globalean idazteko gai izatea (horretarako eta besteentzako!) jarraitzeko.\nUnix/Linux sistema batean:\n\n<pre>cd $2\nmkdir $3\nchmod a+w $3</pre>",
+ "config-sqlite-mkdir-error": "Arazo bat sortu da datuen direktorioa sortzerakoan \"$1\".\nLokalizazio egiaztatu eta berriro saiatu.",
+ "config-sqlite-dir-unwritable": "Ezin izan da \"$1\" direktoriora idatzi.\nAldatu baimenak web-serbidoreak idatzi ahal izateko, eta berriro saiatu.",
+ "config-sqlite-connection-error": "$1.\n\nDatu direktorioa eta datu-basea egiaztatu eta berriro saiatu.",
"config-sqlite-readonly": "Ezin da idatzi <code>$1</code> fitxategian.",
+ "config-sqlite-cant-create-db": "Ezin izan da <code>$1</code> datu-basearen artxiboa sortu.",
+ "config-sqlite-fts3-downgrade": "PHPn FTS3 laguntza falta da, taulen gradua jeisten.",
+ "config-can-upgrade": "Datu base honetan MediaWiki taulak daude.\nMediaWiki $1ra graduz igotzeko, <strong>Jarraitu</strong> klikatu.",
+ "config-upgrade-done": "Berritzea burutu da.\n\nOrain [$1 zure wiki-a erabiltzen hasi] ahal zara.\n\nZure <code>LocalSettings.php</code> fitxategia birsortzea nahi baduzu, egin klik beheko botoian.\nHau <strong>ez da gomendagarria</strong> zure wikiarekin arazoak izan ezean.",
+ "config-upgrade-done-no-regenerate": "Eguneratze prozesua amaitu egin da.\n\nHasi ahal zara [ $1 wikia arabiltzen]",
"config-regenerate": "Birsortu LocalSettings.php →",
+ "config-show-table-status": "<code>SHOW TABLE STATUS</code> kontsulta huts egin du!",
+ "config-unknown-collation": "<strong>Abisua:</strong> Datu-baseak kolazio ezezaguna ari da erabiltzen.",
+ "config-db-web-account": "Datu-basearen kontua web sarbiderako.",
+ "config-db-web-help": "Aukeratu erabiliko den erabiltzaile izena eta pasahitza web serbidorea eta datu-basearen serbidorea konektatzeko, wikiren operazio normalaren bitartean.",
+ "config-db-web-account-same": "Instalazioan erabili duzun kontu berdina erabili.",
+ "config-db-web-create": "Kontua sortu oraindik ez bada existitzen.",
+ "config-db-web-no-create-privs": "Zehaztu duzun kontuak ez dauka pribilegio nahikoak kontu bat sortzeko.\nZehaztu duzun kontua existitu behar da.",
+ "config-mysql-engine": "Biltegiratze motorea:",
"config-mysql-innodb": "InnoDB",
"config-mysql-myisam": "MyISAM",
+ "config-mysql-myisam-dep": "<strong>Oharra:</strong> MyISAM MySQL biltegiratze-motor gisa aukeratu duzu, MediaWikirekin erabiltzeko gomendagarria ez dena honengatik:\n*taula blokeoak direla-eta gauza gutxi onartu ohi du\n*beste motore batzuek baino ustelkeria gehiago izateko aukerak ditu\n*MediaWiki-ren kode baseak ez du beti kudeatzen MyISAM behar bezala\n\nZure MySQL instalazioa InnoDB onartzen badu, hori aukeratzeko gomendatzen da.\nZure MySQL instalazioa InnoDB ez badu onartzen, baliteke bertsioa berritzeko ordua izatea.",
+ "config-mysql-only-myisam-dep": "<strong> Oharra: </strong> MyISAM makinaren MySQL biltegiratze motarako bakarra da, eta hau ez da MediaWiki-rekin erabiltzeko gomendatzen, honengatik:\n* maiztasunez taula blokeoek konkurrentzia ez dute onartzen \n* Beste motore batzuek baino ustelkeria gehiago izaten dute\n* MediaWiki-ren kodekak ez du beti kudeatzen MyISAM behar bezala\n\nZure MySQL instalazioak ez du InnoDB onartzen, agian bertsio berritzeko ordua da.",
+ "config-mysql-engine-help": "<strong>InnoDB</strong> ia beti aukerarik onena da, konkurrentzia-laguntza ona duelako.\n\n<strong>MyISAM</strong> erabiltzaile bakarreko edo irakurketa bakarreko instalazioetan azkarragoa izan daiteke.\nMyISAM datu-basea gehiagokotan hondatuta ageri da InnoDB datu-baseareakin baino.",
+ "config-mysql-charset": "Datu-basearen karaktere multzoa:",
"config-mysql-binary": "Bitarra",
"config-mysql-utf8": "UTF-8",
+ "config-mysql-charset-help": "<strong>Modu bitarrean</strong>, MediaWiki-k UTF-8 testua datu-baseko eremu bitarretan gordetzen du.\nHau MySQL-en UTF-8 modua baino eraginkorragoa da eta Unicode karaktereen barruti osoa erabiltzea ahalbidetzen du.\n\n<Strong>UTF-8 moduan</strong>, MySQL-k jakingo du zer karakterean zure datuak konfiguratzen dituen, aurkeztu eta behar bezala bihurtzeko, baina ez dizkizu karaktereak [https://en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Oinarrizko Eleaniztasun Plana]-ren gainetik gordetzen utziko.",
+ "config-mssql-auth": "Autentifikazio mota:",
+ "config-mssql-install-auth": "Aukeratu instalazio prozesuan zehar datu-basera konektatzeko erabiliko den autentifikazio mota.\n\"{{Int: config-mssql-windowsauth}}\" hautatzen baduzu, web zerbitzariak duen edozein erabiltzailek erabiliko duen kredentziala erabiliko da.",
+ "config-mssql-web-auth": "Aukeratu instalazio prozesuan zehar datu-base zerbitzariari konektatzeko erabiliko den autentifikazio mota.\n\"{{Int: config-mssql-windowsauth}}\" hautatzen baduzu, web zerbitzariak duen edozein erabiltzailek erabiliko duen kredentziala erabiliko da.",
+ "config-mssql-sqlauth": "SQL Serbidorearen Autentifikazioa",
+ "config-mssql-windowsauth": "Windows-eko Autentifikazioa.",
"config-site-name": "Wikiaren izena:",
+ "config-site-name-help": "Hau nabigatzailearen tituluaren lerroan agertuko da eta pare bat leku gehiagotan.",
+ "config-site-name-blank": "Aukeratu webgunearen izena.",
"config-project-namespace": "Proiektuaren izen-tartea:",
"config-ns-generic": "Proiektua",
+ "config-ns-site-name": "Wiki izenaren berdina: $1",
"config-ns-other": "Bestelakoa (zehaztu)",
"config-ns-other-default": "MyWiki",
+ "config-project-namespace-help": "Wikipedia-ren adibidea jarraitzen, wiki askok beren orrien politika mantentzen dute beren edukien orrialdeetatik bereizita, '' 'proiektuaren izen-eremuan' ''.\nOrrialde honetako izenburu guztiek aurrizki jakin batekin hasten dira, hemen zehaztu ahal direnak.\nNormalean, aurrizkia wikiaren izenetik dator, baina ezin du \"#\" edo \":\" puntuazio-karaktereak eduki.",
+ "config-ns-invalid": "Zehaztutako \"<nowiki>$1</nowiki>\" izena baliogabea da.\nZehaztu beste proiektu baten izenaren eremua.",
+ "config-ns-conflict": "\"<nowiki>$1</ nowiki>\" zehaztutako izen-eremuak lehenetsitako MediaWiki izen-eremu batekin gatazkan ari da.\nZehaztu beste proiektu izen-eremu bat.",
"config-admin-box": "Administratzaile kontua",
"config-admin-name": "Zure erabiltzaile-izena:",
"config-admin-password": "Pasahitza:",
"config-admin-password-confirm": "Pasahitza berriz:",
+ "config-admin-help": "Sartu zure gustokoen erabiltzaile izena hemen, adibidez \"Joe Bloggs\".\nHau da erabiliko duzun izena wikian sartzeko.",
+ "config-admin-name-blank": "Sartu administratzaile kontua.",
+ "config-admin-name-invalid": "<nowiki>$1</nowiki> erabiltzaile izena baliogabea da.\nZehaztu erabiltzaile izen desberdin bat.",
+ "config-admin-password-blank": "Sartu pasahitza administratzaile kontuarentzako.",
"config-admin-password-mismatch": "Sartutako bi pasahitzak ez datoz bat.",
"config-admin-email": "E-posta helbidea:",
+ "config-admin-email-help": "Sartu email bat baimena emateko mezuak jasotzeko, pasahitza aldatzeko and orrien aldaketeei buruz berri edukitzeko.\nHutsik utzi ahal duzu.",
+ "config-admin-error-user": "Barneko errorea \"<nowiki>$1</ nowiki>\" izeneko administratzailea sortzerakoan.",
+ "config-admin-error-password": "Barne-arazoa administratzailearen pasahitza sortzerakoan.\"<nowiki>$1</nowiki>\". <pre>$2</pre>",
"config-admin-error-bademail": "Helbide elektroniko okerra idatzi duzu.",
+ "config-subscribe": "Harpidetu [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce posta zerrenda bidez egindako iragarki ohar] zerrendara.",
+ "config-subscribe-help": "Hau bolumen baxuko oharren iragarkietarako erabiltzen den zerrenda da, segurtasun iragarki garrantzitsuak barne.\nHarpidetu horretara eta zure MediaWiki instalazioa eguneratu bertsio berriak ateratzean.",
+ "config-subscribe-noemail": "Ohar iragarkien posta elektroniko zerrendara harpidetzen saiatu zara, helbide elektroniko bat eman gabe.\nEman ezazu helbide elektronikoa posta zerrendan harpidetzea nahi baduzu.",
+ "config-pingback": "Elkarbanatu informazioa instalazio prozesuari buruz MediaWiki-ko sustatzaileekin.",
+ "config-pingback-help": "Aukera hau hautatzen baduzu, MediaWiki-k https://www.mediawiki.org guneari periodikoki ping egingo dio MediaWiki-ren instantzia honi buruzko oinarrizko datuekin. Datu horietan, adibidez, sistema mota, PHP bertsioa eta hautatutako datu-base motorra agertzen dira. Wikimedia Fundazioak datu hauek partekatzen dituu MediaWiki garatzaileekin etorkizuneko garapen-ahaleginak gidatzeko. Ondorengo datuak zure sistemara bidaliko dira:\n<pre>$1</pre>",
+ "config-almost-done": "Ia amaitu duzu!\nFalta den konfigurazioa saltatu ahal duzu eta zuzenean wikia instalatu.",
"config-optional-continue": "Galdera gehiago egin.",
"config-optional-skip": "Aspertuta nago, wikia instalatu bakarrik.",
+ "config-profile": "Erabiltzailearen profilaren eskubideak:",
"config-profile-wiki": "Wikia ireki",
"config-profile-no-anon": "Kontua sortzea beharrezkoa da",
"config-profile-fishbowl": "Baimendutako editoreak bakarrik",
"config-profile-private": "Wiki pribatua",
+ "config-profile-help": "Wikiak hobeto funtzionatzen dute ahalik eta jende gehiagok editatzeko aukera duenean.\nMediaWikian, erraza da azken aldaketak berrikustea eta erabiltzaile desegokiek egindako kalteak konpontzea.\n\nHala eta guztiz ere, askok MediaWiki rol ezberdinetarako baliagarri ikusi dute, nahiz eta batzuetan erraza ez izan wikiaren onureen inguruan konbentzitzea.\nBeraz, aukera zuk egiten duzu.\n\n<Strong>{{int:config-profile-wiki}}</strong> ereduak edonork edita dezake, nahiz eta saioa hasi.\n<Strong>{{int:config-profile-no-anon}}</strong> -ko wiki batek aparteko erantzukizuna ematen du, baina aldi baterako laguntzaileak alda ditzake.\n\n<Strong>{{int:config-profile-fishbowl}}</strong> planteamenduak aukera ematen du baimendutako erabiltzaileek editatzeko, baina publikoak orrialdeak ikusi ditzake, historiak barne.\n<Strong>{{int:config-profile-private}}</strong> bat soilik onartutako erabiltzaileei orriak ikusteko aukera ematen die, talde berak editatu ahal izateko.\n\nErabiltzaileen eskubideen ezarpen konplexuagoak eskuragarri daude instalazioa egin ondoren, ikus ezazu [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights eskuzko sarrera garrantzitsua].",
"config-license": "Copyright eta lizentzia:",
+ "config-license-none": "Ez jarri lizentzia orriaren baimenik",
+ "config-license-cc-by-sa": "Creative Commons-eko esleipen-lizentzia",
"config-license-cc-by": "Creative Commons Aitorpena",
+ "config-license-cc-by-nc-sa": "Creative Commons Attribution-NonCommercial-ShareAlike",
"config-license-cc-0": "Creative Commons Zero (Jabari Publikoa)",
+ "config-license-gfdl": "\nGNU Free Documentation License 1.3 edo berriagoa",
"config-license-pd": "Domeinu Askea",
"config-license-cc-choose": "Aukeratu Creative Commons lizentzia pertsonalizatua",
+ "config-license-help": "Wikilari publiko askok ekarpen guztiak jartzen dituzte [https://freedomdefined.org/Definition lizentzia aske] azpian.\nHonek komunitatearen jabetza zentzuaren kontzeptua sortzen laguntzen du eta epe luzerako ekarpena bultzatzen du.\nEz da, oro har, wiki pribatu edo korporatiborik behar.\n\nWikipediatik testua erabiltzeko aukera izan nahi baduzu eta Wikipediak zure wikietatik kopiatutako testua onartzeko gai izatea nahi baduzu, <strong>{{int:config-license-cc-by-sa}}</strong> aukeratu beharko zenuke.\n\nWikipedia lehenago erabili izan du GNU Dokumentazio Librearen Lizentzia.\nGFDL baliozko lizentzia da, baina ulertzeko zaila da.\nGFDLren baimenarekin lotutako edukiak berrerabiltzea ere zaila da.",
"config-email-settings": "E-posta hobespenak",
+ "config-enable-email": "Aktibatu irteerako emaila.",
+ "config-enable-email-help": "Lan egiteko email-a nahi baduzu, [http://www.php.net/manual/en/mail.configuration.php PHP's mail settings] ondo konfiguratu egin behar da. Email ezaugarririk ez baduzu nahi, hemen kendu ditzakezu.",
+ "config-email-user": "Aktibatu erabiltzaileen arteko emaila.",
+ "config-email-user-help": "Baimena eman erabiltzaileei beraien artean emailak bidaltzeko, lehentasunetan aukera aktibatuta badaukate.",
+ "config-email-usertalk": "Aktibatu erabiltzaileen eztabaida orrien jakinarazpena",
+ "config-email-usertalk-help": "Erabiltzailei baimena eman jakinarazpenak jasotzeko eztabaidan orrietako aldaketei buruz, aukera hau aktibatu badaukate.",
+ "config-email-watchlist": "Jarraipen listaren jakinarazpenak aktibatu.",
+ "config-email-watchlist-help": "Baimena eman erabiltzaileei jakinarazpenak jasotzeko haiek ikusitako orriei buruz, lehenespenetan aktibatu badaukate.",
+ "config-email-auth": "Aktibatu emailaren autentifikazioa.",
+ "config-email-auth-help": "Aukera hau aktibatuta badago, erabiltzaileak konfirmatu behar du bere emaila, sortzerakoen edota aldetzerakoan bidali zaion linka erabiltzen.\n\nBakarrik kautotaku emailak gai izango dira beste erabiltzaileen emailak jasotzeko edota jakinarazpen emailak aldatzeko.\n\nAukera hau hautatzea <strong>gomendagarria</strong> da Wiki publikoentzat, emaileen erramintien abusua dela eta.",
"config-email-sender": "Itzuli helbide elektronikoa:",
+ "config-email-sender-help": "Idatzi helbide elektronikoa bueltan mezuak jasotzeko helbide elektroniko gisa.\nErreboteak bidaliko dira horra.\nPosta-zerbitzari askok gutxienez domeinu izenaren zati bat behar dute baliozkoa izan dadin.",
"config-upload-settings": "Irudi eta fitxategi igoerak",
"config-upload-enable": "Fitxategi igoera gaitu",
+ "config-upload-help": "Fitxategiak kargatzeak zure zerbitzaria segurtasun arriskuei eragin diezaioke.\nInformazio gehiagorako, irakurri [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security security section] eskuliburuan.\n\nFitxategiak igotzea aktibatzeko, aldatu <code> images </ code> subdirektorio modua MediaWiki-ren erroko direktorioaren azpian, web zerbitzariak honela idatz dezake.\nOndoren, gaitu aukera hau.",
"config-upload-deleted": "Ezabatutako artxiboentzako direktorioa:",
+ "config-upload-deleted-help": "Aukeratu fitxategi bat ezabatutako artxiboak gordetzeko.\nGomendagarria da hau webgunetik ez egotea eskuragarri.",
"config-logo": "Logo URL:",
+ "config-logo-help": "MediaWiki-ren lehenetsitako azala alboko barrako menuaren gainetik 135x160 pixeleko logotipo batentzako espazioa dauka.\nKargatu tamaina egokia duen irudia, eta sartu URLa hemen.\n\n<code>$wgStylePath</code> edo <code>$wgScriptPath</code> erabil dezakezu zure logotipoa bide horiei egokitzekotan.\n\nLogotipo bat nahi ez baduzu, utzi lauki hau hutsik.",
"config-instantcommons": "Instant Commons gaitu",
+ "config-instantcommons-help": "[https://www.mediawiki.org/wiki/InstantCommons Instant Commons] aukerak [https://commons.wikimedia.org/ Wikimedia Commons] gunean wikietan aurkitutako irudiak, soinuak eta beste multimedia batzuk erabiltzeko aukera ematen du.\nHorretarako, MediaWiki-k Interneteko sarbidea behar du.\n\nEginbide honi buruzko informazio gehiago lortzeko, wikientzako konfiguratzeko argibideak barne eta Wikimedia Commons-tik at, kontsultatu [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos eskuliburua].",
+ "config-cc-error": "Creative Commons lizentziaren aukeratzaileak ez du emaitzarik eman.\nEskuz sartu lizentzia.",
"config-cc-again": "Berriz aukeratu...",
+ "config-cc-not-chosen": "Aukeratu ze Creative Common lizentzi nahi duzu eta sakatu \"proceed\"",
"config-advanced-settings": "Konfigurazio aurreratua",
+ "config-cache-options": "Objektu cachearen ezarpenak:",
+ "config-cache-help": "Objektuen katxea erabiltzen da MediaWikiko abiadura hobetzeko, sarritan erabiltzen diren datuak gordetzen.\nOso gomendagarria da, webgune handientzako eta ertainentzako, webgune txikiek ere ikusiko dituzte onurak.",
+ "config-cache-none": "Desaktibatu Katxina (ez dira funtzionaltasunak ezabatu, baina wiki orrialde handietan abiaduran eragina izan ahal du)",
+ "config-cache-accel": "PHP objetuen katxea (APC, APCu, XCache edo WinCache)",
+ "config-cache-memcached": "Memcached erabili (konfigurazio eta instalazio gehiago behar du)",
+ "config-memcached-servers": "Memcached serbidoreak:",
+ "config-memcached-help": "Memcached-ekin erabiltzeko IP helbideen lista.\nLerro bakoitzen bat bakarrik jarri behar da eta zehaztu ze ataka erabiliko den. Adibidez:\n127.0.0.1:11211\n192.168.1.25:1234",
+ "config-memcache-needservers": "Memcached aukeratu duzu zure katxe mota bezala baina ez duzu zehaztu inolako serbidorerik.",
+ "config-memcache-badip": "Sartu duzu Memcached-eko baliogabeko IP bat: $1",
+ "config-memcache-noport": "Ez duzu zehaztu portu bat Memcached serbidorerako: $1\nEz badakizu portua, estandarra 11211 da.",
+ "config-memcache-badport": "Memcached-eko ataka zenbakiak $1 eta $2 artean egon behar dira.",
"config-extensions": "Luzapenak",
+ "config-extensions-help": "Goian zerrendatutako luzapenak zure <code> ./ extensions </ code> direktorioan detektatu dira.\n\nBeharbada konfigurazio gehaigo behar dute, baina orain gaitu ditzakezu.",
"config-skins": "Itxurak",
+ "config-skins-help": "Goian aipatutako itxurak zure <code>./skins</code> direktorioan aurkitu dira. Bat gutxienez aktibatu behar duzu, eta estandarra aukeratu.",
+ "config-skins-use-as-default": "Erabili ikusizko estilo hau lehenezpen gisa.",
+ "config-skins-missing": "Ez dira itxurarik aurkitu, MediaWiki-ak oinarrizko itxura bat erabiliko du, zuk itxura egokiak instalatu arte.",
+ "config-skins-must-enable-some": "Gutxienez aukeratu behar duzu ikusizko estilo bat aktibatzeko.",
+ "config-skins-must-enable-default": "Lehenetsia bezala aukeratu duzun ikusizko estilo aktibatuta egon behar da.",
+ "config-install-alreadydone": "<strong>Oharra:</strong>Badirudi MediaWikia instalatu daukazula eta berriz instalatzen saiatzen ari zarela.\n\nMesedez, hurrengo orrian jarraitu",
+ "config-install-begin": "\"{{int:config-continue}}\" sakatuz, MediaWiki instalazioa hasiko duzu.\nAldaketak oraindik egin nahi badituzu, sakatu \"{{int:config-back}}\".",
"config-install-step-done": "egina",
"config-install-step-failed": "Huts egin du",
"config-install-extensions": "Luzapenak barne",
"config-install-database": "Datu-basea konfiguratu",
"config-install-schema": "Eskema sortu",
+ "config-install-pg-schema-not-exist": "PostgreSQL eskema ez da existitzen.",
+ "config-install-pg-schema-failed": "Tauleen sorrerak huts egin du.\nEgiaztatu \"$1\" erabiltzaileak \"$2\" eskeman idatzi ahal duela.",
+ "config-install-pg-commit": "Aldaketak egiten",
+ "config-install-pg-plpgsql": "PL/pgSQL hizkuntza bilatzen.",
+ "config-pg-no-plpgsql": "$1 datu-basean instalatu behar duzu PL/pgSQL hizkuntza.",
+ "config-pg-no-create-privs": "Zehaztu duzun kontua instalazio prozesurako ez dauka pribilegio nahikoak kontu bat sortzeko.",
+ "config-pg-not-in-role": "Zehaztu duzun kontua web erabiltzailerako dagoeneko existitzen da.\nInstalatzeko zehaztutako kontua ez da super erabiltzailea eta ez da web erabiltzailearen rolaren kide bat,hori dela eta ez da gai web erabiltzailearen objetuak erabiltzeko.\n\nMediaWiki-k, gaur egun, taulak web erabiltzailearenak izatea eskatzen du. Mesedez, zehaztu beste web kontuen izen bat, edo egin klik \"atzera\" botoian eta zehaztu instalazio egokia duen kontu bat.",
"config-install-user": "Datubase erabiltzailea sortzen",
"config-install-user-alreadyexists": "\"$1\" erabiltzailea badago.",
"config-install-user-create-failed": "$1 erabiltzailea sortzerakoan huts egin du: $2",
"config-install-user-grant-failed": "$1ri baimena emateak huts egin du: $2",
+ "config-install-user-missing": "Saru duzun erabiltzaile izena \"$1\" ez da existitzen.",
+ "config-install-user-missing-create": "Espezifikatu duzun erabiltzaile izena \"$1\" ez da existitzen.\n\nMesedez klikatu \"kontu bat sortu\" laukia, bat sortu nahi baduzu.",
"config-install-tables": "Taulak sortzen",
+ "config-install-tables-exist": "<strong>Oharra:</strong> Badirudi MediaWkiko- taulak dagoeneko existitzen direla. Sorrera saltatzen.",
+ "config-install-tables-failed": "<strong>Errore:</strong> Taulen sorrerak huts egin du hurrengo erroreakin: $1",
+ "config-install-interwiki": "MediaWiki taula estandarrari datuak sartzen.",
"config-install-interwiki-list": "Ezin izan da <code>interwiki.list</code> fitxategia irakurri.",
+ "config-install-interwiki-exists": "<strong>Oharra:</strong> Interwikiko taula badirudi sarrerak dituela. \nTaula estandarra saltatzen.",
"config-install-stats": "Estatistikak hasten",
"config-install-keys": "Gako sekretuak sortzen",
+ "config-insecure-keys": "<strong>Oharra:</strong> ($1) instalazioan zehar sortu {{PLURAL:$2|den|diren}} {{PLURAL:$2|gako segurua|gako seguruak}}ez d(ir)a guztiz segurua(k). Kontuan hartu {{PLURAL:$2|hau|hauek}} eskuz aldatzeko aukera.",
+ "config-install-updates": "Saihestu egikaratzen behar ez diren aktualizazioak",
+ "config-install-updates-failed": "<strong>Errore</strong> Sartzea eguneratze-gakoak taulen barruan huts egin du hurrengo errorearekin: $1",
"config-install-sysop": "Administratzaile kontua sortzen",
+ "config-install-subscribe-fail": "Ezin izan da mediawiki-announce -ra harpidetu: $1",
+ "config-install-subscribe-notpossible": "cURL ez dago instalatuta eta <code> allow_url_fopen </ code> ez dago erabilgarri.",
+ "config-install-mainpage": "Sortzen orri nagusia eduki estandarrarekin",
+ "config-install-mainpage-exists": "Orri nagusia dagoeneko existitzen da, hurrengora saltatzen",
+ "config-install-extension-tables": "Taulak sortzen aktibatutako luzapenentzako.",
+ "config-install-mainpage-failed": "Orri nagusia ezin izan da txertatu: $1",
+ "config-install-done": "<strong>Zorionak!</strong>\nMediaWiki instalatu duzu.\n\nInstalatzaileak <code>LocalSettings.php</code> fitxategia sortu egin du. \nZure konfigurazio guztia darama.\n\nDeskargatu egin beharko duzu eta zure wiki instalazio oinarrian jarri (index.php-rako direktorio berean). Deskarga automakikoki hasi behar izan da.\n\nDeskargatzeko aukerarik ez bazaizu eskaini, edo kantzelatu egin baduzu, hurrengo linkean klikatu berrabiarazteko deskarga:\n\n$3\n\n<strong>Oharra:</strong> Orain ez baduzu egiten, sortutako konfigurazio fitxategi hau ez da erabilgarri egongo geroago instalazioa bertan behera uzten baduzu deskargatu gabe.\n\nBehin hori eginda, <strong>[$2 zure wikia sartu]</strong> ahal duzu.",
+ "config-install-done-path": "<strong>Zorionak!</strong>\nMediaWiki instalatu duzu.\n\nInstalatzaileak sortu egin du <code>LocalSettings.php</code>\nZure konfigurazio guztia dauka.\n\nDeskargatu egin behar duzu eta jarri <code>$4</code> -ean . Deskarga automakikoki hasiko da.\n\nEz badizu deskargatzeko aukerarik eman, edo kantzalatu egin baduzu, hurrengo linkean klikatu berrabiatzeko:\n\n$3\n\n<strong>Oharra:</strong> Instalazio prozesuatik ateratzen bazara konfigurazio artxikoa deskargatu barik, gero ez da egongo eskuragarri.\n\nBehin hori eginda, <strong>[$2 enter your wiki]</strong> ahal duzu.",
+ "config-install-success": "MediaWiki arrakastaz instalatu da. Orain <$1$2> bisitatu dezakezu zure wikia ikusteko.\nGalderarik izanez gero, begiratu gure maiztasunez egiten diren galderen zerrenda:\n<https://www.mediawiki.org/wiki/Manual:FAQ> edo erabili orrialde honi lotuta dauden laguntza foroetako bat.",
"config-download-localsettings": "Jaitsi <code>LocalSettings.php</code>",
"config-help": "Laguntza",
"config-help-tooltip": "sakatu zabaltzeko",
"config-nofile": "Ezin da \"$1\" fitxategia aurkitu. Ezabatua izan da?",
+ "config-extension-link": "Ba al zenekien wikiak [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensions] onartzen dituela?\n\nArakatu [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensions by category] edo [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix] ikusi ahal izateko luzapenen zerrenda.",
+ "config-skins-screenshots": "$1 (Pantaila-irudia: $2)",
+ "config-screenshot": "Pantaila-irudia",
"mainpagetext": "<strong>MediaWiki instalatu da.</strong>",
- "mainpagedocfooter": "Ikus [https://meta.wikimedia.org/wiki/Help:Contents Erabiltzaile Gida] wiki softwarea erabiltzen hasteko informazio gehiagorako.\n\n== Nola hasi ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Konfigurazio balioen zerrenda]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ (Maiz egindako galderak)]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWikiren argitalpenen posta zerrenda]"
+ "mainpagedocfooter": "Ikusi [https://meta.wikimedia.org/wiki/Help:Contents Erabiltzailearen Gida] wiki softwarea erabiltzen hasteko informazio gehiagorako.\n\n== Nola hasi ==\n\n*\n [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Konfigurazio balioen zerrenda]\n*\n [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ (MediaWikin Maiz egindako galderak)]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWikiren argitalpenen posta zerrenda]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Aurkitu MediaWiki zure hizkuntzan]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Zure wikian spam-a nola borrokatzen ikasi]"
}
diff --git a/www/wiki/includes/installer/i18n/fa.json b/www/wiki/includes/installer/i18n/fa.json
index 97a09a3e..d75b0631 100644
--- a/www/wiki/includes/installer/i18n/fa.json
+++ b/www/wiki/includes/installer/i18n/fa.json
@@ -55,14 +55,14 @@
"config-help-restart": "آیا می‌خواهید همهٔ اطلاعات ذخیره شده‌ای که وارد کرده‌اید را پاک کنید و دوباره روند نصب را شروع کنید؟",
"config-restart": "بله، دوباره شروع کن",
"config-welcome": "===بررسی‌های محیطی===\nبرای فهمیدن اینکه این محیط برای نصب مدیاویکی مناسب است، اکنون بررسی‌های اساسی انجام خواهد‌شد.\nاگر به دنبال پشتیبانی در چگونگی تکمیل نصب هستید،به یاد داشته باشید این اطلاعات را بگنجانید.",
- "config-copyright": "=== حق رونوشت و شرایط ===\n\n$1\n\nاین برنامه، نرم‌افزاری آزاد است. می‌توانید تحت شرایط نگارش ۲ یا (بنا به نظر خود) هر نگارش جدیدتری از پروانهٔ جامع همگانی گنو که توسط بنیاد نرم‌افزار آزاد منتشر شده، بازنشرش کرده و/یا تغییرش دهید.\n\n\nاین برنامه با این امید توزیع شده که مفید باشد، ولی <strong>بدون هیچ ضمانتی</strong>، حتا ضمانت ضمنی <strong>معامله‌پذیری</strong> یا <strong>تناسب برای کاربردی خاص </strong>.\n\nبرای جزئیات بیشتر، پروانهٔ جامع همگانی گنو را ببینید.\n\n\nباید همراه این برنامه، <doclink href=Copying>نگارشی از پروانهٔ جامع همگانی گنو</doclink> را گرفته باشید. اگر چنین نیست، با بنیاد نرم‌افزار آزاد به نشانی 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA مکاتبه کرده یا [http://www.gnu.org/copyleft/gpl.html پروانه را برخط بخوانید].",
+ "config-copyright": "=== حق رونوشت و شرایط ===\n\n$1\n\nاین برنامه، نرم‌افزاری آزاد است. می‌توانید تحت شرایط نگارش ۲ یا (بنا به نظر خود) هر نگارش جدیدتری از پروانهٔ جامع همگانی گنو که توسط بنیاد نرم‌افزار آزاد منتشر شده، بازنشرش کرده و/یا تغییرش دهید.\n\n\nاین برنامه با این امید توزیع شده که مفید باشد، ولی <strong>بدون هیچ ضمانتی</strong>، حتا ضمانت ضمنی <strong>معامله‌پذیری</strong> یا <strong>تناسب برای کاربردی خاص </strong>.\n\nبرای جزئیات بیشتر، پروانهٔ جامع همگانی گنو را ببینید.\n\n\nباید همراه این برنامه، <doclink href=Copying>نگارشی از پروانهٔ جامع همگانی گنو</doclink> را گرفته باشید. اگر چنین نیست، با بنیاد نرم‌افزار آزاد به نشانی 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA مکاتبه کرده یا [https://www.gnu.org/copyleft/gpl.html پروانه را برخط بخوانید].",
"config-sidebar": "* [https://www.mediawiki.org صفحهٔ اصلی مدیاویکی]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents راهنمای کاربر]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents راهنمای مدیر]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ پرسش‌های رایج]\n----\n* <doclink href=Readme>مرا بخوان</doclink>\n* <doclink href=ReleaseNotes>یادداشت‌های انتشار</doclink>\n* <doclink href=Copying>نسخه برداری</doclink>\n* <doclink href=UpgradeDoc>ارتقا</doclink>",
"config-env-good": "محیط بررسی شده‌است.\nشما می‌توانید مدیاویکی را نصب کنید.",
"config-env-bad": "محیط بررسی شده‌است.\nشما نمی‌توانید مدیاویکی را نصب کنید.",
"config-env-php": "پی‌اچ‌پی $1 نصب شده‌است.",
"config-env-hhvm": "اچ‌اچ‌وی‌ام $1 نصب شده‌است.",
- "config-unicode-using-intl": "برای یونیکد عادی از [http://pecl.php.net/intl افزونهٔ intl برای PECL] استفاده کنید.",
- "config-unicode-pure-php-warning": "'''هشدار:''' [http://pecl.php.net/intl intl PECL extension] برای کنترل یونیکد عادی در دسترس نیست،اجرای کاملاً آهسته به تعویق می‌افتد.\n\nاگر شما یک سایت پر‌ ترافیک را اجرا می‌کنید، باید کمی [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization] را بخوانید.",
+ "config-unicode-using-intl": "برای یونیکد عادی از [https://pecl.php.net/intl افزونهٔ intl برای PECL] استفاده کنید.",
+ "config-unicode-pure-php-warning": "'''هشدار:''' [https://pecl.php.net/intl intl PECL extension] برای کنترل یونیکد عادی در دسترس نیست،اجرای کاملاً آهسته به تعویق می‌افتد.\n\nاگر شما یک سایت پر‌ ترافیک را اجرا می‌کنید، باید کمی [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization] را بخوانید.",
"config-unicode-update-warning": "'''هشدار:''' نسخهٔ نصب شدهٔ پوشهٔ یونیکد عادی از ورژن قدیمی‌تر کتابخانه [http://site.icu-project.org/ the ICU project's] استفاده می‌کند.\n\nاگر کلاً علاقه‌مند به استفاده از یونیکد هستید باید [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations upgrade].",
"config-no-db": "درایور پایگاه اطلاعاتی مناسب پیدا نشد! شما لازم دارید یک درایور پایگاه اطلاعاتی برای پی‌اچ‌پی نصب کنید.انواع پایگاه اطلاعاتی زیر پشتیبانی شده‌اند:$1.\nاگر شما در گروه اشتراک‌گذاری هستید، از تهیه کنندهٔ گروه خود برای نصب یک درایور پایگاه اطلاعاتی مناسب {{PLURAL:$2|سوأل کنید.|سوأل کنید.}}\nاگر خود، پی‌اچ‌پی را تهیه کرده‌اید، با یک پردازشگر فعال دوباره پیکربندی کنید، برای مثال از <code>./configure --with-mysqli</code> استفاده کنید.\nاگر پی‌اچ‌پی را از یک بستهٔ دبیان یا آبونتو نصب کرده‌اید، بنابراین لازم دارید بخش php5-mysql را نصب کنید.",
"config-outdated-sqlite": "''' هشدار:''' شما اس‌کیولایت $1 دارید، که پایین‌تر از حداقل نسخهٔ $2 مورد نیاز است.اس‌کیولایت در دسترس نخواهد بود.",
@@ -71,12 +71,11 @@
"config-pcre-no-utf8": "<strong>مخرب:</strong> به‌ نظر می‌رسد پودمان پی‌سی‌آراییِ پی‌اچ‌پی بدون پشتیبانی پی‌سی‌آرایی_یو‌تی‌اف۸ تهیه شده‌است.\nمدیاویکی برای درست عمل کردن نیازمند پشتیبانی یوتی‌اف-۸ است.",
"config-memory-raised": "PHP's <code>memory_limit</code>, نسخهٔ $1 است، به نسخهٔ $2 ارتقاء داده شده‌است.",
"config-memory-bad": "'''هشدار:''' PHP's <code>memory_limit</code> نسخهٔ $1 است.\nاین ممکن است خیلی پایین باشد.\nممکن است نصب با مشکل رو‌به‌رو شود.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] نصب شده‌است.",
"config-apc": "[http://www.php.net/apc APC] نصب شده‌است.",
"config-apcu": "[http://www.php.net/apcu APCu] نصب شده‌است",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] نصب شده‌است.",
- "config-no-cache-apcu": "<strong>هشدار:</strong> پیوند [http://www.php.net/apcu APCu]، [http://xcache.lighttpd.net/ XCache] یا [http://www.iis.net/download/WinCacheForPhp WinCache] یافت نشد. ذخیره شی فعال نیست.",
- "config-mod-security": "'''هشدار:''' وب سرور شما [http://modsecurity.org/ mod_security] فعال است.اگر اشتباه پیکربندی شده‌‌ باشد،می تواند باعث ایجاد مشکلاتی برای مدیاویکی یا دیگر نرم‌افزاری شود که به کاربران اجازه می‌دهد پیام دلخواه ارسال کنند.\nبه [http://modsecurity.org/documentation/ mod_security documentation] مراجعه کنید یا اگر با خطاهای اتفاقی مواجه شدید با پشتیبانی میزبان خود در تماس باشید.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] نصب شده‌است.",
+ "config-no-cache-apcu": "<strong>هشدار:</strong> پیوند [http://www.php.net/apcu APCu] یا [http://www.iis.net/download/WinCacheForPhp WinCache] یافت نشد. ذخیره شی فعال نیست.",
+ "config-mod-security": "'''هشدار:''' وب سرور شما [https://modsecurity.org/ mod_security] فعال است.اگر اشتباه پیکربندی شده‌‌ باشد،می تواند باعث ایجاد مشکلاتی برای مدیاویکی یا دیگر نرم‌افزاری شود که به کاربران اجازه می‌دهد پیام دلخواه ارسال کنند.\nبه [https://modsecurity.org/documentation/ mod_security documentation] مراجعه کنید یا اگر با خطاهای اتفاقی مواجه شدید با پشتیبانی میزبان خود در تماس باشید.",
"config-diff3-bad": "جی‌ان‌یو دیف۳ پیدا نشد.",
"config-git": "کنترل نسخهٔ نرم‌افزار گیت پیدا شد: <code>$1</code>.",
"config-git-bad": "کنترل نسخهٔ نرم‌افزار گیت پیدا نشد.",
@@ -231,7 +230,7 @@
"config-license-gfdl": "مجوز اسنادومدارک آزاد جی‌ان‌یو ۱.۳ یا بالاتر",
"config-license-pd": "مالکیت عمومی",
"config-license-cc-choose": "انتخاب یک مجوز سفارشی عوام خلاق",
- "config-license-help": "بسیاری از وبگاه‌ها ویرایش‌های ها را با [http://freedomdefined.org/Definition اجازه‌نامهٔ آزاد] منتشر می‌کنند.\nاین کار به داشتن حس مالکیت جمعی کمک می‌کند و ویرایش‌های طولانی مدت را اشاعه می‌دهد.\nاین برای ویکی‌های خصوصی یا سازمانی الزامی نیست.\n\nاگر شما می‌خواهید از متون ویکی‌پدیا استفاده کنید، یا اینکه به ویکی‌پدیا اجازه دهید از متون شما استفاده کند باید متون خود را با <strong>{{int:config-license-cc-by-sa}}</strong> منتشر کنید.\n\nویکی‌پدیا در گذشته از اجازه‌نامهٔ داده‌های آزاد گنو استفاده می‌کرد.\nاین اجازه‌نامه مورد قبول است، ولی فهم آن آسان نیست.\nهمچنین استفادهٔ دوباره از متون تحت اجازه‌نامهٔ داده‌های آزاد گنو به سختی انجام می‌گیرد.",
+ "config-license-help": "بسیاری از وبگاه‌ها ویرایش‌های ها را با [https://freedomdefined.org/Definition اجازه‌نامهٔ آزاد] منتشر می‌کنند.\nاین کار به داشتن حس مالکیت جمعی کمک می‌کند و ویرایش‌های طولانی مدت را اشاعه می‌دهد.\nاین برای ویکی‌های خصوصی یا سازمانی الزامی نیست.\n\nاگر شما می‌خواهید از متون ویکی‌پدیا استفاده کنید، یا اینکه به ویکی‌پدیا اجازه دهید از متون شما استفاده کند باید متون خود را با <strong>{{int:config-license-cc-by-sa}}</strong> منتشر کنید.\n\nویکی‌پدیا در گذشته از اجازه‌نامهٔ داده‌های آزاد گنو استفاده می‌کرد.\nاین اجازه‌نامه مورد قبول است، ولی فهم آن آسان نیست.\nهمچنین استفادهٔ دوباره از متون تحت اجازه‌نامهٔ داده‌های آزاد گنو به سختی انجام می‌گیرد.",
"config-email-settings": "تنظیمات ایمیل",
"config-enable-email": "فعال‌سازی ایمیل خروجی",
"config-enable-email-help": "اگر می‌خواهید ارسال ایمیل کار کند، [http://www.php.net/manual/en/mail.configuration.php PHP's mail settings] نیازمند پیکربندی صحیح است.\nاگر هیچ قابلیت ایمیلی نمی‌خواهید، می‌توانید آنها را اینجا غیر‌فعال کنید.",
@@ -261,7 +260,7 @@
"config-cache-options": "تنظیمات برای ذخیره شی:",
"config-cache-help": "کش شی برای بهتر شدن سرعت مدیا ویکی، از طریق کش کردن داده‌های با بیشترین استفاده انجام می‌گیرد.\nوبگاه‌های متوسط تا بزرگ به انجام این کار شدیدا توصیه می‌شوند، در عین حال وبگاه‌های کوچک نیز می‌توانند از مزایای ایم مورد بهره ببرند.",
"config-cache-none": "بدون ذخیره (هیچ کارآمدی پاک نشده‌است، اما ممکن است سرعت در سایت‌های بزرگتر ویکی تأثیر داشته باشد)",
- "config-cache-accel": "کاشهٔ اشیای پی‌اچ‌پی (APC یا APCu یا XCache یا WinCahe)",
+ "config-cache-accel": "کاشهٔ اشیای پی‌اچ‌پی (APC یا APCu یا WinCahe)",
"config-cache-memcached": "از ممکچد (که نیازمند تنظیمات اضافی و پیکربندی) استفاده کنید",
"config-memcached-servers": "سرورهای دریافت حافظه:",
"config-memcached-help": "فهرست آدرس‌های آی‌پی برای استفاده از ممکچد.\nباید هر یک خط را تعیین کند و درگاه مورد استفاده را تعیین کند.برای مثال: \n۱۲۷.۰.۰.۱:۱۱۲۱۱\n۱۹۲.۱۶۸.۱.۲۵:۱۲۳۴",
@@ -317,6 +316,7 @@
"config-install-mainpage-failed": "قادر به درج صفحهٔ اصلی نمی‌باشد:$1",
"config-install-done": "'''تبریک!'''\nبا موفقیت مدیاویکی را نصب کردید.\nبرنامه نصب‌کننده پرونده <code>LocalSettings.php</code> را درست کرد.\nکه شامل تمام تنظیمات می‌باشد.\n\nشما نیاز به دریافت آن دارید و آن را در پایهٔ نصب ویکی قرار دهید (همان پوشهٔ index.php). دریافت باید به صورت خودکار شروع شده‌باشد.\n\nاگر دریافت شروع نشد یا اگر آن را لغو کردید با کلیک روی پیوند زیر می‌توانید آن را دریافت کنید:\n\n$3\n\n'''توجه داشته باشید:''' اگر این را الآن انجام ندهید، این پرونده تولیدشده در صورتی که نصب را بدون دریافت آن تمام کردید بعداً در اختیار شما قرار نخواهد گرفت.\n\nوقتی انجام شد شما می‌توانید '''[$2 وارد ویکی شوید]'''.",
"config-install-done-path": "<strong>تبریک!</strong>\nمدیاویکی با موفقیت نصب گردید.\nبرنامه نصب‌کننده یک پرونده <code>LocalSettings.php</code> ایجاد کرده است که شامل تمام تنظیمات می‌باشد.\n\nلازم است شما آن را دریافت کرده و در <code>$4</code> قرار دهید. اِن دریافت می باِست به صورت خودکار شروع شده‌باشد.\n\nاگر دریافت شروع نشده بود و یا آن را لغو کرده اید با کلیک روی پیوند زیر می‌توانید آن را دریافت کنید:\n\n$3\n\n<strong>توجه:</strong> اگر این کار را هم اکنون انجام ندهید و بدون دریافت آن از برنامه نصب خارج شويد، این پرونده تنظیمات تولیدشده در آینده در اختیار شما قرار نخواهد داشت.\n\nوقتی که آن کار را انجام داديد، می‌توانید <strong>[$2 وارد ويکی خودتان شويد]</strong>.",
+ "config-install-success": "مدیاویکی به صورت موفقیت‌آمیز نصب شد. شما می‌توانید\nاز <$1$2> برای دیدن ویکی‌تان بازدید کنید.\nاگر پرسشی داشتید، فهرست سوال‌های متداول ما را مطالعه کنید:\n<https://www.mediawiki.org/wiki/Manual:FAQ> یا از یکی از انجمن‌های پشیبانی ما که در آن صفحه فهرست شده‌اند استفاده کنید.",
"config-download-localsettings": "دریافت <code>LocalSettings.php</code>",
"config-help": "راهنما",
"config-help-tooltip": "برای گسترش کلیک کنید",
diff --git a/www/wiki/includes/installer/i18n/fi.json b/www/wiki/includes/installer/i18n/fi.json
index 6c99a423..c0b19c02 100644
--- a/www/wiki/includes/installer/i18n/fi.json
+++ b/www/wiki/includes/installer/i18n/fi.json
@@ -24,7 +24,7 @@
"Pyscowicz"
]
},
- "config-desc": "MediaWiki-asennin",
+ "config-desc": "Asennin MediaWikiä varten",
"config-title": "MediaWikin version $1 asennus",
"config-information": "Tiedot",
"config-localsettings-upgrade": "<code>LocalSettings.php</code>-tiedosto havaittiin.\nKirjoita muuttujan <code>$wgUpgradeKey</code> arvo alla olevaan kenttään päivittääksesi asennuksen.\nLöydät sen <code>LocalSettings.php</code>-tiedostosta.",
@@ -37,7 +37,7 @@
"config-session-error": "Istunnon aloittaminen epäonnistui: $1",
"config-session-expired": "Istuntotietosi näyttävät olevan vanhentuneita.\nIstuntojen elinajaksi on määritelty $1.\nVoit muuttaa tätä asetusta vaihtamalla kohtaa <code>session.gc_maxlifetime</code> php.ini-tiedostossa.\nKäynnistä asennusprosessi uudelleen.",
"config-no-session": "Istuntosi tiedot menetettiin!\nTarkista php.ini-tiedostosi ja varmista, että <code>session.save_path</code> on asetettu sopivaan kansioon.",
- "config-your-language": "Asennuksen kieli",
+ "config-your-language": "Kielesi:",
"config-your-language-help": "Valitse kieli, jota haluat käyttää asennuksen ajan.",
"config-wiki-language": "Wikin kieli",
"config-wiki-language-help": "Valitse kieli, jota wikissä tullaan etupäässä käyttämään.",
@@ -61,23 +61,22 @@
"config-help-restart": "Haluatko poistaa kaikki annetut tiedot ja aloittaa asennuksen alusta?",
"config-restart": "Kyllä",
"config-welcome": "=== Ympäristön tarkistukset ===\nVarmistetaan MediaWikin asennettavuus tähän ympäristöön.\nMuista antaa nämä tiedot, jos tarvitset apua asennuksen aikana.",
- "config-copyright": "=== Tekijänoikeudet ja käyttöehdot ===\n\n$1\n\nTämä ohjelma on vapaa ohjelmisto; voit levittää sitä ja/tai muokata sitä Free Software Foundationin GNU General Public Licensen ehdoilla, joko version 2 tai (halutessasi) minkä tahansa myöhemmän version mukaisesti.\n\nTätä ohjelmaa levitetään siinä toivossa, että se olisi hyödyllinen, mutta <strong>ilman mitään takuuta</strong>; ilman edes hiljaista takuuta <strong>kaupallisesti hyväksyttävästä laadusta</strong> tai <strong>soveltuvuudesta tiettyyn tarkoitukseen.</strong\nKatso GNU Generel Public Licensestä lisää yksityiskohtia.\n\nSinun olisi pitänyt saada <doclink href=Copying>kopio GNU General Public Licensestä</doclink> tämän ohjelman mukana; jos et, kirjoita siitä osoitteeseen Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA tai [http://www.gnu.org/copyleft/gpl.html lue se verkossa].",
+ "config-copyright": "=== Tekijänoikeudet ja käyttöehdot ===\n\n$1\n\nTämä ohjelma on vapaa ohjelmisto; voit levittää sitä ja/tai muokata sitä Free Software Foundationin GNU General Public Licensen ehdoilla, joko version 2 tai (halutessasi) minkä tahansa myöhemmän version mukaisesti.\n\nTätä ohjelmaa levitetään siinä toivossa, että se olisi hyödyllinen, mutta <strong>ilman mitään takuuta</strong>; ilman edes hiljaista takuuta <strong>kaupallisesti hyväksyttävästä laadusta</strong> tai <strong>soveltuvuudesta tiettyyn tarkoitukseen.</strong\nKatso GNU Generel Public Licensestä lisää yksityiskohtia.\n\nSinun olisi pitänyt saada <doclink href=Copying>kopio GNU General Public Licensestä</doclink> tämän ohjelman mukana; jos et, kirjoita siitä osoitteeseen Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA tai [https://www.gnu.org/copyleft/gpl.html lue se verkossa].",
"config-sidebar": "* [https://www.mediawiki.org MediaWikin kotisivu]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Käyttöopas]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Hallintaopas]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ UKK]\n----\n* <doclink href=Readme>Lue minut</doclink>\n* <doclink href=ReleaseNotes>Julkaisutiedot</doclink>\n* <doclink href=Copying>Kopiointi</doclink>\n* <doclink href=UpgradeDoc>Päivittäminen</doclink>",
"config-env-good": "Asennusympäristö on tarkastettu.\nVoit asentaa MediaWikin.",
"config-env-bad": "Asennusympäristö on tarkastettu.\nEt voi asentaa MediaWikiä.",
"config-env-php": "PHP $1 on asennettu.",
"config-env-hhvm": "HHVM $1 on asennettu.",
- "config-unicode-using-intl": "Käyttää [http://pecl.php.net/intl intl PECL-laajennusta] Unicode-normalisaatioon.",
+ "config-unicode-using-intl": "Käyttää [https://pecl.php.net/intl intl PECL-laajennusta] Unicode-normalisaatioon.",
"config-no-db": "Sopivaa tietokanta-ajuria ei löytynyt! Sinun täytyy asentaa tietokanta-ajuri PHP:lle.\n{{PLURAL:$2|Seuraava tietokantatyyppi on tuettu|Seuraavat tietokantatyypit ovat tuettuja}}: $1.\n\nJos koostit PHP:n itse, määritä se uudelleen tietokanta-asiakkaan ollessa käytössä, esimerkiksi koodilla <code>./configure --with-mysqli</code>.\nJos asensit PHP:n Debian- tai Ubuntu-pakkauksesta, sinun on myös asennettava esimerkiksi <code>php5-mysql</code>-pakkaus.",
"config-outdated-sqlite": "<strong>Varoitus:</strong> sinulla on käytössä SQLite $1, joke on vanhempi kuin vähintään vaadittava versio $2. SQLite ei ole saatavilla.",
"config-no-fts3": "<strong>Varoitus:</strong> SQLite on koostettu ilman [//sqlite.org/fts3.html FTS3-moduulia], hakuominaisuudet eivät ole käytössä tässä taustajärjestelmässä.",
"config-pcre-old": "<strong>Tärkeää:</strong> PCRE $1 tai uudempi versio tarvitaan.\nPHP-binäärisi on linkitetty versiolla PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Lisätietoja].",
"config-memory-raised": "PHP:n <code>memory_limit</code> on $1, nostetaan arvoon $2.",
"config-memory-bad": "'''Varoitus:''' PHP:n <code>memory_limit</code> on $1.\nTämä on luultavasti liian alhainen.\nAsennus saattaa epäonnistua!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] on asennettu",
"config-apc": "[http://www.php.net/apc APC] on asennettu.",
"config-apcu": "[http://www.php.net/apcu APCu] on asennettu",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] on asennettu",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] on asennettu",
"config-diff3-bad": "GNU diff3:a ei löytynyt.",
"config-git": "Löydetty Git versionhallintaohjelmisto: <code>$1</code>",
"config-git-bad": "Git versionhallintaohjelmistoa ei löydy.",
@@ -92,17 +91,17 @@
"config-no-cli-uploads-check": "<strong>Varoitus:</strong> Tiedostojen lähetyshakemistoa (<code>$1</code>) ei ole tarkistettu haavoittuvuuksien varalta komentoriviasennuksen aikana.",
"config-brokenlibxml": "Järjestelmässäsi on käytössä PHP:n ja libxml2:n versioyhdistelmä, joka ei toimi kunnolla ja voi aiheuttaa tiedon vahingoittumista MediaWikissä ja muissa web-sovelluksissa.\nPäivitä libxml2 versioon 2.7.3 tai uudempaan ([https://bugs.php.net/bug.php?id=45996 bug filed with PHP]).\nAsennus keskeytetty.",
"config-suhosin-max-value-length": "Suhosin on asennettu ja se rajoittaa GET-parametrin <code>length</code> $1 tavuun.\nMediaWikin ResourceLoader-komponentti pystyy toimimaan tämän kanssa, mutta ohjelmiston suorituskyky heikkenee.\nMikäli mahdollista, aseta muuttuja <code>suhosin.get.max_value_length</code> arvoon 1024 (tai suurempaan) tiedostossa <code>php.ini</code> ja aseta myös <code>$wgResourceLoaderMaxQueryLength</code> samaksi arvoksi tiedostossa <code>LocalSettings.php</code>.",
- "config-db-type": "Tietokannan tyyppi",
- "config-db-host": "Tietokantapalvelin",
+ "config-db-type": "Tietokannan tyyppi:",
+ "config-db-host": "Tietokantapalvelin:",
"config-db-host-help": "Jos tietokantapalvelimesi sijaitsee eri palvelimella, syötä palvelimen nimi tai ip-osoite tähän.\n\nJos käytössäsi on ulkoinen palveluntarjoaja, pitäisi palvelimen nimen löytyä yrityksen ohjesivuilta.\n\nJos asennat MediaWikiä Windows-palvelimelle ja käytät MySQL:ää ei palvelimen nimi \"localhost\" välttämättä toimi. Tässä tapauksessa koita käyttää osoitetta 127.0.0.1.\n\nJos käytät PostgreSQL:ää jätä tämä kenttä tyhjäksi.",
"config-db-host-oracle": "Tietokannan TNS:",
"config-db-wiki-settings": "Identifioi tämä wiki",
- "config-db-name": "Tietokannan nimi",
+ "config-db-name": "Tietokannan nimi:",
"config-db-name-help": "Valitse wikiäsi kuvaava nimi.\nNimessä ei saa olla välilyöntejä.\n\nMikäli et pysty itse hallitsemaan tietokantojasi, pyydä palveluntarjoajaasi luomaan tietokanta tai tee se palveluntarjoajasi hallintapaneelissa.",
"config-db-name-oracle": "Tietokannan rakenne:",
"config-db-install-account": "Asennuksessa käytettävä käyttäjätili",
- "config-db-username": "Tietokannan käyttäjätunnus",
- "config-db-password": "Tietokannan salasana",
+ "config-db-username": "Tietokannan käyttäjätunnus:",
+ "config-db-password": "Tietokannan salasana:",
"config-db-install-username": "Syötä käyttäjänimi jota käytetään muodostettaessa yhteys tietokantaan asennuksen aikana.\nTämä ei ole MediaWiki tilin käyttäjänimi; tämä on tietokannan käyttäjänimi.",
"config-db-install-password": "Syötä salasana jota käytetään muodostettaessa yhteys tietokantaan asennuksen aikana.\nTämä ei ole MediaWiki tilin salasana; tämä on tietokannan salasana.",
"config-db-install-help": "Anna käyttäjätunnus ja salasana, joita käytetään asennuksen aikana.",
@@ -150,6 +149,7 @@
"config-postgres-old": "MediaWiki tarvitsee PostgreSQL:n version $1 tai uudemman. Nykyinen versio on $2.",
"config-mssql-old": "Vaaditaan Microsoft SQL Server $1 tai uudempi. Sinulla on käytössä $2.",
"config-sqlite-name-help": "Valitse nimi, joka yksilöi tämän wikin.\nÄlä käytä välilyöntejä tai viivoja.\nNimeä käytetään SQLite-tietokannan tiedostonimessä.",
+ "config-sqlite-mkdir-error": "Virhe luodessa datakansiota \"$1\".\nTarkista sijainti ja yritä uudelleen",
"config-sqlite-dir-unwritable": "Hakemistoon ”$1” kirjoittaminen epäonnistui.\nMuuta hakemiston käyttöoikeuksia siten, että palvelinohjelmisto voi kirjoittaa siihen ja yritä uudelleen.",
"config-sqlite-connection-error": "$1.\n\nTarkista tietohakemiston ja tietokannan nimi alla ja yritä uudelleen.",
"config-sqlite-readonly": "Tiedostoon <code>$1</code> ei voi kirjoittaa.",
@@ -161,6 +161,7 @@
"config-regenerate": "Luo LocalSettings.php uudelleen →",
"config-show-table-status": "Kysely <code>SHOW TABLE STATUS</code> epäonnistui!",
"config-unknown-collation": "<strong>Varoitus:</strong> Tietokanta käyttää tunnistamatonta lajittelua.",
+ "config-db-web-account": "Tietokantatili verkkokäyttöön",
"config-db-web-help": "Valitse käyttäjänimi ja salasana joita palvelin käyttää muodostaessaan yhteyttä tietokantapalvelimeen wikin normaalin toiminnan aikana.",
"config-db-web-account-same": "Käytä samaa tiliä kuin asennuksessa",
"config-db-web-create": "Lisää tili, jos sitä ei ole jo olemassa",
@@ -176,10 +177,10 @@
"config-mssql-web-auth": "Valitse varmennuksen tyyppi, jota verkkopalvelin käyttää yhdistäessään tietokantapalvelimeen wikin tavallisen toiminnan aikana.\nJos valitset \"{{int:config-mssql-windowsauth}}\", käytetään verkkopalvelimen käyttäjän kirjautumistietoja.",
"config-mssql-sqlauth": "SQL Server varmennus",
"config-mssql-windowsauth": "Windows-varmennus",
- "config-site-name": "Wikin nimi",
+ "config-site-name": "Wikin nimi:",
"config-site-name-help": "Tämä näkyy selaimen otsikkona ja muissa kohdissa.",
"config-site-name-blank": "Kirjoita sivuston nimi.",
- "config-project-namespace": "Projektinimiavaruus",
+ "config-project-namespace": "Projektinimiavaruus:",
"config-ns-generic": "Projekti",
"config-ns-site-name": "Sama kuin wikin nimi: $1",
"config-ns-other": "Muu (määritä)",
@@ -189,14 +190,14 @@
"config-ns-conflict": "Määritelty nimiavaruus \"<nowiki>$1</nowiki>\" on ristiriidassa MediaWikin oletusnimiavaruuksien kanssa.\nSyötä joku muu nimiavaruus.",
"config-admin-box": "Ylläpitäjän tili",
"config-admin-name": "Käyttäjänimesi:",
- "config-admin-password": "Salasana",
- "config-admin-password-confirm": "Salasana uudelleen",
+ "config-admin-password": "Salasana:",
+ "config-admin-password-confirm": "Salasana uudelleen:",
"config-admin-help": "Syötä käyttäjänimi tähän, esimerkiksi \"Matti Meikäläinen\".\nTätä nimeä käytetään kirjauduttaessa wikiin.",
"config-admin-name-blank": "Anna ylläpitäjän käyttäjänimi.",
"config-admin-name-invalid": "Annettu nimi \"<nowiki>$1</nowiki>\" on virheellinen.\nSyötä toinen nimi.",
"config-admin-password-blank": "Syötä ylläpitäjän salasana.",
"config-admin-password-mismatch": "Antamasi salasanat eivät täsmää.",
- "config-admin-email": "Sähköpostiosoite",
+ "config-admin-email": "Sähköpostiosoite:",
"config-admin-email-help": "Syötä sähköpostiosoite johon vastaanotetaan viestit muilta wikin käyttäjiltä, nollataan salasana ja ilmoitetaan tarkkailulistalla olevista sivuista. Kenttä voidaan jättää myös tyhjäksi.",
"config-admin-error-user": "Sisäinen virhe luodessa ylläpitäjää nimellä \"<nowiki>$1</nowiki>\".",
"config-admin-error-password": "Sisäinen virhe asetettaessa salasanaa ylläpitäjälle \"<nowiki>$1</nowiki>\":\n<pre>$2</pre>",
@@ -204,6 +205,7 @@
"config-subscribe": "Liity [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce päivityssähköpostilistalle].",
"config-subscribe-help": "Tällä harvoin käytettävällä sähköpostilistalla julkaistaan päivitysilmoituksia ja turvallisuuspäivityksiä.\nLiittymistä listalle suositellaan samoin kuin päivittämään MediaWiki kun uusi versio julkaistaan.",
"config-subscribe-noemail": "Yritit liittyä päivityssähköpostilistalle antamatta sähköpostiosoitetta.\nSyötä sähköpostiosoite jos haluat liittyä listalle.",
+ "config-pingback": "Jaa tietoja tästä asennuksesta MediaWiki-kehittäjien kanssa.",
"config-almost-done": "Olet jo lähes valmis!\nVoit ohittaa jäljellä olevat määritykset ja asentaa wikin juuri nyt.",
"config-optional-continue": "Säädä lisää asetuksia.",
"config-optional-skip": "Nyt riittää, asenna wiki näillä tiedoilla.",
@@ -212,7 +214,9 @@
"config-profile-no-anon": "Tunnuksen luonti vaaditaan",
"config-profile-fishbowl": "Vain hyväksytyt muokkaajat",
"config-profile-private": "Yksityinen wiki",
+ "config-profile-help": "Wikit toimivat parhainten, kun annat niin monen ihmisen muokata niitä kuin mahdollista.\nMediaWikissä on helppoa esikatsella tuoreita muutoksia ja palauttaa kaiken vahingon, joita naiivit tai ilkeät käyttäjät tekevät.\n\nKuitenkin, monet ovat löytäneet MediaWikin hyödylliseksi monissa eri tehtävissä, ja joskus se ei ole helppoa vakuuttaa kaikkia wiki-tien eduista.\nJoten sinun pitää valita,\n\n<strong>{{int:config-profile-wiki}}</strong> -malli sallii kaikkien muokata, jopa ilman sisäänkirjautumista.\nWiki jossa <strong>{{int:config-profile-no-anon}}</strong> antaa lisävelvollisuutta, mutta saattaa estää vapaaehtoisia avustajia.\n\n<strong>{{int:config-profile-fishbowl}}</strong> skenaario sallii hyväksyttyjen käyttäjien muokata, mutta julkiset voivat nähdä sivut, mukaanlukien historia,\n<strong>{{int:config-profile-private}}</strong> sallii vain hyväksyttyjen käyttäjien nähdä ja muokata sivuja.\n\nMonimutkaisempia käyttöoikeuksien kokoonpanoja saatavilla asennuksen jälkeen, katso [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights relevant manual entry].",
"config-license": "Tekijänoikeus ja lisenssi:",
+ "config-license-none": "Ei lisenssin alatunnistetta",
"config-license-cc-by-sa": "Creative Commons Nimeä-Tarttuva",
"config-license-cc-by": "Creative Commons Nimeä",
"config-license-cc-by-nc-sa": "Creative Commons Nimeä-Epäkaupallinen-Tarttuva",
@@ -220,7 +224,7 @@
"config-license-gfdl": "GNU Free Documentation -lisenssi 1.3 tai uudempi",
"config-license-pd": "Public domain",
"config-license-cc-choose": "Valitse mukautettu Creative Commons -lisenssi",
- "config-license-help": "Monet julkiset wikit käyttävät muokkauksiin [http://freedomdefined.org/Definition vapaata lisenssiä].\nTämä auttaa luomaan yhteisöllisen omistajuuden tunteen ja kannustaa pitkäkestoiseen muokkaamiseen.\nSe ei ole yleensä tarpeen yksityiselle tai yrityksen wikille.\n\nJos haluat pystyä käyttämään tekstiä Wikipediasta, ja haluat Wikipedian pystyvän hyväksymään wikistäsi kopioitua tekstiä, sinun tulisi valita <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia käytti aiemmin GNU Free Documentation Licenseä.\nGFDL on kelvollinen lisenssi, mutta vaikea ymmärtää.\nOn myös vaikeaa käyttää uudelleen GFDL-lisensöityä sisältöä.",
+ "config-license-help": "Monet julkiset wikit käyttävät muokkauksiin [https://freedomdefined.org/Definition vapaata lisenssiä].\nTämä auttaa luomaan yhteisöllisen omistajuuden tunteen ja kannustaa pitkäkestoiseen muokkaamiseen.\nSe ei ole yleensä tarpeen yksityiselle tai yrityksen wikille.\n\nJos haluat pystyä käyttämään tekstiä Wikipediasta, ja haluat Wikipedian pystyvän hyväksymään wikistäsi kopioitua tekstiä, sinun tulisi valita <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia käytti aiemmin GNU Free Documentation Licenseä.\nGFDL on kelvollinen lisenssi, mutta vaikea ymmärtää.\nOn myös vaikeaa käyttää uudelleen GFDL-lisensöityä sisältöä.",
"config-email-settings": "Sähköpostiasetukset",
"config-enable-email": "Ota käyttöön sähköpostien lähetys",
"config-enable-email-help": "Jotta sähköposti toimii, [http://www.php.net/manual/en/mail.configuration.php PHP:n sähköpostiasetukset] täytyy asettaa oikein.\nJos et halua käyttää sähköpostiominaisuuksia, ne voi kytkeä pois päältä tästä.",
@@ -237,7 +241,7 @@
"config-upload-enable": "Ota käyttöön tiedostojen lataaminen",
"config-upload-deleted": "Poistettujen tiedostojen hakemisto:",
"config-upload-deleted-help": "Valitse hakemisto johon poistetut tiedostot arkistoidaan.\nHakemiston ei tulisi olla käytettävissä internetverkosta.",
- "config-logo": "Logon URL-osoite",
+ "config-logo": "Logon URL-osoite:",
"config-logo-help": "MediaWikin oletusulkoasussa on paikka 135x160 pikselin kokoiselle logolle sivupalkin yläpuolella.\nTallenna sopivan kokoinen kuva ja lisää URL tähän.\n\nVoit käyttää muuttujia <code>$wgStylePath</code> tai <code>$wgScriptPath</code>, jos logosi on määritelty suhteessa näihin polkuihin.\n\nJos et halua logoa, jätä tämä kenttä tyhjäksi.",
"config-instantcommons": "Aktivoi Instant Commons",
"config-instantcommons-help": "[https://www.mediawiki.org/wiki/InstantCommons Instant Commons] on ominaisuus, joka antaa wikien käyttää kuvia, ääniä ja muuta mediaa [https://commons.wikimedia.org/ Wikimedia Commons] -sivustolta.\nTehdäkseen tämän MediaWiki tarvitsee Internet-yhteyden.\n\nLisätietoja tästä ominaisuudesta, mukaan lukien ohjeet, kuinka sen voi asettaa muille wikeille kuin Wikimedia Commons, löytyy [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos ohjeista].",
@@ -291,20 +295,24 @@
"config-install-keys": "Muodostetaan salausavaimia",
"config-insecure-keys": "<strong>Varoitus:</strong> Asennuksen aikana {{PLURAL:$2|luotu turva-avain|luodut turva-avaimet}} ($1) {{PLURAL:$2|ei|eivät}} ole täysin {{PLURAL:$2|turvallinen|turvallisia}}. Harkitse {{PLURAL:$2|sen|niiden}} muuttamista manuaalisesti.",
"config-install-updates": "Estä tarpeettomien päivitysten asennus",
+ "config-install-updates-failed": "<strong>Virhe:</strong> Päivitysavainten lisääminen taulukoihin epäonnistui seuraavalla virheellä: $1",
"config-install-sysop": "Luodaan ylläpitäjän tiliä",
"config-install-subscribe-fail": "Liittyminen mediawiki-announce listalle epäonnistui: $1",
"config-install-subscribe-notpossible": "cURL-ohjelmaa ei ole asennettu eikä <code>allow_url_fopen</code> ole saatavilla.",
"config-install-mainpage": "Luodaan etusivu oletussisällöllä",
"config-install-mainpage-exists": "Etusivu on jo olemassa, ohitetaan",
- "config-install-extension-tables": "Luodaan tauluja käyttöönotetuille laajuennuksille",
+ "config-install-extension-tables": "Luodaan tauluja käyttöönotetuille laajennuksille",
"config-install-mainpage-failed": "Etusivun lisääminen ei onnistunut: $1",
"config-install-done": "<strong>Onnittelut!</strong>\nOlet asentanut MediaWikin.\n\nAsennusohjelma on luonut <code>LocalSettings.php</code> -tiedoston.\nSiinä on kaikki MediaWikin asetukset.\n\nLataa tiedosto ja laita se MediaWikin asennushakemistoon (sama kuin missä on index.php). Lataamisen olisi pitänyt alkaa automaattisesti.\n\nMikäli latausta ei tarjottu tai keskeytit latauksen, käynnistä se uudestaan tästä linkistä:\n\n$3\n\n<strong>Huom:</strong> Mikäli et nyt lataa tiedostoa, luotu tiedosto ei ole saatavissa myöhemmin, jos poistut asennuksesta lataamatta sitä.\n\nKun olet laittanut tiedoston oikeaan paikkaan, voit <strong>[$2 mennä wikiisi]</strong>.",
"config-install-done-path": "<strong>Onnittelut!</strong>\nOlet asentanut MediaWikin.\n\nAsennusohjelma on luonut <code>LocalSettings.php</code> -tiedoston.\nSiinä on kaikki MediaWikin asetukset.\n\nLataa tiedosto ja laita se sijaintiin <code>$4</code>. Lataamisen olisi pitänyt alkaa automaattisesti.\n\nMikäli latausta ei tarjottu tai keskeytit latauksen, käynnistä se uudestaan tästä linkistä:\n\n$3\n\n<strong>Huom:</strong> Mikäli et nyt lataa tiedostoa, luotu tiedosto ei ole saatavissa myöhemmin, jos poistut asennuksesta lataamatta sitä.\n\nKun olet laittanut tiedoston oikeaan paikkaan, voit <strong>[$2 mennä wikiisi]</strong>.",
+ "config-install-success": "MediaWiki on asennettu onnistuneesti. Voit nyt vierailla <$1$2> katsellaksesi wikiäsi. Jos sinulla on kysyttävää, \ntutustu usein kysyttyjen kysymysten luetteloon: <https://www.mediawiki.org/wiki/Manual:FAQ> tai käytä yhtä sivulle linkitettyä tukifoorumia.",
"config-download-localsettings": "Lataa <code>LocalSettings.php</code>",
"config-help": "ohje",
"config-help-tooltip": "Klikkaa laajentaaksesi",
"config-nofile": "Tiedostoa \"$1\" ei löytynyt. Onko se poistettu?",
"config-extension-link": "Tiesitkö että wiki tukee [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions laajennuksia]?\n\nLaajennuksia voi hakea myös [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category luokittain].",
+ "config-skins-screenshots": "$1 (kuvakaappaukset: $2)",
+ "config-screenshot": "kuvakaappaus",
"mainpagetext": "<strong>MediaWiki on onnistuneesti asennettu.</strong>",
"mainpagedocfooter": "Lisätietoja wiki-ohjelmiston käytöstä on [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents käyttöoppaassa].\n\n=== Aloittaminen ===\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Asetusten teko-ohjeita]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWikin FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Sähköpostilista, jolla tiedotetaan MediaWikin uusista versioista]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Käännä MediaWikiä kielellesi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Katso, kuinka torjua spämmiä wikissäsi]\n\n=== Asetukset ===\n\nTarkista, että alla olevat taivutusmuodot ovat oikein. Jos eivät, tee tarvittavat muutokset tiedostoon LocalSettings.php seuraavasti:\n $wgGrammarForms['fi']['genitive']['{{SITENAME}}'] = '...';\n $wgGrammarForms['fi']['partitive']['{{SITENAME}}'] = '...';\n $wgGrammarForms['fi']['elative']['{{SITENAME}}'] = '...';\n $wgGrammarForms['fi']['inessive']['{{SITENAME}}'] = '...';\n $wgGrammarForms['fi']['illative']['{{SITENAME}}'] = '...';\nTaivutusmuodot: {{GRAMMAR:genitive|{{SITENAME}}}} (yön) – {{GRAMMAR:partitive|{{SITENAME}}}} (yötä) – {{GRAMMAR:elative|{{SITENAME}}}} (yöstä) – {{GRAMMAR:inessive|{{SITENAME}}}} (yössä) – {{GRAMMAR:illative|{{SITENAME}}}} (yöhön)."
}
diff --git a/www/wiki/includes/installer/i18n/fo.json b/www/wiki/includes/installer/i18n/fo.json
index e907dc2f..44a07078 100644
--- a/www/wiki/includes/installer/i18n/fo.json
+++ b/www/wiki/includes/installer/i18n/fo.json
@@ -36,8 +36,8 @@
"config-restart": "Ja, byrja umaftur",
"config-env-php": "PHP $1 er innstallerað.",
"config-env-hhvm": "HHVM $1 er lagt inn.",
- "config-unicode-using-intl": "Brúkar [http://pecl.php.net/intl intl PECL ískoytið] til Unicode normalisering.",
- "config-unicode-pure-php-warning": "<strong>Ávaring:</strong> [http://pecl.php.net/intl intl PECL ískoytið] er ikki tøkt at handfara Unicode normalisering, fellur aftur til eina spakuligari reina-PHP verkseting.\nUm tú koyrir eina netsíðu við høgari ferðslu, so eigur tú at lesa eitt sindur um [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalisering].",
+ "config-unicode-using-intl": "Brúkar [https://pecl.php.net/intl intl PECL ískoytið] til Unicode normalisering.",
+ "config-unicode-pure-php-warning": "<strong>Ávaring:</strong> [https://pecl.php.net/intl intl PECL ískoytið] er ikki tøkt at handfara Unicode normalisering, fellur aftur til eina spakuligari reina-PHP verkseting.\nUm tú koyrir eina netsíðu við høgari ferðslu, so eigur tú at lesa eitt sindur um [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalisering].",
"config-unicode-update-warning": "<strong>Ávaring:</strong> Tann innlagda versjónin av Unicode normalisering wrapper nýtir eina eldri versjón av [http://site.icu-project.org/ bókasavninum hjá ICU verkætlanini].\nTú eigur at [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations fremja uppstigning] um tú stúrir fyri at nýta Unicode.",
"config-diff3-bad": "GNU diff3 ikki funnið.",
"config-git": "Fann Git version control forritið: <code>$1</code>.",
diff --git a/www/wiki/includes/installer/i18n/fr.json b/www/wiki/includes/installer/i18n/fr.json
index 50521f69..2d9b94f4 100644
--- a/www/wiki/includes/installer/i18n/fr.json
+++ b/www/wiki/includes/installer/i18n/fr.json
@@ -28,7 +28,8 @@
"C13m3n7",
"The RedBurn",
"Trial",
- "Tinss"
+ "Tinss",
+ "Thibaut120094"
]
},
"config-desc": "Le programme d’installation de MediaWiki",
@@ -68,37 +69,36 @@
"config-help-restart": "Voulez-vous effacer toutes les données enregistrées que vous avez entrées et relancer le processus d'installation ?",
"config-restart": "Oui, le relancer",
"config-welcome": "=== Vérifications liées à l’environnement ===\nDes vérifications de base vont maintenant être effectuées pour voir si cet environnement est adapté à l’installation de MediaWiki.\nRappelez-vous d’inclure ces informations si vous recherchez de l’aide sur la manière de terminer l’installation.",
- "config-copyright": "=== Droit d'auteur et conditions ===\n\n$1\n\nCe programme est un logiciel libre : vous pouvez le redistribuer et/ou le modifier selon les termes de la Licence Publique Générale GNU telle que publiée par la Free Software Foundation (version 2 de la Licence, ou, à votre choix, toute version ultérieure).\n\nCe programme est distribué dans l’espoir qu’il sera utile, mais '''sans aucune garantie''' : sans même les garanties implicites de '''commerciabilité''' ou d’'''adéquation à un usage particulier'''.\nVoir la Licence Publique Générale GNU pour plus de détails.\n\nVous devriez avoir reçu <doclink href=Copying>une copie de la Licence Publique Générale GNU</doclink> avec ce programme ; dans le cas contraire, écrivez à la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ou [http://www.gnu.org/copyleft/gpl.html lisez-la en ligne].",
+ "config-copyright": "=== Droit d’auteur et conditions ===\n\n$1\n\nCe programme est un logiciel libre : vous pouvez le redistribuer ou le modifier selon les termes de la Licence Publique Générale GNU telle que publiée par la Free Software Foundation (version 2 de la Licence, ou, à votre choix, toute version ultérieure).\n\nCe programme est distribué dans l’espoir qu’il sera utile, mais '''sans aucune garantie''' : sans même les garanties implicites de '''commercialisabilité''' ou d’'''adéquation à un usage particulier'''.\nVoir la Licence Publique Générale GNU pour plus de détails.\n\nVous devriez avoir reçu <doclink href=Copying>une copie de la Licence Publique Générale GNU</doclink> avec ce programme ; dans le cas contraire, écrivez à la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ou [https://www.gnu.org/copyleft/gpl.html lisez-la en ligne].",
"config-sidebar": "* [https://www.mediawiki.org Accueil MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guide de l’utilisateur]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guide de l’administrateur]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Lisez-moi</doclink>\n* <doclink href=ReleaseNotes>Notes de publication</doclink>\n* <doclink href=Copying>Copie</doclink>\n* <doclink href=UpgradeDoc>Mise à jour</doclink>",
"config-env-good": "L’environnement a été vérifié.\nVous pouvez installer MediaWiki.",
"config-env-bad": "L’environnement a été vérifié.\nVous ne pouvez pas installer MediaWiki.",
"config-env-php": "PHP $1 est installé.",
"config-env-hhvm": "HHVM $1 est installé.",
- "config-unicode-using-intl": "Utilisation de [http://pecl.php.net/intl l'extension PECL intl] pour la normalisation Unicode.",
- "config-unicode-pure-php-warning": "<strong>Attention :</strong> L’[http://pecl.php.net/intl extension PECL intl] n’est pas disponible pour la normalisation d’Unicode, retour à la version lente implémentée en PHP.\nSi votre site web sera très fréquenté, vous devriez lire ceci : [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (en anglais).",
- "config-unicode-update-warning": "<strong>Attention</strong>: La version installée du normalisateur Unicode utilise une ancienne version de la [http://site.icu-project.org/ bibliothèque logicielle ''ICU Project''].\nVous devriez faire une [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations mise à jour] si vous êtes concerné par l'usage d'Unicode.",
+ "config-unicode-using-intl": "Utilisation de [https://pecl.php.net/intl l’extension PECL intl] pour la normalisation Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Attention :</strong> L’[https://pecl.php.net/intl extension PECL intl] n’est pas disponible pour la normalisation d’Unicode, retour à la version lente implémentée en PHP seulement.\nSi votre site web sera très fréquenté, vous devriez lire ceci : [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (en anglais).",
+ "config-unicode-update-warning": "<strong>Attention :</strong> la version installée du normalisateur Unicode utilise une ancienne version de la bibliothèque logicielle du [http://site.icu-project.org/ ''Projet ICU''].\nVous devriez faire une [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations mise à jour] si vous êtes concerné par l’usage d’Unicode.",
"config-no-db": "Impossible de trouver un pilote de base de données approprié ! Vous devez installer un pilote de base de données pour PHP. {{PLURAL:$2|Le type suivant|Les types suivants}} de bases de données {{PLURAL:$2|est reconnu|sont reconnus}} : $1.\n\nSi vous avez compilé PHP vous-même, reconfigurez-le avec un client de base de données actif, par exemple en utilisant <code>./configure --with-mysqli</code>. Si vous avez installé PHP depuis un paquet Debian ou Ubuntu, alors vous devrez aussi installer, par exemple, le paquet <code>php5-mysql</code>.",
- "config-outdated-sqlite": "'''Attention''': vous avez SQLite $1, qui est inférieur à la version minimale requise $2. SQLite sera indisponible.",
- "config-no-fts3": "'''Attention :''' SQLite est compilé sans le module [//sqlite.org/fts3.html FTS3] ; les fonctions de recherche ne seront pas disponibles sur ce moteur.",
- "config-pcre-old": "'''Fatal :''' PCRE $1 ou ultérieur est nécessaire.\nVotre binaire PHP est lié avec PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/Plus d’information sur PCRE].",
- "config-pcre-no-utf8": "<strong>Erreur fatale</strong>: le module PCRE de PHP semble être compilé sans la prise en charge de PCRE_UTF8.\nMédiaWiki a besoin de la gestion d’UTF-8 pour fonctionner correctement.",
+ "config-outdated-sqlite": "<strong>Attention :</strong> vous avez SQLite $1, qui est inférieur à la version minimale requise $2. SQLite sera indisponible.",
+ "config-no-fts3": "<strong>Attention :</strong> SQLite est compilé sans le [//sqlite.org/fts3.html module FTS3] ; les fonctions de recherche ne seront pas disponibles sur ce moteur.",
+ "config-pcre-old": "<strong>Erreur fatale :</strong> PCRE $1 ou ultérieur est nécessaire.\nVotre binaire PHP est lié avec PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/Plus d’information sur PCRE].",
+ "config-pcre-no-utf8": "<strong>Erreur fatale :</strong> le module PCRE de PHP semble être compilé sans la prise en charge de PCRE_UTF8.\nMediaWiki a besoin de la gestion d’UTF-8 pour fonctionner correctement.",
"config-memory-raised": "Le paramètre <code>memory_limit</code> de PHP était à $1, porté à $2.",
- "config-memory-bad": "'''Attention :''' Le paramètre <code>memory_limit</code> de PHP est à $1.\nCette valeur est probablement trop faible.\nIl est possible que l’installation échoue !",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] est installé",
+ "config-memory-bad": "<strong>Attention :</strong> Le paramètre <code>memory_limit</code> de PHP est à $1.\nCette valeur est probablement trop faible.\nIl est possible que l’installation échoue !",
"config-apc": "[http://www.php.net/apc APC] est installé",
"config-apcu": "[http://www.php.net/apcu APCu] est installé",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] est installé",
- "config-no-cache-apcu": "<strong>Attention :</strong> Impossible de trouver [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] ou [http://www.iis.net/download/WinCacheForPhp WinCache].\nLa mise en cache d'objets n'est pas activée.",
- "config-mod-security": "'''Attention''': Votre serveur web a [http://modsecurity.org/ mod_security] activé. S’il est mal configuré, cela peut poser des problèmes à MediaWiki ou à d’autres applications qui permettent aux utilisateurs de publier un contenu quelconque.\nReportez-vous à [http://modsecurity.org/documentation/ la documentation de mod_security] ou contactez le soutien de votre hébergeur si vous rencontrez des erreurs aléatoires.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] est installé",
+ "config-no-cache-apcu": "<strong>Attention :</strong> impossible de trouver [http://www.php.net/apcu APCu] ou [http://www.iis.net/download/WinCacheForPhp WinCache].\nLa mise en cache d’objets n’est pas activée.",
+ "config-mod-security": "<strong>Attention :</strong> votre serveur web a [https://modsecurity.org/ mod_security] activé. S’il est mal configuré, cela peut poser des problèmes à MediaWiki ou à d’autres applications qui permettent aux utilisateurs de publier un contenu quelconque. Si possible, ceci devrait être désactivé. Sinon, reportez-vous à [https://modsecurity.org/documentation/ la documentation de mod_security] ou contactez l’assistance de votre hébergeur si vous rencontrez des erreurs aléatoires.",
"config-diff3-bad": "GNU diff3 introuvable.",
"config-git": "Logiciel de contrôle de version Git trouvé : <code>$1</code>.",
"config-git-bad": "Logiciel de contrôle de version Git non trouvé.",
- "config-imagemagick": "ImageMagick trouvé : <code>$1</code>.\nLa miniaturisation d'images sera activée si vous activez le téléversement de fichiers.",
+ "config-imagemagick": "ImageMagick trouvé : <code>$1</code>.\nLa génération de vignettes d’images sera activée si vous activez les téléversements.",
"config-gd": "La bibliothèque graphique GD intégrée a été trouvée.\nLa miniaturisation d'images sera activée si vous activez le téléversement de fichiers.",
- "config-no-scaling": "Impossible de trouver la bibliothèque GD ou ImageMagick.\nLa miniaturisation d'images sera désactivée.",
- "config-no-uri": "'''Erreur :''' Impossible de déterminer l'URI du script actuel.\nInstallation interrompue.",
- "config-no-cli-uri": "'''Attention''': Aucun <code>--scriptpath</code> n'a été spécifié; <code>$1</code> sera utilisé par défaut",
- "config-using-server": "Utilisation du nom de serveur \"<nowiki>$1</nowiki>\".",
+ "config-no-scaling": "Impossible de trouver la bibliothèque GD ou ImageMagick.\nLa miniaturisation d’images sera désactivée.",
+ "config-no-uri": "<strong>Erreur :</strong> impossible de déterminer l’URI du script actuel.\nInstallation interrompue.",
+ "config-no-cli-uri": "<strong>Attention :</strong> Aucun <code>--scriptpath</code> n’a été spécifié ; <code>$1</code> sera utilisé par défaut.",
+ "config-using-server": "Utilisation du nom de serveur « <nowiki>$1</nowiki> ».",
"config-using-uri": "Utilisation de l'URL de serveur \"<nowiki>$1$2</nowiki>\".",
"config-uploads-not-safe": "<strong>Attention :</strong> Votre répertoire par défaut pour les téléversements, <code>$1</code>, est vulnérable, car il peut exécuter n’importe quel script.\nBien que MediaWiki vérifie tous les fichiers téléversés, il est fortement recommandé de [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security fermer cette faille de sécurité] (texte en anglais) avant d’activer les téléversements.",
"config-no-cli-uploads-check": "'''Attention:''' Votre répertoire par défaut pour les imports(<code>$1</code>) n'est pas contrôlé concernant la vulnérabilité d'exécution de scripts arbitraires lors de l'installation CLI.",
@@ -143,7 +143,7 @@
"config-support-info": "MediaWiki prend en charge ces systèmes de bases de données :\n\n$1\n\nSi vous ne voyez pas le système de base de données que vous essayez d’utiliser ci-dessous, alors suivez les instructions ci-dessus (voir liens) pour activer la prise en charge.",
"config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] est le premier choix pour MediaWiki et est le mieux pris en charge. MediaWiki fonctionne aussi avec [{{int:version-db-mariadb-url}} MariaDB] et [{{int:version-db-percona-url}} Percona Server], qui sont compatibles avec MySQL. ([http://www.php.net/manual/en/mysqli.installation.php Comment compiler PHP avec la prise en charge de MySQL])",
"config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] est un système de base de données populaire en ''source ouverte'' qui peut être une alternative à MySQL ([http://www.php.net/manual/en/pgsql.installation.php Comment compiler PHP avec la prise en charge de PostgreSQL]).",
- "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] est un système de base de données léger bien pris en charge. ([http://www.php.net/manual/fr/pdo.installation.php Comment compiler PHP avec la prise en charge de SQLite], en utilisant PDO)",
+ "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] est un système de base de données léger bien pris en charge ([http://www.php.net/manual/fr/pdo.installation.php Comment compiler PHP avec la prise en charge de SQLite], en utilisant PDO).",
"config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] est un système commercial de gestion de base de données d’entreprise. ([http://www.php.net/manual/en/oci8.installation.php Comment compiler PHP avec la prise en charge d’OCI8])",
"config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] est une base de données commerciale d’entreprise pour Windows. ([http://www.php.net/manual/en/sqlsrv.installation.php Comment compiler PHP avec la prise en charge de SQLSRV])",
"config-header-mysql": "Paramètres de MySQL",
@@ -237,7 +237,7 @@
"config-profile-no-anon": "Création de compte requise",
"config-profile-fishbowl": "Éditeurs autorisés seulement",
"config-profile-private": "Wiki privé",
- "config-profile-help": "Les wikis fonctionnent au mieux lorsque vous laissez un maximum de personnes les modifier.\nAvec MediaWiki, il est facile de vérifier les modifications récentes et de révoquer tout dommage créé par des utilisateurs débutants ou mal intentionnés.\n\nCependant, MediaWiki est utilisé dans bien d’autres cas et il n’est pas toujours facile de convaincre chacun des bénéfices de l’esprit wiki.\nVous avez donc le choix.\n\nLe modèle <strong>{{int:config-profile-wiki}}</strong> autorise toute personne à modifier, y compris sans s’identifier.\n<strong>{{int:config-profile-no-anon}}</strong> fournit plus de contrôle, mais peut rebuter les contributeurs occasionnels.\n\n<strong>{{int:config-profile-fishbowl}}</strong> autorise la modification par les utilisateurs approuvés mais le public peut toujours consulter les pages et leur historique.\n<strong>{{int:config-profile-private}}</strong> n’autorise que les utilisateurs approuvés à voir et modifier les pages.\n\nDes configurations de droits d’utilisateurs plus complexes sont disponibles après l’installation, voir la [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights page correspondante du manuel].",
+ "config-profile-help": "Les wikis fonctionnent au mieux lorsque vous laissez un maximum de personnes les modifier.\nAvec MediaWiki, il est facile de vérifier les modifications récentes et de révoquer tout dommage créé par des utilisateurs débutants ou mal intentionnés.\n\nCependant, MediaWiki est utilisé dans bien d’autres cas et il n’est pas toujours facile de convaincre chacun des bénéfices de l’esprit wiki.\nVous avez donc le choix.\n\nLe modèle <strong>{{int:config-profile-wiki}}</strong> autorise toute personne à modifier, y compris sans s’identifier.\nUn wiki avec <strong>{{int:config-profile-no-anon}}</strong> fournit plus de contrôle, mais peut rebuter les contributeurs occasionnels.\n\nLe scénario <strong>{{int:config-profile-fishbowl}}</strong> autorise la modification par les utilisateurs approuvés mais le public peut toujours consulter les pages et leur historique.\n<strong>{{int:config-profile-private}}</strong> n’autorise que les utilisateurs approuvés à voir et modifier les pages.\n\nDes configurations de droits d’utilisateurs plus complexes sont disponibles après l’installation, voir la [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights page correspondante du manuel].",
"config-license": "Droits d'auteur et licence :",
"config-license-none": "Aucune licence en bas de page",
"config-license-cc-by-sa": "Creative Commons attribution partage à l'identique",
@@ -247,7 +247,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 ou ultérieure",
"config-license-pd": "Domaine public",
"config-license-cc-choose": "Sélectionner une licence Creative Commons personnalisée",
- "config-license-help": "Beaucoup de wikis publics mettent l’ensemble des contributions sous une [http://freedomdefined.org/Definition/Fr licence libre].\nCela contribue à créer un sentiment d’appartenance à une communauté et encourage les contributions sur le long terme.\nCe n’est généralement pas nécessaire pour un wiki privé ou d’entreprise.\n\nSi vous souhaitez utiliser des textes de Wikipédia, et souhaitez que Wikipédia puisse réutiliser des textes copiés depuis votre wiki, vous devriez choisir <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipédia utilisait auparavant la Licence de Documentation Libre GNU (GFDL).\nC’est une licence valide, mais difficile à comprendre. \nIl est aussi difficile de réutiliser du contenu sous la licence GFDL.",
+ "config-license-help": "Beaucoup de wikis publics mettent l’ensemble des contributions sous une [https://freedomdefined.org/Definition/Fr licence libre].\nCela contribue à créer un sentiment d’appartenance à une communauté et encourage les contributions sur le long terme.\nCe n’est généralement pas nécessaire pour un wiki privé ou d’entreprise.\n\nSi vous souhaitez utiliser des textes de Wikipédia, et souhaitez que Wikipédia puisse réutiliser des textes copiés depuis votre wiki, vous devriez choisir <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipédia utilisait auparavant la Licence de Documentation Libre GNU (GFDL).\nC’est une licence valide, mais difficile à comprendre. \nIl est aussi difficile de réutiliser du contenu sous la licence GFDL.",
"config-email-settings": "Paramètres de courriel",
"config-enable-email": "Activer les courriels sortants",
"config-enable-email-help": "Si vous souhaitez utiliser le courriel, vous devez [http://www.php.net/manual/en/mail.configuration.php configurer des paramètres PHP] (texte en anglais).\nSi vous ne voulez pas du service de courriel, vous pouvez le désactiver ici.",
@@ -277,7 +277,7 @@
"config-cache-options": "Paramètres pour la mise en cache des objets:",
"config-cache-help": "La mise en cache des objets améliore la vitesse de MediaWiki en mettant en cache les données fréquemment utilisées.\nLes sites de taille moyenne à grande sont fortement encouragés à l'activer. Les petits sites y verront également des avantages.",
"config-cache-none": "Pas de mise en cache (aucune fonctionnalité n'a été supprimée, mais la vitesse peut changer sur les wikis importants)",
- "config-cache-accel": "Mise en cache des objets PHP (APC, APCu, XCache ou WinCache)",
+ "config-cache-accel": "Mise en cache des objets PHP (APC, APCu ou WinCache)",
"config-cache-memcached": "Utiliser Memcached (nécessite une installation et une configuration supplémentaires)",
"config-memcached-servers": "serveurs pour Memcached :",
"config-memcached-help": "Liste des adresses IP à utiliser pour Memcached.\nUne par ligne, en indiquant le port à utiliser. Par exemple :\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -333,6 +333,7 @@
"config-install-mainpage-failed": "Impossible d’insérer la page principale : $1",
"config-install-done": "<strong>Félicitations!</strong>\nVous avez installé MediaWiki.\n\nLe programme d'installation a généré un fichier <code>LocalSettings.php</code>. Il contient tous les paramètres de votre configuration.\n\nVous devrez le télécharger et le mettre à la racine de votre installation wiki (dans le même répertoire que index.php). Le téléchargement devrait démarrer automatiquement.\n\nSi le téléchargement n'a pas été proposé, ou que vous l'avez annulé, vous pouvez redémarrer le téléchargement en cliquant ce lien :\n\n$3\n\n<strong>Note :</strong> Si vous ne le faites pas maintenant, ce fichier de configuration généré ne sera pas disponible plus tard si vous quittez l'installation sans le télécharger.\n\nLorsque c'est fait, vous pouvez <strong>[$2 accéder à votre wiki]</strong> .",
"config-install-done-path": "<strong>Félicitations !</strong>\nVous avez installé MédiaWiki.\n\nL’installeur a généré un fichier <code>LocalSettings.php</code>.\nIl contient toute votre configuration.\n\nVous devez le télécharger et le mettre dans <code>$4</code>. Le téléchargement devrait avoir démarré automatiquement.\n\nSi le téléchargement n’a pas été proposé ou si vous l’avez annulé, vous pouvez le redémarrer en cliquant sur le lien ci-dessous :\n\n$3\n\n<strong>Note :</strong> Si vous ne le faites pas maintenant, ce fichier de configuration généré ne sera plus disponible ultérieurement si vous quittez l’installation sans le télécharger.\n\nUne fois ceci fait, vous pouvez <strong>[$2 entrer dans votre wiki]</strong>.",
+ "config-install-success": "MédiaWiki a bien été installé. Vous pouvez maintenant\nvisiter <$1$2> pour voir votre wiki.\nSi vous avez des questions, consultez notre liste de questions fréquemment posées :\n<https://www.mediawiki.org/wiki/Manual:FAQ> ou utilisez un des\nforums de soutien liés sur cette page.",
"config-download-localsettings": "Télécharger <code>LocalSettings.php</code>",
"config-help": "aide",
"config-help-tooltip": "cliquer pour agrandir",
@@ -341,5 +342,5 @@
"config-skins-screenshots": "$1 (captures d’écran : $2)",
"config-screenshot": "Captures d’écrans",
"mainpagetext": "<strong>MediaWiki a été installé.</strong>",
- "mainpagedocfooter": "Consultez le [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guide de l’utilisateur du contenu] pour plus d’informations sur l’utilisation de ce logiciel de wiki.\n\n== Pour démarrer ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Liste des paramètres de configuration]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/fr Questions courantes sur MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Liste de discussion sur les distributions de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Adaptez MediaWiki dans votre langue]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Apprendre comment lutter contre le pourriel dans votre wiki]"
+ "mainpagedocfooter": "Consultez le [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guide de l’utilisateur] pour plus d’informations sur l’utilisation de ce logiciel de wiki.\n\n== Pour démarrer ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Liste des paramètres de configuration]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/fr Questions courantes sur MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Liste de discussion sur les distributions de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Adaptez MediaWiki dans votre langue]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Apprendre comment lutter contre le pourriel dans votre wiki]"
}
diff --git a/www/wiki/includes/installer/i18n/frc.json b/www/wiki/includes/installer/i18n/frc.json
index d07b2e98..5eac7d39 100644
--- a/www/wiki/includes/installer/i18n/frc.json
+++ b/www/wiki/includes/installer/i18n/frc.json
@@ -28,7 +28,7 @@
"config-restart": "Oui, le relancer",
"config-env-php": "PHP $1 est installé.",
"config-env-hhvm": "HHVM $1 est installé.",
- "config-unicode-using-intl": "Utilisation de [http://pecl.php.net/intl l'extension PECL intl] pour la normalisation Unicode.",
+ "config-unicode-using-intl": "Utilisation de [https://pecl.php.net/intl l'extension PECL intl] pour la normalisation Unicode.",
"config-diff3-bad": "GNU diff3 introuvable.",
"config-db-username": "Nom d’useur de la base de données:",
"config-db-password": "Mot de passe de la base de données:",
diff --git a/www/wiki/includes/installer/i18n/frp.json b/www/wiki/includes/installer/i18n/frp.json
index 5a2edf9c..7f943f24 100644
--- a/www/wiki/includes/installer/i18n/frp.json
+++ b/www/wiki/includes/installer/i18n/frp.json
@@ -30,9 +30,8 @@
"config-page-existingwiki": "Vouiqui ègzistent",
"config-env-php": "PHP $1 est enstalâ.",
"config-memory-raised": "Lo paramètre <code>memory_limit</code> de PHP ére a $1, portâ a $2.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] est enstalâ",
"config-apc": "[http://www.php.net/apc APC] est enstalâ",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] est enstalâ",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] est enstalâ",
"config-diff3-bad": "GNU diff3 entrovâblo.",
"config-db-type": "Tipo de bâsa de balyês :",
"config-db-host": "Hôto de la bâsa de balyês :",
diff --git a/www/wiki/includes/installer/i18n/gl.json b/www/wiki/includes/installer/i18n/gl.json
index e6e4b674..50b124e8 100644
--- a/www/wiki/includes/installer/i18n/gl.json
+++ b/www/wiki/includes/installer/i18n/gl.json
@@ -47,14 +47,14 @@
"config-help-restart": "Quere eliminar todos os datos gardados e reiniciar o proceso de instalación?",
"config-restart": "Si, reiniciala",
"config-welcome": "=== Comprobación da contorna ===\nCómpre realizar agora unhas comprobacións básicas para ver se a contorna é axeitada para a instalación de MediaWiki.\nLembre incluír esta información se necesita axuda para completar a instalación.",
- "config-copyright": "=== Dereitos de autor e termos de uso ===\n\n$1\n\nEste programa é software libre; pode redistribuílo e/ou modificalo segundo os termos da licenza pública xeral GNU publicada pola Free Software Foundation; versión 2 ou (na súa escolla) calquera outra posterior.\n\nEste programa distribúese coa esperanza de que poida ser útil, pero <strong>sen garantía ningunha</strong>; nin sequera a garantía implícita de <strong>comercialización</strong> ou <strong>adecuación a unha finalidade específica</strong>.\nOlle a licenza pública xeral GNU para obter máis detalles.\n\nDebería recibir <doclink href=Copying>unha copia da licenza pública xeral GNU</doclink> xunto ao programa; se non é así, escriba á Free Software Foundation, Inc., rúa Franklin, número 51, quinto andar, Boston, Massachusetts, 02110-1301, Estados Unidos de América ou [http://www.gnu.org/copyleft/gpl.html lea a licenza en liña].",
+ "config-copyright": "=== Dereitos de autor e termos de uso ===\n\n$1\n\nEste programa é software libre; pode redistribuílo e/ou modificalo segundo os termos da licenza pública xeral GNU publicada pola Free Software Foundation; versión 2 ou (na súa escolla) calquera outra posterior.\n\nEste programa distribúese coa esperanza de que poida ser útil, pero <strong>sen garantía ningunha</strong>; nin sequera a garantía implícita de <strong>comercialización</strong> ou <strong>adecuación a unha finalidade específica</strong>.\nOlle a licenza pública xeral GNU para obter máis detalles.\n\nDebería recibir <doclink href=Copying>unha copia da licenza pública xeral GNU</doclink> xunto ao programa; se non é así, escriba á Free Software Foundation, Inc., rúa Franklin, número 51, quinto andar, Boston, Massachusetts, 02110-1301, Estados Unidos de América ou [https://www.gnu.org/copyleft/gpl.html lea a licenza en liña].",
"config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/gl Páxina principal de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guía de usuario]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guía de administrador]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Preguntas máis frecuentes]\n----\n* <doclink href=Readme>Léame</doclink>\n* <doclink href=ReleaseNotes>Notas de lanzamento</doclink>\n* <doclink href=Copying>Copia</doclink>\n* <doclink href=UpgradeDoc>Actualizacións</doclink>",
"config-env-good": "Rematou a comprobación da contorna.\nPode instalar MediaWiki.",
"config-env-bad": "Rematou a comprobación da contorna.\nNon pode instalar MediaWiki.",
"config-env-php": "Está instalado o PHP $1.",
"config-env-hhvm": "Está instalado o HHVM $1.",
- "config-unicode-using-intl": "Usando a [http://pecl.php.net/intl extensión intl PECL] para a normalización Unicode.",
- "config-unicode-pure-php-warning": "<strong>Atención:</strong> A [http://pecl.php.net/intl extensión intl PECL] non está dispoñible para manexar a normalización Unicode; volvendo á execución lenta de PHP puro.\nSe o seu sitio posúe un alto tráfico de visitantes, debería ler un chisco sobre a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalización Unicode].",
+ "config-unicode-using-intl": "Usando a [https://pecl.php.net/intl extensión intl PECL] para a normalización Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Atención:</strong> A [https://pecl.php.net/intl extensión intl PECL] non está dispoñible para manexar a normalización Unicode; volvendo á execución lenta de PHP puro.\nSe o seu sitio posúe un alto tráfico de visitantes, debería ler un chisco sobre a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalización Unicode].",
"config-unicode-update-warning": "<strong>Atención:</strong> A versión instalada da envoltura de normalización Unicode emprega unha versión vella da biblioteca [http://site.icu-project.org/ do proxecto ICU].\nDebería [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations actualizar] se o uso de Unicode é importante para vostede.",
"config-no-db": "Non se puido atopar un controlador axeitado para a base de datos! Necesita instalar un controlador de base de datos para PHP.\n{{PLURAL:$2|Acéptase o seguinte tipo|Acéptanse os seguintes tipos}} de base de datos: $1.\n\nSe compilou o PHP vostede mesmo, reconfigúreo activando un cliente de base de datos, por exemplo, usando <code>./configure --with-mysqli</code>.\nSe instalou o PHP desde un paquete Debian ou Ubuntu, entón tamén necesita instalar, por exemplo, o módulo <code>php5-mysql</code>.",
"config-outdated-sqlite": "<strong>Atención:</strong> Ten o SQLite $1, que é inferior á versión mínima necesaria: $2. O SQLite non estará dispoñible.",
@@ -63,12 +63,11 @@
"config-pcre-no-utf8": "<strong>Erro fatal:</strong> Semella que o módulo PCRE do PHP foi compilado sen o soporte PCRE_UTF8.\nMediaWiki necesita soporte UTF-8 para funcionar correctamente.",
"config-memory-raised": "O parámetro <code>memory_limit</code> do PHP é $1. Aumentado a $2.",
"config-memory-bad": "<strong>Atención:<strong> O parámetro <code>memory_limit</code> do PHP é $1.\nProbablemente é un valor baixo de máis.\nA instalación pode fallar!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] está instalado",
"config-apc": "[http://www.php.net/apc APC] está instalado",
"config-apcu": "[http://www.php.net/apcu APCu] está instalado",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] está instalado",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] está instalado",
"config-no-cache-apcu": "<strong>Advertencia:</strong> Non se puido atopar [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] ou [http://www.iis.net/download/WinCacheForPhp WinCache].\nA caché de obxectos non está activada.",
- "config-mod-security": "<strong>Atención:</strong> O seu servidor web ten o [http://modsecurity.org/ mod_security] activado. Se estivese mal configurado, pode causar problemas a MediaWiki ou calquera outro software que permita aos usuarios publicar contidos arbitrarios.\nOlle a [http://modsecurity.org/documentation/ documentación do mod_security] ou póñase en contacto co soporte do seu servidor se atopa erros aleatorios.",
+ "config-mod-security": "<strong>Atención:</strong> O seu servidor web ten o [https://modsecurity.org/ mod_security] activado. Se estivese mal configurado, pode causar problemas a MediaWiki ou calquera outro software que permita aos usuarios publicar contidos arbitrarios.\nOlle a [https://modsecurity.org/documentation/ documentación do mod_security] ou póñase en contacto co soporte do seu servidor se atopa erros aleatorios.",
"config-diff3-bad": "GNU diff3 non se atopou.",
"config-git": "Atopouse o software de control da versión de Git: <code>$1</code>.",
"config-git-bad": "Non se atopou o software de control da versión de Git.",
@@ -226,7 +225,7 @@
"config-license-gfdl": "Licenza de documentación libre de GNU 1.3 ou posterior",
"config-license-pd": "Dominio público",
"config-license-cc-choose": "Seleccione unha licenza Creative Commons personalizada",
- "config-license-help": "Moitos wikis públicos liberan todas as súas contribucións baixo unha [http://freedomdefined.org/Definition/Gl licenza libre].\nIsto axuda a crear un sentido de propiedade comunitaria e anima a seguir contribuíndo durante moito tempo.\nXeralmente, non é necesario nos wikis privados ou de empresas.\n\nSe quere poder empregar textos da Wikipedia, así como que a Wikipedia poida aceptar textos copiados do seu wiki, escolla a licenza <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nA licenza de documentación libre de GNU era a licenza anterior da Wikipedia.\nMalia aínda ser unha licenza válida, é difícil de entender.\nTamén é difícil reusar contidos baixo esta licenza.",
+ "config-license-help": "Moitos wikis públicos liberan todas as súas contribucións baixo unha [https://freedomdefined.org/Definition/Gl licenza libre].\nIsto axuda a crear un sentido de propiedade comunitaria e anima a seguir contribuíndo durante moito tempo.\nXeralmente, non é necesario nos wikis privados ou de empresas.\n\nSe quere poder empregar textos da Wikipedia, así como que a Wikipedia poida aceptar textos copiados do seu wiki, escolla a licenza <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nA licenza de documentación libre de GNU era a licenza anterior da Wikipedia.\nMalia aínda ser unha licenza válida, é difícil de entender.\nTamén é difícil reusar contidos baixo esta licenza.",
"config-email-settings": "Configuración do correo electrónico",
"config-enable-email": "Activar os correos electrónicos de saída",
"config-enable-email-help": "Se quere que o correo electrónico funcione, cómpre configurar os [http://www.php.net/manual/en/mail.configuration.php parámetros PHP] correctamente.\nSe non quere ningunha característica no correo, pode desactivalas aquí.",
@@ -312,6 +311,7 @@
"config-install-mainpage-failed": "Non se puido inserir a páxina principal: $1",
"config-install-done": "<strong>Parabéns!</strong>\nInstalou MediaWiki.\n\nO programa de instalación xerou un ficheiro <code>LocalSettings.php</code>.\nEste ficheiro contén toda a súa configuración.\n\nTerá que descargalo e poñelo na base da instalación do seu wiki (no mesmo directorio ca index.php). A descarga debería comezar automaticamente.\n\nSe non comezou a descarga ou se a cancelou, pode facer que comece de novo premendo na ligazón que aparece a continuación:\n\n$3\n\n<strong>Nota:</strong> Se non fai iso agora, este ficheiro de configuración xerado non estará dispoñible máis adiante se sae da instalación sen descargalo.\n\nCando faga todo isto, xa poderá <strong>[$2 entrar no seu wiki]</strong>.",
"config-install-done-path": "<strong>Parabéns!</strong>\nInstalou MediaWiki.\n\nO instalador xerou un ficheiro <code>LocalSettings.php</code>.\nEste contén toda a súa configuración.\n\nDeberá descargalo e poñerlo en <code>$4</code>. A descarga debería ter comezado automaticamente.\n\nSe non comenzou a descarga, ou se a cancelou, podes reiniciala descarga premendo na seguinte ligazón:\n\n$3\n\n<strong>Nota</strong>: se non fai isto agora, este ficheiro de configuración xerado non estará dispoñible máis tarde se sae da instalación sen descargarlo.\n\nCando o teña feito, poderá <strong>[$2 entrar na súa wiki]</strong>.",
+ "config-install-success": "MediaWiki instalouse con éxito. Agora podes \nvisitar <$1$2> para ver a túa wiki.\nSe tes dúbidas, revisa a nosa lista de preguntas frecuentes:\n<https://www.mediawiki.org/wiki/Manual:FAQ> ou usa un dos\nforos de axuda ligados nesa páxina.",
"config-download-localsettings": "Descargar o <code>LocalSettings.php</code>",
"config-help": "axuda",
"config-help-tooltip": "prema para expandir",
diff --git a/www/wiki/includes/installer/i18n/gor.json b/www/wiki/includes/installer/i18n/gor.json
index be912701..3b7e1035 100644
--- a/www/wiki/includes/installer/i18n/gor.json
+++ b/www/wiki/includes/installer/i18n/gor.json
@@ -38,7 +38,6 @@
"config-db-name": "Tanggulo basis data",
"config-db-name-oracle": "Skema lo basis data",
"config-db-username": "Basis data lo tanggulo user",
- "config-charset-mysql5": "MySQL 4.1/5.0 UFT-8",
"config-type-mysql": "MySQL (meyalo umopasiya)",
"config-header-mysql": "Aturangi lo MySQL",
"config-header-postgres": "Aturangi lo PostgreSQL",
diff --git a/www/wiki/includes/installer/i18n/gsw.json b/www/wiki/includes/installer/i18n/gsw.json
index 73331d97..44f5eb2b 100644
--- a/www/wiki/includes/installer/i18n/gsw.json
+++ b/www/wiki/includes/installer/i18n/gsw.json
@@ -36,22 +36,21 @@
"config-help-restart": "Witt alli Date, wu Du yygee hesch, lesche un d Inschtallation nomol aafange?",
"config-restart": "Jo, nomol aafange",
"config-welcome": "=== Priefig vu dr Inschtallationsumgäbig ===\nBasispriefige wäre durgfiert zum Feschtstelle, eb d Inschtallationsumgäbig fir d Inschtallation vu MediaWiki geignet isch.\nDu sottsch d Ergebnis vu däre Priefig aagee, wänn Du bi dr Inschtallation Hilf bruchsch.",
- "config-copyright": "=== Copyright un Nutzigsbedingige ===\n\n$1\n\nDes Programm isch e freji Software, d. h. s cha, no dr Bedingige vu dr GNU General Public-Lizänz, wu vu dr Free Software Foundation vereffentligt woren isch, wyterverteilt un/oder modifiziert wäre. Doderbyy cha d Version 2, oder no eigenem Ermässe, jedi nejeri Version vu dr Lizänz brucht wäre.\n\nDes Programm wird in dr Hoffnig verteilt, ass es nitzli isch, aber '''ohni jedi Garanti''' un sogar ohni di impliziert Garanti vun ere '''Märtgängigkeit''' oder '''Eignig fir e bstimmte Zwäck'''. Doderzue git meh Hiiwys in dr GNU General Public-Lizänz.\n\nE <doclink href=Copying>Kopi vu dr GNU General Public-Lizänz</doclink> sott zämme mit däm Programm verteilt wore syy. Wänn des nit eso isch, cha ne Kopi bi dr Free Software Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, schriftli aagforderet oder [http://www.gnu.org/copyleft/gpl.html online gläse] wäre.",
+ "config-copyright": "=== Copyright un Nutzigsbedingige ===\n\n$1\n\nDes Programm isch e freji Software, d. h. s cha, no dr Bedingige vu dr GNU General Public-Lizänz, wu vu dr Free Software Foundation vereffentligt woren isch, wyterverteilt un/oder modifiziert wäre. Doderbyy cha d Version 2, oder no eigenem Ermässe, jedi nejeri Version vu dr Lizänz brucht wäre.\n\nDes Programm wird in dr Hoffnig verteilt, ass es nitzli isch, aber '''ohni jedi Garanti''' un sogar ohni di impliziert Garanti vun ere '''Märtgängigkeit''' oder '''Eignig fir e bstimmte Zwäck'''. Doderzue git meh Hiiwys in dr GNU General Public-Lizänz.\n\nE <doclink href=Copying>Kopi vu dr GNU General Public-Lizänz</doclink> sott zämme mit däm Programm verteilt wore syy. Wänn des nit eso isch, cha ne Kopi bi dr Free Software Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, schriftli aagforderet oder [https://www.gnu.org/copyleft/gpl.html online gläse] wäre.",
"config-sidebar": "* [https://www.mediawiki.org MediaWiki Websyte vu MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Nutzeraaleitig zue MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Adminischtratoreaaleitig zue MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Vilmol gstellti Froge zue MediaWiki]",
"config-env-good": "D Inschtallationsumgäbig isch prieft wore.\nDu chasch MediaWiki inschtalliere.",
"config-env-bad": "D Inschtallationsumgäbigisch prieft wore.\nDu chasch MediaWiki nit inschtalliere.",
"config-env-php": "PHP $1 isch inschtalliert.",
- "config-unicode-using-intl": "For d Unicode-Normalisierig wird d [http://pecl.php.net/intl PECL-Erwyterig intl] yygsetzt.",
- "config-unicode-pure-php-warning": "'''Warnig:''' D [http://pecl.php.net/intl PECL-Erwyterig intl] isch fir d Unicode-Normalisierig nit verfiegbar. Wäge däm wird di langsam pure-PHP-Implementierig brucht.\nWänn Du ne Websyte mit ere große Bsuechrzahl bedrybsch, sottsch e weng ebis läse iber [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-Normalisierig (en)].",
+ "config-unicode-using-intl": "For d Unicode-Normalisierig wird d [https://pecl.php.net/intl PECL-Erwyterig intl] yygsetzt.",
+ "config-unicode-pure-php-warning": "'''Warnig:''' D [https://pecl.php.net/intl PECL-Erwyterig intl] isch fir d Unicode-Normalisierig nit verfiegbar. Wäge däm wird di langsam pure-PHP-Implementierig brucht.\nWänn Du ne Websyte mit ere große Bsuechrzahl bedrybsch, sottsch e weng ebis läse iber [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-Normalisierig (en)].",
"config-unicode-update-warning": "'''Warnig:''' Di inschtalliert Version vum Unicode-Normalisierigswrapper verwändet e elteri Version vu dr Bibliothek vum [http://site.icu-project.org/ ICU-Projäkt].\nDu sottsch si [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations aktualisiere], wänn Dor d Verwändig vu Unicode wichtig isch.",
"config-no-db": "S isch kei adäquate Datebanktryyber gfunde wore!",
"config-no-fts3": "'''Warnig:''' SQLite isch ohni s [//sqlite.org/fts3.html FTS3-Modul] kumpiliert wore, s stehn kei Suechfunktione z Verfiegig.",
"config-pcre-no-utf8": "'''Fatale Fähler: S PHP-Modul PCRE isch schyns ohni PCRE_UTF8-Unterstitzig kompiliert wore.'''\nMediaWiki brucht d UTF-8-Unterstitzi zum fählerfrej lauffähig syy.",
"config-memory-raised": "Dr PHP-Parameter <code>memory_limit</code> lyt bi $1 un isch uf $2 uffegsetzt wore.",
"config-memory-bad": "'''Warnig:''' Dr PHP-Parameter <code>memory_limit</code> lyt bi $1.\nDää Wärt isch wahrschyns z nider.\nDr Inschtallationsvorgang chennt wäge däm fählschlaa!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] isch inschtalliert",
"config-apc": "[http://www.php.net/apc APC] isch inschtalliert",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] isch inschtalliert",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] isch inschtalliert",
"config-diff3-bad": "GNU diff3 isch nit gfunde wore.",
"config-imagemagick": "ImageMagick isch gfunde wore: <code>$1</code>.\nMiniaturaasichte vu Bilder sin megli, sobald s Uffelade vu Dateie aktiviert isch.",
"config-help": "Hilf",
diff --git a/www/wiki/includes/installer/i18n/he.json b/www/wiki/includes/installer/i18n/he.json
index 75834552..d388a595 100644
--- a/www/wiki/includes/installer/i18n/he.json
+++ b/www/wiki/includes/installer/i18n/he.json
@@ -50,14 +50,14 @@
"config-help-restart": "האם ברצונך לנקות את כל הנתונים שהזנת ולהתחיל מחדש את תהליך ההתקנה?",
"config-restart": "כן, להפעיל מחדש",
"config-welcome": "=== בדיקות סביבה ===\nבדיקות בסיסיות תתבצענה עכשיו כדי לראות אם הסביבה הזאת מתאימה להתקנת מדיה־ויקי.\nנא לזכור לכלול את המידע הזה בעת בקשת תמיכה עם השלמת ההתקנה.",
- "config-copyright": "=== זכויות יוצרים ותנאים ===\n\n$1\n\nתכנית זו היא תכנה חופשית; באפשרותך להפיצה מחדש ו/או לשנות אותה על פי תנאי הרישיון הציבורי הכללי של GNU כפי שפורסם על ידי קרן התכנה החופשית; בין אם גרסה 2 של הרישיון, ובין אם (לפי בחירתך) כל גרסה מאוחרת שלו.\n\nתכנית זו מופצת בתקווה שתהיה מועילה, אבל '''בלא אחריות כלשהי'''; ואפילו ללא האחריות המשתמעת בדבר '''מסחריותה''' או '''התאמתה למטרה '''מסוימת'''. לפרטים נוספים, ניתן לעיין ברישיון הציבורי הכללי של GNU.\n\nלתכנית זו אמור היה להיות מצורף <doclink href=Copying>עותק של הרישיון הציבורי הכללי של GNU</doclink>; אם לא קיבלת אותו, אפשר לכתוב ל־Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA או [http://www.gnu.org/copyleft/gpl.html לקרוא אותו דרך האינטרנט].",
+ "config-copyright": "=== זכויות יוצרים ותנאים ===\n\n$1\n\nתכנית זו היא תכנה חופשית; באפשרותך להפיצה מחדש ו/או לשנות אותה על פי תנאי הרישיון הציבורי הכללי של GNU כפי שפורסם על ידי קרן התכנה החופשית; בין אם גרסה 2 של הרישיון, ובין אם (לפי בחירתך) כל גרסה מאוחרת שלו.\n\nתכנית זו מופצת בתקווה שתהיה מועילה, אבל '''בלא אחריות כלשהי'''; ואפילו ללא האחריות המשתמעת בדבר '''מסחריותה''' או '''התאמתה למטרה '''מסוימת'''. לפרטים נוספים, ניתן לעיין ברישיון הציבורי הכללי של GNU.\n\nלתכנית זו אמור היה להיות מצורף <doclink href=Copying>עותק של הרישיון הציבורי הכללי של GNU</doclink>; אם לא קיבלת אותו, אפשר לכתוב ל־Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA או [https://www.gnu.org/copyleft/gpl.html לקרוא אותו דרך האינטרנט].",
"config-sidebar": "* [https://www.mediawiki.org אתר הבית של מדיה־ויקי]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents המדריך למשתמש]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents המדריך למנהל]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ שו״ת]\n----\n* <doclink href=Readme>קרא אותי</doclink>\n* <doclink href=ReleaseNotes>הערות גרסה</doclink>\n* <doclink href=Copying>העתקה</doclink>\n* <doclink href=UpgradeDoc>שדרוג</doclink>",
"config-env-good": "הסביבה שלכם נבדקה.\nאפשר להתקין מדיה־ויקי.",
"config-env-bad": "הסביבה שלכם נבדקה.\nאי־אפשר להתקין מדיה־ויקי.",
"config-env-php": "מותקנת <span dir=\"ltr\">PHP $1</span>.",
"config-env-hhvm": "מותקנת <span dir=\"ltr\">HHVM $1</span>.",
- "config-unicode-using-intl": "משתמש ב[http://pecl.php.net/intl הרחבת intl PECL] לנרמול יוניקוד.",
- "config-unicode-pure-php-warning": "<strong>אזהרה:</strong> [http://pecl.php.net/intl הרחבת intl PECL] אינה זמינה לטיפול בנרמול יוניקוד. משתמש ביישום PHP טהור ואטי יותר.\nאם זהו אתר בעל תעבורה גבוהה, כדאי לקרוא את המסמך הבא: [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
+ "config-unicode-using-intl": "משתמש ב[https://pecl.php.net/intl הרחבת intl PECL] לנרמול יוניקוד.",
+ "config-unicode-pure-php-warning": "<strong>אזהרה:</strong> [https://pecl.php.net/intl הרחבת intl PECL] אינה זמינה לטיפול בנרמול יוניקוד. משתמש ביישום PHP טהור ואטי יותר.\nאם זהו אתר בעל תעבורה גבוהה, כדאי לקרוא את המסמך הבא: [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
"config-unicode-update-warning": "'''אזהרה''': הגרסה המותקנת של מעטפת נרמול יוניקוד משתמשת בגרסה ישנה של הספרייה של [http://site.icu-project.org/ פרויקט ICU].\nכדאי [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations לעדכן] אם הטיפול ביוניקוד חשוב לך.",
"config-no-db": "לא נמצא דרייבר מסד נתונים מתאים. יש להתקין דרייבר מסד נתונים ל־PHP.\n{{PLURAL:$2|נתמך הסוג הבא של מסד נתונים|נתמכים הסוגים הבאים של מסדי נתונים}}: $1.\n\nאם קִמפלת את PHP בעצמך, יש להגדיר אותו מחדש ולהפעיל את לקוח מסד נתונים, למשל באמצעות <code dir=\"ltr\">./configure --with-mysqli</code>.\nאם התקנת את PHP מחבילה של דביאן או של אובונטו, יש להתקין, למשל, גם את המודול <code dir=\"ltr\">php5-mysql</code>.",
"config-outdated-sqlite": "'''אזהרה''': במערכת מתוקן SQLite $1. גרסה זו לא נתמכת ולשימוש ב־SQLite נדרשת גרסה $2 לפחות. SQLlite לא יהיה זמין.",
@@ -66,12 +66,11 @@
"config-pcre-no-utf8": "<strong>שגיאה סופנית</strong>: נראה שמודול PCRE של PHP מהודר ללא תמיכה ב־PCRE_UTF8.\nמדיה־ויקי דורשת תמיכה ב־UTF-8 לפעילות נכונה.",
"config-memory-raised": "ערך האפשרות <code>memory_limit</code> של PHP הוא $1, הועלה ל־$2.",
"config-memory-bad": "'''אזהרה:''' ערך האפשרות <code>memory_limit</code> של PHP הוא $1.\nזה כנראה נמוך מדי.\nההתקנה עשויה להיכשל!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] מותקן",
"config-apc": "[http://www.php.net/apc APC] מותקן",
"config-apcu": "[http://www.php.net/apcu APCu] מותקן",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] מותקן",
- "config-no-cache-apcu": "<strong>אזהרה:</strong> לא נמצא [http://www.php.net/apcu APCu]‏, [http://xcache.lighttpd.net/ XCache] או [http://www.iis.net/download/WinCacheForPhp WinCache].\nמטמון עצמים לא מופעל.",
- "config-mod-security": "'''אזהרה''': בשרת הווב שלך מופעל [http://modsecurity.org/ mod_security]. אם הוא לא מוגדר טוב, זה יכול לגרום לבעיות במדיה־ויקי ובתכנה אחרת שמאפשרת למשתמשים לשלוח תוכן שרירותי.\nיש לקרוא את [http://modsecurity.org/documentation/ התיעוד של mod_security] או ליצור קשר עם אנשי התמיכה של שירותי האירוח שלכם אם מופיעות לך שגיאות אקראיות.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] מותקן",
+ "config-no-cache-apcu": "<strong>אזהרה:</strong> לא נמצא [http://www.php.net/apcu APCu]‏ או [http://www.iis.net/download/WinCacheForPhp WinCache].\nמטמון עצמים לא מופעל.",
+ "config-mod-security": "'''אזהרה''': בשרת הווב שלך מופעל [https://modsecurity.org/ mod_security]. אם הוא לא מוגדר טוב, זה יכול לגרום לבעיות במדיה־ויקי ובתכנה אחרת שמאפשרת למשתמשים לשלוח תוכן שרירותי.\nיש לקרוא את [https://modsecurity.org/documentation/ התיעוד של mod_security] או ליצור קשר עם אנשי התמיכה של שירותי האירוח שלכם אם מופיעות לך שגיאות אקראיות.",
"config-diff3-bad": "GNU diff3 לא נמצא.",
"config-git": "נמצאה Git, תכנת בקרת התצורה: <code dir=\"ltr\">$1</code>.",
"config-git-bad": "תכנת בקרת התצורה Git לא נמצאה.",
@@ -226,7 +225,7 @@
"config-license-gfdl": "רישיון חופשי למסמכים של גנו גרסה 1.3 או חדשה יותר",
"config-license-pd": "נחלת הכלל",
"config-license-cc-choose": "בחירת רישיון קריאייטיב קומונז מותאם אישית",
- "config-license-help": "אתרי ויקי ציבוריים רבים מפרסמים את כל התרומות [http://freedomdefined.org/Definition ברישיון חופשי].\nזה עוזר ליצור תחושה של בעלות קהילתית ומעודד תרומה לאורך זמן.\nזה בדרך כלל לא נחוץ לאתר ויקי פרטי או אתר של חברה מסחרית.\n\nאם האפשרות להשתמש בטקסט מוויקיפדיה והאפשרות שוויקיפדיה תוכל תקבל עותקים של טקסטים מהוויקי שלך חשובות לך, כדאי לבחור ב<strong>{{int:config-license-cc-by-sa}}</strong>.\n\nויקיפדיה השתמשה בעבר ברישיון החופשי למסמכים של גנו (GNU FDL או GFDL).\nהוא עדיין רישיון תקין, אבל קשה להבנה.\nכמו־כן, קשה לעשות שימוש חוזר ביצירות שפורסמו לפי GFDL.",
+ "config-license-help": "אתרי ויקי ציבוריים רבים מפרסמים את כל התרומות [https://freedomdefined.org/Definition ברישיון חופשי].\nזה עוזר ליצור תחושה של בעלות קהילתית ומעודד תרומה לאורך זמן.\nזה בדרך כלל לא נחוץ לאתר ויקי פרטי או אתר של חברה מסחרית.\n\nאם האפשרות להשתמש בטקסט מוויקיפדיה והאפשרות שוויקיפדיה תוכל תקבל עותקים של טקסטים מהוויקי שלך חשובות לך, כדאי לבחור ב<strong>{{int:config-license-cc-by-sa}}</strong>.\n\nויקיפדיה השתמשה בעבר ברישיון החופשי למסמכים של גנו (GNU FDL או GFDL).\nהוא עדיין רישיון תקין, אבל קשה להבנה.\nכמו־כן, קשה לעשות שימוש חוזר ביצירות שפורסמו לפי GFDL.",
"config-email-settings": "הגדרות דוא״ל",
"config-enable-email": "להפעיל דוא״ל יוצא",
"config-enable-email-help": "אם אתם רוצים שדוא״ל יעבוד, [http://www.php.net/manual/en/mail.configuration.php אפשרויות הדוא״ל של PHP] צריכות להיות מוגדרות נכון.\nאם אינכם רוצים להפעיל שום אפשרויות דוא״ל, כבו אותן כאן ועכשיו.",
@@ -256,7 +255,7 @@
"config-cache-options": "הגדרות למטמון עצמים (object caching):",
"config-cache-help": "מטמון עצמים משמש לשיפור המהירות של מדיה־ויקי על־ידי שמירה של נתונים שהשימוש בהם נפוץ במטמון.\nלאתרים בינוניים וגדולים כדאי מאוד להפעיל את זה, וגם אתרים קטנים ייהנו מזה.",
"config-cache-none": "ללא מטמון (שום יכולת אינה מוּסרת, אבל הביצועים באתרים גדולים ייפגעו)",
- "config-cache-accel": "מטמון עצמים (object caching) של PHP&rlm; (APC,&rlm; APCu,&rlm; XCache או WinCache)",
+ "config-cache-accel": "מטמון עצמים (object caching) של PHP&rlm; (APC,&rlm; APCu,&rlm; או WinCache)",
"config-cache-memcached": "להשתמש ב־Memcached (דורש התקנות והגדרות נוספות)",
"config-memcached-servers": "שרתי Memcached:",
"config-memcached-help": "רשימת כתובות IP ש־Memcached ישתמש בהן.\nיש לרשום כתובת אחת בכל שורה ולציין את הפִּתְחָה (port), למשל:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -312,6 +311,7 @@
"config-install-mainpage-failed": "לא הצליחה הכנסת דף ראשי: $1.",
"config-install-done": "<strong>מזל טוב!</strong>\nהתקנת את תוכנת מדיה־ויקי.\n\nתוכנת ההתקנה יצרה את הקובץ <code>LocalSettings.php</code>.\nהוא מכיל את כל ההגדרות שלך.\n\nיש להוריד אותו ולהכניס אותו לתיקיית הבסיס שבה הותקן הוויקי שלך (אותה התיקייה שבה נמצא הקובץ index.php). ההורדה אמורה להתחיל באופן אוטומטי.\n\nאם ההורדה לא התחילה, או אם ביטלת אותה, אפשר להתחיל אותה מחדש באמצעות לחיצה על הקישור הבא:\n\n$3\n\n<strong>לתשומת לבך:</strong> אם ההורדה לא תבוצע כעת, קובץ ההגדרות <strong>לא</strong> יהיה זמין מאוחר יותר אם תוכנת ההתקנה תיסגר לפני שהקובץ יורד.\n\nלאחר שביצעת את הפעולות שלהלן, באפשרותך <strong>[$2 להיכנס לאתר הוויקי שלך]</strong>.",
"config-install-done-path": "<strong>מזל טוב!</strong>\nהתקנת את תוכנת מדיה־ויקי.\n\nתוכנת ההתקנה יצרה את הקובץ <code>LocalSettings.php</code>.\nהוא מכיל את כל ההגדרות שלך.\n\nיש להוריד אותו ולהכניס אותו לתיקייה <code>$4</code>. ההורדה אמורה להתחיל באופן אוטומטי.\n\nאם ההורדה לא התחילה, או אם ביטלת אותה, אפשר להתחיל אותה מחדש באמצעות לחיצה על הקישור הבא:\n\n$3\n\n<strong>לתשומת לבך:</strong> אם ההורדה לא תבוצע כעת, קובץ ההגדרות <strong>לא</strong> יהיה זמין מאוחר יותר אם תוכנת ההתקנה תיסגר לפני שהקובץ יורד.\n\nלאחר שביצעת את הפעולות שלהלן, באפשרותך <strong>[$2 להיכנס לאתר הוויקי שלך]</strong>.",
+ "config-install-success": "מדיה־ויקי הותקנה בהצלחה. עכשיו אפשר\nלבקר בכתובת <$1$2> כדי לצפות בוויקי שלך.\nאם יש לך שאלות, ר' את רשימת השאלות הנפוצות שלנו:\n<https://www.mediawiki.org/wiki/Manual:FAQ> ואפשר גם להשתמש\nבאתרי התמיכה שקישורים אליהם מופיעים באותו הדף.",
"config-download-localsettings": "הורדת <code>LocalSettings.php</code>",
"config-help": "עזרה",
"config-help-tooltip": "להרחיב",
diff --git a/www/wiki/includes/installer/i18n/hi.json b/www/wiki/includes/installer/i18n/hi.json
index 9863beb1..5e9e1e20 100644
--- a/www/wiki/includes/installer/i18n/hi.json
+++ b/www/wiki/includes/installer/i18n/hi.json
@@ -47,10 +47,9 @@
"config-env-php": "PHP $1 स्थापित किया गया है।",
"config-env-hhvm": "एचएचवीएम $1 स्थापित किया गया है।",
"config-memory-raised": "पीएचपी की <code>memory_limit</code> सीमा $1 है, जो $2 तक बढ़ गई है।",
- "config-xcache": "[http://xcache.lighttpd.net/ एक्सकैश] स्थापित है।",
"config-apc": "[http://www.php.net/apc एपीसी] स्थापित है।",
"config-apcu": "[http://www.php.net/apcu एपीसीयू] स्थापित है।",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp विनकैश] स्थापित है।",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp विनकैश] स्थापित है।",
"config-using-32bit": "<विशेष>चेतावनी:</विशेष> आपका सिस्टम 32-बिट पूर्णांक के साथ चल रहा है यह [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit विवेचित नहीं है]।",
"config-db-type": "डेटाबेस प्रकार:",
"config-db-host": "डेटाबेस होस्ट:",
diff --git a/www/wiki/includes/installer/i18n/hrx.json b/www/wiki/includes/installer/i18n/hrx.json
index 6bee175d..343f50b9 100644
--- a/www/wiki/includes/installer/i18n/hrx.json
+++ b/www/wiki/includes/installer/i18n/hrx.json
@@ -42,13 +42,13 @@
"config-help-restart": "Solle all bereits ingebne Daten gelöscht und der Installationsvoargang erneit oogefäng sin?",
"config-restart": "Jo, erneit oonfänge",
"config-welcome": "=== Prüfung von die Installationsumgebung ===\nDie Basisprüfunge were jetzt doorrichgefüahrt, um festzustelle, ob die Installationsumgebung für MediaWiki geeichnet ist.\nNotier die Informatione und geb se an, sofern du Hellf beim Installiere benötichst.",
- "config-copyright": "=== Lizenz und Nutzungsbedingunge ===\n\n$1\n\nDas Programm ist freie Software, d. h. es kann, gemäss den Bedingunge der von der Free Software Foundation veröffentlichte ''GNU General Public License'', weiterverteilt und/oder modifiziert sin. Dabei kann die Version 2, orrer noh eichnem Ermess, jede neuire Version von der Lizenz verwennet sin.\n\nDas Programm weard in der Hoffnung verteilt, dass das nützlich sein weard, dennoch '''ohne jechliche Garantie''' und sogoor ohne die implizierte Garantie von ener '''Marrektgängigkeit''' orrer '''Eichnung für en bestimmte Zweck'''. Hierzu sind weitre Hinweise in der ''GNU General Public License'' enthalt.\n\nEn <doclink href=Copying>Kopie von der GNU General Public License</doclink> sollt zusammer mit dem Programm verteilt woard sin. Sofern das net der Fall woar, kann en Kopie bei der Free Software Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, schriftlich verlangt sin orrer uff ehre Website [http://www.gnu.org/copyleft/gpl.html online gelesen] sin.",
+ "config-copyright": "=== Lizenz und Nutzungsbedingunge ===\n\n$1\n\nDas Programm ist freie Software, d. h. es kann, gemäss den Bedingunge der von der Free Software Foundation veröffentlichte ''GNU General Public License'', weiterverteilt und/oder modifiziert sin. Dabei kann die Version 2, orrer noh eichnem Ermess, jede neuire Version von der Lizenz verwennet sin.\n\nDas Programm weard in der Hoffnung verteilt, dass das nützlich sein weard, dennoch '''ohne jechliche Garantie''' und sogoor ohne die implizierte Garantie von ener '''Marrektgängigkeit''' orrer '''Eichnung für en bestimmte Zweck'''. Hierzu sind weitre Hinweise in der ''GNU General Public License'' enthalt.\n\nEn <doclink href=Copying>Kopie von der GNU General Public License</doclink> sollt zusammer mit dem Programm verteilt woard sin. Sofern das net der Fall woar, kann en Kopie bei der Free Software Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, schriftlich verlangt sin orrer uff ehre Website [https://www.gnu.org/copyleft/gpl.html online gelesen] sin.",
"config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/de Website von MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/de Benutzeroonleitung]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/de Administratorenoonleitung]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/de Häifig gestellte Frache]\n----\n* <doclink href=Readme>Lies mich</doclink>\n* <doclink href=ReleaseNotes>Versionsinformatione</doclink>\n* <doclink href=Copying>Lizenzbestimmunge</doclink>\n* <doclink href=UpgradeDoc>Aktualisierung</doclink>",
"config-env-good": "Die Installationsumgebung woard geprüft.\nMediaWiki kann installiert sin.",
"config-env-bad": "Die Installationsumgebung woard geprüft.\nMediaWiki kann net installiert sin.",
"config-env-php": "Die Skriptsproch „PHP“ ($1) ist installiert.",
- "config-unicode-using-intl": "Zur Unicode-Normalisierung weard die [http://pecl.php.net/intl PECL-Erweiterung intl] ingesetzt.",
- "config-unicode-pure-php-warning": "'''Warnung:''' Die [http://pecl.php.net/intl PECL-Erweiterung intl] ist für die Unicode-Normalisierung net verfüchbar, so dass stattdessen die langsame pure-PHP-Implementierung genutzt weard.\nSofern en Webseit mit grosser Benutzeranzoohl betrieb weard, sollte weitre Informatione uff der Webseite [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-Normalisierung (en)] geles sin.",
+ "config-unicode-using-intl": "Zur Unicode-Normalisierung weard die [https://pecl.php.net/intl PECL-Erweiterung intl] ingesetzt.",
+ "config-unicode-pure-php-warning": "'''Warnung:''' Die [https://pecl.php.net/intl PECL-Erweiterung intl] ist für die Unicode-Normalisierung net verfüchbar, so dass stattdessen die langsame pure-PHP-Implementierung genutzt weard.\nSofern en Webseit mit grosser Benutzeranzoohl betrieb weard, sollte weitre Informatione uff der Webseite [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-Normalisierung (en)] geles sin.",
"config-unicode-update-warning": "'''Warnung:''' Die installierte Version von der Unicode-Normalisierungswrappers nutzt en ältre Version von der [http://site.icu-project.org/ ICU-Projekts] sein Bibliothek.\nDie sollte [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations aktualisiert] sin, sofern uff die Verwennung von Unicode Wert geleht weard.",
"config-no-db": "Es konnt ken adäquater Datenbanktreiwer gefund sin. Es muss doher en Datenbanktreiwer für PHP installiert sin.\nDie folchende Datebanksysteme werre unnerstützt: $1\n\nWenn du PHP sellebst kompiliert host, konfigurier es erneit mit en aktiviert Datebankclient, zum Beispiel dorrich Verwennung von <code>./configure --with-mysqli</code>.\nWenn du PHP von en Debian- orrer Ubuntu-Paket installiert host, dann musst du ooch beispielsweis das <code>php5-mysql</code>-Paket installiere.",
"config-outdated-sqlite": "'''Warnung:''' SQLite $1 ist installiert. Allerdings benöticht MediaWiki SQLite $2 orrer höcher. SQLite weard doher net verfüchbar sin.",
@@ -57,10 +57,9 @@
"config-pcre-no-utf8": "'''Fataler Fehler:''' Das PHP-Modul PCRE scheint ohne PCRE_UTF8-Unterstützung kompiliert worre sin.\nMediaWiki benöticht die UTF-8-Unnerstützung, um fehlerfrei looffähich zu sin.",
"config-memory-raised": "Der PHP-Parameter <code>memory_limit</code> betruch $1 und woard uff $2 erhöcht.",
"config-memory-bad": "'''Warnung:''' Der PHP-Parameter <code>memory_limit</code> beträcht $1.\nDer Weart ist wahrscheinlich zu niedrich.\nDer Installationsvoargang könnt doher scheitre!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] ist installiert",
"config-apc": "[http://www.php.net/apc APC] ist installiert",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] ist installiert",
- "config-mod-security": "'''Warnung:''' Uff dem Webserver woard [http://modsecurity.org/ ModSecurity] aktiviert. Sofern falsch konfiguriert, kann das zu Probleme mit MediaWiki sowie annrer Software uff dem Server führe und es Benutzer ermöchliche beliebiche Inhalte im Wiki Renzustelle.\nFür weitre Informatione empfehle mir die [http://modsecurity.org/documentation/ Dokumentation zu ModSecurity] orrer den Kontakt zum Hoster, sofern Fehler ufftrete.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] ist installiert",
+ "config-mod-security": "'''Warnung:''' Uff dem Webserver woard [https://modsecurity.org/ ModSecurity] aktiviert. Sofern falsch konfiguriert, kann das zu Probleme mit MediaWiki sowie annrer Software uff dem Server führe und es Benutzer ermöchliche beliebiche Inhalte im Wiki Renzustelle.\nFür weitre Informatione empfehle mir die [https://modsecurity.org/documentation/ Dokumentation zu ModSecurity] orrer den Kontakt zum Hoster, sofern Fehler ufftrete.",
"config-diff3-bad": "GNU diff3 woard net gefund.",
"config-git": "Die Versionsverwaltungssoftware „Git“ woard gefund: <code>$1</code>.",
"config-git-bad": "Die Versionsverwaltungssoftware „Git“ woard net gefund.",
@@ -212,7 +211,7 @@
"config-license-gfdl": "GNU-Lizenz für freie Dokumentation 1.3 orrer höcher",
"config-license-pd": "Gemeinfreiheit",
"config-license-cc-choose": "En benutzerdefiniert Creative-Commons-Lizenz auswähle",
- "config-license-help": "Viele öffentliche Wikis publiziere all Beiträche unner en [http://freedomdefined.org/Definition/De freie Lizenz].\nDas träht dozu bei en Gefühl von Gemeinschaft zu schaffe und ermuticht zu längerfristicher Mitoorweit.\nDahinchege ist im Allgemeinen en freie Lizenz uff geschlossne Wikis net notwennich.\n\nSoweit man Texte aus der Wikipedia verwenne möcht und umgekehrt, sollt die Creative Commons-Lizenz \"Noomenennung, Weitergäb unner gleiche Bedingunge\" gewählt sin.\n\nDie Wikipedia nutzte voarmols die GNU-Lizenz für freie Dokumentation (GFDL).\nDie GFDL ist en gültiche Lizenz, wo awer schwear zu verstehn ist.\nEs ist zudem schwierich gemäss die Lizenz lizenziert Inhalte wiederzuverwenne.",
+ "config-license-help": "Viele öffentliche Wikis publiziere all Beiträche unner en [https://freedomdefined.org/Definition/De freie Lizenz].\nDas träht dozu bei en Gefühl von Gemeinschaft zu schaffe und ermuticht zu längerfristicher Mitoorweit.\nDahinchege ist im Allgemeinen en freie Lizenz uff geschlossne Wikis net notwennich.\n\nSoweit man Texte aus der Wikipedia verwenne möcht und umgekehrt, sollt die Creative Commons-Lizenz \"Noomenennung, Weitergäb unner gleiche Bedingunge\" gewählt sin.\n\nDie Wikipedia nutzte voarmols die GNU-Lizenz für freie Dokumentation (GFDL).\nDie GFDL ist en gültiche Lizenz, wo awer schwear zu verstehn ist.\nEs ist zudem schwierich gemäss die Lizenz lizenziert Inhalte wiederzuverwenne.",
"config-email-settings": "E-Mail-Instellunge",
"config-enable-email": "Ausgehende E-Mails ermöchliche",
"config-enable-email-help": "Soweit die E-Mail-Funktione benutzt sin solle, müsse die entsprechende [http://www.php.net/manual/en/mail.configuration.php PHP-E-Mail-Einstellungen] richtich konfiguriert sin.\nFür den Fall, dass die E-Mail-Funktione net benöticht sin, könne die dohier deaktiviert sin.",
diff --git a/www/wiki/includes/installer/i18n/hsb.json b/www/wiki/includes/installer/i18n/hsb.json
index e875961a..1544d210 100644
--- a/www/wiki/includes/installer/i18n/hsb.json
+++ b/www/wiki/includes/installer/i18n/hsb.json
@@ -46,16 +46,15 @@
"config-env-good": "Wokolina je so skontrolowała.\nMóžeš MediaWiki instalować.",
"config-env-bad": "Wokolina je so skontrolowała.\nNjemóžeš MediaWiki instalować.",
"config-env-php": "PHP $1 je instalowany.",
- "config-unicode-using-intl": "Za normalizaciju Unicode so [http://pecl.php.net/intl PECL-rozšěrjenje intl] wužiwa.",
+ "config-unicode-using-intl": "Za normalizaciju Unicode so [https://pecl.php.net/intl PECL-rozšěrjenje intl] wužiwa.",
"config-no-db": "Njeda so přihódny ćěrjak datoweje banki namakać! Dyrbiš ćěrjak datoweje banki za PHP instalować.\nSlědowace typy datoweje banki so podpěruja: $1.\n\nJeli sy PHP sam kompilował, konfiguruj jón znowa z aktiwizowanym programom datoweje banki, na přikład z pomocu <code>./configure --with-mysqli</code>.\nJeli sy PHP z Debianoweho abo Ubuntuoweho paketa instalował, dyrbiš tež paket <code>php5-mysql</code> instalować.",
"config-outdated-sqlite": "'''Warnowanje''': maš SQLite $1, kotryž je starši hač minimalna trěbna wersija $2. SQLite njebudźe k dispoziciji stać.",
"config-no-fts3": "'''Warnowanje''': SQLite je so bjez [//sqlite.org/fts3.html FTS3-modula] kompilował, pytanske funkcije njebudu k dispoziciji stać.",
"config-pcre-no-utf8": "'''Ćežki zmylk''': Zda so, zo PCRE-modul za PHP ma so bjez PCRE_UTF8-podpěry kompilować.\nMediaWiki trjeba UTF-8-podpěru, zo by korektnje fungował.",
"config-memory-raised": "PHP-parameter <code>memory_limit</code> je $1, je so na hódnotu $2 zwyšił.",
"config-memory-bad": "'''Warnowanje:''' PHP-parameter <code>memory_limit</code> ma hódnotu $1,\nTo je najskerje přeniske.\nInstalacija móhła so njeporadźić!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] je instalowany",
"config-apc": "[http://www.php.net/apc APC] je instalowany",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] je instalowany",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] je instalowany",
"config-diff3-bad": "GNU diff3 njenamakany.",
"config-no-uri": "'''Zmylk:''' Aktualny URI njeda so postajić.\nInstalacija bu přetorhnjena.",
"config-no-cli-uri": "'''Warnowanje''': Žana skriptowa šćežka (<code>--scriptpath</code>) podata, standard so wužiwa: <code>$1</code>.",
diff --git a/www/wiki/includes/installer/i18n/hu-formal.json b/www/wiki/includes/installer/i18n/hu-formal.json
index 882a084d..f6c1663e 100644
--- a/www/wiki/includes/installer/i18n/hu-formal.json
+++ b/www/wiki/includes/installer/i18n/hu-formal.json
@@ -14,7 +14,7 @@
"config-page-welcome": "Üdvözli a MediaWiki!",
"config-help-restart": "Szeretné törölni az eddig megadott összes adatot és újraindítani a telepítési folyamatot?",
"config-welcome": "=== Környezet ellenőrzése ===\nAlapvető ellenőrzés, ami megmondja, hogy a környezet alkalmas-e a MediaWiki számára.\nHa probléma merülne fel a telepítés során, meg kell adnia mások számára az alább megjelenő információkat.",
- "config-unicode-pure-php-warning": "<strong>Figyelmeztetés:</strong> Az [http://pecl.php.net/intl intl PECL kiterjesztés] nem érhető el Unicode normalizáláshoz, helyette a lassú, PHP alapú implementáció lesz használva.\nHa nagy látogatottságú oldalt üzemeltet, itt találhat információkat [http://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations a témáról].",
+ "config-unicode-pure-php-warning": "<strong>Figyelmeztetés:</strong> Az [https://pecl.php.net/intl intl PECL kiterjesztés] nem érhető el Unicode normalizáláshoz, helyette a lassú, PHP alapú implementáció lesz használva.\nHa nagy látogatottságú oldalt üzemeltet, itt találhat információkat [http://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations a témáról].",
"config-imagemagick": "Az ImageMagick megtalálható a rendszeren: <code>$1</code>.\nA bélyegképek készítése engedélyezve lesz, ha engedélyezi a feltöltéseket.",
"config-db-name-help": "Válassza ki a wikije azonosítására használt nevet.\nNem tartalmazhat szóközt.\n\nHa megosztott webtárhelyet használ, a szolgáltatója vagy egy konkrét adatbázisnevet ad önnek használatra, vagy létrehozhat egyet a vezérlőpulton keresztül.",
"config-db-install-help": "Adja meg a felhasználónevet és jelszót, amivel a telepítő csatlakozhat az adatbázishoz.",
diff --git a/www/wiki/includes/installer/i18n/hu.json b/www/wiki/includes/installer/i18n/hu.json
index 759b82db..9f5e2862 100644
--- a/www/wiki/includes/installer/i18n/hu.json
+++ b/www/wiki/includes/installer/i18n/hu.json
@@ -11,14 +11,16 @@
"Macofe",
"Máté",
"Seb35",
- "Urbalazs"
+ "Urbalazs",
+ "MeskoBalazs",
+ "Bencemac"
]
},
"config-desc": "A MediaWiki telepítője",
"config-title": "A MediaWiki $1 telepítése",
"config-information": "Információ",
- "config-localsettings-upgrade": "Már létezik a <code>LocalSettings.php</code> fájl.\nA telepített szoftver frissítéséhez írd be az alábbi mezőbe a <code>$wgUpgradeKey</code> beállítás értékét, melyet a <code>LocalSettings.php</code> nevű fájlban találhatsz meg.",
- "config-localsettings-cli-upgrade": "A <code>LocalSettings.php</code> fájl megtalálható.\nA telepített rendszer frissítéséhez futtasd az <code>update.php</code>-t.",
+ "config-localsettings-upgrade": "Már létezik a <code>LocalSettings.php</code> fájl.\nA telepített szoftver frissítéséhez írd be az alábbi mezőbe a <code>$wgUpgradeKey</code> beállítás értékét, melyet a <code>LocalSettings.php</code>-ban találhatsz meg.",
+ "config-localsettings-cli-upgrade": "Már létezik a <code>LocalSettings.php</code> fájl.\nA telepített rendszer frissítéséhez futtasd az <code>update.php</code>-t.",
"config-localsettings-key": "Frissítési kulcs:",
"config-localsettings-badkey": "A megadott frissítési kulcs érvénytelen.",
"config-upgrade-key-missing": "A telepítő a MediaWiki meglévő példányát észlelte.\nA telepített rendszer frissítéséhez helyezd el az alábbi sort a <code>LocalSettings.php</code> végére:\n\n$1",
@@ -50,33 +52,32 @@
"config-page-existingwiki": "Létező wiki",
"config-help-restart": "Szeretnéd törölni az eddig megadott összes adatot és újraindítani a telepítési folyamatot?",
"config-restart": "Igen, újraindítás",
- "config-welcome": "=== A környezet ellenőrzése ===\nNéhány alapvető ellenőrzés kerül végrehajtásra, hogy kiderüljön ,hogy ez a környezet alkalmas-e a MediaWiki telepítésére.\nHa telepítéssel kapcsolatos segítségre van szükséged, add meg ezen ellenőrzések eredményét.",
- "config-copyright": "=== Licenc és feltételek ===\n\n$1\n\nEz a program szabad szoftver; terjeszthető illetve módosítható a Free Software Foundation által kiadott GNU General Public License dokumentumában leírtak; akár a licenc 2-es, akár (tetszőleges) későbbi változata szerint.\n\nEz a program abban a reményben kerül közreadásra, hogy hasznos lesz, de minden egyéb '''garancia nélkül''', az '''eladhatóságra''' vagy '''valamely célra való alkalmazhatóságra''' való származtatott garanciát is beleértve. További részleteket a GNU General Public License tartalmaz.\n\nA felhasználónak a programmal együtt meg kell kapnia a <doclink href=Copying>GNU General Public License egy példányát</doclink>; ha mégsem kapta meg, akkor írjon a Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. címre, vagy [http://www.gnu.org/copyleft/gpl.html tekintse meg online].",
+ "config-welcome": "=== A környezet ellenőrzése ===\nNéhány alapvető ellenőrzés hajtódik végre, hogy kiderüljön, hogy ez a környezet alkalmas-e a MediaWiki telepítésére.\nHa segítséget kérsz a telepítéssel kapcsolatban, add meg ezen ellenőrzések eredményét.",
+ "config-copyright": "=== Licenc és feltételek ===\n\n$1\n\nEz a program szabad szoftver; terjeszthető, illetve módosítható a Free Software Foundation által kiadott GNU General Public License dokumentumában leírtak; akár a licenc 2-es, akár (tetszőleges) későbbi változata szerint.\n\nEz a program abban a reményben kerül közreadásra, hogy hasznos lesz, de minden egyéb <strong>garancia nélkül</strong>, az <strong>eladhatóságra</strong> vagy <strong>valamely célra való alkalmazhatóságra</strong> való származtatott garanciát is beleértve. További részleteket a GNU General Public License tartalmaz.\n\nA felhasználónak a programmal együtt meg kell kapnia a <doclink href=Copying>GNU General Public License egy példányát</doclink>; ha mégsem kapta meg, akkor írjon a Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. címre, vagy [https://www.gnu.org/copyleft/gpl.html tekintse meg online].",
"config-sidebar": "* [https://www.mediawiki.org A MediaWiki honlapja]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Felhasználói kézikönyv]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Útmutató adminisztrátoroknak]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ GyIK]\n----\n* <doclink href=Readme>Ismertető</doclink>\n* <doclink href=ReleaseNotes>Kiadási megjegyzések</doclink>\n* <doclink href=Copying>Másolás</doclink>\n* <doclink href=UpgradeDoc>Frissítés</doclink>",
"config-env-good": "A környezet ellenőrzése befejeződött.\nA MediaWiki telepíthető.",
"config-env-bad": "A környezet ellenőrzése befejeződött.\nA MediaWiki nem telepíthető.",
"config-env-php": "A PHP verziója: $1",
"config-env-hhvm": "HHVM verziója: $1",
- "config-unicode-using-intl": "A rendszer Unicode normalizálására az [http://pecl.php.net/intl intl PECL kiterjesztést] használja.",
- "config-unicode-pure-php-warning": "<strong>Figyelmeztetés:</strong> A Unicode-normalizáláshoz szükséges [http://pecl.php.net/intl intl PECL kiterjesztés] nem érhető el, helyette a lassú, PHP-alapú implementáció lesz használatban.\nHa nagy látogatottságú oldalt üzemeltetsz, [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations itt] találhatsz további információkat a témáról.",
+ "config-unicode-using-intl": "A rendszer Unicode normalizálására az [https://pecl.php.net/intl intl PECL kiterjesztést] használja.",
+ "config-unicode-pure-php-warning": "<strong>Figyelmeztetés:</strong> A Unicode-normalizáláshoz szükséges [https://pecl.php.net/intl intl PECL kiterjesztés] nem érhető el, helyette a lassú, PHP-alapú implementáció lesz használatban.\nHa nagy látogatottságú oldalt üzemeltetsz, [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations itt] találhatsz további információkat a témáról.",
"config-unicode-update-warning": "<strong>Figyelmeztetés:</strong> A Unicode normalizáláshoz szükséges burkolókönyvtár [http://site.icu-project.org/ az ICU projekt] függvénykönyvtárának régebbi változatát használja.\nHa ügyelni kívánsz a Unicode használatára, fontold meg a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations frissítését].",
"config-no-db": "Nem sikerült egyetlen használható adatbázis-illesztőprogramot sem találni. Telepítened kell egyet a PHP-hez.\nA következő {{PLURAL:$2|adatbázistípus támogatott|adatbázistípusok támogatottak}}: $1.\n\nHa a PHP-t magad fordítottad, konfiguráld újra úgy, hogy engedélyezve legyen egy adatbáziskliens, pl. a <code>./configure --with-mysqli</code> parancs használatával.\nHa a PHP-t Debian vagy Ubuntu csomaggal telepítetted, akkor szükséged lesz például a <code>php5-mysql</code> csomagra is.",
"config-outdated-sqlite": "<strong>Figyelmeztetés:</strong> SQLite $1 verziód van, ami alacsonyabb a legalább szükséges $2 verziónál. Az SQLite nem lesz elérhető.",
"config-no-fts3": "<strong>Figyelmeztetés:</strong> Az SQLite [//sqlite.org/fts3.html FTS3 modul] nélkül lett fordítva, a keresési funkciók nem fognak működni ezen a rendszeren.",
- "config-pcre-old": "<strong>Kritikus hiba:</strong> PCRE $1 vagy későbbi szükséges.\nA Te PHP binárisod PCRE $2-vel lett linkelve.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE További információ].",
+ "config-pcre-old": "<strong>Kritikus hiba:</strong> PCRE $1 vagy későbbi szükséges.\nA te PHP binárisod a PCRE $2 verziójával van linkelve.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE További információ].",
"config-pcre-no-utf8": "<strong>Kritikus hiba:</strong> Úgy tűnik, hogy a PHP PRCE modulja PRCE_UTF8 támogatás nélkül lett fordítva.\nA MediaWikinek UTF-8-támogatásra van szüksége a helyes működéshez.",
"config-memory-raised": "A PHP <code>memory_limit</code> beállításának értéke: $1. Meg lett növelve a következő értékre: $2.",
"config-memory-bad": "<strong>Figyelmeztetés:</strong> A PHP <code>memory_limit</code> beállításának értéke $1.\nEz az érték valószínűleg túl kevés, a telepítés sikertelen lehet.",
- "config-xcache": "Az [http://xcache.lighttpd.net/ XCache] telepítve van",
"config-apc": "Az [http://www.php.net/apc APC] telepítve van",
"config-apcu": "Az [http://www.php.net/apcu APCu] telepítve van",
- "config-wincache": "A [http://www.iis.net/download/WinCacheForPhp WinCache] telepítve van",
- "config-no-cache-apcu": "<strong>Figyelmeztetés:</strong> nem találhatók a következők: [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] vagy [http://www.iis.net/download/WinCacheForPhp WinCache].\nAz objektum gyorsítótárazása nincs engedélyezve.",
+ "config-wincache": "A [https://www.iis.net/download/WinCacheForPhp WinCache] telepítve van",
+ "config-no-cache-apcu": "<strong>Figyelmeztetés:</strong> nem találhatók a következők: [http://www.php.net/apcu APCu] vagy [http://www.iis.net/download/WinCacheForPhp WinCache].\nAz objektum gyorsítótárazása nincs engedélyezve.",
"config-diff3-bad": "GNU diff3 nem található.",
"config-git": "Megtaláltam a Git verziókezelő szoftvert: <code>$1</code>.",
"config-git-bad": "A Git verziókezelő rendszer nem található.",
"config-imagemagick": "Az ImageMagick megtalálható a rendszeren: <code>$1</code>.\nA bélyegképek készítése engedélyezve lesz a feltöltések engedélyezése esetén.",
- "config-gd": "A GD grafikai könyvtár elérhető.\nBélyegképek készítése működni fog, miután engedélyezted a fájlfeltöltést.",
+ "config-gd": "A GD grafikai könyvtár elérhető.\nA bélyegképek készítése engedélyezve lesz a feltöltések engedélyezése esetén.",
"config-no-scaling": "Nem található a GD könyvtár és az ImageMagick.\nA bélyegképek készítése le lesz tiltva.",
"config-no-uri": "<strong>Hiba:</strong> Nem sikerült megállapítani a jelenlegi URI-t.\nA telepítés megszakítva.",
"config-no-cli-uri": "<strong>Figyelmeztetés:</strong> Nincs <code>--scriptpath</code> megadva, használom az alapértelmezettet: <code>$1</code>.",
@@ -91,14 +92,14 @@
"config-db-host-oracle": "Adatbázis TNS:",
"config-db-wiki-settings": "A wiki azonosítása",
"config-db-name": "Adatbázisnév:",
- "config-db-name-help": "Válassz egy nevet a wiki azonosítására.\nNe tartalmazzon szóközt.\n\nHa megosztott webtárhelyet használsz, a szolgáltatód vagy egy konkrét adatbázisnevet ad neked használatra, vagy te magad hozhatsz létre adatbázisokat a vezérlőpulton keresztül.",
+ "config-db-name-help": "Válassz egy nevet a wiki azonosítására.\nNe tartalmazzon szóközt.\n\nHa megosztott webtárhelyet használsz, a szolgáltatód vagy megadja a használandó adatbázisnevet, vagy te magad hozhatsz létre adatbázisokat egy vezérlőpulton keresztül.",
"config-db-name-oracle": "Adatbázisséma:",
"config-db-account-oracle-warn": "Oracle adatbázisba való telepítésnek három támogatott módja van:\n\nHa a telepítési folyamat során adatbázisfiókot szeretnél létrehozni, akkor egy olyan fiókot kell használnod, mely rendelkezik SYSDBA jogosultsággal, majd meg kell adnod a létrehozandó, webes hozzáféréshez használt fiók adatait. Emellett a fiók kézzel is létrehozható, ekkor ennek az adatait kell megadni (a fióknak rendelkeznie kell megfelelő jogosul adatbázis-objektumok létrehozásához), vagy megadhatsz két fiókot: egyet a létrehozáshoz szükséges jogosultságokkal, és egy korlátozottat a webes hozzáféréshez.\n\nA megfelelő jogosultságokkal rendelkező fiók létrehozásához használható szkript a szoftver „maintenance/oracle/” könyvtárában található. Ne feledd, hogy korlátozott fiók használatakor az alapértelmezett fiókkal nem végezhetőek el a karbantartási műveletek.",
"config-db-install-account": "A telepítéshez használt felhasználói fiók adatai",
- "config-db-username": "Felhasználónév:",
- "config-db-password": "Jelszó:",
- "config-db-install-username": "Írd be az adatbázisrendszerhez való csatlakozáshoz használt felhasználónevet.\nEz nem a MediaWiki fiók felhasználóneve; ez az adatbázisrendszeren használt felhasználóneved.",
- "config-db-install-password": "Írd be az adatbázisrendszerhez való csatlakozáshoz használt jelszót.\nEz nem a MediaWiki-fiók jelszava; ez az adatbázisrendszeren használt jelszavad.",
+ "config-db-username": "Adatbázis-felhasználónév:",
+ "config-db-password": "Adatbázisjelszó:",
+ "config-db-install-username": "Írd be a telepítés alatt az adatbázisrendszerhez való csatlakozáshoz használt felhasználónevet.\nEz nem a MediaWiki-fiók felhasználóneve; ez az adatbázisrendszeren használt felhasználóneved.",
+ "config-db-install-password": "Írd be a telepítés alatt az adatbázisrendszerhez való csatlakozáshoz használt jelszót.\nEz nem a MediaWiki-fiók jelszava; ez az adatbázisrendszeren használt jelszavad.",
"config-db-install-help": "Add meg a felhasználónevet és jelszót, amivel a telepítő csatlakozhat az adatbázishoz.",
"config-db-account-lock": "Általános működés során is ezen információk használata",
"config-db-wiki-account": "Általános működéshez használt felhasználói adatok",
@@ -192,7 +193,7 @@
"config-admin-name-invalid": "A megadott felhasználónév (<nowiki>$1</nowiki>) érvénytelen.\nAdj meg egy másik felhasználónevet.",
"config-admin-password-blank": "Add meg az adminisztrátori fiók jelszavát!",
"config-admin-password-mismatch": "A megadott jelszavak nem egyeznek.",
- "config-admin-email": "E-mail cím:",
+ "config-admin-email": "E-mail-cím:",
"config-admin-email-help": "Add meg az e-mail címedet, hogy más felhasználók küldhessenek e-maileket a wikin keresztül, új jelszót tudj kérni, és értesülhess a figyelőlistádon lévő lapokon történt változásokról. Üresen is hagyhatod ezt a mezőt.",
"config-admin-error-user": "Belső hiba történt a(z) „<nowiki>$1</nowiki>” nevű adminisztrátor létrehozásakor.",
"config-admin-error-password": "Belső hiba történt a(z) „<nowiki>$1</nowiki>” nevű adminisztrátor jelszavának beállításakor: <pre>$2</pre>",
@@ -211,14 +212,14 @@
"config-profile-help": "A wikik akkor működnek a legjobban, ha minél több felhasználó számára engedélyezett a szerkesztés.\nA MediaWikiben könnyű ellenőrizni a legutóbbi változtatásokat,és visszaállítani a naiv vagy káros felhasználók által okozott károkat.\n\nA MediaWiki azonban számos helyzetben hasznos lehet, és néha nem könnyű mindenkit meggyőzni a wiki előnyeiről.\nVálaszthatsz!\n\n<strong>{{int:config-profile-wiki}}kben</strong> bárki szerkeszthet, akár bejelentkezés nélkül is. A <strong>{{int:config-profile-no-anon}}</strong> beállítás további biztonságot nyújt, azonban elijesztheti az alkalmi szerkesztőket.\n\nLehetőség van arra is, hogy <strong>{{lc:{{int:config-profile-fishbowl}}}}</strong> módosíthassák a lapokat, de a nyilvánosság ekkor megtekintheti a lapokat és azok laptörténetét is. <strong>{{int:config-profile-private}}</strong> esetén csak az engedélyezett szerkesztők tekinthetik meg a lapokat, és ugyanez a csoport szerkeszthet.\n\nTelepítés után jóval összetettebb jogosultságrendszer állítható össze, további információ a [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights kézikönyv kapcsolódó bejegyzésében].",
"config-license": "Szerzői jog és licenc:",
"config-license-none": "Nincs licencjelzés",
- "config-license-cc-by-sa": "Creative Commons Nevezd meg! - Így add tovább!",
+ "config-license-cc-by-sa": "Creative Commons Nevezd meg! – Így add tovább!",
"config-license-cc-by": "Creative Commons Nevezd meg!",
"config-license-cc-by-nc-sa": "Creative Commons Nevezd meg! - Ne add el! - Így add tovább!",
"config-license-cc-0": "Creative Commons Zero (közkincs)",
"config-license-gfdl": "GNU Szabad Dokumentációs Licenc 1.3 vagy újabb",
"config-license-pd": "Közkincs",
"config-license-cc-choose": "Creative Commons-licenc választása",
- "config-license-help": "A legtöbb wiki valamilyen [http://freedomdefined.org/Definition szabad licenc] alatt teszi közzé a szerkesztéseit.\nEz erősíti a közösségi tulajdon érzését, és elősegíti a hosszú távú közreműködők megjelenését.\nÁltalában nem szükséges magán- vagy vállalati wiki esetén.\n\nHa a Wikipédiáról szeretnél szövegeket másolni, és azt szeretnéd, hogy a Wikipédián felhasználhassák a wikidben található szöveget, akkor a <strong>{{int:config-license-cc-by-sa}}</strong> lehetőséget válaszd.\n\nA Wikipédia korábban a GNU Szabad Dokumentációs Licencet használta.\nEz a licenc még ma is használható, azonban nem könnyű megérteni,\ntovábbá a GFDL alatt közzétett tartalom újrafelhasználása nehézkes.",
+ "config-license-help": "A legtöbb wiki valamilyen [https://freedomdefined.org/Definition szabad licenc] alatt teszi közzé a szerkesztéseit.\nEz erősíti a közösségi tulajdon érzését, és elősegíti a hosszú távú közreműködők megjelenését.\nÁltalában nem szükséges magán- vagy vállalati wiki esetén.\n\nHa a Wikipédiáról szeretnél szövegeket másolni, és azt szeretnéd, hogy a Wikipédián felhasználhassák a wikidben található szöveget, akkor a <strong>{{int:config-license-cc-by-sa}}</strong> lehetőséget válaszd.\n\nA Wikipédia korábban a GNU Szabad Dokumentációs Licencet használta.\nEz a licenc még ma is használható, azonban nem könnyű megérteni,\ntovábbá a GFDL alatt közzétett tartalom újrafelhasználása nehézkes.",
"config-email-settings": "E-mail beállítások",
"config-enable-email": "Kimenő e-mailek engedélyezése",
"config-enable-email-help": "E-mailek küldéséhez [http://www.php.net/manual/en/mail.configuration.php a PHP mail beállításait] megfelelően meg kell adni.\nHa nem akarsz semmilyen e-mailes funkciót használni, itt tilthatod le őket.",
@@ -248,7 +249,7 @@
"config-cache-options": "Objektum-gyorsítótárazás beállításai:",
"config-cache-help": "Az objektumgyorsítótárazás célja, hogy felgyorsítsa a MediaWiki működését a gyakran használt adatok gyorsítótárazásával.\nKözepes vagy nagyobb oldalak esetén erősen ajánlott a használata, de kisebb oldalak esetén is hasznos lehet.",
"config-cache-none": "Nincs gyorsítótárazás (minden funkció működik, de nagyobb wiki esetében lassabb működést eredményezhet)",
- "config-cache-accel": "PHP-objektumok gyorsítótárazása (APC, APCu, XCache vagy WinCache)",
+ "config-cache-accel": "PHP-objektumok gyorsítótárazása (APC, APCu vagy WinCache)",
"config-cache-memcached": "Memcached használata (további telepítés és konfigurálás szükséges)",
"config-memcached-servers": "Memcached-szerverek:",
"config-memcached-help": "Azon IP-címek listája, melyeket a Memcached használhat.\nVesszővel kell elválasztani őket, és meg kell adni a portot is. Például:\n 127.0.0.1:11211\n 192.168.1.25:11211",
@@ -296,14 +297,18 @@
"config-install-subscribe-fail": "Nem sikerült feliratkozni a mediawiki-announce levelezőlistára: $1",
"config-install-subscribe-notpossible": "A cURL nincs telepítve és az <code>allow_url_fopen</code> nem érhető el.",
"config-install-mainpage": "Kezdőlap létrehozása az alapértelmezett tartalommal",
+ "config-install-mainpage-exists": "A főoldal már létezik, kihagyás",
"config-install-extension-tables": "Táblák létrehozása az engedélyezett kiterjesztésekhez",
"config-install-mainpage-failed": "Nemsikerült létrehozni a kezdőlapot: $1",
"config-install-done": "<strong>Gratulálunk!</strong>\nA MediaWiki telepítése sikeresen befejeződött.\n\nA telepítő elkészítette a <code>LocalSettings.php</code> fájlt, amely tartalmazza az összes beállítást.\n\nEzt le kell tölteni, majd elhelyezni a wiki telepítési könyvtárába (az a könyvtár, ahol az index.php is található).\n\nA letöltés automatikusan elindul. Ha mégsem indulna el, vagy megszakítottad, az alábbi linkre kattintva újra letöltheted:\n\n$3\n\n<strong>Megjegyzés:</strong> Ha ezt most nem teszed meg, és kilépsz a telepítésből, az elkészített konfigurációs fájlt nem tudod elérni a későbbiekben.\n\nHa végeztél a fájl elhelyezésével, <strong>[$2 beléphetsz a wikibe]</strong>.",
+ "config-install-done-path": "<strong>Gratulálunk!</strong>\nA MediaWiki telepítése sikeresen befejeződött.\n\nA telepítő elkészítette a <code>LocalSettings.php</code> fájlt, amely tartalmazza az összes beállítást.\n\nEzt le kell tölteni, majd elhelyezni ide: <code>$4</code>. A letöltés automatikusan el kell elinduljon.\n\nHa mégsem indulna el, vagy megszakítottad, az alábbi linkre kattintva újra letöltheted:\n\n$3\n\n<strong>Megjegyzés:</strong> Ha ezt most nem teszed meg, és kilépsz a telepítésből, az elkészített konfigurációs fájlt nem tudod elérni a későbbiekben.\n\nHa végeztél a fájl elhelyezésével, <strong>[$2 beléphetsz a wikibe]</strong>.",
"config-download-localsettings": "<code>LocalSettings.php</code> letöltése",
"config-help": "segítség",
"config-help-tooltip": "kattints a kibontáshoz",
"config-nofile": "\"$1\" fájl nem található. Törölve lett?",
"config-extension-link": "Tudtad, hogy a wikid támogat [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions kiterjesztéseket]?\n\nBöngészhetsz [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category kiterjesztéseket kategóriánként] vagy válogathatsz a [https://www.mediawiki.org/wiki/Extension_Matrix kiterjesztésmátrixból] az összes kiterjesztés áttekintéséhez.",
+ "config-skins-screenshots": "$1 (képernyőképek: $2)",
+ "config-screenshot": "képernyőkép",
"mainpagetext": "<strong>A MediaWiki telepítése sikeresen befejeződött.</strong>",
"mainpagedocfooter": "Ha segítségre van szükséged a wikiszoftver használatához, akkor keresd fel a [https://meta.wikimedia.org/wiki/Help:Contents User's Guide] oldalt.\n\n== Alapok (angol nyelven) ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Beállítások listája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki GyIK]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-kiadások levelezőlistája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources A MediaWiki fordítása a saját nyelvedre]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Tudd meg többet, hogyan küzdhetsz a kéretlen levelek ellen a wikiden]"
}
diff --git a/www/wiki/includes/installer/i18n/ia.json b/www/wiki/includes/installer/i18n/ia.json
index 13e7a6b6..a7ede0ae 100644
--- a/www/wiki/includes/installer/i18n/ia.json
+++ b/www/wiki/includes/installer/i18n/ia.json
@@ -3,7 +3,8 @@
"authors": [
"McDutchie",
"아라",
- "Macofe"
+ "Macofe",
+ "Fanjiayi"
]
},
"config-desc": "Le installator de MediaWiki",
@@ -43,14 +44,14 @@
"config-help-restart": "Vole tu rader tote le datos salveguardate que tu ha entrate e reinitiar le processo de installation?",
"config-restart": "Si, reinitia lo",
"config-welcome": "=== Verificationes del ambiente ===\nVerificationes de base essera ora exequite pro determinar si iste ambiente es apte pro le installation de MediaWiki.\nNon oblida de includer iste information si tu cerca adjuta pro completar le installation.",
- "config-copyright": "=== Copyright and Terms ===\n\n$1\n\nIste programma es software libere; vos pote redistribuer lo e/o modificar lo sub le conditiones del Licentia Public General de GNU publicate per le Free Software Foundation; version 2 del Licentia, o (a vostre option) qualcunque version posterior.\n\nIste programma es distribuite in le sperantia que illo sia utile, ma '''sin garantia''', sin mesmo le implicite garantia de '''commercialisation''' o '''aptitude pro un proposito particular'''.\nVide le Licentia Public General de GNU pro plus detalios.\n\nVos deberea haber recipite <doclink href=Copying>un exemplar del Licentia Public General de GNU</doclink> con iste programma; si non, scribe al Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, o [http://www.gnu.org/copyleft/gpl.html lege lo in linea].",
+ "config-copyright": "=== Copyright and Terms ===\n\n$1\n\nIste programma es software libere; vos pote redistribuer lo e/o modificar lo sub le conditiones del Licentia Public General de GNU publicate per le Free Software Foundation; version 2 del Licentia, o (a vostre option) qualcunque version posterior.\n\nIste programma es distribuite in le sperantia que illo sia utile, ma '''sin garantia''', sin mesmo le implicite garantia de '''commercialisation''' o '''aptitude pro un proposito particular'''.\nVide le Licentia Public General de GNU pro plus detalios.\n\nVos deberea haber recipite <doclink href=Copying>un exemplar del Licentia Public General de GNU</doclink> con iste programma; si non, scribe al Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, o [https://www.gnu.org/copyleft/gpl.html lege lo in linea].",
"config-sidebar": "* [https://www.mediawiki.org Pagina principal de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guida pro usatores]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guida pro administratores]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Lege me</doclink>\n* <doclink href=ReleaseNotes>Notas de iste version</doclink>\n* <doclink href=Copying>Conditiones de copia</doclink>\n* <doclink href=UpgradeDoc>Actualisation</doclink>",
"config-env-good": "Le ambiente ha essite verificate.\nTu pote installar MediaWiki.",
"config-env-bad": "Le ambiente ha essite verificate.\nTu non pote installar MediaWiki.",
"config-env-php": "PHP $1 es installate.",
"config-env-hhvm": "HHVM $1 es installate.",
- "config-unicode-using-intl": "Le [http://pecl.php.net/intl extension PECL intl] es usate pro le normalisation Unicode.",
- "config-unicode-pure-php-warning": "'''Aviso''': Le [http://pecl.php.net/intl extension PECL intl] non es disponibile pro exequer le normalisation Unicode; le systema recurre al implementation lente in PHP pur.\nSi tu sito ha un alte volumine de traffico, tu deberea informar te un poco super le [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisation Unicode].",
+ "config-unicode-using-intl": "Le [https://pecl.php.net/intl extension PECL intl] es usate pro le normalisation Unicode.",
+ "config-unicode-pure-php-warning": "'''Aviso''': Le [https://pecl.php.net/intl extension PECL intl] non es disponibile pro exequer le normalisation Unicode; le systema recurre al implementation lente in PHP pur.\nSi tu sito ha un alte volumine de traffico, tu deberea informar te un poco super le [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisation Unicode].",
"config-unicode-update-warning": "'''Aviso''': Le version installate del bibliotheca inveloppante pro normalisation Unicode usa un version ancian del bibliotheca del [http://site.icu-project.org/ projecto ICU].\nTu deberea [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations actualisar lo] si le uso de Unicode importa a te.",
"config-no-db": "Non poteva trovar un driver appropriate pro le base de datos! Es necessari installar un driver de base de datos pro PHP.\nLe sequente {{PLURAL:$2|typo|typos}} de base de datos es supportate: $1.\n\nSi tu compilava PHP tu mesme, reconfigura lo con un cliente de base de datos activate, per exemplo, usante <code>./configure --with-mysqli</code>.\nSi tu installava PHP ex un pacchetto Debian o Ubuntu, tu debe etiam installar, per exemplo, le modulo <code>php5-mysql</code>.",
"config-outdated-sqlite": "'''Attention''': tu ha SQLite $1, que es inferior al version minimal requirite, $2. SQLite essera indisponibile.",
@@ -59,12 +60,11 @@
"config-pcre-no-utf8": "'''Fatal''': Le modulo PCRE de PHP pare haber essite compilate sin supporto de PCRE_UTF8.\nMediaWiki require supporto de UTF-8 pro functionar correctemente.",
"config-memory-raised": "Le <code>memory_limit</code> de PHP es $1, elevate a $2.",
"config-memory-bad": "'''Aviso:''' Le <code>memory_limit</code> de PHP es $1.\nIsto es probabilemente troppo basse.\nLe installation pote faller!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] es installate",
"config-apc": "[http://www.php.net/apc APC] es installate",
"config-apcu": "[http://www.php.net/apcu APCu] es installate",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] es installate",
- "config-no-cache-apcu": "<strong>Attention:</strong> Impossibile trovar [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] o [http://www.iis.net/download/WinCacheForPhp WinCache].\nLe cache de objectos non es activate.",
- "config-mod-security": "'''Attention''': [http://modsecurity.org/ mod_security] es active in tu servitor web. Si mal configurate, isto pote causar problemas pro MediaWiki o altere software que permitte al usatores de publicar contento arbitrari.\nConsulta le [http://modsecurity.org/documentation/ documentation de mod_security] o contacta le servicio de adjuta de tu host si tu incontra estranie errores.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] es installate",
+ "config-no-cache-apcu": "<strong>Attention:</strong> Impossibile trovar [http://www.php.net/apcu APCu] o [http://www.iis.net/download/WinCacheForPhp WinCache].\nLe cache de objectos non es activate.",
+ "config-mod-security": "<strong>Attention</strong>: [https://modsecurity.org/ mod_security]/mod_security2 es active in tu servitor web. Multe configurationes commun de isto causa problemas pro MediaWiki o altere software que permitte al usatores de publicar contento arbitrari. Si possibile, isto deberea esser disactivate.\nAlteremente, consulta le [https://modsecurity.org/documentation/ documentation de mod_security] o contacta le servicio de adjuta de tu servitor si tu incontra estranie errores.",
"config-diff3-bad": "GNU diff3 non trovate.",
"config-git": "Systema de controlo de version Git trovate: <code>$1</code>",
"config-git-bad": "Systema de controlo de version Git non trovate.",
@@ -79,6 +79,7 @@
"config-no-cli-uploads-check": "'''Attention:''' Le directorio predefinite pro files incargate (<code>$1</code>) non es verificate contra le vulnerabilitate\nal execution arbitrari de scripts durante le installation de CLI.",
"config-brokenlibxml": "Vostre systema ha un combination de versiones de PHP e libxml2 que es defectuose e pote causar corruption celate de datos in MediaWiki e altere applicationes web.\nActualisa a libxml2 2.7.3 o plus recente ([https://bugs.php.net/bug.php?id=45996 problema reportate presso PHP]).\nInstallation abortate.",
"config-suhosin-max-value-length": "Suhosin es installate e limita parametro <code>length</code> de GET a $1 bytes.\nLe componente ResourceLoader de MediaWiki va contornar iste limite, ma isto prejudicara le rendimento.\nSi possibile, tu deberea mitter <code>suhosin.get.max_value_length</code> a 1024 o superior in <code>php.ini</code>, e mitter <code>$wgResourceLoaderMaxQueryLength</code> al mesme valor in <code>LocalSettings.php</code>.",
+ "config-using-32bit": "<strong>Attention:</strong> tu systema pare operar con integres de 32 bits. Isto [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit non es recommendate].",
"config-db-type": "Typo de base de datos:",
"config-db-host": "Servitor de base de datos:",
"config-db-host-help": "Si tu servitor de base de datos es in un altere servitor, entra hic le nomine o adresse IP del servitor.\n\nSi tu usa un servitor web usate in commun, tu providitor deberea dar te le correcte nomine de servitor in su documentation.\n\nSi tu face le installation in un servitor Windows e usa MySQL, le nomine \"localhost\" possibilemente non functiona como nomine de servitor. In tal caso, essaya \"127.0.0.1\", i.e. le adresse IP local.\n\nSi tu usa PostgreSQL, lassa iste campo vacue pro connecter via un \"socket\" de Unix.",
@@ -221,7 +222,7 @@
"config-license-gfdl": "Licentia GNU pro Documentation Libere 1.3 o plus recente",
"config-license-pd": "Dominio public",
"config-license-cc-choose": "Seliger un licentia Creative Commons personalisate",
- "config-license-help": "Multe wikis public pone tote le contributiones sub un [http://freedomdefined.org/Definition/Ia?uselang=ia licentia libere].\nIsto adjuta a crear un senso de proprietate communitari e incoragia le contribution in longe termino.\nIsto non es generalmente necessari pro un wiki private o de interprisa.\n\nSi tu vole poter usar texto de Wikipedia, e si tu vole que Wikipedia pote acceptar texto copiate de tu wiki, tu debe seliger <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia usava anteriormente le Licentia GNU pro Documentation Libere (GFDL).\nIste es un licentia valide, ma es difficile a comprender.\nIl es anque difficile reusar le contento licentiate sub GFDL.",
+ "config-license-help": "Multe wikis public pone tote le contributiones sub un [https://freedomdefined.org/Definition/Ia?uselang=ia licentia libere].\nIsto adjuta a crear un senso de proprietate communitari e incoragia le contribution in longe termino.\nIsto non es generalmente necessari pro un wiki private o de interprisa.\n\nSi tu vole poter usar texto de Wikipedia, e si tu vole que Wikipedia pote acceptar texto copiate de tu wiki, tu debe seliger <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia usava anteriormente le Licentia GNU pro Documentation Libere (GFDL).\nIste es un licentia valide, ma es difficile a comprender.\nIl es anque difficile reusar le contento licentiate sub GFDL.",
"config-email-settings": "Configuration de e-mail",
"config-enable-email": "Activar le e-mail sortiente",
"config-enable-email-help": "Si tu vole que e-mail functiona, [http://www.php.net/manual/en/mail.configuration.php le optiones de e-mail de PHP] debe esser configurate correctemente.\nSi tu non vole functiones de e-mail, tu pote disactivar los hic.",
@@ -251,7 +252,7 @@
"config-cache-options": "Configuration del cache de objectos:",
"config-cache-help": "Le cache de objectos es usate pro meliorar le rapiditate de MediaWiki per immagazinar le datos frequentemente usate.\nLe sitos medie o grande es multo incoragiate de activar isto, ma anque le sitos parve percipera le beneficios.",
"config-cache-none": "Nulle cache (nulle functionalitate es removite, ma le rapiditate pote diminuer in grande sitos wiki)",
- "config-cache-accel": "Cache de objectos PHP (APC, APCu, XCache o WinCache)",
+ "config-cache-accel": "Cache de objectos PHP (APC, APCu o WinCache)",
"config-cache-memcached": "Usar Memcached (require additional installation e configuration)",
"config-memcached-servers": "Servitores Memcached:",
"config-memcached-help": "Lista de adresses IP a usar pro Memcached.\nDebe specificar un per linea e specificar le porto a usar. Per exemplo:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -307,11 +308,14 @@
"config-install-mainpage-failed": "Non poteva inserer le pagina principal: $1",
"config-install-done": "<strong>Felicitationes!</strong>\nTu ha installate MediaWiki.\n\nLe installator ha generate un file <code>LocalSettings.php</code>.\nIste contine tote le configuration.\n\nEs necessari discargar lo e poner lo in le base del installation wiki (le mesme directorio que index.php).\nLe discargamento debe haber comenciate automaticamente.\n\nSi le discargamento non ha comenciate, o si illo esseva cancellate, recomencia le discargamento con un clic sur le ligamine sequente:\n\n$3\n\n<strong>Nota:</strong> Si tu non discarga iste file de configuration ora, illo non essera disponibile plus tarde.\n\nPost facer isto, tu pote <strong>[$2 entrar in tu wiki]</strong>.",
"config-install-done-path": "<strong>Felicitationes!</strong>\nTu ha installate MediaWiki.\n\nLe installator ha generate un file <code>LocalSettings.php</code>.\nIste contine tote le configuration.\n\nEs necessari discargar lo e poner lo in <code>$4</code>.\nLe discargamento debe haber comenciate automaticamente.\n\nSi le discargamento non ha comenciate, o si illo esseva cancellate, recomencia le discargamento con un clic sur le ligamine sequente:\n\n$3\n\n<strong>Nota:</strong> Si tu non discarga iste file de configuration ora, illo non essera disponibile plus tarde.\n\nPost facer isto, tu pote <strong>[$2 entrar in tu wiki]</strong>.",
+ "config-install-success": "MediaWiki ha essite installate con successo. Tu pote ora\nvisitar <$1$2> pro vider tu wiki.\nSi tu ha questiones, consulta nostre lista de questiones frequentemente ponite:\n<https://www.mediawiki.org/wiki/Manual:FAQ> o usa un del\nforos de supporto indicate sur ille pagina.",
"config-download-localsettings": "Discargar <code>LocalSettings.php</code>",
"config-help": "adjuta",
"config-help-tooltip": "clicca pro displicar",
"config-nofile": "Le file \"$1\" non poteva esser trovate. Ha illo essite delite?",
"config-extension-link": "Sapeva tu que tu wiki supporta [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensiones]?\n\nTu pote explorar le [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensiones per category] o le [https://www.mediawiki.org/wiki/Extension_Matrix matrice de extensiones] pro vider le lista complete de extensiones.",
+ "config-skins-screenshots": "$1 (capturas de schermo: $2)",
+ "config-screenshot": "captura de schermo",
"mainpagetext": "<strong>MediaWiki ha essite installate.</strong>",
"mainpagedocfooter": "Consulta le [https://meta.wikimedia.org/wiki/Help:Contents Guida del usator] pro information sur le uso del software wiki.\n\n== Pro initiar ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista de configurationes]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ a proposito de MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de diffusion pro annuncios de nove versiones de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Traducer MediaWiki in tu lingua]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Como combatter le spam in tu wiki]"
}
diff --git a/www/wiki/includes/installer/i18n/id.json b/www/wiki/includes/installer/i18n/id.json
index 37172585..6e1834f4 100644
--- a/www/wiki/includes/installer/i18n/id.json
+++ b/www/wiki/includes/installer/i18n/id.json
@@ -14,7 +14,8 @@
"WongKentir",
"Macofe",
"Rachmat.Wahidi",
- "Gombang"
+ "Gombang",
+ "Rachmat04"
]
},
"config-desc": "Penginstal untuk MediaWiki",
@@ -54,15 +55,15 @@
"config-help-restart": "Apakah Anda ingin menghapus semua data tersimpan yang telah Anda masukkan dan mengulang proses instalasi?",
"config-restart": "Ya, nyalakan ulang",
"config-welcome": "=== Pengecekan lingkungan ===\nPengecekan dasar kini akan dilakukan untuk melihat apakah lingkungan ini memadai untuk instalasi MediaWiki.\nIngatlah untuk menyertakan informasi ini jika Anda mencari bantuan tentang cara menyelesaikan instalasi.",
- "config-copyright": "=== Hak cipta dan persyaratan ===\n\n$1\n\nProgram ini adalah perangkat lunak bebas; Anda dapat mendistribusikan dan/atau memodifikasi di bawah persyaratan GNU General Public License seperti yang diterbitkan oleh Free Software Foundation; baik versi 2 lisensi, atau (sesuai pilihan Anda) versi yang lebih baru.\n\nProgram ini didistribusikan dengan harapan bahwa itu akan berguna, tetapi <strong>tanpa jaminan apa pun</strong>; bahkan tanpa jaminan tersirat untuk <strong>dapat diperjualbelikan</strong> atau <strong>sesuai untuk tujuan tertentu</strong>.\nLihat GNU General Public License untuk lebih jelasnya.\n\nAnda seharusnya telah menerima <doclink href=\"Copying\">salinan dari GNU General Public License</doclink> bersama dengan program ini; jika tidak, kirimkan surat untuk Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, atau [http://www.gnu.org/copyleft/gpl.html baca versi daring].",
+ "config-copyright": "=== Hak cipta dan persyaratan ===\n\n$1\n\nProgram ini adalah perangkat lunak bebas; Anda dapat mendistribusikan dan/atau memodifikasi di bawah persyaratan GNU General Public License seperti yang diterbitkan oleh Free Software Foundation; baik versi 2 lisensi, atau (sesuai pilihan Anda) versi yang lebih baru.\n\nProgram ini didistribusikan dengan harapan bahwa itu akan berguna, tetapi <strong>tanpa jaminan apa pun</strong>; bahkan tanpa jaminan tersirat untuk <strong>dapat diperjualbelikan</strong> atau <strong>sesuai untuk tujuan tertentu</strong>.\nLihat GNU General Public License untuk lebih jelasnya.\n\nAnda seharusnya telah menerima <doclink href=\"Copying\">salinan dari GNU General Public License</doclink> bersama dengan program ini; jika tidak, kirimkan surat untuk Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, atau [https://www.gnu.org/copyleft/gpl.html baca versi daring].",
"config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/id Situs MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/id Pedoman Pengguna]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/id Pedoman Administrator]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/id FAQ]\n----\n* <doclink href=Readme>Read me</doclink>\n* <doclink href=ReleaseNotes>Release notes</doclink>\n* <doclink href=Copying>Copying</doclink>\n* <doclink href=UpgradeDoc>Upgrading</doclink>",
"config-env-good": "Kondisi telah diperiksa.\nAnda dapat menginstal MediaWiki.",
"config-env-bad": "Kondisi telah diperiksa.\nAnda tidak dapat menginstal MediaWiki.",
"config-env-php": "PHP $1 diinstal.",
"config-env-hhvm": "HHVM $1 telah dipasang.",
- "config-unicode-using-intl": "Menggunakan [http://pecl.php.net/intl ekstensi PECL intl] untuk normalisasi Unicode.",
- "config-unicode-pure-php-warning": "'''Peringatan''': [http://pecl.php.net/intl Ekstensi intl PECL] untuk menangani normalisasi Unicode tidak tersedia, kembali menggunakan implementasi murni PHP yang lambat.\nJika Anda menjalankan situs berlalu lintas tinggi, Anda harus sedikit membaca [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisasi Unicode].",
- "config-unicode-update-warning": "'''Peringatan''': Versi terinstal dari pembungkus normalisasi Unicode menggunakan versi lama pustaka [http://site.icu-project.org/ proyek ICU].\nAnda harus [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations memutakhirkannya] jika Anda ingin menggunakan Unicode.",
+ "config-unicode-using-intl": "Menggunakan [https://pecl.php.net/intl ekstensi PECL intl] untuk normalisasi Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Peringatan:</strong> [https://pecl.php.net/intl intl Ekstensi PECL] tidak tersedia untuk menangani normalisasi Unicode, dikembalikan untuk melambatkan implementasi PHP asli.\nApabila Anda menjalankan situs dengan lalu-lintas tinggi, Anda harus membaca [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisasi Unicode].",
+ "config-unicode-update-warning": "<strong>Peringatan:</strong> Versi terinstal dari pembungkus normalisasi Unicode menggunakan versi lama pustaka [http://site.icu-project.org/ proyek ICU].\nAnda harus [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations meningkatkan versinya] jika ingin menggunakan Unicode.",
"config-no-db": "Pengandar basis data yang sesuai tidak ditemukan! Anda perlu menginstal pengandar basis data untuk PHP.\n{{PLURAL:$2|Jenis|Jenis}} basis data yang didukung: $1.\n\nJika Anda mengompilasi PHP sendiri, ubahlah konfigurasinya dengan mengaktifkan klien basis data, misalnya menggunakan <code>./configure --with-mysqli</code>.\nJika Anda menginstal PHP dari paket Debian atau Ubuntu, maka Anda juga perlu menginstal seperti paket <code>php5-mysql</code>.",
"config-outdated-sqlite": "<strong>Peringatan:</strong> Anda menggunakan SQLite $1, yang lebih rendah dari versi minimum yang diperlukan $2. SQLite akan tidak tersedia.",
"config-no-fts3": "'''Peringatan''': SQLite dikompilasi tanpa [//sqlite.org/fts3.html modul FTS3], fitur pencarian tidak akan tersedia pada konfigurasi ini.",
@@ -70,11 +71,10 @@
"config-pcre-no-utf8": "'''Fatal''': Modul PCRE PHP tampaknya dikompilasi tanpa dukungan PCRE_UTF8.\nMediaWiki memerlukan dukungan UTF-8 untuk berfungsi dengan benar.",
"config-memory-raised": "<code>memory_limit</code> PHP adalah $1, dinaikkan ke $2.",
"config-memory-bad": "'''Peringatan:''' <code>memory_limit</code> PHP adalah $1.\nIni terlalu rendah.\nInstalasi terancam gagal!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] telah diinstal",
"config-apc": "[http://www.php.net/apc APC] telah diinstal",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] telah diinstal",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] telah diinstal",
"config-no-cache-apcu": "<strong>Peringatan:</strong> Tidak dapat menemukan [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] atau [http://www.iis.net/download/WinCacheForPhp WinCache]. Singgahan obyek tidak diaktifkan.",
- "config-mod-security": "<strong>Peringatan:</strong> Server web Anda memiliki [http://modsecurity.org/ mod_security] yang diaktifkan. Jika salah dalam mengkonfigurasi, ini dapat menyebabkan masalah untuk MediaWiki atau perangkat lunak lain yang memungkinkan pengguna untuk mengirim sembarang konten.\nLihat [http://modsecurity.org/documentation/ dokumentasi mod_security] atau hubungi layanan host Anda jika Anda mengalami kesalahan acak.",
+ "config-mod-security": "<strong>Peringatan:</strong> Server web Anda memiliki [https://modsecurity.org/ mod_security] yang diaktifkan. Jika salah dalam mengkonfigurasi, ini dapat menyebabkan masalah untuk MediaWiki atau perangkat lunak lain yang memungkinkan pengguna untuk mengirim sembarang konten.\nLihat [https://modsecurity.org/documentation/ dokumentasi mod_security] atau hubungi layanan host Anda jika Anda mengalami kesalahan acak.",
"config-diff3-bad": "GNU diff3 tidak ditemukan.",
"config-git": "Menemukan perangkat lunak kontrol versi Git: <code>$1</code>.",
"config-git-bad": "Perangkat lunak kontrol versi Git tidak ditemukan.",
@@ -126,7 +126,7 @@
"config-type-mssql": "Microsoft SQL Server",
"config-support-info": "MediaWiki mendukung sistem basis data berikut:\n\n$1\n\nJika Anda tidak melihat sistem basis data yang Anda gunakan tercantum di bawah ini, ikuti petunjuk terkait di atas untuk mengaktifkan dukungan.",
"config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] adalah target utama MediaWiki dan memiliki dukungan terbaik. MediaWiki juga berjalan dengan [{{int:version-db-mariadb-url}} MariaDB] dan [{{int:version-db-percona-url}} Server Percona], yang kompatibel dengan MySQL. ([http://www.php.net/manual/en/mysql.installation.php Cara mengompilasi PHP dengan dukungan MySQL])",
- "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] adalah sistem basis data sumber terbuka populer sebagai alternatif untuk MySQL. Mungkin ada beberapa bug terbuka dan alternatif ini tidak direkomendasikan untuk dipakai dalam lingkungan produksi. ([http://www.php.net/manual/en/pgsql.installation.php cara mengompilasi PHP dengan dukungan PostgreSQL])",
+ "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] adalah sistem basis data sumber terbuka populer sebagai alternatif MySQL.([http://www.php.net/manual/en/pgsql.installation.php Bagaimana mengompilasikan PHP dengan dukungan PostgreSQL])",
"config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] adalah sistem basis data yang ringan yang sangat baik dukungannya. ([http://www.php.net/manual/en/pdo.installation.php cara mengompilasi PHP dengan dukungan SQLite], menggunakan PDO)",
"config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] adalah basis data komersial untuk perusahaan. ([http://www.php.net/manual/en/oci8.installation.php cara mengompilasi PHP dengan dukungan OCI8])",
"config-dbsupport-mssql": "[{{int:version-db-mssql-url}} Microsoft SQL Server] adalah database perusahaan komersial untuk Windows. ([http://www.php.net/manual/en/sqlsrv.installation.php Bagaimana cara mengkompilasi PHP dengan dukungan SQLSRV])",
@@ -230,7 +230,7 @@
"config-license-gfdl": "Lisensi Dokumentasi Bebas GNU 1.3 atau versi terbaru",
"config-license-pd": "Domain Umum",
"config-license-cc-choose": "Pilih lisensi Creative Commons kustom",
- "config-license-help": "Banyak wiki publik melisensikan semua kontribusi di bawah [http://freedomdefined.org/Definition lisensi bebas].\nHal ini membantu menciptakan rasa kepemilikan komunitas dan mendorong kontribusi jangka panjang.\nHal ini umumnya tidak diperlukan untuk wiki pribadi atau perusahaan.\n\nJika Anda ingin dapat menggunakan teks dari Wikipedia dan Anda ingin agar Wikipedia dapat menerima teks yang disalin dari wiki Anda, Anda harus memilih <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia sebelumnya menggunakan GNU Free Documentation License.\nLisensi ini masih sah, namun sulit dipahami.\nSelain itu, sulit untuk menggunakan ulang konten yang dilisensikan di bawah GFDL.",
+ "config-license-help": "Banyak wiki publik melisensikan semua kontribusi di bawah [https://freedomdefined.org/Definition lisensi bebas].\nHal ini membantu menciptakan rasa kepemilikan komunitas dan mendorong kontribusi jangka panjang.\nHal ini umumnya tidak diperlukan untuk wiki pribadi atau perusahaan.\n\nJika Anda ingin dapat menggunakan teks dari Wikipedia dan Anda ingin agar Wikipedia dapat menerima teks yang disalin dari wiki Anda, Anda harus memilih <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia sebelumnya menggunakan GNU Free Documentation License.\nLisensi ini masih sah, namun sulit dipahami.\nSelain itu, sulit untuk menggunakan ulang konten yang dilisensikan di bawah GFDL.",
"config-email-settings": "Pengaturan surel",
"config-enable-email": "Aktifkan surel keluar",
"config-enable-email-help": "Jika Anda ingin mengaktifkan surel, [http://www.php.net/manual/en/mail.configuration.php setelah surel PHP] perlu dikonfigurasi dengan benar.\nJika Anda tidak perlu fitur surel, Anda dapat menonaktifkannya di sini.",
@@ -260,7 +260,7 @@
"config-cache-options": "Pengaturan untuk penyinggahan objek:",
"config-cache-help": "Penyinggahan objek digunakan untuk meningkatkan kecepatan MediaWiki dengan menyinggahkan data yang sering digunakan.\nSitus berukuran sedang hingga besar sangat dianjurkan untuk mengaktifkan fitur ini, dan situs kecil juga akan merasakan manfaatnya.",
"config-cache-none": "Tidak ada penyinggahan (tidak ada fungsi yang dibuang, tetapi kecepatan dapat terpengaruh pada situs wiki yang besar)",
- "config-cache-accel": "Penyinggahan objek PHP (APC, XCache atau WinCache)",
+ "config-cache-accel": "Penyinggahan objek PHP (APC, APCu, XCache atau WinCache)",
"config-cache-memcached": "Gunakan Memcached (memerlukan setup dan konfigurasi tambahan)",
"config-memcached-servers": "Server Memcached:",
"config-memcached-help": "Daftar alamat IP yang digunakan untuk Memcached.\nHarus dispesifikasikan per baris berikut porta yang akan digunakan. Contoh:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -320,5 +320,5 @@
"config-nofile": "Berkas \"$1\" tidak dapat ditemukan. Mungkin sudah dihapus?",
"config-extension-link": "Tahukah Anda bahwa wiki Anda mendukung [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions ekstensi]?\n\nAnda dapat menjelajahi [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category ekstensi menurut kategori] atau [https://www.mediawiki.org/wiki/Extension_Matrix Ekstensi Matriks] untuk melihat daftar lengkap ekstensi.",
"mainpagetext": "<strong>MediaWiki telah terpasang dengan sukses.</strong>",
- "mainpagedocfooter": "Silakan baca [https://www.mediawiki.org/wiki/Help:Contents Panduan Pengguna] untuk cara penggunaan perangkat lunak wiki ini.\n\n== Memulai penggunaan ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings/id Daftar pengaturan konfigurasi]\n* [https://www.mediawiki.org/wiki/Manual:FAQ/id Daftar pertanyaan yang sering diajukan mengenai MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Milis rilis MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Terjemahkan MediaWiki ke bahasa Anda]"
+ "mainpagedocfooter": "Konsultasikan [https://www.mediawiki.org/wiki/Help:Contents Panduan Pengguna] untuk cara penggunaan perangkat lunak wiki ini.\n\n== Memulai ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Daftar pengaturan konfigurasi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Pertanyaan yang sering diajukan mengenai MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Milis rilis MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Pelokalan MediaWiki untuk bahasa Anda]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Belajar bagaimana menghadapi spam di wiki lokal]"
}
diff --git a/www/wiki/includes/installer/i18n/is.json b/www/wiki/includes/installer/i18n/is.json
index 37f243da..78be96be 100644
--- a/www/wiki/includes/installer/i18n/is.json
+++ b/www/wiki/includes/installer/i18n/is.json
@@ -31,12 +31,11 @@
"config-page-upgradedoc": "Uppfærsla",
"config-page-existingwiki": "Fyrirliggjandi wiki",
"config-restart": "Já, endurræsa",
- "config-copyright": "=== Höfundarréttur og skilmálar ===\n\n$1\n\nÞetta er frjáls hugbúnaður; þú mátt dreifa honum og/eða breyta samkvæmt skilmálum í almenna GNU GPL notkunarleyfinu eins og það er gefið út af Frjálsu hugbúnaðarstofnuninni; annaðhvort útgáfu 2 af GPL-leyfinu, eða (ef þér sýnist svo) einhverri nýrri útgáfu leyfisins.\n\nHugbúnaði þessum er dreift í þeirri von að hann geti verið gagnlegur, en <strong>ÁN ALLRAR ÁBYRGÐAR</strong>; einnig án þeirrar ábyrgðar sem gefin er í skyn með <strong>SELJANLEIKA</strong> eða <strong>EIGINLEIKUM TIL TILTEKINNA NOTA</strong>. Sjá almenna GNU GPL notkunarleyfið fyrir nánari upplýsingar.\n\nÞað ætti að hafa fylgt afrit af almenna <doclink href=Copying>GNU GPL notkunarleyfinu</doclink> með forritinu; ef ekki skrifið þá Fjálsu hugbúnarstofnuninni: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, eða [http://www.gnu.org/copyleft/gpl.html lestu það á netinu].",
+ "config-copyright": "=== Höfundarréttur og skilmálar ===\n\n$1\n\nÞetta er frjáls hugbúnaður; þú mátt dreifa honum og/eða breyta samkvæmt skilmálum í almenna GNU GPL notkunarleyfinu eins og það er gefið út af Frjálsu hugbúnaðarstofnuninni; annaðhvort útgáfu 2 af GPL-leyfinu, eða (ef þér sýnist svo) einhverri nýrri útgáfu leyfisins.\n\nHugbúnaði þessum er dreift í þeirri von að hann geti verið gagnlegur, en <strong>ÁN ALLRAR ÁBYRGÐAR</strong>; einnig án þeirrar ábyrgðar sem gefin er í skyn með <strong>SELJANLEIKA</strong> eða <strong>EIGINLEIKUM TIL TILTEKINNA NOTA</strong>. Sjá almenna GNU GPL notkunarleyfið fyrir nánari upplýsingar.\n\nÞað ætti að hafa fylgt afrit af almenna <doclink href=Copying>GNU GPL notkunarleyfinu</doclink> með forritinu; ef ekki skrifið þá Fjálsu hugbúnarstofnuninni: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, eða [https://www.gnu.org/copyleft/gpl.html lestu það á netinu].",
"config-env-php": "PHP $1 er uppsett.",
"config-env-hhvm": "HHVM $1 er uppsett.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] er uppsett",
"config-apc": "[http://www.php.net/apc APC] er uppsett",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] er uppsett",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] er uppsett",
"config-diff3-bad": "GNU diff3 fannst ekki.",
"config-using-server": "Nota \"<nowiki>$1</nowiki>\" sem heiti á þjóni.",
"config-using-uri": "Nota \"<nowiki>$1$2</nowiki>\" sem slóð á þjón.",
diff --git a/www/wiki/includes/installer/i18n/it.json b/www/wiki/includes/installer/i18n/it.json
index 5453e453..d9b0dbc0 100644
--- a/www/wiki/includes/installer/i18n/it.json
+++ b/www/wiki/includes/installer/i18n/it.json
@@ -60,14 +60,14 @@
"config-help-restart": "Vuoi cancellare tutti i dati salvati che hai inserito e riavviare il processo di installazione?",
"config-restart": "Sì, riavvia",
"config-welcome": "=== Controllo dell'ambiente ===\nSaranno eseguiti controlli di base per vedere se questo ambiente è adatto per l'installazione di MediaWiki.\nRicordati di includere queste informazioni se chiedi assistenza su come completare l'installazione.",
- "config-copyright": "=== Copyright e termini ===\n\n$1\n\nQuesto programma è un software libero; puoi redistribuirlo e/o modificarlo secondo i termini della GNU General Public License, come pubblicata dalla Free Software Foundation; o la versione 2 della Licenza o (a propria scelta) qualunque versione successiva.\n\nQuesto programma è distribuito nella speranza che sia utile, ma SENZA ALCUNA GARANZIA; senza neppure la garanzia implicita di NEGOZIABILITÀ o di APPLICABILITÀ PER UN PARTICOLARE SCOPO.\nSi veda la GNU General Public License per maggiori dettagli.\n\nQuesto programma deve essere distribuito assieme ad <doclink href=Copying>una copia della GNU General Public License</doclink>; in caso contrario, se ne può ottenere una scrivendo alla Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA oppure [http://www.gnu.org/copyleft/gpl.html leggerla in rete].",
+ "config-copyright": "=== Copyright e termini ===\n\n$1\n\nQuesto programma è un software libero; puoi redistribuirlo e/o modificarlo secondo i termini della GNU General Public License, come pubblicata dalla Free Software Foundation; o la versione 2 della Licenza o (a propria scelta) qualunque versione successiva.\n\nQuesto programma è distribuito nella speranza che sia utile, ma SENZA ALCUNA GARANZIA; senza neppure la garanzia implicita di NEGOZIABILITÀ o di APPLICABILITÀ PER UN PARTICOLARE SCOPO.\nSi veda la GNU General Public License per maggiori dettagli.\n\nQuesto programma deve essere distribuito assieme ad <doclink href=Copying>una copia della GNU General Public License</doclink>; in caso contrario, se ne può ottenere una scrivendo alla Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA oppure [https://www.gnu.org/copyleft/gpl.html leggerla in rete].",
"config-sidebar": "* [https://www.mediawiki.org Pagina principale MediaWiki]\n* [https://www.mediawiki.org/wiki/Aiuto:Guida ai contenuti per utenti]\n* [https://www.mediawiki.org/wiki/Manuale:Guida ai contenuti per admin]\n* [https://www.mediawiki.org/wiki/Manuale:FAQ FAQ]\n----\n* <doclink href=Readme>Leggimi</doclink>\n* <doclink href=ReleaseNotes>Note di versione</doclink>\n* <doclink href=Copying>Copie</doclink>\n* <doclink href=UpgradeDoc>Aggiornamenti</doclink>",
"config-env-good": "L'ambiente è stato controllato.\nÈ possibile installare MediaWiki.",
"config-env-bad": "L'ambiente è stato controllato.\nNon è possibile installare MediaWiki.",
"config-env-php": "PHP $1 è installato.",
"config-env-hhvm": "HHVM $1 è installato.",
- "config-unicode-using-intl": "Usa [http://pecl.php.net/intl l'estensione PECL intl] per la normalizzazione Unicode.",
- "config-unicode-pure-php-warning": "'''Attenzione:''' [http://pecl.php.net/intl l'estensione PECL intl] non è disponibile per gestire la normalizzazione Unicode, quindi si torna alla lenta implementazione in PHP puro.\nSe esegui un sito ad alto traffico, dovresti leggere alcune considerazioni sulla [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizzazione Unicode].",
+ "config-unicode-using-intl": "Usa [https://pecl.php.net/intl l'estensione PECL intl] per la normalizzazione Unicode.",
+ "config-unicode-pure-php-warning": "'''Attenzione:''' [https://pecl.php.net/intl l'estensione PECL intl] non è disponibile per gestire la normalizzazione Unicode, quindi si torna alla lenta implementazione in PHP puro.\nSe esegui un sito ad alto traffico, dovresti leggere alcune considerazioni sulla [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizzazione Unicode].",
"config-unicode-update-warning": "'''Attenzione:''' la versione installata del gestore per la normalizzazione Unicode usa una vecchia versione della libreria [http://site.icu-project.org/ del progetto ICU].\nDovresti [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations aggiornare] se vuoi usare l'Unicode.",
"config-no-db": "Impossibile trovare un driver adatto per il database! È necessario installare un driver per PHP.\n{{PLURAL:$2|Il seguente formato di database è supportato|I seguenti formati di database sono supportati}}: $1.\n\nSe compili PHP autonomamente, riconfiguralo attivando un client database, per esempio utilizzando <code>./configure --with-mysqli</code>.\nQualora avessi installato PHP per mezzo di un pacchetto Debian o Ubuntu, allora devi installare anche il pacchetto <code>php5-mysql</code>.",
"config-outdated-sqlite": "'''Attenzione''': è presente SQLite $1 mentre è richiesta la versione $2, SQLite non sarà disponibile.",
@@ -76,12 +76,11 @@
"config-pcre-no-utf8": "'''Errore''': Il modulo PCRE di PHP sembra essere stato compilato senza il supporto PCRE_UTF8, ma MediaWiki lo richiede per funzionare correttamente.",
"config-memory-raised": "Il valore <code>memory_limit</code> di PHP è $1, aumentato a $2.",
"config-memory-bad": "''Attenzione:''' Il valore di <code>memory_limit</code> di PHP è $1.\nProbabilmente è troppo basso.\nL'installazione potrebbe non riuscire!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] è installato",
"config-apc": "[http://www.php.net/apc APC] è installato",
"config-apcu": "[http://www.php.net/apc APC] è installato",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] è installato",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] è installato",
"config-no-cache-apcu": "'''Attenzione:''' [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] o [http://www.iis.net/download/WinCacheForPhp WinCache] non sono stati trovati.\nLa caching degli oggetti non è attivata.",
- "config-mod-security": "<strong>Attenzione:</strong> Il tuo server web ha il [http://modsecurity.org/ mod_security] abilitato. Se non correttamente configurato, può creare problemi a MediaWiki o ad altro software che permette agli utenti di pubblicare contenuto.\nFai riferimento alla [http://modsecurity.org/documentation/ documentazione sul mod_security] o contatta il supporto tecnico del tuo provider di hosting se si verificano errori.",
+ "config-mod-security": "<strong>Attenzione:</strong> Il tuo server web ha il [https://modsecurity.org/ mod_security] abilitato. Se non correttamente configurato, può creare problemi a MediaWiki o ad altro software che permette agli utenti di pubblicare contenuto.\nFai riferimento alla [https://modsecurity.org/documentation/ documentazione sul mod_security] o contatta il supporto tecnico del tuo provider di hosting se si verificano errori.",
"config-diff3-bad": "GNU diff3 non trovato.",
"config-git": "Trovato software di controllo della versione Git: <code>$1</code>.",
"config-git-bad": "Software di controllo della versione Git non trovato.",
@@ -235,7 +234,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 o versioni successive",
"config-license-pd": "Pubblico dominio",
"config-license-cc-choose": "Seleziona una delle licenze Creative Commons",
- "config-license-help": "Molti wiki pubblici rilasciano i loro contributi con una [http://freedomdefined.org/Definition licenza libera]. Questo aiuta a creare un senso di proprietà condivisa nella comunità e incoraggia a contribuire a lungo termine. Non è generalmente necessario per un wiki privato o aziendale.\n\nSe vuoi usare testi da Wikipedia, o desideri che Wikipedia possa essere in grado di accettare testi copiati dal tuo wiki, dovresti scegliere <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nIn precedenza Wikipedia ha utilizzato la GNU Free Documentation License. La GFDL è una licenza valida, ma è di difficile comprensione e complica il riutilizzo dei contenuti.",
+ "config-license-help": "Molti wiki pubblici rilasciano i loro contributi con una [https://freedomdefined.org/Definition licenza libera]. Questo aiuta a creare un senso di proprietà condivisa nella comunità e incoraggia a contribuire a lungo termine. Non è generalmente necessario per un wiki privato o aziendale.\n\nSe vuoi usare testi da Wikipedia, o desideri che Wikipedia possa essere in grado di accettare testi copiati dal tuo wiki, dovresti scegliere <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nIn precedenza Wikipedia ha utilizzato la GNU Free Documentation License. La GFDL è una licenza valida, ma è di difficile comprensione e complica il riutilizzo dei contenuti.",
"config-email-settings": "Impostazioni email",
"config-enable-email": "Abilita la posta elettronica in uscita",
"config-enable-email-help": "Se vuoi che funzionino le email, le [http://www.php.net/manual/en/mail.configuration.php PHP's impostazioni della posta] devono essere configurate correttamente.\nSe non si desidera alcuna funzionalità di posta elettronica, puoi disabilitarla qui.",
@@ -265,7 +264,7 @@
"config-cache-options": "Impostazioni per la cache di oggetti:",
"config-cache-help": "La memorizzazione di oggetti nella cache è utilizzata per migliorare la velocità di MediaWiki attraverso l'allocazione nella cache dei dati utilizzati di frequente.\nPer siti di dimensioni medie e grandi, è caldamente consigliato attivare la cache, ma anche per piccoli siti se ne vedranno i benefici.",
"config-cache-none": "Nessuna memorizzazione in cache (nessuna funzionalità viene impedita, ma sui siti wiki più grandi la velocità potrebbe risentirne)",
- "config-cache-accel": "Mettere in cache oggetti PHP (APC, APCu, XCache o WinCache)",
+ "config-cache-accel": "Mettere in cache oggetti PHP (APC, APCu o WinCache)",
"config-cache-memcached": "Usa Memcached (richiede ulteriori attività di installazione e configurazione)",
"config-memcached-servers": "Server di memcached:",
"config-memcached-help": "Elenco di indirizzi IP da utilizzare per Memcached.\nDovresti specificarne uno per riga e indicare la porta da utilizzare. Per esempio:\n 127.0.0.1:11211\n 192.168.1.25:1234",
diff --git a/www/wiki/includes/installer/i18n/ja.json b/www/wiki/includes/installer/i18n/ja.json
index ef286e36..c54dd81b 100644
--- a/www/wiki/includes/installer/i18n/ja.json
+++ b/www/wiki/includes/installer/i18n/ja.json
@@ -22,7 +22,8 @@
"Foresttttttt",
"ネイ",
"Suchichi02",
- "Omotecho"
+ "Omotecho",
+ "Yusuke1109"
]
},
"config-desc": "MediaWiki のインストーラー",
@@ -62,14 +63,14 @@
"config-help-restart": "入力した保存データをすべて消去して、インストール作業を再起動しますか?",
"config-restart": "はい、再起動します",
"config-welcome": "=== 環境の確認 ===\n基本的な確認では、現在の環境が MediaWiki のインストールに適しているかを確認します。\nインストール方法について助けが必要になった場合は、必ずこの確認結果を添えてください。",
- "config-copyright": "=== 著作権および規約 ===\n$1\n\nこの作品はフリーソフトウェアです。あなたは、フリーソフトウェア財団の発行する GNU 一般公衆利用許諾書 (GNU General Public License) (バージョン 2、またはそれ以降のライセンス) の規約に基づき、このライブラリを再配布および改変できます。\n\nこの作品は、有用であることを期待して配布されていますが、<strong>商用または特定の目的に適するかどうか</strong>も含めて、暗黙的にも、<strong>一切保証されません</strong>。\n詳しくは、 GNU 一般公衆利用許諾書をご覧ください。\n\nあなたはこのプログラムと共に、<doclink href=Copying>GNU 一般公衆利用許諾契約書の複製</doclink>を受け取ったはずです。受け取っていない場合は、フリーソフトウェア財団 (宛先は the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA) まで請求するか、または[http://www.gnu.org/copyleft/gpl.html オンラインでお読みください]。",
+ "config-copyright": "=== 著作権および規約 ===\n$1\n\nこの作品はフリーソフトウェアです。あなたは、フリーソフトウェア財団の発行する GNU 一般公衆利用許諾書 (GNU General Public License) (バージョン 2、またはそれ以降のライセンス) の規約に基づき、このライブラリを再配布および改変できます。\n\nこの作品は、有用であることを期待して配布されていますが、<strong>商用または特定の目的に適するかどうか</strong>も含めて、暗黙的にも、<strong>一切保証されません</strong>。\n詳しくは、 GNU 一般公衆利用許諾書をご覧ください。\n\nあなたはこのプログラムと共に、<doclink href=Copying>GNU 一般公衆利用許諾契約書の複製</doclink>を受け取ったはずです。受け取っていない場合は、フリーソフトウェア財団 (宛先は the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA) まで請求するか、または[https://www.gnu.org/copyleft/gpl.html オンラインでお読みください]。",
"config-sidebar": "* [https://www.mediawiki.org MediaWikiのホーム]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents 利用者向け案内]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents 管理者向け案内]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>お読みください</doclink>\n* <doclink href=ReleaseNotes>リリースノート</doclink>\n* <doclink href=Copying>コピー</doclink>\n* <doclink href=UpgradeDoc>アップグレード</doclink>",
"config-env-good": "環境を確認しました。\nMediaWiki をインストールできます。",
"config-env-bad": "環境を確認しました。\nMediaWiki のインストールはできません。",
"config-env-php": "PHP $1がインストールされています。",
"config-env-hhvm": "HHVM $1 がインストールされています。",
- "config-unicode-using-intl": "Unicode正規化に[http://pecl.php.net/intl intl PECL 拡張機能]を使用。",
- "config-unicode-pure-php-warning": "<strong>警告:</strong> Unicode 正規化の処理に [http://pecl.php.net/intl intl PECL 拡張機能]を利用できないため、処理が遅いピュア PHP の実装を代わりに使用しています。\n高トラフィックのサイトを運営する場合は、[https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode 正規化]をお読みください。",
+ "config-unicode-using-intl": "Unicode正規化に[https://pecl.php.net/intl intl PECL 拡張機能]を使用。",
+ "config-unicode-pure-php-warning": "<strong>警告:</strong> Unicode 正規化の処理に [https://pecl.php.net/intl intl PECL 拡張機能]を利用できないため、処理が遅いピュア PHP の実装を代わりに使用しています。\n高トラフィックのサイトを運営する場合は、[https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode 正規化]をお読みください。",
"config-unicode-update-warning": "<strong>警告:</strong> インストールされているバージョンの Unicode 正規化ラッパーは、[http://site.icu-project.org/ ICU プロジェクト]のライブラリの古いバージョンを使用しています。\nUnicode を少しでも利用する可能性がある場合は、[https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations アップグレード]してください。",
"config-no-db": "適切なデータベース ドライバーが見つかりませんでした! PHP にデータベース ドライバーをインストールする必要があります。\n以下の種類のデータベース{{PLURAL:$2|のタイプ}}に対応しています: $1\n\nPHP を自分でコンパイルした場合は、例えば <code>./configure --with-mysqli</code> を実行して、データベース クライアントを使用できるように再設定してください。\nDebian または Ubuntu のパッケージから PHP をインストールした場合は、モジュール (例: <code>php5-mysql</code>) もインストールする必要があります。",
"config-outdated-sqlite": "<strong>警告:</strong> あなたは SQLite $1 を使用していますが、最低限必要なバージョン $2 より古いバージョンです。SQLite は利用できません。",
@@ -78,12 +79,11 @@
"config-pcre-no-utf8": "<strong>致命的エラー:</strong> PHP の PCRE が PCRE_UTF8 対応なしでコンパイルされているようです。\nMediaWiki を正しく動作させるには、UTF-8 対応が必要です。",
"config-memory-raised": "PHPの<code>memory_limit</code>は$1で、$2に引き上げられました。",
"config-memory-bad": "<strong>警告:</strong> PHPの<code>memory_limit</code>に$1に設定されています。\nこの値はおそらく小さすぎます。\nインストールが失敗するおそれがあります!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] がインストール済み",
"config-apc": "[http://www.php.net/apc APC] がインストール済み",
- "config-apcu": "[http://www.php.net/apc APC] がインストール済みです。",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] がインストール済み",
- "config-no-cache-apcu": "<strong>警告:</strong> [http://www.php.net/apcu APCu]、 [http://xcache.lighttpd.net/ XCache]、 [http://www.iis.net/download/WinCacheForPhp WinCache] のいずれも見つかりませんでした。\nオブジェクトのキャッシュは有効化されません。",
- "config-mod-security": "<strong>警告:</strong> あなたのウェブサーバーでは [http://modsecurity.org/ mod_security] が有効になっています。正しく構成されていない場合は、MediaWiki や利用者にコンテンツの投稿を許可するその他のソフトウェアに問題が発生する場合があります。\n[http://modsecurity.org/documentation/ mod_security の説明文書]を確認するか、ランダムなエラーが発生した場合はあなたのホストのサポートにお問い合わせください。",
+ "config-apcu": "[http://www.php.net/apcu APCu] がインストール済み",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] がインストール済み",
+ "config-no-cache-apcu": "<strong>警告:</strong> [http://www.php.net/apcu APCu]、 [http://www.iis.net/download/WinCacheForPhp WinCache] のいずれも見つかりませんでした。\nオブジェクトのキャッシュは有効化されません。",
+ "config-mod-security": "<strong>警告:</strong> あなたのウェブサーバーでは [https://modsecurity.org/ mod_security] が有効になっています。正しく構成されていない場合は、MediaWiki や利用者にコンテンツの投稿を許可するその他のソフトウェアに問題が発生する場合があります。\n[https://modsecurity.org/documentation/ mod_security の説明文書]を確認するか、ランダムなエラーが発生した場合はあなたのホストのサポートにお問い合わせください。",
"config-diff3-bad": "GNU diff3 が見つかりません。",
"config-git": "バージョン管理ソフトウェア Git が見つかりました: <code>$1</code>",
"config-git-bad": "バージョン管理ソフトウェア Git が見つかりません。",
@@ -98,6 +98,7 @@
"config-no-cli-uploads-check": "<strong>警告:</strong> アップロード用のデフォルトディレクトリ (<code>$1</code>) が、CLIでのインストール中に任意のスクリプト実行の脆弱性チェックを受けていません。",
"config-brokenlibxml": "このシステムで使われているPHPとlibxml2のバージョンのこの組み合わせにはバグがあります。具体的には、MediaWikiやその他のウェブアプリケーションでhiddenデータが破損する可能性があります。\nlibxml2を2.7.3以降のバージョンにアップグレードしてください([https://bugs.php.net/bug.php?id=45996 PHPでのバグ情報])。\nインストールを終了します。",
"config-suhosin-max-value-length": "Suhosin がインストールされており、GET パラメーターの <code>length</code> を $1 バイトに制限しています。\nMediaWiki の ResourceLoader コンポーネントはこの制限を回避しますが、パフォーマンスは低下します。\n可能な限り、<code>php.ini</code> で <code>suhosin.get.max_value_length</code> を 1024 以上に設定し、同じ値を <code>LocalSettings.php</code> 内で <code>$wgResourceLoaderMaxQueryLength</code> に設定してください。",
+ "config-using-32bit": "<strong>警告:</strong>システムが32ビットで動作しているようです。 これは[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit 非推奨]です。",
"config-db-type": "データベースの種類:",
"config-db-host": "データベースのホスト:",
"config-db-host-help": "異なるサーバー上にデータベースサーバーがある場合、ホスト名またはIPアドレスをここに入力してください。\n\nもし、共有されたウェブホスティングを使用している場合、ホスティングプロバイダーは正確なホスト名を解説しているはずです。\n\nWindowsでMySQLを使用している場合に、「localhost」は、サーバー名としてはうまく働かないでしょう。もしそのような場合は、ローカルIPアドレスとして「127.0.0.1」を試してみてください。\n\nPostgreSQLを使用している場合、UNIXソケットで接続するにはこの欄を空欄のままにしてください。",
@@ -193,23 +194,23 @@
"config-mssql-sqlauth": "SQL Server 認証",
"config-mssql-windowsauth": "Windows 認証",
"config-site-name": "ウィキ名:",
- "config-site-name-help": "この事象はブラウザーのタイトルバーと他のさまざまな場所に現れる。",
+ "config-site-name-help": "この欄に入力したウィキ名は、ブラウザーのタイトルバーなど様々な場所で利用されます。",
"config-site-name-blank": "サイト名を入力してください。",
"config-project-namespace": "プロジェクト名前空間:",
"config-ns-generic": "プロジェクト",
"config-ns-site-name": "ウィキ名と同じ: $1",
"config-ns-other": "その他 (指定してください)",
"config-ns-other-default": "マイウィキ",
- "config-project-namespace-help": "ウィキペディアの例に従い、多くのウィキは、コンテンツのページとは分離したポリシーページを'''プロジェクトの名前空間'''に持っています。\nこの名前空間内のページのページ名はすべて特定の接頭辞で始まります。それをここで指定できます。\n通常、この接頭辞はウィキ名に基づきますが、「#」や「:」のような区切り文字を含めることはできません。",
+ "config-project-namespace-help": "ウィキペディアの例に従い、多くのウィキではコンテンツのページとは分離したポリシーページを'''プロジェクト名前空間'''に持っています。\nこの名前空間内のページ名はすべて特定の接頭辞で始まり、この欄ではその指定ができます。\n通常、この接頭辞はウィキ名に基づきますが、「#」や「:」のような区切り文字を含めることはできません。",
"config-ns-invalid": "指定した名前空間「<nowiki>$1</nowiki>」は無効です。\n別のプロジェクト名前空間を指定してください。",
"config-ns-conflict": "指定された名前空間「\"<nowiki>$1</nowiki>\" 」は、MediaWikiのデフォルト名前空間と衝突しています。\n他のプロジェクト名前空間を指定してください。",
"config-admin-box": "管理アカウント",
"config-admin-name": "利用者名:",
"config-admin-password": "パスワード:",
"config-admin-password-confirm": "パスワードの再入力:",
- "config-admin-help": "希望するユーザー名をここに入力してください (例:「Joe Bloggs」)。\nこの名前でこのウィキにログインすることになります。",
- "config-admin-name-blank": "管理者のユーザー名を入力してください。",
- "config-admin-name-invalid": "指定したユーザー名「<nowiki>$1</nowiki>」は無効です。\n別のユーザー名を指定してください。",
+ "config-admin-help": "希望する利用者名をここに入力してください (例:「Joe Bloggs」)。\nこの名前でこのウィキにログインすることになります。",
+ "config-admin-name-blank": "管理者の利用者名を入力してください。",
+ "config-admin-name-invalid": "指定した利用者名「<nowiki>$1</nowiki>」は無効です。\n別の利用者名を指定してください。",
"config-admin-password-blank": "管理者アカウントのパスワードを入力してください。",
"config-admin-password-mismatch": "入力された2つのパスワードが一致しません。",
"config-admin-email": "メールアドレス:",
@@ -221,7 +222,7 @@
"config-subscribe-help": "これは、リリースの告知 (重要なセキュリティに関する案内を含む) に使用される、流量が少ないメーリングリストです。\nこのメーリングリストを購読して、新しいバージョンが出た場合にMediaWikiを更新してください。",
"config-subscribe-noemail": "メールアドレスなしでリリースアナウンスのメーリングリストを購読しようとしています。\nメーリングリストを購読する場合にはメールアドレスを入力してください。",
"config-pingback": "このインストールに関するデータをMediaWikiの開発者と共有する。",
- "config-pingback-help": "もしこのオプションを選択すると、メディアウィキは定期的にhttps://www.mediawiki.orgとメディアウィキのインスタンスに関する基本データを呼び出します。このデータは例えばシステムのタイプ、PHPのバージョンと選択したデータベースのバックエンドなどを含んでいます。メディアウィキ財団はメディアウィキ開発者とこの情報を共有し、将来の開発の方向付けに役立たせます。ご使用のシステムに送るデータは次のとおりです。\n<pre>$1</pre>",
+ "config-pingback-help": "もし下記のオプションを有効にした場合、MediaWiki は定期的にこの MediaWiki インスタンスに関する基本データとともに https://www.mediawiki.org を呼び出します。このデータは例えばシステムのタイプ、PHPのバージョンと選択したデータベースのバックエンドなどを含んでいます。ウィキメディア財団は MediaWiki 開発者とこの情報を共有し、将来の開発に役立たせます。ご使用のシステムに送るデータは次のとおりです。\n<pre>$1</pre>",
"config-almost-done": "これでほぼ終わりました!\n残りの設定を飛ばして、ウィキを今すぐインストールできます。",
"config-optional-continue": "私にもっと質問してください。",
"config-optional-skip": "もう飽きてしまったので、とにかくウィキをインストールしてください。",
@@ -240,7 +241,7 @@
"config-license-gfdl": "GNU フリー文書利用許諾契約書 1.3 以降",
"config-license-pd": "パブリック・ドメイン",
"config-license-cc-choose": "その他のクリエイティブ・コモンズ・ライセンスを選択する",
- "config-license-help": "多くの公開ウィキでは、すべての寄稿物が[http://freedomdefined.org/Definition フリーライセンス]のもとに置かれています。\nこうすることにより、コミュニティによる共有の感覚が生まれ、長期的な寄稿が促されます。\n私的ウィキや企業のウィキでは、通常、フリーライセンスにする必要はありません。\n\nウィキペディアにあるテキストをあなたのウィキで利用し、逆にあなたのウィキにあるテキストをウィキペディアに複製することを許可したい場合には、<strong>{{int:config-license-cc-by-sa}}</strong>を選択するべきです。\n\nウィキペディアは以前、GNUフリー文書利用許諾契約書(GFDL)を使用していました。\nGFDLは有効なライセンスですが、内容を理解するのは困難です。\nまた、GFDLのもとに置かれているコンテンツの再利用も困難です。",
+ "config-license-help": "多くの公開ウィキでは、すべての寄稿物が[https://freedomdefined.org/Definition フリーライセンス]のもとに置かれています。\nこうすることにより、コミュニティによる共有の感覚が生まれ、長期的な寄稿が促されます。\n私的ウィキや企業のウィキでは、通常、フリーライセンスにする必要はありません。\n\nウィキペディアにあるテキストをあなたのウィキで利用し、逆にあなたのウィキにあるテキストをウィキペディアに複製することを許可したい場合には、<strong>{{int:config-license-cc-by-sa}}</strong>を選択するべきです。\n\nウィキペディアは以前、GNUフリー文書利用許諾契約書(GFDL)を使用していました。\nGFDLは有効なライセンスですが、内容を理解するのは困難です。\nまた、GFDLのもとに置かれているコンテンツの再利用も困難です。",
"config-email-settings": "メールの設定",
"config-enable-email": "メール送信を有効にする",
"config-enable-email-help": "メールを使用したい場合は、[http://www.php.net/manual/en/mail.configuration.php PHP のメール設定]が正しく設定されている必要があります。\nメールの機能を使用しない場合は、ここで無効にすることができます。",
@@ -270,7 +271,7 @@
"config-cache-options": "オブジェクトのキャッシュの設定:",
"config-cache-help": "オブジェクトのキャッシュを使用すると、頻繁に使用するデータをキャッシュするため MediaWiki の動作速度を改善できます。\n中〜大規模サイトではこれを有効にすることを強くお勧めします。小規模サイトでも同様に効果があります。",
"config-cache-none": "キャッシングしない(機能は取り払われます、しかもより大きなウィキサイト上でスピードの問題が発生します)",
- "config-cache-accel": "PHPオブジェクトキャッシュ (APC、APCu、XCache、WinCache のいずれか)",
+ "config-cache-accel": "PHPオブジェクトキャッシュ (APC、APCu、WinCache のいずれか)",
"config-cache-memcached": "memcached を使用 (追加の設定が必要)",
"config-memcached-servers": "memcached サーバー:",
"config-memcached-help": "Memcachedを使用するIPアドレスの一覧。\nカンマ区切りで、利用する特定のポートの指定が必要です。例:\n127.0.0.1:11211\n192.168.1.25:1234",
@@ -317,19 +318,23 @@
"config-insecure-keys": "<strong>警告:</strong> インストール中に生成されたセキュアキー ($1) は完璧に安全ではありません。手動で変更することを検討してください。",
"config-install-updates": "不要な更新を実行するのを防ぐ",
"config-install-updates-failed": "<strong>エラー:</strong> 更新キーをテーブルに挿入する際に失敗しました。以下のエラーが起こっています: $1",
- "config-install-sysop": "管理者のアカウントの作成",
+ "config-install-sysop": "管理者アカウントの作成",
"config-install-subscribe-fail": "mediawiki-announce を購読できませんでした: $1",
"config-install-subscribe-notpossible": "cURL がインストールされていないため、<code>allow_url_fopen</code> を利用できません。",
"config-install-mainpage": "メインページを既定の内容で作成",
+ "config-install-mainpage-exists": "メインページはすでに存在するためスキップします",
"config-install-extension-tables": "有効にした拡張機能のためのテーブルを作成しています",
"config-install-mainpage-failed": "メインページを挿入できませんでした: $1",
"config-install-done": "<strong>おめでとうございます!</strong>\nMediaWikiのインストールに成功しました。\n\n<code>LocalSettings.php</code>ファイルが生成されました。\nこのファイルはすべての設定を含んでいます。\n\nこれをダウンロードして、ウィキをインストールした基準ディレクトリ (index.phpと同じディレクトリ) に設置する必要があります。ダウンロードは自動的に開始されるはずです。\n\nダウンロードが開始されていない場合、またはダウンロードをキャンセルした場合は、下記のリンクをクリックしてダウンロードを再開できます:\n\n$3\n\n<strong>注意:</strong> この生成された設定ファイルをダウンロードせずにインストールを終了すると、このファイルは利用できなくなります。\n\n上記の作業が完了すると、<strong>[$2 ウィキに入る]</strong>ことができます。",
"config-install-done-path": "<strong>おめでとうございます!</strong>\nMediaWikiのインストールに成功しました。\n\n<code>LocalSettings.php</code>ファイルが生成されました。\nこのファイルはすべての設定を含んでいます。\n\nこれをダウンロードして、<code>$4</code> に設置する必要があります。ダウンロードは自動的に開始されるはずです。\n\nダウンロードが開始されていない場合、またはダウンロードをキャンセルした場合は、下記のリンクをクリックしてダウンロードを再開できます:\n\n$3\n\n<strong>注意:</strong> この生成された設定ファイルをダウンロードせずにインストールを終了すると、このファイルは利用できなくなります。\n\n上記の作業が完了すると、<strong>[$2 ウィキに入る]</strong>ことができます。",
+ "config-install-success": "MediaWikiが正常にインストールされました。\n今すぐ<$1$2>にアクセスしてあなたのwikiを表示できます。\nご質問がある場合は、よくある質問リストをご覧ください:\n<https://www.mediawiki.org/wiki/Manual:FAQ>または\nそのページにリンクされているサポートフォーラム",
"config-download-localsettings": "<code>LocalSettings.php</code> をダウンロード",
"config-help": "ヘルプ",
"config-help-tooltip": "クリックで展開",
"config-nofile": "ファイル「$1」が見つかりませんでした。削除された可能性があります。",
"config-extension-link": "あなたのウィキは[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions 拡張機能]をサポートしていることをご存知ですか?\n\n[https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category カテゴリ別で拡張機能を見る]か[https://www.mediawiki.org/wiki/Extension_Matrix 拡張機能のマトリックス]で拡張機能すべてのリストをご覧になれます。",
+ "config-skins-screenshots": "$1 (スクリーンショット: $2)",
+ "config-screenshot": "スクリーンショット",
"mainpagetext": "<strong>MediaWiki はインストール済みです。</strong>",
"mainpagedocfooter": "ウィキソフトウェアの使い方に関する情報は[https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents 利用者案内]を参照してください。\n\n== はじめましょう ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings/ja 設定の一覧]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/ja MediaWiki よくある質問と回答]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki リリース情報メーリングリスト]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation/ja MediaWiki のあなたの言語へのローカライズ]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam あなたのウィキでスパムと戦う方法を学ぶ]"
}
diff --git a/www/wiki/includes/installer/i18n/ka.json b/www/wiki/includes/installer/i18n/ka.json
index f5771aa3..fb47fcd2 100644
--- a/www/wiki/includes/installer/i18n/ka.json
+++ b/www/wiki/includes/installer/i18n/ka.json
@@ -27,9 +27,8 @@
"config-sidebar": "* [https://www.mediawiki.org მედიავიკის ვებ-გვერდი]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/ka მომხმარებლების დახმარება]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/ka ადმინისტრატორების დახმარება]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/ka FAQ]\n----\n* <doclink href=Readme>წამიკითხე</doclink>\n* <doclink href=ReleaseNotes>ინფორმაცია გამოშვებაზე</doclink>\n* <doclink href=Copying>ლიცენზია</doclink>\n* <doclink href=UpgradeDoc>განახლება</doclink>",
"config-env-php": "PHP $1 დაინსტალირებულია",
"config-env-hhvm": "HHVM $1 დაინსტალირებულია.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] დაყენდა",
"config-apc": "[http://www.php.net/apc APC] დაყენდა",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] დაყენდა",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] დაყენდა",
"config-diff3-bad": "GNU diff3 ვერ მოიძებნა.",
"config-db-type": "მონაცემთა ბაზის ტიპი:",
"config-db-host-oracle": "მონაცემთა ბაზის TNS:",
diff --git a/www/wiki/includes/installer/i18n/ko.json b/www/wiki/includes/installer/i18n/ko.json
index f268215f..e9314ed4 100644
--- a/www/wiki/includes/installer/i18n/ko.json
+++ b/www/wiki/includes/installer/i18n/ko.json
@@ -52,14 +52,14 @@
"config-help-restart": "입력한 모든 저장된 데이터를 지우고 설치 과정을 다시 시작하겠습니까?",
"config-restart": "예, 다시 시작합니다",
"config-welcome": "=== 사용 환경 검사 ===\n기본 검사는 지금 이 환경이 미디어위키 설치에 적합한지 수행합니다.\n설치를 완료하는 방법에 대한 지원을 찾는다면 이 정보를 포함해야 하는 것을 기억하세요.",
- "config-copyright": "=== 저작권 및 약관 ===\n\n$1\n\n이 프로그램은 자유 소프트웨어입니다. 당신은 자유 소프트웨어 재단이 발표한 GNU 일반 공중 사용 허가서 버전 2나 그 이후 버전에 따라 이 프로그램을 재배포하거나 수정할 수 있습니다.\n\n이 프로그램이 유용하게 사용될 수 있기를 바라지만 <strong>상용으로 사용</strong>되거나 <strong>특정 목적에 맞을 것</strong>이라는 것을 <strong>보증하지 않습니다</strong>.\n자세한 내용은 GNU 일반 공중 사용 허가서를 참조하십시오.\n\n당신은 이 프로그램을 통해 <doclink href=Copying>GNU 일반 공중 사용 허가서 전문</doclink>을 받았습니다. 그렇지 않다면, Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA로 편지를 보내주시거나 [http://www.gnu.org/copyleft/gpl.html 온라인으로 읽어보시기] 바랍니다.",
+ "config-copyright": "=== 저작권 및 약관 ===\n\n$1\n\n이 프로그램은 자유 소프트웨어입니다. 당신은 자유 소프트웨어 재단이 발표한 GNU 일반 공중 사용 허가서 버전 2나 그 이후 버전에 따라 이 프로그램을 재배포하거나 수정할 수 있습니다.\n\n이 프로그램이 유용하게 사용될 수 있기를 바라지만 <strong>상용으로 사용</strong>되거나 <strong>특정 목적에 맞을 것</strong>이라는 것을 <strong>보증하지 않습니다</strong>.\n자세한 내용은 GNU 일반 공중 사용 허가서를 참조하십시오.\n\n당신은 이 프로그램을 통해 <doclink href=Copying>GNU 일반 공중 사용 허가서 전문</doclink>을 받았습니다. 그렇지 않다면, Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA로 편지를 보내주시거나 [https://www.gnu.org/copyleft/gpl.html 온라인으로 읽어보시기] 바랍니다.",
"config-sidebar": "* [https://www.mediawiki.org 미디어위키 홈]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents 사용자 가이드]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents 관리자 가이드]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>읽어보기</doclink>\n* <doclink href=ReleaseNotes>릴리스 노트</doclink>\n* <doclink href=Copying>전문</doclink>\n* <doclink href=UpgradeDoc>업그레이드하기</doclink>",
"config-env-good": "환경이 확인되었습니다.\n미디어위키를 설치할 수 있습니다.",
"config-env-bad": "환경이 확인되었습니다.\n미디어위키를 설치할 수 없습니다.",
"config-env-php": "PHP $1이(가) 설치되어 있습니다.",
"config-env-hhvm": "HHVM $1이(가) 설치되어 있습니다.",
- "config-unicode-using-intl": "유니코드 정규화에 [http://pecl.php.net/intl intl PECL 확장 기능]을 사용합니다.",
- "config-unicode-pure-php-warning": "<strong>경고:</strong> 유니코드 정규화를 처리할 [http://pecl.php.net/intl intl PECL 확장 기능]을 사용할 수 없기 때문에 느린 pure-PHP 구현을 대신 사용합니다.\n트래픽이 높은 사이트에서 실행하시려면 [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations 유니코드 정규화]를 읽어보셔야 합니다.",
+ "config-unicode-using-intl": "유니코드 정규화에 [https://pecl.php.net/intl intl PECL 확장 기능]을 사용합니다.",
+ "config-unicode-pure-php-warning": "<strong>경고:</strong> 유니코드 정규화를 처리할 [https://pecl.php.net/intl intl PECL 확장 기능]을 사용할 수 없기 때문에 느린 pure-PHP 구현을 대신 사용합니다.\n트래픽이 높은 사이트에서 실행하시려면 [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations 유니코드 정규화]를 읽어보셔야 합니다.",
"config-unicode-update-warning": "<strong>경고:</strong> 유니코드 정규화 래퍼의 설치된 버전은 [http://site.icu-project.org/ ICU 프로젝트]의 라이브러리의 이전 버전을 사용합니다.\n만약 유니코드를 사용하는 것에 대해 우려가 된다면 [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations 업그레이드]해야합니다.",
"config-no-db": "적절한 데이터베이스 드라이버를 찾을 수 없습니다! PHP용 데이터베이스 드라이버를 설치해야 합니다.\n다음 데이터베이스 {{PLURAL:$2|유형을}} 지원합니다: $1.\n\nPHP를 직접 컴파일했다면, 예를 들어 <code>./configure --with-mysqli</code>을 사용하여, 데이터베이스 클라이언트를 활성화하도록 다시 설정하세요.\n데비안이나 우분투 패키지에서 PHP를 설치했다면 <code>php5-mysql</code> 패키지도 설치해야 합니다.",
"config-outdated-sqlite": "<strong>경고:</strong> 최소 요구 버전 $2 보다 낮은 SQLite $1이(가) 있습니다. SQLite를 사용할 수 없습니다.",
@@ -68,12 +68,11 @@
"config-pcre-no-utf8": "<strong>치명:</strong> PHP의 PCRE 모듈은 RCRE_UTF8 지원 없이 컴파일된 것 같습니다.\n미디어위키가 올바르게 작동하려면 UTF-8을 지원해야 합니다.",
"config-memory-raised": "PHP의 <code>memory_limit</code>는 $1이며 $2(으)로 늘렸습니다.",
"config-memory-bad": "<strong>경고:</strong> PHP의 <code>memory_limit</code>는 $1입니다.\n아마도 너무 낮은 것 같습니다.\n설치가 실패할 수 있습니다!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache]가 설치되었습니다",
"config-apc": "[http://www.php.net/apc APC]가 설치되었습니다",
"config-apcu": "[http://www.php.net/apcu APCu]가 설치되었습니다",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache]가 설치되었습니다",
- "config-no-cache-apcu": "<strong>경고:</strong> [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] 또는 [http://www.iis.net/download/WinCacheForPhp WinCache]를 찾을 수 없습니다.\n개체 캐싱을 활성화할 수 없습니다.",
- "config-mod-security": "<strong>경고:</strong> 웹 서버에 [http://modsecurity.org/ mod_security]가 허용되었습니다. 잘못 설정된 경우 미디어위키나 사용자가 임의의 내용을 게시할 수 있는 다른 소프트웨어에 대한 문제를 일으킬 수 있습니다.\n[http://modsecurity.org/documentation/ mod_security] 문서를 참고하거나 임의의 오류가 발생할 경우 호스트의 지원 요청에 문의하십시오.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache]가 설치되었습니다",
+ "config-no-cache-apcu": "<strong>경고:</strong> [http://www.php.net/apcu APCu] 또는 [http://www.iis.net/download/WinCacheForPhp WinCache]를 찾을 수 없습니다.",
+ "config-mod-security": "<strong>경고:</strong> 웹 서버에 [https://modsecurity.org/ mod_security]가 허용되었습니다. 잘못 설정된 경우 미디어위키나 사용자가 임의의 내용을 게시할 수 있는 다른 소프트웨어에 대한 문제를 일으킬 수 있습니다.\n[https://modsecurity.org/documentation/ mod_security] 문서를 참고하거나 임의의 오류가 발생할 경우 호스트의 지원 요청에 문의하십시오.",
"config-diff3-bad": "GNU diff3를 찾을 수 없습니다.",
"config-git": "Git 버전 관리 소프트웨어를 찾았습니다: <code>$1</code>.",
"config-git-bad": "Git 버전 관리 소프트웨어를 찾을 수 없습니다.",
@@ -229,7 +228,7 @@
"config-license-gfdl": "GNU 자유 문서 사용 허가서 1.3 이상",
"config-license-pd": "퍼블릭 도메인",
"config-license-cc-choose": "다른 크리에이티브 커먼즈 라이선스 선택",
- "config-license-help": "많은 공개 위키는 모든 기여를 [http://freedomdefined.org/Definition 자유 라이선스]에 따르도록 합니다.\n이렇게 하면 커뮤니티에 대한 소유권을 이해할 수 있도록 하고 장기적인 기여를 장려합니다.\n일반적으로 개인 또는 회사 위키에게는 필요하지 않습니다.\n\n위키백과의 텍스트를 사용할 수 있도록 하고 위키백과가 위키에서 복사한 텍스트를 사용할 수 있도록 원한다면 <strong>{{int:config-license-cc-by-sa}}</strong>으로 선택해야 합니다.\n\n위키백과는 이전에 GNU 자유 문서 사용 허가서(GFDL)를 사용했습니다.\nGFDL은 유효한 라이선스이지만 내용을 이해하기 어렵습니다.\nGFDL에 따라 사용이 허가된 내용을 재사용하는 것도 어렵습니다.",
+ "config-license-help": "많은 공개 위키는 모든 기여를 [https://freedomdefined.org/Definition 자유 라이선스]에 따르도록 합니다.\n이렇게 하면 커뮤니티에 대한 소유권을 이해할 수 있도록 하고 장기적인 기여를 장려합니다.\n일반적으로 개인 또는 회사 위키에게는 필요하지 않습니다.\n\n위키백과의 텍스트를 사용할 수 있도록 하고 위키백과가 위키에서 복사한 텍스트를 사용할 수 있도록 원한다면 <strong>{{int:config-license-cc-by-sa}}</strong>으로 선택해야 합니다.\n\n위키백과는 이전에 GNU 자유 문서 사용 허가서(GFDL)를 사용했습니다.\nGFDL은 유효한 라이선스이지만 내용을 이해하기 어렵습니다.\nGFDL에 따라 사용이 허가된 내용을 재사용하는 것도 어렵습니다.",
"config-email-settings": "이메일 설정",
"config-enable-email": "발신 이메일 활성화",
"config-enable-email-help": "이메일을 작동하려면 [http://www.php.net/manual/en/mail.configuration.php PHP의 메일 설정]을 올바르게 설정해야 합니다.\n이메일 기능을 사용하지 않으려면 이를 비활성화할 수 있습니다.",
@@ -259,7 +258,7 @@
"config-cache-options": "개체 캐싱을 위한 설정:",
"config-cache-help": "개체 캐싱은 자주 사용하는 데이터를 캐싱하여 미디어위키의 속도를 개선하는 데 사용합니다.\n큰 규모의 사이트는 이를 많이 사용하도록 권장하고 있으며, 소규모 사이트들도 물론 혜택을 볼 수 있습니다.",
"config-cache-none": "캐시하지 않음 (기능이 삭제되지는 않지만 큰 위키 사이트에 속도가 영향을 받을 수 있습니다)",
- "config-cache-accel": "PHP 개체 캐싱 (APC, APCu, XCache 또는 WinCache)",
+ "config-cache-accel": "PHP 개체 캐싱 (APC, APCu 또는 WinCache)",
"config-cache-memcached": "Memcached 사용 (추가적인 설치와 설정이 필요합니다)",
"config-memcached-servers": "Memcached 서버:",
"config-memcached-help": "Memcached의 사용하기 위한 IP 주소 목록입니다.\n한 줄에 하나씩 사용할 포트를 지정해야 합니다. 예를 들어:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -315,6 +314,7 @@
"config-install-mainpage-failed": "대문을 삽입할 수 없습니다: $1",
"config-install-done": "<strong>축하합니다!</strong>\n미디어위키를 설치했습니다.\n\n설치 관리자가 <code>LocalSettings.php</code> 파일을 만들었습니다.\n여기에 모든 설정이 포함되어 있습니다.\n\n파일을 다운로드하여 위키 설치의 거점에 넣어야 합니다. (index.php와 같은 디렉터리) 다운로드가 자동으로 시작됩니다.\n\n다운로드가 제공되지 않을 경우나 그것을 취소한 경우에는 아래의 링크를 클릭하여 다운로드를 다시 시작할 수 있습니다:\n\n$3\n\n<strong>참고:</strong> 이 생성한 설정 파일을 다운로드하지 않고 설치를 끝내면 이 파일은 나중에 사용할 수 없습니다.\n\n완료되었으면 <strong>[$2 위키에 들어갈 수 있습니다]</strong>.",
"config-install-done-path": "<strong>축하합니다!</strong>\n미디어위키가 설치되었습니다.\n\n설치 관리자가 <code>LocalSettings.php</code> 파일을 생성했습니다.\n이 파일에 모든 설정이 포함되어 있습니다.\n\n이 파일을 다운로드하여 <code>$4</code> 위치에 넣으세요. 다운로드가 자동으로 시작되었을 것입니다.\n\n다운로드가 시작되지 않았거나 취소했다면, 아래 링크를 클릭하여 다운로드를 재시작할 수 있습니다.\n\n$3\n\n<strong>알림:</strong> 지금 다운로드하지 않는다면, 이후에는 이 설정 파일을 다운로드할 수 없습니다.\n\n모든 작업이 완료되었다면, <strong>[$2 위키에 들어갈 수 있습니다]</strong>.",
+ "config-install-success": "미디어위키가 성공적으로 설치되었습니다. 이제\n<$1$2>에 방문하여 위키를 볼 수 있습니다.\n질문이 있으시다면 자주 묻는 질문 목록을 살펴보십시오:\n<https://www.mediawiki.org/wiki/Manual:FAQ> 아니면\n해당 문서에 연결된 지원 포럼 중 한곳을 이용하십시오.",
"config-download-localsettings": "<code>LocalSettings.php</code> 다운로드",
"config-help": "도움말",
"config-help-tooltip": "확장하려면 클릭",
diff --git a/www/wiki/includes/installer/i18n/ksh.json b/www/wiki/includes/installer/i18n/ksh.json
index 9a33d5b8..2ccf4e04 100644
--- a/www/wiki/includes/installer/i18n/ksh.json
+++ b/www/wiki/includes/installer/i18n/ksh.json
@@ -44,14 +44,14 @@
"config-help-restart": "Wells De all Ding enjejovve Saache fottjeschmeße han, un dä janze Vörjang vun fürre aan neu aanfange?",
"config-restart": "Joh, neu aanfange!",
"config-welcome": "=== Ömjevong Pröhfe ===\nMer maache en Aanzahl jrundlääje Pröhvunge, öm erus ze fenge, ov di Ömjävvong heh paß för Mediawiki opzesäze.\nWann de Hölp bem Opsäze hölls, saach wigger, wat heh erus kohm, alsu wat heh schteiht.",
- "config-copyright": "=== Urhävverrääsch un Lizänzbedengunge ===\n\n$1\n\nDat Projramm heh es frei, mer kann et wiggerjävve un verdeijle un och verändere onger dä Bedengunge vun de GNU <i lang=\"en\">General Public License</i> (Alljemeine öffentlesche Lizänz) wi se vun de <i lang=\"en\">Free Software Foundation</i> (de Schteftung för frei Projramme) veröffentlesch woode es. Dobei kanns De Der de Version 2 vun dä Lizanz ußsöhke, udder jeede Version donoh, wi et Der jefällt.\n\nDat Projramm weed wigger jejovve met dä Hoffnung, dat et jät nöz, ävver <strong>der ohne Jarrantie</strong>, sujaa der ohne de onußjeshproche Jarantie, <strong>verkoufbaa</strong> ze sin, udder <strong>för öhnds_ene beshtemmpte Zweck ze bruche</strong> ze sin.\nLiß de GNU <i lang=\"en\">General Public License</i> sellver, öm mieh ze erfahre.\n\nDo sullts en <doclink href=Copying>Kopie vun dä alljemene öffentlesche Lizänz vun dä GNU</doclink> (<i lang=\"en\">GNU General Public License</i>) zosamme met heh däm Projramm krääje han. Wann dat nit esu es, schrief aan de <i lang=\"en\">Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA</i>, udder [http://www.gnu.org/copyleft/gpl.html liß se online övver et Internet].",
+ "config-copyright": "=== Urhävverrääsch un Lizänzbedengunge ===\n\n$1\n\nDat Projramm heh es frei, mer kann et wiggerjävve un verdeijle un och verändere onger dä Bedengunge vun de GNU <i lang=\"en\">General Public License</i> (Alljemeine öffentlesche Lizänz) wi se vun de <i lang=\"en\">Free Software Foundation</i> (de Schteftung för frei Projramme) veröffentlesch woode es. Dobei kanns De Der de Version 2 vun dä Lizanz ußsöhke, udder jeede Version donoh, wi et Der jefällt.\n\nDat Projramm weed wigger jejovve met dä Hoffnung, dat et jät nöz, ävver <strong>der ohne Jarrantie</strong>, sujaa der ohne de onußjeshproche Jarantie, <strong>verkoufbaa</strong> ze sin, udder <strong>för öhnds_ene beshtemmpte Zweck ze bruche</strong> ze sin.\nLiß de GNU <i lang=\"en\">General Public License</i> sellver, öm mieh ze erfahre.\n\nDo sullts en <doclink href=Copying>Kopie vun dä alljemene öffentlesche Lizänz vun dä GNU</doclink> (<i lang=\"en\">GNU General Public License</i>) zosamme met heh däm Projramm krääje han. Wann dat nit esu es, schrief aan de <i lang=\"en\">Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA</i>, udder [https://www.gnu.org/copyleft/gpl.html liß se online övver et Internet].",
"config-sidebar": "* [https://www.mediawiki.org MediaWiki sing Hompäjdsch]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Handbohch för Aanwänder]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Handbohch för Administratohre un Wiki_Köbesse]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Öff jeschtallte Frohre met Antwoote]\n----\n* <doclink href=Readme>Liß Mesch! (<i lang=\"en\">Read me</i>)</doclink>\n* <doclink href=ReleaseNotes><i lang=\"en\">Release notes</i> Övver heh di Projrammversion</doclink>\n* <doclink href=Copying><i lang=\"en\">Copying</i> — Lizänzbeshtemmunge</doclink>\n* <doclink href=UpgradeDoc><i lang=\"en\">Upgrading</i> — Ob en neu Projrammversion jonn</doclink>",
"config-env-good": "De Ömjävvöng es jepröhf.\nDo kanns MehdijaWikki opsäze.",
"config-env-bad": "De Ömjävong es jeprööf.\nDo kanns MehdijaWikki nit opsäze.",
"config-env-php": "PHP $1 es doh.",
"config-env-hhvm": "HHVM $1 es enschtalleerd.",
- "config-unicode-using-intl": "För et <i lang=\"en\">Unicode</i>-Nommaliseere dom_mer dä [http://pecl.php.net/intl Zohsaz <code lang=\"en\">intl</code> uss em <code lang=\"en\">PECL</code>] nämme.",
- "config-unicode-pure-php-warning": "'''Opjepaß:''' Mer kunnte dä [http://pecl.php.net/intl Zohsaz <code lang=\"en\">intl</code> uss em <code lang=\"en\">PECL</code>] för et <i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"a standard for the consistent encoding, representation, and handling of text expressed in most of the world's writing systems\">UNICODE</i>-Nommalisehre nit fenge. Dröm nämme mer dat eijfache, ävver ärsch lahme, <i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"PHP Hypertext Preprocessor\">PHP</i>-Projrammschtök doför.\nFör jruuße Wikis met vill Metmaachere doht Üsch di Sigg övver et [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations <i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"a standard for the consistent encoding, representation, and handling of text expressed in most of the world's writing systems\">UNICODE</i>-Nommaliseere] (es op Änglesch) aanloore.",
+ "config-unicode-using-intl": "För et <i lang=\"en\">Unicode</i>-Nommaliseere dom_mer dä [https://pecl.php.net/intl Zohsaz <code lang=\"en\">intl</code> uss em <code lang=\"en\">PECL</code>] nämme.",
+ "config-unicode-pure-php-warning": "'''Opjepaß:''' Mer kunnte dä [https://pecl.php.net/intl Zohsaz <code lang=\"en\">intl</code> uss em <code lang=\"en\">PECL</code>] för et <i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"a standard for the consistent encoding, representation, and handling of text expressed in most of the world's writing systems\">UNICODE</i>-Nommalisehre nit fenge. Dröm nämme mer dat eijfache, ävver ärsch lahme, <i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"PHP Hypertext Preprocessor\">PHP</i>-Projrammschtök doför.\nFör jruuße Wikis met vill Metmaachere doht Üsch di Sigg övver et [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations <i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"a standard for the consistent encoding, representation, and handling of text expressed in most of the world's writing systems\">UNICODE</i>-Nommaliseere] (es op Änglesch) aanloore.",
"config-unicode-update-warning": "'''Opjepaß:''' Dat Projramm för der <i lang=\"en\">Unicode</i> zo normaliseere boud em Momang op en ählter Version vun dä Bibliothek vum [http://site.icu-project.org/ ICU-Projäk] op.\nDoht di [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations op der neuste Shtand bränge], wann auf dat Wiki em Äänz <i lang=\"en\">Unicode</i> bruche sull.",
"config-no-db": "Mer kunnte kei zopaß Daatebangk-Driiverprojamm fenge.\nMer bruche e Daatebangk-Driiverprojamm för PHP. Dat moß enjeresht wääde.\nMer künne met heh dä {{PLURAL:$2|Daatebangk|Daatebangke|Daatebangk}} ömjonn: $1.\n\nWann De nit om eijene Rääshner bes, moß De Dinge <i lang=\"en\">provider</i> bedde, dat hä Der ene zopaß Driiver enresht.\nWann de PHP sellver övversaz häs, donn e Zohjangsprojramm för en Daatebangk enbenge, för e Beishpell met: <code lang=\"en\">./configure --with-mysqli</code>.\nWann De PHP uss enem <i lang=\"en\">Debian</i> udder <i lang=\"en\">Ubuntu</i> Pakätt enjeresht häs, moß De dann och noch et <code lang=\"en\">php5-mysql</code> op Dinge Räschner bränge.",
"config-outdated-sqlite": "'''Opjepaß:''' <i lang=\"en\">SQLite</i> $1 es enschtaleert. Avver MediaWiki bruch <i lang=\"en\">SQLite</i> $2 udder hühter. <i lang=\"en\">SQLite</i> kann dröm nit enjesaz wääde.",
@@ -60,11 +60,10 @@
"config-pcre-no-utf8": "'''Dä:''' Et PHP-Modul <i lang=\"en\">PCRE</i> schingk ohne de <i lang=\"en\">PCRE_UTF8</i>-Aandeile övversaz ze sin.\nMediaWiki bruch dä UTF-8-Krohm ävver, öm ohne Fähler loufe ze künne.",
"config-memory-raised": "Der jrühzte zohjelasse Shpeisherbedarf vum PHP, et <code lang=\"en\">memory_limit</code>, shtund op $1 un es op $2 erop jesaz woode.",
"config-memory-bad": "'''Opjepaß:''' Dem PHP singe Parameeter <code lang=\"en\">memory_limit</code> es $1.\nDat es wall ze winnisch.\nEt Enreeschte kunnt doh draan kappott jon!",
- "config-xcache": "Dä <code lang=\"en\">[http://xcache.lighttpd.net/ XCache]</code> es ennjeresht.",
"config-apc": "Dä <code lang=\"en\">[http://www.php.net/apc APC]</code> es ennjeresht.",
- "config-wincache": "Dä <code lang=\"en\">[http://www.iis.net/download/WinCacheForPhp WinCache]</code> es ennjeresht.",
+ "config-wincache": "Dä <code lang=\"en\">[https://www.iis.net/download/WinCacheForPhp WinCache]</code> es ennjeresht.",
"config-no-cache-apcu": "'''Opjepaß:''' Mer kunnte dä <code lang=\"en\" xml:lang=\"en\" dir=\"rtl\">[http://www.php.net/apcu APCu]</code>, dä <code lang=\"en\" xml:lang=\"en\" dir=\"rtl\">[http://xcache.lighttpd.net/ XCache]</code> udder dä <code lang=\"en\" xml:lang=\"en\" dir=\"rtl\">[http://www.iis.net/download/WinCacheForPhp WinCache]</code> nit fenge.\nEt <i lang=\"en\" xml:lang=\"en\" dir=\"rtl\">object caching</i> es nit müjjelesch un es ußjeschalldt.",
- "config-mod-security": "<strong>Opjepaß</strong>: Dinge Wäbßööver hät <code lang=\"en\" xml:lang=\"en\" dir=\"ltr\">[http://modsecurity.org/ mod_security]</code> enjeschalldt. Jenohch schtandattmähßejje Enschtällonge heh em Wikki künne Problehme met MehdijaWikki un och met ander Projramme aanschtivvelle, di zohlohße, dat vun ußerhallef öhndsene Krohm op dä Webßööver jebraat wähde künnt.\nWann müjjelesch sullt mer dat affschallde. Söns beloor Der di Sigg <code lang=\"en\" xml:lang=\"en\" dir=\"ltr\">[http://modsecurity.org/documentation/ mod_security documentation]</code> udder donn met dä Fachlück för Dinge Webßööver kalle, wann zohfälleje un koomijje Fähler bemärke deihß.",
+ "config-mod-security": "<strong>Opjepaß</strong>: Dinge Wäbßööver hät <code lang=\"en\" xml:lang=\"en\" dir=\"ltr\">[https://modsecurity.org/ mod_security]</code> enjeschalldt. Jenohch schtandattmähßejje Enschtällonge heh em Wikki künne Problehme met MehdijaWikki un och met ander Projramme aanschtivvelle, di zohlohße, dat vun ußerhallef öhndsene Krohm op dä Webßööver jebraat wähde künnt.\nWann müjjelesch sullt mer dat affschallde. Söns beloor Der di Sigg <code lang=\"en\" xml:lang=\"en\" dir=\"ltr\">[https://modsecurity.org/documentation/ mod_security documentation]</code> udder donn met dä Fachlück för Dinge Webßööver kalle, wann zohfälleje un koomijje Fähler bemärke deihß.",
"config-diff3-bad": "Mer han <i lang=\"en\">GNU</i> <code lang=\"en\">diff3</code> nit jefonge.",
"config-git": "Mer han de Väsjohn <code>$1</code> vun däm Väsjohnsverwalldongsprojamm <i lang=\"en\">Git</i> jefonge.",
"config-git-bad": "Dat Väsjohnsverwalldongsprojamm <i lang=\"en\">Git</i> ham_mer nit jefonge.",
@@ -220,7 +219,7 @@
"config-license-gfdl": "De <i lang=\"en\">GNU</i>-Lizänz för frei Dokemäntazjuhne, Version 1.3 udder en späädere",
"config-license-pd": "Allmende (jemeinfrei, <i lang=\"en\">public domain</i>)",
"config-license-cc-choose": "En <i lang=\"en\">Creative Commons</i> Lizänz, sellver ußjesöhk:",
- "config-license-help": "Ättlijje öffentleje Wikis donn iehr Beidrääsch onger en [http://freedomdefined.org/Definition freije Lizänz] schtelle.\nDat hellef, e Jeföhl vun Jemeinsamkeid opzeboue, un op lange Seesch emmer wider Beidrääsch ze krijje.\nDat es nit onbedengk nüüdesh för e Jeschäffs- udder Privaat_Wiki.\n\nWä Stöcke uß de Wikipedia bruche well, un dröm han well, dat mer för Wikipedia uss_em eije Wiki jät övvernämme kann, sullt „'''<i lang=\"en\">Creative Commons</i>, dem Schriever singe Name moß jenannt wääde, un Wiggerjävve zoh dersellve Bedengunge es zohjelohße'''“ ußwähle.\n\nDe su jenannte '''<i lang=\"en\">GNU Free Documentation License</i>''' (de freije Lizänz för Dokemäntazjuhne vun dä GNU) sen de ahle Lizänzbedenonge vun de Wikipedia. Se es emmer noch in Odenong un jöltesch, ävver se es schwer ze verschtonn un et Wiggerjävve un widder Bruche es ens schwieerejer domet.",
+ "config-license-help": "Ättlijje öffentleje Wikis donn iehr Beidrääsch onger en [https://freedomdefined.org/Definition freije Lizänz] schtelle.\nDat hellef, e Jeföhl vun Jemeinsamkeid opzeboue, un op lange Seesch emmer wider Beidrääsch ze krijje.\nDat es nit onbedengk nüüdesh för e Jeschäffs- udder Privaat_Wiki.\n\nWä Stöcke uß de Wikipedia bruche well, un dröm han well, dat mer för Wikipedia uss_em eije Wiki jät övvernämme kann, sullt „'''<i lang=\"en\">Creative Commons</i>, dem Schriever singe Name moß jenannt wääde, un Wiggerjävve zoh dersellve Bedengunge es zohjelohße'''“ ußwähle.\n\nDe su jenannte '''<i lang=\"en\">GNU Free Documentation License</i>''' (de freije Lizänz för Dokemäntazjuhne vun dä GNU) sen de ahle Lizänzbedenonge vun de Wikipedia. Se es emmer noch in Odenong un jöltesch, ävver se es schwer ze verschtonn un et Wiggerjävve un widder Bruche es ens schwieerejer domet.",
"config-email-settings": "Enschtellunge för de <i lang=\"en\">e-mail</i>",
"config-enable-email": "De <i lang=\"en\">e-mail</i> noh druße zohlohße",
"config-enable-email-help": "Sulle \n<i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"„de eläktrohnesche Poß“\">e-mails</i> zohjelohße sin, moß mer, domet et noher flupp, dä Datteij <code lang=\"en\" xml:lang=\"en\" dir=\"ltr\">[http://www.php.net/manual/en/mail.configuration.php</code> Enschtällonge em PHP för de <i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"„de eläktrohnesche Poß“\">e-mail</i>] zopaß jemaat han.\nWann kein <i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"„de eläktrohnesche Poß“\">e-mails</i> nüüdesch sin, kam_mer se heh afschallde.",
diff --git a/www/wiki/includes/installer/i18n/ku-latn.json b/www/wiki/includes/installer/i18n/ku-latn.json
index b68b9d8f..6d40a10a 100644
--- a/www/wiki/includes/installer/i18n/ku-latn.json
+++ b/www/wiki/includes/installer/i18n/ku-latn.json
@@ -26,9 +26,8 @@
"config-page-upgradedoc": "Bilindkirin",
"config-page-existingwiki": "Wîkiya heye",
"config-restart": "Erê, jinûve bide destpêkirin",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] hate avakirin",
"config-apc": "[http://www.php.net/apc APC] hate avakirin",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] hate avakirin",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] hate avakirin",
"config-diff3-bad": "GNU diff3 nehate dîtin.",
"config-db-type": "Cureya danegehê:",
"config-db-wiki-settings": "Vî wîkîyê bide danasîn",
@@ -64,6 +63,6 @@
"config-cc-again": "Dîsa hilbijêre...",
"config-install-step-done": "çêbû",
"config-help": "alîkarî",
- "mainpagetext": "'''MediaWiki serketî hate çêkirin.'''",
+ "mainpagetext": "<strong>MediaWiki hate sazkirin.<strong>",
"mainpagedocfooter": "Alîkarî ji bo bikaranîn û guherandin yê datayê Wîkî tu di bin [https://meta.wikimedia.org/wiki/Help:Contents pirtûka alîkarîyê ji bikarhêneran] da dikarê bibînê.\n\n== Alîkarî ji bo destpêkê ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lîsteya varîyablên konfîgûrasîyonê]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lîsteya e-nameyên versyonên nuh yê MediaWiki]"
}
diff --git a/www/wiki/includes/installer/i18n/lb.json b/www/wiki/includes/installer/i18n/lb.json
index e7952114..66fb4cad 100644
--- a/www/wiki/includes/installer/i18n/lb.json
+++ b/www/wiki/includes/installer/i18n/lb.json
@@ -49,10 +49,9 @@
"config-no-db": "Et konnt kee passenden Datebank-Driver fonnt ginn! Dir musst een Datebank-Driver fir PHP installéieren.\n{{PLURAL:$2|Dësn Datebank-Typ gëtt|Dës Datebank-Type ginn}} ënnerstëtzt: $1.\n\nWann Dir PHP selwer compiléiert hutt, da rekonfiguréiert en mat dem ageschalten Datebank-Client, zum Beispill an deem Dir <code>./configure --with-mysqli</code> benotzt.\nWann Dir PHP vun engem Debian oder Ubuntu Package aus installéiert hutt, da musst Dir och den php5-mysql Modul installéieren.",
"config-outdated-sqlite": "'''Warnung:''' SQLite $1 ass installéiert. Allerdengs brauch MediaWiki SQLite $2 oder méi nei. SQLite ass dofir net disponibel.",
"config-memory-bad": "'''Opgepasst:''' De Parameter <code>memory_limit</code> vu PHP ass $1.\nDat ass wahrscheinlech ze niddreg.\nD'Installatioun kéint net funktionéieren.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] ass installéiert",
"config-apc": "[http://www.php.net/apc APC] ass installéiert",
"config-apcu": "[http://www.php.net/apcu APCu] ass installéiert.",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] ass installéiert",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] ass installéiert",
"config-diff3-bad": "GNU diff3 gouf net fonnt.",
"config-git": "D'Software Git fir d'Kontroll vu Versioune gouf fonnt: <code>$1</code>.",
"config-git-bad": "D'Software fir d'Kontroll vun de Versiounen 'Git' gouf net fonnt.",
diff --git a/www/wiki/includes/installer/i18n/lij.json b/www/wiki/includes/installer/i18n/lij.json
index 0ecc0978..7a7ed57c 100644
--- a/www/wiki/includes/installer/i18n/lij.json
+++ b/www/wiki/includes/installer/i18n/lij.json
@@ -41,14 +41,14 @@
"config-help-restart": "Ti voeu scassâ tutti i dæti sarvæ che ti t'hæ inseio e riavviâ o processo de installaçion?",
"config-restart": "Scì, riavvia",
"config-welcome": "=== Controllo de l'ambiente ===\nSaiâ eseguio di controlli de base pe vedde se questo ambiente o l'è adatto pe l'installaçion de MediaWiki.\nRegordite de includde queste informaçioin se ti domandi ascistença insce comme completâ l'installaçion.",
- "config-copyright": "=== Copyright e termini ===\n\n$1\n\nQuesto programma o l'è un software libero; ti poeu redistriboîlo e/ò modificâlo segondo i termi da GNU General Public License, comme pubbricâ da-a Free Software Foundation; ò a verscion 2 da Liçença ò (a proppia scelta) qualunque verscion succesciva.\n\nQuesto programma o l'è distribuio inta sperança ch'o segge utile, ma SENSA ARCUNA GARANTIA; sença manco a garantia impliçita de NEGOSSIABILITÆ o de APPRICABILITÆ PE UN PARTICOLÂ SCOPO.\nS'amie a GNU General Public License pe maggioî dettaggi.\n\nQuesto programma o dev'ese distribuio insemme a <doclink href=Copying>una copia da GNU General Public License</doclink>; in caxo contraio, se ne poeu otegnî un-a scrivendo a-a Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA oppù [http://www.gnu.org/copyleft/gpl.html lezila inta ræ'].",
+ "config-copyright": "=== Copyright e termini ===\n\n$1\n\nQuesto programma o l'è un software libero; ti poeu redistriboîlo e/ò modificâlo segondo i termi da GNU General Public License, comme pubbricâ da-a Free Software Foundation; ò a verscion 2 da Liçença ò (a proppia scelta) qualunque verscion succesciva.\n\nQuesto programma o l'è distribuio inta sperança ch'o segge utile, ma SENSA ARCUNA GARANTIA; sença manco a garantia impliçita de NEGOSSIABILITÆ o de APPRICABILITÆ PE UN PARTICOLÂ SCOPO.\nS'amie a GNU General Public License pe maggioî dettaggi.\n\nQuesto programma o dev'ese distribuio insemme a <doclink href=Copying>una copia da GNU General Public License</doclink>; in caxo contraio, se ne poeu otegnî un-a scrivendo a-a Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA oppù [https://www.gnu.org/copyleft/gpl.html lezila inta ræ'].",
"config-sidebar": "* [https://www.mediawiki.org Paggina prinçipâ MediaWiki]\n* [https://www.mediawiki.org/wiki/Agiutto:Guidda a-i contegnui pe utenti]\n* [https://www.mediawiki.org/wiki/Manoâ:Guidda ai contegnui per admin]\n* [https://www.mediawiki.org/wiki/Manoâ:FAQ FAQ]\n----\n* <doclink href=Readme>Lezime</doclink>\n* <doclink href=ReleaseNotes>Notte de verscion</doclink>\n* <doclink href=Copying>Copie</doclink>\n* <doclink href=UpgradeDoc>Aggiornamenti</doclink>",
"config-env-good": "L'ambiente o l'è stæto controllou.\nL'è poscibile installâ MediaWiki.",
"config-env-bad": "L'ambiente o l'è stæto controllou.\nNon l'è poscibbile installâ MediaWiki.",
"config-env-php": "PHP $1 o l'è installou.",
"config-env-hhvm": "HHVM $1 o l'è installou.",
- "config-unicode-using-intl": "Adoeuvia [http://pecl.php.net/intl l'estenscion PECL intl] pe-a normalizzaçion Unicode.",
- "config-unicode-pure-php-warning": "'''Attençion:''' [http://pecl.php.net/intl l'estenscion PECL intl] a no l'è disponibile pe gestî a normalizzaçion Unicode, quindi se torna a-a lenta implementaçion in PHP puo.\nSe ti esegui un scito a ato traffego, ti doviesci leze arcun-e conscidiaçioin in sciâ [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizzaçion Unicode].",
+ "config-unicode-using-intl": "Adoeuvia [https://pecl.php.net/intl l'estenscion PECL intl] pe-a normalizzaçion Unicode.",
+ "config-unicode-pure-php-warning": "'''Attençion:''' [https://pecl.php.net/intl l'estenscion PECL intl] a no l'è disponibile pe gestî a normalizzaçion Unicode, quindi se torna a-a lenta implementaçion in PHP puo.\nSe ti esegui un scito a ato traffego, ti doviesci leze arcun-e conscidiaçioin in sciâ [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizzaçion Unicode].",
"config-unicode-update-warning": "'''Attençion:''' a verscion installaa do gestô pe-a normalizzaçion Unicode a l'adoeuvia una vegia verscion da libraia [http://site.icu-project.org/ do progetto ICU].\nTi doviesci [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations aggiornâ] se ti voeu doeuviâ l'Unicode.",
"config-no-db": "Imposcibile trovâ un driver adatto pe-o database! L'è necessaio installâ un driver pe PHP.\n{{PLURAL:$2|O seguente formato de database o l'è supportou|I seguenti formati de database son supportæ}}: $1.\n\nSe ti compilli PHP aotonomamente, riconfiguilo attivando un client database, presempio utilizzando <code>./configure --with-mysqli</code>.\nQualoa t'avesci installou PHP pe mezo de 'n pacchetto Debian ò Ubuntu, alloa ti devi installâ o pacchetto <code>php5-mysql</code> ascì.",
"config-outdated-sqlite": "'''Atençion''': ti g'hæ SQLite $1, ma te ghe voeu comme minnimo a verscion $2. SQLite o no saiâ disponibile.",
@@ -57,12 +57,11 @@
"config-pcre-no-utf8": "'''Fatale''': o modulo PCRE de PHP pâ ch'o segge stæto compilou sença o supporto PCRE_UTF8. A MediaWiki a-o richiede pe fonçionâ corettamente.",
"config-memory-raised": "O valô <code>memory_limit</code> de PHP o l'è $1, aomentou a $2.",
"config-memory-bad": "''Atençion:''' O valô de <code>memory_limit</code> do PHP o l'è $1.\nFoscia o l'è troppo basso.\nL'installaçion a porriæ fallî!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] o l'è installou",
"config-apc": "[http://www.php.net/apc APC] o l'è installou",
"config-apcu": "[http://www.php.net/apc APC] o l'è installou",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] o l'è installou",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] o l'è installou",
"config-no-cache-apcu": "'''Atençion:''' [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] ò [http://www.iis.net/download/WinCacheForPhp WinCache] no son stæti trovæ.\nA caching di ogetti a no l'è attivâ.",
- "config-mod-security": "<strong>Atençion:</strong> O to serviou web o g'ha o [http://modsecurity.org/ mod_security] abilitou. Gh'è tante configuaçioin che crean di problemi a-a MediaWiki ò a atro software ch'o permette a-i utenti de pubbricâ quâ-se-segge contegnuo. Se poscibbile o doviæ ese disabilitou.\nFanni rifeimento a-a [http://modsecurity.org/documentation/ documentaçion insce-o mod_security] ò contatta o supporto tecnico do to provider de hosting se se veifica di erroî.",
+ "config-mod-security": "<strong>Atençion:</strong> O to serviou web o g'ha o [https://modsecurity.org/ mod_security] abilitou. Gh'è tante configuaçioin che crean di problemi a-a MediaWiki ò a atro software ch'o permette a-i utenti de pubbricâ quâ-se-segge contegnuo. Se poscibbile o doviæ ese disabilitou.\nFanni rifeimento a-a [https://modsecurity.org/documentation/ documentaçion insce-o mod_security] ò contatta o supporto tecnico do to provider de hosting se se veifica di erroî.",
"config-diff3-bad": "GNU diff3 non trovou.",
"config-git": "Trovou software de controllo da verscion Git: <code>$1</code>.",
"config-git-bad": "Software de controllo da verscion Git non trovou.",
@@ -216,7 +215,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 o verscioin sucescive",
"config-license-pd": "Pubbrico dominnio",
"config-license-cc-choose": "Seleçion-a un-a de liçençie Creative Commons",
- "config-license-help": "Tante wiki pubbriche rilascian i so contributi co-ina [http://freedomdefined.org/Definition liçençia libbera]. Sto fæto o l'agiutta a creâ un senso de propietæ condivisa inta comunitæ e o l'incoragisce a contriboî a longo termine. O no l'è generalmente necessaio pe 'na wiki privâ ò aziendale.\n\nSe ti voeu doeuviâ di scriti da Wikipedia, ò ti dexiddei che a Wikipedia a posse vese in graddo de acetâ di scriti copiæ da-a to wiki, ti doviesci scellie <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nIn precedença a Wikipedia a l'ha doeuviou a GNU Free Documentation License. A GFDL a l'è 'na liçençia vallida, ma a l'è difiççile da capî e a complica o riutilizzo di contegnui.",
+ "config-license-help": "Tante wiki pubbriche rilascian i so contributi co-ina [https://freedomdefined.org/Definition liçençia libbera]. Sto fæto o l'agiutta a creâ un senso de propietæ condivisa inta comunitæ e o l'incoragisce a contriboî a longo termine. O no l'è generalmente necessaio pe 'na wiki privâ ò aziendale.\n\nSe ti voeu doeuviâ di scriti da Wikipedia, ò ti dexiddei che a Wikipedia a posse vese in graddo de acetâ di scriti copiæ da-a to wiki, ti doviesci scellie <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nIn precedença a Wikipedia a l'ha doeuviou a GNU Free Documentation License. A GFDL a l'è 'na liçençia vallida, ma a l'è difiççile da capî e a complica o riutilizzo di contegnui.",
"config-email-settings": "Impostaçioin e-mail",
"config-enable-email": "Abillita a sciortia da posta elettronica",
"config-enable-email-help": "Se ti voeu che fonçion-e l'e-mail, e [http://www.php.net/manual/en/mail.configuration.php PHP's impostaçioin della posta] dev'esan configuæ corettamente.\nSe non ti dexiddei arcun-a fonçionalitæ de posta eletronnica, ti a poeu disabilitâ chie.",
diff --git a/www/wiki/includes/installer/i18n/lki.json b/www/wiki/includes/installer/i18n/lki.json
index 45f13314..9547e423 100644
--- a/www/wiki/includes/installer/i18n/lki.json
+++ b/www/wiki/includes/installer/i18n/lki.json
@@ -44,14 +44,14 @@
"config-help-restart": "آیا می‌خواهید همهٔ اطلاعات ذخیره شده‌ای که وارد کرده‌اید را پاک کنید و دوباره روند نصب را شروع کنید؟",
"config-restart": "أرێ، دوواره راه‌اندازی کة",
"config-welcome": "===بررسی‌های محیطی===\nبرای فهمیدن اینکه این محیط برای نصب مدیاویکی مناسب است، اکنون بررسی‌های اساسی انجام خواهد‌شد.\nاگر به دنبال پشتیبانی در چگونگی تکمیل نصب هستید،به یاد داشته باشید این اطلاعات را بگنجانید.",
- "config-copyright": "===حق چاپ و شرایط===\n$1\nاین برنامه، یک نرم‌افزاری آزاد است. شما می‌توانید آن را بازتوزیع کرده و/یا با شرایط نگارش ۲ یا (با نظر خودتان) هر نگارش جدیدتری از پروانه جامع همگانی گنو که توسط بنیاد نرم‌افزار آزاد منتشر شده، تغییر دهید.\n\nاین برنامه با امید این که مفید واقع‌ شود توزیع شده‌است،اما '''بدون هیچ ضمانتی'''; حتی بدون اشارهٔ ضمانتی از '''قابلیت عرضه''' یا ''' صلاحیت برای یک هدف خاص'''.\nبرای جزئیات بیش‌تر پروانه جامع همگانی گنو را مشاهده کنید.\n\nشما باید <doclink href=Copying> یک نگارش ازمجوز عمومی کلی </doclink> همراه این برنامه دریافت کرده باشید. در غیر این صورت با بنیاد نرم‌افزار آزاد، ایالات متحده امریکا، بوستون، خیابان فرانکلین، پلاک ۵۱، طبقه پنجم، صندوق پستی MA۰۲۱۱۰-۱۳۰ مکاتبه کنید، یا [http://www.gnu.org/copyleft/gpl.html در این‌جا به صورت برخط بخوانید].",
+ "config-copyright": "===حق چاپ و شرایط===\n$1\nاین برنامه، یک نرم‌افزاری آزاد است. شما می‌توانید آن را بازتوزیع کرده و/یا با شرایط نگارش ۲ یا (با نظر خودتان) هر نگارش جدیدتری از پروانه جامع همگانی گنو که توسط بنیاد نرم‌افزار آزاد منتشر شده، تغییر دهید.\n\nاین برنامه با امید این که مفید واقع‌ شود توزیع شده‌است،اما '''بدون هیچ ضمانتی'''; حتی بدون اشارهٔ ضمانتی از '''قابلیت عرضه''' یا ''' صلاحیت برای یک هدف خاص'''.\nبرای جزئیات بیش‌تر پروانه جامع همگانی گنو را مشاهده کنید.\n\nشما باید <doclink href=Copying> یک نگارش ازمجوز عمومی کلی </doclink> همراه این برنامه دریافت کرده باشید. در غیر این صورت با بنیاد نرم‌افزار آزاد، ایالات متحده امریکا، بوستون، خیابان فرانکلین، پلاک ۵۱، طبقه پنجم، صندوق پستی MA۰۲۱۱۰-۱۳۰ مکاتبه کنید، یا [https://www.gnu.org/copyleft/gpl.html در این‌جا به صورت برخط بخوانید].",
"config-sidebar": "* [https://www.mediawiki.org وةڵگة اصلی مدیاویکی]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents راهنمای کاربر]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents راهنمای مدیر]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ پرسش‌های رایج]\n----\n* <doclink href=Readme>مرا بخوان</doclink>\n* <doclink href=ReleaseNotes>یادداشت‌های انتشار</doclink>\n* <doclink href=Copying>نسخه برداری</doclink>\n* <doclink href=UpgradeDoc>ارتقا</doclink>",
"config-env-good": "محیط بررسی شده‌است.\nشما می‌توانید مدیاویکی را نصب کنید.",
"config-env-bad": "محیط بررسی شده‌است.\nشما نمی‌توانید مدیاویکی را نصب کنید.",
"config-env-php": "پی‌اچ‌پی $1 نصب شده‌است.",
"config-env-hhvm": "HHVM $1 نصب شده‌است.",
- "config-unicode-using-intl": "برای یونیکد عادی از [http://pecl.php.net/intl intl PECL extension] استفاده کنید.",
- "config-unicode-pure-php-warning": "'''هشدار:''' [http://pecl.php.net/intl intl PECL extension] برای کنترل یونیکد عادی در دسترس نیست،اجرای کاملاً آهسته به تعویق می‌افتد.\nاگر شما یک سایت پر‌ ترافیک را اجرا می‌کنید، باید کمی [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization] را بخوانید.",
+ "config-unicode-using-intl": "برای یونیکد عادی از [https://pecl.php.net/intl intl PECL extension] استفاده کنید.",
+ "config-unicode-pure-php-warning": "'''هشدار:''' [https://pecl.php.net/intl intl PECL extension] برای کنترل یونیکد عادی در دسترس نیست،اجرای کاملاً آهسته به تعویق می‌افتد.\nاگر شما یک سایت پر‌ ترافیک را اجرا می‌کنید، باید کمی [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization] را بخوانید.",
"config-unicode-update-warning": "'''هشدار:''' نسخهٔ نصب شدهٔ پوشهٔ یونیکد عادی از ورژن قدیمی‌تر کتابخانه [http://site.icu-project.org/ the ICU project's] استفاده می‌کند.\nاگر کلاً علاقه‌مند به استفاده از یونیکد هستید باید [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations upgrade].",
"config-no-db": "درایور پایگاه اطلاعاتی مناسب پیدا نشد! شما لازم دارید یک درایور پایگاه اطلاعاتی برای پی‌اچ‌پی نصب کنید.انواع پایگاه اطلاعاتی زیر پشتیبانی شده‌اند:$1.\nاگر شما در گروه اشتراک‌گذاری هستید، از تهیه کنندهٔ گروه خود برای نصب یک درایور پایگاه اطلاعاتی مناسب {{PLURAL:$2|سوأل کنید.|پرسش کنید.}}\nاگر خود، پی‌اچ‌پی را تهیه کرده‌اید، با یک پردازشگر فعال دوباره پیکربندی کنید، برای مثال از <code>./configure --with-mysqli</code> استفاده کنید.\nاگر پی‌اچ‌پی را از یک بستهٔ دبیان یا آبونتو نصب کرده‌اید، بنابراین لازم دارید بخش php5-mysql را نصب کنید.",
"config-outdated-sqlite": "''' هشدار:''' شما اس‌کیولایت $1 دارید، که پایین‌تر از حداقل نسخهٔ $2 مورد نیاز است.اس‌کیولایت در دسترس نخواهد بود.",
diff --git a/www/wiki/includes/installer/i18n/lt.json b/www/wiki/includes/installer/i18n/lt.json
index 07e4c907..4506139f 100644
--- a/www/wiki/includes/installer/i18n/lt.json
+++ b/www/wiki/includes/installer/i18n/lt.json
@@ -52,10 +52,9 @@
"config-env-hhvm": "HHVM $1 yra įdiegtas.",
"config-outdated-sqlite": "<strong>Įspėjimas:</strong> jūs turite SQLite $1, kuri yra mažesnė nei minimali reikalinga versija $2. SQLite nebus prieinama.",
"config-memory-raised": "PHP <code>memory_limit</code> yra $1, padidintas iki $2.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] yra įdiegtas",
"config-apc": "[http://www.php.net/apc APC] yra įdiegtas",
"config-apcu": "[http://www.php.net/apcu APCu] yra įdiegtas",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] yra įdiegtas",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] yra įdiegtas",
"config-no-cache-apcu": "<strong>Įspėjimas:</strong> Nepavyko rasti [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] or [http://www.iis.net/download/WinCacheForPhp WinCache].\nObjekto spartinimas neįjungtas.",
"config-diff3-bad": "GNU diff3 nerastas.",
"config-git": "Rasta Git versijų kontrolės sistema: <code>$1</code>.",
diff --git a/www/wiki/includes/installer/i18n/mg.json b/www/wiki/includes/installer/i18n/mg.json
index 2a270fc3..44930c51 100644
--- a/www/wiki/includes/installer/i18n/mg.json
+++ b/www/wiki/includes/installer/i18n/mg.json
@@ -37,14 +37,14 @@
"config-help-restart": "Tianao hofafana avokoa ve ny data voaangona natsofokao ary hamerina ny fizotran'ny fametrahana ?",
"config-restart": "Eny, avereno atao",
"config-welcome": "=== Fanamarinana mikasika ny tontolo ===\nNy fanamarihana tsotsotra dia atao hijerena raha mety ho ana rindrankajy Mediawiki ny tontolo.\nTadidio ny mametraka ireto torohay ireo raha mitady fanohanana mikasika ny fomba famaranana ny fametrahana ianao.",
- "config-copyright": "== Zom-pamorona ary fepetra ==\n\n$1\n\n\nIo fandaharana dia rindrambaiko maimaim-poana; dia afaka zarazarain ary ovaina araka ny fepetra ao amin'ny GNU General Public License navoakan'ny Free Software Foundation; na versiona 2 ao amin'ny lisansa, na (araka ny safidinao) versiona tatỳ aoriana.\n\nIo fandaharaa io dia zaraina amin'ny fanantenana fa ho ilaina, anefa kosa dia <strong>tsy misy fiantohana</strong>; tsy misy fiantohana mikasika ny <strong>fivarotana azy</strong> na <strong>famendrehana ho azo ampiasaina amin'ny tranga iray manokana</strong>.\nJereo ny GNU General Public License hahazoana zavatra amin'ny antsipiriany.\n\nIanao dia tokony nandray <doclink href=Copying> kôpian'nyGNU General Public License </doclink> miaraka amin'ny fandaharana ity; raha tsy izany, manorata any amin'ny Free Software Foundation, Inc., 51 Franklin Street, Fahadimy Floor, Boston, MA 02110-1301, USA, na [http://www.gnu.org/copyleft/gpl.html vakio ao amin'ny Internet izany].",
+ "config-copyright": "== Zom-pamorona ary fepetra ==\n\n$1\n\n\nIo fandaharana dia rindrambaiko maimaim-poana; dia afaka zarazarain ary ovaina araka ny fepetra ao amin'ny GNU General Public License navoakan'ny Free Software Foundation; na versiona 2 ao amin'ny lisansa, na (araka ny safidinao) versiona tatỳ aoriana.\n\nIo fandaharaa io dia zaraina amin'ny fanantenana fa ho ilaina, anefa kosa dia <strong>tsy misy fiantohana</strong>; tsy misy fiantohana mikasika ny <strong>fivarotana azy</strong> na <strong>famendrehana ho azo ampiasaina amin'ny tranga iray manokana</strong>.\nJereo ny GNU General Public License hahazoana zavatra amin'ny antsipiriany.\n\nIanao dia tokony nandray <doclink href=Copying> kôpian'nyGNU General Public License </doclink> miaraka amin'ny fandaharana ity; raha tsy izany, manorata any amin'ny Free Software Foundation, Inc., 51 Franklin Street, Fahadimy Floor, Boston, MA 02110-1301, USA, na [https://www.gnu.org/copyleft/gpl.html vakio ao amin'ny Internet izany].",
"config-sidebar": "* [https://www.mediawiki.org MediaWiki fandraisana]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Torolalan'ny mampiasa]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Torolalan'ny mpandrindra]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Fanontaniana mipetraka matetika]\n----\n* <doclink href=Readme>Vakio aho</doclink>\n* <doclink href=ReleaseNotes>Naoty famoahana</doclink>\n* <doclink href=Copying>Fandikàna</doclink>\n* <doclink href=UpgradeDoc>Fampihatsaràna</doclink>",
"config-env-good": "Voamarina ny tontolo.\nAfaka apetrakao i MediaWiki.",
"config-env-bad": "Voamarina ny tontolo.\nTsy afaka mametraka an'i MediaWiki ianao.",
"config-env-php": "Misy ato PHP $1.",
"config-env-hhvm": "Misy ato HHVM $1.",
- "config-unicode-using-intl": "Mampiasa ny [http://pecl.php.net/intl itatra PECL intl] ho an'ny fampifenerana Unicode.",
- "config-unicode-pure-php-warning": "<strong>Fampitandremana: </strong> Ny [http://pecl.php.net/intl itatra PECL intl] dia tsy misy mba hahazakana ny fampifenerana Unicode, ka mitontona amin'ny implementasiona PHP ranoray noho ny tsifisiany.\nRaha hametraka tranonkala be mpamangy ianao dia tokony mamaky ny [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (amin'ny teny anglisy)",
+ "config-unicode-using-intl": "Mampiasa ny [https://pecl.php.net/intl itatra PECL intl] ho an'ny fampifenerana Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Fampitandremana: </strong> Ny [https://pecl.php.net/intl itatra PECL intl] dia tsy misy mba hahazakana ny fampifenerana Unicode, ka mitontona amin'ny implementasiona PHP ranoray noho ny tsifisiany.\nRaha hametraka tranonkala be mpamangy ianao dia tokony mamaky ny [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (amin'ny teny anglisy)",
"config-db-type": "Karazana banky angona:",
"config-db-host": "Anaran'ny lohamilin'ny banky angona:",
"config-db-host-help": "Raha lohamila hafa ny lohamilin'ny banky angona, dia atsofohy eto ny anaran'ilay lohamilina na ny adiresy IP-ny.\n\nRaha mampiasa fampiantranoana iombonana ianao dia tokony hanome anao ny anaran-dohamilina izy ao amin'ny toromariny.\n\nRaha mametraka amin'ny lohamilina Windows ianao sady mampiasa MySQL, dia mety tsy mandeha ny anaran-dohamilina \"localhost\". Raha tsy mandeha ilay izy dia \"127.0.0.1\" no atao adiresy IP an-toerana.\n\nRaha mampiasa PostgreSQL ianao, dia avelaho ho fotsy ity saha ity ahafahana mifandray amin'ny alalan'ny socket Unix.",
diff --git a/www/wiki/includes/installer/i18n/mk.json b/www/wiki/includes/installer/i18n/mk.json
index d028a8c4..a9ae2fe1 100644
--- a/www/wiki/includes/installer/i18n/mk.json
+++ b/www/wiki/includes/installer/i18n/mk.json
@@ -44,14 +44,14 @@
"config-help-restart": "Дали сакате да ги исчистите сите зачувани податоци што ги внесовте и да ја започнете воспоставката одново?",
"config-restart": "Да, почни одново",
"config-welcome": "=== Проверки на околината ===\nСега ќе се извршиме основни проверки за да се востанови дали околината е погодна за воспоставкa на МедијаВики. Не заборавајте да ги приложите овие информации ако барате помош со довршување на воспоставката.",
- "config-copyright": "=== Авторски права и услови ===\n\n$1\n\nОва е слободна програмска опрема (free software); можете да го редистрибуирате и/или менувате согласно условите на ГНУ-овата општа јавна лиценца (GNU General Public License) на Фондацијата за слободна програмска опрема (Free Software Foundation); верзија 2 или било која понова верзија на лиценцата (по ваш избор).\n\nОвој програм се нуди со надеж дека ќе биде корисен, но '''без никаква гаранција'''; дури ни подразбраната гаранција за '''продажна способност''' или '''погодност за определена цел'''.\nПовеќе информации ќе најдете во текстот на ГНУ-овата општа јавна лиценца.\n\nБи требало да имате добиено <doclink href=Copying>примерок од ГНУ-овата општа јавна лиценца</doclink> заедно со програмов; ако немате добиено, тогаш пишете ни на Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. или [http://www.gnu.org/copyleft/gpl.html прочитајте ја тука].",
+ "config-copyright": "=== Авторски права и услови ===\n\n$1\n\nОва е слободна програмска опрема (free software); можете да го редистрибуирате и/или менувате согласно условите на ГНУ-овата општа јавна лиценца (GNU General Public License) на Фондацијата за слободна програмска опрема (Free Software Foundation); верзија 2 или било која понова верзија на лиценцата (по ваш избор).\n\nОвој програм се нуди со надеж дека ќе биде корисен, но '''без никаква гаранција'''; дури ни подразбраната гаранција за '''продажна способност''' или '''погодност за определена цел'''.\nПовеќе информации ќе најдете во текстот на ГНУ-овата општа јавна лиценца.\n\nБи требало да имате добиено <doclink href=Copying>примерок од ГНУ-овата општа јавна лиценца</doclink> заедно со програмов; ако немате добиено, тогаш пишете ни на Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. или [https://www.gnu.org/copyleft/gpl.html прочитајте ја тука].",
"config-sidebar": "* [https://www.mediawiki.org Домашна страница на МедијаВики]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Водич за корисници]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Водич за администратори]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ ЧПП]\n----\n* <doclink href=Readme>Прочитај ме</doclink>\n* <doclink href=ReleaseNotes>Белешки за изданието</doclink>\n* <doclink href=Copying>Копирање</doclink>\n* <doclink href=UpgradeDoc>Надградување</doclink>",
"config-env-good": "Околината е проверена.\nМожете да го воспоставите МедијаВики.",
"config-env-bad": "Околината е проверена.\nНе можете да го воспоставите МедијаВики.",
"config-env-php": "PHP $1 е воспоставен.",
"config-env-hhvm": "HHVM $1 е воспоставен.",
- "config-unicode-using-intl": "Со додатокот [http://pecl.php.net/intl intl PECL] за уникодна нормализација.",
- "config-unicode-pure-php-warning": "'''Предупредување''': Додатокот [http://pecl.php.net/intl intl PECL] не е достапен за врши уникодна нормализација, враќајќи се на бавна примена на чист PHP.\n\nАко имате високопрометно мрежно место, тогаш ќе треба да прочитате повеќе за [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations уникодната нормализација].",
+ "config-unicode-using-intl": "Со додатокот [https://pecl.php.net/intl intl PECL] за уникодна нормализација.",
+ "config-unicode-pure-php-warning": "'''Предупредување''': Додатокот [https://pecl.php.net/intl intl PECL] не е достапен за врши уникодна нормализација, враќајќи се на бавна примена на чист PHP.\n\nАко имате високопрометно мрежно место, тогаш ќе треба да прочитате повеќе за [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations уникодната нормализација].",
"config-unicode-update-warning": "'''Предупредување:''' Воспоставената верзија на обвивката за уникодна нормализација користи постара верзија на библиотеката на [http://site.icu-project.org/ проектот ICU].\nЗа да користите Уникод, ќе треба да направите [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations надградба].",
"config-no-db": "Не можев да најдам соодветен двигател за базата на податоци! Ќе треба да воспоставите двигател за PHP-база.\n{{PLURAL:$2|Поддржан се следниов вид|Поддржани се следниве видови}} бази: $1.\n\nДоколку самите го срочивте овој PHP, овозможете го базниот клиент во поставките — на пр. со <code>./configure --with-mysqli</code>.\nАко овој PHP го воспоставите од пакет на Debian или Ubuntu, тогаш ќе треба исто така да го воспоставите, на пр., пакетот <code>php5-mysql</code>.",
"config-outdated-sqlite": "'''Предупредување''': имате SQLite $1. Најстарата допуштена верзија е $2. Затоа, SQLite ќе биде недостапен.",
@@ -60,12 +60,11 @@
"config-pcre-no-utf8": "<strong>Кобно</strong>: PCRE-модулот на PHP е срочен без поддршка за PCRE_UTF8.\nМедијаВики бара поддршка за UTF-8 за да може да работи правилно.",
"config-memory-raised": "<code>memory_limit</code> за PHP изнесува $1, зголемен на $2.",
"config-memory-bad": "<strong>Предупредување:</strong> <code>memory_limit</code> за PHP изнесува $1.\nОва е веројатно премалку.\nВоспоставката може да не успее!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] е воспоставен",
"config-apc": "[http://www.php.net/apc APC] е воспоставен",
"config-apcu": "[http://www.php.net/apcu APCu] е воспоставен",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] е воспоставен",
- "config-no-cache-apcu": "<strong>Предупредување:</strong> Не можев да го најдам [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] или [http://www.iis.net/download/WinCacheForPhp WinCache].\nМеѓускладирањето на објекти не е овозможено",
- "config-mod-security": "'''Предупредување''': на вашиот опслужувач има овозможено [http://modsecurity.org/ mod_security]. Ако не е поставено како што треба, ова може да предизвика проблеми кај МедијаВики и други програми што им овозможуваат на корисниците да објавуваат произволни содржини.\nПогледнете ја [http://modsecurity.org/documentation/ mod_security документацијата] или обратете се кај домаќинот ако наидете на случајни грешки.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] е воспоставен",
+ "config-no-cache-apcu": "<strong>Предупредување:</strong> Не можев да го најдам [http://www.php.net/apcu APCu] или [http://www.iis.net/download/WinCacheForPhp WinCache].",
+ "config-mod-security": "'''Предупредување''': на вашиот опслужувач има овозможено [https://modsecurity.org/ mod_security]. Ако не е поставено како што треба, ова може да предизвика проблеми кај МедијаВики и други програми што им овозможуваат на корисниците да објавуваат произволни содржини.\nПогледнете ја [https://modsecurity.org/documentation/ mod_security документацијата] или обратете се кај домаќинот ако наидете на случајни грешки.",
"config-diff3-bad": "GNU diff3 не е пронајден.",
"config-git": "Го пронајдов Git програмот за контрола на верзии: <code>$1</code>.",
"config-git-bad": "Не го пронајдов Git-програмот за контрола на верзии.",
@@ -223,14 +222,14 @@
"config-license-gfdl": "ГНУ-ова лиценца за слободна документација 1.3 или понова",
"config-license-pd": "Јавна сопственост",
"config-license-cc-choose": "Одберете друга лиценца на Криејтив комонс по ваш избор",
- "config-license-help": "Многу јавни викија ги ставаат сите придонеси под [http://freedomdefined.org/Definition слободна лиценца].\nСо ова се создава атмосфера на општа сопственост и поттикнува долгорочно учество.\nОва не е неопходно за викија на поединечни физички или правни лица.\n\nАко сакате да користите текст од Википедија, и сакате Википедија да прифаќа текст прекопиран од вашето вики, тогаш треба да ја одберете лиценцата <strong>{{int:config-license-cc-by-sa}}</strong>..\n\nГНУ-овата лиценца за слободна документација (ГЛСД) е старата лиценца на Википедија.\nОваа лиценца сè уште важи, но е тешка за разбирање.\nИсто така треба да се има на ум дека пренамената на содржините под ГЛСД не е лесна.",
+ "config-license-help": "Многу јавни викија ги ставаат сите придонеси под [https://freedomdefined.org/Definition слободна лиценца].\nСо ова се создава атмосфера на општа сопственост и поттикнува долгорочно учество.\nОва не е неопходно за викија на поединечни физички или правни лица.\n\nАко сакате да користите текст од Википедија, и сакате Википедија да прифаќа текст прекопиран од вашето вики, тогаш треба да ја одберете лиценцата <strong>{{int:config-license-cc-by-sa}}</strong>..\n\nГНУ-овата лиценца за слободна документација (ГЛСД) е старата лиценца на Википедија.\nОваа лиценца сè уште важи, но е тешка за разбирање.\nИсто така треба да се има на ум дека пренамената на содржините под ГЛСД не е лесна.",
"config-email-settings": "Нагодувања за е-пошта",
"config-enable-email": "Овозможи излезна е-пошта",
"config-enable-email-help": "Ако сакате да работи е-поштата, [http://www.php.net/manual/en/mail.configuration.php поштенските нагодувања на PHP] треба да се правилно наместени.\nАко воопшто не сакате никакви функции за е-пошта, тогаш можете да ги оневозможите тука.",
"config-email-user": "Овозможи е-пошта од корисник до корисник",
"config-email-user-help": "Дозволи сите корисници да можат да си праќаат е-пошта ако ја имаат овозможено во нагодувањата.",
"config-email-usertalk": "Овозможи известувања за промени во кориснички страници за разговор",
- "config-email-usertalk-help": "Овозможи корисниците да добиваат известувања за промени во нивните кориснички страници за разговор ако ги имаат овозможено во нагодувањата.",
+ "config-email-usertalk-help": "Овозможи корисниците да добиваат известувања за промени во нивните кориснички разговорни страници ако ги имаат овозможено во нагодувањата.",
"config-email-watchlist": "Овозможи известувања за список на набљудувања",
"config-email-watchlist-help": "Овозможи корисниците да добиваат известувања за нивните набљудувани страници ако ги имаат овозможено во нагодувањата.",
"config-email-auth": "Овозможи потврдување на е-пошта",
@@ -253,7 +252,7 @@
"config-cache-options": "Нагодувања за меѓускладирање на објекти:",
"config-cache-help": "Меѓускладирањето на објекти се користи за зголемување на брзината на МедијаВики со меѓускладирање на често употребуваните податоци.\nОва многу се препорачува на средни до големи викија, но од тоа ќе имаат полза и малите викија.",
"config-cache-none": "Без меѓускладирање (не се остранува ниедна функција, но може да влијае на брзината кај поголеми викија)",
- "config-cache-accel": "Меѓускладирање на PHP-објекти (APC, APCu, XCache или WinCache)",
+ "config-cache-accel": "Меѓускладирање на PHP-објекти (APC, APCu или WinCache)",
"config-cache-memcached": "Користи Memcached (бара дополнително поставување и нагодување)",
"config-memcached-servers": "Memcached-опслужувачи:",
"config-memcached-help": "Список на IP-адреси за употреба во Memcached.\nТреба да се наведе по една во секој ред, како и портата што ќе се користи. На пример:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -309,6 +308,7 @@
"config-install-mainpage-failed": "Не можев да вметнам главна страница: $1",
"config-install-done": "<strong>Честитаме!</strong>\nУспешно го воспоставивте МедијаВики.\n\nВоспоставувачот создаде податотека <code>LocalSettings.php</code>.\nТаму се содржат сите ваши нагодувања.\n\nЌе треба да ја преземете и да ја ставите во основата на воспоставката (истата папка во која се наоѓа index.php). Преземањето треба да е започнато автоматски.\n\nАко не ви е понудено преземање, или пак ако сте го откажале, можете да го почнете одново стискајќи на следнава врска:\n\n$3\n\n<strong>Напомена</strong>: Ако ова не го направите сега, податотеката со поставки повеќе нема да биде на достапна.\n\nОткога ќе завршите со тоа, можете да <strong>[$2 влезете на вашето вики]</strong>.",
"config-install-done-path": "<strong>Честитаме!</strong>\nГо воспоставивте МедијаВики.\n\nВоспоставувачот создаде податотека <code>LocalSettings.php</code>.\nТаму се содржат сите ваши нагодувања.\n\nЌе треба да ја преземете и да ја ставите во <code>$4</code>. Преземањето треба да е започнато автоматски.\n\nАко не ви е понудено преземање, или пак ако сте го откажале, можете да го почнете одново стискајќи на следнава врска:\n\n$3\n\n<strong>Напомена</strong>: Ако ова не го направите сега, создадената податотека со поставки повеќе нема да биде на достапна, освен ако не ја преземете пред да излезете.\n\nОткога ќе завршите со тоа, можете да <strong>[$2 влезете на вашето вики]</strong>.",
+ "config-install-success": "МедијаВики е успешно воспоставен. Сега можете да појдете на <$1$2> за да го погледате вашето вики.\nАко имате било какви прашања, погледајте го списокот на често поставувани прашања:\n<https://www.mediawiki.org/wiki/Manual:FAQ> или појдете на еден од форумите за поддршка наведени на таа страница.",
"config-download-localsettings": "Преземи го <code>LocalSettings.php</code>",
"config-help": "помош",
"config-help-tooltip": "стиснете да расклопите",
diff --git a/www/wiki/includes/installer/i18n/mr.json b/www/wiki/includes/installer/i18n/mr.json
index d3e0e173..8e7cbee3 100644
--- a/www/wiki/includes/installer/i18n/mr.json
+++ b/www/wiki/includes/installer/i18n/mr.json
@@ -42,19 +42,18 @@
"config-help-restart": "आपण टाकून जतन केलेला सर्व डाटा आपणास साफ करावयाचा व उभारणीची प्रक्रिया पुन्हा सुरू करावयाची आहे काय?",
"config-restart": "होय, परत चालू करा",
"config-welcome": "=== पारिसरीक तपासण्या ===\nमिडियाविकिच्या उभारणीस हा परिसर योग्य आहे काय याच्या मूळ तपासण्या आता केल्या जातील.\nजर आपणास पुढे याची उभारणी करण्याबद्दल साहाय्य लागल्यास, याचा अंतर्भाव करणे लक्षात ठेवा.",
- "config-copyright": "=== प्रताधिकार व अटी ===\n\n$1\nहा कार्यसंच,हे एक मुक्त संचेतन आहे;आपण त्यास पुनर्वितरीत व/किंवा त्यास फ्री सॉफ्टवेअर फाऊंडेशन द्वारे प्रकाशित, GNU जनरल पब्लिक लायसन्स अंतर्गत बदलु शकता;या परवान्याची आवृत्ती २ किंवा (आपल्या इच्छेनुसार)त्यानंतरची आवृत्ती.\n\nहा कार्यसंचाचे वितरण,पण, <strong>कोणत्याही हमीशिवाय</strong>; याशिवाय <strong>व्यापारीकरणाच्या</strong> कोणत्याही अभिप्रेत आश्वासनाशिवाय किंवा <strong>एखाद्या विशिष्ट कार्यासाठीच्या अर्हतेशिवाय</strong>ही आशा ठेऊन केले आहे कि, तो उपयोगी असेल.\nअधिक माहितीसाठी GNU जनरल पब्लिक लायसन्स बघा.\nआपणास या कार्यसंचासमवेत <doclink href=Copying>GNU जनरल पब्लिक लायसन्सची प्रत मिळाली असेल,</doclink>नसल्यास,फ्री सॉफ्टवेअर फाऊंडेशनला या पत्त्यावर लिहा.Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. किंवा त्यास [http://www.gnu.org/copyleft/gpl.html ऑनलाईन वाचा].",
+ "config-copyright": "=== प्रताधिकार व अटी ===\n\n$1\nहा कार्यसंच,हे एक मुक्त संचेतन आहे;आपण त्यास पुनर्वितरीत व/किंवा त्यास फ्री सॉफ्टवेअर फाऊंडेशन द्वारे प्रकाशित, GNU जनरल पब्लिक लायसन्स अंतर्गत बदलु शकता;या परवान्याची आवृत्ती २ किंवा (आपल्या इच्छेनुसार)त्यानंतरची आवृत्ती.\n\nहा कार्यसंचाचे वितरण,पण, <strong>कोणत्याही हमीशिवाय</strong>; याशिवाय <strong>व्यापारीकरणाच्या</strong> कोणत्याही अभिप्रेत आश्वासनाशिवाय किंवा <strong>एखाद्या विशिष्ट कार्यासाठीच्या अर्हतेशिवाय</strong>ही आशा ठेऊन केले आहे कि, तो उपयोगी असेल.\nअधिक माहितीसाठी GNU जनरल पब्लिक लायसन्स बघा.\nआपणास या कार्यसंचासमवेत <doclink href=Copying>GNU जनरल पब्लिक लायसन्सची प्रत मिळाली असेल,</doclink>नसल्यास,फ्री सॉफ्टवेअर फाऊंडेशनला या पत्त्यावर लिहा.Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. किंवा त्यास [https://www.gnu.org/copyleft/gpl.html ऑनलाईन वाचा].",
"config-sidebar": "* [https://www.mediawiki.org मिडियाविकि गृह]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents सदस्य मार्गदर्शिका]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents प्रशासकाची मार्गदर्शिका]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ एफएक्यू]\n----\n* <doclink href=Readme>रीड मी</doclink>\n* <doclink href=ReleaseNotes>विमोचन टिप्पण्या</doclink>\n* <doclink href=Copying>नकलविणे</doclink>\n* <doclink href=UpgradeDoc>दर्जोन्नती करणे</doclink>",
"config-env-good": "पारिसरीक तपासणी झाली आहे.\nआपण मिडियाविकि उभारू शकता.",
"config-env-bad": "पारिसरीक तपासणी झाली आहे.\nआपण मिडियाविकि उभारू शकत नाही.",
"config-env-php": "PHP $1 उभारल्या गेली.",
"config-env-hhvm": "HHVM $1 उभारल्या गेली.",
- "config-unicode-using-intl": "यूनिकोड सामान्यिकरणासाठी [http://pecl.php.net/intl intl PECL विस्तारक] वापरत आहे.",
+ "config-unicode-using-intl": "यूनिकोड सामान्यिकरणासाठी [https://pecl.php.net/intl intl PECL विस्तारक] वापरत आहे.",
"config-outdated-sqlite": "<strong>इशारा:</strong> आपणापाशी SQLite $1 आहे, जी किमान आवश्यक आवृत्ती $2 पेक्षा, निम्न आहे. SQLite अनुपलब्ध राहील.",
"config-memory-raised": "पीएचपीची <code>memory_limit</code> ही $1 आहे, त्यास $2 ला वाढविली.",
"config-memory-bad": "पीएचपीची <code>memory_limit</code> ही $1 आहे.\nही बरीच खालच्या स्तरावरची आहे.\nउभारणी अयशस्वी होऊ शकते!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] उभारली",
"config-apc": "[http://www.php.net/apc APC] उभारली आहे",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] उभारली आहे",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] उभारली आहे",
"config-diff3-bad": "GNU diff3 सापडली नाही.",
"config-git-bad": "गीट आवृत्ती नियमन संचेतन सापडली नाही.",
"config-db-type": "डाटाबेसचा प्रकार:",
diff --git a/www/wiki/includes/installer/i18n/ms.json b/www/wiki/includes/installer/i18n/ms.json
index 61acd3ec..c848c423 100644
--- a/www/wiki/includes/installer/i18n/ms.json
+++ b/www/wiki/includes/installer/i18n/ms.json
@@ -47,21 +47,20 @@
"config-help-restart": "Adakah anda ingin untuk membersihkan semua data yang disimpan yang anda telah masukkan dan memulakan semula proses pemasangan?",
"config-restart": "Ya, mula semula",
"config-welcome": "=== Pemeriksaan persekitaran ===\nPemeriksaan asas kini boleh dilakukan untuk melihat jika persekitaran ini adalah sesuai untuk pemasangan MediaWiki.\nIngat untuk memasukkan maklumat ini jika anda mahukan sokongan tentang bagaimana untuk menyelesaikan pemasangan.",
- "config-copyright": "=== Hakcipta dan Syarat-Syarat ===\n\n$1\n\nProgram ini merupakan perisian bebas; anda boleh mengedarkannya semula dan/atau mengubahsuainya di bawah syarat-syarat Lesen Awam GNU seperti yang diterbitkan oleh Yayasan Perisian Bebas; sama ada versi 2 Lesen ini atau (mengikut pilihan anda) mana-mana versi selepas ini.\n\nProgram ini diedarkan dengan harapan bahawa ia akan menjadi berguna, tetapi '''tanpa sebarang waranti'''; tanpa jaminan yang tersirat '''kebolehdagangan''' atau '''kesesuaian untuk tujuan tertentu'''.\nLihat Lesen Awam GNU untuk maklumat lanjut.\n\nAnda sepatutnya telah menerima <doclink href=Copying> satu salinan Lesen Awam GNU </doclink> bersama-sama dengan program ini, jika tidak, menulis surat kepada Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, atau [http://www.gnu.org/copyleft/gpl.html membacanya dalam talian].",
+ "config-copyright": "=== Hakcipta dan Syarat-Syarat ===\n\n$1\n\nProgram ini merupakan perisian bebas; anda boleh mengedarkannya semula dan/atau mengubahsuainya di bawah syarat-syarat Lesen Awam GNU seperti yang diterbitkan oleh Yayasan Perisian Bebas; sama ada versi 2 Lesen ini atau (mengikut pilihan anda) mana-mana versi selepas ini.\n\nProgram ini diedarkan dengan harapan bahawa ia akan menjadi berguna, tetapi '''tanpa sebarang waranti'''; tanpa jaminan yang tersirat '''kebolehdagangan''' atau '''kesesuaian untuk tujuan tertentu'''.\nLihat Lesen Awam GNU untuk maklumat lanjut.\n\nAnda sepatutnya telah menerima <doclink href=Copying> satu salinan Lesen Awam GNU </doclink> bersama-sama dengan program ini, jika tidak, menulis surat kepada Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, atau [https://www.gnu.org/copyleft/gpl.html membacanya dalam talian].",
"config-sidebar": "* [https://www.mediawiki.org Laman utama MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Panduan Pengguna]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Panduan Penyelia]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Soalan lazim]\n----\n* <doclink href=Readme>Baca saya</doclink>\n* <doclink href=ReleaseNotes>Nota keluaran</doclink>\n* <doclink href=Copying>Menyalin</doclink>\n* <doclink href=UpgradeDoc>Menaik taraf</doclink>",
"config-env-good": "Persekitaran telah diperiksa.\nAnda boleh memasang MediaWiki.",
"config-env-bad": "Persekitaran telah diperiksa. \nAnda tidak boleh memasang MediaWiki.",
"config-env-php": "PHP $1 dipasang.",
- "config-unicode-using-intl": "[http://pecl.php.net/intl Sambungan intl PECL] digunakan untuk penormalan Unicode.",
+ "config-unicode-using-intl": "[https://pecl.php.net/intl Sambungan intl PECL] digunakan untuk penormalan Unicode.",
"config-unicode-update-warning": "<strong>Amaran:</strong> Versi pembalut penormalan Unicode yang terpasang menggunakan perpustakaan [http://site.icu-project.org/ projek ICU] dalam versi yang lampau.\nAnda harus [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations menaik taraf] jika Unicode penting bagi anda.",
"config-outdated-sqlite": "<strong>Amaran:</strong> anda mempunyai SQLite $1 yang lebih rendah daripada versi keperluan minimum $1. SQLite tidak akan disediakan.",
"config-no-fts3": "<strong>Amaran:</strong> SQLite disusun tanpa [//sqlite.org/fts3.html modil FTS3], maka ciri-ciri pencarian tidak akan disediakan pada backend ini.",
"config-pcre-old": "<strong>Amaran keras:</strong> PCRE $1 ke atas diperlukan.\nBinari PHP anda berpaut dengan PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Keterangan lanjut].",
"config-memory-bad": "<strong>Amaran:</strong> <code>memory_limit</code> (Had memori) PHP ialah $1.\nIni mungkin terlalu rendah.\nPemasangan mungkin akan gagal!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] dipasang",
"config-apc": "[http://www.php.net/apc APC] dipasang",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] dipasang",
- "config-mod-security": "<strong>Amaran:</strong> Pelayan web anda dihidupkan [http://modsecurity.org/ mod_security]/mod_security2. Kebanyakan konfigurasinya yang umum boleh menimbulkan kesulitan untuk MediaWiki dan perisian-perisian lain yang membolehkan pengguna untuk mengeposkan kandungan yang sewenang-wenang.\nJika boleh, ciri-ciri ini harus dimatikan. Jika tidak, rujuki [http://modsecurity.org/documentation/ dokumentasi mod_security] atau hubungi bantuan hos anda jika anda menghadapi ralat sembarangan.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] dipasang",
+ "config-mod-security": "<strong>Amaran:</strong> Pelayan web anda dihidupkan [https://modsecurity.org/ mod_security]/mod_security2. Kebanyakan konfigurasinya yang umum boleh menimbulkan kesulitan untuk MediaWiki dan perisian-perisian lain yang membolehkan pengguna untuk mengeposkan kandungan yang sewenang-wenang.\nJika boleh, ciri-ciri ini harus dimatikan. Jika tidak, rujuki [https://modsecurity.org/documentation/ dokumentasi mod_security] atau hubungi bantuan hos anda jika anda menghadapi ralat sembarangan.",
"config-diff3-bad": "GNU diff3 tidak dijumpai.",
"config-git": "Perisian kawalan versi Git dijumpai: <code>$1</code>.",
"config-git-bad": "Perisian kawalan versi Git tidak dijumpai.",
diff --git a/www/wiki/includes/installer/i18n/mzn.json b/www/wiki/includes/installer/i18n/mzn.json
index b8218463..1763fcde 100644
--- a/www/wiki/includes/installer/i18n/mzn.json
+++ b/www/wiki/includes/installer/i18n/mzn.json
@@ -29,12 +29,11 @@
"config-env-bad": "محیط بررسی بیه.\nشما نتوندی مدیاویکی ره نصب هاکنی.",
"config-env-php": "پی‌اچ‌پی $1 نصب بیه.",
"config-env-hhvm": "اچ‌اچ‌وی‌ام $1 نصب بیه.",
- "config-unicode-using-intl": "عادی یونیکد وسه [http://pecl.php.net/intl افزونهٔ intl برای PECL] جه استفاده هاکن.",
+ "config-unicode-using-intl": "عادی یونیکد وسه [https://pecl.php.net/intl افزونهٔ intl برای PECL] جه استفاده هاکن.",
"config-memory-raised": "PHP's <code>memory_limit</code>, نسخهٔ $1 هسته، ونه نسخهٔ $2 ره بَیری آپگریت هاکنی.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] نصب بیه.",
"config-apc": "[http://www.php.net/apc APC] نصب بیه.",
"config-apcu": "[http://www.php.net/apcu APCu] نصب بیه.",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] نصب بیه.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] نصب بیه.",
"config-diff3-bad": "GNU diff3 پیدا نیه.",
"config-mysql-binary": "باینری",
"config-mysql-utf8": "UTF-8",
diff --git a/www/wiki/includes/installer/i18n/nan.json b/www/wiki/includes/installer/i18n/nan.json
index 697b9fb0..953b672a 100644
--- a/www/wiki/includes/installer/i18n/nan.json
+++ b/www/wiki/includes/installer/i18n/nan.json
@@ -2,7 +2,8 @@
"@metadata": {
"authors": [
"Ianbu",
- "唐吉訶德的侍從"
+ "唐吉訶德的侍從",
+ "Yoxem"
]
},
"config-desc": "MediaWiki的安裝程式",
@@ -13,7 +14,7 @@
"config-localsettings-key": "Seng-kip--ê bi̍t-bé:",
"config-localsettings-badkey": "Lí phah--ê bi̍t-bé bô chèng-khak.",
"config-upgrade-key-missing": "已經有一个MediaWiki矣。若要升級,請共下面這逝加去<code>LocalSettings.php</code>的下跤:\n\n$1",
- "config-localsettings-incomplete": "這馬的<code>LocalSettings.php</code>可能無齊全,因為無設變量$1。請佇<code>LocalSettings.php</code>設彼个變量,並且揤「{{int:Config-continue}}」。",
+ "config-localsettings-incomplete": "Chit-má--ê <code>LocalSettings.php</code> khó-lêng bô chiâu-chn̂g, in-ūi bô siat piān-liōng $1. Chhiáⁿ tī <code>LocalSettings.php</code> siat hit-ê piān-liōng, pēng-chhiá chhi̍h \"{{int:Config-continue}}\".",
"config-localsettings-connection-error": "An error was encountered when connecting to the database 用<code>LocalSettings.php</code>的設定去連接資料庫的時陣有一个錯誤發生,請改遮的設定了,才閣試。\n\n$1",
"config-session-error": "連線開始了的錯誤:$1",
"config-session-expired": "你連線資料已經過時矣,連線的使用期限是設做$1。你會使改共php.ini的<code>session.gc_maxlifetime</code>改較長,並且重新安裝動作。",
@@ -42,17 +43,17 @@
"config-help-restart": "你敢欲共你拍的佮保存的資料攏清掉,並且重開始安裝的動作?",
"config-restart": "是,重來",
"config-welcome": "=== 環境檢測 ===\n這馬欲做基本的檢測,看環境是毋是適合裝 MediaWiki。\n若你愛有支援,才裝會起來,請共遮的資訊記起來。",
- "config-copyright": "=== 版權聲明佮授權條款 ===\n\n$1\n\n本程式是自由軟體;你會當照自由軟體基金會所發表的 GNU 通用公共授權條款規定,共本程式重新發佈抑是修改;無論你是照本授權條款的第二版抑第二版以後的任何版本(你會當家己選) 。\n\n本程式發佈的目的是希望會當提供幫助,但是 <strong>無負任何擔保責任</strong>;抑無表示講對 <strong>販賣性</strong> 抑 <strong>特定用途的適用性</strong> 的情形擔保。詳情請參照 GNU 通用公共授權。\n\n你應該已隨本程式收著 <doclink href=\"Copying\">GNU 通用公共授權條款的副本</doclink>;若無,請寫批通知自由軟體基金會,51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA,或 [http://www.gnu.org/copyleft/gpl.html 線頂看]。",
+ "config-copyright": "=== Pán-koân seng-bêng kap siū-koân tiâu-khoán ===\n\n$1\n\nPún têng-sek sī chū-iû nńg-thé; lí thang chiàu Chū-iû Nńg-thé Ki-kim-hoē só͘ hoat-piáu--ê GNU Thong-iōng Kong-kiōng Siū-koân Tiâu-khoán kui-tēng, kā pún têng-sek têng hoat-pò͘ iah-sī siu-kái; bô-lūn lí sī chiàu pún siū-koân tiâu-khoán--ê tē 2 pán iah koh khah sin--ê pán-pún (lí thang ka-kī kéng).\n\nPún têng-sek hoat-pò͘--ê bo̍k-tek sī ǹg-bāng ē-tàng pang-chān, m̄-koh <strong>bô hù jīm-hô tam-pó͘ chek-jīm</strong>; iah bô piáu-sī kóng tùi <strong>hoàn-bē-sèng</strong> iah <strong>te̍k-tēng iōng-tô͘--ê sek-iōng-sèng</strong>--ê chêng-hêng tam-pó͘. Siông-sè chhiáⁿ chham-khó GNU Thong-iōng Kong-kiōng Siū-koân.\n\nLí èng-kai tùi pún têng-sek siu-tio̍h <doclink href=\"Copying\">GNU Thong-iōng Kong-kiōng Siū-koân--ê Hù-pún</doclink>; nā-bô, chhiáⁿ siá-phoe thong-tī Chū-iû Nńg-thé KI-kim-hoē, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, iah-sī [https://www.gnu.org/copyleft/gpl.html soàⁿ-téng khoàⁿ].",
"config-sidebar": "* [www.mediawiki.org/wiki/MediaWiki/zh-hant MediaWiki 頭頁]\n* [www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/zh 使用者指南]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/zh 管理者指南]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/zh-hant 四常問題集]\n----\n* <doclink href=Readme>讀我說明</doclink>\n* <doclink href=ReleaseNotes>發行說明</doclink>\n* <doclink href=Copying>版權聲明</doclink>\n* <doclink href=UpgradeDoc>升級</doclink>",
"config-env-good": "環境檢查已完成。\n你會當安裝 MediaWiki。",
- "config-env-bad": "環境檢查已完成。\n你無法度安裝 MediaWiki。",
+ "config-env-bad": "Khoân-kèng kiám-cha oân-sêng--ah.\nLí bô-hoat-tō͘ an-chng MediaWiki.",
"config-env-php": "PHP $1 已經安裝。",
- "config-unicode-using-intl": "用 [http://pecl.php.net/intl intl PECL 擴充套件] 做 Unicode 正規化。",
- "config-unicode-pure-php-warning": "<strong>警告:</strong> 無法度用 [http://pecl.php.net/intl intl PECL 擴充套件] 處理 Unicode 正規化,所以退回用純 PHP 實作的正規化程式,這種方式處理速度較慢。\n\n若你的網站瀏覽人數誠濟,你應該先看 [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations/zh Unicode 正規化]。",
+ "config-unicode-using-intl": "用 [https://pecl.php.net/intl intl PECL 擴充套件] 做 Unicode 正規化。",
+ "config-unicode-pure-php-warning": "<strong>Kéng-kò:</strong> Bô-hoat-tō͘ iōng [https://pecl.php.net/intl intl PECL extension] chhú-lí Unicode chèng-kui-hoà, só͘-í thè kàu iōng sûn PHP si̍t-chok--ê chèng-kui-hoà têng-sek, chit khoán hong-sek chhú-lí sok-tō͘ khah bān. Nā lí--ê bāng-chām liú-lám--ê lâng chiâⁿ chē, lí èng-kai ài khoàⁿ [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations/zh Unicode chèng-kui-hoà].",
"config-unicode-update-warning": "<strong>警告</strong>:這馬安裝的 Unicode 正規化包裝程式用舊版 [http://site.icu-project.org/ ICU 計劃] 的程式庫。\n若你需要用 Unicode,你應該先進行 [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations 升級]。",
"config-no-db": "揣無適合的資料庫驅動程式!你需要安裝 PHP 資料庫驅動程式。\n這馬支援下跤類型的資料庫: $1 。\n\n若你是家己編譯 PHP,你需要重新設定並且開資料庫客戶端,譬如:用 <code>./configure --with-mysqli</code> 指令參數。\n如你是用 Debian 或 Ubuntu 的套件安裝,你著需要閣另外安裝,例:<code>php5-mysql</code> 套件。",
- "config-outdated-sqlite": "<strong>警告:</strong>你已經安裝 SQLite $1,毋閣伊的版本比會當裝的版本 $2閣較舊。所以你無法度用 SQLite。",
- "config-no-fts3": "<strong>警告:</strong> SQLite 佇編譯的時陣無包括 [//sqlite.org/fts3.html FTS3 模組],後台搜揣功能就會無法度用。",
+ "config-outdated-sqlite": "<strong>Kéng-kò:</strong> Lí í-keng an-chng SQLite $1, m̄-koh i--ê pán-pún pí thang-chng--ê pán-pún $2 khah kū. Só͘-í lí bô-hoat-tō͘ ēng SQLite.",
+ "config-no-fts3": "<strong>Kéng-kò: </strong> SQLite tī pian-e̍k--ê sî-chūn bô pau-koat [//sqlite.org/fts3.html FTS3 module], āu-tâi chhiau-chhoē kong-lêng tiō ē bô-hoat-tō͘ iōng.",
"mainpagetext": "'''MediaWiki已經裝好矣。'''",
"mainpagedocfooter": "請查看[https://meta.wikimedia.org/wiki/Help:Contents 用者說明書]的資料通使用wiki 軟體\n\n== 入門 ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings 配置的設定]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki時常問答]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki的公布列單]"
}
diff --git a/www/wiki/includes/installer/i18n/nap.json b/www/wiki/includes/installer/i18n/nap.json
index 84fb9a3c..fa412573 100644
--- a/www/wiki/includes/installer/i18n/nap.json
+++ b/www/wiki/includes/installer/i18n/nap.json
@@ -43,14 +43,14 @@
"config-help-restart": "Vulite scancellà tutt' 'e date astipate c'avite nzertato e riabbià 'o prucesso d'installazione?",
"config-restart": "Sì, riabbìa",
"config-welcome": "=== Cuntrollo 'e ll'ambiente ===\nSarranno eseguite 'e cuntrolle bbase pe' putè vedè si st'ambiente è adatto pe' ne ffà l'installazione 'e MediaWiki.\nArricurdateve d'includere sti nfurmaziune si spiate assistenza ncopp' 'a maniera 'e cumpletà l'installazione.",
- "config-copyright": "=== Copyright e termine ===\n\n$1\n\nChistu programma è nu software libbero; vuje 'o putite redestribbuì e/o cagnà sott' 'e termine d' 'a licienza GNU GPL ('a Licienza Pubbreca Generale) comme pubbrecata d' 'a Free Software Foundation; o pure 'a verziona 2 d' 'a Licienza, o pure (comme vulite vuje) 'a n'ata verziona cchiù nnova.\n\nChistu programma è destribbuito c' 'a speranza d'essere utile, ma SENZA NISCIUNA GARANZIA; senza manco 'a garanzia p' 'a CUMMERCIABBELETÀ O IDONIETÀ PE' NU SCOPO PARTICULARE.\nIate a vedé 'a GNU GPL pe' n'avé cchiù nfurmaziune.\n\nCu stu programma avísseve 'a ricevere <doclink href=Copying>na copia d' 'a Licienza GNU GPL</doclink> cu stu prugramma; si nò, scrivete â Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA o [http://www.gnu.org/copyleft/gpl.html liggite sta paggena ncopp' 'a l'Internet].",
+ "config-copyright": "=== Copyright e termine ===\n\n$1\n\nChistu programma è nu software libbero; vuje 'o putite redestribbuì e/o cagnà sott' 'e termine d' 'a licienza GNU GPL ('a Licienza Pubbreca Generale) comme pubbrecata d' 'a Free Software Foundation; o pure 'a verziona 2 d' 'a Licienza, o pure (comme vulite vuje) 'a n'ata verziona cchiù nnova.\n\nChistu programma è destribbuito c' 'a speranza d'essere utile, ma SENZA NISCIUNA GARANZIA; senza manco 'a garanzia p' 'a CUMMERCIABBELETÀ O IDONIETÀ PE' NU SCOPO PARTICULARE.\nIate a vedé 'a GNU GPL pe' n'avé cchiù nfurmaziune.\n\nCu stu programma avísseve 'a ricevere <doclink href=Copying>na copia d' 'a Licienza GNU GPL</doclink> cu stu prugramma; si nò, scrivete â Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA o [https://www.gnu.org/copyleft/gpl.html liggite sta paggena ncopp' 'a l'Internet].",
"config-sidebar": "* [https://www.mediawiki.org Paggina prencepale MediaWiki]\n* [https://www.mediawiki.org/wiki/Aiuto:Guida a 'e cuntenute pe' l'utente]\n* [https://www.mediawiki.org/wiki/Manuale:Guida a 'e cuntenute pe l'ammenistrature]\n* [https://www.mediawiki.org/wiki/Manuale:FAQ FAQ]\n----\n* <doclink href=Readme>Lieggeme</doclink>\n* <doclink href=ReleaseNotes>Note 'e verziona</doclink>\n* <doclink href=Copying>Copie</doclink>\n* <doclink href=UpgradeDoc>Agghiurnamento</doclink>",
"config-env-good": "L'ambiente è stato cuntrullato.\nÈ pussibbele installare MediaWiki.",
"config-env-bad": "L'ambiente è stato cuntrullato.\nNun se può installà MediaWiki.",
"config-env-php": "PHP $1 è installato.",
"config-env-hhvm": "HHVM $1 è installato.",
- "config-unicode-using-intl": "Aúsa [http://pecl.php.net/intl l'estensione PECL intl] pe' ne fà 'a normalizzazione Unicode.",
- "config-unicode-pure-php-warning": "<strong>Attenziò:</strong> L' [http://pecl.php.net/intl estensione intl PECL] nun è a disposizione pe' gestire 'a normalizzazione Unicode, accussì se ausasse n'imprementazziona llenta 'n puro PHP.\nSi state a gestire nu pizzo ad alto traffico, avisseve a lieggere cocche considerazione ncopp' 'a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizzaziona Unicode].",
+ "config-unicode-using-intl": "Aúsa [https://pecl.php.net/intl l'estensione PECL intl] pe' ne fà 'a normalizzazione Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Attenziò:</strong> L' [https://pecl.php.net/intl estensione intl PECL] nun è a disposizione pe' gestire 'a normalizzazione Unicode, accussì se ausasse n'imprementazziona llenta 'n puro PHP.\nSi state a gestire nu pizzo ad alto traffico, avisseve a lieggere cocche considerazione ncopp' 'a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizzaziona Unicode].",
"config-unicode-update-warning": "<strong>Attenziò:</strong> 'A verziona installata 'e normalizzazione Unicode aùsa 'a verziona viecchia d' 'o [http://site.icu-project.org/ pruggetto ICU].\nV'avite 'a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations agghiurnà] si state a penzà ncopp' 'o fatto d'ausà Unicode.",
"config-no-db": "Nun se può truvà nu driver adatto p' 'o database! È necessario installare nu driver p' 'o PHP.\n{{PLURAL:$2|'O furmato suppurtato|'E furmate suppurtate}} 'e database ccà annanze: $1.\n\nSi cumpilate PHP autonomamente, riaccunciatevello attivando nu client database, p'esempio ausannoo <code>./configure --with-mysqli</code>.\nQuanno fosse installato PHP pe' bbìa 'e nu pacchetto Debian o Ubuntu, allora avite 'a installà pure 'o pacchetto <code>php5-mysql</code>.",
"config-outdated-sqlite": "'''Attenziò''': tenite 'o SQLite $1 pe' tramente ca ce vulesse 'a verziona $2, SQLite nun sarrà a disposizione.",
@@ -59,11 +59,10 @@
"config-pcre-no-utf8": "<strong>Fatale:</strong> 'E module PCRE d' 'o PHP pare ca se so' compilate senza PCRE_UTF8 supporto.\nA MediaWiki serve nu supporto UTF-8 pe' putè funziunà apposto.",
"config-memory-raised": "'O valore 'e PHP <code>memory_limit</code> è $1, aumentato a $2.",
"config-memory-bad": "<strong>Attenziò:</strong> 'o valore 'e PHP <code>memory_limit</code> è $1.\nProbabbilmente troppo basso.\n'A installazione se putesse scassà!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] è installato",
"config-apc": "[http://www.php.net/apc APC] è installato",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] è installato",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] è installato",
"config-no-cache-apcu": "<strong>Attenziò:</strong> [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] o [http://www.iis.net/download/WinCacheForPhp WinCache] nun so' state truvate.\n'A funziona caching 'e ll'oggette non è apicciata.",
- "config-mod-security": "<strong>Attenziò:</strong> 'O servitore web vuosto téne [http://modsecurity.org/ mod_security]/mod_security2 appicciato. Ce stanno tante mpustaziune commune ca 'o facessero causà prubbleme a MediaWiki e ll'ati software ca permettessero ll'utente 'e pubbrecà cuntenute.\nSi putite, stutate sta funziona. Sinò, riferite 'a [http://modsecurity.org/documentation/ documentaziona ncopp' 'o mod_security] o cuntattate 'o host vuosto pe' ve dà supporto quanno se scummogliasse cocch'errore.",
+ "config-mod-security": "<strong>Attenziò:</strong> 'O servitore web vuosto téne [https://modsecurity.org/ mod_security]/mod_security2 appicciato. Ce stanno tante mpustaziune commune ca 'o facessero causà prubbleme a MediaWiki e ll'ati software ca permettessero ll'utente 'e pubbrecà cuntenute.\nSi putite, stutate sta funziona. Sinò, riferite 'a [https://modsecurity.org/documentation/ documentaziona ncopp' 'o mod_security] o cuntattate 'o host vuosto pe' ve dà supporto quanno se scummogliasse cocch'errore.",
"config-diff3-bad": "GNU diff3 nun truvato.",
"config-git": "Truvato software 'e cuntrollo d' 'a verziona Git: <code>$1</code>.",
"config-git-bad": "Software 'e cuntrollo d' 'a verziona Git nun truvato.",
@@ -215,7 +214,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 o verziune aroppo",
"config-license-pd": "Pubbreco duminio",
"config-license-cc-choose": "Sciglite na licienza Creative Commons ca vulite",
- "config-license-help": "Nu cuofeno 'e wiki pubbrece lassano 'e cuntribbute lloro cu na [http://freedomdefined.org/Definition licienza libbera]. Chesto aiutasse a crià nu senso 'e pruprietà spartuta dint'a communità e ncuraggiasse a cuntribbuiì a nu tèrmene luongo. Nun è generalmente necessario pe' nu wiki privato o aziendale.\n\nSi vulite ausà testi 'a Wikipedia, o vulite ca Wikipedia se pozza miette 'n grado d'accettà teste cupiate d' 'o wiki vuosto, avissev'a scegiere <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nApprimma Wikipedia aveva ausato 'a GNU Free Documentation License. 'A GFDL è una licienza valida, ma è di difficile comprensiona e complica 'o riutilizzo 'e cuntenute.",
+ "config-license-help": "Nu cuofeno 'e wiki pubbrece lassano 'e cuntribbute lloro cu na [https://freedomdefined.org/Definition licienza libbera]. Chesto aiutasse a crià nu senso 'e pruprietà spartuta dint'a communità e ncuraggiasse a cuntribbuiì a nu tèrmene luongo. Nun è generalmente necessario pe' nu wiki privato o aziendale.\n\nSi vulite ausà testi 'a Wikipedia, o vulite ca Wikipedia se pozza miette 'n grado d'accettà teste cupiate d' 'o wiki vuosto, avissev'a scegiere <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nApprimma Wikipedia aveva ausato 'a GNU Free Documentation License. 'A GFDL è una licienza valida, ma è di difficile comprensiona e complica 'o riutilizzo 'e cuntenute.",
"config-email-settings": "Mpustaziune email",
"config-enable-email": "Premmette mmasciate elettroniche r'asciuta",
"config-enable-email-help": "Si vulite ca 'o sistema 'e mmasciate mail funziunasse, [http://www.php.net/manual/en/mail.configuration.php 'e mpustaziune PHP] s'avesser'a ffà bbuone.\nSi nun vulite 'a funziona mmasciata e-mail, allora stutate chiste llàn.",
diff --git a/www/wiki/includes/installer/i18n/nb.json b/www/wiki/includes/installer/i18n/nb.json
index 1eb418ed..276d8a94 100644
--- a/www/wiki/includes/installer/i18n/nb.json
+++ b/www/wiki/includes/installer/i18n/nb.json
@@ -49,14 +49,14 @@
"config-help-restart": "Ønsker du å fjerne alle lagrede data som du har skrevet inn og starte installasjonsprosessen på nytt?",
"config-restart": "Ja, start på nytt",
"config-welcome": "=== Miljøsjekker ===\nGrunnleggende sjekker utføres for å se om dette miljøet er egnet for en MediaWiki-installasjon.\nDu bør oppgi resultatene fra disse sjekkene om du trenger hjelp under installasjonen.",
- "config-copyright": "=== Opphavsrett og vilkår ===\n\n$1\n\nMediaWiki er fri programvare; du kan redistribuere det og/eller modifisere det under betingelsene i GNU General Public License som publisert av Free Software Foundation; enten versjon 2 av lisensen, eller (etter eget valg) enhver senere versjon.\n\nDette programmet er distribuert i håp om at det vil være nyttig, men '''uten noen garanti'''; ikke engang implisitt garanti av '''salgbarhet''' eller '''egnethet for et bestemt formål'''.\nSe GNU General Public License for flere detaljer.\n\nDu skal ha mottatt <doclink href=Copying>en kopi av GNU General Public License</doclink> sammen med dette programmet; hvis ikke, skriv til Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA eller [http://www.gnu.org/copyleft/gpl.html les det på nettet].",
+ "config-copyright": "=== Opphavsrett og vilkår ===\n\n$1\n\nMediaWiki er fri programvare; du kan redistribuere det og/eller modifisere det under betingelsene i GNU General Public License som publisert av Free Software Foundation; enten versjon 2 av lisensen, eller (etter eget valg) enhver senere versjon.\n\nDette programmet er distribuert i håp om at det vil være nyttig, men '''uten noen garanti'''; ikke engang implisitt garanti av '''salgbarhet''' eller '''egnethet for et bestemt formål'''.\nSe GNU General Public License for flere detaljer.\n\nDu skal ha mottatt <doclink href=Copying>en kopi av GNU General Public License</doclink> sammen med dette programmet; hvis ikke, skriv til Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA eller [https://www.gnu.org/copyleft/gpl.html les det på nettet].",
"config-sidebar": "* [https://www.mediawiki.org MediaWiki hjem]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Brukerguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administratorguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ OSS]\n----\n* <doclink href=Readme>Les meg</doclink>\n* <doclink href=ReleaseNotes>Utgivelsesnotater</doclink>\n* <doclink href=Copying>Kopiering</doclink>\n* <doclink href=UpgradeDoc>Oppgradering</doclink>",
"config-env-good": "Miljøet har blitt sjekket.\nDu kan installere MediaWiki.",
- "config-env-bad": "Miljøet har blitt sjekket.\nDu kan installere MediaWiki.",
+ "config-env-bad": "Miljøet har blitt sjekket.\nDu kan ikke installere MediaWiki.",
"config-env-php": "PHP $1 er installert.",
"config-env-hhvm": "HHVM $1 er installert.",
- "config-unicode-using-intl": "Bruker [http://pecl.php.net/intl intl PECL-utvidelsen] for Unicode-normalisering.",
- "config-unicode-pure-php-warning": "'''Advarsel''': [http://pecl.php.net/intl intl PECL-utvidelsen] er ikke tilgjengelig for å håndtere Unicode-normaliseringen, faller tilbake til en langsommere ren-PHP-implementasjon.\nOm du kjører et nettsted med høy trafikk bør du lese litt om [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-normalisering].",
+ "config-unicode-using-intl": "Bruker [https://pecl.php.net/intl intl PECL-utvidelsen] for Unicode-normalisering.",
+ "config-unicode-pure-php-warning": "'''Advarsel''': [https://pecl.php.net/intl intl PECL-utvidelsen] er ikke tilgjengelig for å håndtere Unicode-normaliseringen, faller tilbake til en langsommere ren-PHP-implementasjon.\nOm du kjører et nettsted med høy trafikk bør du lese litt om [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-normalisering].",
"config-unicode-update-warning": "<strong>Advarsel:</strong> Den installerte versjonen av Unicode-normalisereren bruker en eldre versjon av [http://site.icu-project.org/ ICU-prosjektets] bibliotek.\nDu bør [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations oppgradere] om du er bekymret for å bruke Unicode.",
"config-no-db": "Fant ingen passende databasedriver! Du må installere en databasedriver for PHP.\nFølgende {{PLURAL:$2|databasetype|databasetyper}} støttes: $1\n\nOm du kompilerte PHP selv, rekonfigurer den med en aktivert databaseklient, for eksempel ved å bruke <code>./configure --with-mysqli</code>.\nOm du installerte PHP fra en Debian- eller Ubuntu-pakke, må du også installere for eksempel <code>php5-mysql</code>-pakken.",
"config-outdated-sqlite": "'''Advarsel''': Du har SQLite $1, som er en eldre versjon enn minimumskravet SQLite $2. SQLite vil ikke være tilgjengelig.",
@@ -65,12 +65,11 @@
"config-pcre-no-utf8": "'''Fatal''': PHPs PCRE modul ser ut til å være kompilert uten PCRE_UTF8-støtte.\nMediaWiki krever UTF-8-støtte for å fungere riktig.",
"config-memory-raised": "PHPs <code>memory_limit</code> er $1, økt til $2.",
"config-memory-bad": "'''Advarsel:''' PHPs <code>memory_limit</code> er $1.\nDette er sannsynligvis for lavt.\nInstallasjonen kan mislykkes!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] er installert",
"config-apc": "[http://www.php.net/apc APC] er installert",
"config-apcu": "[http://www.php.net/apcu APCu] er installert",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] er installert",
- "config-no-cache-apcu": "<strong>Advarsel:</strong> Kunne ikke finne [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache] eller [http://www.iis.net/download/WinCacheForPhp WinCache].\nObjekthurtiglagring er ikke aktivert.",
- "config-mod-security": "'''Advarsel''': Din web-tjener har [http://modsecurity.org/ mod_security] påslått. Hvis denne er feilinnstilt, kan det gi problemer for MediaWiki eller annen programvare som tillater brukere å poste vilkårlig innhold.\nSjekk [http://modsecurity.org/documentation/ mod_security-dokumentasjonen] eller ta kontakt med din nettleverandør hvis du opplever tilfeldige feil.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] er installert",
+ "config-no-cache-apcu": "<strong>Advarsel:</strong> Kunne ikke finne [http://www.php.net/apcu APCu] eller [http://www.iis.net/download/WinCacheForPhp WinCache].\nObjekthurtiglagring er ikke aktivert.",
+ "config-mod-security": "'''Advarsel''': Din web-tjener har [https://modsecurity.org/ mod_security] påslått. Hvis denne er feilinnstilt, kan det gi problemer for MediaWiki eller annen programvare som tillater brukere å poste vilkårlig innhold.\nSjekk [https://modsecurity.org/documentation/ mod_security-dokumentasjonen] eller ta kontakt med din nettleverandør hvis du opplever tilfeldige feil.",
"config-diff3-bad": "GNU diff3 ikke funnet.",
"config-git": "Har funnet Git version control software: <code>$1</code>.",
"config-git-bad": "Git version control software ble ikke funnet.",
@@ -228,7 +227,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 eller senere",
"config-license-pd": "Offentlig eiendom",
"config-license-cc-choose": "Velg en egendefinert Creative Commons-lisens",
- "config-license-help": "Mange åpne wikier legger alle bidrag under en [http://freedomdefined.org/Definition gratislisens].\nDette gir en følelse av felleseie og stimulerer til langvarige bidrag.\nDette er normalt unødvendig for en privat eller virksomhetsbegrenset wiki.\n\nHvis du ønsker å kunne bruke tekst fra Wikipedia, og at Wikipedia skal kunne ta i mot tekst kopiert fra din wiki, bør du velge <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia brukte tidligere GNU Free Documentation License.\nGFDL er en grei lisens, med vanskelig å forstå.\nDet er også vanskelig å gjenbruke innhold lisensiert under GFDL.",
+ "config-license-help": "Mange åpne wikier legger alle bidrag under en [https://freedomdefined.org/Definition gratislisens].\nDette gir en følelse av felleseie og stimulerer til langvarige bidrag.\nDette er normalt unødvendig for en privat eller virksomhetsbegrenset wiki.\n\nHvis du ønsker å kunne bruke tekst fra Wikipedia, og at Wikipedia skal kunne ta i mot tekst kopiert fra din wiki, bør du velge <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia brukte tidligere GNU Free Documentation License.\nGFDL er en grei lisens, med vanskelig å forstå.\nDet er også vanskelig å gjenbruke innhold lisensiert under GFDL.",
"config-email-settings": "E-postinnstillinger",
"config-enable-email": "Aktiver utgående e-post",
"config-enable-email-help": "Hvis du vil at e-post skal virke må [http://www.php.net/manual/en/mail.configuration.php PHPs e-postinnstillinger] bli konfigurert riktig.\nHvis du ikke ønsker noen e-postfunksjoner kan du deaktivere dem her.",
@@ -258,7 +257,7 @@
"config-cache-options": "Innstillinger for objekt-mellomlagring:",
"config-cache-help": "Objekt-mellomlagring brukes for å forbedre hastigheten for MediaWiki. Ofte forekommende data lagres for gjenbruk.\nMiddels til store nettsteder bør absolutt aktivisere mellomlagring, med også små nettsteder kan ha nytte av dette.",
"config-cache-none": "Ingen mellomlagring (ingen funksjonalitet mistes, men hastigheten kan bli dårlig for store wikier-nettsteder)",
- "config-cache-accel": "Mellomlagring av PHP-objekter (APC, APCu, XCache eller WinCache)",
+ "config-cache-accel": "Mellomlagring av PHP-objekter (APCu eller WinCache)",
"config-cache-memcached": "Bruk Memcached (krever tilleggsoppsett og -konfigurering)",
"config-memcached-servers": "Memcached-servere:",
"config-memcached-help": "Liste av IP-adresser for bruk fra Memcached.\nDet bør angis en per linje sammen med porten som brukes. For eksempel:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -314,6 +313,7 @@
"config-install-mainpage-failed": "Kunne ikke sette inn hovedside: $1",
"config-install-done": "<strong>Gratulrerer!</strong>\nDu har lykkes i å installere MediaWiki.\n\nInstallasjonsprogrammet har generert en <code>LocalSettings.php</code>-fil.\nDen inneholder alle dine konfigureringer.\n\nDu må laste den ned og legge den på hovedfolderen for din wiki-installasjon (der index.php ligger). Nedlastingen skulle ha startet automatisk.\n\nHvis ingen nedlasting ble tilbudt, eller du avbrøt den, kan du få den i gang ved å klikke på lenken under:\n\n$3\n\n<strong>OBS:</strong> Hvis du ikke gjør dette nå, vil den genererte konfigurasjonsfilen ikke være tilgjengelig for deg senere.\n\nNår dette er gjort, kan du <strong>[$2 gå inn i wikien]</strong>.",
"config-install-done-path": "<strong>Gratulerer!</strong>\nDu har installert MediaWiki.\n\nInstallereren har generert en <code>LocalSettings.php</code>-fil.\nDen inneholder all konfigurasjonen for wikien.\n\nDu må laste den ned og legge den i <code>$4</code>. Nedlastingen skal ha startet automatisk.\n\nOm nedlastingen ikke ble startet, eller om du avbrøt den, kan du starte på nytt ved å klikke lenken nedenfor:\n\n$3\n\n<strong>Merk:</strong> Om du ikke gjør dette nå vil den genererte konfigurasjonen ikke være tilgjengelig senere.\n\nNår dette er gjort kan du <strong>[$2 gå til wikien din]</strong>.",
+ "config-install-success": "MediaWiki har blitt installert. Du kan nå\nbesøke <$1$2> for å se wikien din.\nOm du har spørsmål, sjekk de ofte stilte spørsmålene:\n<https://www.mediawiki.org/wiki/Manual:FAQ> eller bruk et av\nsupportforumene som lenkes til fra den siden.",
"config-download-localsettings": "Last ned <code>LocalSettings.php</code>",
"config-help": "hjelp",
"config-help-tooltip": "klikk for å utvide",
diff --git a/www/wiki/includes/installer/i18n/nl-informal.json b/www/wiki/includes/installer/i18n/nl-informal.json
index 299b3b14..566f72aa 100644
--- a/www/wiki/includes/installer/i18n/nl-informal.json
+++ b/www/wiki/includes/installer/i18n/nl-informal.json
@@ -3,7 +3,8 @@
"authors": [
"Siebrand",
"Seb35",
- "Macofe"
+ "Macofe",
+ "Mar(c)"
]
},
"config-localsettings-badkey": "De sleutel die je hebt opgegeven is onjuist",
@@ -13,14 +14,14 @@
"config-your-language": "Jouw taal:",
"config-help-restart": "Wil je alle opgeslagen gegevens die je hebt ingevoerd wissen en het installatieproces opnieuw starten?",
"config-welcome": "=== Controle omgeving ===\nEr worden een aantal basiscontroles uitgevoerd met als doel vast te stellen of deze omgeving geschikt is voor een installatie van MediaWiki.\nAls je hulp nodig hebt bij de installatie, lever deze gegevens dan ook aan.",
- "config-copyright": "=== Auteursrechten en voorwaarden ===\n\n$1\n\nDit programma is vrije software. Je mag het verder verspreiden en/of aanpassen in overeenstemming met de voorwaarden van de GNU General Public License zoals uitgegeven door de Free Software Foundation; ofwel versie 2 van de Licentie of - naar eigen keuze - enige latere versie.\n\nDit programma wordt verspreid in de hoop dat het nuttig is, maar '''zonder enige garantie''', zelfs zonder de impliciete garantie van '''verkoopbaarheid''' of '''geschiktheid voor een bepaald doel'''.\nZie de GNU General Public License voor meer informatie.\n\nSamen met dit programma hoor je een <doclink href=Copying>exemplaar van de GNU General Public License</doclink> ontvangen te hebben; zo niet, schrijf dan aan de Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, Verenigde Staten. Of [http://www.gnu.org/copyleft/gpl.html lees de licentie online].",
+ "config-copyright": "=== Auteursrechten en voorwaarden ===\n\n$1\n\nDit programma is vrije software. Je mag het verder verspreiden en/of aanpassen in overeenstemming met de voorwaarden van de GNU General Public License zoals uitgegeven door de Free Software Foundation; ofwel versie 2 van de Licentie of - naar eigen keuze - enige latere versie.\n\nDit programma wordt verspreid in de hoop dat het nuttig is, maar '''zonder enige garantie''', zelfs zonder de impliciete garantie van '''verkoopbaarheid''' of '''geschiktheid voor een bepaald doel'''.\nZie de GNU General Public License voor meer informatie.\n\nSamen met dit programma hoor je een <doclink href=Copying>exemplaar van de GNU General Public License</doclink> ontvangen te hebben; zo niet, schrijf dan aan de Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, Verenigde Staten. Of [https://www.gnu.org/copyleft/gpl.html lees de licentie online].",
"config-env-good": "De omgeving is gecontroleerd.\nJe kunt MediaWiki installeren.",
"config-env-bad": "De omgeving is gecontroleerd.\nJe kunt MediaWiki niet installeren.",
- "config-unicode-pure-php-warning": "'''Waarschuwing''': de [http://pecl.php.net/intl PECL-extensie intl] is niet beschikbaar om de Unicodenormalisatie af te handelen en daarom wordt de langzame PHP-implementatie gebruikt.\nAls je MediaWiki voor een website met veel verkeer installeert, lees je dan in over [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicodenormalisatie].",
+ "config-unicode-pure-php-warning": "'''Waarschuwing''': de [https://pecl.php.net/intl PECL-extensie intl] is niet beschikbaar om de Unicodenormalisatie af te handelen en daarom wordt de langzame PHP-implementatie gebruikt.\nAls je MediaWiki voor een website met veel verkeer installeert, lees je dan in over [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicodenormalisatie].",
"config-unicode-update-warning": "'''Waarschuwing''': de geïnstalleerde versie van de Unicodenormalisatiewrapper maakt gebruik van een oudere versie van [http://site.icu-project.org/ de bibliotheek van het ICU-project].\nJe moet [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations bijwerken] als Unicode voor jou van belang is.",
"config-no-db": "Het was niet mogelijk een geschikte databasedriver te vinden voor PHP.\nJe moet een databasedriver installeren voor PHP.\nDe volgende databases worden ondersteund: $1.\n\nAls je op een gedeelde omgeving zit, vraag dan aan je hostingprovider een geschikte databasedriver te installeren.\nAls je PHP zelf hebt gecompileerd, wijzig dan je instellingen zodat een databasedriver wordt geactiveerd, bijvoorbeeld via <code>./configure --with-mysql</code>.\nAls je PHP hebt geïnstalleerd via een Debian- of Ubuntu-package, installeer dan ook de module php5-mysql.",
"config-outdated-sqlite": "''' Waarschuwing:''' je gebruikt SQLite $1. SQLite is niet beschikbaar omdat de minimaal vereiste versie $2 is.",
- "config-mod-security": "'''Waarschuwing:''' je webserver heeft de module [http://modsecurity.org/ mod_security] ingeschakeld. Als deze onjuist is ingesteld, kan dit problemen geven in combinatie met MediaWiki of andere software die gebruikers in staat stelt willekeurige inhoud te posten.\nLees de [http://modsecurity.org/documentation/ documentatie over mod_security] of neem contact op met de helpdesk van je provider als je tegen problemen aanloopt.",
+ "config-mod-security": "'''Waarschuwing:''' je webserver heeft de module [https://modsecurity.org/ mod_security] ingeschakeld. Als deze onjuist is ingesteld, kan dit problemen geven in combinatie met MediaWiki of andere software die gebruikers in staat stelt willekeurige inhoud te posten.\nLees de [https://modsecurity.org/documentation/ documentatie over mod_security] of neem contact op met de helpdesk van je provider als je tegen problemen aanloopt.",
"config-imagemagick": "ImageMagick aangetroffen: <code>$1</code>.\nHet aanmaken van miniaturen van afbeeldingen wordt ingeschakeld als je uploaden inschakelt.",
"config-gd": "Ingebouwde GD grafische bibliotheek aangetroffen.\nHet aanmaken van miniaturen van afbeeldingen wordt ingeschakeld als je uploaden inschakelt.",
"config-uploads-not-safe": "'''Waarschuwing:''' je uploadmap <code>$1</code> kan gebruikt worden voor het arbitrair uitvoeren van scripts.\nHoewel MediaWiki alle toegevoegde bestanden controleert op bedreigingen, is het zeer aan te bevelen het [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security beveiligingslek te verhelpen] alvorens uploads in te schakelen.",
@@ -29,7 +30,7 @@
"config-db-host-help": "Als je databaseserver een andere server is, voer dan de hostnaam of het IP-adres hier in.\n\nAls je gebruik maakt van gedeelde webhosting, hoort je provider je de juiste hostnaam te hebben verstrekt.\n\nAls je MediaWiki op een Windowsserver installeert en MySQL gebruikt, dan werkt \"localhost\" mogelijk niet als servernaam.\nAls het inderdaad niet werkt, probeer dan \"127.0.0.1\" te gebruiken als lokaal IP-adres.\n\nAls je PostgreSQL gebruikt, laat dit veld dan leeg om via een Unix-socket te verbinden.",
"config-db-host-oracle-help": "Voer een geldige [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name] in; een tnsnames.ora-bestand moet zichtbaar zijn voor deze installatie.<br />Als je gebruik maakt van clientlibraries 10g of een latere versie, kan je ook gebruik maken van de naamgevingsmethode [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].",
"config-db-name-help": "Kies een naam die je wiki identificeert.\nEr mogen geen spaties gebruikt worden.\nAls je gebruik maakt van gedeelde webhosting, dan hoort je provider ofwel jou een te gebruiken databasenaam gegeven te hebben, of je aangegeven te hebben hoe je databases kunt aanmaken.",
- "config-db-account-oracle-warn": "Er zijn drie ondersteunde scenario's voor het installeren van Oracle als databasebackend:\n\nAls je een databasegebruiker wilt aanmaken als onderdeel van het installatieproces, geef dan de gegevens op van een databasegebruiker in met de rol SYSDBA voor de installatie en voer de gewenste aanmeldgegevens in voor de gebruiker met webtoegang. Je kunt ook de gebruiker met webtoegang handmatig aanmaken en alleen van die gebruiker de aanmeldgegevens opgeven als deze de vereiste rechten heeft om schemaobjecten aan te maken. Als laatste is het mogelijk om aanmeldgegevens van twee verschillende gebruikers op te geven; een met de rechten om schemaobjecten aan te maken, en een met alleen webtoegang.\n\nEen script voor het aanmaken van een gebruiker met de vereiste rechten is te vinden in de map \"maintenance/oracle/\" van deze installatie. Onthoud dat het gebruiken van een gebruiker met beperkte rechten alle mogelijkheden om beheerscripts uit te voeren met de standaard gebruiker onmogelijk maakt.",
+ "config-db-account-oracle-warn": "Er zijn drie ondersteunde scenario's voor het installeren van Oracle als databasebackend:\n\nAls je een database-account wilt aanmaken als onderdeel van het installatieproces, geef dan de gegevens op van een database-account in met de rol SYSDBA voor de installatie en voer de gewenste aanmeldgegevens in voor het account met webtoegang. Je kunt ook het account met webtoegang handmatig aanmaken en alleen van dat account de aanmeldgegevens opgeven als deze de vereiste rechten heeft om schemaobjecten aan te maken. Als laatste is het mogelijk om aanmeldgegevens van twee verschillende accounts op te geven; een met de rechten om schemaobjecten aan te maken, en een met alleen webtoegang.\n\nEen script voor het aanmaken van een account met de vereiste rechten is te vinden in de map \"maintenance/oracle/\" van deze installatie. Onthoud dat het gebruiken van een account met beperkte rechten alle mogelijkheden om beheerscripts uit te voeren met het standaardaccount onmogelijk maakt.",
"config-db-prefix-help": "Als je een database moet gebruiken voor meerdere wiki's, of voor MediaWiki en een andere toepassing, dan kan je ervoor kiezen om een voorvoegsel toe te voegen aan de tabelnamen om conflicten te voorkomen.\nGebruik geen spaties.\n\nDit veld wordt meestal leeg gelaten.",
"config-mysql-old": "Je moet MySQL $1 of later gebruiken.\nJij gebruikt $2.",
"config-db-schema-help": "Dit schema klopt meestal.\nWijzig het alleen als je weet dat dit nodig is.",
@@ -42,7 +43,7 @@
"config-sqlite-name-help": "Kies een naam die je wiki identificeert.\nGebruik geen spaties of koppeltekens.\nDeze naam wordt gebruikt voor het gegevensbestand van SQLite.",
"config-upgrade-done": "Het bijwerken is afgerond.\n\nJe kunt [$1 je wiki nu gebruiken].\n\nAls je je <code>LocalSettings.php</code> opnieuw wilt aanmaken, klik dan op de knop hieronder.\nDit is '''niet aan te raden''' tenzij je problemen hebt met je wiki.",
"config-upgrade-done-no-regenerate": "Het bijwerken is afgerond.\n\nJe kunt nu [$1 je wiki gebruiken].",
- "config-db-web-no-create-privs": "De gebruiker die je hebt opgegeven voor de installatie heeft niet voldoende rechten om een gebruiker aan te maken.\nDe gebruiker die je hier opgeeft moet al bestaan.",
+ "config-db-web-no-create-privs": "Het account dat je voor installatie hebt opgegeven, heeft niet voldoende rechten om een account aan te maken.\nHet account dat je hier opgeeft, moet al bestaan.",
"config-mysql-myisam-dep": "'''Waarschuwing''': je hebt MyISAM geselecteerd als opslagengine voor MySQL. Dit is niet aan te raden voor MediaWiki omdat:\n* het nauwelijks ondersteuning biedt voor gebruik door meerdere gebruikers tegelijkertijd door het locken van tabellen;\n* het meer vatbaar is voor corruptie dan andere engines;\n* de code van MediaWiki niet alstijd omgaat met MyISAM zoals dat zou moeten.\n\nAls je installatie van MySQL InnoDB ondersteunt, gebruik dat dan vooral.\nAls je installatie van MySQL geen ondersteuning heeft voor InnoDB, denk dan na over upgraden.",
"config-mysql-charset-help": "In '''binaire modus''' slaat MediaWiki tekst in UTF-8 op in binaire databasevelden.\nDit is efficiënter dan de UTF-8-modus van MySQL en stelt je in staat de volledige reeks Unicodetekens te gebruiken.\n\nIn '''UTF-8-modus''' kent MySQL de tekenset van je gegevens en kan de databaseserver ze juist weergeven en converteren.\nHet is dat niet mogelijk tekens op te slaan die de \"[https://nl.wikipedia.org/wiki/Lijst_van_Unicode-subbereiken#Basic_Multilingual_Plane Basic Multilingual Plane]\" te boven gaan.",
"config-project-namespace-help": "In het kielzog van Wikipedia beheren veel wiki's hun beleidspagina's apart van hun inhoudelijke pagina's in een \"'''projectnaamruimte'''\".\nAlle paginanamen in deze naamruimte beginnen met een bepaald voorvoegsel dat je hier kunt opgeven.\nDit voorvoegsel wordt meestal afgeleid van de naam van de wiki, maar het kan geen bijzondere tekens bevatten als \"#\" of \":\".",
@@ -54,7 +55,7 @@
"config-subscribe-noemail": "Je hebt geprobeerd je te abonneren op de mailinglijst voor release-aankondigingen zonder een e-mailadres op te geven.\nGeef een e-mailadres op als je je wilt abonneren op de mailinglijst.",
"config-almost-done": "Je bent bijna klaar!\nAls je wilt kan je de overige instellingen overslaan en de wiki nu installeren.",
"config-profile-help": "Wiki's werken het beste als ze door zoveel mogelijk gebruikers worden bewerkt.\nIn MediaWiki is het eenvoudig om de recente wijzigingen te controleren en eventuele foutieve of kwaadwillende bewerkingen terug te draaien.\n\nDaarnaast vinden velen MediaWiki goed inzetbaar in vele andere rollen, en soms is het niet handig om helemaal \"op de wikimanier\" te werken.\nDaarom biedt dit installatieprogramma je de volgende keuzes voor de basisinstelling van gebruikersvrijheden:\n\nEen '''{{int:config-profile-wiki}}''' staat iedereen toe te bewerken, zonder zelfs aan te melden.\nEen wiki met '''{{int:config-profile-no-anon}}\" biedt extra verantwoordelijkheid, maar kan afschrikken toevallige gebruikers afschrikken.\n\nHet scenario '''{{int:config-profile-fishbowl}}''' laat gebruikers waarvoor dat is ingesteld bewerkt, maar andere gebruikers kunnen alleen pagina's bekijken, inclusief de bewerkingsgeschiedenis.\nIn een '''{{int:config-profile-private}}''' kunnen alleen goedgekeurde gebruikers pagina's bekijken en bewerken.\n\nMeer complexe instellingen voor gebruikersrechten zijn te maken na de installatie; hierover is meer te lezen in de [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights handleiding].",
- "config-license-help": "In veel openbare wiki's zijn alle bijdragen beschikbaar onder een [http://freedomdefined.org/Definition vrije licentie].\nDit helpt bij het creëren van een gevoel van gemeenschappelijk eigendom en stimuleert bijdragen op lange termijn.\nDit is over het algemeen niet nodig is voor een particuliere of zakelijke wiki.\n\nAls je teksten uit Wikipedia wilt kunnen gebruiken en je wilt het mogelijk maken teksten uit je wiki naar Wikipedia te kopiëren, kies dan de licentie '''Creative Commons Naamsvermelding-Gelijk delen'''.\n\nDe GNU Free Documentation License is de oude licentie voor inhoud uit Wikipedia.\nDit is nog steeds een geldige licentie, maar deze licentie is lastig te begrijpen.\nHet is ook lastig inhoud te hergebruiken onder de GFDL.",
+ "config-license-help": "In veel openbare wiki's zijn alle bijdragen beschikbaar onder een [https://freedomdefined.org/Definition vrije licentie].\nDit helpt bij het creëren van een gevoel van gemeenschappelijk eigendom en stimuleert bijdragen op lange termijn.\nDit is over het algemeen niet nodig is voor een particuliere of zakelijke wiki.\n\nAls je teksten uit Wikipedia wilt kunnen gebruiken en je wilt het mogelijk maken teksten uit je wiki naar Wikipedia te kopiëren, kies dan de licentie '''Creative Commons Naamsvermelding-Gelijk delen'''.\n\nDe GNU Free Documentation License is de oude licentie voor inhoud uit Wikipedia.\nDit is nog steeds een geldige licentie, maar deze licentie is lastig te begrijpen.\nHet is ook lastig inhoud te hergebruiken onder de GFDL.",
"config-enable-email-help": "Als je wilt dat e-mailen mogelijk is, dan moeten de [http://www.php.net/manual/en/mail.configuration.php e-mailinstellingen van PHP] correct zijn.\nAls je niet wilt dat e-mailen mogelijk is, dan kan je de instellingen hier uitschakelen.",
"config-upload-help": "Het toestaan van het uploaden van bestanden stelt je server mogelijk bloot aan beveiligingsrisico's.\nEr is meer [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security informatie over beveiliging] beschikbaar in de handleiding.\n\nOm het bestandsuploads mogelijk te maken kan je de rechten op de submap <code>images</code> onder de hoofdmap van MediaWiki aanpassen, zodat de webserver erin kan schrijven.\nDaarmee wordt deze functie ingeschakeld.",
"config-logo-help": "Het standaarduiterlijk van MediaWiki bevat ruimte voor een logo van 135x160 pixels boven het menu.\nUpload een afbeelding met de juiste afmetingen en voer de URL hier in.\n\nAls je geen logo wilt gebruiken, kan je dit veld leeg laten.",
@@ -66,9 +67,9 @@
"config-install-alreadydone": "'''Waarschuwing:''' het lijkt alsof je MediaWiki al hebt geïnstalleerd en probeert het programma opnieuw te installeren.\nGa door naar de volgende pagina.",
"config-install-begin": "Als je nu op \"{{int:config-continue}}\" klikt, begint de installatie van MediaWiki.\nAls je nog wijzigingen wilt maken, klik dan op \"Terug\".",
"config-pg-no-plpgsql": "Je moet de taal PL/pgSQL installeren in de database $1",
- "config-pg-no-create-privs": "De gebruiker die je hebt opgegeven door de installatie heeft niet voldoende rechten om een gebruiker aan te maken.",
- "config-pg-not-in-role": "De gebruiker die je hebt opgegeven voor de webgebruiker bestaat al.\nDe gebruiker die je hebt opgegeven voor installatie is geen superuser en geen lid van de rol van de webgebruiker, en kan het dus geen objecten aanmaken die van de webgebruiker zijn.\n\nMediaWiki vereist momenteel dat de tabellen van de webgebruiker zijn. Geef een andere webgebruikersnaam op, of klik op \"terug\" en geef een gebruiker op die voldoende installatierechten heeft.",
- "config-install-user-missing-create": "De opgegeven gebruiker \"$1\" bestaat niet.\nKlik op \"registreren\" onderaan als je de gebruiker wilt aanmaken.",
+ "config-pg-no-create-privs": "Het account dat je voor installatie hebt opgegeven, heeft niet voldoende rechten om een account aan te maken.",
+ "config-pg-not-in-role": "Het account dat je voor de webgebruiker hebt opgegeven, bestaat al.\nHet account dat je voor installatie hebt opgegeven, is geen superuser en geen lid van de rol van de webgebruiker, en kan het dus geen objecten aanmaken die van de webgebruiker zijn.\n\nMediaWiki vereist momenteel dat de tabellen van de webgebruiker zijn. Geef een andere webaccountnaam op, of klik op \"terug\" en geef een gebruiker op die voldoende installatierechten heeft.",
+ "config-install-user-missing-create": "Het opgegeven account \"$1\" bestaat niet.\nKlik op \"registreren\" onderaan als je het account wilt aanmaken.",
"config-install-done": "'''Gefeliciteerd!'''\nJe hebt MediaWiki met geïnstalleerd.\n\nHet installatieprogramma heeft het bestand <code>LocalSettings.php</code> aangemaakt.\nDit bevat al je instellingen.\n\nJe moet het bestand downloaden en in de hoofdmap van uw wikiinstallatie plaatsten; in dezelfde map als index.php.\nDe download moet je automatisch zijn aangeboden.\n\nAls de download niet is aangeboden of als je de download hebt geannuleerd, dan kan je de download opnieuw starten door op de onderstaande koppeling te klikken:\n\n$3\n\n'''Let op''': als je dit niet nu doet, dan het is bestand als u later de installatieprocedure afsluit zonder het bestand te downloaden niet meer beschikbaar.\n\nNa het plaatsen van het bestand met instellingen kan je '''[$2 je wiki betreden]'''.",
"mainpagedocfooter": "Raadpleeg de [https://meta.wikimedia.org/wiki/Help:Contents Inhoudsopgave handleiding] voor informatie over het gebruik van de wikisoftware.\n\n== Meer hulp over MediaWiki ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lijst met instellingen]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Veelgestelde vragen (FAQ)]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailinglijst voor aankondigingen van nieuwe versies]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Maak MediaWiki beschikbaar in jouw taal]"
}
diff --git a/www/wiki/includes/installer/i18n/nl.json b/www/wiki/includes/installer/i18n/nl.json
index 6185ef4c..81416ccc 100644
--- a/www/wiki/includes/installer/i18n/nl.json
+++ b/www/wiki/includes/installer/i18n/nl.json
@@ -61,14 +61,14 @@
"config-help-restart": "Wilt u alle opgeslagen gegevens die u hebt ingevoerd wissen en het installatieproces opnieuw starten?",
"config-restart": "Ja, opnieuw starten",
"config-welcome": "=== Controle omgeving ===\nEr worden een aantal basiscontroles uitgevoerd met als doel vast te stellen of deze omgeving geschikt is voor een installatie van MediaWiki.\nLever deze gegevens aan als u ondersteuning vraagt bij de installatie.",
- "config-copyright": "=== Auteursrechten en voorwaarden ===\n\n$1\n\nDit programma is vrije software. U mag het verder verspreiden en/of aanpassen in overeenstemming met de voorwaarden van de GNU General Public License zoals uitgegeven door de Free Software Foundation; ofwel versie 2 van de Licentie of - naar uw keuze - enige latere versie.\n\nDit programma wordt verspreid in de hoop dat het nuttig is, maar '''zonder enige garantie''', zelfs zonder de impliciete garantie van '''verkoopbaarheid''' of '''geschiktheid voor een bepaald doel'''.\nZie de GNU General Public License voor meer informatie.\n\nSamen met dit programma hoort u een <doclink href=Copying>exemplaar van de GNU General Public License</doclink> ontvangen te hebben; zo niet, schrijf dan aan de Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, Verenigde Staten. Of [http://www.gnu.org/copyleft/gpl.html lees de licentie online].",
+ "config-copyright": "=== Auteursrechten en voorwaarden ===\n\n$1\n\nDit programma is vrije software. U mag het verder verspreiden en/of aanpassen in overeenstemming met de voorwaarden van de GNU General Public License zoals uitgegeven door de Free Software Foundation; ofwel versie 2 van de Licentie of - naar uw keuze - enige latere versie.\n\nDit programma wordt verspreid in de hoop dat het nuttig is, maar '''zonder enige garantie''', zelfs zonder de impliciete garantie van '''verkoopbaarheid''' of '''geschiktheid voor een bepaald doel'''.\nZie de GNU General Public License voor meer informatie.\n\nSamen met dit programma hoort u een <doclink href=Copying>exemplaar van de GNU General Public License</doclink> ontvangen te hebben; zo niet, schrijf dan aan de Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, Verenigde Staten. Of [https://www.gnu.org/copyleft/gpl.html lees de licentie online].",
"config-sidebar": "* [https://www.mediawiki.org MediaWiki-thuispagina]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Gebruikershandleiding] (Engelstalig)\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Beheerdershandleiding] (Engelstalig)\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Veelgestelde vragen] (Engelstalig)\n----\n* <doclink href=Readme>Leesmij</doclink> (Engelstalig)\n* <doclink href=ReleaseNotes>Release notes</doclink> (Engelstalig)\n* <doclink href=Copying>Kopiëren</doclink> (Engelstalig)\n* <doclink href=UpgradeDoc>Versie bijwerken</doclink> (Engelstalig)",
"config-env-good": "De omgeving is gecontroleerd.\nU kunt MediaWiki installeren.",
"config-env-bad": "De omgeving is gecontroleerd.\nU kunt MediaWiki niet installeren.",
"config-env-php": "PHP $1 is geïnstalleerd.",
"config-env-hhvm": "HHVM $1 is geïnstalleerd.",
- "config-unicode-using-intl": "Voor Unicode-normalisatie wordt de [http://pecl.php.net/intl PECL-extensie intl] gebruikt.",
- "config-unicode-pure-php-warning": "<strong>Waarschuwing:</strong> de [http://pecl.php.net/intl PECL-extensie intl] is niet beschikbaar om de Unicodenormalisatie af te handelen en daarom wordt de langzamere PHP-implementatie gebruikt.\nAls u MediaWiki voor een website met veel verkeer installeert, lees u dan in over [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicodenormalisatie].",
+ "config-unicode-using-intl": "Voor Unicode-normalisatie wordt de [https://pecl.php.net/intl PECL-extensie intl] gebruikt.",
+ "config-unicode-pure-php-warning": "<strong>Waarschuwing:</strong> de [https://pecl.php.net/intl PECL-extensie intl] is niet beschikbaar om de Unicodenormalisatie af te handelen en daarom wordt de langzamere PHP-implementatie gebruikt.\nAls u MediaWiki voor een website met veel verkeer installeert, lees u dan in over [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicodenormalisatie].",
"config-unicode-update-warning": "<strong>Waarschuwing:</strong> de geïnstalleerde versie van de Unicodenormalisatiewrapper maakt gebruik van een oudere versie van [http://site.icu-project.org/ de bibliotheek van het ICU-project].\nU moet [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations bijwerken] als Unicode voor u van belang is.",
"config-no-db": "Het was niet mogelijk een geschikte databasedriver te vinden voor PHP! U moet een databasedriver installeren voor PHP.\n{{PLURAL:$2|Het volgende databasetype wordt|De volgende databasetypes worden}} ondersteund: $1.\n\nAls u PHP zelf hebt gecompileerd, wijzig dan uw instellingen zodat een databasedriver wordt geactiveerd, bijvoorbeeld via <code>./configure --with-mysqli</code>.\nAls u PHP hebt geïnstalleerd via een Debian- of Ubuntu-package, installeer dan ook bijvoorbeeld de module <code>php5-mysql</code>.",
"config-outdated-sqlite": "''' Waarschuwing:''' u gebruikt SQLite $1. SQLite is niet beschikbaar omdat de minimaal vereiste versie $2 is.",
@@ -77,12 +77,11 @@
"config-pcre-no-utf8": "<strong>Onherstelbare fout:</strong> de module PRCE van PHP lijkt te zijn gecompileerd zonder ondersteuning voor PCRE_UTF8.\nMediaWiki heeft ondersteuning voor UTF-8 nodig om correct te kunnen werken.",
"config-memory-raised": "PHP's <code>memory_limit</code> is $1 en is verhoogd tot $2.",
"config-memory-bad": "'''Waarschuwing:''' PHP's <code>memory_limit</code> is $1.\nDit is waarschijnlijk te laag.\nDe installatie kan mislukken!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] is op dit moment geïnstalleerd",
"config-apc": "[http://www.php.net/apc APC] is op dit moment geïnstalleerd",
"config-apcu": "[http://www.php.net/apcu APCu] is geïnstalleerd",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] is op dit moment geïnstalleerd",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] is op dit moment geïnstalleerd",
"config-no-cache-apcu": "<strong>Waarschuwing:</strong> [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] of [http://www.iis.net/download/WinCacheForPhp WinCache] is niet aangetroffen.\nHet cachen van objecten is niet ingeschakeld.",
- "config-mod-security": "<strong>Waarschuwing:</strong> Uw webserver heeft de module [http://modsecurity.org/ mod_security]/mod_security2 ingeschakeld. Veel standaard instellingen hiervan zorgen voor problemen in combinatie met MediaWiki en andere software die gebruikers in staat stelt willekeurige inhoud te posten.\nIndien mogelijk, zou deze moeten worden uitgeschakeld. Lees anders de [http://modsecurity.org/documentation/ documentatie over mod_security] of neem contact op met de helpdesk van uw provider als u tegen problemen aanloopt.",
+ "config-mod-security": "<strong>Waarschuwing:</strong> Uw webserver heeft de module [https://modsecurity.org/ mod_security]/mod_security2 ingeschakeld. Veel standaard instellingen hiervan zorgen voor problemen in combinatie met MediaWiki en andere software die gebruikers in staat stelt willekeurige inhoud te posten.\nIndien mogelijk, zou deze moeten worden uitgeschakeld. Lees anders de [https://modsecurity.org/documentation/ documentatie over mod_security] of neem contact op met de helpdesk van uw provider als u tegen problemen aanloopt.",
"config-diff3-bad": "GNU diff3 niet aangetroffen.",
"config-git": "Versiecontrolesoftware git is aangetroffen: <code>$1</code>",
"config-git-bad": "Geen git versiecontrolesoftware aangetroffen.",
@@ -107,7 +106,7 @@
"config-db-name": "Databasenaam:",
"config-db-name-help": "Kies een naam die uw wiki identificeert.\nEr mogen geen spaties gebruikt worden.\nAls u gebruik maakt van gedeelde webhosting, dan hoort uw provider ofwel u een te gebruiken databasenaam gegeven te hebben, of u aangegeven te hebben hoe u databases kunt aanmaken.",
"config-db-name-oracle": "Databaseschema:",
- "config-db-account-oracle-warn": "Er zijn drie ondersteunde scenario's voor het installeren van Oracle als databasebackend:\n\nAls u een databasegebruiker wilt aanmaken als onderdeel van het installatieproces, geef dan de gegevens op van een databasegebruiker in met de rol SYSDBA voor de installatie en voer de gewenste aanmeldgegevens in voor de gebruiker met webtoegang. U kunt ook de gebruiker met webtoegang handmatig aanmaken en alleen van die gebruiker de aanmeldgegevens opgeven als deze de vereiste rechten heeft om schemaobjecten aan te maken. Als laatste is het mogelijk om aanmeldgegevens van twee verschillende gebruikers op te geven; een met de rechten om schemaobjecten aan te maken, en een met alleen webtoegang.\n\nEen script voor het aanmaken van een gebruiker met de vereiste rechten is te vinden in de map \"maintenance/oracle/\" van deze installatie. Onthoud dat het gebruiken van een gebruiker met beperkte rechten alle mogelijkheden om beheerscripts uit te voeren met de standaard gebruiker onmogelijk maakt.",
+ "config-db-account-oracle-warn": "Er zijn drie ondersteunde scenario's voor het installeren van Oracle als databasebackend:\n\nAls u een database-account wilt aanmaken als onderdeel van het installatieproces, geef dan de gegevens op van een database-account in met de rol SYSDBA voor de installatie en voer de gewenste aanmeldgegevens in voor het account met webtoegang. U kunt ook het account met webtoegang handmatig aanmaken en alleen van dat account de aanmeldgegevens opgeven als deze de vereiste rechten heeft om schemaobjecten aan te maken. Als laatste is het mogelijk om aanmeldgegevens van twee verschillende accounts op te geven; een met de rechten om schemaobjecten aan te maken, en een met alleen webtoegang.\n\nEen script voor het aanmaken van een account met de vereiste rechten is te vinden in de map \"maintenance/oracle/\" van deze installatie. Onthoud dat het gebruiken van een account met beperkte rechten alle mogelijkheden om beheerscripts uit te voeren met het standaardaccount onmogelijk maakt.",
"config-db-install-account": "Gebruiker voor installatie",
"config-db-username": "Gebruikersnaam voor database:",
"config-db-password": "Wachtwoord voor database:",
@@ -153,8 +152,8 @@
"config-invalid-db-prefix": "Ongeldig databasevoorvoegsel \"$1\".\nGebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_) en streepjes (-).",
"config-connection-error": "$1.\n\nControleer de host, gebruikersnaam en wachtwoord en probeer het opnieuw.",
"config-invalid-schema": "Ongeldig schema voor MediaWiki \"$1\".\nGebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_).",
- "config-db-sys-create-oracle": "Het installatieprogramma biedt alleen de mogelijkheid een nieuwe gebruiker aan te maken met de SYSDBA-gebruiker.",
- "config-db-sys-user-exists-oracle": "De gebruiker \"$1\" bestaat al. SYSDBA kan alleen gebruikt worden voor het aanmaken van een nieuwe gebruiker!",
+ "config-db-sys-create-oracle": "Het installatieprogramma biedt alleen de mogelijkheid een nieuw account aan te maken met een SYSDBA-account.",
+ "config-db-sys-user-exists-oracle": "Gebruikersaccount \"$1\" bestaat al. SYSDBA kan alleen gebruikt worden voor het aanmaken van een nieuw account!",
"config-postgres-old": "PostgreSQL $1 of hoger is vereist.\nU gebruikt $2.",
"config-mssql-old": "Microsoft SQL Server $1 of hoger is vereist. U hebt $2.",
"config-sqlite-name-help": "Kies een naam die uw wiki identificeert.\nGebruik geen spaties of koppeltekens.\nDeze naam wordt gebruikt voor het gegevensbestand van SQLite.",
@@ -176,7 +175,7 @@
"config-db-web-help": "Selecteer de gebruikersnaam en het wachtwoord die de webserver gebruikt om verbinding te maken met de databaseserver na de installatie.",
"config-db-web-account-same": "Hetzelfde account gebruiken als voor de installatie",
"config-db-web-create": "Maak de gebruiker aan als deze nog niet bestaat",
- "config-db-web-no-create-privs": "De gebruiker die u hebt opgegeven voor de installatie heeft niet voldoende rechten om een gebruiker aan te maken.\nDe gebruiker die u hier opgeeft moet al bestaan.",
+ "config-db-web-no-create-privs": "Het account dat u voor de installatie hebt opgegeven, heeft niet voldoende rechten om een account aan te maken.\nHet account dat u hier opgeeft, moet al bestaan.",
"config-mysql-engine": "Opslagmethode:",
"config-mysql-innodb": "InnoDB",
"config-mysql-myisam": "MyISAM",
@@ -240,7 +239,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 of hoger",
"config-license-pd": "Publiek domein",
"config-license-cc-choose": "Een Creative Commons-licentie selecteren",
- "config-license-help": "In veel openbare wiki's zijn alle bijdragen beschikbaar onder een [http://freedomdefined.org/Definition vrije licentie].\nDit helpt bij het creëren van een gevoel van gemeenschappelijk eigendom en stimuleert bijdragen op lange termijn.\nDit is over het algemeen niet nodig is voor een particuliere of zakelijke wiki.\n\nAls u teksten uit Wikipedia wilt kunnen gebruiken en u wilt het mogelijk maken teksten uit uw wiki naar Wikipedia te kopiëren, kies dan de licentie <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nDe GNU Free Documentation License is de oude licentie voor inhoud uit Wikipedia.\nDit is nog steeds een geldige licentie, maar deze licentie is lastig te begrijpen.\nHet is ook lastig inhoud te hergebruiken onder de GFDL.",
+ "config-license-help": "In veel openbare wiki's zijn alle bijdragen beschikbaar onder een [https://freedomdefined.org/Definition vrije licentie].\nDit helpt bij het creëren van een gevoel van gemeenschappelijk eigendom en stimuleert bijdragen op lange termijn.\nDit is over het algemeen niet nodig is voor een particuliere of zakelijke wiki.\n\nAls u teksten uit Wikipedia wilt kunnen gebruiken en u wilt het mogelijk maken teksten uit uw wiki naar Wikipedia te kopiëren, kies dan de licentie <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nDe GNU Free Documentation License is de oude licentie voor inhoud uit Wikipedia.\nDit is nog steeds een geldige licentie, maar deze licentie is lastig te begrijpen.\nHet is ook lastig inhoud te hergebruiken onder de GFDL.",
"config-email-settings": "E-mailinstellingen",
"config-enable-email": "Uitgaande e-mail inschakelen",
"config-enable-email-help": "Als u wilt dat e-mailen mogelijk is, dan moeten de [http://www.php.net/manual/en/mail.configuration.php e-mailinstellingen van PHP] correct zijn.\nAls u niet wilt dat e-mailen mogelijk is, dan kunt u de instellingen hier uitschakelen.",
@@ -298,14 +297,14 @@
"config-install-pg-commit": "Wijzigingen worden doorgevoerd",
"config-install-pg-plpgsql": "Controle op de taal PL/pgSQL",
"config-pg-no-plpgsql": "U moet de taal PL/pgSQL installeren in de database $1",
- "config-pg-no-create-privs": "De gebruiker die u hebt opgegeven door de installatie heeft niet voldoende rechten om een gebruiker aan te maken.",
- "config-pg-not-in-role": "De gebruiker die u hebt opgegeven voor de webgebruiker bestaat al.\nDe gebruiker die u hebt opgegeven voor installatie is geen superuser en geen lid van de rol van de webgebruiker, en kan het dus geen objecten aanmaken die van de webgebruiker zijn.\n\nMediaWiki vereist momenteel dat de tabellen van de webgebruiker zijn. Geef een andere webgebruikersnaam op, of klik op \"terug\" en geef een gebruiker op die voldoende installatierechten heeft.",
+ "config-pg-no-create-privs": "Het account dat u voor installatie hebt opgegeven, heeft niet voldoende rechten om een account aan te maken.",
+ "config-pg-not-in-role": "Het account dat u voor de webgebruiker hebt opgegeven, bestaat al.\nHet account dat u voor installatie hebt opgegeven, is geen superuser en geen lid van de rol van webgebruiker, en kan dus geen objecten aanmaken die van de webgebruiker zijn.\n\nMediaWiki vereist momenteel dat de tabellen van de webgebruiker zijn. Geef een andere webaccountnaam op, of klik op \"terug\" en geef een gebruiker op die voldoende installatierechten heeft.",
"config-install-user": "Databasegebruiker aan het aanmaken",
"config-install-user-alreadyexists": "Gebruiker \"$1\" bestaat al",
- "config-install-user-create-failed": "Het aanmaken van de gebruiker \"$1\" is mislukt: $2",
+ "config-install-user-create-failed": "Aanmaken van account \"$1\" is mislukt: $2",
"config-install-user-grant-failed": "Het geven van rechten aan gebruiker \"$1\" is mislukt: $2",
"config-install-user-missing": "De opgegeven gebruiker \"$1\" bestaat niet.",
- "config-install-user-missing-create": "De opgegeven gebruiker \"$1\" bestaat niet.\nKlik op \"registreren\" onderaan als u de gebruiker wilt aanmaken.",
+ "config-install-user-missing-create": "Het opgegeven account \"$1\" bestaat niet.\nKlik op \"registreren\" onderaan als u het account wilt aanmaken.",
"config-install-tables": "Tabellen aanmaken",
"config-install-tables-exist": "'''Waarschuwing''': de MediaWikitabellen lijken al te bestaan.\nHet aanmaken wordt overgeslagen.",
"config-install-tables-failed": "'''Fout''': het aanmaken van een tabel is mislukt met de volgende foutmelding: $1",
@@ -326,6 +325,7 @@
"config-install-mainpage-failed": "Het was niet mogelijk de hoofdpagina in te voegen: $1",
"config-install-done": "<strong>Gefeliciteerd!</strong>\nU hebt MediaWiki geïnstalleerd.\n\nHet installatieprogramma heeft het bestand <code>LocalSettings.php</code> aangemaakt.\nDit bevat al uw instellingen.\n\nU moet het bestand downloaden en in de hoofdmap van uw wiki-installatie plaatsen, in dezelfde map als index.php.\nDe download zou automatisch moeten zijn gestart.\n\nAls de download niet is gestart of als u de download hebt geannuleerd, dan kunt u de download opnieuw starten door op de onderstaande koppeling te klikken:\n\n$3\n\n<strong>Let op:</strong> als u dit niet nu doet, dan is het bestand als u later de installatieprocedure afsluit zonder het bestand te downloaden niet meer beschikbaar.\n\nNa het plaatsen van het bestand met instellingen kunt u <strong>[$2 uw wiki gebruiken]</strong>.",
"config-install-done-path": "<strong>Gefeliciteerd!</strong>\nU hebt MediaWiki geïnstalleerd.\n\nHet installatieprogramma heeft het bestand <code>LocalSettings.php</code> aangemaakt.\nDit bevat al uw instellingen.\n\nU moet het bestand downloaden en in <code>$4</code> plaatsen. De download zou automatisch moeten zijn gestart.\n\nAls de download niet is gestart of als u de download hebt geannuleerd, dan kunt u de download opnieuw starten door op de onderstaande koppeling te klikken:\n\n$3\n\n<strong>Let op:</strong> Als u dit niet nu doet, dan is het bestand als u later de installatieprocedure afsluit zonder het bestand te downloaden niet meer beschikbaar.\n\nNa het plaatsen van het bestand met instellingen kunt u <strong>[$2 uw wiki gebruiken]</strong>.",
+ "config-install-success": "MediaWiki is geïnstalleerd. U kunt nu\n<$1$2> bezoeken om uw wiki te bekijken.\nAls u vragen hebt, bekijk dan onze lijst met veelgestelde vragen:\n<https://www.mediawiki.org/wiki/Manual:FAQ>, of gebruik een van de hulpforums vermeld op die pagina.",
"config-download-localsettings": "<code>LocalSettings.php</code> downloaden",
"config-help": "hulp",
"config-help-tooltip": "klik om uit te vouwen",
diff --git a/www/wiki/includes/installer/i18n/oc.json b/www/wiki/includes/installer/i18n/oc.json
index 3573c3ef..7ac065be 100644
--- a/www/wiki/includes/installer/i18n/oc.json
+++ b/www/wiki/includes/installer/i18n/oc.json
@@ -39,11 +39,10 @@
"config-env-bad": "L’environament es estat verificat.\nPodètz pas installar MediaWiki.",
"config-env-php": "PHP $1 es installat.",
"config-env-hhvm": "HHVM $1 es installat.",
- "config-unicode-using-intl": "Utilizacion de [http://pecl.php.net/intl l'extension PECL intl] per la normalizacion Unicode.",
+ "config-unicode-using-intl": "Utilizacion de [https://pecl.php.net/intl l'extension PECL intl] per la normalizacion Unicode.",
"config-memory-raised": "Lo paramètre <code>memory_limit</code> de PHP èra a $1, portat a $2.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] es installat",
"config-apc": "[http://www.php.net/apc APC] es installat",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] es installat",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] es installat",
"config-diff3-bad": "GNU diff3 pas trobat.",
"config-git": "Logicial de contraròtle de version Git trobat : <code>$1</code>.",
"config-git-bad": "Logicial de contraròtle de version Git pas trobat.",
diff --git a/www/wiki/includes/installer/i18n/pl.json b/www/wiki/includes/installer/i18n/pl.json
index d712afe1..5b4718cf 100644
--- a/www/wiki/includes/installer/i18n/pl.json
+++ b/www/wiki/includes/installer/i18n/pl.json
@@ -22,7 +22,9 @@
"The Polish",
"Macofe",
"Sethakill",
- "Peter Bowman"
+ "Peter Bowman",
+ "Ankam",
+ "Railfail536"
]
},
"config-desc": "Instalator MediaWiki",
@@ -62,14 +64,14 @@
"config-help-restart": "Czy chcesz usunąć wszystkie zapisane dane i uruchomić ponownie proces instalacji?",
"config-restart": "Tak, zacznij od nowa",
"config-welcome": "=== Sprawdzenie środowiska instalacji ===\nTeraz zostaną wykonane podstawowe testy sprawdzające czy to środowisko jest odpowiednie dla instalacji MediaWiki.\nJeśli potrzebujesz pomocy podczas instalacji, załącz wyniki tych testów.",
- "config-copyright": "=== Prawa autorskie i warunki użytkowania ===\n\n$1\n\nTo oprogramowanie jest wolne; możesz je rozprowadzać dalej i modyfikować zgodnie z warunkami licencji GNU General Public License opublikowanej przez Free Software Foundation w wersji 2 tej licencji lub (według Twojego wyboru) którejś z późniejszych jej wersji.\n\nNiniejsze oprogramowanie jest rozpowszechniane w nadziei, że będzie użyteczne, ale '''bez żadnej gwarancji'''; nawet bez domniemanej gwarancji '''handlowej''' lub '''przydatności do określonego celu'''.\nZobacz treść licencji GNU General Public License, aby uzyskać więcej szczegółów.\n\nRazem z oprogramowaniem powinieneś otrzymać <doclink href=Copying>kopię licencji GNU General Public License</doclink>. Jeśli jej nie otrzymałeś, napisz do Free Software Foundation, Inc, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. lub [http://www.gnu.org/copyleft/gpl.html przeczytaj ją online].",
+ "config-copyright": "=== Prawa autorskie i warunki użytkowania ===\n\n$1\n\nTo oprogramowanie jest wolne; możesz je rozprowadzać dalej i modyfikować zgodnie z warunkami licencji GNU General Public License opublikowanej przez Free Software Foundation w wersji 2 tej licencji lub (według Twojego wyboru) którejś z późniejszych jej wersji.\n\nNiniejsze oprogramowanie jest rozpowszechniane w nadziei, że będzie użyteczne, ale '''bez żadnej gwarancji'''; nawet bez domniemanej gwarancji '''handlowej''' lub '''przydatności do określonego celu'''.\nZobacz treść licencji GNU General Public License, aby uzyskać więcej szczegółów.\n\nRazem z oprogramowaniem powinieneś otrzymać <doclink href=Copying>kopię licencji GNU General Public License</doclink>. Jeśli jej nie otrzymałeś, napisz do Free Software Foundation, Inc, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. lub [https://www.gnu.org/copyleft/gpl.html przeczytaj ją online].",
"config-sidebar": "* [https://www.mediawiki.org Strona domowa MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Podręcznik użytkownika]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Podręcznik administratora]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Odpowiedzi na często zadawane pytania]\n----\n* <doclink href=Readme>Przeczytaj to</doclink>\n* <doclink href=ReleaseNotes>Informacje o tej wersji</doclink>\n* <doclink href=Copying>Kopiowanie</doclink>\n* <doclink href=UpgradeDoc>Aktualizacja</doclink>",
"config-env-good": "Środowisko oprogramowania zostało sprawdzone.\nMożesz teraz zainstalować MediaWiki.",
"config-env-bad": "Środowisko oprogramowania zostało sprawdzone.\nNie możesz zainstalować MediaWiki.",
"config-env-php": "Zainstalowane jest PHP w wersji $1.",
"config-env-hhvm": "Zainstalowany jest HHVM $1.",
- "config-unicode-using-intl": "Korzystanie z [http://pecl.php.net/intl rozszerzenia intl PECL] do normalizacji Unicode.",
- "config-unicode-pure-php-warning": "<strong>Uwaga:<strong> [http://pecl.php.net/intl Rozszerzenie intl PECL] do obsługi normalizacji Unicode nie jest dostępne. Użyta zostanie mało wydajna zwykła implementacja w PHP.\nJeśli prowadzisz stronę o dużym natężeniu ruchu, powinieneś zapoznać się z informacjami o [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizacji Unicode].",
+ "config-unicode-using-intl": "Korzystanie z [https://pecl.php.net/intl rozszerzenia intl PECL] do normalizacji Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Uwaga:<strong> [https://pecl.php.net/intl Rozszerzenie intl PECL] do obsługi normalizacji Unicode nie jest dostępne. Użyta zostanie mało wydajna zwykła implementacja w PHP.\nJeśli prowadzisz stronę o dużym natężeniu ruchu, powinieneś zapoznać się z informacjami o [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizacji Unicode].",
"config-unicode-update-warning": "<strong>Uwaga:</strong> zainstalowana wersja normalizacji Unicode korzysta z nieaktualnej biblioteki [http://site.icu-project.org/ projektu ICU].\nPowinieneś [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations wykonać aktualizację], jeśli chcesz korzystać w pełni z Unicode.",
"config-no-db": "Nie można odnaleźć właściwego sterownika bazy danych! Musisz zainstalować sterownik bazy danych dla PHP.\nMożna użyć {{PLURAL:$2|następującego typu bazy|następujących typów baz}} danych: $1.\n\nJeśli skompilowałeś PHP samodzielnie, skonfiguruj go ponownie z włączonym klientem bazy danych, na przykład za pomocą polecenia <code>./configure --with-mysqli</code>.\nJeśli zainstalowałeś PHP jako pakiet Debiana lub Ubuntu, musisz również zainstalować np. moduł <code>php5-mysql</code>.",
"config-outdated-sqlite": "'''Ostrzeżenie''': masz SQLite $1, która jest niższa od minimalnej wymaganej wersji $2 . SQLite będzie niedostępne.",
@@ -78,12 +80,11 @@
"config-pcre-no-utf8": "'''Błąd krytyczny''' – wydaje się, że moduł PCRE w PHP został skompilowany bez wsparcia dla UTF‐8.\nMediaWiki wymaga wsparcia dla UTF‐8 do prawidłowego działania.",
"config-memory-raised": "PHP <code>memory_limit</code> było ustawione na $1, zostanie zwiększone do $2.",
"config-memory-bad": "'''Uwaga:''' PHP <code>memory_limit</code> jest ustawione na $1.\nTo jest prawdopodobnie zbyt mało.\nInstalacja może się nie udać!",
- "config-xcache": "[Http://trac.lighttpd.net/xcache/ XCache] jest zainstalowany",
"config-apc": "[Http://www.php.net/apc APC] jest zainstalowany",
"config-apcu": "[http://www.php.net/apcu APCu] jest zainstalowany",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] jest zainstalowany",
- "config-no-cache-apcu": "<strong>Ostrzeżenie:</strong> Nie można znaleźć [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] lub [http://www.iis.net/download/WinCacheForPhp WinCache].\nPamięć podręczna obiektów nie zostanie włączona.",
- "config-mod-security": "''' Ostrzeżenie ''': Serwer sieci web ma włączone [http://modsecurity.org/ mod_security]. Jeśli jest niepoprawnie skonfigurowane, może być przyczyną problemów MediaWiki lub innego oprogramowania, które pozwala użytkownikom na wysyłanie dowolnej zawartości.\nSprawdź w [http://modsecurity.org/documentation/ dokumentacji mod_security] lub skontaktuj się z obsługa hosta, jeśli wystąpią losowe błędy.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] jest zainstalowany",
+ "config-no-cache-apcu": "<strong>Ostrzeżenie:</strong> Nie można było znaleźć [http://www.php.net/apcu APCu] ani [http://www.iis.net/download/WinCacheForPhp WinCache].\nPamięć podręczna obiektów nie została włączona.",
+ "config-mod-security": "''' Ostrzeżenie ''': Serwer sieci web ma włączone [https://modsecurity.org/ mod_security]. Jeśli jest niepoprawnie skonfigurowane, może być przyczyną problemów MediaWiki lub innego oprogramowania, które pozwala użytkownikom na wysyłanie dowolnej zawartości.\nSprawdź w [https://modsecurity.org/documentation/ dokumentacji mod_security] lub skontaktuj się z obsługa hosta, jeśli wystąpią losowe błędy.",
"config-diff3-bad": "Nie znaleziono GNU diff3.",
"config-git": "Znaleziono oprogramowanie kontroli wersji Git: <code>$1</code>.",
"config-git-bad": "Oprogramowanie systemu kontroli wersji Git nie zostało znalezione.",
@@ -98,6 +99,7 @@
"config-no-cli-uploads-check": "'''Ostrzeżenie:''' Katalog domyślny przesyłanych plików ( <code>$1</code> ) nie jest sprawdzona względem luki\n wykonania dowolnego skryptu podczas instalacji CLI w zabezpieczeniach.",
"config-brokenlibxml": "Twój system jest kombinacją wersji PHP i libxml2, która zawiera błędy mogące powodować ukryte uszkodzenia danych w MediaWiki i innych aplikacjach sieci web.\nWykonaj aktualizację libxml2 do wersji 2.7.3 lub późniejszej ([https://bugs.php.net/bug.php?id=45996 bug filed with PHP]).\nInstalacja została przerwana.",
"config-suhosin-max-value-length": "Jest zainstalowany Suhosin i ogranicza długość parametru GET <code>length</code> do $1 bajtów. Komponent ResourceLoader w MediaWiki wykona obejście tego ograniczenia, ale kosztem wydajności.\nJeśli to możliwe, należy ustawić <code>suhosin.get.max_value_length</code> na 1024 lub więcej w <code>php.ini</code> oraz ustawić <code>$wgResourceLoaderMaxQueryLength</code> w <code>LocalSettings.php</code> na tę samą wartość.",
+ "config-using-32bit": "<strong>Uwaga:</strong> twój system wydaje się działać na 32 bitowej architekturze. Jest to [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit niezalecane].",
"config-db-type": "Typ bazy danych:",
"config-db-host": "Adres serwera bazy danych:",
"config-db-host-help": "Jeśli serwer bazy danych jest na innej maszynie, wprowadź jej nazwę domenową lub adres IP.\n\nJeśli korzystasz ze współdzielonego hostingu, operator serwera powinien podać Ci prawidłową nazwę serwera w swojej dokumentacji.\n\nJeśli instalujesz oprogramowanie na serwerze Windows i korzystasz z MySQL, użycie „localhost” może nie zadziałać jako nazwa hosta. Jeśli wystąpi ten problem, użyj „127.0.0.1” jako lokalnego adresu IP.\n\nJeżeli korzystasz z PostgreSQL, pozostaw to pole puste, aby połączyć się poprzez gniazdo Unixa.",
@@ -237,7 +239,7 @@
"config-license-gfdl": "GNU licencja wolnej dokumentacji 1.3 lub nowsza",
"config-license-pd": "Domena publiczna",
"config-license-cc-choose": "Wybierz własną licencję Creative Commons",
- "config-license-help": "Wiele publicznych wiki umieszcza wszystkie dopisane treści na [http://freedomdefined.org/Definition wolnej licencji].\nPomaga to tworzyć poczucie wspólnoty i zachęca do długoterminowego wkładu.\nNie jest to zazwyczaj konieczne w prywatnych lub firmowych wiki.\n\nJeśli chcesz móc użyć tekstu z Wikipedii i chcesz Wikipedia mogła zaakceptować tekst skopiowany z twojej wiki, należy wybrać <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia używała poprzednio GNU Free Documentation License.\nGFDL jest poprawną licencję, ale trudno ją zrozumieć.\nTrudno także ponowne użyć zawartości na licencji GFDL.",
+ "config-license-help": "Wiele publicznych wiki umieszcza wszystkie dopisane treści na [https://freedomdefined.org/Definition wolnej licencji].\nPomaga to tworzyć poczucie wspólnoty i zachęca do długoterminowego wkładu.\nNie jest to zazwyczaj konieczne w prywatnych lub firmowych wiki.\n\nJeśli chcesz móc użyć tekstu z Wikipedii i chcesz Wikipedia mogła zaakceptować tekst skopiowany z twojej wiki, należy wybrać <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia używała poprzednio GNU Free Documentation License.\nGFDL jest poprawną licencję, ale trudno ją zrozumieć.\nTrudno także ponowne użyć zawartości na licencji GFDL.",
"config-email-settings": "Ustawienia e-maili",
"config-enable-email": "Włącz wychodzące wiadomości e–mail",
"config-enable-email-help": "Jeśli chcesz, aby działał e-mail, [http://www.php.net/manual/en/mail.configuration.php Ustawienia poczty PHP] muszą być poprawnie wprowadzone.\nJeśli nie chcesz jakichś funkcji poczty e-mail, można je wyłączyć tutaj.",
@@ -267,7 +269,7 @@
"config-cache-options": "Ustawienia buforowania obiektów:",
"config-cache-help": "Buforowanie obiektów jest używane do przyspieszenia MediaWiki przez trzymanie w pamięci podręcznej często używanych danych.\nŚrednie oraz duże witryny są wysoce zachęcane by je włączyć, ale małe witryny również dostrzegą korzyści.",
"config-cache-none": "Brak buforowania (wszystkie funkcje będą działać, ale mogą wystąpić kłopoty z wydajnością na dużych witrynach wiki)",
- "config-cache-accel": "Buforowania obiektów PHP (APC, APCu, XCache lub WinCache)",
+ "config-cache-accel": "Buforowania obiektów PHP (APC, APCu lub WinCache)",
"config-cache-memcached": "Użyj Memcached (wymaga dodatkowej instalacji i konfiguracji)",
"config-memcached-servers": "Serwery Memcached:",
"config-memcached-help": "Lista adresów IP do wykorzystania przez Memcached.\nAdresy powinny być umieszczane po jednym w linii i określać również wykorzystywany port. Na przykład:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -323,11 +325,14 @@
"config-install-mainpage-failed": "Nie udało się wstawić strony głównej: $1",
"config-install-done": "<strong>'''Gratulacje!</strong>\nUdało Ci się zainstalować MediaWiki.\n\nInstalator wygenerował plik konfiguracyjny <code>LocalSettings.php</code>.\n\nMusisz go pobrać i umieścić w katalogu głównym Twojej instalacji wiki (tym samym katalogu co index.php). Pobieranie powinno zacząć się automatycznie.\n\nJeżeli pobieranie nie zostało zaproponowane lub jeśli użytkownik je anulował, można ponownie uruchomić pobranie klikając poniższe łącze:\n\n$3\n\n<strong>Uwaga</strong>: Jeśli nie zrobisz tego teraz, wygenerowany plik konfiguracyjny nie będzie już dostępny po zakończeniu instalacji.\n\nPo załadowaniu pliku konfiguracyjnego możesz <strong>[$2 wejść na wiki]</strong>.",
"config-install-done-path": "<strong>Gratulacje!</strong>\nZainstalowałeś właśnie MediaWiki.\n\nInstalator wygenerował plik <code>LocalSettings.php</code>.\nZawiera całą Twoją konfigurację.\n\nMusisz go pobrać i umieścić w <code>$4</code>. Pobieranie powinno rozpocząć się automatycznie.\n\nJeżeli nie pojawiła się informacja o pobieraniu lub jeżeli ja anulowałeś, kliknij poniższy link:\n\n$3\n\n<strong>Uwaga:</strong> Jeżeli nie zrobisz tego teraz, wygenerowany plik konfiguracyjny nie będzie potem dostępny, jeżeli wyjdziesz z instalacji bez jego pobrania.\n\nGdy to będzie zrobione, możesz <strong>[$2 wejść na swoją wiki]</strong>.",
+ "config-install-success": "MediaWiki została pomyślnie zainstalowana. Możesz teraz\nodwiedzić <$1$2>, aby zobaczyć swoją wiki.\nJeśli masz pytania, sprawdź naszą listę najczęściej zadawanych pytań:\n<https://www.mediawiki.org/wiki/Manual:FAQ> lub użyj jednej z\nform wsparcia odsyłanej z tej strony.",
"config-download-localsettings": "Pobierz <code>LocalSettings.php</code>",
"config-help": "pomoc",
"config-help-tooltip": "kliknij, aby rozwinąć",
"config-nofile": "Nie udało się odnaleźć pliku \"$1\". Czy nie został usunięty?",
"config-extension-link": "Czy wiesz, że twoja wiki obsługuje [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions rozszerzenia]?\n\nMożesz przejrzeć [https://www.mediawiki.org/wiki/Category:Extensions_by_category rozszerzenia według kategorii] lub [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix], aby zobaczyć pełną listę rozszerzeń.",
+ "config-skins-screenshots": "$1 (zrzut ekranu: $2)",
+ "config-screenshot": "zrzut ekranu",
"mainpagetext": "<strong>Instalacja MediaWiki powiodła się.</strong>",
"mainpagedocfooter": "Zapoznaj się z [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Podręcznikiem użytkownika] zawierającym informacje o tym jak korzystać z oprogramowania wiki.\n\n== Na początek ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista ustawień konfiguracyjnych]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Komunikaty o nowych wersjach MediaWiki (lista dyskusyjna)]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Przetłumacz MediaWiki na swój język]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Dowiedz się, jak walczyć ze spamem na swojej wiki]"
}
diff --git a/www/wiki/includes/installer/i18n/pms.json b/www/wiki/includes/installer/i18n/pms.json
index e97db426..e89b5db7 100644
--- a/www/wiki/includes/installer/i18n/pms.json
+++ b/www/wiki/includes/installer/i18n/pms.json
@@ -47,14 +47,14 @@
"config-help-restart": "Veul-lo scancelé tùit ij dat salvà ch'a l'ha anserì e anandié torna ël process d'instalassion?",
"config-restart": "É!, felo torna parte",
"config-welcome": "=== Contròj d'ambient ===\nDle verìfiche ëd base a saran adess fàite për vëdde se st'ambient a va bin për l'instalassion ëd MediaWiki.\nCh'as visa d'anserì coste anformassion s'a sërca d'agiut su com completé l'instalassion.",
- "config-copyright": "=== Drit d'Autor e Condission ===\n\n$1\n\nCost-sì a l'é un programa lìber e a gràtis: a peul ridistribuilo e/o modifichelo sota le condission dla licensa pùblica general GNU com publicà da la Free Software Foundation; la version 2 dla Licensa, o (a toa sèrnìa) qualsëssìa version pi recenta.\n\nCost programa a l'é distribuì ant la speransa ch'a sia ùtil, ma '''sensa gnun-e garansìe'''; sensa gnanca la garansia implìssita ëd '''comersiabilità''' o '''d'esse adat a un but particolar'''.\n\nA dovrìa avèj arseivù <doclink href=Copying>na còpia ëd la licensa pùblica general GNU</doclink> ansema a sto programa; dësnò, ch'a scriva a la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA opura [http://www.gnu.org/copyleft/gpl.html ch'a la lesa an linia].",
+ "config-copyright": "=== Drit d'Autor e Condission ===\n\n$1\n\nCost-sì a l'é un programa lìber e a gràtis: a peul ridistribuilo e/o modifichelo sota le condission dla licensa pùblica general GNU com publicà da la Free Software Foundation; la version 2 dla Licensa, o (a toa sèrnìa) qualsëssìa version pi recenta.\n\nCost programa a l'é distribuì ant la speransa ch'a sia ùtil, ma '''sensa gnun-e garansìe'''; sensa gnanca la garansia implìssita ëd '''comersiabilità''' o '''d'esse adat a un but particolar'''.\n\nA dovrìa avèj arseivù <doclink href=Copying>na còpia ëd la licensa pùblica general GNU</doclink> ansema a sto programa; dësnò, ch'a scriva a la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA opura [https://www.gnu.org/copyleft/gpl.html ch'a la lesa an linia].",
"config-sidebar": "* [https://www.mediawiki.org Intrada MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guida dl'Utent]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guida dl'Aministrator]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Soens an ciamo]\n----\n* <doclink href=Readme>Ch'am lesa</doclink>\n* <doclink href=ReleaseNotes>Nòte ëd publicassion</doclink>\n* <doclink href=Copying>Còpia</doclink>\n* <doclink href=UpgradeDoc>Agiornament</doclink>",
"config-env-good": "L'ambient a l'é stàit controlà.\nIt peule instalé MediaWiki.",
"config-env-bad": "L'ambient a l'é stàit controlà.\nIt peule pa instalé MediaWiki.",
"config-env-php": "PHP $1 a l'é instalà.",
"config-env-hhvm": "HHVM $1 a l'é instalà.",
- "config-unicode-using-intl": "As deuvra l'[http://pecl.php.net/intl estension intl PECL] për la normalisassion Unicode.",
- "config-unicode-pure-php-warning": "'''Avis:''' L'[http://pecl.php.net/intl estension intl PECL] a l'é pa disponìbil për gestì la normalisassion Unicode, da già che l'implementassion an PHP pur a faliss për lentëssa.\nS'a gestiss un sit a àut tràfich, a dovrìa lese cheicòs an sla [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisassion Unicode].",
+ "config-unicode-using-intl": "As deuvra l'[https://pecl.php.net/intl estension intl PECL] për la normalisassion Unicode.",
+ "config-unicode-pure-php-warning": "'''Avis:''' L'[https://pecl.php.net/intl estension intl PECL] a l'é pa disponìbil për gestì la normalisassion Unicode, da già che l'implementassion an PHP pur a faliss për lentëssa.\nS'a gestiss un sit a àut tràfich, a dovrìa lese cheicòs an sla [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisassion Unicode].",
"config-unicode-update-warning": "'''Avis:''' La version instalà dlë spassiador ëd normalisassion Unicode a deuvra na version veja ëd la librarìa dël [http://site.icu-project.org/ proget ICU].\nA dovrìa fé n'[https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations agiornament] s'a l'é anteressà a dovré Unicode.",
"config-no-db": "Impossìbil trové un pilòta ëd base ëd dàit bon! A dev instalé un pilòta ëd base ëd dàit për PHP.\n{{PLURAL:$2|La sòrt ëd base ëd dàit mantnùa a l'é costa|Le sòrt ëd base ëd dàit mantùe a son coste}} sì-dapress: $1.\n\nS'a l'é compilasse PHP chiel-midem, ch'a lo configura torna con un client ëd base ëd dàit abilità, për esempi an dovrand <code>./configure --with-mysqli</code>.\nS'a l'ha instalà PHP dai pachèt Debian o Ubuntu, antlora a dev ëdcò anstalé, për esempi, ël mòdul <code>php5-mysql</code>.",
"config-outdated-sqlite": "'''Avis''': chiel a l'ha SQLite $1, che a l'é pi vej che la version mìnima dont a-i é damanca $2. SQLite a sarà pa disponìbil.",
@@ -62,10 +62,9 @@
"config-pcre-no-utf8": "'''Fatal''': ël mòdul PCRE ëd PHP a smija esse compilà sensa l'apògg PCRE_UTF8.\nMediaWiki a ciama l'apògg d'UTF8 për marcé për da bin.",
"config-memory-raised": "<code>memory_limit</code> ëd PHP a l'é $1, aussà a $2.",
"config-memory-bad": "'''Avis:''' <code>memory_limit</code> ëd PHP a l'é $1.\nSossì a l'é probabilment tròp bass.\nL'instalassion a peul falì!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] a l'é instalà",
"config-apc": "[http://www.php.net/apc APC] a l'é instalà",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] a l'é instalà",
- "config-mod-security": "'''Avis''': Sò servent për l'aragnà a l'ha [http://modsecurity.org/ mod_security] abilità. Se mal configurà, a peul causé dij problema për MediaWiki o d'àutri programa ch'a përmëtto a j'utent dë spedì un contnù qualsëssìa.\nCh'a fasa arferiment a la [http://modsecurity.org/documentation/ mod_security documentassion] o ch'a contata l'echip ëd sò servissi s'a-j rivo dj'eror casuaj.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] a l'é instalà",
+ "config-mod-security": "'''Avis''': Sò servent për l'aragnà a l'ha [https://modsecurity.org/ mod_security] abilità. Se mal configurà, a peul causé dij problema për MediaWiki o d'àutri programa ch'a përmëtto a j'utent dë spedì un contnù qualsëssìa.\nCh'a fasa arferiment a la [https://modsecurity.org/documentation/ mod_security documentassion] o ch'a contata l'echip ëd sò servissi s'a-j rivo dj'eror casuaj.",
"config-diff3-bad": "GNU diff3 pa trovà.",
"config-imagemagick": "Trovà ImageMagick: <code>$1</code>.\nLa miniaturisassion ëd figure a sarà abilità s'it abìlite le carie.",
"config-gd": "Trovà la librarìa gràfica antëgrà GD.\nLa miniaturisassion ëd figure a sarà abilità s'a abìlita ij cariament.",
@@ -204,7 +203,7 @@
"config-license-gfdl": "Licensa GNU Free Documentation 1.3 o pi neuva",
"config-license-pd": "Domini Pùblich",
"config-license-cc-choose": "Selessioné na licensa Creative Commons përsonalisà",
- "config-license-help": "Vàire wiki pùbliche a buto tute le contribussion sota na [http://freedomdefined.org/Definition licensa lìbera]. Sòn a giuta a creé un sens d'apartenensa a la comunità e a ancoragia ëd contribussion ëd longa durà.\nA l'é generalment nen necessari për na wiki privà o d'asienda.\n\nS'a veul podèj dovré dij test da Wikipedia, e a veul che Wikipedia a aceta dij test copià da soa wiki, a dovrìa serne '''Creative Commons Attribution Share Alike'''.\n\nWikipedia prima a dovrava la GNU Free Documentation License.\nLa GDFL a l'é anco' na licensa bon-a, ma a l'é malfé da capila.\nA l'é ëdcò mal fé riutilisé dël contnù licensià sota la GDFL.",
+ "config-license-help": "Vàire wiki pùbliche a buto tute le contribussion sota na [https://freedomdefined.org/Definition licensa lìbera]. Sòn a giuta a creé un sens d'apartenensa a la comunità e a ancoragia ëd contribussion ëd longa durà.\nA l'é generalment nen necessari për na wiki privà o d'asienda.\n\nS'a veul podèj dovré dij test da Wikipedia, e a veul che Wikipedia a aceta dij test copià da soa wiki, a dovrìa serne '''Creative Commons Attribution Share Alike'''.\n\nWikipedia prima a dovrava la GNU Free Documentation License.\nLa GDFL a l'é anco' na licensa bon-a, ma a l'é complicà capila.\nA l'é ëdcò mal fé riutilisé dël contnù licensià sota la GDFL.",
"config-email-settings": "Ampostassion ëd pòsta eletrònica",
"config-enable-email": "Abilité ij mëssagi ëd pòsta eletrònica an surtìa",
"config-enable-email-help": "S'a veul che la pòsta eletrònica a marcia, j'[http://www.php.net/manual/en/mail.configuration.php ampostassion ëd pòsta eletrònica PHP] a devo esse configurà për da bin.\nS'a veul pa 'd funsion ëd pòsta eletrònica, a dev disabiliteje ambelessì.",
diff --git a/www/wiki/includes/installer/i18n/ps.json b/www/wiki/includes/installer/i18n/ps.json
index 5e8d7dec..68c2376b 100644
--- a/www/wiki/includes/installer/i18n/ps.json
+++ b/www/wiki/includes/installer/i18n/ps.json
@@ -30,9 +30,8 @@
"config-restart": "هو، سر له نوي يې پيل کړه",
"config-env-php": "د $1 PHP نصب شو.",
"config-env-hhvm": "HHVM $1 نصب شو.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] نصب شو",
"config-apc": "[http://www.php.net/apc APC] نصب شو",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] نصب شو",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] نصب شو",
"config-diff3-bad": "جي ان يو ډيف3 و نه موندل شو.",
"config-using-server": "د پالنگر نوم \"<nowiki>$1</nowiki>\" کارېږي.",
"config-using-uri": "د پالنگر URL \"<nowiki>$1$2</nowiki>\" کارېږي.",
diff --git a/www/wiki/includes/installer/i18n/pt-br.json b/www/wiki/includes/installer/i18n/pt-br.json
index 7a490513..b6b1357a 100644
--- a/www/wiki/includes/installer/i18n/pt-br.json
+++ b/www/wiki/includes/installer/i18n/pt-br.json
@@ -62,14 +62,14 @@
"config-help-restart": "Deseja limpar todos os dados salvos que você introduziu e reiniciar o processo de instalação?",
"config-restart": "Sim, reiniciar",
"config-welcome": "=== Verificações de ambiente ===\nSerão realizadas verificações básicas para determinar se este ambiente é apropriado para a instalação do MediaWiki.\nLembre-se de incluir estas informações se for procurar por suporte para como concluir a instalação.",
- "config-copyright": "=== Direitos autorais e Termos de uso ===\n\n$1\n\nEste programa é software livre; você pode redistribuí-lo e/ou modificá-lo nos termos da licença GNU General Public License tal como publicada pela Free Software Foundation; tanto a versão 2 da Licença, como (por opção sua) qualquer versão posterior.\n\nEste programa é distribuído na esperança de que seja útil, mas <strong>sem qualquer garantia</strong>; inclusive, sem a garantia implícita da <strong>possibilidade de ser comercializado</strong> ou de <strong>adequação para qualquer finalidade específica</strong>.\nConsulte a licença GNU General Public License para mais detalhes.\n\nEm conjunto com este programa você deve ter recebido <doclink href=Copying>uma cópia da licença GNU General Public License</doclink>; se não a recebeu, peça-a por escrito para Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ou [http://www.gnu.org/copyleft/gpl.html leia-a na internet].",
+ "config-copyright": "=== Direitos autorais e Termos de uso ===\n\n$1\n\nEste programa é software livre; você pode redistribuí-lo e/ou modificá-lo nos termos da licença GNU General Public License tal como publicada pela Free Software Foundation; tanto a versão 2 da Licença, como (por opção sua) qualquer versão posterior.\n\nEste programa é distribuído na esperança de que seja útil, mas <strong>sem qualquer garantia</strong>; inclusive, sem a garantia implícita da <strong>possibilidade de ser comercializado</strong> ou de <strong>adequação para qualquer finalidade específica</strong>.\nConsulte a licença GNU General Public License para mais detalhes.\n\nEm conjunto com este programa você deve ter recebido <doclink href=Copying>uma cópia da licença GNU General Public License</doclink>; se não a recebeu, peça-a por escrito para Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ou [https://www.gnu.org/copyleft/gpl.html leia-a na internet].",
"config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki Página principal do MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Manual do usuário]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Manual do administrador]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Leia-me</doclink>\n* <doclink href=ReleaseNotes>Notas de lançamento</doclink>\n* <doclink href=Copying>Licença</doclink>\n* <doclink href=UpgradeDoc>Atualizando</doclink>",
"config-env-good": "O ambiente foi verificado.\nVocê pode instalar o MediaWiki.",
"config-env-bad": "O ambiente foi verificado.\nVocê não pode instalar o MediaWiki.",
"config-env-php": "O PHP $1 está instalado.",
"config-env-hhvm": "O HHVM $1 está instalado.",
- "config-unicode-using-intl": "Usando a [http://pecl.php.net/intl extensão intl PECL] para a normalização Unicode.",
- "config-unicode-pure-php-warning": "<strong>Aviso</strong>: A [http://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode, abortando e passando para a lenta implementação de PHP puro.\nSe o seu site tem um alto volume de tráfego, informe-se sobre a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalização Unicode].",
+ "config-unicode-using-intl": "Usando a [https://pecl.php.net/intl extensão intl PECL] para a normalização Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Aviso</strong>: A [https://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode, abortando e passando para a lenta implementação de PHP puro.\nSe o seu site tem um alto volume de tráfego, informe-se sobre a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalização Unicode].",
"config-unicode-update-warning": "<strong>Aviso:</strong> A versão instalada do wrapper de normalização Unicode usa uma versão mais antiga da biblioteca do [http://www.site.icu-project.org/projeto ICU].\nVocê deve [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations atualizar] se você tem quaisquer preocupações com o uso do Unicode.",
"config-no-db": "Não foi possível encontrar um driver apropriado para a banco de dados! Você precisa instalar um driver de banco de dados para PHP. {{PLURAL:$2|É aceito o seguinte tipo|São aceitos os seguintes tipos}} de banco de dados: $1.\n\nSe você compilou o PHP, reconfigure-o com um cliente de banco de dados ativado, por exemplo, usando <code>./configure --with-mysqli</code>.\nSe instalou o PHP a partir de um pacote Debian ou Ubuntu, então também precisa instalar, por exemplo, o pacote <code>php5-mysql</code>.",
"config-outdated-sqlite": "<strong>Aviso:</strong> você tem o SQLite versão $1, que é menor do que a versão mínima necessária $2. O SQLite não estará disponível.",
@@ -78,12 +78,11 @@
"config-pcre-no-utf8": "<strong>Erro fatal:</strong> O módulo PCRE do PHP parece ser compilado sem suporte a PCRE_UTF8.\nO MediaWiki requer suporte a UTF-8 para funcionar corretamente.",
"config-memory-raised": "A configuração <code>memory_limit</code> do PHP era $1; foi aumentada para $2.",
"config-memory-bad": "<strong>Aviso:</strong> A configuração <code>memory_limit</code> do PHP é $1.\nIsso provavelmente é muito baixo.\nA instalação pode falhar!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] está instalado",
"config-apc": "[http://www.php.net/apc APC] está instalado",
"config-apcu": "[http://www.php.net/apcu APCu] está instalado",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] está instalado",
- "config-no-cache-apcu": "<strong>Aviso:</strong> Não se pode encontrar [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] ou [http://www.iis.net/download/WinCacheForPhp WinCache].\nO caching de objetos não foi ativado.",
- "config-mod-security": "<strong>Aviso:</strong> Seu servidor web tem [http://modsecurity.org/ mod_security2] habilitado. Muitas configurações comuns de módulo podem causar problemas para o MediaWiki ou outro software que permite aos usuários postar conteúdo arbitrário.\nSe possível, ele dever ser desativad. Consulte a [http://modsecurity.org/documentation/ documentação do mod_security] ou entre em contato com o suporte do seu host se você encontrar erros aleatórios.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] está instalado",
+ "config-no-cache-apcu": "<strong>Aviso:</strong> Não foram encontrados o [http://www.php.net/apcu APCu], ou o [http://www.iis.net/download/WinCacheForPhp WinCache].\nA cache de objetos não está ativa.",
+ "config-mod-security": "<strong>Aviso:</strong> Seu servidor web tem [https://modsecurity.org/ mod_security2] habilitado. Muitas configurações comuns de módulo podem causar problemas para o MediaWiki ou outro software que permite aos usuários postar conteúdo arbitrário.\nSe possível, ele dever ser desativad. Consulte a [https://modsecurity.org/documentation/ documentação do mod_security] ou entre em contato com o suporte do seu host se você encontrar erros aleatórios.",
"config-diff3-bad": "O GNU diff3 não foi encontrado.",
"config-git": "Foi encontrado o software de controle de versão Git: <code>$1</code>.",
"config-git-bad": "O software de controle de versão Git não foi encontrado.",
@@ -241,7 +240,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 ou posterior",
"config-license-pd": "Domínio público",
"config-license-cc-choose": "Selecionar uma licença personalizada da organização Creative Commons",
- "config-license-help": "Muitas wikis públicas colocam todas as contribuições sob uma [http://freedomdefined.org/Definition licença livre].\nIsso ajuda a criar um senso de propriedade da comunidade e incentiva a contribuição de longo prazo.\nGeralmente não é necessário para uma empresa privada ou wiki corporativa.\nSe você quiser poder usar o texto da Wikipédia e quiser que a Wikipédia possa aceitar o texto copiado da sua wiki, você deve escolher <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nA Wikipédia usou anteriormente a Licença de Documentação Livre GNU.\n A GFDL é uma licença válida, mas é difícil de entender.\nTambém é difícil reutilizar conteúdo licenciado sob o GFDL.",
+ "config-license-help": "Muitas wikis públicas colocam todas as contribuições sob uma [https://freedomdefined.org/Definition licença livre].\nIsso ajuda a criar um senso de propriedade da comunidade e incentiva a contribuição de longo prazo.\nGeralmente não é necessário para uma empresa privada ou wiki corporativa.\nSe você quiser poder usar o texto da Wikipédia e quiser que a Wikipédia possa aceitar o texto copiado da sua wiki, você deve escolher <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nA Wikipédia usou anteriormente a Licença de Documentação Livre GNU.\n A GFDL é uma licença válida, mas é difícil de entender.\nTambém é difícil reutilizar conteúdo licenciado sob o GFDL.",
"config-email-settings": "Configurações de e-mail",
"config-enable-email": "Ativar envio de e-mail",
"config-enable-email-help": "Se você quer que o e-mail funcione, estas [http://www.php.net/manual/en/mail.configuration.php configurações de e-mail PHP] precisam ser configuradas corretamente.\nSe você não quiser usar nenhuma das funcionalidades, você pode desabilitá-las aqui.",
@@ -271,7 +270,7 @@
"config-cache-options": "Configuração da cache de objetos:",
"config-cache-help": "O cache de objetos é usado para melhorar o desempenho do MediaWiki, armazenando dados usados com frequência.\nSites de tamanho médio ou grande são altamente encorajados a ativar esta funcionalidade e os sites pequenos também terão alguns benefícios em fazê-lo.",
"config-cache-none": "Sem cache (nenhuma funcionalidade é removida, mas a velocidade pode ser afetada em wikis maiores)",
- "config-cache-accel": "Cache de objetos PHP (APC, APCu, XCache ou WinCache)",
+ "config-cache-accel": "Cache de objetos do PHP (APC, APCu, ou WinCache)",
"config-cache-memcached": "Usar Memcached (requer instalação e configurações adicionais)",
"config-memcached-servers": "Servidores Memcached:",
"config-memcached-help": "Lista de endereços IP a serem usados para Memcached.\nDeve especificar um por linha e especificar a porta a ser utilizada. Por exemplo:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -327,6 +326,7 @@
"config-install-mainpage-failed": "Não foi possível inserir a página principal: $1",
"config-install-done": "<strong>Parabéns!</strong>\nVocê instalou o MediaWiki.\n\nO instalador gerou um arquivo <code>LocalSettings.php</code>.\nEste arquivo contém todas as suas configurações.\n\nVocê precisa fazer o download desse arquivo e colocá-lo na raiz da sua instalação (o mesmo diretório onde está o arquivo index.php). O download deve iniciar automaticamente.\n\nSe o download não foi iniciado ou se ele foi cancelado, você pode recomeçá-lo clicando no link abaixo:\n\n$3\n\n<strong>Nota:</strong> Se você não fizer isso agora, o arquivo de configuração que foi gerado não estará mais disponível se você sair da instalação sem fazer o download.\n\nQuando isso tiver sido feito, você pode <strong>[$2 entrar na sua wiki]</strong>.",
"config-install-done-path": "<strong>Parabéns!</strong>\nVocê instalou o MediaWiki.\n\nO instalador gerou um arquivo <code>LocalSettings.php</code>.\nEste arquivo contém todas as suas configurações.\n\nVocê precisa fazer o download desse arquivo e colocá-lo em <code>$4</code>. O download deve iniciar automaticamente.\n\nSe o download não foi iniciado ou se ele foi cancelado, você pode recomeçá-lo clicando no link abaixo:\n\n$3\n\n<strong>Nota:</strong> Se você não fizer isso agora, o arquivo de configuração que foi gerado não estará mais disponível se você sair da instalação sem fazer o download.\n\nQuando isso tiver sido feito, você pode <strong>[$2 entrar na sua wiki]</strong>.",
+ "config-install-success": "O MediaWiki foi instalado. Já pode visitar <$1$2> para ver a sua wiki.\nSe tiver dúvidas, veja a nossa lista de perguntas frequentes,\n<https://www.mediawiki.org/wiki/Manual:FAQ/pt-br>, ou utilize um dos fóruns de suporte indicados nessa página.",
"config-download-localsettings": "Baixar <code>LocalSettings.php</code>",
"config-help": "ajuda",
"config-help-tooltip": "clique para expandir",
diff --git a/www/wiki/includes/installer/i18n/pt.json b/www/wiki/includes/installer/i18n/pt.json
index a6ebd92a..21410c8d 100644
--- a/www/wiki/includes/installer/i18n/pt.json
+++ b/www/wiki/includes/installer/i18n/pt.json
@@ -18,7 +18,9 @@
"Macofe",
"Diniscoelho",
"Ruila",
- "Seb35"
+ "Seb35",
+ "MokaAkashiyaPT",
+ "Athena in Wonderland"
]
},
"config-desc": "O instalador do MediaWiki",
@@ -35,7 +37,7 @@
"config-session-expired": "Os seus dados de sessão parecem ter expirado.\nAs sessões estão configuradas para uma duração de $1.\nPode aumentar esta duração configurando <code>session.gc_maxlifetime</code> no php.ini.\nReinicie o processo de instalação.",
"config-no-session": "Os seus dados de sessão foram perdidos!\nVerifique o seu php.ini e certifique-se de que em <code>session.save_path</code> está definido um diretório apropriado.",
"config-your-language": "A sua língua:",
- "config-your-language-help": "Selecione o idioma que será usado durante o processo de instalação.",
+ "config-your-language-help": "Selecione a língua que será usada durante o processo de instalação.",
"config-wiki-language": "Língua da wiki:",
"config-wiki-language-help": "Selecione a língua que será predominante na wiki.",
"config-back": "← Voltar",
@@ -58,28 +60,27 @@
"config-help-restart": "Deseja limpar todos os dados gravados que introduziu e reiniciar o processo de instalação?",
"config-restart": "Sim, reiniciar",
"config-welcome": "=== Verificações do ambiente ===\nSerão agora realizadas verificações básicas para determinar se este ambiente é apropriado para instalação do MediaWiki.\nLembre-se de fornecer esta informação se necessitar de pedir ajuda para concluir a instalação.",
- "config-copyright": "=== Direitos de autor e Condições de uso ===\n\n$1\n\nEste programa é software livre; pode redistribuí-lo e/ou modificá-lo nos termos da licença GNU General Public License, tal como publicada pela Free Software Foundation; tanto a versão 2 da Licença, como (por opção sua) qualquer versão posterior.\n\nEste programa é distribuído na esperança de que seja útil, mas '''sem qualquer garantia'''; inclusive, sem a garantia implícita da '''possibilidade de ser comercializado''' ou de '''adequação para qualquer finalidade específica'''.\nConsulte a licença GNU General Public License para mais detalhes.\n\nEm conjunto com este programa deve ter recebido <doclink href=Copying>uma cópia da licença GNU General Public License</doclink>; se não a recebeu, peça-a por escrito a Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ou [http://www.gnu.org/copyleft/gpl.html leia-a na internet].",
+ "config-copyright": "=== Direitos de autor e Condições de uso ===\n\n$1\n\nEste programa é software livre; pode redistribuí-lo e/ou modificá-lo nos termos da licença GNU General Public License, tal como publicada pela Free Software Foundation; tanto a versão 2 da Licença, como (por opção sua) qualquer versão posterior.\n\nEste programa é distribuído na esperança de que seja útil, mas '''sem qualquer garantia'''; inclusive, sem a garantia implícita da '''possibilidade de ser comercializado''' ou de '''adequação para qualquer finalidade específica'''.\nConsulte a licença GNU General Public License para mais detalhes.\n\nEm conjunto com este programa deve ter recebido <doclink href=Copying>uma cópia da licença GNU General Public License</doclink>; se não a recebeu, peça-a por escrito a Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ou [https://www.gnu.org/copyleft/gpl.html leia-a na internet].",
"config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/pt Página principal do MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/pt Ajuda]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/pt Manual técnico]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Leia-me</doclink>\n* <doclink href=ReleaseNotes>Notas de lançamento</doclink>\n* <doclink href=Copying>Cópia</doclink>\n* <doclink href=UpgradeDoc>Atualização</doclink>",
"config-env-good": "O ambiente foi verificado.\nPode instalar o MediaWiki.",
"config-env-bad": "O ambiente foi verificado.\nNão pode instalar o MediaWiki.",
"config-env-php": "O PHP $1 está instalado.",
"config-env-hhvm": "HHVM $1 está instalado.",
- "config-unicode-using-intl": "A usar a [http://pecl.php.net/intl extensão intl PECL] para a normalização Unicode.",
- "config-unicode-pure-php-warning": "<strong>Aviso:</strong> A [http://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode. Irá recorrer-se à implementação em PHP puro, que é mais lenta.\nSe o seu site tem alto volume de tráfego, devia informar-se um pouco sobre a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations/pt normalização Unicode].",
+ "config-unicode-using-intl": "A usar a [https://pecl.php.net/intl extensão intl PECL] para a normalização Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Aviso:</strong> A [https://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode. Irá recorrer-se à implementação em PHP puro, que é mais lenta.\nSe o seu sítio tem alto volume de tráfego, devia informar-se um pouco sobre a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations/pt normalização Unicode].",
"config-unicode-update-warning": "<strong>Aviso:</strong> A versão instalada do wrapper de normalização Unicode usa uma versão mais antiga da biblioteca do [http://site.icu-project.org/ projeto ICU].\nDevia [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations atualizá-la] se tem quaisquer preocupações sobre o uso do Unicode.",
"config-no-db": "Não foi possível encontrar um controlador apropriado da base de dados! Precisa de instalar um controlador da base de dados para o PHP. {{PLURAL:$2|É aceite o seguinte tipo|São aceites os seguintes tipos}} de base de dados: $1.\n\nSe fez a compilação do PHP, reconfigure-o com um cliente de base de dados ativado; por exemplo, usando <code>./configure --with-mysqli</code>.\nSe instalou o PHP a partir de um pacote Debian ou Ubuntu, então precisa de instalar também, por exemplo, o pacote <code>php5-mysql</code>.",
"config-outdated-sqlite": "<strong>Aviso:</strong> Tem a versão $1 do SQLite, que é anterior à versão mínima necessária, a $2. O SQLite não estará disponível.",
"config-no-fts3": "<strong>Aviso:</strong> O SQLite foi compilado sem o módulo [//sqlite.org/fts3.html FTS3]; as funcionalidades de pesquisa não estarão disponíveis nesta instalação.",
- "config-pcre-old": "<strong>Erro fatal:</strong> É necessário o PCRE $1 ou versão posterior.\nO <i>link</i> do seu binário PHP foi feito com o PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Mais informações].",
+ "config-pcre-old": "<strong>Erro fatal:</strong> É necessário o PCRE $1 ou versão posterior.\nO seu binário PHP foi linkado com o PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Mais informações].",
"config-pcre-no-utf8": "'''Erro fatal''': O módulo PCRE do PHP parece ter sido compilado sem suporte PCRE_UTF8.\nO MediaWiki necessita do suporte UTF-8 para funcionar corretamente.",
"config-memory-raised": "A configuração <code>memory_limit</code> do PHP era $1; foi aumentada para $2.",
"config-memory-bad": "<strong>Aviso:</strong> A configuração <code>memory_limit</code> do PHP é $1.\nIsto é provavelmente demasiado baixo.\nA instalação poderá falhar!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] instalada",
"config-apc": "[http://www.php.net/apc APC] instalada",
"config-apcu": "[http://www.php.net/apcu APCu] instalado",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] instalada",
- "config-no-cache-apcu": "<strong>Aviso:</strong> Não foram encontrados o [http://www.php.net/apcu APCu], o [http://xcache.lighttpd.net/ XCache] ou o [http://www.iis.net/download/WinCacheForPhp WinCache].\nA cache de objetos não está ativa.",
- "config-mod-security": "<strong>Aviso:</strong> O seu servidor de Internet tem o [http://modsecurity.org/ mod_security]/mod_security2 ativado. Muitas das suas configurações normais podem causar problemas ao MediaWiki e a outros programas, permitindo que os utilizadores publiquem conteúdos arbitrários.\nSe possível, isto deve ser desativado. Se não, consulte a [http://modsecurity.org/documentation/ mod_security documentação] ou peça apoio ao fornecedor do alojamento do seu servidor se encontrar erros aleatórios.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] instalada",
+ "config-no-cache-apcu": "<strong>Aviso:</strong> Não foi encontrado o [http://www.php.net/apcu APCu] nem o [http://www.iis.net/download/WinCacheForPhp WinCache].\nA cache de objetos não está ativa.",
+ "config-mod-security": "<strong>Aviso:</strong> O seu servidor de Internet tem o [https://modsecurity.org/ mod_security]/mod_security2 ativado. Muitas das suas configurações normais podem causar problemas ao MediaWiki e a outros programas, permitindo que os utilizadores publiquem conteúdos arbitrários.\nSe possível, isto deve ser desativado. Se não, consulte a [https://modsecurity.org/documentation/ mod_security documentação] ou peça apoio ao fornecedor do alojamento do seu servidor se encontrar erros aleatórios.",
"config-diff3-bad": "O GNU diff3 não foi encontrado.",
"config-git": "Foi encontrado o software de controlo de versões Git: <code>$1</code>.",
"config-git-bad": "Não foi encontrado o software de controlo de versões Git.",
@@ -130,7 +131,7 @@
"config-type-sqlite": "SQLite",
"config-type-oracle": "Oracle",
"config-type-mssql": "Microsoft SQL Server",
- "config-support-info": "O MediaWiki suporta as seguintes plataformas de base de dados:\n\n$1\n\nSe a plataforma que pretende usar não está listada abaixo, siga as instruções nos links acima para ativar o suporte.",
+ "config-support-info": "O MediaWiki suporta as seguintes plataformas de base de dados:\n\n$1\n\nSe a plataforma que pretende usar não está listada abaixo, siga as instruções nas hiperligações acima para ativar o suporte.",
"config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] é a plataforma primária do MediaWiki e é a melhor suportada. O MediaWiki também trabalha com [{{int:version-db-mariadb-url}} MariaDB] e [{{int:version-db-percona-url}} Percona Server], que são compatíveis com MySQL. ([http://www.php.net/manual/en/mysql.installation.php Como compilar PHP com suporte a MySQL])",
"config-dbsupport-postgres": "* O [{{int:version-db-postgres-url}} PostgreSQL] é uma plataforma popular de base de dados de código aberto, alternativa ao MySQL. ([http://www.php.net/manual/en/pgsql.installation.php Como compilar o PHP com suporte PostgreSQL])",
"config-dbsupport-sqlite": "* O [{{int:version-db-sqlite-url}} SQLite] é uma plataforma de base de dados ligeira muito bem suportada. ([http://www.php.net/manual/en/pdo.installation.php Como compilar o PHP com suporte SQLite], usa PDO)",
@@ -191,7 +192,7 @@
"config-mssql-windowsauth": "Autenticação do Windows",
"config-site-name": "Nome da wiki:",
"config-site-name-help": "Este nome aparecerá no título da janela do seu navegador e em vários outros sítios.",
- "config-site-name-blank": "Introduza o nome do site.",
+ "config-site-name-blank": "Introduza o nome do sítio.",
"config-project-namespace": "Espaço nominal do projeto:",
"config-ns-generic": "Projeto",
"config-ns-site-name": "O mesmo que o nome da wiki: $1",
@@ -201,7 +202,7 @@
"config-ns-invalid": "O espaço nominal especificado \"<nowiki>$1</nowiki>\" é inválido.\nIntroduza um espaço nominal de projeto diferente.",
"config-ns-conflict": "O espaço nominal que especificou, \"<nowiki>$1</nowiki>\", cria um conflito com um dos espaços nominais padrão do MediaWiki.\nEspecifique um espaço nominal do projeto diferente.",
"config-admin-box": "Conta de administrador",
- "config-admin-name": "Seu nome de utilizador:",
+ "config-admin-name": "O seu nome de utilizador:",
"config-admin-password": "Palavra-passe:",
"config-admin-password-confirm": "Repita a palavra-passe:",
"config-admin-help": "Introduza aqui o seu nome de utilizador preferido, por exemplo, \"João Beltrão\".\nEste é o nome que irá utilizar para entrar na wiki.",
@@ -237,7 +238,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 ou posterior",
"config-license-pd": "Domínio Público",
"config-license-cc-choose": "Selecionar uma licença personalizada Creative Commons",
- "config-license-help": "Muitas wikis de acesso público licenciam todas as colaborações com uma [http://freedomdefined.org/Definition licença livre].\nIsto ajuda a criar um sentido de propriedade da comunidade e encoraja as colaborações a longo prazo.\nTal não é geralmente necessário nas wikis privadas ou corporativas.\n\nSe pretende que seja possível usar textos da Wikipédia na sua wiki e que seja possível a Wikipédia aceitar textos copiados da sua wiki, deve escolher a licença <strong>{{int:config-license-cc-by-sa}}</strong>..\n\nA licença anterior da Wikipédia era a licença GNU Free Documentation License.\nA GFDL é uma licença válida, mas de difícil compreensão.\nTambém é difícil reutilizar conteúdos licenciados com a GFDL.",
+ "config-license-help": "Muitas wikis de acesso público licenciam todas as colaborações com uma [https://freedomdefined.org/Definition licença livre].\nIsto ajuda a criar um sentido de propriedade da comunidade e encoraja as colaborações a longo prazo.\nTal não é geralmente necessário nas wikis privadas ou corporativas.\n\nSe pretende que seja possível usar textos da Wikipédia na sua wiki e que seja possível a Wikipédia aceitar textos copiados da sua wiki, deve escolher a licença <strong>{{int:config-license-cc-by-sa}}</strong>..\n\nA licença anterior da Wikipédia era a licença GNU Free Documentation License.\nA GFDL é uma licença válida, mas de difícil compreensão.\nTambém é difícil reutilizar conteúdos licenciados com a GFDL.",
"config-email-settings": "Definições do correio eletrónico",
"config-enable-email": "Ativar mensagens eletrónicas de saída",
"config-enable-email-help": "Se quer que o correio eletrónico funcione, as [http://www.php.net/manual/en/mail.configuration.php definições de correio eletrónico do PHP] têm de estar configuradas corretamente.\nSe não pretende viabilizar qualquer funcionalidade de correio eletrónico, pode desativá-lo aqui.",
@@ -248,7 +249,7 @@
"config-email-watchlist": "Ativar notificação de alterações às páginas vigiadas",
"config-email-watchlist-help": "Permitir que os utilizadores recebam notificações de alterações às suas páginas vigiadas, se tiverem ativado esta funcionalidade nas suas preferências.",
"config-email-auth": "Ativar autenticação do correio eletrónico",
- "config-email-auth-help": "Se esta opção for ativada, os utilizadores têm de confirmar o seu endereço de correio eletrónico usando um link que lhes é enviado sempre que o definirem ou alterarem.\nSó os endereços de correio eletrónico autenticados podem receber mensagens eletrónicas dos outros utilizadores ou alterar as mensagens de notificação.\nÉ '''recomendado''' que esta opção seja ativada nas wikis de acesso público para impedir o uso abusivo das funcionalidades de correio eletrónico.",
+ "config-email-auth-help": "Se esta opção for ativada, os utilizadores têm de confirmar o seu endereço de correio eletrónico usando uma hiperligação que lhes é enviada sempre que o definirem ou alterarem.\nSó os endereços de correio eletrónico autenticados podem receber mensagens eletrónicas dos outros utilizadores ou alterar as mensagens de notificação.\nÉ '''recomendado''' que esta opção seja ativada nas wikis de acesso público para impedir o uso abusivo das funcionalidades de correio eletrónico.",
"config-email-sender": "Endereço de correio eletrónico de retorno:",
"config-email-sender-help": "Introduza o endereço de correio eletrónico que será usado como endereço de retorno nas mensagens eletrónicas de saída.\nÉ para este endereço que serão enviadas as mensagens que não podem ser entregues.\nMuitos servidores de correio eletrónico exigem que pelo menos a parte do nome do domínio seja válida. \\",
"config-upload-settings": "Carregamento de imagens e ficheiros",
@@ -259,15 +260,15 @@
"config-logo": "URL do logótipo:",
"config-logo-help": "O tema padrão do MediaWiki inclui espaço para um logótipo de 135x160 píxeis acima do menu da barra lateral.\nColoque na wiki uma imagem com estas dimensões e introduza aqui o URL dessa imagem.\n\nSe não pretende usar um logótipo, deixe este campo em branco.",
"config-instantcommons": "Ativar Instant Commons",
- "config-instantcommons-help": "O [https://www.mediawiki.org/wiki/InstantCommons Instant Commons] é uma funcionalidade que permite que as wikis usem imagens, áudio e outros ficheiros multimédia disponíveis no site [https://commons.wikimedia.org/ Wikimedia Commons].\nPara poder usá-los, o MediaWiki necessita de acesso à Internet.\n\nPara mais informações sobre esta funcionalidade, incluindo instruções sobre como configurá-la para usar outras wikis em vez da Wikimedia Commons, consulte o [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos Manual Técnico].",
+ "config-instantcommons-help": "O [https://www.mediawiki.org/wiki/InstantCommons Instant Commons] é uma funcionalidade que permite que as wikis usem imagens, áudio e outros ficheiros multimédia disponíveis no sítio [https://commons.wikimedia.org/ Wikimedia Commons].\nPara poder usá-los, o MediaWiki necessita de acesso à Internet.\n\nPara mais informações sobre esta funcionalidade, incluindo instruções sobre como configurá-la para usar outras wikis em vez da Wikimedia Commons, consulte o [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos Manual Técnico].",
"config-cc-error": "O auxiliar de escolha de licenças da Creative Commons não produziu resultados.\nIntroduza o nome da licença manualmente.",
"config-cc-again": "Escolha outra vez...",
"config-cc-not-chosen": "Escolha a licença da Creative Commons que pretende e clique \"proceed\".",
"config-advanced-settings": "Configuração avançada",
"config-cache-options": "Configuração da cache de objetos:",
- "config-cache-help": "A cache de objetos é usada para melhorar o desempenho do MediaWiki. Armazena dados usados com frequência.\nSites de tamanho médio ou grande são altamente encorajados a ativar esta funcionalidade e os sites pequenos também terão alguns benefícios em fazê-lo.",
+ "config-cache-help": "A cache de objetos é usada para melhorar o desempenho do MediaWiki. Armazena dados usados com frequência.\nSítios de tamanho médio ou grande são altamente encorajados a ativar esta funcionalidade e os sítios pequenos também terão alguns benefícios em fazê-lo.",
"config-cache-none": "Sem cache (não é removida nenhuma funcionalidade, mas a velocidade de operação pode ser afectada nas wikis grandes)",
- "config-cache-accel": "Cache de objetos do PHP (APC, APCu, XCache ou WinCache)",
+ "config-cache-accel": "Cache de objetos do PHP (APC, APCu ou WinCache)",
"config-cache-memcached": "Usar Memcached (requer instalação e configurações adicionais)",
"config-memcached-servers": "Servidores Memcached:",
"config-memcached-help": "Lista de endereços IP que serão usados para o Memcached.\nDeve-se colocar um por linha e indicar a porta a utilizar. Por exemplo:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -306,7 +307,7 @@
"config-install-tables": "A criar as tabelas",
"config-install-tables-exist": "<strong>Aviso:</strong> As tabelas do MediaWiki parecem já existir.\nA criação das tabelas será saltada.",
"config-install-tables-failed": "<strong>Erro:</strong> A criação das tabelas falhou com o seguinte erro: $1",
- "config-install-interwiki": "A preencher a tabela padrão de links interwikis",
+ "config-install-interwiki": "A preencher a tabela padrão de hiperligações interwikis",
"config-install-interwiki-list": "Não foi possível ler o ficheiro <code>interwiki.list</code>.",
"config-install-interwiki-exists": "<strong>Aviso:</strong> A tabela de interwikis parece já conter entradas.\nO preenchimento padrão desta tabela será saltado.",
"config-install-stats": "A inicializar as estatísticas",
@@ -321,8 +322,9 @@
"config-install-mainpage-exists": "A página principal já existe; a saltar este passo",
"config-install-extension-tables": "A criar as tabelas das extensões ativadas",
"config-install-mainpage-failed": "Não foi possível inserir a página principal: $1",
- "config-install-done": "<strong>Parabéns!</strong>\nTerminou a instalação do MediaWiki.\n\nO instalador gerou um ficheiro <code>LocalSettings.php</code>.\nEste ficheiro contém todas as configurações.\n\nPrecisa de descarregar o ficheiro e colocá-lo no diretório de raiz da sua instalação (o mesmo diretório onde está o ficheiro index.php). Este descarregamento deverá ter sido iniciado automaticamente.\n\nSe o descarregamento não foi iniciado, ou se o cancelou, pode recomeçá-lo clicando na ligação abaixo:\n\n$3\n\n<strong>Nota</strong>: Se não o descarregar agora, o ficheiro que foi gerado deixará de estar disponível quando sair do processo de instalação.\n\nDepois de terminar o passo anterior, pode <strong>[$2 entrar na wiki]</strong>.",
- "config-install-done-path": "<strong>Parabéns!</strong>\nTerminou a instalação do MediaWiki.\n\nO instalador gerou um ficheiro <code>LocalSettings.php</code>.\nEste ficheiro contém todas as configurações.\n\nPrecisa de descarregar o ficheiro e colocá-lo no diretório <code>$4</code>. Este descarregamento deverá ter sido iniciado automaticamente.\n\nSe o descarregamento não foi iniciado, ou se o cancelou, pode recomeçá-lo clicando na ligação abaixo:\n\n$3\n\n<strong>Nota</strong>: Se não fizer o descarregamento agora, o ficheiro que foi gerado deixará de estar disponível quando sair do processo de instalação.\n\nDepois de terminar o passo anterior, pode <strong>[$2 entrar na wiki]</strong>.",
+ "config-install-done": "<strong>Parabéns!</strong>\nTerminou a instalação do MediaWiki.\n\nO instalador gerou um ficheiro <code>LocalSettings.php</code>.\nEste ficheiro contém todas as configurações.\n\nPrecisa de descarregar o ficheiro e colocá-lo no diretório de raiz da sua instalação (o mesmo diretório onde está o ficheiro index.php). Este descarregamento deverá ter sido iniciado automaticamente.\n\nSe o descarregamento não foi iniciado, ou se o cancelou, pode recomeçá-lo clicando na hiperligação abaixo:\n\n$3\n\n<strong>Nota</strong>: Se não o descarregar agora, o ficheiro que foi gerado deixará de estar disponível quando sair do processo de instalação.\n\nDepois de terminar o passo anterior, pode <strong>[$2 entrar na wiki]</strong>.",
+ "config-install-done-path": "<strong>Parabéns!</strong>\nTerminou a instalação do MediaWiki.\n\nO instalador gerou um ficheiro <code>LocalSettings.php</code>.\nEste ficheiro contém todas as configurações.\n\nPrecisa de descarregar o ficheiro e colocá-lo no diretório <code>$4</code>. Este descarregamento deverá ter sido iniciado automaticamente.\n\nSe o descarregamento não foi iniciado, ou se o cancelou, pode recomeçá-lo clicando a hiperligação abaixo:\n\n$3\n\n<strong>Nota</strong>: Se não fizer o descarregamento agora, o ficheiro que foi gerado deixará de estar disponível quando sair do processo de instalação.\n\nDepois de terminar o passo anterior, pode <strong>[$2 entrar na wiki]</strong>.",
+ "config-install-success": "O MediaWiki foi instalado. Já pode visitar <$1$2> para ver a sua wiki.\nSe tiver dúvidas, veja a nossa lista de perguntas frequentes,\n<https://www.mediawiki.org/wiki/Manual:FAQ/pt>, ou utilize um dos fóruns de suporte indicados nessa página.",
"config-download-localsettings": "Descarregar <code>LocalSettings.php</code>",
"config-help": "ajuda",
"config-help-tooltip": "clique para expandir",
diff --git a/www/wiki/includes/installer/i18n/qqq.json b/www/wiki/includes/installer/i18n/qqq.json
index a5c67903..db181219 100644
--- a/www/wiki/includes/installer/i18n/qqq.json
+++ b/www/wiki/includes/installer/i18n/qqq.json
@@ -19,7 +19,8 @@
"Waldir",
"Jdforrester",
"Liuxinyu970226",
- "Metalhead64"
+ "Metalhead64",
+ "Tacsipacsi"
]
},
"config-desc": "Short description of the installer.",
@@ -30,7 +31,7 @@
"config-localsettings-key": "Label for the upgrade key that confirms a user upgrading through the web UI has access to LocalSettings.php. Details at https://www.mediawiki.org/wiki/Manual:Upgrading#Web_browser.",
"config-localsettings-badkey": "Error message when an incorrect upgrade key has been provided while trying to upgrade.",
"config-upgrade-key-missing": "Used in info box. Parameters:\n* $1 - the upgrade key, enclosed in <code><nowiki><pre></nowikI></code> tag.",
- "config-localsettings-incomplete": "{{doc-important|Do not translate <code>LocalSettings.php</code> and <code><nowiki>{{int:Config-continue}}</nowiki><code>.}}\nParameters:\n* $1 - name of variable (any one of required variables or installer-specific global variables)",
+ "config-localsettings-incomplete": "{{doc-important|Do not translate <code>LocalSettings.php</code> and <code><nowiki>{{int:Config-continue}}</nowiki></code>.}}\nParameters:\n* $1 - name of variable (any one of required variables or installer-specific global variables)",
"config-localsettings-connection-error": "{{doc-important|Do not translate <code>LocalSettings.php</code>.}}\nUsed as error message. Parameters:\n* $1 - (probably empty string)",
"config-session-error": "Parameters:\n* $1 is the error that was encountered with the session.",
"config-session-expired": "Parameters:\n* $1 is the configured session lifetime.",
@@ -69,13 +70,12 @@
"config-unicode-pure-php-warning": "PECL is the name of a group producing standard pieces of software for PHP, and intl is the name of their library handling some aspects of internationalization.",
"config-unicode-update-warning": "ICU is a body producing standard software tools for support of Unicode and other internationalization aspects. This message warns the system administrator installing MediaWiki that the server's software is not up-to-date and MediaWiki will have problems handling some characters.",
"config-no-db": "{{doc-important|Do not translate \"<code>./configure --with-mysqli</code>\" and \"<code>php5-mysql</code>\".}}\nParameters:\n* $1 is comma separated list of database types supported by MediaWiki.\n* $2 is the count of items in $1 - for use in plural.",
- "config-outdated-sqlite": "Used as warning. Parameters:\n* $1 - the version of SQLite that has been installed\n* $2 - minimum version",
+ "config-outdated-sqlite": "Used as warning. Parameters:\n* $2 - the version of SQLite that has been installed\n* $1 - minimum version",
"config-no-fts3": "A \"[[:wikipedia:Front and back ends|backend]]\" is a system or component that ordinary users don't interact with directly and don't need to know about, and that is responsible for a distinct task or service - for example, a storage back-end is a generic system for storing data which other applications can use. Possible alternatives for back-end are \"system\" or \"service\", or (depending on context and language) even leave it untranslated.",
"config-pcre-old": "Parameters:\n* $1 - minimum PCRE version number\n* $2 - the installed version of [[wikipedia:PCRE|PCRE]]\n{{Related|Config-fatal}}",
"config-pcre-no-utf8": "PCRE is a name of a programmers' library for supporting regular expressions. It can probably be translated without change.\n{{Related|Config-fatal}}",
"config-memory-raised": "Parameters:\n* $1 is the configured <code>memory_limit</code>.\n* $2 is the value to which <code>memory_limit</code> was raised.",
"config-memory-bad": "Parameters:\n* $1 is the configured <code>memory_limit</code>.",
- "config-xcache": "Message indicates if this program is available",
"config-apc": "Message indicates if this program is available",
"config-apcu": "Message indicates if this program is available",
"config-wincache": "Message indicates if this program is available",
@@ -181,10 +181,6 @@
"config-mysql-myisam-dep": "Warning message in the MediaWiki installer when MyISAM is chosen as MySQL storage engine.",
"config-mysql-only-myisam-dep": "Used as warning message when mysql does not support the minimum suggested feature set.",
"config-mysql-engine-help": "Help text in MediaWiki installer with advice for picking a MySQL storage engine.",
- "config-mysql-charset": "Field label for the MySQL character set in the MediaWiki installer.",
- "config-mysql-binary": "{{Identical|Binary}}",
- "config-mysql-utf8": "Option for the MySQL character set in the MediaWiki installer.",
- "config-mysql-charset-help": "Help text for the MySQL character set setting in the MediaWiki installer.",
"config-mssql-auth": "Radio button group label.\n\nFollowed by the following radio button labels:\n* {{msg-mw|Config-mssql-sqlauth}}\n* {{msg-mw|Config-mssql-windowsauth}}",
"config-mssql-install-auth": "Used as the help text for the \"Authentication type\" radio button when typing in database settings for installation.\n\nRefers to {{msg-mw|Config-mssql-windowsauth}}.\n\nSee also:\n* {{msg-mw|Config-mssql-web-auth}}",
"config-mssql-web-auth": "Used as the help text for the \"Authentication type\" radio button when typing in database settings for normal wiki usage.\n\nRefers to {{msg-mw|Config-mssql-windowsauth}}.\n\nSee also:\n* {{msg-mw|Config-mssql-install-auth}}",
@@ -324,6 +320,7 @@
"config-install-mainpage-failed": "Used as error message. Parameters:\n* $1 - detailed error message",
"config-install-done": "Parameters:\n* $1 is the URL to LocalSettings download\n* $2 is a link to the wiki.\n* $3 is a download link with attached download icon. The config-download-localsettings message will be used as the link text.",
"config-install-done-path": "Parameters:\n* $1 is the URL to LocalSettings download\n* $2 is a link to the wiki.\n* $3 is a download link with attached download icon. The config-download-localsettings message will be used as the link text.\n* $4 is the filesystem location of where the LocalSettings.php file should be saved to.",
+ "config-install-success": "Gives user information that installation was successful. Parameters:\n* $1 - server name\n* $2 - script path",
"config-download-localsettings": "The link text used in the download link in config-install-done.",
"config-help": "This is used in help boxes.\n{{Identical|Help}}",
"config-help-tooltip": "Tooltip for the 'help' links ({{msg-mw|config-help}}), to make it clear they'll expand in place rather than open a new page",
@@ -331,6 +328,7 @@
"config-extension-link": "Shown on last page of installation to inform about possible extensions.\n{{Identical|Did you know}}",
"config-skins-screenshots": "Radio button text, $1 is the skin name, and $2 is a list of links to screenshots of that skin",
"config-skins-screenshot": "Radio button text, $1 is the skin name, and $2 is a link to a screenshot of that skin, where the link text is {{msg-mw|config-screenshot}}.",
+ "config-extensions-requires": "Radio button text, $1 is the extension name, and $2 are links to other extensions that this one requires.",
"config-screenshot": "Link text for the link in {{msg-mw|config-skins-screenshot}}\n{{Identical|Screenshot}}",
"mainpagetext": "Along with {{msg-mw|mainpagedocfooter}}, the text you will see on the Main Page when your wiki is installed.",
"mainpagedocfooter": "Along with {{msg-mw|mainpagetext}}, the text you will see on the Main Page when your wiki is installed.\nThis might be a good place to put information about <nowiki>{{GRAMMAR:}}</nowiki>. See [[{{NAMESPACE}}:{{BASEPAGENAME}}/fi]] for an example. For languages having grammatical distinctions and not having an appropriate <nowiki>{{GRAMMAR:}}</nowiki> software available, a suggestion to check and possibly amend the messages having <nowiki>{{SITENAME}}</nowiki> may be valuable. See [[{{NAMESPACE}}:{{BASEPAGENAME}}/ksh]] for an example."
diff --git a/www/wiki/includes/installer/i18n/ro.json b/www/wiki/includes/installer/i18n/ro.json
index 14c60fe5..0d3f3123 100644
--- a/www/wiki/includes/installer/i18n/ro.json
+++ b/www/wiki/includes/installer/i18n/ro.json
@@ -47,9 +47,8 @@
"config-env-bad": "Verificarea mediului a fost efectuată.\nNu puteți instala MediaWiki.",
"config-env-php": "PHP $1 este instalat.",
"config-env-hhvm": "HHVM $1 este instalat.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] este instalat",
"config-apc": "[http://www.php.net/apc APC] este instalat",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] este instalat",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] este instalat",
"config-diff3-bad": "GNU diff3 nu a fost găsit.",
"config-no-uri": "<strong>Eroare:</strong> Nu pot determina URI-ul curent.\nInstalare întreruptă.",
"config-db-type": "Tipul bazei de date:",
diff --git a/www/wiki/includes/installer/i18n/ru.json b/www/wiki/includes/installer/i18n/ru.json
index 90456055..b002d85a 100644
--- a/www/wiki/includes/installer/i18n/ru.json
+++ b/www/wiki/includes/installer/i18n/ru.json
@@ -24,7 +24,8 @@
"StasR",
"Irus",
"Mailman",
- "Facenapalm"
+ "Facenapalm",
+ "Movses"
]
},
"config-desc": "Инсталлятор MediaWiki",
@@ -64,14 +65,14 @@
"config-help-restart": "Вы хотите удалить все сохранённые данные, которые вы ввели, и запустить процесс установки заново?",
"config-restart": "Да, начать заново",
"config-welcome": "=== Проверка окружения ===\nБудут проведены базовые проверки с целью определить, подходит ли данная система для установки MediaWiki.\nНе забудьте включить эту информацию, если вам потребуется помощь для завершения установки.",
- "config-copyright": "=== Авторские права и условия ===\n\n$1\n\nMediaWiki — свободное программное обеспечение, которое вы можете распространять и/или изменять в соответствии с условиями лицензии GNU General Public License, опубликованной фондом свободного программного обеспечения; второй версии, либо любой более поздней версии.\n\nMediaWiki распространяется в надежде, что она будет полезной, но <strong>без каких-либо гарантий</strong>, даже без подразумеваемых гарантий <strong>коммерческой ценности</strong> или <strong>пригодности для определённой цели</strong>. См. лицензию GNU General Public License для более подробной информации.\n\nВы должны были получить <doclink href=Copying>копию GNU General Public License</doclink> вместе с этой программой, если нет, то напишите Free Software Foundation, Inc., по адресу: 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA или [http://www.gnu.org/copyleft/gpl.html прочтите её онлайн].",
+ "config-copyright": "=== Авторские права и условия ===\n\n$1\n\nMediaWiki — свободное программное обеспечение, которое вы можете распространять и/или изменять в соответствии с условиями лицензии GNU General Public License, опубликованной фондом свободного программного обеспечения; второй версии, либо любой более поздней версии.\n\nMediaWiki распространяется в надежде, что она будет полезной, но <strong>без каких-либо гарантий</strong>, даже без подразумеваемых гарантий <strong>коммерческой ценности</strong> или <strong>пригодности для определённой цели</strong>. См. лицензию GNU General Public License для более подробной информации.\n\nВы должны были получить <doclink href=Copying>копию GNU General Public License</doclink> вместе с этой программой, если нет, то напишите Free Software Foundation, Inc., по адресу: 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA или [https://www.gnu.org/copyleft/gpl.html прочтите её онлайн].",
"config-sidebar": "* [https://www.mediawiki.org Сайт MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/ru Справка для пользователей]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/ru Справка для администраторов]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/ru FAQ]\n----\n* <doclink href=Readme>Readme-файл</doclink>\n* <doclink href=ReleaseNotes>Информация о выпуске</doclink>\n* <doclink href=Copying>Лицензия</doclink>\n* <doclink href=UpgradeDoc>Обновление</doclink>",
"config-env-good": "Проверка внешней среды была успешно проведена.\nВы можете установить MediaWiki.",
"config-env-bad": "Была проведена проверка внешней среды.\nВы не можете установить MediaWiki.",
"config-env-php": "Установленная версия PHP: $1.",
"config-env-hhvm": "HHVM $1 установлена.",
- "config-unicode-using-intl": "Будет использовано [http://pecl.php.net/intl расширение «intl» для PECL] для нормализации Юникода.",
- "config-unicode-pure-php-warning": "'''Внимание!''': [http://pecl.php.net/intl расширение intl из PECL] недоступно для нормализации Юникода, будет использоваться медленная реализация на чистом PHP.\nЕсли ваш сайт работает под высокой нагрузкой, вам следует больше узнать о [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормализации Юникода].",
+ "config-unicode-using-intl": "Будет использовано [https://pecl.php.net/intl расширение «intl» для PECL] для нормализации Юникода.",
+ "config-unicode-pure-php-warning": "'''Внимание!''': [https://pecl.php.net/intl расширение intl из PECL] недоступно для нормализации Юникода, будет использоваться медленная реализация на чистом PHP.\nЕсли ваш сайт работает под высокой нагрузкой, вам следует больше узнать о [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормализации Юникода].",
"config-unicode-update-warning": "'''Предупреждение''': установленная версия обёртки нормализации Юникода использует старую версию библиотеки [http://site.icu-project.org/ проекта ICU].\nВы должны [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations обновить версию], если хотите полноценно использовать Юникод.",
"config-no-db": "Не удалось найти подходящие драйвера баз данных! Вам необходимо установить драйвера базы данных для PHP.\n{{PLURAL:$2|Поддерживается следующий тип|Поддерживаются следующие типы}} баз данных: $1.\n\nЕсли вы скомпилировали PHP сами, перенастройте его с включением клиента баз данных, например, с помощью <code>./configure --with-mysqli</code>.\nЕсли вы установили PHP из пакетов Debian или Ubuntu, то вам также необходимо установить, например, пакет <code>php5-mysql</code>.",
"config-outdated-sqlite": "'''Предупреждение''': у Вас установлен SQLite $1, версия которого ниже требуемой $2 . SQLite будет недоступен.",
@@ -80,12 +81,11 @@
"config-pcre-no-utf8": "'''Фатальная ошибка'''. Модуль PCRE для PHP, похоже, собран без поддержки PCRE_UTF8.\nMediaWiki требует поддержки UTF-8 для корректной работы.",
"config-memory-raised": "Ограничение на доступную PHP память (<code>memory_limit</code>) поднято с $1 до $2.",
"config-memory-bad": "'''Внимание:''' размер PHP <code>memory_limit</code> составляет $1.\nВероятно, этого слишком мало.\nУстановка может потерпеть неудачу!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] установлен",
"config-apc": "[http://www.php.net/apc APC] установлен",
"config-apcu": "[http://www.php.net/apcu APCu] установлен",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] установлен",
- "config-no-cache-apcu": "'''Внимание:''' Не найдены [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] или [http://www.iis.net/download/WinCacheForPhp WinCache].\nКэширование объектов будет отключено.",
- "config-mod-security": "<strong>Внимание</strong>: На вашем веб-сервере включен [http://modsecurity.org/ mod_security]/mod_security2. Многие его стандартные настройки могут вызывать проблемы для MediaWiki или другого ПО, позволяющего пользователям отправлять на сервер произвольный контент.\nОбратитесь к [http://modsecurity.org/documentation/ документации mod_security] или в службу поддержки вашего хостинг-провайдера, если вы сталкиваетесь со случайными ошибками.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] установлен",
+ "config-no-cache-apcu": "<strong>Внимание:</strong> Не найдены [http://www.php.net/apcu APCu] или [http://www.iis.net/download/WinCacheForPhp WinCache].\nКэширование объектов будет отключено.",
+ "config-mod-security": "<strong>Внимание</strong>: На вашем веб-сервере включён [https://modsecurity.org/ mod_security]/mod_security2. Многие его стандартные настройки могут вызывать проблемы для MediaWiki или другого ПО, позволяющего пользователям отправлять на сервер произвольный контент.\nПо возможности он должен быть отключён. Обратитесь к [https://modsecurity.org/documentation/ документации mod_security] или в службу поддержки вашего хостинг-провайдера, если вы сталкиваетесь со случайными ошибками.",
"config-diff3-bad": "GNU diff3 не найден.",
"config-git": "Найдена система контроля версий Git: <code>$1</code>.",
"config-git-bad": "Программное обеспечение по управлению версиями Git не найдено.",
@@ -95,7 +95,7 @@
"config-no-uri": "'''Ошибка:''' Не могу определить текущий URI.\nУстановка прервана.",
"config-no-cli-uri": "'''Предупреждение''': нет задан параметр <code>--scriptpath</code>, используется по умолчанию: <code>$1</code> .",
"config-using-server": "Используется имя сервера «<nowiki>$1</nowiki>».",
- "config-using-uri": "Используется имя сервера \"<nowiki>$1$2</nowiki>\".",
+ "config-using-uri": "Используется URL сервера \"<nowiki>$1$2</nowiki>\".",
"config-uploads-not-safe": "'''Внимание:''' директория, используемая по умолчанию для загрузок (<code>$1</code>) уязвима к выполнению произвольных скриптов.\nХотя MediaWiki проверяет все загружаемые файлы на наличие угроз, настоятельно рекомендуется [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security закрыть данную уязвимость] перед включением загрузки файлов.",
"config-no-cli-uploads-check": "'''Предупреждение:''' каталог для загрузки по умолчанию ( <code>$1</code> ) не проверялся на уязвимости\n на выполнение произвольного сценария во время установки CLI.",
"config-brokenlibxml": "В вашей системе имеется сочетание версий PHP и libxml2, которое может привести к скрытым повреждениям данных в MediaWiki и других веб-приложениях.\nОбновите libxml2 до версии 2.7.3 или старше ([https://bugs.php.net/bug.php?id=45996 сведения об ошибке]).\nУстановка прервана.",
@@ -243,7 +243,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 или более поздняя",
"config-license-pd": "Общественное достояние",
"config-license-cc-choose": "Выберите одну из лицензий Creative Commons",
- "config-license-help": "Многие общедоступные вики разрешают использовать свои материалы на условиях [http://freedomdefined.org/Definition/Ru свободных лицензий].\nЭто помогает созданию чувства общности, стимулирует долгосрочное участие.\nНо в этом нет необходимости для частных или корпоративных вики.\n\nЕсли вы хотите использовать тексты из Википедии или хотите, что в Википедию можно было копировать тексты из вашей вики, вам следует выбрать <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nВикипедия ранее использовала лицензию GNU Free Documentation License.\nGFDL может быть использована, но она сложна для понимания и осложняет повторное использование материалов.",
+ "config-license-help": "Многие общедоступные вики разрешают использовать свои материалы на условиях [https://freedomdefined.org/Definition/Ru свободных лицензий].\nЭто помогает созданию чувства общности, стимулирует долгосрочное участие.\nНо в этом нет необходимости для частных или корпоративных вики.\n\nЕсли вы хотите использовать тексты из Википедии или хотите, что в Википедию можно было копировать тексты из вашей вики, вам следует выбрать <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nВикипедия ранее использовала лицензию GNU Free Documentation License.\nGFDL может быть использована, но она сложна для понимания и осложняет повторное использование материалов.",
"config-email-settings": "Настройки электронной почты",
"config-enable-email": "Включить исходящие e-mail",
"config-enable-email-help": "Если вы хотите, чтобы электронная почта работала, необходимо выполнить [http://www.php.net/manual/ru/mail.configuration.php соответствующие настройки PHP].\nЕсли вы не хотите использовать возможности электронной почты в вики, вы можете её отключить.",
@@ -273,7 +273,7 @@
"config-cache-options": "Параметры кэширования объектов:",
"config-cache-help": "Кэширование объектов используется для повышения скорости MediaWiki путем кэширования часто используемых данных.\nДля средних и больших сайтов кеширование настоятельно рекомендуется включать, а для небольших сайтов кеширование может показать преимущество.",
"config-cache-none": "Без кэширования (никакой функционал не теряется, но крупные вики-сайты могут работать медленнее)",
- "config-cache-accel": "Кэширование PHP-объектов (APC, APCu, XCache или WinCache)",
+ "config-cache-accel": "Кэширование PHP-объектов (APC, APCu или WinCache)",
"config-cache-memcached": "Использовать Memcached (требует дополнительной настройки)",
"config-memcached-servers": "Сервера Memcached:",
"config-memcached-help": "Список IP-адресов, используемых Memcached.\nПеречислите по одному адресу на строку с указанием портов. Например:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -329,6 +329,7 @@
"config-install-mainpage-failed": "Не удаётся вставить главную страницу: $1",
"config-install-done": "<strong>Поздравляем!</strong>\nВы установили MediaWiki.\n\nВо время установки был создан файл <code>LocalSettings.php</code>.\nОн содержит все ваши настройки.\n\nВам необходимо скачать его и положить в корневую директорию вашей вики (ту же директорию, где находится файл index.php). Его загрузка должна начаться автоматически.\n\nЕсли автоматическая загрузка не началась или вы её отменили, вы можете скачать по ссылке ниже:\n\n$3\n\n<strong>Примечание</strong>: Если вы не сделаете этого сейчас, то сгенерированный файл конфигурации не будет доступен вам в дальнейшем, если вы выйдете из установки, не скачивая его.\n\nПо окончании действий, описанных выше, вы сможете <strong>[$2 войти в вашу вики]</strong>.",
"config-install-done-path": "<strong>Поздравляем!</strong>\nВы установили MediaWiki.\n\nВо время установки был создан файл <code>LocalSettings.php</code>.\nОн содержит все ваши настройки.\n\nВам необходимо скачать его и положить в <code>$4</code>. Его загрузка должна начаться автоматически.\n\nЕсли автоматическая загрузка не началась или вы её отменили, вы можете скачать по ссылке ниже:\n\n$3\n\n<strong>Примечание</strong>: Если вы не сделаете этого сейчас, то сгенерированный файл конфигурации не будет доступен вам в дальнейшем, если вы выйдете из установки, не скачивая его.\n\nПо окончании действий, описанных выше, вы сможете <strong>[$2 войти в вашу вики]</strong>.",
+ "config-install-success": "MediaWiki успешно установлена. Сейчас вы можете перейти на <$1 $2>, чтобы просмотреть свою вики\nЕсли у вас есть вопросы, ознакомьтесь с нашим часто задаваемыми вопросами:\n<https://www.mediawiki.org/wiki/Manual:FAQ> или используйте один из форумов поддержки, указанный на этой странице.",
"config-download-localsettings": "Загрузить <code>LocalSettings.php</code>",
"config-help": "справка",
"config-help-tooltip": "нажмите, чтобы развернуть",
diff --git a/www/wiki/includes/installer/i18n/sco.json b/www/wiki/includes/installer/i18n/sco.json
index c79c22d3..292e6361 100644
--- a/www/wiki/includes/installer/i18n/sco.json
+++ b/www/wiki/includes/installer/i18n/sco.json
@@ -45,14 +45,14 @@
"config-help-restart": "Div ye wish tae clear aw hained data that ye'v entered n restairt the instawlation process?",
"config-restart": "Ai, restart it",
"config-welcome": "=== Environmêntal checks ===\nBasic checks will nou be performed tae see gif this environment is suitable fer MediaWiki installâtion.\nMynd tae inclæde this information gif ye seek heelp oan hou tae complete the installâtion.",
- "config-copyright": "=== Copiericht n Terms ===\n\n$1\n\nThis program is free saffware; ye can redistreebute it n/or modifie it unner the terms o the GNU General Public License aes published bi the Free Software Foundation; either version 2 o the License, or (yer optie) onie later version.\n\nThis program is distributed in the hope that it will be uiseful, but <strong>wioot onie warrantie</strong>; wioot even the implied warrantie o <strong>merchantabeelity</strong> or <strong>fitness fer ae parteecular purpose</strong>.\nSee the GNU General Public License fer mair details.\n\nYe shid hae receeved <doclink href=Copying> ae copie o the GNU General Publeec License</doclink> alang wi this program; gif naw, write til the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, or [http://www.gnu.org/copyleft/gpl.html read it online].",
+ "config-copyright": "=== Copiericht n Terms ===\n\n$1\n\nThis program is free saffware; ye can redistreebute it n/or modifie it unner the terms o the GNU General Public License aes published bi the Free Software Foundation; either version 2 o the License, or (yer optie) onie later version.\n\nThis program is distributed in the hope that it will be uiseful, but <strong>wioot onie warrantie</strong>; wioot even the implied warrantie o <strong>merchantabeelity</strong> or <strong>fitness fer ae parteecular purpose</strong>.\nSee the GNU General Public License fer mair details.\n\nYe shid hae receeved <doclink href=Copying> ae copie o the GNU General Publeec License</doclink> alang wi this program; gif naw, write til the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, or [https://www.gnu.org/copyleft/gpl.html read it online].",
"config-sidebar": "* [https://www.mediawiki.org MediaWiki home]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents User's Guide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administrator's Guide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Read me</doclink>\n* <doclink href=ReleaseNotes>Release notes</doclink>\n* <doclink href=Copying>Copiein</doclink>\n* <doclink href=UpgradeDoc>Upgradin</doclink>",
"config-env-good": "The environment haes been checked.\nYe can install MediaWiki.",
"config-env-bad": "The environment haes been checked.\nYe canna install MediaWiki.",
"config-env-php": "PHP $1 is instâlled.",
"config-env-hhvm": "HHVM $1 is instawed.",
- "config-unicode-using-intl": "Uising the [http://pecl.php.net/intl intl PECL extension] fer Unicode normalization.",
- "config-unicode-pure-php-warning": "<strong>Warnishment:</strong> The [http://pecl.php.net/intl intl PECL extension] is no available tae haunle Unicode normalisation, fawin back tae slaw pure-PHP implementation.\nGif ye rin ae hei-traffic steid, ye shid read ae wee bit oan [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
+ "config-unicode-using-intl": "Uising the [https://pecl.php.net/intl intl PECL extension] fer Unicode normalization.",
+ "config-unicode-pure-php-warning": "<strong>Warnishment:</strong> The [https://pecl.php.net/intl intl PECL extension] is no available tae haunle Unicode normalisation, fawin back tae slaw pure-PHP implementation.\nGif ye rin ae hei-traffic steid, ye shid read ae wee bit oan [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
"config-unicode-update-warning": "<strong>Wairnin:</strong> The installed version o the Unicode normalisation wrapper uises an aulder version o [http://site.icu-project.org/ the ICU project's] library.\nYe shoud [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations upgrade] if ye are at aw concerned aboot uisin Unicode.",
"config-no-db": "Could nae find a suitable database driver! Ye need tae install a database driver for PHP.\nThe follaein database {{PLURAL:$2|type is|types are}} supportit: $1.\n\nIf you compiled PHP yersel, reconfigur it wi a database client enabled, for example, uisin <code>./configure --with-mysqli</code>.\nIf ye installed PHP frae a Debian or Ubuntu package, then ye an aa need tae install, for example, the <code>php5-mysql</code> package.",
"config-outdated-sqlite": "<strong>Warnishment:</strong> ye have SQLite $1, this is lower than minimum required version $2. SQLite will be onavailable.",
@@ -61,11 +61,10 @@
"config-pcre-no-utf8": "<strong>Fatal:</strong> PHP's PCRE module seems tae be compiled wioot PCRE_UTF8 support.\nMediaWiki requires UTF-8 support tae function correctly.",
"config-memory-raised": "PHP's <code>memerie_limit</code> is $1, raised til $2.",
"config-memory-bad": "<strong>Warnishment:</strong> PHP's <code>memerie_limit</code> is $1.\nThis is proably ower low.\nThe installation micht fail!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] is installed.",
"config-apc": "[http://www.php.net/apc APC] is installed.",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] is instawed.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] is instawed.",
"config-no-cache-apcu": "<strong>Wairnin:</strong> Could nae find [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] or [http://www.iis.net/download/WinCacheForPhp WinCache].\nObject cachin isna enabled.",
- "config-mod-security": "<strong>Warnishment:</strong> Yer wab server haes [http://modsecurity.org/ mod_security] enabled. Gif misconfeegured, it can cause problems fer MediaWiki or ither saffware that allous uisers tae post arbitrie content.\nRefer til [http://modsecurity.org/documentation/ mod_security documentation] or contact yer host's support gif ye encounter random mistaks.",
+ "config-mod-security": "<strong>Warnishment:</strong> Yer wab server haes [https://modsecurity.org/ mod_security] enabled. Gif misconfeegured, it can cause problems fer MediaWiki or ither saffware that allous uisers tae post arbitrie content.\nRefer til [https://modsecurity.org/documentation/ mod_security documentation] or contact yer host's support gif ye encounter random mistaks.",
"config-diff3-bad": "GNU diff3 naw foond.",
"config-git": "Foond the Git version control saffware: <code>$1</code>.",
"config-git-bad": "Git version control saffware no foond.",
@@ -217,7 +216,7 @@
"config-license-gfdl": "GNU Free Documentâtion License 1.3 or later",
"config-license-pd": "Public Domain",
"config-license-cc-choose": "Select ae custym Creative Commyns license",
- "config-license-help": "Monie publeec wikis pit aw contreebutions unner ae [http://freedomdefined.org/Defineetion free license].\nThis heelps tae creaut ae sense o communitie ainership n encoorages lang-term contreebution.\nIt's naw generallie necessair fer ae preevate or corporate wiki.\n\nGif ye wish tae be able tae uise tex fae Wikipædia, n ye want Wikipædia tae be able tae accept tex copied fae yer wiki, than ye shid chuise <strong>Creative Commons Attribution Shair Alike</strong>.\n\nWikipædia preeveeooslie uised the GNU Free Documentation License.\nThe GFDL is ae valid license, but it's difficult tae unnerstaunn.\nMairower, it's difficult tae reuise content licensed unner the GFDL.",
+ "config-license-help": "Monie publeec wikis pit aw contreebutions unner ae [https://freedomdefined.org/Defineetion free license].\nThis heelps tae creaut ae sense o communitie ainership n encoorages lang-term contreebution.\nIt's naw generallie necessair fer ae preevate or corporate wiki.\n\nGif ye wish tae be able tae uise tex fae Wikipædia, n ye want Wikipædia tae be able tae accept tex copied fae yer wiki, than ye shid chuise <strong>Creative Commons Attribution Shair Alike</strong>.\n\nWikipædia preeveeooslie uised the GNU Free Documentation License.\nThe GFDL is ae valid license, but it's difficult tae unnerstaunn.\nMairower, it's difficult tae reuise content licensed unner the GFDL.",
"config-email-settings": "Wab-mail settins",
"config-enable-email": "Enable ootboond wab-mail",
"config-enable-email-help": "Gif ye want wab-mail tae wark, [http://www.php.net/manual/en/mail.configuration.php PHP's mail settins] need tae be confeegured jyst richt.\nGif ye dinna want oni wab-mail features, ye can disable theim here.",
diff --git a/www/wiki/includes/installer/i18n/sl.json b/www/wiki/includes/installer/i18n/sl.json
index 86d88a3d..0bd079b8 100644
--- a/www/wiki/includes/installer/i18n/sl.json
+++ b/www/wiki/includes/installer/i18n/sl.json
@@ -3,7 +3,8 @@
"authors": [
"Dbc334",
"Eleassar",
- "Yerpo"
+ "Yerpo",
+ "HairyFotr"
]
},
"config-desc": "Namestitveni program za MediaWiki",
@@ -48,11 +49,10 @@
"config-env-bad": "Okolje je pregledano.\nNe morete namestiti MediaWiki.",
"config-env-php": "Nameščen je PHP $1.",
"config-env-hhvm": "HHVM $1 je nameščen.",
- "config-unicode-using-intl": "Uporaba [http://pecl.php.net/intl razširitve PECL intl] za normalizacijo unikoda.",
+ "config-unicode-using-intl": "Uporaba [https://pecl.php.net/intl razširitve PECL intl] za normalizacijo unikoda.",
"config-memory-raised": "PHP-jev <code>memory_limit</code> je $1, dvignjen na $2.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] je nameščen",
"config-apc": "[http://www.php.net/apc APC] je nameščen",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] je nameščen",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] je nameščen",
"config-diff3-bad": "GNU diff3 ni bilo mogoče najti.",
"config-using-server": "Uporabljam ime strežnika \"<nowiki>$1</nowiki>\".",
"config-using-uri": "Uporabljam URL strežnika \"<nowiki>$1$2</nowiki>\".",
@@ -165,7 +165,7 @@
"config-cc-again": "Izberi ponovno ...",
"config-cc-not-chosen": "Izberite licenco Creative Commons, ki jo želite uporabiti, in kliknite »proceed«.",
"config-advanced-settings": "Napredna konfiguracija",
- "config-cache-accel": "Predpomnjenje predmetov PHP (APC, APCu, XCache ali WinCache)",
+ "config-cache-accel": "Predpomnjenje predmetov PHP (APC, APCu ali WinCache)",
"config-cache-memcached": "Uporabi Memcached (zahteva dodatno namestitev in konfiguracijo)",
"config-memcached-servers": "Strežniki Memcached:",
"config-memcache-badip": "Vnesli ste neveljaven IP-naslov za Memcached: $1",
diff --git a/www/wiki/includes/installer/i18n/sr-ec.json b/www/wiki/includes/installer/i18n/sr-ec.json
index 02f63359..30e2b9a6 100644
--- a/www/wiki/includes/installer/i18n/sr-ec.json
+++ b/www/wiki/includes/installer/i18n/sr-ec.json
@@ -6,12 +6,17 @@
"Milicevic01",
"Aktron",
"Сербијана",
- "Zoranzoki21"
+ "Zoranzoki21",
+ "Acamicamacaraca",
+ "Obsuser"
]
},
"config-desc": "Инсталација за Медијавики",
"config-title": "Инсталација Медијавикија $1",
- "config-information": "Информације",
+ "config-information": "Информација",
+ "config-localsettings-upgrade": "Откривена је датотека <code>LocalSettings.php</code>.\nДа бисте надоградили инсталацију, унесите вредности од <code>$wgUpgradeKey</code> у оквиру испод.\nНаћи ћете га у <code>LocalSettings.php</code>.",
+ "config-localsettings-key": "Кључ за уградњу:",
+ "config-localsettings-badkey": "Наведени кључ за надоградњу је неисправан.",
"config-session-error": "Грешка при започињању сесије: $1",
"config-session-expired": "Ваши подаци о сесији су истекли.\nСесије су подешене да трају $1.\nЊихов рок можете повећати постављањем <code>session.gc_maxlifetime</code> у php.ini.\nПоново покрените инсталацију.",
"config-no-session": "Ваши подаци о сесији су изгубљени!\nПроверите Ваш php.ini и обезбедите да је <code>session.save_path</code> постављен на одговарајући директоријум.",
@@ -22,12 +27,12 @@
"config-back": "← Назад",
"config-continue": "Настави →",
"config-page-language": "Језик",
- "config-page-welcome": "Добро дошли на МедијаВики!",
+ "config-page-welcome": "Добро дошли на Медијавики!",
"config-page-dbconnect": "Повезивање са базом података",
"config-page-upgrade": "Надоградња постојеће инсталације",
"config-page-dbsettings": "Подешавања базе података",
"config-page-name": "Назив",
- "config-page-options": "Поставке",
+ "config-page-options": "Подешавања",
"config-page-install": "Инсталирај",
"config-page-complete": "Завршено!",
"config-page-restart": "Поновно покретање инсталације",
@@ -38,15 +43,21 @@
"config-page-existingwiki": "Постојећи вики",
"config-help-restart": "Желите ли да обришете све сачуване податке које сте унели и поново покренете инсталацију?",
"config-restart": "Да, покрени поново",
+ "config-env-good": "Окружење је проверено.\nМожете инсталирати Медијавики.",
+ "config-env-bad": "Окружење је проверено.\nНе можете инсталирати Медијавики.",
"config-env-php": "PHP $1 је инсталиран.",
"config-env-hhvm": "HHVM $1 је инсталиран.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] је инсталиран",
"config-apc": "[http://www.php.net/apc APC] је инсталиран",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] је инсталиран",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] је инсталиран",
"config-db-type": "Тип базе података:",
"config-db-host": "Хост базе података",
+ "config-db-wiki-settings": "Идентификуј овај вики",
"config-db-name": "Назив базе података:",
- "config-db-password": "Лозинка за базу података:",
+ "config-db-name-oracle": "Шема базе података:",
+ "config-db-username": "Корисничко име базе података:",
+ "config-db-password": "Лозинка базе података:",
+ "config-db-port": "Порт базе података:",
+ "config-db-schema": "Шема за Медијавики:",
"config-type-mysql": "MySQL (или компактибилан)",
"config-type-postgres": "PostgreSQL",
"config-type-sqlite": "SQLite",
@@ -60,13 +71,27 @@
"config-mysql-myisam": "MyISAM",
"config-mysql-utf8": "UTF-8",
"config-mssql-auth": "Тип провере идентитета:",
- "config-mssql-sqlauth": "Провера идентитета за SQL Server",
- "config-mssql-windowsauth": "Провера идентитета Windows-а",
+ "config-mssql-sqlauth": "Провера идентитета SQL Server-а",
+ "config-mssql-windowsauth": "Провера идентитета Виндоуса",
"config-site-name": "Име викија:",
- "config-admin-name": "Корисничко име:",
+ "config-site-name-blank": "Унесите име сајта.",
+ "config-project-namespace": "Именски простор пројекта:",
+ "config-ns-generic": "Пројекат",
+ "config-ns-site-name": "Исти као име викија: $1",
+ "config-ns-other": "Друго (наведите)",
+ "config-ns-other-default": "MyWiki",
+ "config-admin-box": "Налог администратора",
+ "config-admin-name": "Ваше корисничко име:",
"config-admin-password": "Лозинка:",
+ "config-admin-password-confirm": "Поново унесите лозинку:",
+ "config-admin-help": "Овде упишите жељено корисничко име; на пример, „Јован Крстић“.\nОво име ћете користити за пријаву на вики.",
+ "config-admin-name-blank": "Унесите корисничко име администратора.",
+ "config-admin-password-blank": "Унесите лозинку за налог администратора.",
+ "config-admin-password-mismatch": "Лозинке које сте унели се не поклапају.",
"config-admin-email": "Имејл адреса:",
- "config-optional-skip": "Досадно ми је, хајде да инсталирамо вики.",
+ "config-admin-error-bademail": "Унели сте неисправну имејл адресу.",
+ "config-optional-skip": "Досадно ми је, само инсталирај вики.",
+ "config-profile-wiki": "Отворен вики",
"config-profile-no-anon": "Неопходно је отворити налог",
"config-profile-fishbowl": "Само овлашћени корисници",
"config-profile-private": "Приватна вики",
@@ -76,16 +101,42 @@
"config-license-cc-by": "Creative Commons Ауторство (CC BY)",
"config-license-cc-by-nc-sa": "Creative Commons Ауторство-Некомерцијално-Делити под истим условима (CC BY-NC-SA)",
"config-license-cc-0": "Creative Commons Zero (јавно власништво)",
- "config-license-gfdl": "ГНУ-ова лиценца за слободну документацију верзија 1.3 или новија верзија",
+ "config-license-gfdl": "ГНУ-ова лиценца за слободну документацију издање 1.3 или новије",
"config-license-pd": "Јавно власништво",
"config-email-settings": "Подешавања имејла",
+ "config-enable-email": "Омогући одлазни имејл",
+ "config-upload-settings": "Отпремања слика и датотека",
+ "config-upload-enable": "Омогући отпремање датотека",
+ "config-upload-deleted": "Фасцикла за обрисане датотеке:",
+ "config-logo": "URL логотипа:",
+ "config-instantcommons": "Омогући Instant Commons",
+ "config-cc-again": "Изаберите поново...",
"config-cc-not-chosen": "Одаберите која Кријејтив комонс лиценца вам одговара и потврдите.",
+ "config-advanced-settings": "Напредна конфигурација",
+ "config-memcached-servers": "Memcached сервери:",
+ "config-extensions": "Екстензије",
"config-skins": "Теме",
+ "config-skins-use-as-default": "Користи ову тему као подразумевану",
+ "config-skins-must-enable-some": "Морате изабрати барем једну тему.",
"config-install-step-done": "готово",
"config-install-step-failed": "није успело",
+ "config-install-extensions": "Обухвата екстензије",
+ "config-install-schema": "Прављење шеме",
+ "config-install-tables": "Прављење табела",
+ "config-install-stats": "Покрећем статистику",
+ "config-install-keys": "Генеришем тајне кључеве",
+ "config-install-sysop": "Правим кориснички налог администратора",
+ "config-install-subscribe-fail": "Не могу да Вас претплатим на mediawiki-announce: $1",
+ "config-install-mainpage": "Правим главну страну са стандардним садржајем",
"config-install-mainpage-exists": "Главна страна већ постоји, прескакање",
+ "config-install-mainpage-failed": "Не могу да убацим главну страну: „$1”",
+ "config-download-localsettings": "Преузми <code>LocalSettings.php</code>",
"config-help": "помоћ",
"config-help-tooltip": "кликните да проширите",
+ "config-nofile": "Не могу да пронађем датотеку „$1”. Није ли она била избрисана?",
+ "config-skins-screenshots": "„$1” (снимци екрана: $2)",
+ "config-skins-screenshot": "$1 ($2)",
+ "config-screenshot": "снимак екрана",
"mainpagetext": "<strong>Медијавики је успешно инсталиран.</strong>",
- "mainpagedocfooter": "Погледајте [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents кориснички водич] за коришћење програма.\n\n== Увод ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Помоћ у вези са подешавањима]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Често постављена питања]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Дописна листа о издањима Медијавикија]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Научите како да се борете против спама на Вашој вики]"
+ "mainpagedocfooter": "Погледајте [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents кориснички водич] за коришћење програма.\n\n== Увод ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Помоћ у вези са подешавањима]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Често постављана питања]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Дописни списак о издањима Медијавикија]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Научите како да се борите против спама на својој вики]"
}
diff --git a/www/wiki/includes/installer/i18n/sv.json b/www/wiki/includes/installer/i18n/sv.json
index a338387a..0ca73d3d 100644
--- a/www/wiki/includes/installer/i18n/sv.json
+++ b/www/wiki/includes/installer/i18n/sv.json
@@ -49,14 +49,14 @@
"config-help-restart": "Vill du rensa all sparad data som du har angivit och starta om installationen?",
"config-restart": "Ja, starta om",
"config-welcome": "=== Miljökontroller ===\nGrundläggande kontroller kommer nu att utföras för att se om denna miljö är lämplig för installation av MediaWiki.\nKom ihåg att ta med denna information om du söker stöd för hur du skall slutföra installationen.",
- "config-copyright": "=== Upphovsrätt och Villkor ===\n\n$1\n\nDetta program är fri programvara; du kan vidaredistribuera den och/eller modifiera det enligt villkoren i GNU General Public License som publicerats av Free Software Foundation; antingen genom version 2 av licensen, eller (på ditt initiativ) någon senare version.\n\nDetta program är distribuerat i hopp om att det kommer att vara användbart, men '''utan någon garanti'''; utan att ens ha en underförstådd garanti om '''säljbarhet''' eller '''lämplighet för ett särskilt ändamål'''.\nSe GNU General Public License för mer detaljer.\n\nDu bör ha fått <doclink href=Copying>en kopia av GNU General Public License</doclink> tillsammans med detta program; om inte, skriv till Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, eller [http://www.gnu.org/copyleft/gpl.html läs den online].",
+ "config-copyright": "=== Upphovsrätt och Villkor ===\n\n$1\n\nDetta program är fri programvara; du kan vidaredistribuera den och/eller modifiera det enligt villkoren i GNU General Public License som publicerats av Free Software Foundation; antingen genom version 2 av licensen, eller (på ditt initiativ) någon senare version.\n\nDetta program är distribuerat i hopp om att det kommer att vara användbart, men '''utan någon garanti'''; utan att ens ha en underförstådd garanti om '''säljbarhet''' eller '''lämplighet för ett särskilt ändamål'''.\nSe GNU General Public License för mer detaljer.\n\nDu bör ha fått <doclink href=Copying>en kopia av GNU General Public License</doclink> tillsammans med detta program; om inte, skriv till Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, eller [https://www.gnu.org/copyleft/gpl.html läs den online].",
"config-sidebar": "* [https://www.mediawiki.org MediaWikis webbplats]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Användarguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administratörguide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Frågor och svar]\n----\n* <doclink href=Readme>Läs mig</doclink>\n* <doclink href=ReleaseNotes>Utgivningsanteckningar</doclink>\n* <doclink href=Copying>Kopiering</doclink>\n* <doclink href=UpgradeDoc>Uppgradering</doclink>",
"config-env-good": "Miljön har kontrollerats.\nDu kan installera MediaWiki.",
"config-env-bad": "Miljön har kontrollerats.\nDu kan inte installera MediaWiki.",
"config-env-php": "PHP $1 är installerat.",
"config-env-hhvm": "HHVM $1 är installerat.",
- "config-unicode-using-intl": "Använder [http://pecl.php.net/intl intl PECL-tillägget] för Unicode-normalisering.",
- "config-unicode-pure-php-warning": "'''Varning:''' [http://pecl.php.net/intl intl PECL-tillägget] är inte tillgängligt för att hantera Unicode-normalisering, faller tillbaka till en långsamt implementering i ren PHP.\nOm du driver en högtrafikerad webbplats bör du läsa lite om [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-normalisering].",
+ "config-unicode-using-intl": "Använder [https://pecl.php.net/intl intl PECL-tillägget] för Unicode-normalisering.",
+ "config-unicode-pure-php-warning": "'''Varning:''' [https://pecl.php.net/intl intl PECL-tillägget] är inte tillgängligt för att hantera Unicode-normalisering, faller tillbaka till en långsamt implementering i ren PHP.\nOm du driver en högtrafikerad webbplats bör du läsa lite om [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-normalisering].",
"config-unicode-update-warning": "<strong>Varning:</strong> Den installerade versionen av Unicode-normaliserings \"wrappern\" använder en äldre version av [http://site.icu-project.org/ ICU projektets] bibliotek.\nDu bör [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations uppgradera] om är intresserad av att använda Unicode.",
"config-no-db": "Kunde inte hitta en lämplig databasdrivrutin! Du måste installera en databasdrivrutin för PHP.\nFöljande databas{{PLURAL:$2|typ |typer}} stöds: $1.\n\nI du själv kompilerat din PHP, konfigurera den med en databasklient aktiverad genom att t.ex. använda <code>./configure --with-mysqli</code>.\nOm du installerade PHP från ett Debian- eller Ubuntupaket måste du även installera, t.ex. <code>php5-mysql</code>-paketet.",
"config-outdated-sqlite": "'''Varning:''' du har SQLite $1, vilket är lägre än minimikravet version $2. SQLite kommer inte att vara tillgänglig.",
@@ -65,12 +65,11 @@
"config-pcre-no-utf8": "'''Kritiskt:''' PHP:s PCRE-modul verkar vara kompilerat utan PCRE_UTF8-stöd.\nMediaWiki kräver stöd för UTF-8 för att fungera korrekt.",
"config-memory-raised": "PHPs <code>memory_limit</code> är $1, ökad till $2.",
"config-memory-bad": "''' Varning:''' PHP:s <code>memory_limit</code> är $1.\nDetta är förmodligen för lågt.\nInstallationen kan misslyckas!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] är installerat",
"config-apc": "[http://www.php.net/apc APC] är installerat",
"config-apcu": "[http://www.php.net/apcu APCu] är installerat",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] är installerat",
- "config-no-cache-apcu": "'''Varning:''' Kunde inte hitta [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] eller [http://www.iis.net/download/WinCacheForPhp WinCache].\nCachelagring av objekt är inte aktiverat.",
- "config-mod-security": "'''Varning:''' Din webbserver har [http://modsecurity.org/ mod_security] aktiverat. Om felaktigt konfigurerat kan den skapa problem för MediaWiki eller annan programvara som tillåter användaren att posta godtyckligt innehåll.\nTitta på [http://modsecurity.org/documentation/ mod_security-dokumentationen] eller kontakta din värd om du påträffar slumpmässiga fel.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] är installerat",
+ "config-no-cache-apcu": "<strong>Varning:</strong> Kunde inte hitta [http://www.php.net/apcu APCu] eller [http://www.iis.net/download/WinCacheForPhp WinCache].\nCachelagring av objekt är inte aktiverat.",
+ "config-mod-security": "'''Varning:''' Din webbserver har [https://modsecurity.org/ mod_security] aktiverat. Om felaktigt konfigurerat kan den skapa problem för MediaWiki eller annan programvara som tillåter användaren att posta godtyckligt innehåll.\nTitta på [https://modsecurity.org/documentation/ mod_security-dokumentationen] eller kontakta din värd om du påträffar slumpmässiga fel.",
"config-diff3-bad": "GNU diff3 hittades inte.",
"config-git": "Hittade Git-mjukvara för versionskontroll: <code>$1</code>.",
"config-git-bad": "Git-mjukvara för versionskontroll hittades inte.",
@@ -225,7 +224,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 eller senare",
"config-license-pd": "Public Domain",
"config-license-cc-choose": "Välj en anpassad Creative Commons-licens",
- "config-license-help": "Många publika wikis släpper alla bidrag under en [http://freedomdefined.org/Definition fri licens].\nDetta bidrar till en känsla av gemensamt ägandeskap och uppmuntrar till långsiktiga bidrag.\nDet är i allmänhet inte nödvändigt för en privat eller företagswiki.\n\nOm du vill kunna använda text från Wikipedia, och du vill att Wikipedia ska kunna acceptera text kopierad ifrån din wiki bör du välja <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia använde tidigare GNU Free Documentation License.\nGFDL är en giltig licens, men svår att förstå.\nDet är även svårt att återanvända innehåll som licensierats under GFDL.",
+ "config-license-help": "Många publika wikis släpper alla bidrag under en [https://freedomdefined.org/Definition fri licens].\nDetta bidrar till en känsla av gemensamt ägandeskap och uppmuntrar till långsiktiga bidrag.\nDet är i allmänhet inte nödvändigt för en privat eller företagswiki.\n\nOm du vill kunna använda text från Wikipedia, och du vill att Wikipedia ska kunna acceptera text kopierad ifrån din wiki bör du välja <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia använde tidigare GNU Free Documentation License.\nGFDL är en giltig licens, men svår att förstå.\nDet är även svårt att återanvända innehåll som licensierats under GFDL.",
"config-email-settings": "E-postinställningar",
"config-enable-email": "Aktivera utgående e-post",
"config-enable-email-help": "Om du vill att e-post ska fungera behöver,[http://www.php.net/manual/en/mail.configuration.php PHPs e-postinställningar] vara konfigurerad på rätt sätt.\nOm du inte vill ha några e-postfunktioner, kan du inaktivera dem här.",
@@ -255,7 +254,7 @@
"config-cache-options": "Inställningar för cachelagring av objekt:",
"config-cache-help": "Cachelagring av objekt används för att förbättra hastigheten på MediaWiki genom att cachelagra data som används ofta.\nMedelstora till stora webbplatser är starkt uppmuntrade att aktivera detta, och små webbplatser kommer även att se fördelar.",
"config-cache-none": "Ingen cachelagring (ingen funktionalitet tas bort, men hastighet kan påverkas på större wiki-webbplatser)",
- "config-cache-accel": "Cachelagring av PHP-objekt (APC, APCu, XCache eller WinCache)",
+ "config-cache-accel": "Cachelagring av PHP-objekt (APC, APCu eller WinCache)",
"config-cache-memcached": "Använda Memcached (kräver ytterligare inställningar och konfiguration)",
"config-memcached-servers": "Memcached-servrar:",
"config-memcached-help": "Lista över IP-adresser som ska användas för Memcached.\nBör ange en per rad och specificera den port som ska användas. Till exempel:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -311,6 +310,7 @@
"config-install-mainpage-failed": "Kunde inte infoga huvudsidan: $1",
"config-install-done": "<strong>Grattis!</strong>\nDu har installerat MediaWiki.\n\nInstallationsprogrammet har genererat filen <code>LocalSettings.php</code>.\nDet innehåller alla dina konfigurationer.\n\nDu kommer att behöva ladda ner den och placera den i roten för din wiki-installation (samma katalog som index.php). Nedladdningen borde ha startats automatiskt.\n\nOm ingen nedladdning erbjöds, eller om du har avbrutit det kan du starta om nedladdningen genom att klicka på länken nedan:\n\n$3\n\n<strong>OBS</strong>: Om du inte gör detta nu, kommer denna genererade konfigurationsfil inte vara tillgänglig för dig senare om du avslutar installationen utan att ladda ned den.\n\nNär det är klart, kan du <strong>[$2 gå in på din wiki]</strong>",
"config-install-done-path": "<strong>Grattis!</strong>\nDu har installerat MediaWiki.\n\nInstallationsprogrammet har genererat filen <code>LocalSettings.php</code>.\nDet innehåller alla dina konfigurationer.\n\nDu kommer att behöva ladda ner den och placera den i <code>$4</code>. Nedladdningen borde ha startats automatiskt.\n\nOm ingen nedladdning erbjöds, eller om du har avbrutit det kan du starta om nedladdningen genom att klicka på länken nedan:\n\n$3\n\n<strong>OBS</strong>: Om du inte gör detta nu, kommer denna genererade konfigurationsfil inte vara tillgänglig för dig senare om du avslutar installationen utan att ladda ned den.\n\nNär det är klart, kan du <strong>[$2 gå in på din wiki]</strong>",
+ "config-install-success": "MediaWiki har installerats. Du kan nu besöka <$1$2> för att se din wiki.\nOm du undrar någonting, kolla in vår lista över vanliga ställda frågor:\n<https://www.mediawiki.org/wiki/Manual:FAQ> eller använda något supportforum som länkas på sidan.",
"config-download-localsettings": "Ladda ner <code>LocalSettings.php</code>",
"config-help": "hjälp",
"config-help-tooltip": "klicka för att expandera",
diff --git a/www/wiki/includes/installer/i18n/te.json b/www/wiki/includes/installer/i18n/te.json
index 1978af0d..9f7c531e 100644
--- a/www/wiki/includes/installer/i18n/te.json
+++ b/www/wiki/includes/installer/i18n/te.json
@@ -44,18 +44,17 @@
"config-help-restart": "మీరు భద్రపరిచిన డేటా మొత్తాన్ని తీసివేసి స్థాపనను తిరిగి ప్రారంభించాలా?",
"config-restart": "ఔను, తిరిగి ప్రారంభించు",
"config-welcome": "=== పర్యావరణ పరీక్షలు ===\nఈ పర్యావరణం MediaWiki స్థాపనకు అనుకూలంగా ఉందో లేదో చూసే ప్రాథమిక పరీక్షలు ఇపుడు చేస్తాం.\nస్థాపనను ఎలా పూర్తి చెయ్యాలనే విషయమై మీకు సహాయం అడిగేటపుడు, ఈ సమాచారాన్ని ఇవ్వాలని గుర్తుంచుకోండి.",
- "config-copyright": "=== కాపీహక్కు, నిబంధనలు===\n\n$1\n\nఇది ఉచిత సాఫ్ట్‌వేరు; ఫ్రీ సాఫ్ట్‌వేర్ ఫౌండేషన్ వారు ప్రచురించిన GNU జనరల్ పబ్లిక్ లైసెన్సును (2వ లేదా తరువాతి వర్షన్) అనుసరించి దీన్ని పంపిణీ చెయ్యవచ్చు లేదా మార్చుకోనూవచ్చు.\n\nదీని వలన ఉపయోగం ఉంటుందనే నమ్మకంతో ప్రచురింపబడింది. కానీ <strong>ఎటువంటి వారంటీ లేదు</strong>; <strong> వర్తకం చేయదగ్గ </strong> లేదా <strong> ఒక అవసరానికి సరిపడే సామర్థ్యం</strong> ఉన్నదనే అంతరార్థ వారంటీ కూడా లేదు.\nమరిన్ని వివరాలకు GNU జనరల్ పబ్లిక్ లైసెన్స్ చూడండి.\n\nమీరు ఈ ప్రోగ్రాముతో పాటు <doclink href=Copying> GNU జనరల్ పబ్లిక్ లైసెన్స్ ప్రతిని </doclink> అందుకుని ఉండాలి; లేకపోతే, Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA కు జాబు రాయండి లేదా [http://www.gnu.org/copyleft/gpl.html ఆన్‌లైన్‌లో చదివండి].",
+ "config-copyright": "=== కాపీహక్కు, నిబంధనలు===\n\n$1\n\nఇది ఉచిత సాఫ్ట్‌వేరు; ఫ్రీ సాఫ్ట్‌వేర్ ఫౌండేషన్ వారు ప్రచురించిన GNU జనరల్ పబ్లిక్ లైసెన్సును (2వ లేదా తరువాతి వర్షన్) అనుసరించి దీన్ని పంపిణీ చెయ్యవచ్చు లేదా మార్చుకోనూవచ్చు.\n\nదీని వలన ఉపయోగం ఉంటుందనే నమ్మకంతో ప్రచురింపబడింది. కానీ <strong>ఎటువంటి వారంటీ లేదు</strong>; <strong> వర్తకం చేయదగ్గ </strong> లేదా <strong> ఒక అవసరానికి సరిపడే సామర్థ్యం</strong> ఉన్నదనే అంతరార్థ వారంటీ కూడా లేదు.\nమరిన్ని వివరాలకు GNU జనరల్ పబ్లిక్ లైసెన్స్ చూడండి.\n\nమీరు ఈ ప్రోగ్రాముతో పాటు <doclink href=Copying> GNU జనరల్ పబ్లిక్ లైసెన్స్ ప్రతిని </doclink> అందుకుని ఉండాలి; లేకపోతే, Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA కు జాబు రాయండి లేదా [https://www.gnu.org/copyleft/gpl.html ఆన్‌లైన్‌లో చదివండి].",
"config-sidebar": "* [https://www.mediawiki.org MediaWiki మొదటిపేజీ]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents వాడుకరుల మార్గదర్శి]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents అధికారుల మార్గదర్శి]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>చదవాల్సినవి</doclink>\n* <doclink href=ReleaseNotes>విడుదల గమనికలు</doclink>\n* <doclink href=Copying>కాపీ చెయ్యడం</doclink>\n* <doclink href=UpgradeDoc>ఉన్నతీకరించడం</doclink>",
"config-env-good": "పర్యావరణాన్ని పరీక్షించాం.\nఇక మీరు MediaWiki ని స్థాపించుకోవచ్చు.",
"config-env-bad": "పర్యావరణాన్ని పరీక్షించాం.\nమీరు MediaWiki ని స్థాపించలేరు.",
"config-env-php": "PHP $1 స్థాపించబడింది.",
- "config-unicode-using-intl": "యూనికోడు నార్మలైజేషన్ కోసం [http://pecl.php.net/intl intl PECL పొడిగింత] ను వాడుతున్నాం.",
+ "config-unicode-using-intl": "యూనికోడు నార్మలైజేషన్ కోసం [https://pecl.php.net/intl intl PECL పొడిగింత] ను వాడుతున్నాం.",
"config-outdated-sqlite": "<strong>హెచ్చరిక:</strong> మీ వద్ద SQLite $1 ఉంది. అదికావలసిన వెర్షను $2 కంటే దిగువది. SQLite అందుబాటులో ఉండదు.",
"config-memory-raised": "PHP యొక్క <code>memory_limit</code> $1, దాన్ని $2 కి పెంచాం.",
"config-memory-bad": "<strong>హెచ్చరిక:</strong> PHP యొక్క <code>memory_limit</code> $1.\nబహుశా ఇది మరీ తక్కువ.\nస్థాపన విఫలం కావచ్చు!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] స్థాపించబడింది",
"config-apc": "[http://www.php.net/apc APC] స్థాపించబడింది",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] స్థాపించబడింది",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] స్థాపించబడింది",
"config-diff3-bad": "GNU diff3 కనబడలేదు.",
"config-no-uri": "<strong>లోపం:</strong> ప్రస్తుత URI ఏమిటో నిర్ధారించలేకపోయాం.\nస్థాపన ఆగిపోయింది.",
"config-using-server": "సర్వరు పేరు \"<nowiki>$1</nowiki>\" ను వాడుతున్నాం.",
diff --git a/www/wiki/includes/installer/i18n/th.json b/www/wiki/includes/installer/i18n/th.json
index b60f2a6b..ba398e51 100644
--- a/www/wiki/includes/installer/i18n/th.json
+++ b/www/wiki/includes/installer/i18n/th.json
@@ -45,14 +45,14 @@
"config-help-restart": "คุณต้องการล้างข้อมูลทั้งหมดที่คุณกรอกและเริ่มกระบวนการติดตั้งใหม่อีกครั้งหรือไม่?",
"config-restart": "ใช่ เริ่มใหม่อีกครั้ง",
"config-welcome": "=== การตรวจสอบสภาพแวดล้อม ===\nการตรวจสอบเบื้องต้นจะกระทำขึ้น เพื่อยืนยันว่าสภาพแวดล้อมปัจจุบันเหมาะสมสำหรับการติดตั้ง MediaWiki หรือไม่\nโปรดจำไว้ว่าให้รวบรวมผลลัพธ์การตรวจสอบนี้ ถ้าคุณต้องการแสวงหาการสนับสนุนเพื่อที่จะติดตั้งให้สมบูรณ์",
- "config-copyright": "=== ลิขสิทธิ์และเงื่อนไข ===\n\n$1\n\nโปรแกรมนี้เป็นซอฟต์แวร์เสรี คุณสามารถนำโปรแกรมนี้มาเผยแพร่ซ้ำและ/หรือดัดแปลงได้ภายใต้เงื่อนไขของสัญญาอนุญาตสาธารณะทั่วไปของ GNU (GNU General Public License) ซึ่งเผยแพร่โดย Free Software Foundation (สัญญาอนุญาตรุ่น 2 ขึ้นไป)\n\nโปรแกรมนี้ถูกเผยแพร่โดยหวังว่าจะเป็นประโยชน์แก่ผู้ใช้ แต่<strong>จะไม่มีการรับประกันใด ๆ</strong> แม้แต่การรับประกันเกี่ยวกับ<strong>การนำไปใช้ในการซื้อขาย</strong> หรือ<strong>ความเหมาะสมสำหรับวัตถุประสงค์เฉพาะ</strong>\nสำหรับรายละเอียดเพิ่มเติม โปรดดูที่สัญญาอนุญาตสาธารณะทั่วไปของ GNU\n\nคุณควรได้รับ<doclink href=Copying>สำเนาของสัญญาอนุญาตสาธารณะทั่วไปของ GNU</doclink> มาพร้อมกับโปรแกรมนี้ ถ้าไม่ได้รับ ให้ขอได้ที่ Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, \nหรือ[http://www.gnu.org/copyleft/gpl.html อ่านออนไลน์ที่นี่]",
+ "config-copyright": "=== ลิขสิทธิ์และเงื่อนไข ===\n\n$1\n\nโปรแกรมนี้เป็นซอฟต์แวร์เสรี คุณสามารถนำโปรแกรมนี้มาเผยแพร่ซ้ำและ/หรือดัดแปลงได้ภายใต้เงื่อนไขของสัญญาอนุญาตสาธารณะทั่วไปของ GNU (GNU General Public License) ซึ่งเผยแพร่โดย Free Software Foundation (สัญญาอนุญาตรุ่น 2 ขึ้นไป)\n\nโปรแกรมนี้ถูกเผยแพร่โดยหวังว่าจะเป็นประโยชน์แก่ผู้ใช้ แต่<strong>จะไม่มีการรับประกันใด ๆ</strong> แม้แต่การรับประกันเกี่ยวกับ<strong>การนำไปใช้ในการซื้อขาย</strong> หรือ<strong>ความเหมาะสมสำหรับวัตถุประสงค์เฉพาะ</strong>\nสำหรับรายละเอียดเพิ่มเติม โปรดดูที่สัญญาอนุญาตสาธารณะทั่วไปของ GNU\n\nคุณควรได้รับ<doclink href=Copying>สำเนาของสัญญาอนุญาตสาธารณะทั่วไปของ GNU</doclink> มาพร้อมกับโปรแกรมนี้ ถ้าไม่ได้รับ ให้ขอได้ที่ Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, \nหรือ[https://www.gnu.org/copyleft/gpl.html อ่านออนไลน์ที่นี่]",
"config-sidebar": "* [https://www.mediawiki.org โฮมเพจของ MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents แนวปฏิบัติของผู้ใช้]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents แนวปฏิบัติของผู้ดูแลระบบ]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ คำถามที่ถามบ่อย]\n----\n* <doclink href=Readme>อ่านเอกสารกำกับ</doclink>\n* <doclink href=ReleaseNotes>บันทึกการเผยแพร่</doclink>\n* <doclink href=Copying>การคัดลอก</doclink>\n* <doclink href=UpgradeDoc>การอัปเกรด</doclink>",
"config-env-good": "ตรวจสอบสภาพแวดล้อมแล้ว\nคุณสามารถติดตั้ง MediaWiki",
"config-env-bad": "ตรวจสอบสภาพแวดล้อมแล้ว\nคุณไม่สามารถติดตั้ง MediaWiki",
"config-env-php": "มี PHP $1 ติดตั้งอยู่",
"config-env-hhvm": "มี HHVM $1 ติดตั้งอยู่",
- "config-unicode-using-intl": "ใช้[http://pecl.php.net/intl ส่วนขยาย intl PECL] สำหรับการปรับ Unicode เข้าสู่รูปปกติ (Unicode normalization)",
- "config-unicode-pure-php-warning": "<strong>คำเตือน:</strong> [http://pecl.php.net/intl intl ส่วนขยาย PECL] ไม่พร้อมใช้งานสำหรับการจัดมาตรฐาน Unicode กำลังกลับไปใช้ PHP ที่แท้จริงแบบช้า\nถ้าคุณเปิดดำเนินการไซต์ที่มีปริมาณการใช้งานสูง คุณควรอ่านดูเกี่ยวกับ[https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations การจัดมาตรฐาน Unicode] สักเล็กน้อย",
+ "config-unicode-using-intl": "ใช้[https://pecl.php.net/intl ส่วนขยาย intl PECL] สำหรับการปรับ Unicode เข้าสู่รูปปกติ (Unicode normalization)",
+ "config-unicode-pure-php-warning": "<strong>คำเตือน:</strong> [https://pecl.php.net/intl intl ส่วนขยาย PECL] ไม่พร้อมใช้งานสำหรับการจัดมาตรฐาน Unicode กำลังกลับไปใช้ PHP ที่แท้จริงแบบช้า\nถ้าคุณเปิดดำเนินการไซต์ที่มีปริมาณการใช้งานสูง คุณควรอ่านดูเกี่ยวกับ[https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations การจัดมาตรฐาน Unicode] สักเล็กน้อย",
"config-no-db": "ไม่พบไดรเวอร์ฐานข้อมูลที่เหมาะสม! คุณจำเป็นต้องติดตั้งไดรเวอร์ฐานข้อมูลสำหรับ PHP\nชนิดฐานข้อมูลต่อไปนี้ได้รับการสนับสนุน: $1\n\nถ้าคุณคอมไพล์ PHP ด้วยตนเอง ให้กำหนดค่าใหม่อีกครั้งโดยเปิดใช้งานไคลเอนต์ฐานข้อมูล ตัวอย่างเช่น ใช้ <code>./configure --with-mysqli</code>\nถ้าคุณติดตั้ง PHP จากแพกเกจ Debian หรือ Ubuntu คุณก็จำเป็นต้องติดตั้งแพกเกจต่อไปนี้ ตัวอย่างเช่น แพกเกจ <code>php5-mysql</code>",
"config-outdated-sqlite": "<strong>คำเตือน:</strong> คุณมี SQLite $1 ซึ่งต่ำกว่ารุ่นขั้นต่ำที่ต้องการ $2 ดังนั้น SQLite จะไม่พร้อมให้ใช้งาน",
"config-no-fts3": "<strong>คำเตือน:</strong> SQLite ถูกคอมไพล์โดยไม่มี[//sqlite.org/fts3.html โมดูล FTS3] คุณลักษณะการค้นหาจะไม่พร้อมใช้งานบนแบ็กเอนด์นี้",
@@ -60,12 +60,11 @@
"config-pcre-no-utf8": "<strong>ข้อผิดพลาดร้ายแรง:</strong> โมดูล PCRE ของ PHP ดูเหมือนจะถูกคอมไพล์โดยไม่มีการรองรับ PCRE_UTF8\nMediaWiki ต้องการการรองรับ UTF-8 เพื่อให้ทำงานได้อย่างถูกต้อง",
"config-memory-raised": "<code>memory_limit</code> ของ PHP คือ $1 ได้เพิ่มเป็น $2",
"config-memory-bad": "<strong>คำเตือน:</strong> <code>memory_limit</code> ของ PHP คือ $1.\nเป็นไปได้ว่ามันอาจต่ำเกินไป\nการติดตั้งอาจล้มเหลวได้!",
- "config-xcache": "มี [http://xcache.lighttpd.net/ XCache] ติดตั้งอยู่",
"config-apc": "มี [http://www.php.net/apc APC] ติดตั้งอยู่",
"config-apcu": "มี [http://www.php.net/apcu APCu] ติดตั้งอยู่",
- "config-wincache": "มี [http://www.iis.net/download/WinCacheForPhp WinCache] ติดตั้งอยู่",
+ "config-wincache": "มี [https://www.iis.net/download/WinCacheForPhp WinCache] ติดตั้งอยู่",
"config-no-cache-apcu": "<strong>คำเตือน:</strong> ไม่พบ [http://www.php.net/apcu APCu] [http://xcache.lighttpd.net/ XCache] หรือ [http://www.iis.net/download/WinCacheForPhp WinCache]\nการแคชวัตถุไม่ได้ถูกเปิดใช้งาน",
- "config-mod-security": "<strong>คำเตือน:</strong> เว็บเซิร์ฟเวอร์ของคุณมี [http://modsecurity.org/ mod_security]/mod_security2 เปิดใช้งานอยู่ การตั้งค่าทั่วไปหลายอย่างของสิ่งนี้จะก่อให้เกิดปัญหาสำหรับ MediaWiki และซอฟต์แวร์อื่นที่อนุญาตให้ผู้ใช้สามารถโพสต์เนื้อหาได้ตามที่ผู้ใช้\nหากเป็นไปได้ ควรปิดใช้งานคุณลักษณะนี้ หรือมิฉะนั้นก็ อ้างไปยัง[http://modsecurity.org/documentation/ เอกสารกำกับการใช้งาน mod_security] หรือติดต่อการสนับสนุนจากโฮสต์ของคุณ ถ้าคุณพบข้อผิดพลาดโดยสุ่ม",
+ "config-mod-security": "<strong>คำเตือน:</strong> เว็บเซิร์ฟเวอร์ของคุณมี [https://modsecurity.org/ mod_security]/mod_security2 เปิดใช้งานอยู่ การตั้งค่าทั่วไปหลายอย่างของสิ่งนี้จะก่อให้เกิดปัญหาสำหรับ MediaWiki และซอฟต์แวร์อื่นที่อนุญาตให้ผู้ใช้สามารถโพสต์เนื้อหาได้ตามที่ผู้ใช้\nหากเป็นไปได้ ควรปิดใช้งานคุณลักษณะนี้ หรือมิฉะนั้นก็ อ้างไปยัง[https://modsecurity.org/documentation/ เอกสารกำกับการใช้งาน mod_security] หรือติดต่อการสนับสนุนจากโฮสต์ของคุณ ถ้าคุณพบข้อผิดพลาดโดยสุ่ม",
"config-diff3-bad": "ไม่พบ GNU diff3",
"config-git": "พบซอฟต์แวร์ควบคุมรุ่น Git: <code>$1</code>",
"config-git-bad": "ไม่พบซอฟต์แวร์ควบคุมรุ่น Git",
@@ -235,6 +234,6 @@
"config-download-localsettings": "ดาวน์โหลด <code>LocalSettings.php</code>",
"config-help-tooltip": "คลิกเพื่อขยาย",
"config-nofile": "ไม่พบไฟล์ \"$1\" มันอาจถูกลบไปแล้วหรือไม่?",
- "mainpagetext": "<strong>ติดตั้งมีเดียวิกิสำเร็จ</strong>",
+ "mainpagetext": "<strong>ติดตั้งมีเดียวิกิแล้ว</strong>",
"mainpagedocfooter": "ศึกษา[https://meta.wikimedia.org/wiki/Help:Contents คู่มือการใช้งาน] สำหรับเริ่มต้นใช้งานซอฟต์แวร์วิกิ\n\n== เริ่มต้น ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings รายการการปรับแต่งระบบ] (ภาษาอังกฤษ)\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ คำถามที่ถามบ่อยในมีเดียวิกิ] (ภาษาอังกฤษ)\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce เมลลิงลิสต์ของมีเดียวิกิ]"
}
diff --git a/www/wiki/includes/installer/i18n/tl.json b/www/wiki/includes/installer/i18n/tl.json
index e078752f..54a20168 100644
--- a/www/wiki/includes/installer/i18n/tl.json
+++ b/www/wiki/includes/installer/i18n/tl.json
@@ -7,7 +7,8 @@
"Amire80",
"Jojit fb",
"Macofe",
- "Emem.calist"
+ "Emem.calist",
+ "LR Guanzon"
]
},
"config-desc": "Ang tagapagluklok para sa MediaWiki",
@@ -16,7 +17,7 @@
"config-localsettings-upgrade": "Napansin ang isang talaksang <code>LocalSettings.php</code>.\nUpang maitaas ang uri ng pagluluklok na ito, paki ipasok ang halaga ng <code>$wgUpgradeKey</code> sa loob ng kahong nasa ibaba.\nMatatagpuan mo ito sa loob ng <code>LocalSettings.php</code>.",
"config-localsettings-cli-upgrade": "Napansin ang isang talaksan ng <code>LocalSettings.php</code>.\nUpang isapanahon ang pagtatalagang ito, mangyaring patakbuhin sa halip ang <code>update.php</code>",
"config-localsettings-key": "Susi ng pagsasapanahon:",
- "config-localsettings-badkey": "Hindi tama ang susing ibinigay mo.",
+ "config-localsettings-badkey": "Hindi tama ang susi sa pag-upgrade na ibinigay mo.",
"config-upgrade-key-missing": "Napansin ang isang umiiral na pagtatalaga ng MediaWiki.\nUpang isapanahon ang katalagahang ito, mangyaring ilagay ang sumusunod na guhit sa ilalim ng iyong <code>LocalSettings.php</code>:\n\n$1",
"config-localsettings-incomplete": "Lumilitaw na hindi pa buo ang umiiral na <code>LocalSettings.php</code>.\nAng pabagu-bagong $1 ay hindi nakatakda.\nMangyaring baguhin ang <code>LocalSettings.php</code> upang ang maitakda ang pagpapabagu-bagong ito, at pindutin ang \"{{int:Config-continue}}\".",
"config-localsettings-connection-error": "Isang kamalian ang nakatagpo noong kumakabit sa kalipunan ng dato na ginagamit ang tinukoy na mga katakdaan sa loob ng <code>LocalSettings.php</code> o\n<code>LocalSettings.php</code>. Paki kumpunihin ang mga katakdaang ito at subukang muli.\n\n$1",
@@ -46,25 +47,25 @@
"config-page-existingwiki": "Umiiral na wiki",
"config-help-restart": "Nais mo bang hawiin ang lahat ng nasagip na datong ipinasok mo at muling simulan ang proseso ng pagluluklok?",
"config-restart": "Oo, muling simulan ito",
- "config-welcome": "=== Pagsusuring pangkapaligiran ===\nIsinasagawa ang payak na mga pagsusuri upang makita kung ang kapaligirang ito ay angkop para sa pagluluklok ng MediaWiki.\nDapat mong ibigay ang mga kinalabasan ng mga pagsusuring ito kung kailangan mo ng tulong habang nagluluklok.",
- "config-copyright": "=== Karapatang-ari at Tadhana ===\n\n$1\n\nAng programang ito ay malayang software; maaari mo itong ipamahagi at/o baguhin sa ilalim ng mga tadhana ng Pangkalahatang Pampublikong Lisensiyang GNU ayon sa pagkakalathala ng Free Software Foundation; na maaaring bersyong 2 ng Lisensiya, o (kung nais mo) anumang susunod na bersyon.\n\nIpinamamahagi ang programang ito na umaasang magiging gamitin, subaliut '''walang anumang katiyakan'''; na walang pahiwatig ng '''pagiging mabenta''' o '''kaangkupan para sa isang tiyak na layunin'''.\nTingnan ang Pangkalahatang Pampublikong Lisensiyang GNU para sa mas maraming detalye.\n\nDapat nakatanggap ka ng <doclink href=Copying>isang sipi ng Pangkalahatang Pampublikong Lisensiyang GNU</doclink> kasama ng programang ito; kung hindi, sumulat sa Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, o [http://www.gnu.org/licenses//gpl.html basahin ito sa Internet].",
+ "config-welcome": "=== Pagsusuring pangkapaligiran ===\nIsasagawa ang payak na mga pagsusuri upang makita kung ang kapaligirang ito ay angkop para sa pagluluklok ng MediaWiki.\nTandaan na dapat isama mo itong impormasyon kung kailangan mo ng tulong kung paano tapusin ang instalasyon.",
+ "config-copyright": "=== Karapatang-ari at Tadhana ===\n\n$1\n\nAng programang ito ay malayang software; maaari mo itong ipamahagi at/o baguhin sa ilalim ng mga tadhana ng Pangkalahatang Pampublikong Lisensiyang GNU ayon sa pagkakalathala ng Free Software Foundation; na maaaring bersyong 2 ng Lisensiya, o (kung nais mo) anumang susunod na bersyon.\n\nIpinamamahagi ang programang ito na umaasang magiging gamitin, subaliut '''walang anumang katiyakan'''; na walang pahiwatig ng '''pagiging mabenta''' o '''kaangkupan para sa isang tiyak na layunin'''.\nTingnan ang Pangkalahatang Pampublikong Lisensiyang GNU para sa mas maraming detalye.\n\nDapat nakatanggap ka ng <doclink href=Copying>isang sipi ng Pangkalahatang Pampublikong Lisensiyang GNU</doclink> kasama ng programang ito; kung hindi, sumulat sa Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, o [https://www.gnu.org/licenses//gpl.html basahin ito sa Internet].",
"config-sidebar": "* [https://www.mediawiki.org Tahanan ng MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Gabay ng Tagagamit]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Gabay ng Tagapangasiwa]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Mga Malimit Itanong]\n----\n* <doclink href=Readme>Basahin ako</doclink>\n* <doclink href=ReleaseNotes>Mga tala ng paglalabas</doclink>\n* <doclink href=Copying>Pagkopya</doclink>\n* <doclink href=UpgradeDoc>Pagsasapanahon</doclink>",
"config-env-good": "Nasuri na ang kapaligiran.\nMailuluklok mo ang MediaWiki.",
"config-env-bad": "Nasuri na ang kapaligiran.\nHindi mo mailuklok ang MediaWiki.",
"config-env-php": "Naitalaga ang PHP na $1.",
- "config-unicode-using-intl": "Ginagamit ang [http://pecl.php.net/intl intl dugtong na PECL] para sa pagsasanormal ng Unikodigo.",
- "config-unicode-pure-php-warning": "'''Babala''': Ang [http://pecl.php.net/intl dugtong ng internasyunal na PECL] ay hindi makukuha upang makapanghawak ng pagpapanormal ng Unikodigo, na babagsak na pabalik sa mabagal na pagsasakatuparan ng dalisay na PHP.\nKapag nagpapatakbo ka ng isang pook na mataas ang trapiko, dapat kang bumasa ng kaunti hinggil sa [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations pagpapanormal ng Unikodigo].",
+ "config-env-hhvm": "Naka-install ang HHVM $1.",
+ "config-unicode-using-intl": "Ginagamit ang [https://pecl.php.net/intl intl dugtong na PECL] para sa pagsasanormal ng Unikodigo.",
+ "config-unicode-pure-php-warning": "'''Babala''': Ang [https://pecl.php.net/intl dugtong ng internasyunal na PECL] ay hindi makukuha upang makapanghawak ng pagpapanormal ng Unikodigo, na babagsak na pabalik sa mabagal na pagsasakatuparan ng dalisay na PHP.\nKapag nagpapatakbo ka ng isang pook na mataas ang trapiko, dapat kang bumasa ng kaunti hinggil sa [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations pagpapanormal ng Unikodigo].",
"config-unicode-update-warning": "'''Babala''': Ang nakaluklok na bersiyon ng pambalot ng pagpapanormal ng Unikodigo ay gumagamit ng isang mas matandang bersiyon ng aklatan ng [http://site.icu-project.org/ proyekto ng ICU].\nDapat kang [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations magtaas ng uri] kung may pag-aalala ka hinggil sa paggamit ng Unikodigo.",
- "config-no-db": "Hindi matagpuan ang isang angkop na tagapagmaneho ng kalipunan ng datos! Kailangan mong magluklok ng isang tagapagmaneho ng kalipunan ng dato para sa PHP.\nTinatangkilik ang sumusunod na mga uri ng kalipunan ng dato: $1.\n\nKung ikaw ay nasa isang pinagsasaluhang pagpapasinaya, hilingin sa iyong tagapagbigay ng pagpapasinaya na iluklok ang isang angkop na tagapagmaneho ng kalipunan ng dato.\nKung ikaw mismo ang nangalap ng PHP, muling isaayos ito na pinagagana ang isang kliyente ng kalipunan ng dato, halimbawa na ang paggamit ng <code>./configure --with-mysql</code>.\nKung iniluklok mo ang PHP mula sa isang pakete ng Debian o Ubuntu, kung gayon kailangan mo ring magluklok ng modyul na php5-mysql.",
+ "config-no-db": "Hindi matagpuan ang isang angkop na tagapagmaneho ng kalipunan ng datos! Kailangan mong magluklok ng isang tagapagmaneho ng kalipunan ng dato para sa PHP.\nTinatangkilik ang {{PLURAL:$2|mga}} sumusunod na uri ng kalipunan ng dato: $1.\n\nKung ikaw mismo ang nangalap ng PHP, muling isaayos ito na pinagagana ang isang kliyente ng kalipunan ng dato, halimbawa na ang paggamit ng <code>./configure --with-mysqli</code>.\nKung iniluklok mo ang PHP mula sa isang pakete ng Debian o Ubuntu, kung gayon kailangan mo ring magluklok, halimbawa ay ang paketeng <code>php5-mysql</code>.",
"config-outdated-sqlite": "'''Babala''': mayroong kang $1 ng SQLite, na mas mababa kaysa sa pinaka mababang kailangang bersiyon na $2. Magiging hindi makukuha ang SQLite.",
"config-no-fts3": "'''Warning''': Ang SQLite ay hindi itinala at tinipon na wala ang [//sqlite.org/fts3.html modulong FTS3], ang mga tampok na panghanap ay magiging hindi makukuha sa ibabaw ng panlikod na dulong ito.",
"config-pcre-no-utf8": "'''Malubha''': Tila tinipon ang modyul na PCRE ng PHP na wala ang suporta ng PCRE_UTF8.\nNangangailangan ang MediaWiki ng suporta ng UTF-8 upang maging tama ang pag-andar.",
"config-memory-raised": "Ang <code>hangganan_ng_alaala</code> ng PHP ay $1, itinaas sa $2.",
"config-memory-bad": "'''Babala:''' Ang <code>hangganan_ng_alaala</code> ng PHP ay $1.\nIto ay maaaring napakababa.\nMaaaring mabigo ang pagluluklok!",
- "config-xcache": "Ininstala na ang [http://xcache.lighttpd.net/ XCache]",
"config-apc": "Ininstala na ang [http://www.php.net/apc APC]",
- "config-wincache": "Ininstala na ang [http://www.iis.net/download/WinCacheForPhp WinCache]",
- "config-mod-security": "'''Babala''': Ang tagapaghain mo ng sangkasaputan ay pinagana na mayroong [http://modsecurity.org/ mod_security]. Kung mali ang kaayusan, makapagdurulot ito ng mga suliranin para sa MediaWiki o ibang mga sopwer na nagpapahintulot sa mga tagagamit na magpaskil ng hindi makatwirang nilalaman.\nSumangguni sa [http://modsecurity.org/documentation/ mod_security kasulatan] o makipag-ugnayan sa suporta ng iyong tagapagpasinaya kapag nakatagpo ng alin mang mga kamalian.",
+ "config-wincache": "Ininstala na ang [https://www.iis.net/download/WinCacheForPhp WinCache]",
+ "config-mod-security": "'''Babala''': Ang tagapaghain mo ng sangkasaputan ay pinagana na mayroong [https://modsecurity.org/ mod_security]. Kung mali ang kaayusan, makapagdurulot ito ng mga suliranin para sa MediaWiki o ibang mga sopwer na nagpapahintulot sa mga tagagamit na magpaskil ng hindi makatwirang nilalaman.\nSumangguni sa [https://modsecurity.org/documentation/ mod_security kasulatan] o makipag-ugnayan sa suporta ng iyong tagapagpasinaya kapag nakatagpo ng alin mang mga kamalian.",
"config-diff3-bad": "Hindi natagpuan ang GNU diff3.",
"config-imagemagick": "Natagpuan ang ImageMagick: <code>$1</code>.\nPapaganahin ang pagkakagyat ng larawan kapag pinagana mo ang mga pagkakargang paitaas.",
"config-gd": "Natagpuan ang pinasadyang nakapaloob na grapiks ng GD.\nPapaganahin ang pagkakagyat ng larawan kapag pinagana mo ang mga pagkakargang paitaas.",
@@ -107,24 +108,24 @@
"config-sqlite-dir-help": "Iniimbak ng SQLite ang lahat ng dato sa loob ng isang nag-iisang file.\n\nAng ibibigay mong directory ay dapat na maging masusulatan ng tagapaghain ng kasaputan habang nag-i-install.\n\n'''Hindi''' ito dapat na mapuntahan sa pamamagitan ng web server, ito ang dahilan kung bakit hindi namin ito inilalagay sa kung nasaan ang iyong mga file ng PHP.\n\nAng installer ay magsusulat ng isang file na <code>.htaccess</code> na kasama ito, subalit kapag nabigo iyon mayroong isang tao na maaaring makakuha ng pagka nakakapunta sa iyong hilaw na database.\nKasama riyan ang hilaw na dato ng tagagamit (mga email address, pinaghalong mga password) pati na ang nabura nang mga pagbabago at iba pang may pagbabawal na dato ng wiki.\n\nIsaalang-alang ang paglalagay na magkakasama ang database sa ibang lugar, halimbawa na ang sa loob ng <code>/var/lib/mediawiki/yourwiki</code>.",
"config-oracle-def-ts": "Likas na nakatakdang puwang ng talahanayan:",
"config-oracle-temp-ts": "Pansamantalang puwang ng talahanayan:",
- "config-type-mysql": "MySQL",
+ "config-type-mysql": "MySQL (o katugma)",
"config-type-postgres": "PostgreSQL",
"config-type-sqlite": "SQLite",
"config-type-oracle": "Oracle",
"config-support-info": "Sinusuportahan ng MediaWiki ang sumusunod na mga sistema ng kalipunan ng dato:\n\n$1\n\nKung hindi mo makita ang sistema ng kalipunan ng dato na sinusubukan mong gamitin na nakatala sa ibaba, kung gayon ay sundi ang mga tagubilin na nakakawing sa itaas upang mapagana ang suporta,",
- "config-dbsupport-mysql": "* Ang $1 ay ang pangunahing puntirya para sa MediaWiki at ang pinaka sinusuportahan ([http://www.php.net/manual/en/mysql.installation.php paano magtipon ng PHP na mayroong suporta ng MySQL])",
- "config-dbsupport-postgres": "* Ang $1 ay isang bantog na sistema ng kalipunan ng dato na bukas ang pinagmulan na panghalili sa MySQL ([http://www.php.net/manual/en/pgsql.installation.php paano magtipon ng PHP na mayroong suporta ng PostgreSQL]). Maaaring mayroong ilang hindi pangunahing mga surot na natitira pa, at hindi iminumungkahi para gamitin sa loob ng isang kapaligiran ng produksiyon.",
- "config-dbsupport-sqlite": "Ang $1 ay isang magaan ang timbang na sistema ng kalipunan ng dato na sinusuportahan nang napaka mainam. ([http://www.php.net/manual/en/pdo.installation.php Paano magtipon ng PHP na mayroong suporta ng SQLite], gumagamit ng PDO)",
- "config-dbsupport-oracle": "* Ang $1 ay isang kalipunan ng dato ng kasigasigang pangkalakal. ([http://www.php.net/manual/en/oci8.installation.php Paano magtipunan ng PHP na mayroong suporta ng OCI8])",
+ "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] ang pangunahing puntirya para sa MediaWiki at ang pinaka sinusuportahan. Gumagana rin ang MediaWiki [{{int:version-db-mariadb-url}} MariaDB] at sa [{{int:version-db-percona-url}} Percona Server], na tugma sa MySQL. ([http://www.php.net/manual/en/mysql.installation.php Paano magtipon ng PHP na mayroong suporta ng MySQL])",
+ "config-dbsupport-postgres": "* Ang [{{int:version-db-postgres-url}} PostgreSQL] ay isang bantog na sistema ng kalipunan ng dato na bukas ang pinagmulan na panghalili sa MySQL. ([http://www.php.net/manual/en/pgsql.installation.php Paano magtipon ng PHP na mayroong suporta ng PostgreSQL]).",
+ "config-dbsupport-sqlite": "* Ang [{{int:version-db-sqlite-url}} SQLite] ay isang magaan ang timbang na sistema ng kalipunan ng dato na sinusuportahan nang napaka mainam. ([http://www.php.net/manual/en/pdo.installation.php Paano magtipon ng PHP na mayroong suporta ng SQLite], gumagamit ng PDO)",
+ "config-dbsupport-oracle": "* Ang [{{int:version-db-oracle-url}} Oracle] ay isang kalipunan ng dato ng kasigasigang pangkalakal. ([http://www.php.net/manual/en/oci8.installation.php Paano magtipunan ng PHP na mayroong suporta ng OCI8])",
"config-header-mysql": "Mga katakdaan ng MySQL",
"config-header-postgres": "Mga katakdaan ng PostgreSQL",
"config-header-sqlite": "Mga katakdaan ng SQLite",
"config-header-oracle": "Mga katakdaan ng Oracle",
"config-invalid-db-type": "Hindi tanggap na uri ng kalipunan ng dato",
- "config-missing-db-name": "Dapat kang magpasok ng isang halaga para sa \"Pangalan ng kalipunan ng dato\"",
- "config-missing-db-host": "Dapat kang magpasok ng isang halaga para sa \"Tagapagpasinaya ng kalipunan ng dato\"",
- "config-missing-db-server-oracle": "Dapat kang magpasok ng isang halaga para sa \"TNS ng kalipunan ng dato\"",
- "config-invalid-db-server-oracle": "Hindi katanggap-tanggap na pangalan ng TNSng kalipunan ng dato na \"$1\".\nGumamit lamang ng mga titik ng ASCII (a-z, A-Z), mga bilang (0-9), mga salungguhit (_) at mga tuldok (.).",
+ "config-missing-db-name": "Dapat kang magpasok ng isang halaga para sa \"{{int:config-db-name}}\".",
+ "config-missing-db-host": "Dapat kang magpasok ng isang halaga para sa \"{{int:config-db-host}}\".",
+ "config-missing-db-server-oracle": "Dapat kang magpasok ng isang halaga para sa \"{{int:config-db-host-oracle}}\".",
+ "config-invalid-db-server-oracle": "Hindi katanggap-tanggap na pangalan ng TNSng kalipunan ng dato na \"$1\".\nGumamit ng kahit na \"TNS Name\" o \"Easy Connect\" na tali ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Paraan ng Pagpapangalan ng Oracle]).",
"config-invalid-db-name": "Hindi tanggap na pangalan ng kalipunan ng dato na \"$1\".\nGumamit lamang ng mga titik ng ASCII (a-z, A-Z), mga bilang (0-9), mga salungguhit (_) at mga gitling (-).",
"config-invalid-db-prefix": "Hindi tanggap na unlapi ng kalipunan ng dato na \"$1\".\nGamitin lamang ang mga titik na ASCII (a-z, A-Z), mga bilang (0-9), mga salungguhit (_) at mga gitling (-).",
"config-connection-error": "$1.\n\nSuriin ang host, pangalan at password na nasa ibaba at subukan ulit.",
@@ -145,7 +146,7 @@
"config-upgrade-done": "Buo na ang pagtataas ng uri.\n\nMaaari mo na ngayong [$1 gamitin ang iyong wiki].\n\nKung nais mong muling likhain ang iyong talaksang <code>LocalSettings.php</code>, lagitikin ang pindutang nasa ibaba.\n'''Hindi minumungkahi''' ito maliban na lamang kung nagkakaroon ka ng mga suliranin sa piling ng wiki mo.",
"config-upgrade-done-no-regenerate": "Buo na ang pagsasapanahon.\n\nMaaari ka na ngayong [$1 magsimula sa paggamit ng wiki mo].",
"config-regenerate": "Muling likhain ang LocalSettings.php →",
- "config-show-table-status": "Nabigo ang pagtatanong na IPAKITA ANG KALAGAYAN NG TALAHANAYAN!",
+ "config-show-table-status": "Nabigo ang pagtatanong na <code>SHOW TABLE STATUS</code>!",
"config-unknown-collation": "'''Babala:''' Ang kalipunan ng dato ay gumagagamit ng hindi nakikilalang pag-iipon.",
"config-db-web-account": "Account ng kalipunan ng dato para sa pagpunta sa web",
"config-db-web-help": "Piliin ang pangalan ng tagagamit at password na gagamitin ng tagapaghain ng web upang umugnay sa tagapaghain ng database, habang nasa pangkaraniwang pagtakbo ng wiki.",
@@ -169,7 +170,7 @@
"config-ns-site-name": "Katulad ng sa pangalan ng wiki: $1",
"config-ns-other": "Iba pa (tukuyin)",
"config-ns-other-default": "Wiki Ko",
- "config-project-namespace-help": "Bilang pagsunod sa halimbawa ng Wikipedia, maraming mga wiki ang nagpapanatili ng kanilang mga pahina ng patakaran na nakahiwalay magmula sa kanilang mga pahina ng nilalaman, na nasa loob ng isang \"'''puwang na pampangalan ng proyekto'''\".\nAng lahat ng mga pamagat ng pahina na nasa loob ng puwang ng pangalang ito ay nagsisimula na mayroong isang partikular na unlapi, na maaari mong tukuyin dito.\nSa nakaugalian, ang unlaping ito ay hinango mula sa pangalan ng wiki, subalit hindi ito maaaring maglaman ng mga panitik ng palabantasan na katulad ng \"#\" o \":\".",
+ "config-project-namespace-help": "Bilang pagsunod sa halimbawa ng Wikipedia, maraming mga wiki ang nagpapanatili ng kanilang mga pahina ng patakaran na nakahiwalay magmula sa kanilang mga pahina ng nilalaman, na nasa loob ng isang '''puwang na pampangalan ng proyekto'''.\nAng lahat ng mga pamagat ng pahina na nasa loob ng puwang ng pangalang ito ay nagsisimula na mayroong isang partikular na unlapi, na maaari mong tukuyin dito.\nKadalasan, ang unlaping ito ay hinango mula sa pangalan ng wiki, subalit hindi ito maaaring maglaman ng mga panitik ng palabantasan na katulad ng \"#\" o \":\".",
"config-ns-invalid": "Ang tinukoy na puwang ng pangalan na \"<nowiki>$1</nowiki>\" ay hindi katanggap-tanggap.\nTumukoy ng isang ibang puwang ng pangalan ng proyekto.",
"config-ns-conflict": "Ang tinukoy na puwang ng pangalan na \"<nowiki>$1</nowiki>\" ay sumasalungat sa isang likas na nakatakdang puwang ng pangalan ng MediaWiki.\nTumukoy ng isang ibang puwang ng pangalan ng proyekto.",
"config-admin-box": "Account ng tagapangasiwa",
@@ -193,7 +194,7 @@
"config-optional-continue": "Magtanong sa akin ng marami pang mga tanong.",
"config-optional-skip": "Naiinip na ako, basta iluklok na lang ang wiki.",
"config-profile": "Balangkas ng mga karapatan ng tagagamit:",
- "config-profile-wiki": "Tradisyonal na wiki",
+ "config-profile-wiki": "Bukas na wiki",
"config-profile-no-anon": "Kailangan ang paglikha ng account",
"config-profile-fishbowl": "Pinahintulutang mga patnugot lamang",
"config-profile-private": "Pribadong wiki",
@@ -207,7 +208,7 @@
"config-license-gfdl": "Lisensiyang 1.3 ng Malayang Dokumentasyon ng GNU o mas lalong huli",
"config-license-pd": "Nasasakupan ng Madla",
"config-license-cc-choose": "Pumili ng isang pasadyang Lisensiya ng Malikhaing mga Pangkaraniwan",
- "config-license-help": "Maraming mga pangmadlang wiki ang naglalagay ng lahat ng mga ambag sa ilalim ng [http://freedomdefined.org/Definition lisensiyang malaya].\nNakakatulong ito sa paglikha ng isang diwa ng pagmamay-ari ng pamayanan at nakapanghihikayat ng ambag na pangmahabang panahon.\nSa pangkalahatan, hindi kailangan ang isang wiking pribado o pangsamahan.\n\nKung nais mong magamit ang teksto magmula sa Wikipedia, at nais mong makatanggap ang Wikipedia ng tekstong kinopya magmula sa wiki mo, dapat mong piliin ang '''Creative Commons Attribution Share Alike''' (Pagbanggit na Pinagsasaluhang Magkatulad ng Malikhaing Pangkaraniwan).\n\nDating ginamit ng Wikipedia ang Lisensiya ng Kasulatang Malaya ng GNU (GNU Free Documentation License o GFDL).\nIsang katanggap-tanggap na lisensiya ang GFDL, subalit mahirap itong maunawaan.\nMahirap din ang paggamit na muli ng nilalaman na nasa ilalim ng GFDL.",
+ "config-license-help": "Maraming mga pangmadlang wiki ang naglalagay ng lahat ng mga ambag sa ilalim ng [https://freedomdefined.org/Definition lisensiyang malaya].\nNakakatulong ito sa paglikha ng isang diwa ng pagmamay-ari ng pamayanan at nakapanghihikayat ng ambag na pangmahabang panahon.\nSa pangkalahatan, hindi kailangan ang isang wiking pribado o pangsamahan.\n\nKung nais mong magamit ang teksto magmula sa Wikipedia, at nais mong makatanggap ang Wikipedia ng tekstong kinopya magmula sa wiki mo, dapat mong piliin ang <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nDating ginamit ng Wikipedia ang Lisensiya ng Kasulatang Malaya ng GNU (GNU Free Documentation License o GFDL).\nIsang katanggap-tanggap na lisensiya ang GFDL, subalit mahirap itong maunawaan.\nMahirap din ang paggamit na muli ng nilalaman na nasa ilalim ng GFDL.",
"config-email-settings": "Mga katakdaan ng e-liham",
"config-enable-email": "Paganahin ang palabas na e-liham",
"config-enable-email-help": "Kung nais mong gumana ang e-liham, ang mga katakdaan ng liham ng [http://www.php.net/manual/en/mail.configuration.php PHP] ay kailangang maging wasto ang pagkakaayos.\nKung ayaw mo nang anumang mga katampukan ng e-liham, maaari mong huwag paganahin ang mga ito rito.",
@@ -227,7 +228,7 @@
"config-upload-deleted": "Direktoryo para sa binurang mga talaksan:",
"config-upload-deleted-help": "Pumili ng isang direktoryong pagsusupnayan ng naburang mga talaksan.\nIdeyal na dapat itong hindi mapupuntahan mula sa web.",
"config-logo": "URL ng logo:",
- "config-logo-help": "Ang likas na nakatakdang pabalat ng MediaWiki ay nagsasama ng puwang para sa isang logong 135x160 ang piksel na nasa itaas ng menu ng panggilid na bareta.\nMagkargang papaitaas ng isang imahe na mayroong naaangkop na sukat, at ipasok dito ang URL.\n\nKung ayaw mo ng isang logo, iwanang walang laman ang kahong ito.",
+ "config-logo-help": "Ang likas na nakatakdang pabalat ng MediaWiki ay nagsasama ng puwang para sa isang logong 135x160 ang piksel na nasa itaas ng menu ng panggilid na bareta.\nMagkargang papaitaas ng isang imahe na mayroong naaangkop na sukat, at ipasok dito ang URL.\n\nMaaari mong gamiting ang <code>$wgStylePath</code> o <code>$wgScriptPath</code> kung ang iyong logo ay kaugnay sa mga daan na iyon.\n\nKung ayaw mo ng isang logo, iwanang walang laman ang kahong ito.",
"config-instantcommons": "Paganahin ang Mga Pangkaraniwang Biglaan",
"config-instantcommons-help": "Ang [https://www.mediawiki.org/wiki/InstantCommons Instant Commons] ay isang tampok na nagpapahintulot sa mga wiki upang gumamit ng mga imahe, mga tunog at iba pang mga midyang matatagpuan sa pook ng [https://commons.wikimedia.org/ Wikimedia Commons].\nUpang magawa ito, nangangailangan ang MediaWiki ng pagka nakakapunta sa Internet.\n\nPara sa mas marami pang kabatiran hinggil sa tampok na ito, kabilang na ang mga tagubilin sa kung paano ito itakda para sa mga wiki na bukod pa kaysa sa Wikimedia Commons, sumangguni sa [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos gabay].",
"config-cc-error": "Hindi nagbigay ng resulta ang pampili ng lisensiya ng Malikhaing Pangkaraniwan.\nIpasok na kinakamay ang pangalan ng lisensiya.",
diff --git a/www/wiki/includes/installer/i18n/tokipona.json b/www/wiki/includes/installer/i18n/tokipona.json
deleted file mode 100644
index 348380f7..00000000
--- a/www/wiki/includes/installer/i18n/tokipona.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "@metadata": {
- "authors": [
- "Robin0van0der0vliet"
- ]
- },
- "config-page-language": "toki"
-}
diff --git a/www/wiki/includes/installer/i18n/tr.json b/www/wiki/includes/installer/i18n/tr.json
index c3a75398..6d942979 100644
--- a/www/wiki/includes/installer/i18n/tr.json
+++ b/www/wiki/includes/installer/i18n/tr.json
@@ -17,7 +17,9 @@
"HakanIST",
"McAang",
"Elftrkn",
- "Vito Genovese"
+ "Vito Genovese",
+ "Incelemeelemani",
+ "Hedda"
]
},
"config-desc": "MediaWiki yükleyicisi",
@@ -57,23 +59,27 @@
"config-help-restart": "Girişini yaptığınız tüm kayıtlı verileri silerek, yükleme işlemini yeniden başlatmak ister misiniz?",
"config-restart": "Evet, yeniden başlat",
"config-welcome": "===Ortam Kontrolleri===\nOrtamın Mediawiki kurulumuna uygun olup olmadığını anlamak için basit kontroller yapılacak.\nKurulumu nasıl tamamlayacağınız konusunda destek isterken bu bilgileri eklemeyi unutmayın.",
- "config-copyright": "=== Telif Hakları ve Koşulları ===\n\n$1\n\nBu program ücretsiz bir yazılımdır; yeniden dağıtabilir veya Özgür Yazılım Kuruluşu tarafından yayınlanan (GNU) Genel Kamu Lisansı koşulları altında değiştirebilirsiniz; isterseniz ikinci lisans sürümünü veya (sizin seçeneğiniz) herhangi bir sonraki lisans sürümünü kullanabilirsiniz.\n\nBu program, faydalı olacağı umuduyla dağıtılmaktadır, ancak ''' herhangi bir garantisi yoktur '''; ''' uygunluk ''' veya ''' belirli bir amaca uygunluk ''' gibi dolaylı garantileri bile yoktur.\nDaha fazla ayrıntı için (GNU) Genel Kamu Lisansına bakınız.\n\nBu program ile birlikte <doclink href=\"Copying\">bir (GNU) Genel Kamu Lisansının bir kopyasını </doclink> almış olmanız gerekir; bu program (GNU) Genel Kamu Lisansı ile dağıtılmadıysa, Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, ABD adresine yazın veya [http://www.gnu.org/copyleft/gpl.html online olarak okuyun].",
+ "config-copyright": "=== Telif Hakları ve Koşulları ===\n\n$1\n\nBu program ücretsiz bir yazılımdır; yeniden dağıtabilir veya Özgür Yazılım Kuruluşu tarafından yayınlanan (GNU) Genel Kamu Lisansı koşulları altında değiştirebilirsiniz; isterseniz ikinci lisans sürümünü veya (sizin seçeneğiniz) herhangi bir sonraki lisans sürümünü kullanabilirsiniz.\n\nBu program, faydalı olacağı umuduyla dağıtılmaktadır, ancak ''' herhangi bir garantisi yoktur '''; ''' uygunluk ''' veya ''' belirli bir amaca uygunluk ''' gibi dolaylı garantileri bile yoktur.\nDaha fazla ayrıntı için (GNU) Genel Kamu Lisansına bakınız.\n\nBu program ile birlikte <doclink href=\"Copying\">bir (GNU) Genel Kamu Lisansının bir kopyasını </doclink> almış olmanız gerekir; bu program (GNU) Genel Kamu Lisansı ile dağıtılmadıysa, Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, ABD adresine yazın veya [https://www.gnu.org/copyleft/gpl.html online olarak okuyun].",
"config-sidebar": "* [https://www.mediawiki.org MediaWiki anasayfa]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Kullanıcı Kılavuzu]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Hizmetli Rehberi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ SSS]\n----\n* <doclink href=Readme>Beni oku</doclink>\n* <doclink href=ReleaseNotes>Sürüm notları</doclink>\n* <doclink href=Copying>Kopyalama</doclink>\n* <doclink href=UpgradeDoc>Yükseltme</doclink>",
"config-env-good": "Ortam kontrol edildi.\nMediaWiki'yi kurabilirsiniz.",
"config-env-bad": "Ortam kontrol edildi.\nMediaWiki'yi kuramazsınız.",
"config-env-php": "PHP $1 kurulu.",
"config-env-hhvm": "HHVM $1 kuruldu",
- "config-unicode-using-intl": "Unikod normalleştirmesi için [http://pecl.php.net/intl intl PECL uzantısı] kullanılıyor.",
- "config-unicode-pure-php-warning": "<strong>Uyarı:</strong> [http://pecl.php.net/intl intl PECL uzantısı] Unicode normalizasyonunu kaldırabilecek şekilde müsait değil; bu yüzden sayfa saf PHP uygulamasına dönüyor. Yüksek trafik alan bir sayfa çalıştırıyorsanız, [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalizasyonu] ile ilgili biraz bilgi almalısınız.",
+ "config-unicode-using-intl": "Unikod normalleştirmesi için [https://pecl.php.net/intl intl PECL uzantısı] kullanılıyor.",
+ "config-unicode-pure-php-warning": "<strong>Uyarı:</strong> [https://pecl.php.net/intl intl PECL uzantısı] Unicode normalizasyonunu kaldırabilecek şekilde müsait değil; bu yüzden sayfa saf PHP uygulamasına dönüyor. Yüksek trafik alan bir sayfa çalıştırıyorsanız, [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalizasyonu] ile ilgili biraz bilgi almalısınız.",
+ "config-unicode-update-warning": "<strong>Uyarı:</strong> Yüklü durumdaki Unicode normalleştirme sarıcı [http://site.icu-project.org/ ICU proje] kütüphanesinin eski bir sürümünü kullanır.\nUnicode kullanımı konusunda endişeleriniz varsa [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations yükseltme] yapmanız gerekmektedir.",
+ "config-no-db": "Uygun bir veri tabanı sürücüsü bulunamadı! PHP için bir veri tabanı sürücüsü kurmanız gerekir. Şu veri tabanı {{PLURAL:$2|türleri|türleri}} desteklenmektedir: $1\n\nEğer PHP'yi kendiniz derlediyseniz, bu durumda <code>./configure --with-mysqli</code> kullanarak etkinleştirilmiş veri tabanı istemcisi ile yeniden yapılandırmalısınız.\nPHP'yi bir Debian veya Ubuntu paketinden yüklediyseniz, bu durumda <code>php5-mysql</code> paketini de kurmanız gerekir.",
"config-outdated-sqlite": "<strong>Uyarı:</strong> Elinizde SQLite $1 var. Gerekli minimum sürüm: $2. SQLite kullanılamayacaktır.",
"config-no-fts3": "<strong>Uyarı:</strong> SQLite [//sqlite.org/fts3.html FTS3 modülü] olmadan derlendi, bu arkayüzde arama özellikleri kullanılamayacaktır.",
"config-pcre-old": "<strong>Ağır hata:</strong> PCRE $1 veya daha üst versiyon gerekli.\nSizin PHP kurulumunuz PCRE $2 ile bağlı.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Daha fazla bilgi].",
+ "config-pcre-no-utf8": "<strong>Önemli hata:</strong> PHP'nin PCRE modülü PCRE_UTF8 desteği olmadan derlenmiş gözüküyor.\nMediaWiki'nin doğru çalışabilmesi için UTF-8 desteği gereklidir.",
"config-memory-raised": "PHP'nin <code>memory_limit</code> (hafıza sınırı) değeri $1, $2'ye yükseltildi.",
"config-memory-bad": "<strong>Uyarı:</strong> PHP'nin <code>memory_limit</code> (hafıza sınırı) değeri $1.\nBu büyük ihtimalle çok düşük.\nKurulum başarısız olabilir!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] kurulu",
"config-apc": "[http://www.php.net/apc APC] kurulu",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] kurulu",
- "config-mod-security": "'''Uyarı:''' Web sunucunuz [http://modsecurity.org/ mod_security] etkin. Eğer yanlış yapılandırılmış ise, bu MediaWiki ve kullanıcılara isteğe bağlı içerik göndermesine izin veren diğer yazılımlar için sorun oluşturabilir.\nRastgele hatalar alırsanız [http://modsecurity.org/documentation/ mod_security belgelemesine] bakın ya da sunucunuzun desteğine başvurun.",
+ "config-apcu": "[http://www.php.net/apcu APCu] yüklendi",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] kurulu",
+ "config-no-cache-apcu": "<strong>Uyarı:</strong> [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] ya da [http://www.iis.net/download/WinCacheForPhp WinCache] kurulumu bulunamadı.\nNesne önbellekleme etkin değil.",
+ "config-mod-security": "<strong>'''Uyarı:'''</strong> Web sunucunuz [https://modsecurity.org/mod_security2 mod_security] etkin. Bunun birçok yaygın yapılandırması bulunur ve eğer yanlış yapılandırılmış ise, bu MediaWiki ve kullanıcılara isteğe bağlı içerik göndermesine izin veren diğer yazılımlar için sorun oluşturabilir.\nMümkünse bu devre dışı bırakılmalıdır. Aksi takdirde rastgele hatalar alırsanız [https://modsecurity.org/documentation/ mod_security belgelemesine] bakın ya da sunucunuzun desteğine başvurun.",
"config-diff3-bad": "GNU diff3 bulunamadı.",
"config-git": "Sürüm kontrol yazılımı Git bulundu: <code>$1</code>.",
"config-git-bad": "Sürüm kontrol yazılımı Git bulunamadı.",
@@ -188,6 +194,11 @@
"config-profile-private": "Özel wiki",
"config-profile-help": "Vikiler, mümkün olan en fazla kişiye değişiklik imkânı verdiğinizde, en iyi şekilde çalışır.\nMediaWiki'de son değişiklikleri incelemek ve tecrübesiz veya kötü niyetli kullanıcıların verdiği zararları geri almak kolaydır.\n\nAncak birçok kişi MediaWiki'yi farklı şekillerde kullanışlı bulmaktadır ve bazen herkesi viki yolunun faydalarına ikna etmek zordur.\nYani seçim sizin.\n\n<strong>{{int:config-profile-wiki}}</strong> modeli, giriş yapmamış olsa bile herkese değişiklik izni verir.\n\n<strong>{{int:config-profile-no-anon}}</strong> kullanan bir viki ise daha izlenebilirdir ancak sıradan, basit, gündelik katkı yapan kullanıcıları caydırabilir.\n\n<strong>{{int:config-profile-fishbowl}}</strong> onaylanmış kullanıcıların değişikliklerine izin verir ama herkes sayfaları ve sayfa geçmişlerini görebilir.\n\n<strong>{{int:config-profile-private}}</strong> sadece onaylanmış kullanıcıları değişiklik yapma ve sayfaları görme imkânı tanır.\n\nDaha karmaşık kullanıcı hakkı ayarları, yüklemeden sonra görülebilir; [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights ilgili kılavuza] bakınız.",
"config-license": "Telif Hakkı ve Lisans",
+ "config-license-none": "Lisans altbilgisi yok",
+ "config-license-cc-by-sa": "Creative Commons Attribution-ShareAlike",
+ "config-license-cc-by": "Creative Commons Attribution",
+ "config-license-cc-by-nc-sa": "Creative Commons Attribution-NonCommercial-ShareAlike",
+ "config-license-cc-0": "Creative Commons Zero (Kamu Malı)",
"config-license-gfdl": "GNU Free Documentation License 1.3 veya üstü",
"config-license-pd": "Kamu Malı",
"config-license-cc-choose": "Özel bir Creative Commons lisansı seçin",
@@ -228,6 +239,8 @@
"config-install-interwiki-exists": "''' Uyarı:'' ' interwiki Tablo girdileri zaten görünüyor.\nVarsayılan liste atlanıyor.",
"config-install-stats": "İstatistik başlatılıyor",
"config-install-keys": "Gizli anahtar oluşturma",
+ "config-install-updates-failed": "<strong>Hata:</strong> Güncelleme anahtarlarını tablolara ekleme şu hatayla başarısız oldu: $1",
+ "config-install-sysop": "Yönetici kullanıcı hesabı oluşturma",
"config-install-subscribe-notpossible": "cURL yüklü değil ve <code>allow_url_fopen</code> kullanılamaz.",
"config-install-mainpage": "Varsayılan içerik ile anasayfa oluşturma",
"config-install-extension-tables": "Uzantılar için etkinleştirilmiş tablolar oluşturma",
@@ -237,6 +250,8 @@
"config-help-tooltip": "genişletmek için tıklayın",
"config-nofile": "\"$1\" dosyası bulunamadı. Silindi mi?",
"config-extension-link": "Vikinizin [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions eklentileri] desteklediğini biliyor musunuz?\n\n[https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category Eklentileri kategorilerine göre] inceleyebilir ya da tüm eklentilerin listesini görmek için [https://www.mediawiki.org/wiki/Extension_Matrix Eklenti Matrisine] bakabilirsiniz.",
+ "config-skins-screenshots": "$1 (ekran görüntüleri: $2)",
+ "config-screenshot": "ekran görüntüsü",
"mainpagetext": "'''MediaWiki başarı ile kuruldu.'''",
"mainpagedocfooter": "Viki yazılımının kullanımı hakkında bilgi almak için [https://meta.wikimedia.org/wiki/Help:Contents kullanıcı rehberine] bakınız.\n\n== Yeni Başlayanlar ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Yapılandırma ayarlarının listesi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki SSS]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-posta listesi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Kendi diliniz için MediaWiki yerelleştirmesi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Kendi vikinizde spam ile nasıl savaşılacağını öğrennin]"
}
diff --git a/www/wiki/includes/installer/i18n/tt-cyrl.json b/www/wiki/includes/installer/i18n/tt-cyrl.json
index 2cc0d1ac..acd12816 100644
--- a/www/wiki/includes/installer/i18n/tt-cyrl.json
+++ b/www/wiki/includes/installer/i18n/tt-cyrl.json
@@ -27,9 +27,8 @@
"config-page-upgradedoc": "Яңарту",
"config-page-existingwiki": "Хәзерге вики",
"config-restart": "Әйе, яңадан башларга",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] куелды",
"config-apc": "[http://www.php.net/apc APC] куелды",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] куелды",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] куелды",
"config-diff3-bad": "GNU diff3 табылмады.",
"config-git": "Git юрамалар идарә итү системасы табылды: <code>$1</code>.",
"config-db-type": "Мәгълүмат базасы төре:",
diff --git a/www/wiki/includes/installer/i18n/uk.json b/www/wiki/includes/installer/i18n/uk.json
index 54339c3b..f57bcd13 100644
--- a/www/wiki/includes/installer/i18n/uk.json
+++ b/www/wiki/includes/installer/i18n/uk.json
@@ -12,7 +12,8 @@
"아라",
"Amire80",
"Piramidion",
- "Macofe"
+ "Macofe",
+ "Movses"
]
},
"config-desc": "Інсталятор MediaWiki",
@@ -52,14 +53,14 @@
"config-help-restart": "Ви бажаєте видалити всі введені та збережені вами дані і запустити процес установки спочатку?",
"config-restart": "Так, перезапустити установку",
"config-welcome": "=== Перевірка оточення ===\nБудуть проведені базові перевірки, щоб виявити, чи можлива установка MediaWiki у даній системі.\nНе забудьте включити цю інформацію, якщо ви звернетеся по підтримку, як завершити установку.",
- "config-copyright": "=== Авторське право і умови ===\n\n$1\n\nЦя програма є вільним програмним забезпеченням; Ви можете розповсюджувати та/або змінювати її під ліцензією GNU General Public License, опублікованою Фондом вільного програмного забезпечення; версією 2 цієї ліцензії або будь-якою пізнішою на Ваш вибір.\n\nЦя програма поширюється з надією на те, що вона буде корисною, однак '''без жодних гарантій'''; навіть без неявної гарантії '''комерційної цінності''' або '''придатності для певних цілей'''.\nДив. GNU General Public License для детальної інформації.\n\nВи повинні були отримати <doclink href=Copying>копію GNU General Public License</doclink> разом із цією програмою; якщо ж ні, зверніться до Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. або [http://www.gnu.org/copyleft/gpl.html ознайомтесь з нею онлайн].",
+ "config-copyright": "=== Авторське право і умови ===\n\n$1\n\nЦя програма є вільним програмним забезпеченням; Ви можете розповсюджувати та/або змінювати її під ліцензією GNU General Public License, опублікованою Фондом вільного програмного забезпечення; версією 2 цієї ліцензії або будь-якою пізнішою на Ваш вибір.\n\nЦя програма поширюється з надією на те, що вона буде корисною, однак '''без жодних гарантій'''; навіть без неявної гарантії '''комерційної цінності''' або '''придатності для певних цілей'''.\nДив. GNU General Public License для детальної інформації.\n\nВи повинні були отримати <doclink href=Copying>копію GNU General Public License</doclink> разом із цією програмою; якщо ж ні, зверніться до Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. або [https://www.gnu.org/copyleft/gpl.html ознайомтесь з нею онлайн].",
"config-sidebar": "* [https://www.mediawiki.org Сайт MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Посібник користувача]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Посібник адміністратора]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Read me</doclink>\n* <doclink href=ReleaseNotes>Інформація про випуск</doclink>\n* <doclink href=Copying>Ліцензія</doclink>\n* <doclink href=UpgradeDoc>Оновлення</doclink>",
"config-env-good": "Перевірку середовища успішно завершено.\nВи можете встановити MediaWiki.",
"config-env-bad": "Було проведено перевірку середовища. Ви не можете встановити MediaWiki.",
"config-env-php": "Встановлено версію PHP: $1.",
"config-env-hhvm": "HHVM $1 встановлено.",
- "config-unicode-using-intl": "Використовувати [http://pecl.php.net/intl міжнародне розширення PECL] для нормалізації Юнікоду.",
- "config-unicode-pure-php-warning": "'''Увага''': [http://pecl.php.net/intl міжнародне розширення PECL] не може провести нормалізацію Юнікоду.\nЯкщо ваш сайт має високий трафік, вам варто почитати про [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормалізацію Юнікоду].",
+ "config-unicode-using-intl": "Використовувати [https://pecl.php.net/intl міжнародне розширення PECL] для нормалізації Юнікоду.",
+ "config-unicode-pure-php-warning": "'''Увага''': [https://pecl.php.net/intl міжнародне розширення PECL] не може провести нормалізацію Юнікоду.\nЯкщо ваш сайт має високий трафік, вам варто почитати про [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормалізацію Юнікоду].",
"config-unicode-update-warning": "'''Увага''': Встановлена версія обгортки нормалізації Юнікоду використовує стару версію бібліотеки [http://site.icu-project.org/ проекту ICU].\nВи маєте [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations оновити версію], якщо плануєте повноцінно використовувати Юнікод.",
"config-no-db": "Не вдалося знайти потрібний драйвер бази даних! Вам необхідно встановити драйвер бази даних для PHP. Підтримуються {{PLURAL:$2|такий тип|такі типи}} баз даних: $1.\n\nЯкщо ви скомпілювали PHP самостійно, переналаштуйте його з увімкненим клієнтом бази даних, наприклад за допомогою <code>./configure --with-mysqli</code>.\n\nЯкщо установлено PHP з пакетів Debian або Ubuntu, тоді ви також повинні встановити, наприклад, пакунок <code>php5-mysql</code>.",
"config-outdated-sqlite": "'''Увага''': у Вас встановлена версія SQLite $1, а це нижче, ніж мінімально необхідна версія $2. SQLite буде недоступним.",
@@ -68,12 +69,11 @@
"config-pcre-no-utf8": "'''Помилка''': PCRE-модуть PHP, вочевидь, було зібрано без підтримки PCRE_UTF8.\nMediaWiki вимагає підтримку UTF-8 для коректної роботи.",
"config-memory-raised": "Обмеження пам'яті PHP (<code>memory_limit</code>) $1, піднято до $2.",
"config-memory-bad": "'''Увага:''' Розмір пам'яті PHP (<code>memory_limit</code>) становить $1.\nІмовірно, це замало.\nВстановлення може не вдатись!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] встановлено",
"config-apc": "[http://www.php.net/apc APC] встановлено",
"config-apcu": "[http://www.php.net/apcu APCu] встановлено",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] встановлено",
- "config-no-cache-apcu": "<strong>Увага:</strong> Не вдалося знайти [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] чи [http://www.iis.net/download/WinCacheForPhp WinCache].\nКешування об'єктів не ввімкнено.",
- "config-mod-security": "'''Увага''': на Вашому веб-сервері увімкнено [http://modsecurity.org/ mod_security]. У разі неправильних налаштувать, він може викликати проблеми MediaWiki або іншого ПЗ, яке дозволяє користувачам надсилати довільний вміст.\nЗверніться до [http://modsecurity.org/documentation/ документації mod_security] або підтримки Вашого хостера, якщо під час роботи виникають незрозумілі помилки.",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] встановлено",
+ "config-no-cache-apcu": "<strong>Увага:</strong> Не вдалося знайти [http://www.php.net/apcu APCu] чи [http://www.iis.net/download/WinCacheForPhp WinCache].\nКешування об'єктів не ввімкнено.",
+ "config-mod-security": "'''Увага''': на Вашому веб-сервері увімкнено [https://modsecurity.org/ mod_security]. У разі неправильних налаштувать, він може викликати проблеми MediaWiki або іншого ПЗ, яке дозволяє користувачам надсилати довільний вміст.\nЗверніться до [https://modsecurity.org/documentation/ документації mod_security] або підтримки Вашого хостера, якщо під час роботи виникають незрозумілі помилки.",
"config-diff3-bad": "GNU diff3 не знайдено.",
"config-git": "Знайшов програму управління версіями Git: <code>$1</code>.",
"config-git-bad": "Програму управління версіями Git не знайдено.",
@@ -228,7 +228,7 @@
"config-license-gfdl": "GNU Free Documentation License 1.3 або пізніша",
"config-license-pd": "Суспільне надбання (Public Domain)",
"config-license-cc-choose": "Виберіть одну з ліцензій Creative Commons",
- "config-license-help": "Чимало загальнодоступних вікі публікують увесь свій вміст під [http://freedomdefined.org/Definition вільною ліцензією]. Це розвиває відчуття спільної власності і заохочує довготривалу участь. У загальному випадку для приватної чи корпоративної вікі у цьому немає необхідності.\n\nЯкщо Ви хочете мати змогу використовувати текст з Вікіпедії і дати Вікіпедії змогу використовувати текст, скопійований з Вашої вікі, вам необхідно обрати <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nРаніше Вікіпедія використовувала GNU Free Documentation License.\nGFDL — допустима ліцензія, але у ній важко розібратися, а контент під GFDL важко використовувати повторно.",
+ "config-license-help": "Чимало загальнодоступних вікі публікують увесь свій вміст під [https://freedomdefined.org/Definition вільною ліцензією]. Це розвиває відчуття спільної власності і заохочує довготривалу участь. У загальному випадку для приватної чи корпоративної вікі у цьому немає необхідності.\n\nЯкщо Ви хочете мати змогу використовувати текст з Вікіпедії і дати Вікіпедії змогу використовувати текст, скопійований з Вашої вікі, вам необхідно обрати <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nРаніше Вікіпедія використовувала GNU Free Documentation License.\nGFDL — допустима ліцензія, але у ній важко розібратися, а контент під GFDL важко використовувати повторно.",
"config-email-settings": "Налаштування електронної пошти",
"config-enable-email": "Увімкнути вихідну електронну пошту",
"config-enable-email-help": "Якщо Ви хочете, що електронна пошта працювала, необхідно виставити коректні [http://www.php.net/manual/en/mail.configuration.php налаштування пошти у PHP].\nЯкщо Вам не потрібні жодні можливості електронної пошти у вікі, можете тут їх відключити.",
@@ -258,7 +258,7 @@
"config-cache-options": "Налаштування кешування об'єктів:",
"config-cache-help": "Кешування об'єктів використовується для покращення швидкодії MediaWiki методом кешування часто використовуваних даних.\nЗаохочується увімкнення цієї можливості для середніх і великих сайтів, малі сайти також можуть відчути її перевагу.",
"config-cache-none": "Без кешування (жодні функції не втрачаються, але впливає на швидкодію великих вікі-сайтів)",
- "config-cache-accel": "PHP кешування об'єктів (APC, APCu, XCache чи WinCache)",
+ "config-cache-accel": "PHP кешування об'єктів (APC, APCu чи WinCache)",
"config-cache-memcached": "Використовувати Memcached (вимагає додаткової установки і налаштування)",
"config-memcached-servers": "Сервери Memcached:",
"config-memcached-help": "Список IP-адрес, що викоритовує Memcached.\nВкажіть по одному в рядку, разом з портами. Наприклад:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -314,6 +314,7 @@
"config-install-mainpage-failed": "Не вдається вставити головну сторінку: $1",
"config-install-done": "<strong>Вітаємо!</strong>\nВи успішно встановили MediaWiki.\n\nІнсталятор згенерував файл <code>LocalSettings.php</code>, який містить усі Ваші налаштування.\n\nВам необхідно завантажити його і помістити у кореневу папку Вашої вікі (туди ж, де index.php). Завантаження мало початись автоматично.\n\nЯкщо завантаження не почалось або Ви його скасували, можете заново його почати, натиснувши на посилання внизу:\n\n$3\n\n<strong>Примітка</strong>: Якщо Ви не зробите цього зараз, цей файл не буде доступним пізніше, коли Ви вийдете з встановлення, не скачавши його.\n\nПісля виконання дій, описаних вище, Ви зможете <strong>[$2 увійти у свою вікі]</strong>.",
"config-install-done-path": "<strong>Вітаємо!</strong>\nВи встановили Медіавікі.\n\nІнсталятор створив файл <code>LocalSettings.php</code>.\nУ ньому містяться всі Ваші налаштування.\n\nВам потрібно завантажити його й помістити в <code>$4</code>. Завантаження повинно було автоматично розпочатись.\n\nЯкщо завантаження не було запропоновано, або Ви його скасували, Ви можете перезапустити завантаження натиснувши на посилання нижче:\n\n$3\n\n<strong>Зверніть увагу:</strong> Якщо Ви не зробите це зараз, цей згенерований файл налаштувань не буде доступним для Вас пізніше якщо Ви вийдете зі встановлення не завантаживши його.\n\nКоли це було зроблено Ви можете <strong>[$2 зайти до своєї вікі]</strong>.",
+ "config-install-success": "Mediawiki успішно встановлено. Зараз ви можете перейти до <$1$2>, щоб переглянути свою вікі. Якщо у вас є питання, ознайомтеся з нашим FAQ: <https://www.mediawiki.org/wiki/Manual:FAQ> або використовуйте один з форумів підтримки, які вказано на цій сторінці.",
"config-download-localsettings": "Завантажити <code>LocalSettings.php</code>",
"config-help": "допомога",
"config-help-tooltip": "натисніть, щоб розгорнути",
diff --git a/www/wiki/includes/installer/i18n/vi.json b/www/wiki/includes/installer/i18n/vi.json
index 1c441cd7..4b9da0e1 100644
--- a/www/wiki/includes/installer/i18n/vi.json
+++ b/www/wiki/includes/installer/i18n/vi.json
@@ -46,14 +46,14 @@
"config-help-restart": "Bạn có muốn xóa tất cả dữ liệu được lưu mà bạn vừa nhập và khởi động lại quá trình cài đặt?",
"config-restart": "Có, khởi động lại nó",
"config-welcome": "=== Kiểm tra môi trường ===\nBây giờ sẽ kiểm tra sơ qua môi trường này có phù hợp cho việc cài đặt MediaWiki.\nHãy nhớ bao gồm thông tin này khi nào xin hỗ trợ hoàn thành việc cài đặt.",
- "config-copyright": "=== Bản quyền và Điều khoản ===\n\n$1\n\nChương trình này là phần mềm tự do; bạn có thể phân phối lại và/hoặc chỉnh sửa nó dưới điều khoản của Giấy phép Công cộng GNU (GNU General Public License) do Quỹ Phần mềm Tự do (Free Software Foundation) xuất bản; hoặc phiên bản 2 của giấy phép đó, hoặc (tùy theo ý bạn) bất kỳ phiên bản nào sau này.\n\nChương trình này được phân phối với hy vọng rằng nó sẽ hữu ích, nhưng <strong>không có bất kỳ bảo hành nào</strong>; không có thậm chí bảo hành bao hàm về <strong>khả năng thương mại</strong> hoặc <strong>sự thích hợp với một mục đích cụ thể</strong>.\nXem Giấy phép Công cộng GNU để biết thêm chi tiết.\n\nBạn phải nhận <doclink href=Copying>một bản sao của Giấy phép Công cộng GNU</doclink> đi kèm chương trình này; nếu không, hãy viết thư cho Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, Hoa Kỳ, hoặc [http://www.gnu.org/copyleft/gpl.html đọc nó trên mạng].",
+ "config-copyright": "=== Bản quyền và Điều khoản ===\n\n$1\n\nChương trình này là phần mềm tự do; bạn có thể phân phối lại và/hoặc chỉnh sửa nó dưới điều khoản của Giấy phép Công cộng GNU (GNU General Public License) do Quỹ Phần mềm Tự do (Free Software Foundation) xuất bản; hoặc phiên bản 2 của giấy phép đó, hoặc (tùy theo ý bạn) bất kỳ phiên bản nào sau này.\n\nChương trình này được phân phối với hy vọng rằng nó sẽ hữu ích, nhưng <strong>không có bất kỳ bảo hành nào</strong>; không có thậm chí bảo hành bao hàm về <strong>khả năng thương mại</strong> hoặc <strong>sự thích hợp với một mục đích cụ thể</strong>.\nXem Giấy phép Công cộng GNU để biết thêm chi tiết.\n\nBạn phải nhận <doclink href=Copying>một bản sao của Giấy phép Công cộng GNU</doclink> đi kèm chương trình này; nếu không, hãy viết thư cho Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, Hoa Kỳ, hoặc [https://www.gnu.org/copyleft/gpl.html đọc nó trên mạng].",
"config-sidebar": "* [https://www.mediawiki.org/wiki/Special:MyLanguage/MediaWiki Trang chủ MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Hướng dẫn sử dụng]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Hướng dẫn quản lý]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Câu thường hỏi]\n----\n* <doclink href=Readme>Cần đọc trước</doclink>\n* <doclink href=ReleaseNotes>Ghi chú phát hành</doclink>\n* <doclink href=Copying>Sao chép</doclink>\n* <doclink href=UpgradeDoc>Nâng cấp</doclink>",
"config-env-good": "Đã kiểm tra môi trường.\nBạn có thể cài đặt MediaWiki.",
"config-env-bad": "Đã kiểm tra môi trường.\nBạn không thể cài đặt MediaWiki.",
"config-env-php": "PHP $1 đã được cài đặt.",
"config-env-hhvm": "HHVM $1 được cài đặt.",
- "config-unicode-using-intl": "Sẽ sử dụng [http://pecl.php.net/intl phần mở rộng PECL intl] để chuẩn hóa Unicode.",
- "config-unicode-pure-php-warning": "<strong>Cảnh báo:</strong> [http://pecl.php.net/intl intl PECL extension] không được phép xử lý Unicode chuẩn hóa, trả lại thực thi PHP-gốc chậm.\nNếu bạn chạy một site lưu lượng lớn, bạn phải để ý qua một chút trên [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
+ "config-unicode-using-intl": "Sẽ sử dụng [https://pecl.php.net/intl phần mở rộng PECL intl] để chuẩn hóa Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Cảnh báo:</strong> [https://pecl.php.net/intl intl PECL extension] không được phép xử lý Unicode chuẩn hóa, trả lại thực thi PHP-gốc chậm.\nNếu bạn chạy một site lưu lượng lớn, bạn phải để ý qua một chút trên [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
"config-unicode-update-warning": "<strong>Cảnh báo:</strong> Phiên bản cài đặt của gói Unicode chuẩn hóa sử dụng một phiên bản cũ của thư viện [http://site.icu-project.org/ the ICU project].\nBạn phải [https://www.mediawiki.org/wiki/Special:MyLanguage/nâng cấp Unicode_normalization_considerations] nếu bạn quan tâm đến việc sử dụng Unicode.",
"config-no-db": "Không tìm thấy một trình điều khiển cơ sở dữ liệu phù hợp! Bạn cần phải cài một trình điều khiển cơ sở dữ liệu cho PHP.\n{{PLURAL:$2|Loại|Các loại}} cơ sở dữ liệu sau đây được hỗ trợ: $1.\n\nNếu bạn đã biên dịch PHP lấy, cấu hình lại nó mà kích hoạt một trình khách cơ sở dữ liệu, ví dụ bằng lệnh <code>./configure --with-mysqli</code>.\nNếu bạn đã cài PHP từ một gói Debian hoặc Ubuntu, thì bạn cũng cần phải cài ví dụ gói <code>php5-mysql</code>.",
"config-outdated-sqlite": "<strong>Chú ý:</strong> Bạn có SQLite $1, phiên bản này thấp hơn phiên bản yêu câu tối thiểu $2. SQLite sẽ không có tác dụng.",
@@ -62,12 +62,11 @@
"config-pcre-no-utf8": "<strong>Lỗi chí tử:</strong> Mô đun PCRE của PHP dường như được biên dịch mà không có hỗ trợ PCRE_UTF8.\nMediaWiki yêu cầu phải có hỗ trợ UTF-8 để hoạt động chính xác.",
"config-memory-raised": "<code>memory_limit</code> của PHP là $1, tăng lên $2.",
"config-memory-bad": "<strong>Cảnh báo:</strong> <code>memory_limit</code> của PHP là $1.\nGiá trị này có lẽ quá thấp.\nCài đặt có thể bị thất bại!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] đã được cài đặt",
"config-apc": "[http://www.php.net/apc APC] đã được cài đặt",
"config-apcu": "[http://www.php.net/apcu APCu] đã được cài đặt",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] đã được cài đặt",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] đã được cài đặt",
"config-no-cache-apcu": "<strong>Cảnh báo:</strong> Không tìm thấy [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache], hoặc [http://www.iis.net/download/WinCacheForPhp WinCache].\nVùng nhớ đệm đối tượng không được kích hoạt.",
- "config-mod-security": "<strong>Cảnh báo:</strong> [http://modsecurity.org/ mod_security]/mod_security2 đã được kích hoạt trên máy chủ Web của bạn. Nhiều cấu hình phổ biến của phần mềm này sẽ gây vấn đề cho MediaWiki và những phần mềm khác cho phép người dùng đăng các nội dung tùy tiện.\nNếu có thể, bạn nên vô hiệu nó. Còn không, tra cứu [http://modsecurity.org/documentation/ tài liệu mod_security] hoặc liên hệ với nhà cung cấp hỗ trợ cho máy chủ nếu bạn gặp những lỗi ngẫu nhiên nào đó.",
+ "config-mod-security": "<strong>Cảnh báo:</strong> [https://modsecurity.org/ mod_security]/mod_security2 đã được kích hoạt trên máy chủ Web của bạn. Nhiều cấu hình phổ biến của phần mềm này sẽ gây vấn đề cho MediaWiki và những phần mềm khác cho phép người dùng đăng các nội dung tùy tiện.\nNếu có thể, bạn nên vô hiệu nó. Còn không, tra cứu [https://modsecurity.org/documentation/ tài liệu mod_security] hoặc liên hệ với nhà cung cấp hỗ trợ cho máy chủ nếu bạn gặp những lỗi ngẫu nhiên nào đó.",
"config-diff3-bad": "Không tìm thấy GNU diff3.",
"config-git": "Đã tìm thấy phần mềm điều khiển phiên bản Git: <code>$1</code>.",
"config-git-bad": "Không tìm thấy phần mềm điều khiển phiên bản Git.",
@@ -222,7 +221,7 @@
"config-license-gfdl": "Giấy pháp Tài liệu Tự do GNU 1.3 trở lên",
"config-license-pd": "Phạm vi công cộng",
"config-license-cc-choose": "Chọn một giấy phép Creative Commons tùy biến",
- "config-license-help": "Nhiều wiki công khai phát hành tất cả các đóng góp theo một [http://freedomdefined.org/Definition/Vi?uselang=vi giấy phép tự do].\nĐiều này giúp tạo nên thái độ cộng đồng sở hữu và ủng hộ sự đóng góp lâu dài.\nNói chung, một wiki riêng tư hoặc của công ty không nhất thiết phải sử dụng một giấy phép tự do.\n\nNếu bạn muốn được phép sử dụng văn bản từ Wikipedia và muốn Wikipedia nhận được những văn bản được sao chép từ wiki của bạn, bạn nên chọn <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia từng sử dụng Giấy phép Tài liệu Tự do GNU.\nGFDL là một giấy phép hợp lệ nhưng khó hiểu trên thực tế.\nNội dung được phát hành theo GFDL cũng khó tái sử dụng.",
+ "config-license-help": "Nhiều wiki công khai phát hành tất cả các đóng góp theo một [https://freedomdefined.org/Definition/Vi?uselang=vi giấy phép tự do].\nĐiều này giúp tạo nên thái độ cộng đồng sở hữu và ủng hộ sự đóng góp lâu dài.\nNói chung, một wiki riêng tư hoặc của công ty không nhất thiết phải sử dụng một giấy phép tự do.\n\nNếu bạn muốn được phép sử dụng văn bản từ Wikipedia và muốn Wikipedia nhận được những văn bản được sao chép từ wiki của bạn, bạn nên chọn <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia từng sử dụng Giấy phép Tài liệu Tự do GNU.\nGFDL là một giấy phép hợp lệ nhưng khó hiểu trên thực tế.\nNội dung được phát hành theo GFDL cũng khó tái sử dụng.",
"config-email-settings": "Thiết lập thư điện tử",
"config-enable-email": "Cho phép gửi thư điện tử đi",
"config-enable-email-help": "Nếu bạn muốn khả năng gửi thư điện tử, [http://www.php.net/manual/en/mail.configuration.php thiết lập mail của PHP] cần phải được cấu hình đúng.\nNếu bạn không muốn sử dụng bất kỳ tính năng thư điện tử nào, bạn có thể vô hiệu chúng ở đây.",
diff --git a/www/wiki/includes/installer/i18n/war.json b/www/wiki/includes/installer/i18n/war.json
index ef7694a7..75d2350f 100644
--- a/www/wiki/includes/installer/i18n/war.json
+++ b/www/wiki/includes/installer/i18n/war.json
@@ -44,8 +44,8 @@
"config-welcome": "=== Mga pagpanginano panlibong ===\nMagkakamay-ada yano nga panginano para masabtan kun ini nga libong in naaangay para hiton pagtataod hiton MediaWiki. Hinumdomi iton paglakip hinin nga impormasyon kun karuyag mo mangaro hin suporta kun paunan-on humanon an pagtataod.",
"config-env-php": "Gin-install an PHP $1.",
"config-env-hhvm": "Gin-install an HHVM $1.",
- "config-unicode-using-intl": "Gamita an [http://pecl.php.net/intl intl PECL extension] para han normalisasyon han Unicode.",
- "config-unicode-pure-php-warning": "<strong>Pahimatngon:</strong> An [http://pecl.php.net/intl intl PECL extension] in waray akos kumapot hin Unicode normalization, tungod hini mabalik ha mahinay nga puro-PHP nga implementasyon.\nKun nagpapadalagan ka hin high-traffic site, alayon pagbasa hin guti han [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
+ "config-unicode-using-intl": "Gamita an [https://pecl.php.net/intl intl PECL extension] para han normalisasyon han Unicode.",
+ "config-unicode-pure-php-warning": "<strong>Pahimatngon:</strong> An [https://pecl.php.net/intl intl PECL extension] in waray akos kumapot hin Unicode normalization, tungod hini mabalik ha mahinay nga puro-PHP nga implementasyon.\nKun nagpapadalagan ka hin high-traffic site, alayon pagbasa hin guti han [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
"config-no-db": "Diri nakakabiling hin naaangay nga database driver! Kinahanglan mo magtaod hin uska database driver para han PHP. An masunod nga mga klase hin database in ginsusuporatahan: $1.\n\nKun ikaw mismo an nag-compile han PHP, kinahanglan ma-reconfigure iton nga para maapandar an database client, pananglitan, han paggamit han <code>./configure --with-mysqli</code>.\nKun gintaod mo an PHP tikang ha uska Debian o Ubuntu nga pakete, kinahanglan nimo magtaod liwat, pananglitan, hiton an <code>php5-mysql</code> nga pakete.",
"config-pcre-old": "<strong>Nangangarat-an:</strong> Nagkikinahanglan hin PCRE $1 o mas urhi pa.\nAn imo PHP nga binaryo in nakasumpay hin PCRE $2. [https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE More information].",
"config-db-name": "Ngaran han database:",
diff --git a/www/wiki/includes/installer/i18n/yi.json b/www/wiki/includes/installer/i18n/yi.json
index e89113cb..5c5aff5a 100644
--- a/www/wiki/includes/installer/i18n/yi.json
+++ b/www/wiki/includes/installer/i18n/yi.json
@@ -39,9 +39,8 @@
"config-env-bad": "מ'האט קאנטראלירט די סביבה.\nאיר קענט נישט אינסטאלירן מעדיעוויקי.",
"config-env-php": "PHP $1 איז אינצטאלירט.",
"config-env-hhvm": "HHVM $1 איז אינסטאלירט.",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] איז אינסטאלירט",
"config-apc": "[http://www.php.net/apc APC] איז אינסטאלירט",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] איז אינסטאלירט",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] איז אינסטאלירט",
"config-diff3-bad": "GNU diff3 נישט געטראפן.",
"config-using-server": "באניצן סארווער־נאמען \"<nowiki>$1</nowiki>\".",
"config-using-uri": "באניצן סארווער־אדרעס \"<nowiki>$1$2</nowiki>\".",
@@ -66,6 +65,7 @@
"config-admin-email": "בליצפּאָסט אַדרעס:",
"config-install-tables": "שאפן טאבעלעס",
"config-install-tables-exist": "'''ווארענונג''': זעט אויס אז די מעדיעוויקי טאבעלעס עקזיסטירן שוין.\nאיבערהיפן שאפֿן.",
+ "config-install-stats": "סטאַטיסטיק ווערן איניציאַליזירט",
"config-install-mainpage-failed": "מ'האט נישט געקענט אריינגעבן הויפטבלאט: $1",
"config-download-localsettings": "אראפלאדן <code>LocalSettings.php</code>",
"config-help": "הילף",
diff --git a/www/wiki/includes/installer/i18n/zh-hans.json b/www/wiki/includes/installer/i18n/zh-hans.json
index fab5eef9..635fc347 100644
--- a/www/wiki/includes/installer/i18n/zh-hans.json
+++ b/www/wiki/includes/installer/i18n/zh-hans.json
@@ -62,14 +62,14 @@
"config-help-restart": "是否要清除所有已输入且保存的数据,并重新启动安装过程吗?",
"config-restart": "是的,重启吧",
"config-welcome": "=== 环境检查 ===\n将简单检查当前环境是否适合安装MediaWiki。如果您要寻求安装过程的支持,请记得附上此信息。",
- "config-copyright": "=== 版权和条款 ===\n\n$1\n\n本程序为自由软件;您可依据自由软件基金会所发表的GNU通用公共授权条款规定,就本程序再为发布与/或修改;无论您依据的是本授权的第二版或(您自行选择的)任一日后发行的版本。\n\n本程序是基于使用目的而加以发布,然而'''不负任何担保责任''';亦无对'''适售性'''或'''特定目的适用性'''所为的默示性担保。详情请参照GNU通用公共授权。\n\n您应已收到附随于本程序的<doclink href=\"Copying\">GNU通用公共授权的副本</doclink>;如果没有,请写信至自由软件基金会:59 Temple Place - Suite 330, Boston, Ma 02111-1307, USA,或[http://www.gnu.org/copyleft/gpl.html 在线阅读]。",
+ "config-copyright": "=== 版权和条款 ===\n\n$1\n\n本程序为自由软件;您可依据自由软件基金会所发表的GNU通用公共授权条款规定,就本程序再为发布与/或修改;无论您依据的是本授权的第二版或(您自行选择的)任一日后发行的版本。\n\n本程序是基于使用目的而加以发布,然而'''不负任何担保责任''';亦无对'''适售性'''或'''特定目的适用性'''所为的默示性担保。详情请参照GNU通用公共授权。\n\n您应已收到附随于本程序的<doclink href=\"Copying\">GNU通用公共授权的副本</doclink>;如果没有,请写信至自由软件基金会:59 Temple Place - Suite 330, Boston, Ma 02111-1307, USA,或[https://www.gnu.org/copyleft/gpl.html 在线阅读]。",
"config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/zh-hans MediaWiki首页]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/zh-hans 用户指南]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents 管理员指南]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/zh-hans 常见问题解答]\n----\n* <doclink href=Readme>自述文件</doclink>\n* <doclink href=ReleaseNotes>发行说明</doclink>\n* <doclink href=Copying>协议副本</doclink>\n* <doclink href=UpgradeDoc>升级</doclink>",
"config-env-good": "环境检查已经完成。您可以安装MediaWiki。",
"config-env-bad": "环境检查已经完成。您不能安装MediaWiki。",
"config-env-php": "PHP $1已安装。",
"config-env-hhvm": "HHVM $1已安装。",
- "config-unicode-using-intl": "使用[http://pecl.php.net/intl intl PECL扩展程序]标准化Unicode。",
- "config-unicode-pure-php-warning": "<strong>警告:</strong>因为尚未安装 [http://pecl.php.net/intl intl PECL 扩展]以处理 Unicode 正常化,故只能退而采用运行较慢的纯 PHP 实现的方法。如果您运行着一个高流量的网站,请参阅 [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode标准化]一文。",
+ "config-unicode-using-intl": "使用[https://pecl.php.net/intl intl PECL扩展程序]标准化Unicode。",
+ "config-unicode-pure-php-warning": "<strong>警告:</strong>因为尚未安装 [https://pecl.php.net/intl intl PECL 扩展]以处理 Unicode 正常化,故只能退而采用运行较慢的纯 PHP 实现的方法。如果您运行着一个高流量的网站,请参阅 [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode标准化]一文。",
"config-unicode-update-warning": "<strong>警告:</strong>Unicode正常化封装器的已安装版本使用了旧版本的[http://site.icu-project.org/ ICU项目]库。如果您需要使用Unicode,请将其[https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations 升级]。",
"config-no-db": "无法找到合适的数据库驱动!您需要为PHP安装数据库驱动。目前支持以下数据库{{PLURAL:$2|类型}}:$1。\n\n如果您自己编译了PHP,请通过启用数据库客户端重新配置它,例如使用 <code>./configure --with-mysqli</code>。如果您从 Debian 或 Ubuntu 安装包安装了PHP,那么您也需要安装,例如 <code>php5-mysql</code> 安装包。",
"config-outdated-sqlite": "<strong>警告:</strong>您已安装SQLite $1,但是它的版本低于最低要求版本$2。因此您无法选择SQLite。",
@@ -78,16 +78,15 @@
"config-pcre-no-utf8": "<strong>致命错误:</strong>PHP的PCRE模块在编译时可能没有包含PCRE_UTF8支持。\nMediaWiki需要UTF-8支持才能正常工作。",
"config-memory-raised": "PHP的内存使用上限<code>memory_limit</code>为$1,自动提升到$2。",
"config-memory-bad": "<strong>警告:</strong>PHP的内存使用上限<code>memory_limit</code>为$1。\n该设定可能过低,并导致安装失败!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache]已安装",
"config-apc": "[http://www.php.net/apc APC]已安装",
"config-apcu": "已安装[http://www.php.net/apcu APCu]",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache]已安装",
- "config-no-cache-apcu": "<strong>警告:</strong>找不到[http://www.php.net/apcu APCu]、[http://xcache.lighttpd.net/ XCache]或[http://www.iis.net/download/WinCacheForPhp WinCache]。\n对象缓存未启用。",
- "config-mod-security": "<strong>警告:</strong>您的web服务器已启用[http://modsecurity.org/ mod_security]/mod_security2。它的很多常见配置可能导致MediaWiki及其他软件允许用户发布任意内容的问题。如果可能,这应当被禁用。否则,当您遭遇随机错误时,请参考[http://modsecurity.org/documentation/ mod_security 文档]或联络您的主机支持。",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache]已安装",
+ "config-no-cache-apcu": "<strong>警告:</strong>找不到[http://www.php.net/apcu APCu]或[http://www.iis.net/download/WinCacheForPhp WinCache]。对象缓存未启用。",
+ "config-mod-security": "<strong>警告:</strong>您的web服务器已启用[https://modsecurity.org/ mod_security]/mod_security2。它的很多常见配置可能导致MediaWiki及其他软件允许用户发布任意内容的问题。如果可能,这应当被禁用。否则,当您遭遇随机错误时,请参考[https://modsecurity.org/documentation/ mod_security 文档]或联络您的主机支持。",
"config-diff3-bad": "找不到GNU diff3。",
"config-git": "发现Git版本控制软件:<code>$1</code>",
"config-git-bad": "Git版本控制软件未找到。",
- "config-imagemagick": "已找到ImageMagick:<code>$1</code>。如果你启用了上传功能,缩略图功能也将被启用。",
+ "config-imagemagick": "已找到ImageMagick:<code>$1</code>。如果您启用了上传功能,缩略图功能也将被启用。",
"config-gd": "已找到内建的GD图形库。如果你启用了上传功能,缩略图功能也将被启用。",
"config-no-scaling": "找不到GD库或ImageMagick。缩略图功能将不可用。",
"config-no-uri": "<strong>错误:</strong>无法确定当前的URI。\n安装已中断。",
@@ -238,7 +237,7 @@
"config-license-gfdl": "GNU自由文档许可证1.3或更高版本",
"config-license-pd": "公有领域",
"config-license-cc-choose": "选择自定义的知识共享许可证",
- "config-license-help": "许多公共wiki将所有用户贡献置于[http://freedomdefined.org/Definition 自由许可证]之下。这有助于构建社区的主人翁意识,并鼓励长期贡献。对于非公共wiki或公司wiki,这并非必要条件。\n\n如果您希望使用来自维基百科的内容,并希望维基百科能接受复制自您的wiki的内容,您应当选择<strong>{{int:config-license-cc-by-sa}}</strong>。\n\nGNU自由文档许可证是维基百科曾经使用过的许可证,并迄今有效。然而,该许可证难以理解,并会增加重用内容的难度。",
+ "config-license-help": "许多公共wiki将所有用户贡献置于[https://freedomdefined.org/Definition 自由许可证]之下。这有助于构建社区的主人翁意识,并鼓励长期贡献。对于非公共wiki或公司wiki,这并非必要条件。\n\n如果您希望使用来自维基百科的内容,并希望维基百科能接受复制自您的wiki的内容,您应当选择<strong>{{int:config-license-cc-by-sa}}</strong>。\n\nGNU自由文档许可证是维基百科曾经使用过的许可证,并迄今有效。然而,该许可证难以理解,并会增加重用内容的难度。",
"config-email-settings": "电子邮件设置",
"config-enable-email": "启用出站电子邮件",
"config-enable-email-help": "如果您希望使用电子邮件功能,请正确配置[http://www.php.net/manual/en/mail.configuration.php PHP的邮件设定]。如果您不需要任何电子邮件功能,请在此处禁用它。",
@@ -268,7 +267,7 @@
"config-cache-options": "对象缓存设置:",
"config-cache-help": "对象缓存可通过缓存频繁使用的数据来提高MediaWiki的速度。高度推荐中到大型的网站启用该功能,小型网站亦能从其中受益。",
"config-cache-none": "无缓存(不影响功能,但对较大型的wiki网站会有速度影响)",
- "config-cache-accel": "PHP对象缓存(APC、APCu、XCache或WinCache)",
+ "config-cache-accel": "PHP对象缓存(APC、APCu或WinCache)",
"config-cache-memcached": "使用Memcached(需要另外安装并配置)",
"config-memcached-servers": "Memcached服务器:",
"config-memcached-help": "用于Memcached的IP地址列表。请保持每行一条,并指定要使用的端口。例如:\n127.0.0.1:11211\n192.168.1.25:1234",
@@ -324,6 +323,7 @@
"config-install-mainpage-failed": "无法插入首页:$1",
"config-install-done": "<strong>恭喜!</strong>\n您已经安装了MediaWiki。\n\n安装程序已经生成了<code>LocalSettings.php</code>文件,其中包含了您所有的配置。\n\n您需要下载该文件,并将其放在您wiki的根目录(index.php的同级目录)中。稍后下载将自动开始。\n\n如果浏览器没有提示您下载,或者您取消了下载,您可以点击下面的链接重新开始下载:\n\n$3\n\n<strong>注意:</strong>如果您现在不完成本步骤,而是没有下载便退出了安装过程,此后您将无法获得自动生成的配置文件。\n\n当本步骤完成后,您可以<strong>[$2 进入您的wiki]</strong>。",
"config-install-done-path": "<strong>祝贺!</strong>您已经安装了MediaWiki。\n\n安装程序已经生成了<code>LocalSettings.php</code>文件。它包含您所有的配置。\n\n您需要下载该文件,并将其放在<code>$4</code>。下载应已自动开始。\n\n如果没有提供下载,或者您取消了下载,您可以点击下面的链接重新开始下载:\n\n$3\n\n<strong>注意:</strong>如果您现在不完成本步骤,而是没有下载便退出了安装过程,此后您将无法获得自动生成的配置文件。\n\n当本步骤完成后,您可以<strong>[$2 进入您的wiki]</strong>。",
+ "config-install-success": "MediaWiki已成功安装。您现在可以访问<$1$2>以查看您的wiki。如果您有问题,请阅览我们的常见问题列表:<https://www.mediawiki.org/wiki/Manual:FAQ/zh>或使用在该页面上链接的支持论坛之一。",
"config-download-localsettings": "下载<code>LocalSettings.php</code>",
"config-help": "帮助",
"config-help-tooltip": "单击展开",
diff --git a/www/wiki/includes/installer/i18n/zh-hant.json b/www/wiki/includes/installer/i18n/zh-hant.json
index ab365ec2..acc83026 100644
--- a/www/wiki/includes/installer/i18n/zh-hant.json
+++ b/www/wiki/includes/installer/i18n/zh-hant.json
@@ -19,7 +19,9 @@
"Suchichi02",
"Winstonyin",
"Wehwei",
- "Wwycheuk"
+ "Wwycheuk",
+ "蘭斯特",
+ "Kly"
]
},
"config-desc": "MediaWiki 安裝程式",
@@ -37,7 +39,7 @@
"config-no-session": "您的連線階段資料遺失!\n請檢查 php.ini 設定檔並確認 <code>session.save_path</code> 所設定的目錄是否合適。",
"config-your-language": "您的語言:",
"config-your-language-help": "請選擇接下來安裝程序中要使用的語言。",
- "config-wiki-language": "Wiki 語言:",
+ "config-wiki-language": "wiki 語言:",
"config-wiki-language-help": "選擇將要安裝的 Wiki 多數情況主要使用的語言。",
"config-back": "← 返回",
"config-continue": "繼續 →",
@@ -55,18 +57,18 @@
"config-page-releasenotes": "發佈說明",
"config-page-copying": "複製",
"config-page-upgradedoc": "升級",
- "config-page-existingwiki": "現有 Wiki",
+ "config-page-existingwiki": "現有的 wiki",
"config-help-restart": "是否要清除所有已輸入且儲存的資料,並重新開始安裝程序嗎?",
"config-restart": "是的,重新開始",
"config-welcome": "=== 環境檢查 ===\n現在會做基本的檢查,檢查環境是否符合 MediaWiki 安裝所需。\n若您要尋求如何完成安裝的協助,請記得提供以下訊息。",
- "config-copyright": "=== 版權聲明與授權條款 ===\n\n$1\n\n本程式為自由軟體;您可依據自由軟體基金會所發表的 GNU 通用公共授權條款規定,將本程式重新發佈與/或修改;無論您依據的是本授權條款的第二版或 (您可自行選擇) 之後的任何版本。\n\n本程式發佈的目的是希望可以提供幫助,但 <strong>不負任何擔保責任</strong>;亦無隱含對 <strong>適售性</strong> 或 <strong>特定用途的適用性</strong> 的情形擔保。詳情請參照 GNU 通用公共授權。\n\n您應已隨本程式收到 <doclink href=\"Copying\">GNU 通用公共授權條款的副本</doclink>;如果沒有,請信件通知自由軟體基金會,51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA,或 [http://www.gnu.org/copyleft/gpl.html 線上閱讀]。",
+ "config-copyright": "=== 版權聲明與授權條款 ===\n\n$1\n\n本程式為自由軟體;您可依據自由軟體基金會所發表的 GNU 通用公共授權條款規定,將本程式重新發佈與/或修改;無論您依據的是本授權條款的第二版或 (您可自行選擇) 之後的任何版本。\n\n本程式發佈的目的是希望可以提供幫助,但 <strong>不負任何擔保責任</strong>;亦無隱含對 <strong>適售性</strong> 或 <strong>特定用途的適用性</strong> 的情形擔保。詳情請參照 GNU 通用公共授權。\n\n您應已隨本程式收到 <doclink href=\"Copying\">GNU 通用公共授權條款的副本</doclink>;如果沒有,請信件通知自由軟體基金會,51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA,或 [https://www.gnu.org/copyleft/gpl.html 線上閱讀]。",
"config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/zh-hant MediaWiki 首頁]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/zh 使用者指南]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/zh 管理員指南]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/zh 常見問題集]\n----\n* <doclink href=Readme>讀我說明</doclink>\n* <doclink href=ReleaseNotes>發行說明</doclink>\n* <doclink href=Copying>版權聲明</doclink>\n* <doclink href=UpgradeDoc>升級</doclink>",
"config-env-good": "環境檢查已完成。\n您可以安裝 MediaWiki。",
"config-env-bad": "環境檢查已完成。\n您無法安裝 MediaWiki。",
"config-env-php": "PHP $1 已安裝。",
"config-env-hhvm": "HHVM $1 已安裝。",
- "config-unicode-using-intl": "使用 [http://pecl.php.net/intl intl PECL 擴充套件] 做 Unicode 正規化。",
- "config-unicode-pure-php-warning": "<strong>警告:</strong> 無法使用 [http://pecl.php.net/intl intl PECL 擴充套件] 處理 Unicode 正規化,故回退使用純 PHP 實作的正規化程式,此方式處理速度較緩慢。\n\n如果您的網站瀏覽人次很高,您應先閱讀 [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations/zh Unicode 正規化]。",
+ "config-unicode-using-intl": "使用 [https://pecl.php.net/intl intl PECL 擴充套件] 做 Unicode 正規化。",
+ "config-unicode-pure-php-warning": "<strong>警告:</strong> 無法使用 [https://pecl.php.net/intl intl PECL 擴充套件] 處理 Unicode 正規化,故回退使用純 PHP 實作的正規化程式,此方式處理速度較緩慢。\n\n如果您的網站瀏覽人次很高,您應先閱讀 [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations/zh Unicode 正規化]。",
"config-unicode-update-warning": "<strong>警告</strong>:目前安裝的 Unicode 正規化包裝程式使用了舊版 [http://site.icu-project.org/ ICU 計劃] 的程式庫。\n若您需要使用 Unicode,您應先進行 [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations 升級]。",
"config-no-db": "找不到合適的資料庫驅動程式!您需要安裝 PHP 資料庫驅動程式。\n目前支援以下{{PLURAL:$2|類型|類型}}的資料庫: $1 。\n\n如果您是自行編譯 PHP,您必須重新設定並開啟資料庫客戶端,例:使用 <code>./configure --with-mysqli</code> 指令參數。\n如果您是使用 Debian 或 Ubuntu 的套件安裝 PHP ,您則需要額外安裝,例:<code>php5-mysql</code> 套件。",
"config-outdated-sqlite": "<strong>警告:</strong>您已安裝 SQLite $1,但是它的版本低於最低需求版本 $2。 因此您無法使用 SQLite。",
@@ -75,12 +77,11 @@
"config-pcre-no-utf8": "<strong>嚴重:</strong> PHP 的 PCRE 模組在編譯時未包含 PCRE_UTF8 支援。\nMediaWiki 需要支援 UTF-8 才可正常運作。",
"config-memory-raised": "PHP 的記憶體使用上限 <code>memory_limit</code> 目前為 $1,自動提高到 $2。",
"config-memory-bad": "<strong>警告:</strong>PHP 的記憶體使用上限 <code>memory_limit</code> 為 $1。\n該設定值可能過低。\n這可能導致後續的安裝失敗!",
- "config-xcache": "[http://xcache.lighttpd.net/ XCache] 已安裝",
"config-apc": "[http://www.php.net/apc APC] 已安裝",
"config-apcu": "已安裝[http://www.php.net/apcu APCu]",
- "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] 已安裝",
- "config-no-cache-apcu": "<strong>警告:</strong>找不到[http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache]或[http://www.iis.net/download/WinCacheForPhp WinCache]。未開啟物件緩存。",
- "config-mod-security": "<strong>警告:</strong>您的網頁伺服器已開啟 [http://modsecurity.org/ mod_security] 模組,如果設定不恰當會導致使用者可在 MediaWiki 或其他應用程式發佈任意的內容。\n若您遇到任何問題,請參考 [http://modsecurity.org/documentation/ mod_security 文件] 或聯繫您的伺服器技術支援人員。",
+ "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] 已安裝",
+ "config-no-cache-apcu": "<strong>警告:</strong>找不到[http://www.php.net/apcu APCu]或[http://www.iis.net/download/WinCacheForPhp WinCache]。未開啟物件快取。",
+ "config-mod-security": "<strong>警告:</strong>您的網頁伺服器已開啟 [https://modsecurity.org/ mod_security] 模組,如果設定不恰當會導致使用者可在 MediaWiki 或其他應用程式發佈任意的內容。\n若您遇到任何問題,請參考 [https://modsecurity.org/documentation/ mod_security 文件] 或聯繫您的伺服器技術支援人員。",
"config-diff3-bad": "找不到 GNU diff3。",
"config-git": "找到 Git 版本控制軟體:<code>$1</code>。",
"config-git-bad": "查無 Git 版本控制軟體。",
@@ -95,12 +96,13 @@
"config-no-cli-uploads-check": "<strong>警告:</strong>透過指令介面安不會檢查您預設的上傳目錄 (<code>$1</code>) 是否有可任意執行 Script 的安全性漏洞。",
"config-brokenlibxml": "您的系統使用了可能造成 MediaWiki 或其他網頁應用程式資料損毀問題的 PHP 與 limbxml2 版本。\n請升級 libxml2 2.7.3 或更新的版本 ([https://bugs.php.net/bug.php?id=45996 PHP 問題報告])。\n安裝已中止。",
"config-suhosin-max-value-length": "Suhosin 已安裝並且限制 GET 參數的長度 <code>length</code> 為 $1 位元組。\nMediaWiki 的 ResourceLoader 元件可以在此限制下正常運作,但仍會降低執行的效能。\n如果可能的情況下,您應該設定 <code>php.ini</code> 設定檔中的項目 <code>suhosin.get.max_value_length</code> 為 1024 或者更高的數值,並且將\n<code>LocalSettings.php</code> 中的設定項目 <code>$wgResourceLoaderMaxQueryLength</code> 設為相同的數值。",
+ "config-using-32bit": "<strong>警告:</strong>您的系統似乎是 32 位元系統,[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit 不推薦]您使用。",
"config-db-type": "資料庫類型:",
"config-db-host": "資料庫主機:",
"config-db-host-help": "如果您的資料庫安裝在其他伺服器上,請在此輸入該主機的名稱或 IP 位址。\n\n如果您使用共用的網頁主機,您的主機提供商應會在說明文件上告訴您正確的主機名稱。\n\n如果您安裝在 Windows 伺服器並且使用 MySQL,伺服器名稱可能無法使用使用 \"localhost\"。若確實無法使用,請改嘗試使用本機的 IP 位址 \"127.0.0.1\"。\n\n如果您使用 PostgreSQL,將此欄位空白以使用 Unix socket 來連線。",
"config-db-host-oracle": "資料庫的 TNS:",
"config-db-host-oracle-help": "請輸入有效的 [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm 本地連線名稱],並確認安裝程式可以讀取 tnsnames.ora 檔案。<br />如果您使用的客戶端程式庫為 10g 或者更新的版本,您也可使用 [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm 簡易連線] 的命名方法進行連線。",
- "config-db-wiki-settings": "此 Wiki 的 ID",
+ "config-db-wiki-settings": "識別此 wiki",
"config-db-name": "資料庫名稱:",
"config-db-name-help": "請輸入一個可以辨識您的 Wiki 的名稱,\n請勿包含空格。\n\n如果您使用的是共用的網頁主機,您的主機提供商會給您一個指定的資料庫名稱,或者讓您透過管理介面建立資料庫。",
"config-db-name-oracle": "資料庫 Schema:",
@@ -186,12 +188,12 @@
"config-mssql-web-auth": "請選擇一般操作中要用來連線資料庫使用的身份驗證類型。\n若您選擇 \"{{int:config-mssql-windowsauth}}\",不論網頁伺服器是使用何種身份執行都會使用這組驗證資料。",
"config-mssql-sqlauth": "SQL Server 身份驗證",
"config-mssql-windowsauth": "Windows 身份驗證",
- "config-site-name": "Wiki 的名稱:",
+ "config-site-name": "wiki 的名稱:",
"config-site-name-help": "您所填入的內容會出現在瀏覽器的標題列以及各種其他地方。",
"config-site-name-blank": "請輸入網站名稱。",
"config-project-namespace": "專案命名空間:",
"config-ns-generic": "專案",
- "config-ns-site-name": "同 Wiki 名稱:$1",
+ "config-ns-site-name": "與 wiki 名稱一致:$1",
"config-ns-other": "其他 (請註明)",
"config-ns-other-default": "我的 wiki",
"config-project-namespace-help": "許多 Wiki 以維基百科 (Wikipedia) 做為範例將政策頁面從內容頁面抽離,放置在 \"'''專案命名空間'''\" 中。\n所有在此命名空間裡的頁面都會有特定的字首,您可以在此處設定。\n通常這些字首是由該 Wiki 的名稱所衍伸出來,但無法使用標點符號,如 \"#\" 或 \":\"。",
@@ -220,7 +222,7 @@
"config-optional-continue": "多問我一些問題吧。",
"config-optional-skip": "我已經不耐煩了,請趕緊安裝 Wiki。",
"config-profile": "使用者權限基本資料:",
- "config-profile-wiki": "開放式 Wiki",
+ "config-profile-wiki": "開放式 wiki",
"config-profile-no-anon": "需要註冊帳號",
"config-profile-fishbowl": "僅授權的編輯者",
"config-profile-private": "私人 wiki",
@@ -234,7 +236,7 @@
"config-license-gfdl": "GNU 自由文件授權條款 1.3 或更高版本",
"config-license-pd": "公有領域",
"config-license-cc-choose": "請選擇一個自訂的創作共用授權條款",
- "config-license-help": "許多開放式 Wiki 會以 [http://freedomdefined.org/Definition 自由授權條款] 的方式釋放出編者的所有貢獻,這有助於構建社群的所有權,並且能鼓勵長期貢獻。對於封閉式的 Wiki 或公司 Wiki 則是非必要的。\n\n如果您希望使用來自維基百科(Wikipedia)的內容,並希望維基百科能接受您的 Wiki 內容,請應選擇 <strong>{{int:config-license-cc-by-sa}}</strong> 授權條款。\n\n維基百科(Wikipedia)先前是使用 GNU 自由文件授權條款,\n但該授權條款的內容較難理解,因此較難再利用在該條款底下的內容。",
+ "config-license-help": "許多開放式 Wiki 會以 [https://freedomdefined.org/Definition 自由授權條款] 的方式釋放出編者的所有貢獻,這有助於構建社群的所有權,並且能鼓勵長期貢獻。對於封閉式的 Wiki 或公司 Wiki 則是非必要的。\n\n如果您希望使用來自維基百科(Wikipedia)的內容,並希望維基百科能接受您的 Wiki 內容,請應選擇 <strong>{{int:config-license-cc-by-sa}}</strong> 授權條款。\n\n維基百科(Wikipedia)先前是使用 GNU 自由文件授權條款,\n但該授權條款的內容較難理解,因此較難再利用在該條款底下的內容。",
"config-email-settings": "E-mail 設定",
"config-enable-email": "開啟外寄電子郵件",
"config-enable-email-help": "如果您要使用電子郵件功能,請正確設定 [http://www.php.net/manual/en/mail.configuration.php PHP 的郵件設定]。\n如果您不需要使用電子郵件功能,請在此處關閉。",
@@ -264,7 +266,7 @@
"config-cache-options": "物件快取設定:",
"config-cache-help": "物件快取是用來增進 MediaWiki 速度的一項功能,透過快取經常使用的資料。\n中型到大型的網站我們會建議開啟這個選項,對小型的網站也有一定程度的效果。",
"config-cache-none": "不快取 (不會影響功能,但在大型 Wiki 網站可能會有處理速度的問題)",
- "config-cache-accel": "使用 PHP 物件快取 (APC、APCu、XCache 或 WinCache)",
+ "config-cache-accel": "使用 PHP 物件快取 (APC、APCu、或是 WinCache)",
"config-cache-memcached": "使用 Memcached (需要額外安裝與設定)",
"config-memcached-servers": "Memcached 伺服器:",
"config-memcached-help": "請列出 Memcached 伺服器的 IP 位址。\n每一行只指定一個位置並且要註明使用的埠號,例如:\n 127.0.0.1:11211\n 192.168.1.25:1234",
@@ -320,11 +322,14 @@
"config-install-mainpage-failed": "無法插入首頁: $1",
"config-install-done": "<strong>恭喜!</strong>\n您已經成功安裝MediaWiki。\n\n安裝程式已自動產生<code>LocalSettings.php</code>檔案,\n該檔案中包含了您所有的設定項目。\n\n您需要下載該檔案,並將其放置在您的Wiki的根目錄(index.php所在的目錄)中,下載應已自動開始。\n\n若瀏覽器沒有提示您下載,或者您取消了下載,您可以點選下方連結重新下載:\n\n$3\n\n<strong>注意:</strong>如果您現在不下載此檔案,稍後結束安裝程式之後將無法再下載設定檔。\n\n當您完成本步驟後,您可以<strong>[$2 進入您的Wiki]</strong>。",
"config-install-done-path": "<strong>恭喜!</strong>\n您已經成功安裝MediaWiki。\n\n安裝程式已自動產生<code>LocalSettings.php</code>檔案,\n該檔案中包含了您所有的設定項目。\n\n您需要下載該檔案,並將其放置在<code>$4</code>中,下載應已自動開始。\n\n若瀏覽器沒有提示您下載,或者您取消了下載,您可以點選下方連結重新下載:\n\n$3\n\n<strong>注意:</strong>如果您現在不下載此檔案,稍後結束安裝程式之後將無法再下載設定檔。\n\n當您完成本步驟後,您可以<strong>[$2 進入您的Wiki]</strong>。",
+ "config-install-success": "MediaWiki 已安裝成功。您現在可以在 <$1$2> 上檢視您的 wiki。若您有任何問題,請閱讀常見問題清單:<https://www.mediawiki.org/wiki/Manual:FAQ/zh>,或是利用在頁面上所連結的支援論壇之一。",
"config-download-localsettings": "下載 <code>LocalSettings.php</code>",
"config-help": "說明",
"config-help-tooltip": "點選以展開",
"config-nofile": "查無檔案 \"$1\",是否已被刪除?",
"config-extension-link": "您是否了解您的 Wiki 支援 [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions 擴充套件]?\n\n\n您可以瀏覽 [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category 擴充套件分類] 或 [https://www.mediawiki.org/wiki/Extension_Matrix 擴充套件資料表] 以取得相關的資訊。",
+ "config-skins-screenshots": "$1 (螢幕截圖: $2)",
+ "config-screenshot": "螢幕截圖",
"mainpagetext": "<strong>已安裝 MediaWiki。</strong>",
"mainpagedocfooter": "有關使用wiki的訊息,請參閱[https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents 使用者指南]。\n\n== 新手入門 ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings 系統設定]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki常見問題]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki郵寄清單]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources 將MediaWiki翻譯至您的語言]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam 了解如何在您的wiki上防禦破壞]"
}
diff --git a/www/wiki/includes/interwiki/ClassicInterwikiLookup.php b/www/wiki/includes/interwiki/ClassicInterwikiLookup.php
index d9c04240..d5103da9 100644
--- a/www/wiki/includes/interwiki/ClassicInterwikiLookup.php
+++ b/www/wiki/includes/interwiki/ClassicInterwikiLookup.php
@@ -1,6 +1,4 @@
<?php
-namespace MediaWiki\Interwiki;
-
/**
* InterwikiLookup implementing the "classic" interwiki storage (hardcoded up to MW 1.26).
*
@@ -21,14 +19,17 @@ namespace MediaWiki\Interwiki;
*
* @file
*/
-use \Cdb\Exception as CdbException;
-use \Cdb\Reader as CdbReader;
-use Wikimedia\Rdbms\Database;
+
+namespace MediaWiki\Interwiki;
+
+use Cdb\Exception as CdbException;
+use Cdb\Reader as CdbReader;
use Hooks;
use Interwiki;
use Language;
use MapCacheLRU;
use WANObjectCache;
+use Wikimedia\Rdbms\Database;
/**
* InterwikiLookup implementing the "classic" interwiki storage (hardcoded up to MW 1.26).
diff --git a/www/wiki/includes/interwiki/Interwiki.php b/www/wiki/includes/interwiki/Interwiki.php
index 21568205..5a996d9d 100644
--- a/www/wiki/includes/interwiki/Interwiki.php
+++ b/www/wiki/includes/interwiki/Interwiki.php
@@ -32,7 +32,7 @@ class Interwiki {
/** @var string The URL of the wiki, with "$1" as a placeholder for an article name. */
protected $mURL;
- /** @var string The URL of the file api.php */
+ /** @var string The URL of the file api.php */
protected $mAPI;
/** @var string The name of the database (for a connection to be established
diff --git a/www/wiki/includes/interwiki/NullInterwikiLookup.php b/www/wiki/includes/interwiki/NullInterwikiLookup.php
new file mode 100644
index 00000000..09fa1ecd
--- /dev/null
+++ b/www/wiki/includes/interwiki/NullInterwikiLookup.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+namespace MediaWiki\Interwiki;
+
+/**
+ * An interwiki lookup that has no data, intended
+ * for use in the installer.
+ *
+ * @since 1.31
+ */
+class NullInterwikiLookup implements InterwikiLookup {
+
+ /**
+ * @inheritDoc
+ */
+ public function isValidInterwiki( $prefix ) {
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function fetch( $prefix ) {
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getAllPrefixes( $local = null ) {
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function invalidateCache( $prefix ) {
+ }
+}
diff --git a/www/wiki/includes/jobqueue/Job.php b/www/wiki/includes/jobqueue/Job.php
index 703e4856..f9c416f3 100644
--- a/www/wiki/includes/jobqueue/Job.php
+++ b/www/wiki/includes/jobqueue/Job.php
@@ -50,6 +50,12 @@ abstract class Job implements IJobSpecification {
/** @var callable[] */
protected $teardownCallbacks = [];
+ /** @var int Bitfield of JOB_* class constants */
+ protected $executionFlags = 0;
+
+ /** @var int Job must not be wrapped in the usual explicit LBFactory transaction round */
+ const JOB_NO_EXPLICIT_TRX_ROUND = 1;
+
/**
* Run the job
* @return bool Success
@@ -109,6 +115,15 @@ abstract class Job implements IJobSpecification {
}
/**
+ * @param int $flag JOB_* class constant
+ * @return bool
+ * @since 1.31
+ */
+ public function hasExecutionFlag( $flag ) {
+ return ( $this->executionFlags && $flag ) === $flag;
+ }
+
+ /**
* Batch-insert a group of jobs into the queue.
* This will be wrapped in a transaction with a forced commit.
*
@@ -337,6 +352,7 @@ abstract class Job implements IJobSpecification {
* @deprecated since 1.21
*/
public function insert() {
+ wfDeprecated( __METHOD__, '1.21' );
JobQueueGroup::singleton()->push( $this );
return true;
}
diff --git a/www/wiki/includes/jobqueue/JobQueue.php b/www/wiki/includes/jobqueue/JobQueue.php
index 1f4f179a..3a8bf1ab 100644
--- a/www/wiki/includes/jobqueue/JobQueue.php
+++ b/www/wiki/includes/jobqueue/JobQueue.php
@@ -362,7 +362,7 @@ abstract class JobQueue {
global $wgJobClasses;
$this->assertNotReadOnly();
- if ( $this->wiki !== wfWikiID() ) {
+ if ( !WikiMap::isCurrentWikiDbDomain( $this->wiki ) ) {
throw new MWException( "Cannot pop '{$this->type}' job off foreign wiki queue." );
} elseif ( !isset( $wgJobClasses[$this->type] ) ) {
// Do not pop jobs if there is no class for the queue type
diff --git a/www/wiki/includes/jobqueue/JobQueueDB.php b/www/wiki/includes/jobqueue/JobQueueDB.php
index b68fdaef..f01ba63f 100644
--- a/www/wiki/includes/jobqueue/JobQueueDB.php
+++ b/www/wiki/includes/jobqueue/JobQueueDB.php
@@ -190,7 +190,7 @@ class JobQueueDB extends JobQueue {
// If the connection is busy with a transaction, then defer the job writes
// until right before the main round commit step. Any errors that bubble
// up will rollback the main commit round.
- // b) mysql/postgres; DB connection is generally a separate CONN_TRX_AUTO handle.
+ // b) mysql/postgres; DB connection is generally a separate CONN_TRX_AUTOCOMMIT handle.
// No transaction is active nor will be started by writes, so enqueue the jobs
// now so that any errors will show up immediately as the interface expects. Any
// errors that bubble up will rollback the main commit round.
@@ -780,7 +780,7 @@ class JobQueueDB extends JobQueue {
return ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' )
// Keep a separate connection to avoid contention and deadlocks;
// However, SQLite has the opposite behavior due to DB-level locking.
- ? $lb->getConnectionRef( $index, [], $this->wiki, $lb::CONN_TRX_AUTO )
+ ? $lb->getConnectionRef( $index, [], $this->wiki, $lb::CONN_TRX_AUTOCOMMIT )
// Jobs insertion will be defered until the PRESEND stage to reduce contention.
: $lb->getConnectionRef( $index, [], $this->wiki );
}
diff --git a/www/wiki/includes/jobqueue/JobQueueFederated.php b/www/wiki/includes/jobqueue/JobQueueFederated.php
index e7433111..7f3b2b1d 100644
--- a/www/wiki/includes/jobqueue/JobQueueFederated.php
+++ b/www/wiki/includes/jobqueue/JobQueueFederated.php
@@ -184,11 +184,10 @@ class JobQueueFederated extends JobQueue {
// Try to insert the jobs and update $partitionsTry on any failures.
// Retry to insert any remaning jobs again, ignoring the bad partitions.
$jobsLeft = $jobs;
- // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
+ // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
for ( $i = $this->maxPartitionsTry; $i > 0 && count( $jobsLeft ); --$i ) {
- // @codingStandardsIgnoreEnd
try {
- $partitionRing->getLiveRing();
+ $partitionRing->getLiveLocationWeights();
} catch ( UnexpectedValueException $e ) {
break; // all servers down; nothing to insert to
}
diff --git a/www/wiki/includes/jobqueue/JobQueueGroup.php b/www/wiki/includes/jobqueue/JobQueueGroup.php
index addc7fc2..df55fc57 100644
--- a/www/wiki/includes/jobqueue/JobQueueGroup.php
+++ b/www/wiki/includes/jobqueue/JobQueueGroup.php
@@ -33,8 +33,8 @@ class JobQueueGroup {
/** @var ProcessCacheLRU */
protected $cache;
- /** @var string Wiki ID */
- protected $wiki;
+ /** @var string Wiki DB domain ID */
+ protected $domain;
/** @var string|bool Read only rationale (or false if r/w) */
protected $readOnlyReason;
/** @var bool Whether the wiki is not recognized in configuration */
@@ -56,34 +56,40 @@ class JobQueueGroup {
const CACHE_VERSION = 1; // integer; cache version
/**
- * @param string $wiki Wiki ID
+ * @param string $domain Wiki DB domain ID
* @param string|bool $readOnlyReason Read-only reason or false
*/
- protected function __construct( $wiki, $readOnlyReason ) {
- $this->wiki = $wiki;
+ protected function __construct( $domain, $readOnlyReason ) {
+ $this->domain = $domain;
$this->readOnlyReason = $readOnlyReason;
$this->cache = new ProcessCacheLRU( 10 );
}
/**
- * @param bool|string $wiki Wiki ID
+ * @param bool|string $domain Wiki domain ID
* @return JobQueueGroup
*/
- public static function singleton( $wiki = false ) {
+ public static function singleton( $domain = false ) {
global $wgLocalDatabases;
- $wiki = ( $wiki === false ) ? wfWikiID() : $wiki;
+ if ( $domain === false ) {
+ $domain = WikiMap::getCurrentWikiDbDomain()->getId();
+ }
- if ( !isset( self::$instances[$wiki] ) ) {
- self::$instances[$wiki] = new self( $wiki, wfConfiguredReadOnlyReason() );
+ if ( !isset( self::$instances[$domain] ) ) {
+ self::$instances[$domain] = new self( $domain, wfConfiguredReadOnlyReason() );
// Make sure jobs are not getting pushed to bogus wikis. This can confuse
// the job runner system into spawning endless RPC requests that fail (T171371).
- if ( $wiki !== wfWikiID() && !in_array( $wiki, $wgLocalDatabases ) ) {
- self::$instances[$wiki]->invalidWiki = true;
+ $wikiId = WikiMap::getWikiIdFromDomain( $domain );
+ if (
+ !WikiMap::isCurrentWikiDbDomain( $domain ) &&
+ !in_array( $wikiId, $wgLocalDatabases )
+ ) {
+ self::$instances[$domain]->invalidWiki = true;
}
}
- return self::$instances[$wiki];
+ return self::$instances[$domain];
}
/**
@@ -104,7 +110,7 @@ class JobQueueGroup {
public function get( $type ) {
global $wgJobTypeConf;
- $conf = [ 'wiki' => $this->wiki, 'type' => $type ];
+ $conf = [ 'wiki' => $this->domain, 'type' => $type ];
if ( isset( $wgJobTypeConf[$type] ) ) {
$conf = $conf + $wgJobTypeConf[$type];
} else {
@@ -133,7 +139,7 @@ class JobQueueGroup {
if ( $this->invalidWiki ) {
// Do not enqueue job that cannot be run (T171371)
- $e = new LogicException( "Domain '{$this->wiki}' is not recognized." );
+ $e = new LogicException( "Domain '{$this->domain}' is not recognized." );
MWExceptionHandler::logException( $e );
return;
}
@@ -163,13 +169,13 @@ class JobQueueGroup {
$cache = ObjectCache::getLocalClusterInstance();
$cache->set(
- $cache->makeGlobalKey( 'jobqueue', $this->wiki, 'hasjobs', self::TYPE_ANY ),
+ $cache->makeGlobalKey( 'jobqueue', $this->domain, 'hasjobs', self::TYPE_ANY ),
'true',
15
);
if ( array_diff( array_keys( $jobsByType ), $wgJobTypesExcludedFromDefaultQueue ) ) {
$cache->set(
- $cache->makeGlobalKey( 'jobqueue', $this->wiki, 'hasjobs', self::TYPE_DEFAULT ),
+ $cache->makeGlobalKey( 'jobqueue', $this->domain, 'hasjobs', self::TYPE_DEFAULT ),
'true',
15
);
@@ -190,7 +196,7 @@ class JobQueueGroup {
public function lazyPush( $jobs ) {
if ( $this->invalidWiki ) {
// Do not enqueue job that cannot be run (T171371)
- throw new LogicException( "Domain '{$this->wiki}' is not recognized." );
+ throw new LogicException( "Domain '{$this->domain}' is not recognized." );
}
if ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) {
@@ -338,7 +344,7 @@ class JobQueueGroup {
*/
public function queuesHaveJobs( $type = self::TYPE_ANY ) {
$cache = ObjectCache::getLocalClusterInstance();
- $key = $cache->makeGlobalKey( 'jobqueue', $this->wiki, 'hasjobs', $type );
+ $key = $cache->makeGlobalKey( 'jobqueue', $this->domain, 'hasjobs', $type );
$value = $cache->get( $key );
if ( $value === false ) {
@@ -407,7 +413,7 @@ class JobQueueGroup {
$this->coalescedQueues = [];
foreach ( $wgJobTypeConf as $type => $conf ) {
$queue = JobQueue::factory(
- [ 'wiki' => $this->wiki, 'type' => 'null' ] + $conf );
+ [ 'wiki' => $this->domain, 'type' => 'null' ] + $conf );
$loc = $queue->getCoalesceLocationInternal();
if ( !isset( $this->coalescedQueues[$loc] ) ) {
$this->coalescedQueues[$loc]['queue'] = $queue;
@@ -433,22 +439,21 @@ class JobQueueGroup {
*/
private function getCachedConfigVar( $name ) {
// @TODO: cleanup this whole method with a proper config system
- if ( $this->wiki === wfWikiID() ) {
+ if ( WikiMap::isCurrentWikiDbDomain( $this->domain ) ) {
return $GLOBALS[$name]; // common case
} else {
- $wiki = $this->wiki;
+ $wiki = WikiMap::getWikiIdFromDomain( $this->domain );
$cache = ObjectCache::getMainWANInstance();
$value = $cache->getWithSetCallback(
- $cache->makeGlobalKey( 'jobqueue', 'configvalue', $wiki, $name ),
+ $cache->makeGlobalKey( 'jobqueue', 'configvalue', $this->domain, $name ),
$cache::TTL_DAY + mt_rand( 0, $cache::TTL_DAY ),
function () use ( $wiki, $name ) {
global $wgConf;
-
+ // @TODO: use the full domain ID here
return [ 'v' => $wgConf->getConfig( $wiki, $name ) ];
},
[ 'pcTTL' => WANObjectCache::TTL_PROC_LONG ]
);
-
return $value['v'];
}
}
diff --git a/www/wiki/includes/jobqueue/JobQueueSecondTestQueue.php b/www/wiki/includes/jobqueue/JobQueueSecondTestQueue.php
index 4e3409af..01f467f2 100644
--- a/www/wiki/includes/jobqueue/JobQueueSecondTestQueue.php
+++ b/www/wiki/includes/jobqueue/JobQueueSecondTestQueue.php
@@ -27,6 +27,11 @@ class JobQueueSecondTestQueue extends JobQueue {
*/
private $debugQueue;
+ /**
+ * @var bool
+ */
+ private $onlyWriteToDebugQueue;
+
protected function __construct( array $params ) {
if ( !isset( $params['mainqueue'] ) ) {
throw new MWException( "mainqueue parameter must be provided to the debug queue" );
@@ -39,6 +44,7 @@ class JobQueueSecondTestQueue extends JobQueue {
$conf = [ 'wiki' => $params['wiki'], 'type' => $params['type'] ];
$this->mainQueue = JobQueue::factory( $params['mainqueue'] + $conf );
$this->debugQueue = JobQueue::factory( $params['debugqueue'] + $conf );
+ $this->onlyWriteToDebugQueue = isset( $params['readonly'] ) ? $params['readonly'] : false;
// We need to construct parent after creating the main and debug queue
// because super constructor calls some methods we delegate to the main queue.
@@ -118,7 +124,9 @@ class JobQueueSecondTestQueue extends JobQueue {
* @param int $flags
*/
protected function doBatchPush( array $jobs, $flags ) {
- $this->mainQueue->doBatchPush( $jobs, $flags );
+ if ( !$this->onlyWriteToDebugQueue ) {
+ $this->mainQueue->doBatchPush( $jobs, $flags );
+ }
try {
$this->debugQueue->doBatchPush( $jobs, $flags );
diff --git a/www/wiki/includes/jobqueue/JobRunner.php b/www/wiki/includes/jobqueue/JobRunner.php
index db881d5e..977fbdaa 100644
--- a/www/wiki/includes/jobqueue/JobRunner.php
+++ b/www/wiki/includes/jobqueue/JobRunner.php
@@ -119,6 +119,7 @@ class JobRunner implements LoggerAwareInterface {
$response['reached'] = 'none-possible';
return $response;
}
+
// Bail out if DB is in read-only mode
if ( wfReadOnly() ) {
$response['reached'] = 'read-only';
@@ -126,6 +127,9 @@ class JobRunner implements LoggerAwareInterface {
}
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ if ( $lbFactory->hasTransactionRound() ) {
+ throw new LogicException( __METHOD__ . ' called with an active transaction round.' );
+ }
// Bail out if there is too much DB lag.
// This check should not block as we want to try other wiki queues.
list( , $maxLag ) = $lbFactory->getMainLB( wfWikiID() )->getMaxLag();
@@ -134,9 +138,6 @@ class JobRunner implements LoggerAwareInterface {
return $response;
}
- // Flush any pending DB writes for sanity
- $lbFactory->commitAll( __METHOD__ );
-
// Catch huge single updates that lead to replica DB lag
$trxProfiler = Profiler::instance()->getTransactionProfiler();
$trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
@@ -170,7 +171,6 @@ class JobRunner implements LoggerAwareInterface {
} else {
$job = $group->pop( $type ); // job from a single queue
}
- $lbFactory->commitMasterChanges( __METHOD__ ); // flush any JobQueueDB writes
if ( $job ) { // found a job
++$jobsPopped;
@@ -193,7 +193,6 @@ class JobRunner implements LoggerAwareInterface {
$info = $this->executeJob( $job, $lbFactory, $stats, $popTime );
if ( $info['status'] !== false || !$job->allowRetries() ) {
$group->ack( $job ); // succeeded or job cannot be retried
- $lbFactory->commitMasterChanges( __METHOD__ ); // flush any JobQueueDB writes
}
// Back off of certain jobs for a while (for throttling and for errors)
@@ -291,7 +290,9 @@ class JobRunner implements LoggerAwareInterface {
$jobStartTime = microtime( true );
try {
$fnameTrxOwner = get_class( $job ) . '::run'; // give run() outer scope
- $lbFactory->beginMasterChanges( $fnameTrxOwner );
+ if ( !$job->hasExecutionFlag( $job::JOB_NO_EXPLICIT_TRX_ROUND ) ) {
+ $lbFactory->beginMasterChanges( $fnameTrxOwner );
+ }
$status = $job->run();
$error = $job->getLastError();
$this->commitMasterChanges( $lbFactory, $job, $fnameTrxOwner );
diff --git a/www/wiki/includes/jobqueue/JobSpecification.php b/www/wiki/includes/jobqueue/JobSpecification.php
index d8447951..b62b83c6 100644
--- a/www/wiki/includes/jobqueue/JobSpecification.php
+++ b/www/wiki/includes/jobqueue/JobSpecification.php
@@ -18,7 +18,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup JobQueue
*/
/**
diff --git a/www/wiki/includes/jobqueue/aggregator/JobQueueAggregator.php b/www/wiki/includes/jobqueue/aggregator/JobQueueAggregator.php
index f26beee4..433de93a 100644
--- a/www/wiki/includes/jobqueue/aggregator/JobQueueAggregator.php
+++ b/www/wiki/includes/jobqueue/aggregator/JobQueueAggregator.php
@@ -158,6 +158,9 @@ abstract class JobQueueAggregator {
}
}
+/**
+ * @ingroup JobQueue
+ */
class JobQueueAggregatorNull extends JobQueueAggregator {
protected function doNotifyQueueEmpty( $wiki, $type ) {
return true;
diff --git a/www/wiki/includes/jobqueue/jobs/ActivityUpdateJob.php b/www/wiki/includes/jobqueue/jobs/ActivityUpdateJob.php
index da4ec233..8cc14e51 100644
--- a/www/wiki/includes/jobqueue/jobs/ActivityUpdateJob.php
+++ b/www/wiki/includes/jobqueue/jobs/ActivityUpdateJob.php
@@ -22,6 +22,12 @@
/**
* Job for updating user activity like "last viewed" timestamps
*
+ * Job parameters include:
+ * - type: one of (updateWatchlistNotification) [required]
+ * - userid: affected user ID [required]
+ * - notifTime: timestamp to set watchlist entries to [required]
+ * - curTime: UNIX timestamp of the event that triggered this job [required]
+ *
* @ingroup JobQueue
* @since 1.26
*/
@@ -29,8 +35,10 @@ class ActivityUpdateJob extends Job {
function __construct( Title $title, array $params ) {
parent::__construct( 'activityUpdateJob', $title, $params );
- if ( !isset( $params['type'] ) ) {
- throw new InvalidArgumentException( "Missing 'type' parameter." );
+ static $required = [ 'type', 'userid', 'notifTime', 'curTime' ];
+ $missing = implode( ', ', array_diff( $required, array_keys( $this->params ) ) );
+ if ( $missing != '' ) {
+ throw new InvalidArgumentException( "Missing paramter(s) $missing" );
}
$this->removeDuplicates = true;
@@ -40,8 +48,7 @@ class ActivityUpdateJob extends Job {
if ( $this->params['type'] === 'updateWatchlistNotification' ) {
$this->updateWatchlistNotification();
} else {
- throw new InvalidArgumentException(
- "Invalid 'type' parameter '{$this->params['type']}'." );
+ throw new InvalidArgumentException( "Invalid 'type' '{$this->params['type']}'." );
}
return true;
diff --git a/www/wiki/includes/jobqueue/jobs/CategoryMembershipChangeJob.php b/www/wiki/includes/jobqueue/jobs/CategoryMembershipChangeJob.php
index 3907fc65..16640be4 100644
--- a/www/wiki/includes/jobqueue/jobs/CategoryMembershipChangeJob.php
+++ b/www/wiki/includes/jobqueue/jobs/CategoryMembershipChangeJob.php
@@ -25,6 +25,8 @@ use Wikimedia\Rdbms\LBFactory;
/**
* Job to add recent change entries mentioning category membership changes
*
+ * This allows users to easily scan categories for recent page membership changes
+ *
* Parameters include:
* - pageId : page ID
* - revTimestamp : timestamp of the triggering revision
@@ -59,6 +61,13 @@ class CategoryMembershipChangeJob extends Job {
return false; // deleted?
}
+ // Cut down on the time spent in safeWaitForMasterPos() in the critical section
+ $dbr = $lb->getConnection( DB_REPLICA, [ 'recentchanges' ] );
+ if ( !$lb->safeWaitForMasterPos( $dbr ) ) {
+ $this->setLastError( "Timed out while pre-waiting for replica DB to catch up" );
+ return false;
+ }
+
// Use a named lock so that jobs for this page see each others' changes
$lockKey = "CategoryMembershipUpdates:{$page->getId()}";
$scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 3 );
@@ -67,8 +76,7 @@ class CategoryMembershipChangeJob extends Job {
return false;
}
- $dbr = $lb->getConnection( DB_REPLICA, [ 'recentchanges' ] );
- // Wait till the replica DB is caught up so that jobs for this page see each others' changes
+ // Wait till replica DB is caught up so that jobs for this page see each others' changes
if ( !$lb->safeWaitForMasterPos( $dbr ) ) {
$this->setLastError( "Timed out while waiting for replica DB to catch up" );
return false;
@@ -81,28 +89,28 @@ class CategoryMembershipChangeJob extends Job {
// between COMMIT and actual enqueueing of the CategoryMembershipChangeJob job.
$cutoffUnix -= self::ENQUEUE_FUDGE_SEC;
- // Get the newest revision that has a SRC_CATEGORIZE row...
+ // Get the newest page revision that has a SRC_CATEGORIZE row.
+ // Assume that category changes before it were already handled.
$row = $dbr->selectRow(
- [ 'revision', 'recentchanges' ],
+ 'revision',
[ 'rev_timestamp', 'rev_id' ],
[
'rev_page' => $page->getId(),
- 'rev_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( $cutoffUnix ) )
- ],
- __METHOD__,
- [ 'ORDER BY' => 'rev_timestamp DESC, rev_id DESC' ],
- [
- 'recentchanges' => [
- 'INNER JOIN',
+ 'rev_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( $cutoffUnix ) ),
+ 'EXISTS (' . $dbr->selectSQLText(
+ 'recentchanges',
+ '1',
[
'rc_this_oldid = rev_id',
'rc_source' => RecentChange::SRC_CATEGORIZE,
// Allow rc_cur_id or rc_timestamp index usage
'rc_cur_id = rev_page',
- 'rc_timestamp >= rev_timestamp'
+ 'rc_timestamp = rev_timestamp'
]
- ]
- ]
+ ) . ')'
+ ],
+ __METHOD__,
+ [ 'ORDER BY' => 'rev_timestamp DESC, rev_id DESC' ]
);
// Only consider revisions newer than any such revision
if ( $row ) {
@@ -115,16 +123,18 @@ class CategoryMembershipChangeJob extends Job {
// Find revisions to this page made around and after this revision which lack category
// notifications in recent changes. This lets jobs pick up were the last one left off.
$encCutoff = $dbr->addQuotes( $dbr->timestamp( $cutoffUnix ) );
+ $revQuery = Revision::getQueryInfo();
$res = $dbr->select(
- 'revision',
- Revision::selectFields(),
+ $revQuery['tables'],
+ $revQuery['fields'],
[
'rev_page' => $page->getId(),
"rev_timestamp > $encCutoff" .
" OR (rev_timestamp = $encCutoff AND rev_id > $lastRevId)"
],
__METHOD__,
- [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC' ]
+ [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC' ],
+ $revQuery['joins']
);
// Apply all category updates in revision timestamp order
diff --git a/www/wiki/includes/jobqueue/jobs/ClearUserWatchlistJob.php b/www/wiki/includes/jobqueue/jobs/ClearUserWatchlistJob.php
new file mode 100644
index 00000000..77adfa1a
--- /dev/null
+++ b/www/wiki/includes/jobqueue/jobs/ClearUserWatchlistJob.php
@@ -0,0 +1,118 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * Job to clear a users watchlist in batches.
+ *
+ * @author Addshore
+ *
+ * @ingroup JobQueue
+ * @since 1.31
+ */
+class ClearUserWatchlistJob extends Job {
+
+ /**
+ * @param User $user User to clear the watchlist for.
+ * @param int $maxWatchlistId The maximum wl_id at the time the job was first created.
+ *
+ * @return ClearUserWatchlistJob
+ */
+ public static function newForUser( User $user, $maxWatchlistId ) {
+ return new self(
+ null,
+ [ 'userId' => $user->getId(), 'maxWatchlistId' => $maxWatchlistId ]
+ );
+ }
+
+ /**
+ * @param Title|null $title Not used by this job.
+ * @param array $params
+ * - userId, The ID for the user whose watchlist is being cleared.
+ * - maxWatchlistId, The maximum wl_id at the time the job was first created,
+ */
+ public function __construct( Title $title = null, array $params ) {
+ parent::__construct(
+ 'clearUserWatchlist',
+ SpecialPage::getTitleFor( 'EditWatchlist', 'clear' ),
+ $params
+ );
+
+ $this->removeDuplicates = true;
+ }
+
+ public function run() {
+ global $wgUpdateRowsPerQuery;
+ $userId = $this->params['userId'];
+ $maxWatchlistId = $this->params['maxWatchlistId'];
+ $batchSize = $wgUpdateRowsPerQuery;
+
+ $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ $dbw = $loadBalancer->getConnection( DB_MASTER );
+ $dbr = $loadBalancer->getConnection( DB_REPLICA, [ 'watchlist' ] );
+
+ // Wait before lock to try to reduce time waiting in the lock.
+ if ( !$loadBalancer->safeWaitForMasterPos( $dbr ) ) {
+ $this->setLastError( 'Timed out waiting for replica to catch up before lock' );
+ return false;
+ }
+
+ // Use a named lock so that jobs for this user see each others' changes
+ $lockKey = "ClearUserWatchlistJob:$userId";
+ $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 10 );
+ if ( !$scopedLock ) {
+ $this->setLastError( "Could not acquire lock '$lockKey'" );
+ return false;
+ }
+
+ if ( !$loadBalancer->safeWaitForMasterPos( $dbr ) ) {
+ $this->setLastError( 'Timed out waiting for replica to catch up within lock' );
+ return false;
+ }
+
+ // Clear any stale REPEATABLE-READ snapshot
+ $dbr->flushSnapshot( __METHOD__ );
+
+ $watchlistIds = $dbr->selectFieldValues(
+ 'watchlist',
+ 'wl_id',
+ [
+ 'wl_user' => $userId,
+ 'wl_id <= ' . $maxWatchlistId
+ ],
+ __METHOD__,
+ [
+ 'ORDER BY' => 'wl_id ASC',
+ 'LIMIT' => $batchSize,
+ ]
+ );
+
+ if ( count( $watchlistIds ) == 0 ) {
+ return true;
+ }
+
+ $dbw->delete( 'watchlist', [ 'wl_id' => $watchlistIds ], __METHOD__ );
+
+ // Commit changes and remove lock before inserting next job.
+ $lbf = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $lbf->commitMasterChanges( __METHOD__ );
+ unset( $scopedLock );
+
+ if ( count( $watchlistIds ) === (int)$batchSize ) {
+ // Until we get less results than the limit, recursively push
+ // the same job again.
+ JobQueueGroup::singleton()->push( new self( $this->getTitle(), $this->getParams() ) );
+ }
+
+ return true;
+ }
+
+ public function getDeduplicationInfo() {
+ $info = parent::getDeduplicationInfo();
+ // This job never has a namespace or title so we can't use it for deduplication
+ unset( $info['namespace'] );
+ unset( $info['title'] );
+ return $info;
+ }
+
+}
diff --git a/www/wiki/includes/jobqueue/jobs/ClearWatchlistNotificationsJob.php b/www/wiki/includes/jobqueue/jobs/ClearWatchlistNotificationsJob.php
new file mode 100644
index 00000000..94c1351a
--- /dev/null
+++ b/www/wiki/includes/jobqueue/jobs/ClearWatchlistNotificationsJob.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup JobQueue
+ */
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * Job for clearing all of the "last viewed" timestamps for a user's watchlist
+ *
+ * Job parameters include:
+ * - userId: affected user ID [required]
+ * - casTime: UNIX timestamp of the event that triggered this job [required]
+ *
+ * @ingroup JobQueue
+ * @since 1.31
+ */
+class ClearWatchlistNotificationsJob extends Job {
+ function __construct( Title $title, array $params ) {
+ parent::__construct( 'clearWatchlistNotifications', $title, $params );
+
+ static $required = [ 'userId', 'casTime' ];
+ $missing = implode( ', ', array_diff( $required, array_keys( $this->params ) ) );
+ if ( $missing != '' ) {
+ throw new InvalidArgumentException( "Missing paramter(s) $missing" );
+ }
+
+ $this->removeDuplicates = true;
+ }
+
+ public function run() {
+ $services = MediaWikiServices::getInstance();
+ $lbFactory = $services->getDBLoadBalancerFactory();
+ $rowsPerQuery = $services->getMainConfig()->get( 'UpdateRowsPerQuery' );
+
+ $dbw = $lbFactory->getMainLB()->getConnection( DB_MASTER );
+ $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
+
+ $asOfTimes = array_unique( $dbw->selectFieldValues(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ [ 'wl_user' => $this->params['userId'], 'wl_notificationtimestamp IS NOT NULL' ],
+ __METHOD__,
+ [ 'ORDER BY' => 'wl_notificationtimestamp DESC' ]
+ ) );
+
+ foreach ( array_chunk( $asOfTimes, $rowsPerQuery ) as $asOfTimeBatch ) {
+ $dbw->update(
+ 'watchlist',
+ [ 'wl_notificationtimestamp' => null ],
+ [
+ 'wl_user' => $this->params['userId'],
+ 'wl_notificationtimestamp' => $asOfTimeBatch,
+ // New notifications since the reset should not be cleared
+ 'wl_notificationtimestamp < ' .
+ $dbw->addQuotes( $dbw->timestamp( $this->params['casTime'] ) )
+ ],
+ __METHOD__
+ );
+ $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+ }
+ }
+}
diff --git a/www/wiki/includes/jobqueue/jobs/DeleteLinksJob.php b/www/wiki/includes/jobqueue/jobs/DeleteLinksJob.php
index 5c0f89f7..d0969e46 100644
--- a/www/wiki/includes/jobqueue/jobs/DeleteLinksJob.php
+++ b/www/wiki/includes/jobqueue/jobs/DeleteLinksJob.php
@@ -20,7 +20,8 @@
* @file
* @ingroup JobQueue
*/
-use \MediaWiki\MediaWikiServices;
+
+use MediaWiki\MediaWikiServices;
/**
* Job to prune link tables for pages that were deleted
diff --git a/www/wiki/includes/jobqueue/jobs/EnqueueJob.php b/www/wiki/includes/jobqueue/jobs/EnqueueJob.php
index 5ffb01b4..ea7a8d78 100644
--- a/www/wiki/includes/jobqueue/jobs/EnqueueJob.php
+++ b/www/wiki/includes/jobqueue/jobs/EnqueueJob.php
@@ -24,11 +24,10 @@
/**
* Router job that takes jobs and enqueues them to their proper queues
*
- * This can be used for several things:
- * - a) Making multi-job enqueues more robust by atomically enqueueing
- * a single job that pushes the actual jobs (with retry logic)
- * - b) Masking the latency of pushing jobs to different queues/wikis
- * - c) Low-latency enqueues to push jobs from warm to hot datacenters
+ * This can be used for getting sets of multiple jobs or sets of jobs intended for multiple
+ * queues to be inserted more robustly. This is a single job that, upon running, enqueues the
+ * wrapped jobs. If some of those fail to enqueue then the EnqueueJob will be retried. Due to
+ * the possibility of duplicate enqueues, the wrapped jobs should be idempotent.
*
* @ingroup JobQueue
* @since 1.25
diff --git a/www/wiki/includes/jobqueue/jobs/HTMLCacheUpdateJob.php b/www/wiki/includes/jobqueue/jobs/HTMLCacheUpdateJob.php
index 0aa33cac..34028df1 100644
--- a/www/wiki/includes/jobqueue/jobs/HTMLCacheUpdateJob.php
+++ b/www/wiki/includes/jobqueue/jobs/HTMLCacheUpdateJob.php
@@ -47,14 +47,16 @@ class HTMLCacheUpdateJob extends Job {
// Multiple pages per job make matches unlikely
!( isset( $params['pages'] ) && count( $params['pages'] ) != 1 )
);
+ $this->params += [ 'causeAction' => 'unknown', 'causeAgent' => 'unknown' ];
}
/**
* @param Title $title Title to purge backlink pages from
* @param string $table Backlink table name
+ * @param array $params Additional job parameters
* @return HTMLCacheUpdateJob
*/
- public static function newForBacklinks( Title $title, $table ) {
+ public static function newForBacklinks( Title $title, $table, $params = [] ) {
return new self(
$title,
[
@@ -62,7 +64,7 @@ class HTMLCacheUpdateJob extends Job {
'recursive' => true
] + Job::newRootJobParams( // "overall" refresh links job info
"htmlCacheUpdate:{$table}:{$title->getPrefixedText()}"
- )
+ ) + $params
);
}
@@ -75,6 +77,11 @@ class HTMLCacheUpdateJob extends Job {
// Job to purge all (or a range of) backlink pages for a page
if ( !empty( $this->params['recursive'] ) ) {
+ // Carry over information for de-duplication
+ $extraParams = $this->getRootJobParams();
+ // Carry over cause information for logging
+ $extraParams['causeAction'] = $this->params['causeAction'];
+ $extraParams['causeAgent'] = $this->params['causeAgent'];
// Convert this into no more than $wgUpdateRowsPerJob HTMLCacheUpdateJob per-title
// jobs and possibly a recursive HTMLCacheUpdateJob job for the rest of the backlinks
$jobs = BacklinkJobUtils::partitionBacklinkJob(
@@ -82,7 +89,7 @@ class HTMLCacheUpdateJob extends Job {
$wgUpdateRowsPerJob,
$wgUpdateRowsPerQuery, // jobs-per-title
// Carry over information for de-duplication
- [ 'params' => $this->getRootJobParams() ]
+ [ 'params' => $extraParams ]
);
JobQueueGroup::singleton()->push( $jobs );
// Job to purge pages for a set of titles
@@ -103,7 +110,7 @@ class HTMLCacheUpdateJob extends Job {
* @param array $pages Map of (page ID => (namespace, DB key)) entries
*/
protected function invalidateTitles( array $pages ) {
- global $wgUpdateRowsPerQuery, $wgUseFileCache;
+ global $wgUpdateRowsPerQuery, $wgUseFileCache, $wgPageLanguageUseDB;
// Get all page IDs in this query into an array
$pageIds = array_keys( $pages );
@@ -145,7 +152,10 @@ class HTMLCacheUpdateJob extends Job {
// Get the list of affected pages (races only mean something else did the purge)
$titleArray = TitleArray::newFromResult( $dbw->select(
'page',
- [ 'page_namespace', 'page_title' ],
+ array_merge(
+ [ 'page_namespace', 'page_title' ],
+ $wgPageLanguageUseDB ? [ 'page_lang' ] : []
+ ),
[ 'page_id' => $pageIds, 'page_touched' => $dbw->timestamp( $touchTimestamp ) ],
__METHOD__
) );
@@ -166,6 +176,20 @@ class HTMLCacheUpdateJob extends Job {
}
}
+ public function getDeduplicationInfo() {
+ $info = parent::getDeduplicationInfo();
+ if ( is_array( $info['params'] ) ) {
+ // For per-pages jobs, the job title is that of the template that changed
+ // (or similar), so remove that since it ruins duplicate detection
+ if ( isset( $info['params']['pages'] ) ) {
+ unset( $info['namespace'] );
+ unset( $info['title'] );
+ }
+ }
+
+ return $info;
+ }
+
public function workItemCount() {
if ( !empty( $this->params['recursive'] ) ) {
return 0; // nothing actually purged
diff --git a/www/wiki/includes/jobqueue/jobs/RecentChangesUpdateJob.php b/www/wiki/includes/jobqueue/jobs/RecentChangesUpdateJob.php
index 6f349d44..8f508283 100644
--- a/www/wiki/includes/jobqueue/jobs/RecentChangesUpdateJob.php
+++ b/www/wiki/includes/jobqueue/jobs/RecentChangesUpdateJob.php
@@ -35,6 +35,7 @@ class RecentChangesUpdateJob extends Job {
throw new Exception( "Missing 'type' parameter." );
}
+ $this->executionFlags |= self::JOB_NO_EXPLICIT_TRX_ROUND;
$this->removeDuplicates = true;
}
@@ -76,23 +77,25 @@ class RecentChangesUpdateJob extends Job {
$lockKey = wfWikiID() . ':recentchanges-prune';
$dbw = wfGetDB( DB_MASTER );
- if ( !$dbw->lockIsFree( $lockKey, __METHOD__ )
- || !$dbw->lock( $lockKey, __METHOD__, 1 )
- ) {
- return; // already in progress
+ if ( !$dbw->lock( $lockKey, __METHOD__, 0 ) ) {
+ // already in progress
+ return;
}
$factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
$cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
+ $rcQuery = RecentChange::getQueryInfo();
do {
$rcIds = [];
$rows = [];
- $res = $dbw->select( 'recentchanges',
- RecentChange::selectFields(),
+ $res = $dbw->select(
+ $rcQuery['tables'],
+ $rcQuery['fields'],
[ 'rc_timestamp < ' . $dbw->addQuotes( $cutoff ) ],
__METHOD__,
- [ 'LIMIT' => $wgUpdateRowsPerQuery ]
+ [ 'LIMIT' => $wgUpdateRowsPerQuery ],
+ $rcQuery['joins']
);
foreach ( $res as $row ) {
$rcIds[] = $row->rc_id;
@@ -125,119 +128,118 @@ class RecentChangesUpdateJob extends Job {
$window = $wgActiveUserDays * 86400;
$dbw = wfGetDB( DB_MASTER );
- // JobRunner uses DBO_TRX, but doesn't call begin/commit itself;
- // onTransactionIdle() will run immediately since there is no trx.
- $dbw->onTransactionIdle(
- function () use ( $dbw, $days, $window ) {
- $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
- $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
- // Avoid disconnect/ping() cycle that makes locks fall off
- $dbw->setSessionOptions( [ 'connTimeout' => 900 ] );
-
- $lockKey = wfWikiID() . '-activeusers';
- if ( !$dbw->lockIsFree( $lockKey, __METHOD__ ) || !$dbw->lock( $lockKey, __METHOD__, 1 ) ) {
- // Exclusive update (avoids duplicate entries)… it's usually fine to just drop out here,
- // if the Job is already running.
- return;
- }
+ $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
- $nowUnix = time();
- // Get the last-updated timestamp for the cache
- $cTime = $dbw->selectField( 'querycache_info',
- 'qci_timestamp',
- [ 'qci_type' => 'activeusers' ]
- );
- $cTimeUnix = $cTime ? wfTimestamp( TS_UNIX, $cTime ) : 1;
-
- // Pick the date range to fetch from. This is normally from the last
- // update to till the present time, but has a limited window for sanity.
- // If the window is limited, multiple runs are need to fully populate it.
- $sTimestamp = max( $cTimeUnix, $nowUnix - $days * 86400 );
- $eTimestamp = min( $sTimestamp + $window, $nowUnix );
-
- // Get all the users active since the last update
- $res = $dbw->select(
- [ 'recentchanges' ],
- [ 'rc_user_text', 'lastedittime' => 'MAX(rc_timestamp)' ],
- [
- 'rc_user > 0', // actual accounts
- 'rc_type != ' . $dbw->addQuotes( RC_EXTERNAL ), // no wikidata
- 'rc_log_type IS NULL OR rc_log_type != ' . $dbw->addQuotes( 'newusers' ),
- 'rc_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $sTimestamp ) ),
- 'rc_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $eTimestamp ) )
- ],
- __METHOD__,
- [
- 'GROUP BY' => [ 'rc_user_text' ],
- 'ORDER BY' => 'NULL' // avoid filesort
- ]
- );
- $names = [];
- foreach ( $res as $row ) {
- $names[$row->rc_user_text] = $row->lastedittime;
- }
+ $lockKey = wfWikiID() . '-activeusers';
+ if ( !$dbw->lock( $lockKey, __METHOD__, 0 ) ) {
+ // Exclusive update (avoids duplicate entries)… it's usually fine to just
+ // drop out here, if the Job is already running.
+ return;
+ }
- // Find which of the recently active users are already accounted for
- if ( count( $names ) ) {
- $res = $dbw->select( 'querycachetwo',
- [ 'user_name' => 'qcc_title' ],
- [
- 'qcc_type' => 'activeusers',
- 'qcc_namespace' => NS_USER,
- 'qcc_title' => array_keys( $names ),
- 'qcc_value >= ' . $dbw->addQuotes( $nowUnix - $days * 86400 ), // TS_UNIX
- ],
- __METHOD__
- );
- // Note: In order for this to be actually consistent, we would need
- // to update these rows with the new lastedittime.
- foreach ( $res as $row ) {
- unset( $names[$row->user_name] );
- }
- }
+ // Long-running queries expected
+ $dbw->setSessionOptions( [ 'connTimeout' => 900 ] );
- // Insert the users that need to be added to the list
- if ( count( $names ) ) {
- $newRows = [];
- foreach ( $names as $name => $lastEditTime ) {
- $newRows[] = [
- 'qcc_type' => 'activeusers',
- 'qcc_namespace' => NS_USER,
- 'qcc_title' => $name,
- 'qcc_value' => wfTimestamp( TS_UNIX, $lastEditTime ),
- 'qcc_namespacetwo' => 0, // unused
- 'qcc_titletwo' => '' // unused
- ];
- }
- foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) {
- $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ );
- $factory->commitAndWaitForReplication( __METHOD__, $ticket );
- }
- }
+ $nowUnix = time();
+ // Get the last-updated timestamp for the cache
+ $cTime = $dbw->selectField( 'querycache_info',
+ 'qci_timestamp',
+ [ 'qci_type' => 'activeusers' ]
+ );
+ $cTimeUnix = $cTime ? wfTimestamp( TS_UNIX, $cTime ) : 1;
+
+ // Pick the date range to fetch from. This is normally from the last
+ // update to till the present time, but has a limited window for sanity.
+ // If the window is limited, multiple runs are need to fully populate it.
+ $sTimestamp = max( $cTimeUnix, $nowUnix - $days * 86400 );
+ $eTimestamp = min( $sTimestamp + $window, $nowUnix );
+
+ // Get all the users active since the last update
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
+ $res = $dbw->select(
+ [ 'recentchanges' ] + $actorQuery['tables'],
+ [
+ 'rc_user_text' => $actorQuery['fields']['rc_user_text'],
+ 'lastedittime' => 'MAX(rc_timestamp)'
+ ],
+ [
+ $actorQuery['fields']['rc_user'] . ' > 0', // actual accounts
+ 'rc_type != ' . $dbw->addQuotes( RC_EXTERNAL ), // no wikidata
+ 'rc_log_type IS NULL OR rc_log_type != ' . $dbw->addQuotes( 'newusers' ),
+ 'rc_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $sTimestamp ) ),
+ 'rc_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $eTimestamp ) )
+ ],
+ __METHOD__,
+ [
+ 'GROUP BY' => [ 'rc_user_text' ],
+ 'ORDER BY' => 'NULL' // avoid filesort
+ ],
+ $actorQuery['joins']
+ );
+ $names = [];
+ foreach ( $res as $row ) {
+ $names[$row->rc_user_text] = $row->lastedittime;
+ }
+
+ // Find which of the recently active users are already accounted for
+ if ( count( $names ) ) {
+ $res = $dbw->select( 'querycachetwo',
+ [ 'user_name' => 'qcc_title' ],
+ [
+ 'qcc_type' => 'activeusers',
+ 'qcc_namespace' => NS_USER,
+ 'qcc_title' => array_keys( $names ),
+ 'qcc_value >= ' . $dbw->addQuotes( $nowUnix - $days * 86400 ), // TS_UNIX
+ ],
+ __METHOD__
+ );
+ // Note: In order for this to be actually consistent, we would need
+ // to update these rows with the new lastedittime.
+ foreach ( $res as $row ) {
+ unset( $names[$row->user_name] );
+ }
+ }
+
+ // Insert the users that need to be added to the list
+ if ( count( $names ) ) {
+ $newRows = [];
+ foreach ( $names as $name => $lastEditTime ) {
+ $newRows[] = [
+ 'qcc_type' => 'activeusers',
+ 'qcc_namespace' => NS_USER,
+ 'qcc_title' => $name,
+ 'qcc_value' => wfTimestamp( TS_UNIX, $lastEditTime ),
+ 'qcc_namespacetwo' => 0, // unused
+ 'qcc_titletwo' => '' // unused
+ ];
+ }
+ foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) {
+ $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ );
+ $factory->commitAndWaitForReplication( __METHOD__, $ticket );
+ }
+ }
+
+ // If a transaction was already started, it might have an old
+ // snapshot, so kludge the timestamp range back as needed.
+ $asOfTimestamp = min( $eTimestamp, (int)$dbw->trxTimestamp() );
+
+ // Touch the data freshness timestamp
+ $dbw->replace( 'querycache_info',
+ [ 'qci_type' ],
+ [ 'qci_type' => 'activeusers',
+ 'qci_timestamp' => $dbw->timestamp( $asOfTimestamp ) ], // not always $now
+ __METHOD__
+ );
+
+ $dbw->unlock( $lockKey, __METHOD__ );
- // If a transaction was already started, it might have an old
- // snapshot, so kludge the timestamp range back as needed.
- $asOfTimestamp = min( $eTimestamp, (int)$dbw->trxTimestamp() );
-
- // Touch the data freshness timestamp
- $dbw->replace( 'querycache_info',
- [ 'qci_type' ],
- [ 'qci_type' => 'activeusers',
- 'qci_timestamp' => $dbw->timestamp( $asOfTimestamp ) ], // not always $now
- __METHOD__
- );
-
- $dbw->unlock( $lockKey, __METHOD__ );
-
- // Rotate out users that have not edited in too long (according to old data set)
- $dbw->delete( 'querycachetwo',
- [
- 'qcc_type' => 'activeusers',
- 'qcc_value < ' . $dbw->addQuotes( $nowUnix - $days * 86400 ) // TS_UNIX
- ],
- __METHOD__
- );
- },
+ // Rotate out users that have not edited in too long (according to old data set)
+ $dbw->delete( 'querycachetwo',
+ [
+ 'qcc_type' => 'activeusers',
+ 'qcc_value < ' . $dbw->addQuotes( $nowUnix - $days * 86400 ) // TS_UNIX
+ ],
__METHOD__
);
}
diff --git a/www/wiki/includes/jobqueue/jobs/RefreshLinksJob.php b/www/wiki/includes/jobqueue/jobs/RefreshLinksJob.php
index 424fcecb..8854c656 100644
--- a/www/wiki/includes/jobqueue/jobs/RefreshLinksJob.php
+++ b/www/wiki/includes/jobqueue/jobs/RefreshLinksJob.php
@@ -53,6 +53,7 @@ class RefreshLinksJob extends Job {
// Multiple pages per job make matches unlikely
!( isset( $params['pages'] ) && count( $params['pages'] ) != 1 )
);
+ $this->params += [ 'causeAction' => 'unknown', 'causeAgent' => 'unknown' ];
}
/**
@@ -102,6 +103,9 @@ class RefreshLinksJob extends Job {
// Carry over information for de-duplication
$extraParams = $this->getRootJobParams();
$extraParams['triggeredRecursive'] = true;
+ // Carry over cause information for logging
+ $extraParams['causeAction'] = $this->params['causeAction'];
+ $extraParams['causeAgent'] = $this->params['causeAgent'];
// Convert this into no more than $wgUpdateRowsPerJob RefreshLinks per-title
// jobs and possibly a recursive RefreshLinks job for the rest of the backlinks
$jobs = BacklinkJobUtils::partitionBacklinkJob(
@@ -254,6 +258,8 @@ class RefreshLinksJob extends Job {
$lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
foreach ( $updates as $update ) {
+ // Carry over cause in case so the update can do extra logging
+ $update->setCause( $this->params['causeAction'], $this->params['causeAgent'] );
// FIXME: This code probably shouldn't be here?
// Needed by things like Echo notifications which need
// to know which user caused the links update
@@ -288,10 +294,12 @@ class RefreshLinksJob extends Job {
public function getDeduplicationInfo() {
$info = parent::getDeduplicationInfo();
+ unset( $info['causeAction'] );
+ unset( $info['causeAgent'] );
if ( is_array( $info['params'] ) ) {
// For per-pages jobs, the job title is that of the template that changed
// (or similar), so remove that since it ruins duplicate detection
- if ( isset( $info['pages'] ) ) {
+ if ( isset( $info['params']['pages'] ) ) {
unset( $info['namespace'] );
unset( $info['title'] );
}
diff --git a/www/wiki/includes/jobqueue/jobs/UserGroupExpiryJob.php b/www/wiki/includes/jobqueue/jobs/UserGroupExpiryJob.php
new file mode 100644
index 00000000..0945e58f
--- /dev/null
+++ b/www/wiki/includes/jobqueue/jobs/UserGroupExpiryJob.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Job that purges expired user group memberships.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup JobQueue
+ */
+
+class UserGroupExpiryJob extends Job {
+ public function __construct( $params = false ) {
+ parent::__construct( 'userGroupExpiry', Title::newMainPage(), $params );
+ $this->removeDuplicates = true;
+ }
+
+ /**
+ * Run the job
+ * @return bool Success
+ */
+ public function run() {
+ UserGroupMembership::purgeExpired();
+
+ return true;
+ }
+}
diff --git a/www/wiki/includes/json/FormatJson.php b/www/wiki/includes/json/FormatJson.php
index 0c77a7bc..bd6a3654 100644
--- a/www/wiki/includes/json/FormatJson.php
+++ b/www/wiki/includes/json/FormatJson.php
@@ -294,7 +294,7 @@ class FormatJson {
$lookAhead = ( $idx + 1 < $maxLen ) ? $str[$idx + 1] : '';
$lookBehind = ( $idx - 1 >= 0 ) ? $str[$idx - 1] : '';
if ( $inString ) {
- continue;
+ break;
} elseif ( !$inComment &&
( $lookAhead === '/' || $lookAhead === '*' )
diff --git a/www/wiki/includes/libs/CSSMin.php b/www/wiki/includes/libs/CSSMin.php
index ee88d0d2..1ee7a3bc 100644
--- a/www/wiki/includes/libs/CSSMin.php
+++ b/www/wiki/includes/libs/CSSMin.php
@@ -29,8 +29,6 @@
*/
class CSSMin {
- /* Constants */
-
/** @var string Strip marker for comments. **/
const PLACEHOLDER = "\x7fPLACEHOLDER\x7f";
@@ -42,8 +40,6 @@ class CSSMin {
const EMBED_REGEX = '\/\*\s*\@embed\s*\*\/';
const COMMENT_REGEX = '\/\*.*?\*\/';
- /* Protected Static Members */
-
/** @var array List of common image files extensions and MIME-types */
protected static $mimeTypes = [
'gif' => 'image/gif',
@@ -57,8 +53,6 @@ class CSSMin {
'svg' => 'image/svg+xml',
];
- /* Static Methods */
-
/**
* Get a list of local files referenced in a stylesheet (includes non-existent files).
*
@@ -138,6 +132,9 @@ class CSSMin {
*/
public static function encodeStringAsDataURI( $contents, $type, $ie8Compat = true ) {
// Try #1: Non-encoded data URI
+
+ // Remove XML declaration, it's not needed with data URI usage
+ $contents = preg_replace( "/<\\?xml.*?\\?>/", '', $contents );
// The regular expression matches ASCII whitespace and printable characters.
if ( preg_match( '/^[\r\n\t\x20-\x7e]+$/', $contents ) ) {
// Do not base64-encode non-binary files (sane SVGs).
@@ -149,7 +146,15 @@ class CSSMin {
'%2F' => '/', // Unencode slashes
'%3A' => ':', // Unencode colons
'%3D' => '=', // Unencode equals signs
+ '%0A' => ' ', // Change newlines to spaces
+ '%0D' => ' ', // Change carriage returns to spaces
+ '%09' => ' ', // Change tabs to spaces
] );
+ // Consolidate runs of multiple spaces in a row
+ $encoded = preg_replace( '/ {2,}/', ' ', $encoded );
+ // Remove leading and trailing spaces
+ $encoded = preg_replace( '/^ | $/', '', $encoded );
+
$uri = 'data:' . $type . ',' . $encoded;
if ( !$ie8Compat || strlen( $uri ) < self::DATA_URI_SIZE_LIMIT ) {
return $uri;
@@ -168,18 +173,14 @@ class CSSMin {
/**
* Serialize a string (escape and quote) for use as a CSS string value.
- * http://www.w3.org/TR/2013/WD-cssom-20131205/#serialize-a-string
+ * https://www.w3.org/TR/2016/WD-cssom-1-20160317/#serialize-a-string
*
* @param string $value
* @return string
- * @throws Exception
*/
public static function serializeStringValue( $value ) {
- if ( strstr( $value, "\0" ) ) {
- throw new Exception( "Invalid character in CSS string" );
- }
- $value = strtr( $value, [ '\\' => '\\\\', '"' => '\\"' ] );
- $value = preg_replace_callback( '/[\x01-\x1f\x7f-\x9f]/', function ( $match ) {
+ $value = strtr( $value, [ "\0" => "\\fffd ", '\\' => '\\\\', '"' => '\\"' ] );
+ $value = preg_replace_callback( '/[\x01-\x1f\x7f]/', function ( $match ) {
return '\\' . base_convert( ord( $match[0] ), 10, 16 ) . ' ';
}, $value );
return '"' . $value . '"';
@@ -401,6 +402,7 @@ class CSSMin {
// Match these three variants separately to avoid broken urls when
// e.g. a double quoted url contains a parenthesis, or when a
// single quoted url contains a double quote, etc.
+ // FIXME: Simplify now we only support PHP 7.0.0+
// Note: PCRE doesn't support multiple capture groups with the same name by default.
// - PCRE 6.7 introduced the "J" modifier (PCRE_INFO_JCHANGED for PCRE_DUPNAMES).
// https://secure.php.net/manual/en/reference.pcre.pattern.modifiers.php
@@ -419,11 +421,11 @@ class CSSMin {
// is only supported in PHP 5.6. Use a getter method for now.
$urlRegex = '(' .
// Unquoted url
- 'url\(\s*(?P<file0>[^\'"][^\?\)]*?)(?P<query0>\?[^\)]*?|)\s*\)' .
+ 'url\(\s*(?P<file0>[^\s\'"][^\?\)]+?)(?P<query0>\?[^\)]*?|)\s*\)' .
// Single quoted url
- '|url\(\s*\'(?P<file1>[^\?\']*?)(?P<query1>\?[^\']*?|)\'\s*\)' .
+ '|url\(\s*\'(?P<file1>[^\?\']+?)(?P<query1>\?[^\']*?|)\'\s*\)' .
// Double quoted url
- '|url\(\s*"(?P<file2>[^\?"]*?)(?P<query2>\?[^"]*?|)"\s*\)' .
+ '|url\(\s*"(?P<file2>[^\?"]+?)(?P<query2>\?[^"]*?|)"\s*\)' .
')';
}
return $urlRegex;
@@ -441,6 +443,9 @@ class CSSMin {
$match['file'] = $match['file1'];
$match['query'] = $match['query1'];
} else {
+ if ( !isset( $match['file2'] ) || $match['file2'][1] === -1 ) {
+ throw new Exception( 'URL must be non-empty' );
+ }
$match['file'] = $match['file2'];
$match['query'] = $match['query2'];
}
@@ -452,6 +457,9 @@ class CSSMin {
$match['file'] = $match['file1'];
$match['query'] = $match['query1'];
} else {
+ if ( !isset( $match['file2'] ) || $match['file2'] === '' ) {
+ throw new Exception( 'URL must be non-empty' );
+ }
$match['file'] = $match['file2'];
$match['query'] = $match['query2'];
}
@@ -530,8 +538,8 @@ class CSSMin {
public static function minify( $css ) {
return trim(
str_replace(
- [ '; ', ': ', ' {', '{ ', ', ', '} ', ';}' ],
- [ ';', ':', '{', '{', ',', '}', '}' ],
+ [ '; ', ': ', ' {', '{ ', ', ', '} ', ';}', '( ', ' )', '[ ', ' ]' ],
+ [ ';', ':', '{', '{', ',', '}', '}', '(', ')', '[', ']' ],
preg_replace( [ '/\s+/', '/\/\*.*?\*\//s' ], [ ' ', '' ], $css )
)
);
diff --git a/www/wiki/includes/libs/CryptRand.php b/www/wiki/includes/libs/CryptRand.php
index 859d58b5..f7702dd3 100644
--- a/www/wiki/includes/libs/CryptRand.php
+++ b/www/wiki/includes/libs/CryptRand.php
@@ -94,9 +94,9 @@ class CryptRand {
$files[] = dirname( __DIR__ );
foreach ( $files as $file ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$stat = stat( $file );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $stat ) {
// stat() duplicates data into numeric and string keys so kill off all the numeric ones
foreach ( $stat as $k => $v ) {
@@ -259,43 +259,40 @@ class CryptRand {
}
}
- if ( strlen( $buffer ) < $bytes ) {
+ if ( strlen( $buffer ) < $bytes && function_exists( 'mcrypt_create_iv' ) ) {
// If available make use of mcrypt_create_iv URANDOM source to generate randomness
// On unix-like systems this reads from /dev/urandom but does it without any buffering
// and bypasses openbasedir restrictions, so it's preferable to reading directly
// On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate
// entropy so this is also preferable to just trying to read urandom because it may work
// on Windows systems as well.
- if ( function_exists( 'mcrypt_create_iv' ) ) {
- $rem = $bytes - strlen( $buffer );
- $iv = mcrypt_create_iv( $rem, MCRYPT_DEV_URANDOM );
- if ( $iv === false ) {
- $this->logger->debug( "mcrypt_create_iv returned false." );
- } else {
- $buffer .= $iv;
- $this->logger->debug( "mcrypt_create_iv generated " . strlen( $iv ) .
- " bytes of randomness." );
- }
+ $rem = $bytes - strlen( $buffer );
+ $iv = mcrypt_create_iv( $rem, MCRYPT_DEV_URANDOM );
+ if ( $iv === false ) {
+ $this->logger->debug( "mcrypt_create_iv returned false." );
+ } else {
+ $buffer .= $iv;
+ $this->logger->debug( "mcrypt_create_iv generated " . strlen( $iv ) .
+ " bytes of randomness." );
}
}
- if ( strlen( $buffer ) < $bytes ) {
- if ( function_exists( 'openssl_random_pseudo_bytes' ) ) {
- $rem = $bytes - strlen( $buffer );
- $openssl_bytes = openssl_random_pseudo_bytes( $rem, $openssl_strong );
- if ( $openssl_bytes === false ) {
- $this->logger->debug( "openssl_random_pseudo_bytes returned false." );
- } else {
- $buffer .= $openssl_bytes;
- $this->logger->debug( "openssl_random_pseudo_bytes generated " .
- strlen( $openssl_bytes ) . " bytes of " .
- ( $openssl_strong ? "strong" : "weak" ) . " randomness." );
- }
- if ( strlen( $buffer ) >= $bytes ) {
- // openssl tells us if the random source was strong, if some of our data was generated
- // using it use it's say on whether the randomness is strong
- $this->strong = !!$openssl_strong;
- }
+ if ( strlen( $buffer ) < $bytes && function_exists( 'openssl_random_pseudo_bytes' ) ) {
+ $rem = $bytes - strlen( $buffer );
+ $openssl_strong = false;
+ $openssl_bytes = openssl_random_pseudo_bytes( $rem, $openssl_strong );
+ if ( $openssl_bytes === false ) {
+ $this->logger->debug( "openssl_random_pseudo_bytes returned false." );
+ } else {
+ $buffer .= $openssl_bytes;
+ $this->logger->debug( "openssl_random_pseudo_bytes generated " .
+ strlen( $openssl_bytes ) . " bytes of " .
+ ( $openssl_strong ? "strong" : "weak" ) . " randomness." );
+ }
+ if ( strlen( $buffer ) >= $bytes ) {
+ // openssl tells us if the random source was strong, if some of our data was generated
+ // using it use it's say on whether the randomness is strong
+ $this->strong = !!$openssl_strong;
}
}
@@ -310,9 +307,9 @@ class CryptRand {
}
// /dev/urandom is generally considered the best possible commonly
// available random source, and is available on most *nix systems.
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$urandom = fopen( "/dev/urandom", "rb" );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
// Attempt to read all our random data from urandom
// php's fread always does buffered reads based on the stream's chunk_size
diff --git a/www/wiki/includes/libs/DeferredStringifier.php b/www/wiki/includes/libs/DeferredStringifier.php
index a6fd11a4..94704133 100644
--- a/www/wiki/includes/libs/DeferredStringifier.php
+++ b/www/wiki/includes/libs/DeferredStringifier.php
@@ -22,6 +22,7 @@
/**
* @since 1.25
+ * @deprecated since 1.31, use Message::listParam() instead
*/
class DeferredStringifier {
/** @var callable Callback used for result string generation */
diff --git a/www/wiki/includes/libs/HashRing.php b/www/wiki/includes/libs/HashRing.php
index be40965e..3b9c24d9 100644
--- a/www/wiki/includes/libs/HashRing.php
+++ b/www/wiki/includes/libs/HashRing.php
@@ -26,14 +26,14 @@
* @since 1.22
*/
class HashRing {
- /** @var Array (location => weight) */
+ /** @var array (location => weight) */
protected $sourceMap = [];
- /** @var Array (location => (start, end)) */
+ /** @var array (location => (start, end)) */
protected $ring = [];
/** @var HashRing|null */
protected $liveRing;
- /** @var Array (location => UNIX timestamp) */
+ /** @var array (location => UNIX timestamp) */
protected $ejectionExpiries = [];
/** @var int UNIX timestamp */
protected $ejectionNextExpiry = INF;
@@ -83,7 +83,7 @@ class HashRing {
* @param string $item
* @return string Location
*/
- public function getLocation( $item ) {
+ final public function getLocation( $item ) {
$locations = $this->getLocations( $item, 1 );
return $locations[0];
@@ -116,11 +116,12 @@ class HashRing {
// If more locations are requested, wrap-around and keep adding them
reset( $this->ring );
while ( count( $locations ) < $limit ) {
- list( $location, ) = each( $this->ring );
+ $location = key( $this->ring );
if ( $location === $primaryLocation ) {
break; // don't go in circles
}
$locations[] = $location;
+ next( $this->ring );
}
return $locations;
@@ -136,19 +137,6 @@ class HashRing {
}
/**
- * Get a new hash ring with a location removed from the ring
- *
- * @param string $location
- * @return HashRing|bool Returns false if no non-zero weighted spots are left
- */
- public function newWithoutLocation( $location ) {
- $map = $this->sourceMap;
- unset( $map[$location] );
-
- return count( $map ) ? new self( $map ) : false;
- }
-
- /**
* Remove a location from the "live" hash ring
*
* @param string $location
@@ -173,7 +161,7 @@ class HashRing {
* @return HashRing
* @throws UnexpectedValueException
*/
- public function getLiveRing() {
+ protected function getLiveRing() {
$now = time();
if ( $this->liveRing === null || $this->ejectionNextExpiry <= $now ) {
$this->ejectionExpiries = array_filter(
diff --git a/www/wiki/includes/libs/HtmlArmor.php b/www/wiki/includes/libs/HtmlArmor.php
index 1c141ab0..6e6ad7c9 100644
--- a/www/wiki/includes/libs/HtmlArmor.php
+++ b/www/wiki/includes/libs/HtmlArmor.php
@@ -16,7 +16,7 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL-2.0+
+ * @license GPL-2.0-or-later
* @author Kunal Mehta <legoktm@member.fsf.org>
*/
diff --git a/www/wiki/includes/libs/IEContentAnalyzer.php b/www/wiki/includes/libs/IEContentAnalyzer.php
deleted file mode 100644
index 0d1e527b..00000000
--- a/www/wiki/includes/libs/IEContentAnalyzer.php
+++ /dev/null
@@ -1,851 +0,0 @@
-<?php
-/**
- * Simulation of Microsoft Internet Explorer's MIME type detection algorithm.
- *
- * @file
- * @todo Define the exact license of this file.
- */
-
-/**
- * This class simulates Microsoft Internet Explorer's terribly broken and
- * insecure MIME type detection algorithm. It can be used to check web uploads
- * with an apparently safe type, to see if IE will reinterpret them to produce
- * something dangerous.
- *
- * It is full of bugs and strange design choices should not under any
- * circumstances be used to determine a MIME type to present to a user or
- * client. (Apple Safari developers, this means you too.)
- *
- * This class is based on a disassembly of IE 5.0, 6.0 and 7.0. Although I have
- * attempted to ensure that this code works in exactly the same way as Internet
- * Explorer, it does not share any source code, or creative choices such as
- * variable names, thus I (Tim Starling) claim copyright on it.
- *
- * It may be redistributed without restriction. To aid reuse, this class does
- * not depend on any MediaWiki module.
- */
-class IEContentAnalyzer {
- /**
- * Relevant data taken from the type table in IE 5
- */
- protected $baseTypeTable = [
- 'ambiguous' /*1*/ => [
- 'text/plain',
- 'application/octet-stream',
- 'application/x-netcdf', // [sic]
- ],
- 'text' /*3*/ => [
- 'text/richtext', 'image/x-bitmap', 'application/postscript', 'application/base64',
- 'application/macbinhex40', 'application/x-cdf', 'text/scriptlet'
- ],
- 'binary' /*4*/ => [
- 'application/pdf', 'audio/x-aiff', 'audio/basic', 'audio/wav', 'image/gif',
- 'image/pjpeg', 'image/jpeg', 'image/tiff', 'image/x-png', 'image/png', 'image/bmp',
- 'image/x-jg', 'image/x-art', 'image/x-emf', 'image/x-wmf', 'video/avi',
- 'video/x-msvideo', 'video/mpeg', 'application/x-compressed',
- 'application/x-zip-compressed', 'application/x-gzip-compressed', 'application/java',
- 'application/x-msdownload'
- ],
- 'html' /*5*/ => [ 'text/html' ],
- ];
-
- /**
- * Changes to the type table in later versions of IE
- */
- protected $addedTypes = [
- 'ie07' => [
- 'text' => [ 'text/xml', 'application/xml' ]
- ],
- ];
-
- /**
- * An approximation of the "Content Type" values in HKEY_CLASSES_ROOT in a
- * typical Windows installation.
- *
- * Used for extension to MIME type mapping if detection fails.
- */
- protected $registry = [
- '.323' => 'text/h323',
- '.3g2' => 'video/3gpp2',
- '.3gp' => 'video/3gpp',
- '.3gp2' => 'video/3gpp2',
- '.3gpp' => 'video/3gpp',
- '.aac' => 'audio/aac',
- '.ac3' => 'audio/ac3',
- '.accda' => 'application/msaccess',
- '.accdb' => 'application/msaccess',
- '.accdc' => 'application/msaccess',
- '.accde' => 'application/msaccess',
- '.accdr' => 'application/msaccess',
- '.accdt' => 'application/msaccess',
- '.ade' => 'application/msaccess',
- '.adp' => 'application/msaccess',
- '.adts' => 'audio/aac',
- '.ai' => 'application/postscript',
- '.aif' => 'audio/aiff',
- '.aifc' => 'audio/aiff',
- '.aiff' => 'audio/aiff',
- '.amc' => 'application/x-mpeg',
- '.application' => 'application/x-ms-application',
- '.asf' => 'video/x-ms-asf',
- '.asx' => 'video/x-ms-asf',
- '.au' => 'audio/basic',
- '.avi' => 'video/avi',
- '.bmp' => 'image/bmp',
- '.caf' => 'audio/x-caf',
- '.cat' => 'application/vnd.ms-pki.seccat',
- '.cbo' => 'application/sha',
- '.cdda' => 'audio/aiff',
- '.cer' => 'application/x-x509-ca-cert',
- '.conf' => 'text/plain',
- '.crl' => 'application/pkix-crl',
- '.crt' => 'application/x-x509-ca-cert',
- '.css' => 'text/css',
- '.csv' => 'application/vnd.ms-excel',
- '.der' => 'application/x-x509-ca-cert',
- '.dib' => 'image/bmp',
- '.dif' => 'video/x-dv',
- '.dll' => 'application/x-msdownload',
- '.doc' => 'application/msword',
- '.docm' => 'application/vnd.ms-word.document.macroEnabled.12',
- '.docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- '.dot' => 'application/msword',
- '.dotm' => 'application/vnd.ms-word.template.macroEnabled.12',
- '.dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
- '.dv' => 'video/x-dv',
- '.dwfx' => 'model/vnd.dwfx+xps',
- '.edn' => 'application/vnd.adobe.edn',
- '.eml' => 'message/rfc822',
- '.eps' => 'application/postscript',
- '.etd' => 'application/x-ebx',
- '.exe' => 'application/x-msdownload',
- '.fdf' => 'application/vnd.fdf',
- '.fif' => 'application/fractals',
- '.gif' => 'image/gif',
- '.gsm' => 'audio/x-gsm',
- '.hqx' => 'application/mac-binhex40',
- '.hta' => 'application/hta',
- '.htc' => 'text/x-component',
- '.htm' => 'text/html',
- '.html' => 'text/html',
- '.htt' => 'text/webviewhtml',
- '.hxa' => 'application/xml',
- '.hxc' => 'application/xml',
- '.hxd' => 'application/octet-stream',
- '.hxe' => 'application/xml',
- '.hxf' => 'application/xml',
- '.hxh' => 'application/octet-stream',
- '.hxi' => 'application/octet-stream',
- '.hxk' => 'application/xml',
- '.hxq' => 'application/octet-stream',
- '.hxr' => 'application/octet-stream',
- '.hxs' => 'application/octet-stream',
- '.hxt' => 'application/xml',
- '.hxv' => 'application/xml',
- '.hxw' => 'application/octet-stream',
- '.ico' => 'image/x-icon',
- '.iii' => 'application/x-iphone',
- '.ins' => 'application/x-internet-signup',
- '.iqy' => 'text/x-ms-iqy',
- '.isp' => 'application/x-internet-signup',
- '.jfif' => 'image/jpeg',
- '.jnlp' => 'application/x-java-jnlp-file',
- '.jpe' => 'image/jpeg',
- '.jpeg' => 'image/jpeg',
- '.jpg' => 'image/jpeg',
- '.jtx' => 'application/x-jtx+xps',
- '.latex' => 'application/x-latex',
- '.log' => 'text/plain',
- '.m1v' => 'video/mpeg',
- '.m2v' => 'video/mpeg',
- '.m3u' => 'audio/x-mpegurl',
- '.mac' => 'image/x-macpaint',
- '.man' => 'application/x-troff-man',
- '.mda' => 'application/msaccess',
- '.mdb' => 'application/msaccess',
- '.mde' => 'application/msaccess',
- '.mfp' => 'application/x-shockwave-flash',
- '.mht' => 'message/rfc822',
- '.mhtml' => 'message/rfc822',
- '.mid' => 'audio/mid',
- '.midi' => 'audio/mid',
- '.mod' => 'video/mpeg',
- '.mov' => 'video/quicktime',
- '.mp2' => 'video/mpeg',
- '.mp2v' => 'video/mpeg',
- '.mp3' => 'audio/mpeg',
- '.mp4' => 'video/mp4',
- '.mpa' => 'video/mpeg',
- '.mpe' => 'video/mpeg',
- '.mpeg' => 'video/mpeg',
- '.mpf' => 'application/vnd.ms-mediapackage',
- '.mpg' => 'video/mpeg',
- '.mpv2' => 'video/mpeg',
- '.mqv' => 'video/quicktime',
- '.NMW' => 'application/nmwb',
- '.nws' => 'message/rfc822',
- '.odc' => 'text/x-ms-odc',
- '.ols' => 'application/vnd.ms-publisher',
- '.p10' => 'application/pkcs10',
- '.p12' => 'application/x-pkcs12',
- '.p7b' => 'application/x-pkcs7-certificates',
- '.p7c' => 'application/pkcs7-mime',
- '.p7m' => 'application/pkcs7-mime',
- '.p7r' => 'application/x-pkcs7-certreqresp',
- '.p7s' => 'application/pkcs7-signature',
- '.pct' => 'image/pict',
- '.pdf' => 'application/pdf',
- '.pdx' => 'application/vnd.adobe.pdx',
- '.pfx' => 'application/x-pkcs12',
- '.pic' => 'image/pict',
- '.pict' => 'image/pict',
- '.pinstall' => 'application/x-picasa-detect',
- '.pko' => 'application/vnd.ms-pki.pko',
- '.png' => 'image/png',
- '.pnt' => 'image/x-macpaint',
- '.pntg' => 'image/x-macpaint',
- '.pot' => 'application/vnd.ms-powerpoint',
- '.potm' => 'application/vnd.ms-powerpoint.template.macroEnabled.12',
- '.potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
- '.ppa' => 'application/vnd.ms-powerpoint',
- '.ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
- '.pps' => 'application/vnd.ms-powerpoint',
- '.ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
- '.ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
- '.ppt' => 'application/vnd.ms-powerpoint',
- '.pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
- '.pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
- '.prf' => 'application/pics-rules',
- '.ps' => 'application/postscript',
- '.pub' => 'application/vnd.ms-publisher',
- '.pwz' => 'application/vnd.ms-powerpoint',
- '.py' => 'text/plain',
- '.pyw' => 'text/plain',
- '.qht' => 'text/x-html-insertion',
- '.qhtm' => 'text/x-html-insertion',
- '.qt' => 'video/quicktime',
- '.qti' => 'image/x-quicktime',
- '.qtif' => 'image/x-quicktime',
- '.qtl' => 'application/x-quicktimeplayer',
- '.rat' => 'application/rat-file',
- '.rmf' => 'application/vnd.adobe.rmf',
- '.rmi' => 'audio/mid',
- '.rqy' => 'text/x-ms-rqy',
- '.rtf' => 'application/msword',
- '.sct' => 'text/scriptlet',
- '.sd2' => 'audio/x-sd2',
- '.sdp' => 'application/sdp',
- '.shtml' => 'text/html',
- '.sit' => 'application/x-stuffit',
- '.sldm' => 'application/vnd.ms-powerpoint.slide.macroEnabled.12',
- '.sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
- '.slk' => 'application/vnd.ms-excel',
- '.snd' => 'audio/basic',
- '.so' => 'application/x-apachemodule',
- '.sol' => 'text/plain',
- '.sor' => 'text/plain',
- '.spc' => 'application/x-pkcs7-certificates',
- '.spl' => 'application/futuresplash',
- '.sst' => 'application/vnd.ms-pki.certstore',
- '.stl' => 'application/vnd.ms-pki.stl',
- '.swf' => 'application/x-shockwave-flash',
- '.thmx' => 'application/vnd.ms-officetheme',
- '.tif' => 'image/tiff',
- '.tiff' => 'image/tiff',
- '.txt' => 'text/plain',
- '.uls' => 'text/iuls',
- '.vcf' => 'text/x-vcard',
- '.vdx' => 'application/vnd.ms-visio.viewer',
- '.vsd' => 'application/vnd.ms-visio.viewer',
- '.vss' => 'application/vnd.ms-visio.viewer',
- '.vst' => 'application/vnd.ms-visio.viewer',
- '.vsx' => 'application/vnd.ms-visio.viewer',
- '.vtx' => 'application/vnd.ms-visio.viewer',
- '.wav' => 'audio/wav',
- '.wax' => 'audio/x-ms-wax',
- '.wbk' => 'application/msword',
- '.wdp' => 'image/vnd.ms-photo',
- '.wiz' => 'application/msword',
- '.wm' => 'video/x-ms-wm',
- '.wma' => 'audio/x-ms-wma',
- '.wmd' => 'application/x-ms-wmd',
- '.wmv' => 'video/x-ms-wmv',
- '.wmx' => 'video/x-ms-wmx',
- '.wmz' => 'application/x-ms-wmz',
- '.wpl' => 'application/vnd.ms-wpl',
- '.wsc' => 'text/scriptlet',
- '.wvx' => 'video/x-ms-wvx',
- '.xaml' => 'application/xaml+xml',
- '.xbap' => 'application/x-ms-xbap',
- '.xdp' => 'application/vnd.adobe.xdp+xml',
- '.xfdf' => 'application/vnd.adobe.xfdf',
- '.xht' => 'application/xhtml+xml',
- '.xhtml' => 'application/xhtml+xml',
- '.xla' => 'application/vnd.ms-excel',
- '.xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
- '.xlk' => 'application/vnd.ms-excel',
- '.xll' => 'application/vnd.ms-excel',
- '.xlm' => 'application/vnd.ms-excel',
- '.xls' => 'application/vnd.ms-excel',
- '.xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
- '.xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
- '.xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
- '.xlt' => 'application/vnd.ms-excel',
- '.xltm' => 'application/vnd.ms-excel.template.macroEnabled.12',
- '.xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
- '.xlw' => 'application/vnd.ms-excel',
- '.xml' => 'text/xml',
- '.xps' => 'application/vnd.ms-xpsdocument',
- '.xsl' => 'text/xml',
- ];
-
- /**
- * IE versions which have been analysed to bring you this class, and for
- * which some substantive difference exists. These will appear as keys
- * in the return value of getRealMimesFromData(). The names are chosen to sort correctly.
- */
- protected $versions = [ 'ie05', 'ie06', 'ie07', 'ie07.strict', 'ie07.nohtml' ];
-
- /**
- * Type table with versions expanded
- */
- protected $typeTable = [];
-
- /** constructor */
- function __construct() {
- // Construct versioned type arrays from the base type array plus additions
- $types = $this->baseTypeTable;
- foreach ( $this->versions as $version ) {
- if ( isset( $this->addedTypes[$version] ) ) {
- foreach ( $this->addedTypes[$version] as $format => $addedTypes ) {
- $types[$format] = array_merge( $types[$format], $addedTypes );
- }
- }
- $this->typeTable[$version] = $types;
- }
- }
-
- /**
- * Get the MIME types from getMimesFromData(), but convert the result from IE's
- * idiosyncratic private types into something other apps will understand.
- *
- * @param string $fileName the file name (unused at present)
- * @param string $chunk the first 256 bytes of the file
- * @param string $proposed the MIME type proposed by the server
- *
- * @return Array: map of IE version to detected MIME type
- */
- public function getRealMimesFromData( $fileName, $chunk, $proposed ) {
- $types = $this->getMimesFromData( $fileName, $chunk, $proposed );
- $types = array_map( [ $this, 'translateMimeType' ], $types );
- return $types;
- }
-
- /**
- * Translate a MIME type from IE's idiosyncratic private types into
- * more commonly understood type strings
- * @param $type
- * @return string
- */
- public function translateMimeType( $type ) {
- static $table = [
- 'image/pjpeg' => 'image/jpeg',
- 'image/x-png' => 'image/png',
- 'image/x-wmf' => 'application/x-msmetafile',
- 'image/bmp' => 'image/x-bmp',
- 'application/x-zip-compressed' => 'application/zip',
- 'application/x-compressed' => 'application/x-compress',
- 'application/x-gzip-compressed' => 'application/x-gzip',
- 'audio/mid' => 'audio/midi',
- ];
- if ( isset( $table[$type] ) ) {
- $type = $table[$type];
- }
- return $type;
- }
-
- /**
- * Get the untranslated MIME types for all known versions
- *
- * @param string $fileName the file name (unused at present)
- * @param string $chunk the first 256 bytes of the file
- * @param string $proposed the MIME type proposed by the server
- *
- * @return Array: map of IE version to detected MIME type
- */
- public function getMimesFromData( $fileName, $chunk, $proposed ) {
- $types = [];
- foreach ( $this->versions as $version ) {
- $types[$version] = $this->getMimeTypeForVersion( $version, $fileName, $chunk, $proposed );
- }
- return $types;
- }
-
- /**
- * Get the MIME type for a given named version
- * @param $version
- * @param $fileName
- * @param $chunk
- * @param $proposed
- * @return bool|string
- */
- protected function getMimeTypeForVersion( $version, $fileName, $chunk, $proposed ) {
- // Strip text after a semicolon
- $semiPos = strpos( $proposed, ';' );
- if ( $semiPos !== false ) {
- $proposed = substr( $proposed, 0, $semiPos );
- }
-
- $proposedFormat = $this->getDataFormat( $version, $proposed );
- if ( $proposedFormat == 'unknown'
- && $proposed != 'multipart/mixed'
- && $proposed != 'multipart/x-mixed-replace' )
- {
- return $proposed;
- }
- if ( strval( $chunk ) === '' ) {
- return $proposed;
- }
-
- // Truncate chunk at 255 bytes
- $chunk = substr( $chunk, 0, 255 );
-
- // IE does the Check*Headers() calls last, and instead does the following image
- // type checks by directly looking for the magic numbers. What I do here should
- // have the same effect since the magic number checks are identical in both cases.
- $result = $this->sampleData( $version, $chunk );
- $sampleFound = $result['found'];
- $counters = $result['counters'];
- $binaryType = $this->checkBinaryHeaders( $version, $chunk );
- $textType = $this->checkTextHeaders( $version, $chunk );
-
- if ( $proposed == 'text/html' && isset( $sampleFound['html'] ) ) {
- return 'text/html';
- }
- if ( $proposed == 'image/gif' && $binaryType == 'image/gif' ) {
- return 'image/gif';
- }
- if ( ( $proposed == 'image/pjpeg' || $proposed == 'image/jpeg' )
- && $binaryType == 'image/pjpeg' )
- {
- return $proposed;
- }
- // PNG check added in IE 7
- if ( $version >= 'ie07'
- && ( $proposed == 'image/x-png' || $proposed == 'image/png' )
- && $binaryType == 'image/x-png' )
- {
- return $proposed;
- }
-
- // CDF was removed in IE 7 so it won't be in $sampleFound for later versions
- if ( isset( $sampleFound['cdf'] ) ) {
- return 'application/x-cdf';
- }
-
- // RSS and Atom were added in IE 7 so they won't be in $sampleFound for
- // previous versions
- if ( isset( $sampleFound['rss'] ) ) {
- return 'application/rss+xml';
- }
- if ( isset( $sampleFound['rdf-tag'] )
- && isset( $sampleFound['rdf-url'] )
- && isset( $sampleFound['rdf-purl'] ) )
- {
- return 'application/rss+xml';
- }
- if ( isset( $sampleFound['atom'] ) ) {
- return 'application/atom+xml';
- }
-
- if ( isset( $sampleFound['xml'] ) ) {
- // TODO: I'm not sure under what circumstances this flag is enabled
- if ( strpos( $version, 'strict' ) !== false ) {
- if ( $proposed == 'text/html' || $proposed == 'text/xml' ) {
- return 'text/xml';
- }
- } else {
- return 'text/xml';
- }
- }
- if ( isset( $sampleFound['html'] ) ) {
- // TODO: I'm not sure under what circumstances this flag is enabled
- if ( strpos( $version, 'nohtml' ) !== false ) {
- if ( $proposed == 'text/plain' ) {
- return 'text/html';
- }
- } else {
- return 'text/html';
- }
- }
- if ( isset( $sampleFound['xbm'] ) ) {
- return 'image/x-bitmap';
- }
- if ( isset( $sampleFound['binhex'] ) ) {
- return 'application/macbinhex40';
- }
- if ( isset( $sampleFound['scriptlet'] ) ) {
- if ( strpos( $version, 'strict' ) !== false ) {
- if ( $proposed == 'text/plain' || $proposed == 'text/scriptlet' ) {
- return 'text/scriptlet';
- }
- } else {
- return 'text/scriptlet';
- }
- }
-
- // Freaky heuristics to determine if the data is text or binary
- // The heuristic is of course broken for non-ASCII text
- if ( $counters['ctrl'] != 0 && ( $counters['ff'] + $counters['low'] )
- < ( $counters['ctrl'] + $counters['high'] ) * 16 )
- {
- $kindOfBinary = true;
- $type = $binaryType ? $binaryType : $textType;
- if ( $type === false ) {
- $type = 'application/octet-stream';
- }
- } else {
- $kindOfBinary = false;
- $type = $textType ? $textType : $binaryType;
- if ( $type === false ) {
- $type = 'text/plain';
- }
- }
-
- // Check if the output format is ambiguous
- // This generally means that detection failed, real types aren't ambiguous
- $detectedFormat = $this->getDataFormat( $version, $type );
- if ( $detectedFormat != 'ambiguous' ) {
- return $type;
- }
-
- if ( $proposedFormat != 'ambiguous' ) {
- // FormatAgreesWithData()
- if ( $proposedFormat == 'text' && !$kindOfBinary ) {
- return $proposed;
- }
- if ( $proposedFormat == 'binary' && $kindOfBinary ) {
- return $proposed;
- }
- if ( $proposedFormat == 'html' ) {
- return $proposed;
- }
- }
-
- // Find a MIME type by searching the registry for the file extension.
- $dotPos = strrpos( $fileName, '.' );
- if ( $dotPos === false ) {
- return $type;
- }
- $ext = substr( $fileName, $dotPos );
- if ( isset( $this->registry[$ext] ) ) {
- return $this->registry[$ext];
- }
-
- // TODO: If the extension has an application registered to it, IE will return
- // application/octet-stream. We'll skip that, so we could erroneously
- // return text/plain or application/x-netcdf where application/octet-stream
- // would be correct.
-
- return $type;
- }
-
- /**
- * Check for text headers at the start of the chunk
- * Confirmed same in 5 and 7.
- * @param $version
- * @param $chunk
- * @return bool|string
- */
- private function checkTextHeaders( $version, $chunk ) {
- $chunk2 = substr( $chunk, 0, 2 );
- $chunk4 = substr( $chunk, 0, 4 );
- $chunk5 = substr( $chunk, 0, 5 );
- if ( $chunk4 == '%PDF' ) {
- return 'application/pdf';
- }
- if ( $chunk2 == '%!' ) {
- return 'application/postscript';
- }
- if ( $chunk5 == '{\\rtf' ) {
- return 'text/richtext';
- }
- if ( $chunk5 == 'begin' ) {
- return 'application/base64';
- }
- return false;
- }
-
- /**
- * Check for binary headers at the start of the chunk
- * Confirmed same in 5 and 7.
- * @param $version
- * @param $chunk
- * @return bool|string
- */
- private function checkBinaryHeaders( $version, $chunk ) {
- $chunk2 = substr( $chunk, 0, 2 );
- $chunk3 = substr( $chunk, 0, 3 );
- $chunk4 = substr( $chunk, 0, 4 );
- $chunk5 = substr( $chunk, 0, 5 );
- $chunk5uc = strtoupper( $chunk5 );
- $chunk8 = substr( $chunk, 0, 8 );
- if ( $chunk5uc == 'GIF87' || $chunk5uc == 'GIF89' ) {
- return 'image/gif';
- }
- if ( $chunk2 == "\xff\xd8" ) {
- return 'image/pjpeg'; // actually plain JPEG but this is what IE returns
- }
-
- if ( $chunk2 == 'BM'
- && substr( $chunk, 6, 2 ) == "\000\000"
- && substr( $chunk, 8, 2 ) == "\000\000" )
- {
- return 'image/bmp'; // another non-standard MIME
- }
- if ( $chunk4 == 'RIFF'
- && substr( $chunk, 8, 4 ) == 'WAVE' )
- {
- return 'audio/wav';
- }
- // These were integer literals in IE
- // Perhaps the author was not sure what the target endianness was
- if ( $chunk4 == ".sd\000"
- || $chunk4 == ".snd"
- || $chunk4 == "\000ds."
- || $chunk4 == "dns." )
- {
- return 'audio/basic';
- }
- if ( $chunk3 == "MM\000" ) {
- return 'image/tiff';
- }
- if ( $chunk2 == 'MZ' ) {
- return 'application/x-msdownload';
- }
- if ( $chunk8 == "\x89PNG\x0d\x0a\x1a\x0a" ) {
- return 'image/x-png'; // [sic]
- }
- if ( strlen( $chunk ) >= 5 ) {
- $byte2 = ord( $chunk[2] );
- $byte4 = ord( $chunk[4] );
- if ( $byte2 >= 3 && $byte2 <= 31 && $byte4 == 0 && $chunk2 == 'JG' ) {
- return 'image/x-jg';
- }
- }
- // More endian confusion?
- if ( $chunk4 == 'MROF' ) {
- return 'audio/x-aiff';
- }
- $chunk4_8 = substr( $chunk, 8, 4 );
- if ( $chunk4 == 'FORM' && ( $chunk4_8 == 'AIFF' || $chunk4_8 == 'AIFC' ) ) {
- return 'audio/x-aiff';
- }
- if ( $chunk4 == 'RIFF' && $chunk4_8 == 'AVI ' ) {
- return 'video/avi';
- }
- if ( $chunk4 == "\x00\x00\x01\xb3" || $chunk4 == "\x00\x00\x01\xba" ) {
- return 'video/mpeg';
- }
- if ( $chunk4 == "\001\000\000\000"
- && substr( $chunk, 40, 4 ) == ' EMF' )
- {
- return 'image/x-emf';
- }
- if ( $chunk4 == "\xd7\xcd\xc6\x9a" ) {
- return 'image/x-wmf';
- }
- if ( $chunk4 == "\xca\xfe\xba\xbe" ) {
- return 'application/java';
- }
- if ( $chunk2 == 'PK' ) {
- return 'application/x-zip-compressed';
- }
- if ( $chunk2 == "\x1f\x9d" ) {
- return 'application/x-compressed';
- }
- if ( $chunk2 == "\x1f\x8b" ) {
- return 'application/x-gzip-compressed';
- }
- // Skip redundant check for ZIP
- if ( $chunk5 == "MThd\000" ) {
- return 'audio/mid';
- }
- if ( $chunk4 == '%PDF' ) {
- return 'application/pdf';
- }
- return false;
- }
-
- /**
- * Do heuristic checks on the bulk of the data sample.
- * Search for HTML tags.
- * @param $version
- * @param $chunk
- * @return array
- */
- protected function sampleData( $version, $chunk ) {
- $found = [];
- $counters = [
- 'ctrl' => 0,
- 'high' => 0,
- 'low' => 0,
- 'lf' => 0,
- 'cr' => 0,
- 'ff' => 0
- ];
- $htmlTags = [
- 'html',
- 'head',
- 'title',
- 'body',
- 'script',
- 'a href',
- 'pre',
- 'img',
- 'plaintext',
- 'table'
- ];
- $rdfUrl = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
- $rdfPurl = 'http://purl.org/rss/1.0/';
- $xbmMagic1 = '#define';
- $xbmMagic2 = '_width';
- $xbmMagic3 = '_bits';
- $binhexMagic = 'converted with BinHex';
- $chunkLength = strlen( $chunk );
-
- for ( $offset = 0; $offset < $chunkLength; $offset++ ) {
- $curChar = $chunk[$offset];
- if ( $curChar == "\x0a" ) {
- $counters['lf']++;
- continue;
- } elseif ( $curChar == "\x0d" ) {
- $counters['cr']++;
- continue;
- } elseif ( $curChar == "\x0c" ) {
- $counters['ff']++;
- continue;
- } elseif ( $curChar == "\t" ) {
- $counters['low']++;
- continue;
- } elseif ( ord( $curChar ) < 32 ) {
- $counters['ctrl']++;
- continue;
- } elseif ( ord( $curChar ) >= 128 ) {
- $counters['high']++;
- continue;
- }
-
- $counters['low']++;
- if ( $curChar == '<' ) {
- // XML
- $remainder = substr( $chunk, $offset + 1 );
- if ( !strncasecmp( $remainder, '?XML', 4 ) ) {
- $nextChar = substr( $chunk, $offset + 5, 1 );
- if ( $nextChar == ':' || $nextChar == ' ' || $nextChar == "\t" ) {
- $found['xml'] = true;
- }
- }
- // Scriptlet (JSP)
- if ( !strncasecmp( $remainder, 'SCRIPTLET', 9 ) ) {
- $found['scriptlet'] = true;
- break;
- }
- // HTML
- foreach ( $htmlTags as $tag ) {
- if ( !strncasecmp( $remainder, $tag, strlen( $tag ) ) ) {
- $found['html'] = true;
- }
- }
- // Skip broken check for additional tags (HR etc.)
-
- // CHANNEL replaced by RSS, RDF and FEED in IE 7
- if ( $version < 'ie07' ) {
- if ( !strncasecmp( $remainder, 'CHANNEL', 7 ) ) {
- $found['cdf'] = true;
- }
- } else {
- // RSS
- if ( !strncasecmp( $remainder, 'RSS', 3 ) ) {
- $found['rss'] = true;
- break; // return from SampleData
- }
- if ( !strncasecmp( $remainder, 'rdf:RDF', 7 ) ) {
- $found['rdf-tag'] = true;
- // no break
- }
- if ( !strncasecmp( $remainder, 'FEED', 4 ) ) {
- $found['atom'] = true;
- break;
- }
- }
- continue;
- }
- // Skip broken check for -->
-
- // RSS URL checks
- // For some reason both URLs must appear before it is recognised
- $remainder = substr( $chunk, $offset );
- if ( !strncasecmp( $remainder, $rdfUrl, strlen( $rdfUrl ) ) ) {
- $found['rdf-url'] = true;
- if ( isset( $found['rdf-tag'] )
- && isset( $found['rdf-purl'] ) ) // [sic]
- {
- break;
- }
- continue;
- }
-
- if ( !strncasecmp( $remainder, $rdfPurl, strlen( $rdfPurl ) ) ) {
- if ( isset( $found['rdf-tag'] )
- && isset( $found['rdf-url'] ) ) // [sic]
- {
- break;
- }
- continue;
- }
-
- // XBM checks
- if ( !strncasecmp( $remainder, $xbmMagic1, strlen( $xbmMagic1 ) ) ) {
- $found['xbm1'] = true;
- continue;
- }
- if ( $curChar == '_' ) {
- if ( isset( $found['xbm2'] ) ) {
- if ( !strncasecmp( $remainder, $xbmMagic3, strlen( $xbmMagic3 ) ) ) {
- $found['xbm'] = true;
- break;
- }
- } elseif ( isset( $found['xbm1'] ) ) {
- if ( !strncasecmp( $remainder, $xbmMagic2, strlen( $xbmMagic2 ) ) ) {
- $found['xbm2'] = true;
- }
- }
- }
-
- // BinHex
- if ( !strncmp( $remainder, $binhexMagic, strlen( $binhexMagic ) ) ) {
- $found['binhex'] = true;
- }
- }
- return [ 'found' => $found, 'counters' => $counters ];
- }
-
- /**
- * @param $version
- * @param $type
- * @return int|string
- */
- protected function getDataFormat( $version, $type ) {
- $types = $this->typeTable[$version];
- if ( $type == '(null)' || strval( $type ) === '' ) {
- return 'ambiguous';
- }
- foreach ( $types as $format => $list ) {
- if ( in_array( $type, $list ) ) {
- return $format;
- }
- }
- return 'unknown';
- }
-}
diff --git a/www/wiki/includes/libs/IEUrlExtension.php b/www/wiki/includes/libs/IEUrlExtension.php
index 2d1c58b6..0d969fb2 100644
--- a/www/wiki/includes/libs/IEUrlExtension.php
+++ b/www/wiki/includes/libs/IEUrlExtension.php
@@ -186,7 +186,7 @@ class IEUrlExtension {
* - if we find a possible extension followed by a dot or another illegal
* character, we ignore it and continue searching
*
- * @param string $url URL
+ * @param string $url
* @return mixed Detected extension (string), or false if none found
*/
public static function findIE6Extension( $url ) {
diff --git a/www/wiki/includes/libs/IP.php b/www/wiki/includes/libs/IP.php
index 1c48f49d..f95bb1eb 100644
--- a/www/wiki/includes/libs/IP.php
+++ b/www/wiki/includes/libs/IP.php
@@ -21,7 +21,7 @@
* @author Antoine Musso "<hashar at free dot fr>"
*/
-use IPSet\IPSet;
+use Wikimedia\IPSet;
// Some regex definition to "play" with IP address and IP address ranges
diff --git a/www/wiki/includes/libs/JavaScriptMinifier.php b/www/wiki/includes/libs/JavaScriptMinifier.php
index 141a5153..5ecfc7cc 100644
--- a/www/wiki/includes/libs/JavaScriptMinifier.php
+++ b/www/wiki/includes/libs/JavaScriptMinifier.php
@@ -1,5 +1,4 @@
<?php
-// @codingStandardsIgnoreFile File external to MediaWiki. Ignore coding conventions checks.
/**
* JavaScript Minifier
*
@@ -18,7 +17,6 @@
*/
class JavaScriptMinifier {
- /* Class constants */
/* Parsing states.
* The state machine is only necessary to decide whether to parse a slash as division
* operator or as regexp literal.
@@ -64,25 +62,24 @@ class JavaScriptMinifier {
// Sanity limit to avoid excessive memory usage
const STACK_LIMIT = 1000;
- /* Static functions */
+ /**
+ * NOTE: This isn't a strict maximum. Longer lines will be produced when
+ * literals (e.g. quoted strings) longer than this are encountered
+ * or when required to guard against semicolon insertion.
+ */
+ const MAX_LINE_LENGTH = 1000;
/**
* Returns minified JavaScript code.
*
- * NOTE: $maxLineLength isn't a strict maximum. Longer lines will be produced when
- * literals (e.g. quoted strings) longer than $maxLineLength are encountered
- * or when required to guard against semicolon insertion.
- *
* @param string $s JavaScript code to minify
- * @param bool $statementsOnOwnLine Whether to put each statement on its own line
- * @param int $maxLineLength Maximum length of a single line, or -1 for no maximum.
* @return String Minified code
*/
- public static function minify( $s, $statementsOnOwnLine = false, $maxLineLength = 1000 ) {
+ public static function minify( $s ) {
// First we declare a few tables that contain our parsing rules
// $opChars : characters, which can be combined without whitespace in between them
- $opChars = array(
+ $opChars = [
'!' => true,
'"' => true,
'%' => true,
@@ -109,10 +106,10 @@ class JavaScriptMinifier {
'|' => true,
'}' => true,
'~' => true
- );
+ ];
// $tokenTypes : maps keywords and operators to their corresponding token type
- $tokenTypes = array(
+ $tokenTypes = [
'!' => self::TYPE_UN_OP,
'~' => self::TYPE_UN_OP,
'delete' => self::TYPE_UN_OP,
@@ -184,13 +181,13 @@ class JavaScriptMinifier {
'try' => self::TYPE_DO,
'var' => self::TYPE_DO,
'function' => self::TYPE_FUNC
- );
+ ];
// $goto : This is the main table for our state machine. For every state/token pair
// the following state is defined. When no rule exists for a given pair,
// the state is left unchanged.
- $goto = array(
- self::STATEMENT => array(
+ $goto = [
+ self::STATEMENT => [
self::TYPE_UN_OP => self::EXPRESSION,
self::TYPE_INCR_OP => self::EXPRESSION,
self::TYPE_ADD_OP => self::EXPRESSION,
@@ -199,29 +196,29 @@ class JavaScriptMinifier {
self::TYPE_IF => self::CONDITION,
self::TYPE_FUNC => self::CONDITION,
self::TYPE_LITERAL => self::EXPRESSION_OP
- ),
- self::CONDITION => array(
+ ],
+ self::CONDITION => [
self::TYPE_PAREN_OPEN => self::PAREN_EXPRESSION
- ),
- self::PROPERTY_ASSIGNMENT => array(
+ ],
+ self::PROPERTY_ASSIGNMENT => [
self::TYPE_COLON => self::PROPERTY_EXPRESSION,
self::TYPE_BRACE_OPEN => self::STATEMENT
- ),
- self::EXPRESSION => array(
+ ],
+ self::EXPRESSION => [
self::TYPE_SEMICOLON => self::STATEMENT,
self::TYPE_BRACE_OPEN => self::PROPERTY_ASSIGNMENT,
self::TYPE_PAREN_OPEN => self::PAREN_EXPRESSION,
self::TYPE_FUNC => self::EXPRESSION_FUNC,
self::TYPE_LITERAL => self::EXPRESSION_OP
- ),
- self::EXPRESSION_NO_NL => array(
+ ],
+ self::EXPRESSION_NO_NL => [
self::TYPE_SEMICOLON => self::STATEMENT,
self::TYPE_BRACE_OPEN => self::PROPERTY_ASSIGNMENT,
self::TYPE_PAREN_OPEN => self::PAREN_EXPRESSION,
self::TYPE_FUNC => self::EXPRESSION_FUNC,
self::TYPE_LITERAL => self::EXPRESSION_OP
- ),
- self::EXPRESSION_OP => array(
+ ],
+ self::EXPRESSION_OP => [
self::TYPE_BIN_OP => self::EXPRESSION,
self::TYPE_ADD_OP => self::EXPRESSION,
self::TYPE_HOOK => self::EXPRESSION_TERNARY,
@@ -229,33 +226,33 @@ class JavaScriptMinifier {
self::TYPE_COMMA => self::EXPRESSION,
self::TYPE_SEMICOLON => self::STATEMENT,
self::TYPE_PAREN_OPEN => self::PAREN_EXPRESSION
- ),
- self::EXPRESSION_FUNC => array(
+ ],
+ self::EXPRESSION_FUNC => [
self::TYPE_BRACE_OPEN => self::STATEMENT
- ),
- self::EXPRESSION_TERNARY => array(
+ ],
+ self::EXPRESSION_TERNARY => [
self::TYPE_BRACE_OPEN => self::PROPERTY_ASSIGNMENT,
self::TYPE_PAREN_OPEN => self::PAREN_EXPRESSION,
self::TYPE_FUNC => self::EXPRESSION_TERNARY_FUNC,
self::TYPE_LITERAL => self::EXPRESSION_TERNARY_OP
- ),
- self::EXPRESSION_TERNARY_OP => array(
+ ],
+ self::EXPRESSION_TERNARY_OP => [
self::TYPE_BIN_OP => self::EXPRESSION_TERNARY,
self::TYPE_ADD_OP => self::EXPRESSION_TERNARY,
self::TYPE_HOOK => self::EXPRESSION_TERNARY,
self::TYPE_COMMA => self::EXPRESSION_TERNARY,
self::TYPE_PAREN_OPEN => self::PAREN_EXPRESSION
- ),
- self::EXPRESSION_TERNARY_FUNC => array(
+ ],
+ self::EXPRESSION_TERNARY_FUNC => [
self::TYPE_BRACE_OPEN => self::STATEMENT
- ),
- self::PAREN_EXPRESSION => array(
+ ],
+ self::PAREN_EXPRESSION => [
self::TYPE_BRACE_OPEN => self::PROPERTY_ASSIGNMENT,
self::TYPE_PAREN_OPEN => self::PAREN_EXPRESSION,
self::TYPE_FUNC => self::PAREN_EXPRESSION_FUNC,
self::TYPE_LITERAL => self::PAREN_EXPRESSION_OP
- ),
- self::PAREN_EXPRESSION_OP => array(
+ ],
+ self::PAREN_EXPRESSION_OP => [
self::TYPE_BIN_OP => self::PAREN_EXPRESSION,
self::TYPE_ADD_OP => self::PAREN_EXPRESSION,
self::TYPE_HOOK => self::PAREN_EXPRESSION,
@@ -263,107 +260,107 @@ class JavaScriptMinifier {
self::TYPE_COMMA => self::PAREN_EXPRESSION,
self::TYPE_SEMICOLON => self::PAREN_EXPRESSION,
self::TYPE_PAREN_OPEN => self::PAREN_EXPRESSION
- ),
- self::PAREN_EXPRESSION_FUNC => array(
+ ],
+ self::PAREN_EXPRESSION_FUNC => [
self::TYPE_BRACE_OPEN => self::STATEMENT
- ),
- self::PROPERTY_EXPRESSION => array(
+ ],
+ self::PROPERTY_EXPRESSION => [
self::TYPE_BRACE_OPEN => self::PROPERTY_ASSIGNMENT,
self::TYPE_PAREN_OPEN => self::PAREN_EXPRESSION,
self::TYPE_FUNC => self::PROPERTY_EXPRESSION_FUNC,
self::TYPE_LITERAL => self::PROPERTY_EXPRESSION_OP
- ),
- self::PROPERTY_EXPRESSION_OP => array(
+ ],
+ self::PROPERTY_EXPRESSION_OP => [
self::TYPE_BIN_OP => self::PROPERTY_EXPRESSION,
self::TYPE_ADD_OP => self::PROPERTY_EXPRESSION,
self::TYPE_HOOK => self::PROPERTY_EXPRESSION,
self::TYPE_COMMA => self::PROPERTY_ASSIGNMENT,
self::TYPE_PAREN_OPEN => self::PAREN_EXPRESSION
- ),
- self::PROPERTY_EXPRESSION_FUNC => array(
+ ],
+ self::PROPERTY_EXPRESSION_FUNC => [
self::TYPE_BRACE_OPEN => self::STATEMENT
- )
- );
+ ]
+ ];
// $push : This table contains the rules for when to push a state onto the stack.
// The pushed state is the state to return to when the corresponding
// closing token is found
- $push = array(
- self::STATEMENT => array(
+ $push = [
+ self::STATEMENT => [
self::TYPE_BRACE_OPEN => self::STATEMENT,
self::TYPE_PAREN_OPEN => self::EXPRESSION_OP
- ),
- self::CONDITION => array(
+ ],
+ self::CONDITION => [
self::TYPE_PAREN_OPEN => self::STATEMENT
- ),
- self::PROPERTY_ASSIGNMENT => array(
+ ],
+ self::PROPERTY_ASSIGNMENT => [
self::TYPE_BRACE_OPEN => self::PROPERTY_ASSIGNMENT
- ),
- self::EXPRESSION => array(
+ ],
+ self::EXPRESSION => [
self::TYPE_BRACE_OPEN => self::EXPRESSION_OP,
self::TYPE_PAREN_OPEN => self::EXPRESSION_OP
- ),
- self::EXPRESSION_NO_NL => array(
+ ],
+ self::EXPRESSION_NO_NL => [
self::TYPE_BRACE_OPEN => self::EXPRESSION_OP,
self::TYPE_PAREN_OPEN => self::EXPRESSION_OP
- ),
- self::EXPRESSION_OP => array(
+ ],
+ self::EXPRESSION_OP => [
self::TYPE_HOOK => self::EXPRESSION,
self::TYPE_PAREN_OPEN => self::EXPRESSION_OP
- ),
- self::EXPRESSION_FUNC => array(
+ ],
+ self::EXPRESSION_FUNC => [
self::TYPE_BRACE_OPEN => self::EXPRESSION_OP
- ),
- self::EXPRESSION_TERNARY => array(
+ ],
+ self::EXPRESSION_TERNARY => [
self::TYPE_BRACE_OPEN => self::EXPRESSION_TERNARY_OP,
self::TYPE_PAREN_OPEN => self::EXPRESSION_TERNARY_OP
- ),
- self::EXPRESSION_TERNARY_OP => array(
+ ],
+ self::EXPRESSION_TERNARY_OP => [
self::TYPE_HOOK => self::EXPRESSION_TERNARY,
self::TYPE_PAREN_OPEN => self::EXPRESSION_TERNARY_OP
- ),
- self::EXPRESSION_TERNARY_FUNC => array(
+ ],
+ self::EXPRESSION_TERNARY_FUNC => [
self::TYPE_BRACE_OPEN => self::EXPRESSION_TERNARY_OP
- ),
- self::PAREN_EXPRESSION => array(
+ ],
+ self::PAREN_EXPRESSION => [
self::TYPE_BRACE_OPEN => self::PAREN_EXPRESSION_OP,
self::TYPE_PAREN_OPEN => self::PAREN_EXPRESSION_OP
- ),
- self::PAREN_EXPRESSION_OP => array(
+ ],
+ self::PAREN_EXPRESSION_OP => [
self::TYPE_PAREN_OPEN => self::PAREN_EXPRESSION_OP
- ),
- self::PAREN_EXPRESSION_FUNC => array(
+ ],
+ self::PAREN_EXPRESSION_FUNC => [
self::TYPE_BRACE_OPEN => self::PAREN_EXPRESSION_OP
- ),
- self::PROPERTY_EXPRESSION => array(
+ ],
+ self::PROPERTY_EXPRESSION => [
self::TYPE_BRACE_OPEN => self::PROPERTY_EXPRESSION_OP,
self::TYPE_PAREN_OPEN => self::PROPERTY_EXPRESSION_OP
- ),
- self::PROPERTY_EXPRESSION_OP => array(
+ ],
+ self::PROPERTY_EXPRESSION_OP => [
self::TYPE_PAREN_OPEN => self::PROPERTY_EXPRESSION_OP
- ),
- self::PROPERTY_EXPRESSION_FUNC => array(
+ ],
+ self::PROPERTY_EXPRESSION_FUNC => [
self::TYPE_BRACE_OPEN => self::PROPERTY_EXPRESSION_OP
- )
- );
+ ]
+ ];
// $pop : Rules for when to pop a state from the stack
- $pop = array(
- self::STATEMENT => array( self::TYPE_BRACE_CLOSE => true ),
- self::PROPERTY_ASSIGNMENT => array( self::TYPE_BRACE_CLOSE => true ),
- self::EXPRESSION => array( self::TYPE_BRACE_CLOSE => true ),
- self::EXPRESSION_NO_NL => array( self::TYPE_BRACE_CLOSE => true ),
- self::EXPRESSION_OP => array( self::TYPE_BRACE_CLOSE => true ),
- self::EXPRESSION_TERNARY_OP => array( self::TYPE_COLON => true ),
- self::PAREN_EXPRESSION => array( self::TYPE_PAREN_CLOSE => true ),
- self::PAREN_EXPRESSION_OP => array( self::TYPE_PAREN_CLOSE => true ),
- self::PROPERTY_EXPRESSION => array( self::TYPE_BRACE_CLOSE => true ),
- self::PROPERTY_EXPRESSION_OP => array( self::TYPE_BRACE_CLOSE => true )
- );
+ $pop = [
+ self::STATEMENT => [ self::TYPE_BRACE_CLOSE => true ],
+ self::PROPERTY_ASSIGNMENT => [ self::TYPE_BRACE_CLOSE => true ],
+ self::EXPRESSION => [ self::TYPE_BRACE_CLOSE => true ],
+ self::EXPRESSION_NO_NL => [ self::TYPE_BRACE_CLOSE => true ],
+ self::EXPRESSION_OP => [ self::TYPE_BRACE_CLOSE => true ],
+ self::EXPRESSION_TERNARY_OP => [ self::TYPE_COLON => true ],
+ self::PAREN_EXPRESSION => [ self::TYPE_PAREN_CLOSE => true ],
+ self::PAREN_EXPRESSION_OP => [ self::TYPE_PAREN_CLOSE => true ],
+ self::PROPERTY_EXPRESSION => [ self::TYPE_BRACE_CLOSE => true ],
+ self::PROPERTY_EXPRESSION_OP => [ self::TYPE_BRACE_CLOSE => true ]
+ ];
// $semicolon : Rules for when a semicolon insertion is appropriate
- $semicolon = array(
- self::EXPRESSION_NO_NL => array(
+ $semicolon = [
+ self::EXPRESSION_NO_NL => [
self::TYPE_UN_OP => true,
self::TYPE_INCR_OP => true,
self::TYPE_ADD_OP => true,
@@ -374,8 +371,8 @@ class JavaScriptMinifier {
self::TYPE_DO => true,
self::TYPE_FUNC => true,
self::TYPE_LITERAL => true
- ),
- self::EXPRESSION_OP => array(
+ ],
+ self::EXPRESSION_OP => [
self::TYPE_UN_OP => true,
self::TYPE_INCR_OP => true,
self::TYPE_BRACE_OPEN => true,
@@ -384,33 +381,16 @@ class JavaScriptMinifier {
self::TYPE_DO => true,
self::TYPE_FUNC => true,
self::TYPE_LITERAL => true
- )
- );
-
- // Rules for when newlines should be inserted if
- // $statementsOnOwnLine is enabled.
- // $newlineBefore is checked before switching state,
- // $newlineAfter is checked after
- $newlineBefore = array(
- self::STATEMENT => array(
- self::TYPE_BRACE_CLOSE => true,
- ),
- );
- $newlineAfter = array(
- self::STATEMENT => array(
- self::TYPE_BRACE_OPEN => true,
- self::TYPE_PAREN_CLOSE => true,
- self::TYPE_SEMICOLON => true,
- ),
- );
+ ]
+ ];
// $divStates : Contains all states that can be followed by a division operator
- $divStates = array(
+ $divStates = [
self::EXPRESSION_OP => true,
self::EXPRESSION_TERNARY_OP => true,
self::PAREN_EXPRESSION_OP => true,
self::PROPERTY_EXPRESSION_OP => true
- );
+ ];
// Here's where the minifying takes place: Loop through the input, looking for tokens
// and output them to $out, taking actions to the above defined rules when appropriate.
@@ -420,24 +400,24 @@ class JavaScriptMinifier {
$lineLength = 0;
$newlineFound = true;
$state = self::STATEMENT;
- $stack = array();
+ $stack = [];
$last = ';'; // Pretend that we have seen a semicolon yet
- while( $pos < $length ) {
+ while ( $pos < $length ) {
// First, skip over any whitespace and multiline comments, recording whether we
// found any newline character
$skip = strspn( $s, " \t\n\r\xb\xc", $pos );
- if( !$skip ) {
+ if ( !$skip ) {
$ch = $s[$pos];
- if( $ch === '/' && substr( $s, $pos, 2 ) === '/*' ) {
+ if ( $ch === '/' && substr( $s, $pos, 2 ) === '/*' ) {
// Multiline comment. Search for the end token or EOT.
$end = strpos( $s, '*/', $pos + 2 );
$skip = $end === false ? $length - $pos : $end - $pos + 2;
}
}
- if( $skip ) {
+ if ( $skip ) {
// The semicolon insertion mechanism needs to know whether there was a newline
// between two tokens, so record it now.
- if( !$newlineFound && strcspn( $s, "\r\n", $pos, $skip ) !== $skip ) {
+ if ( !$newlineFound && strcspn( $s, "\r\n", $pos, $skip ) !== $skip ) {
$newlineFound = true;
}
$pos += $skip;
@@ -446,7 +426,7 @@ class JavaScriptMinifier {
// Handle C++-style comments and html comments, which are treated as single line
// comments by the browser, regardless of whether the end tag is on the same line.
// Handle --> the same way, but only if it's at the beginning of the line
- if( ( $ch === '/' && substr( $s, $pos, 2 ) === '//' )
+ if ( ( $ch === '/' && substr( $s, $pos, 2 ) === '//' )
|| ( $ch === '<' && substr( $s, $pos, 4 ) === '<!--' )
|| ( $ch === '-' && $newlineFound && substr( $s, $pos, 3 ) === '-->' )
) {
@@ -454,65 +434,113 @@ class JavaScriptMinifier {
continue;
}
- // Find out which kind of token we're handling. $end will point past the end of it.
+ // Find out which kind of token we're handling.
+ // Note: $end must point past the end of the current token
+ // so that `substr($s, $pos, $end - $pos)` would be the entire token.
+ // In order words, $end will be the offset of the last relevant character
+ // in the stream + 1, or simply put: The offset of the first character
+ // of any next token in the stream.
$end = $pos + 1;
// Handle string literals
- if( $ch === "'" || $ch === '"' ) {
+ if ( $ch === "'" || $ch === '"' ) {
// Search to the end of the string literal, skipping over backslash escapes
$search = $ch . '\\';
do{
+ // Speculatively add 2 to the end so that if we see a backslash,
+ // the next iteration will start 2 characters further (one for the
+ // backslash, one for the escaped character).
+ // We'll correct this outside the loop.
$end += strcspn( $s, $search, $end ) + 2;
- } while( $end - 2 < $length && $s[$end - 2] === '\\' );
+ // If the last character in our search for a quote or a backlash
+ // matched a backslash and we haven't reached the end, keep searching..
+ } while ( $end - 2 < $length && $s[$end - 2] === '\\' );
+ // Correction (1): Undo speculative add, keep only one (end of string literal)
$end--;
+ if ( $end > $length ) {
+ // Correction (2): Loop wrongly assumed an end quote ended the search,
+ // but search ended because we've reached the end. Correct $end.
+ // TODO: This is invalid and should throw.
+ $end--;
+ }
// We have to distinguish between regexp literals and division operators
// A division operator is only possible in certain states
- } elseif( $ch === '/' && !isset( $divStates[$state] ) ) {
- // Regexp literal, search to the end, skipping over backslash escapes and
- // character classes
- for( ; ; ) {
+ } elseif ( $ch === '/' && !isset( $divStates[$state] ) ) {
+ // Regexp literal
+ for ( ; ; ) {
+ // Search until we find "/" (end of regexp), "\" (backslash escapes),
+ // or "[" (start of character classes).
do{
+ // Speculatively add 2 to ensure next iteration skips
+ // over backslash and escaped character.
+ // We'll correct this outside the loop.
$end += strcspn( $s, '/[\\', $end ) + 2;
- } while( $end - 2 < $length && $s[$end - 2] === '\\' );
+ // If backslash escape, keep searching...
+ } while ( $end - 2 < $length && $s[$end - 2] === '\\' );
+ // Correction (1): Undo speculative add, keep only one (end of regexp)
$end--;
- if( $end - 1 >= $length || $s[$end - 1] === '/' ) {
+ if ( $end > $length ) {
+ // Correction (2): Loop wrongly assumed end slash was seen
+ // String ended without end of regexp. Correct $end.
+ // TODO: This is invalid and should throw.
+ $end--;
+ break;
+ }
+ if ( $s[$end - 1] === '/' ) {
break;
}
+ // (Implicit else), we must've found the start of a char class,
+ // skip until we find "]" (end of char class), or "\" (backslash escape)
do{
+ // Speculatively add 2 for backslash escape.
+ // We'll substract one outside the loop.
$end += strcspn( $s, ']\\', $end ) + 2;
- } while( $end - 2 < $length && $s[$end - 2] === '\\' );
+ // If backslash escape, keep searching...
+ } while ( $end - 2 < $length && $s[$end - 2] === '\\' );
+ // Correction (1): Undo speculative add, keep only one (end of regexp)
$end--;
- };
+ if ( $end > $length ) {
+ // Correction (2): Loop wrongly assumed "]" was seen
+ // String ended without ending char class or regexp. Correct $end.
+ // TODO: This is invalid and should throw.
+ $end--;
+ break;
+ }
+ }
// Search past the regexp modifiers (gi)
- while( $end < $length && ctype_alpha( $s[$end] ) ) {
+ while ( $end < $length && ctype_alpha( $s[$end] ) ) {
$end++;
}
- } elseif(
+ } elseif (
$ch === '0'
- && ($pos + 1 < $length) && ($s[$pos + 1] === 'x' || $s[$pos + 1] === 'X' )
+ && ( $pos + 1 < $length ) && ( $s[$pos + 1] === 'x' || $s[$pos + 1] === 'X' )
) {
// Hex numeric literal
$end++; // x or X
$len = strspn( $s, '0123456789ABCDEFabcdef', $end );
if ( !$len ) {
- return self::parseError($s, $pos, 'Expected a hexadecimal number but found ' . substr( $s, $pos, 5 ) . '...' );
+ return self::parseError(
+ $s,
+ $pos,
+ 'Expected a hexadecimal number but found ' . substr( $s, $pos, 5 ) . '...'
+ );
}
$end += $len;
- } elseif(
+ } elseif (
ctype_digit( $ch )
|| ( $ch === '.' && $pos + 1 < $length && ctype_digit( $s[$pos + 1] ) )
) {
$end += strspn( $s, '0123456789', $end );
$decimal = strspn( $s, '.', $end );
- if ($decimal) {
+ if ( $decimal ) {
if ( $decimal > 2 ) {
- return self::parseError($s, $end, 'The number has too many decimal points' );
+ return self::parseError( $s, $end, 'The number has too many decimal points' );
}
$end += strspn( $s, '0123456789', $end + 1 ) + $decimal;
}
$exponent = strspn( $s, 'eE', $end );
- if( $exponent ) {
+ if ( $exponent ) {
if ( $exponent > 1 ) {
- return self::parseError($s, $end, 'Number with several E' );
+ return self::parseError( $s, $end, 'Number with several E' );
}
$end++;
@@ -520,13 +548,17 @@ class JavaScriptMinifier {
$end += strspn( $s, '-+', $end );
$len = strspn( $s, '0123456789', $end );
if ( !$len ) {
- return self::parseError($s, $pos, 'No decimal digits after e, how many zeroes should be added?' );
+ return self::parseError(
+ $s,
+ $pos,
+ 'No decimal digits after e, how many zeroes should be added?'
+ );
}
$end += $len;
}
- } elseif( isset( $opChars[$ch] ) ) {
+ } elseif ( isset( $opChars[$ch] ) ) {
// Punctuation character. Search for the longest matching operator.
- while(
+ while (
$end < $length
&& isset( $tokenTypes[substr( $s, $pos, $end - $pos + 1 )] )
) {
@@ -542,26 +574,25 @@ class JavaScriptMinifier {
$token = substr( $s, $pos, $end - $pos ); // so $end - $pos == strlen( $token )
$type = isset( $tokenTypes[$token] ) ? $tokenTypes[$token] : self::TYPE_LITERAL;
- if( $newlineFound && isset( $semicolon[$state][$type] ) ) {
+ if ( $newlineFound && isset( $semicolon[$state][$type] ) ) {
// This token triggers the semicolon insertion mechanism of javascript. While we
// could add the ; token here ourselves, keeping the newline has a few advantages.
$out .= "\n";
$state = self::STATEMENT;
$lineLength = 0;
- } elseif( $maxLineLength > 0 && $lineLength + $end - $pos > $maxLineLength &&
- !isset( $semicolon[$state][$type] ) && $type !== self::TYPE_INCR_OP )
- {
+ } elseif ( $lineLength + $end - $pos > self::MAX_LINE_LENGTH &&
+ !isset( $semicolon[$state][$type] ) && $type !== self::TYPE_INCR_OP ) {
// This line would get too long if we added $token, so add a newline first.
// Only do this if it won't trigger semicolon insertion and if it won't
// put a postfix increment operator on its own line, which is illegal in js.
$out .= "\n";
$lineLength = 0;
// Check, whether we have to separate the token from the last one with whitespace
- } elseif( !isset( $opChars[$last] ) && !isset( $opChars[$ch] ) ) {
+ } elseif ( !isset( $opChars[$last] ) && !isset( $opChars[$ch] ) ) {
$out .= ' ';
$lineLength++;
// Don't accidentally create ++, -- or // tokens
- } elseif( $last === $ch && ( $ch === '+' || $ch === '-' || $ch === '/' ) ) {
+ } elseif ( $last === $ch && ( $ch === '+' || $ch === '-' || $ch === '/' ) ) {
$out .= ' ';
$lineLength++;
}
@@ -580,35 +611,20 @@ class JavaScriptMinifier {
$pos = $end;
$newlineFound = false;
- // Output a newline after the token if required
- // This is checked before AND after switching state
- $newlineAdded = false;
- if ( $statementsOnOwnLine && !$newlineAdded && isset( $newlineBefore[$state][$type] ) ) {
- $out .= "\n";
- $lineLength = 0;
- $newlineAdded = true;
- }
-
// Now that we have output our token, transition into the new state.
- if( isset( $push[$state][$type] ) && count( $stack ) < self::STACK_LIMIT ) {
+ if ( isset( $push[$state][$type] ) && count( $stack ) < self::STACK_LIMIT ) {
$stack[] = $push[$state][$type];
}
- if( $stack && isset( $pop[$state][$type] ) ) {
+ if ( $stack && isset( $pop[$state][$type] ) ) {
$state = array_pop( $stack );
- } elseif( isset( $goto[$state][$type] ) ) {
+ } elseif ( isset( $goto[$state][$type] ) ) {
$state = $goto[$state][$type];
}
-
- // Check for newline insertion again
- if ( $statementsOnOwnLine && !$newlineAdded && isset( $newlineAfter[$state][$type] ) ) {
- $out .= "\n";
- $lineLength = 0;
- }
}
return $out;
}
- static function parseError($fullJavascript, $position, $errorMsg) {
+ static function parseError( $fullJavascript, $position, $errorMsg ) {
// TODO: Handle the error: trigger_error, throw exception, return false...
return false;
}
diff --git a/www/wiki/includes/libs/MWMessagePack.php b/www/wiki/includes/libs/MWMessagePack.php
index a9da3660..be7e93d5 100644
--- a/www/wiki/includes/libs/MWMessagePack.php
+++ b/www/wiki/includes/libs/MWMessagePack.php
@@ -53,137 +53,137 @@ class MWMessagePack {
}
switch ( gettype( $value ) ) {
- case 'NULL':
- return "\xC0";
+ case 'NULL':
+ return "\xC0";
- case 'boolean':
- return $value ? "\xC3" : "\xC2";
+ case 'boolean':
+ return $value ? "\xC3" : "\xC2";
- case 'double':
- case 'float':
- return self::$bigendian
- ? "\xCB" . pack( 'd', $value )
- : "\xCB" . strrev( pack( 'd', $value ) );
+ case 'double':
+ case 'float':
+ return self::$bigendian
+ ? "\xCB" . pack( 'd', $value )
+ : "\xCB" . strrev( pack( 'd', $value ) );
- case 'string':
- $length = strlen( $value );
- if ( $length < 32 ) {
- return pack( 'Ca*', 0xA0 | $length, $value );
- } elseif ( $length <= 0xFFFF ) {
- return pack( 'Cna*', 0xDA, $length, $value );
- } elseif ( $length <= 0xFFFFFFFF ) {
- return pack( 'CNa*', 0xDB, $length, $value );
- }
- throw new InvalidArgumentException( __METHOD__
- . ": string too long (length: $length; max: 4294967295)" );
-
- case 'integer':
- if ( $value >= 0 ) {
- if ( $value <= 0x7F ) {
- // positive fixnum
- return chr( $value );
- }
- if ( $value <= 0xFF ) {
- // uint8
- return pack( 'CC', 0xCC, $value );
- }
- if ( $value <= 0xFFFF ) {
- // uint16
- return pack( 'Cn', 0xCD, $value );
- }
- if ( $value <= 0xFFFFFFFF ) {
- // uint32
- return pack( 'CN', 0xCE, $value );
- }
- if ( $value <= 0xFFFFFFFFFFFFFFFF ) {
- // uint64
- $hi = ( $value & 0xFFFFFFFF00000000 ) >> 32;
- $lo = $value & 0xFFFFFFFF;
- return self::$bigendian
- ? pack( 'CNN', 0xCF, $lo, $hi )
- : pack( 'CNN', 0xCF, $hi, $lo );
- }
- } else {
- if ( $value >= -32 ) {
- // negative fixnum
- return pack( 'c', $value );
- }
- if ( $value >= -0x80 ) {
- // int8
- return pack( 'Cc', 0xD0, $value );
- }
- if ( $value >= -0x8000 ) {
- // int16
- $p = pack( 's', $value );
- return self::$bigendian
- ? pack( 'Ca2', 0xD1, $p )
- : pack( 'Ca2', 0xD1, strrev( $p ) );
- }
- if ( $value >= -0x80000000 ) {
- // int32
- $p = pack( 'l', $value );
- return self::$bigendian
- ? pack( 'Ca4', 0xD2, $p )
- : pack( 'Ca4', 0xD2, strrev( $p ) );
- }
- if ( $value >= -0x8000000000000000 ) {
- // int64
- // pack() does not support 64-bit ints either so pack into two 32-bits
- $p1 = pack( 'l', $value & 0xFFFFFFFF );
- $p2 = pack( 'l', ( $value >> 32 ) & 0xFFFFFFFF );
- return self::$bigendian
- ? pack( 'Ca4a4', 0xD3, $p1, $p2 )
- : pack( 'Ca4a4', 0xD3, strrev( $p2 ), strrev( $p1 ) );
+ case 'string':
+ $length = strlen( $value );
+ if ( $length < 32 ) {
+ return pack( 'Ca*', 0xA0 | $length, $value );
+ } elseif ( $length <= 0xFFFF ) {
+ return pack( 'Cna*', 0xDA, $length, $value );
+ } elseif ( $length <= 0xFFFFFFFF ) {
+ return pack( 'CNa*', 0xDB, $length, $value );
}
- }
- throw new InvalidArgumentException( __METHOD__ . ": invalid integer '$value'" );
-
- case 'array':
- $buffer = '';
- $length = count( $value );
- if ( $length > 0xFFFFFFFF ) {
throw new InvalidArgumentException( __METHOD__
- . ": array too long (length: $length, max: 4294967295)" );
- }
+ . ": string too long (length: $length; max: 4294967295)" );
- $index = 0;
- foreach ( $value as $k => $v ) {
- if ( $index !== $k || $index === $length ) {
- break;
+ case 'integer':
+ if ( $value >= 0 ) {
+ if ( $value <= 0x7F ) {
+ // positive fixnum
+ return chr( $value );
+ }
+ if ( $value <= 0xFF ) {
+ // uint8
+ return pack( 'CC', 0xCC, $value );
+ }
+ if ( $value <= 0xFFFF ) {
+ // uint16
+ return pack( 'Cn', 0xCD, $value );
+ }
+ if ( $value <= 0xFFFFFFFF ) {
+ // uint32
+ return pack( 'CN', 0xCE, $value );
+ }
+ if ( $value <= 0xFFFFFFFFFFFFFFFF ) {
+ // uint64
+ $hi = ( $value & 0xFFFFFFFF00000000 ) >> 32;
+ $lo = $value & 0xFFFFFFFF;
+ return self::$bigendian
+ ? pack( 'CNN', 0xCF, $lo, $hi )
+ : pack( 'CNN', 0xCF, $hi, $lo );
+ }
} else {
- $index++;
+ if ( $value >= -32 ) {
+ // negative fixnum
+ return pack( 'c', $value );
+ }
+ if ( $value >= -0x80 ) {
+ // int8
+ return pack( 'Cc', 0xD0, $value );
+ }
+ if ( $value >= -0x8000 ) {
+ // int16
+ $p = pack( 's', $value );
+ return self::$bigendian
+ ? pack( 'Ca2', 0xD1, $p )
+ : pack( 'Ca2', 0xD1, strrev( $p ) );
+ }
+ if ( $value >= -0x80000000 ) {
+ // int32
+ $p = pack( 'l', $value );
+ return self::$bigendian
+ ? pack( 'Ca4', 0xD2, $p )
+ : pack( 'Ca4', 0xD2, strrev( $p ) );
+ }
+ if ( $value >= -0x8000000000000000 ) {
+ // int64
+ // pack() does not support 64-bit ints either so pack into two 32-bits
+ $p1 = pack( 'l', $value & 0xFFFFFFFF );
+ $p2 = pack( 'l', ( $value >> 32 ) & 0xFFFFFFFF );
+ return self::$bigendian
+ ? pack( 'Ca4a4', 0xD3, $p1, $p2 )
+ : pack( 'Ca4a4', 0xD3, strrev( $p2 ), strrev( $p1 ) );
+ }
}
- }
- $associative = $index !== $length;
+ throw new InvalidArgumentException( __METHOD__ . ": invalid integer '$value'" );
- if ( $associative ) {
- if ( $length < 16 ) {
- $buffer .= pack( 'C', 0x80 | $length );
- } elseif ( $length <= 0xFFFF ) {
- $buffer .= pack( 'Cn', 0xDE, $length );
- } else {
- $buffer .= pack( 'CN', 0xDF, $length );
+ case 'array':
+ $buffer = '';
+ $length = count( $value );
+ if ( $length > 0xFFFFFFFF ) {
+ throw new InvalidArgumentException( __METHOD__
+ . ": array too long (length: $length, max: 4294967295)" );
}
+
+ $index = 0;
foreach ( $value as $k => $v ) {
- $buffer .= self::pack( $k );
- $buffer .= self::pack( $v );
+ if ( $index !== $k || $index === $length ) {
+ break;
+ } else {
+ $index++;
+ }
}
- } else {
- if ( $length < 16 ) {
- $buffer .= pack( 'C', 0x90 | $length );
- } elseif ( $length <= 0xFFFF ) {
- $buffer .= pack( 'Cn', 0xDC, $length );
+ $associative = $index !== $length;
+
+ if ( $associative ) {
+ if ( $length < 16 ) {
+ $buffer .= pack( 'C', 0x80 | $length );
+ } elseif ( $length <= 0xFFFF ) {
+ $buffer .= pack( 'Cn', 0xDE, $length );
+ } else {
+ $buffer .= pack( 'CN', 0xDF, $length );
+ }
+ foreach ( $value as $k => $v ) {
+ $buffer .= self::pack( $k );
+ $buffer .= self::pack( $v );
+ }
} else {
- $buffer .= pack( 'CN', 0xDD, $length );
- }
- foreach ( $value as $v ) {
- $buffer .= self::pack( $v );
+ if ( $length < 16 ) {
+ $buffer .= pack( 'C', 0x90 | $length );
+ } elseif ( $length <= 0xFFFF ) {
+ $buffer .= pack( 'Cn', 0xDC, $length );
+ } else {
+ $buffer .= pack( 'CN', 0xDD, $length );
+ }
+ foreach ( $value as $v ) {
+ $buffer .= self::pack( $v );
+ }
}
- }
- return $buffer;
+ return $buffer;
- default:
- throw new InvalidArgumentException( __METHOD__ . ': unsupported type ' . gettype( $value ) );
+ default:
+ throw new InvalidArgumentException( __METHOD__ . ': unsupported type ' . gettype( $value ) );
}
}
}
diff --git a/www/wiki/includes/libs/MapCacheLRU.php b/www/wiki/includes/libs/MapCacheLRU.php
index db6869bd..553315f2 100644
--- a/www/wiki/includes/libs/MapCacheLRU.php
+++ b/www/wiki/includes/libs/MapCacheLRU.php
@@ -49,15 +49,44 @@ class MapCacheLRU {
}
/**
+ * @param array $values Key/value map in order of increasingly recent access
+ * @param int $maxKeys
+ * @return MapCacheLRU
+ * @since 1.30
+ */
+ public static function newFromArray( array $values, $maxKeys ) {
+ $mapCache = new self( $maxKeys );
+ $mapCache->cache = ( count( $values ) > $maxKeys )
+ ? array_slice( $values, -$maxKeys, null, true )
+ : $values;
+
+ return $mapCache;
+ }
+
+ /**
+ * @return array Key/value map in order of increasingly recent access
+ * @since 1.30
+ */
+ public function toArray() {
+ return $this->cache;
+ }
+
+ /**
* Set a key/value pair.
* This will prune the cache if it gets too large based on LRU.
* If the item is already set, it will be pushed to the top of the cache.
*
+ * To reduce evictions due to one-off use of many new keys, $rank can be
+ * set to have keys start at the top of a bottom fraction of the list. A value
+ * of 3/8 means values start at the top of the bottom 3/8s of the list and are
+ * moved to the top of the list when accessed a second time.
+ *
* @param string $key
* @param mixed $value
+ * @param float $rank Bottom fraction of the list where keys start off [Default: 1.0]
* @return void
*/
- public function set( $key, $value ) {
+ public function set( $key, $value, $rank = 1.0 ) {
if ( $this->has( $key ) ) {
$this->ping( $key );
} elseif ( count( $this->cache ) >= $this->maxCacheKeys ) {
@@ -65,7 +94,15 @@ class MapCacheLRU {
$evictKey = key( $this->cache );
unset( $this->cache[$evictKey] );
}
- $this->cache[$key] = $value;
+
+ if ( $rank < 1.0 && $rank > 0 ) {
+ $offset = intval( $rank * count( $this->cache ) );
+ $this->cache = array_slice( $this->cache, 0, $offset, true )
+ + [ $key => $value ]
+ + array_slice( $this->cache, $offset, null, true );
+ } else {
+ $this->cache[$key] = $value;
+ }
}
/**
@@ -76,7 +113,8 @@ class MapCacheLRU {
*/
public function has( $key ) {
if ( !is_int( $key ) && !is_string( $key ) ) {
- throw new MWException( __METHOD__ . ' called with invalid key. Must be string or integer.' );
+ throw new UnexpectedValueException(
+ __METHOD__ . ' called with invalid key. Must be string or integer.' );
}
return array_key_exists( $key, $this->cache );
}
@@ -115,15 +153,16 @@ class MapCacheLRU {
* @since 1.28
* @param string $key
* @param callable $callback Callback that will produce the value
+ * @param float $rank Bottom fraction of the list where keys start off [Default: 1.0]
* @return mixed The cached value if found or the result of $callback otherwise
*/
- public function getWithSetCallback( $key, callable $callback ) {
+ public function getWithSetCallback( $key, callable $callback, $rank = 1.0 ) {
if ( $this->has( $key ) ) {
$value = $this->get( $key );
} else {
$value = call_user_func( $callback );
if ( $value !== false ) {
- $this->set( $key, $value );
+ $this->set( $key, $value, $rank );
}
}
diff --git a/www/wiki/includes/libs/MultiHttpClient.php b/www/wiki/includes/libs/MultiHttpClient.php
index 16168e6b..dd8772c0 100644
--- a/www/wiki/includes/libs/MultiHttpClient.php
+++ b/www/wiki/includes/libs/MultiHttpClient.php
@@ -48,7 +48,7 @@ use Psr\Log\NullLogger;
class MultiHttpClient implements LoggerAwareInterface {
/** @var resource */
protected $multiHandle = null; // curl_multi handle
- /** @var string|null SSL certificates path */
+ /** @var string|null SSL certificates path */
protected $caBundlePath;
/** @var int */
protected $connTimeout = 10;
@@ -346,16 +346,7 @@ class MultiHttpClient implements LoggerAwareInterface {
// Don't interpret POST parameters starting with '@' as file uploads, because this
// makes it impossible to POST plain values starting with '@' (and causes security
// issues potentially exposing the contents of local files).
- // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6,
- // but we support lower versions, and the option doesn't exist in HHVM 5.6.99.
- if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) {
- curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true );
- } elseif ( is_array( $req['body'] ) ) {
- // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS
- // is an array, but not if it's a string. So convert $req['body'] to a string
- // for safety.
- $req['body'] = http_build_query( $req['body'] );
- }
+ curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true );
curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] );
} else {
if ( is_resource( $req['body'] ) || $req['body'] !== '' ) {
@@ -421,9 +412,14 @@ class MultiHttpClient implements LoggerAwareInterface {
/**
* @return resource
+ * @throws Exception
*/
protected function getCurlMulti() {
if ( !$this->multiHandle ) {
+ if ( !function_exists( 'curl_multi_init' ) ) {
+ throw new Exception( "PHP cURL extension missing. " .
+ "Check https://www.mediawiki.org/wiki/Manual:CURL" );
+ }
$cmh = curl_multi_init();
curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining );
curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
diff --git a/www/wiki/includes/libs/ObjectFactory.php b/www/wiki/includes/libs/ObjectFactory.php
deleted file mode 100644
index 6c47c3ca..00000000
--- a/www/wiki/includes/libs/ObjectFactory.php
+++ /dev/null
@@ -1,198 +0,0 @@
-<?php
-/**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * Construct objects from configuration instructions.
- *
- * @copyright © 2014 Wikimedia Foundation and contributors
- */
-class ObjectFactory {
-
- /**
- * Instantiate an object based on a specification array.
- *
- * The specification array must contain a 'class' key with string value
- * that specifies the class name to instantiate or a 'factory' key with
- * a callable (is_callable() === true). It can optionally contain
- * an 'args' key that provides arguments to pass to the
- * constructor/callable.
- *
- * Values in the arguments collection which are Closure instances will be
- * expanded by invoking them with no arguments before passing the
- * resulting value on to the constructor/callable. This can be used to
- * pass IDatabase instances or other live objects to the
- * constructor/callable. This behavior can be suppressed by adding
- * closure_expansion => false to the specification.
- *
- * The specification may also contain a 'calls' key that describes method
- * calls to make on the newly created object before returning it. This
- * pattern is often known as "setter injection". The value of this key is
- * expected to be an associative array with method names as keys and
- * argument lists as values. The argument list will be expanded (or not)
- * in the same way as the 'args' key for the main object.
- *
- * @param array $spec Object specification
- * @return object
- * @throws InvalidArgumentException when object specification does not
- * contain 'class' or 'factory' keys
- * @throws ReflectionException when 'args' are supplied and 'class'
- * constructor is non-public or non-existent
- */
- public static function getObjectFromSpec( $spec ) {
- $args = isset( $spec['args'] ) ? $spec['args'] : [];
- $expandArgs = !isset( $spec['closure_expansion'] ) ||
- $spec['closure_expansion'] === true;
-
- if ( $expandArgs ) {
- $args = static::expandClosures( $args );
- }
-
- if ( isset( $spec['class'] ) ) {
- $clazz = $spec['class'];
- if ( !$args ) {
- $obj = new $clazz();
- } else {
- $obj = static::constructClassInstance( $clazz, $args );
- }
- } elseif ( isset( $spec['factory'] ) ) {
- $obj = call_user_func_array( $spec['factory'], $args );
- } else {
- throw new InvalidArgumentException(
- 'Provided specification lacks both factory and class parameters.'
- );
- }
-
- if ( isset( $spec['calls'] ) && is_array( $spec['calls'] ) ) {
- // Call additional methods on the newly created object
- foreach ( $spec['calls'] as $method => $margs ) {
- if ( $expandArgs ) {
- $margs = static::expandClosures( $margs );
- }
- call_user_func_array( [ $obj, $method ], $margs );
- }
- }
-
- return $obj;
- }
-
- /**
- * Iterate a list and call any closures it contains.
- *
- * @param array $list List of things
- * @return array List with any Closures replaced with their output
- */
- protected static function expandClosures( $list ) {
- return array_map( function ( $value ) {
- if ( is_object( $value ) && $value instanceof Closure ) {
- // If $value is a Closure, call it.
- return $value();
- } else {
- return $value;
- }
- }, $list );
- }
-
- /**
- * Construct an instance of the given class using the given arguments.
- *
- * PHP's `call_user_func_array()` doesn't work with object construction so
- * we have to use other measures. Starting with PHP 5.6.0 we could use the
- * "splat" operator (`...`) to unpack the array into an argument list.
- * Sadly there is no way to conditionally include a syntax construct like
- * a new operator in a way that allows older versions of PHP to still
- * parse the file. Instead, we will try a loop unrolling technique that
- * works for 0-10 arguments. If we are passed 11 or more arguments we will
- * take the performance penalty of using
- * `ReflectionClass::newInstanceArgs()` to construct the desired object.
- *
- * @param string $clazz Class name
- * @param array $args Constructor arguments
- * @return mixed Constructed instance
- */
- public static function constructClassInstance( $clazz, $args ) {
- // $args should be a non-associative array; show nice error if that's not the case
- if ( $args && array_keys( $args ) !== range( 0, count( $args ) - 1 ) ) {
- throw new InvalidArgumentException( __METHOD__ . ': $args cannot be an associative array' );
- }
-
- // TODO: when PHP min version supported is >=5.6.0 replace this
- // with `return new $clazz( ... $args );`.
- $obj = null;
- switch ( count( $args ) ) {
- case 0:
- $obj = new $clazz();
- break;
- case 1:
- $obj = new $clazz( $args[0] );
- break;
- case 2:
- $obj = new $clazz( $args[0], $args[1] );
- break;
- case 3:
- $obj = new $clazz( $args[0], $args[1], $args[2] );
- break;
- case 4:
- $obj = new $clazz( $args[0], $args[1], $args[2], $args[3] );
- break;
- case 5:
- $obj = new $clazz(
- $args[0], $args[1], $args[2], $args[3], $args[4]
- );
- break;
- case 6:
- $obj = new $clazz(
- $args[0], $args[1], $args[2], $args[3], $args[4],
- $args[5]
- );
- break;
- case 7:
- $obj = new $clazz(
- $args[0], $args[1], $args[2], $args[3], $args[4],
- $args[5], $args[6]
- );
- break;
- case 8:
- $obj = new $clazz(
- $args[0], $args[1], $args[2], $args[3], $args[4],
- $args[5], $args[6], $args[7]
- );
- break;
- case 9:
- $obj = new $clazz(
- $args[0], $args[1], $args[2], $args[3], $args[4],
- $args[5], $args[6], $args[7], $args[8]
- );
- break;
- case 10:
- $obj = new $clazz(
- $args[0], $args[1], $args[2], $args[3], $args[4],
- $args[5], $args[6], $args[7], $args[8], $args[9]
- );
- break;
- default:
- // Fall back to using ReflectionClass and curse the developer
- // who decided that 11+ args was a reasonable method
- // signature.
- $ref = new ReflectionClass( $clazz );
- $obj = $ref->newInstanceArgs( $args );
- }
- return $obj;
- }
-}
diff --git a/www/wiki/includes/libs/ProcessCacheLRU.php b/www/wiki/includes/libs/ProcessCacheLRU.php
index 03e23edb..b374259f 100644
--- a/www/wiki/includes/libs/ProcessCacheLRU.php
+++ b/www/wiki/includes/libs/ProcessCacheLRU.php
@@ -23,14 +23,17 @@
use Wikimedia\Assert\Assert;
/**
- * Handles per process caching of items
+ * Class for process caching individual properties of expiring items
+ *
+ * When the key for an entire item is deleted, all properties for it are deleted
+ *
* @ingroup Cache
*/
class ProcessCacheLRU {
- /** @var Array */
+ /** @var array */
protected $cache = []; // (key => prop => value)
- /** @var Array */
+ /** @var array */
protected $cacheTimes = []; // (key => prop => UNIX timestamp)
protected $maxCacheKeys; // integer; max entries
diff --git a/www/wiki/includes/libs/SamplingStatsdClient.php b/www/wiki/includes/libs/SamplingStatsdClient.php
deleted file mode 100644
index 2e780c97..00000000
--- a/www/wiki/includes/libs/SamplingStatsdClient.php
+++ /dev/null
@@ -1,139 +0,0 @@
-<?php
-/**
- * Copyright 2015
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-use Liuggio\StatsdClient\StatsdClient;
-use Liuggio\StatsdClient\Entity\StatsdData;
-use Liuggio\StatsdClient\Entity\StatsdDataInterface;
-
-/**
- * A statsd client that applies the sampling rate to the data items before sending them.
- *
- * @since 1.26
- */
-class SamplingStatsdClient extends StatsdClient {
- /**
- * Sets sampling rate for all items in $data.
- * The sample rate specified in a StatsdData entity overrides the sample rate specified here.
- *
- * {@inheritDoc}
- */
- public function appendSampleRate( $data, $sampleRate = 1 ) {
- if ( $sampleRate < 1 ) {
- array_walk( $data, function( $item ) use ( $sampleRate ) {
- /** @var $item StatsdData */
- if ( $item->getSampleRate() === 1 ) {
- $item->setSampleRate( $sampleRate );
- }
- } );
- }
-
- return $data;
- }
-
- /*
- * Send the metrics over UDP
- * Sample the metrics according to their sample rate and send the remaining ones.
- *
- * @param StatsdDataInterface|StatsdDataInterface[] $data message(s) to sent
- * strings are not allowed here as sampleData requires a StatsdDataInterface
- * @param int $sampleRate
- *
- * @return integer the data sent in bytes
- */
- public function send( $data, $sampleRate = 1 ) {
- if ( !is_array( $data ) ) {
- $data = [ $data ];
- }
- if ( !$data ) {
- return;
- }
- foreach ( $data as $item ) {
- if ( !( $item instanceof StatsdDataInterface ) ) {
- throw new InvalidArgumentException(
- 'SamplingStatsdClient does not accept stringified messages' );
- }
- }
-
- // add sampling
- if ( $sampleRate < 1 ) {
- $data = $this->appendSampleRate( $data, $sampleRate );
- }
- $data = $this->sampleData( $data );
-
- $data = array_map( 'strval', $data );
-
- // reduce number of packets
- if ( $this->getReducePacket() ) {
- $data = $this->reduceCount( $data );
- }
-
- // failures in any of this should be silently ignored if ..
- $written = 0;
- try {
- $fp = $this->getSender()->open();
- if ( !$fp ) {
- return;
- }
- foreach ( $data as $message ) {
- $written += $this->getSender()->write( $fp, $message );
- }
- $this->getSender()->close( $fp );
- } catch ( Exception $e ) {
- $this->throwException( $e );
- }
-
- return $written;
- }
-
- /**
- * Throw away some of the data according to the sample rate.
- * @param StatsdDataInterface[] $data
- * @return StatsdDataInterface[]
- * @throws LogicException
- */
- protected function sampleData( $data ) {
- $newData = [];
- $mt_rand_max = mt_getrandmax();
- foreach ( $data as $item ) {
- $samplingRate = $item->getSampleRate();
- if ( $samplingRate <= 0.0 || $samplingRate > 1.0 ) {
- throw new LogicException( 'Sampling rate shall be within ]0, 1]' );
- }
- if (
- $samplingRate === 1 ||
- ( mt_rand() / $mt_rand_max <= $samplingRate )
- ) {
- $newData[] = $item;
- }
- }
- return $newData;
- }
-
- /**
- * {@inheritDoc}
- */
- protected function throwException( Exception $exception ) {
- if ( !$this->getFailSilently() ) {
- throw $exception;
- }
- }
-}
diff --git a/www/wiki/includes/libs/ScopedCallback.php b/www/wiki/includes/libs/ScopedCallback.php
deleted file mode 100644
index 96075aad..00000000
--- a/www/wiki/includes/libs/ScopedCallback.php
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-/**
- * This file deals with RAII style scoped callbacks.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * Class for asserting that a callback happens when an dummy object leaves scope
- *
- * @since 1.21
- */
-class ScopedCallback {
- /** @var callable */
- protected $callback;
- /** @var array */
- protected $params;
-
- /**
- * @param callable|null $callback
- * @param array $params Callback arguments (since 1.25)
- * @throws Exception
- */
- public function __construct( $callback, array $params = [] ) {
- if ( $callback !== null && !is_callable( $callback ) ) {
- throw new InvalidArgumentException( "Provided callback is not valid." );
- }
- $this->callback = $callback;
- $this->params = $params;
- }
-
- /**
- * Trigger a scoped callback and destroy it.
- * This is the same is just setting it to null.
- *
- * @param ScopedCallback $sc
- */
- public static function consume( ScopedCallback &$sc = null ) {
- $sc = null;
- }
-
- /**
- * Destroy a scoped callback without triggering it
- *
- * @param ScopedCallback $sc
- */
- public static function cancel( ScopedCallback &$sc = null ) {
- if ( $sc ) {
- $sc->callback = null;
- }
- $sc = null;
- }
-
- /**
- * Trigger the callback when this leaves scope
- */
- function __destruct() {
- if ( $this->callback !== null ) {
- call_user_func_array( $this->callback, $this->params );
- }
- }
-}
diff --git a/www/wiki/includes/libs/StatusValue.php b/www/wiki/includes/libs/StatusValue.php
index f9dcc1b5..6f348c2b 100644
--- a/www/wiki/includes/libs/StatusValue.php
+++ b/www/wiki/includes/libs/StatusValue.php
@@ -214,7 +214,7 @@ class StatusValue {
/**
* Merge another status object into this one
*
- * @param StatusValue $other Other StatusValue object
+ * @param StatusValue $other
* @param bool $overwriteValue Whether to override the "value" member
*/
public function merge( $other, $overwriteValue = false ) {
diff --git a/www/wiki/includes/libs/StringUtils.php b/www/wiki/includes/libs/StringUtils.php
index 9638706d..7915ccf1 100644
--- a/www/wiki/includes/libs/StringUtils.php
+++ b/www/wiki/includes/libs/StringUtils.php
@@ -39,19 +39,7 @@ class StringUtils {
* @return bool Whether the given $value is a valid UTF-8 encoded string
*/
static function isUtf8( $value ) {
- $value = (string)$value;
-
- // HHVM 3.4 and older come with an outdated version of libmbfl that
- // incorrectly allows values above U+10FFFF, so we have to check
- // for them separately. (This issue also exists in PHP 5.3 and
- // older, which are no longer supported.)
- static $newPHP;
- if ( $newPHP === null ) {
- $newPHP = !mb_check_encoding( "\xf4\x90\x80\x80", 'UTF-8' );
- }
-
- return mb_check_encoding( $value, 'UTF-8' ) &&
- ( $newPHP || preg_match( "/\xf4[\x90-\xbf]|[\xf5-\xff]/S", $value ) === 0 );
+ return mb_check_encoding( (string)$value, 'UTF-8' );
}
/**
diff --git a/www/wiki/includes/libs/Timing.php b/www/wiki/includes/libs/Timing.php
index 57c253d5..7b1a9140 100644
--- a/www/wiki/includes/libs/Timing.php
+++ b/www/wiki/includes/libs/Timing.php
@@ -94,9 +94,7 @@ class Timing implements LoggerAwareInterface {
'requestStart' => [
'name' => 'requestStart',
'entryType' => 'mark',
- 'startTime' => isset( $_SERVER['REQUEST_TIME_FLOAT'] )
- ? $_SERVER['REQUEST_TIME_FLOAT']
- : $_SERVER['REQUEST_TIME'],
+ 'startTime' => $_SERVER['REQUEST_TIME_FLOAT'],
'duration' => 0,
],
];
diff --git a/www/wiki/includes/libs/XhprofData.php b/www/wiki/includes/libs/XhprofData.php
index 0be4ff6a..5af22ed5 100644
--- a/www/wiki/includes/libs/XhprofData.php
+++ b/www/wiki/includes/libs/XhprofData.php
@@ -18,7 +18,7 @@
* @file
*/
-use RunningStat\RunningStat;
+use Wikimedia\RunningStat;
/**
* Convenience class for working with XHProf profiling data
@@ -209,14 +209,14 @@ class XhprofData {
foreach ( $this->inclusive as $func => $stats ) {
foreach ( $stats as $name => $value ) {
if ( $value instanceof RunningStat ) {
- $total = $value->m1 * $value->n;
+ $total = $value->getMean() * $value->getCount();
$percent = ( isset( $main[$name] ) && $main[$name] )
? 100 * $total / $main[$name]
: 0;
$this->inclusive[$func][$name] = [
'total' => $total,
'min' => $value->min,
- 'mean' => $value->m1,
+ 'mean' => $value->getMean(),
'max' => $value->max,
'variance' => $value->m2,
'percent' => $percent,
diff --git a/www/wiki/includes/libs/XmlTypeCheck.php b/www/wiki/includes/libs/XmlTypeCheck.php
deleted file mode 100644
index 7659dfdd..00000000
--- a/www/wiki/includes/libs/XmlTypeCheck.php
+++ /dev/null
@@ -1,508 +0,0 @@
-<?php
-/**
- * XML syntax and type checker.
- *
- * Since 1.24.2, it uses XMLReader instead of xml_parse, which gives us
- * more control over the expansion of XML entities. When passed to the
- * callback, entities will be fully expanded, but may report the XML is
- * invalid if expanding the entities are likely to cause a DoS.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-class XmlTypeCheck {
- /**
- * Will be set to true or false to indicate whether the file is
- * well-formed XML. Note that this doesn't check schema validity.
- */
- public $wellFormed = null;
-
- /**
- * Will be set to true if the optional element filter returned
- * a match at some point.
- */
- public $filterMatch = false;
-
- /**
- * Will contain the type of filter hit if the optional element filter returned
- * a match at some point.
- * @var mixed
- */
- public $filterMatchType = false;
-
- /**
- * Name of the document's root element, including any namespace
- * as an expanded URL.
- */
- public $rootElement = '';
-
- /**
- * A stack of strings containing the data of each xml element as it's processed. Append
- * data to the top string of the stack, then pop off the string and process it when the
- * element is closed.
- */
- protected $elementData = [];
-
- /**
- * A stack of element names and attributes, as we process them.
- */
- protected $elementDataContext = [];
-
- /**
- * Current depth of the data stack.
- */
- protected $stackDepth = 0;
-
- /**
- * Additional parsing options
- */
- private $parserOptions = [
- 'processing_instruction_handler' => '',
- 'external_dtd_handler' => '',
- 'dtd_handler' => '',
- 'require_safe_dtd' => true
- ];
-
- /**
- * Allow filtering an XML file.
- *
- * Filters should return either true or a string to indicate something
- * is wrong with the file. $this->filterMatch will store if the
- * file failed validation (true = failed validation).
- * $this->filterMatchType will contain the validation error.
- * $this->wellFormed will contain whether the xml file is well-formed.
- *
- * @note If multiple filters are hit, only one of them will have the
- * result stored in $this->filterMatchType.
- *
- * @param string $input a filename or string containing the XML element
- * @param callable $filterCallback (optional)
- * Function to call to do additional custom validity checks from the
- * SAX element handler event. This gives you access to the element
- * namespace, name, attributes, and text contents.
- * Filter should return a truthy value describing the error.
- * @param bool $isFile (optional) indicates if the first parameter is a
- * filename (default, true) or if it is a string (false)
- * @param array $options list of additional parsing options:
- * processing_instruction_handler: Callback for xml_set_processing_instruction_handler
- * external_dtd_handler: Callback for the url of external dtd subset
- * dtd_handler: Callback given the full text of the <!DOCTYPE declaration.
- * require_safe_dtd: Only allow non-recursive entities in internal dtd (default true)
- */
- function __construct( $input, $filterCallback = null, $isFile = true, $options = [] ) {
- $this->filterCallback = $filterCallback;
- $this->parserOptions = array_merge( $this->parserOptions, $options );
- $this->validateFromInput( $input, $isFile );
- }
-
- /**
- * Alternative constructor: from filename
- *
- * @param string $fname the filename of an XML document
- * @param callable $filterCallback (optional)
- * Function to call to do additional custom validity checks from the
- * SAX element handler event. This gives you access to the element
- * namespace, name, and attributes, but not to text contents.
- * Filter should return 'true' to toggle on $this->filterMatch
- * @return XmlTypeCheck
- */
- public static function newFromFilename( $fname, $filterCallback = null ) {
- return new self( $fname, $filterCallback, true );
- }
-
- /**
- * Alternative constructor: from string
- *
- * @param string $string a string containing an XML element
- * @param callable $filterCallback (optional)
- * Function to call to do additional custom validity checks from the
- * SAX element handler event. This gives you access to the element
- * namespace, name, and attributes, but not to text contents.
- * Filter should return 'true' to toggle on $this->filterMatch
- * @return XmlTypeCheck
- */
- public static function newFromString( $string, $filterCallback = null ) {
- return new self( $string, $filterCallback, false );
- }
-
- /**
- * Get the root element. Simple accessor to $rootElement
- *
- * @return string
- */
- public function getRootElement() {
- return $this->rootElement;
- }
-
- /**
- * @param string $fname the filename
- */
- private function validateFromInput( $xml, $isFile ) {
- $reader = new XMLReader();
- if ( $isFile ) {
- $s = $reader->open( $xml, null, LIBXML_NOERROR | LIBXML_NOWARNING );
- } else {
- $s = $reader->XML( $xml, null, LIBXML_NOERROR | LIBXML_NOWARNING );
- }
- if ( $s !== true ) {
- // Couldn't open the XML
- $this->wellFormed = false;
- } else {
- $oldDisable = libxml_disable_entity_loader( true );
- $reader->setParserProperty( XMLReader::SUBST_ENTITIES, true );
- try {
- $this->validate( $reader );
- } catch ( Exception $e ) {
- // Calling this malformed, because we didn't parse the whole
- // thing. Maybe just an external entity refernce.
- $this->wellFormed = false;
- $reader->close();
- libxml_disable_entity_loader( $oldDisable );
- throw $e;
- }
- $reader->close();
- libxml_disable_entity_loader( $oldDisable );
- }
- }
-
- private function readNext( XMLReader $reader ) {
- set_error_handler( [ $this, 'XmlErrorHandler' ] );
- $ret = $reader->read();
- restore_error_handler();
- return $ret;
- }
-
- public function XmlErrorHandler( $errno, $errstr ) {
- $this->wellFormed = false;
- }
-
- private function validate( $reader ) {
-
- // First, move through anything that isn't an element, and
- // handle any processing instructions with the callback
- do {
- if ( !$this->readNext( $reader ) ) {
- // Hit the end of the document before any elements
- $this->wellFormed = false;
- return;
- }
- if ( $reader->nodeType === XMLReader::PI ) {
- $this->processingInstructionHandler( $reader->name, $reader->value );
- }
- if ( $reader->nodeType === XMLReader::DOC_TYPE ) {
- $this->DTDHandler( $reader );
- }
- } while ( $reader->nodeType != XMLReader::ELEMENT );
-
- // Process the rest of the document
- do {
- switch ( $reader->nodeType ) {
- case XMLReader::ELEMENT:
- $name = $this->expandNS(
- $reader->name,
- $reader->namespaceURI
- );
- if ( $this->rootElement === '' ) {
- $this->rootElement = $name;
- }
- $empty = $reader->isEmptyElement;
- $attrs = $this->getAttributesArray( $reader );
- $this->elementOpen( $name, $attrs );
- if ( $empty ) {
- $this->elementClose();
- }
- break;
-
- case XMLReader::END_ELEMENT:
- $this->elementClose();
- break;
-
- case XMLReader::WHITESPACE:
- case XMLReader::SIGNIFICANT_WHITESPACE:
- case XMLReader::CDATA:
- case XMLReader::TEXT:
- $this->elementData( $reader->value );
- break;
-
- case XMLReader::ENTITY_REF:
- // Unexpanded entity (maybe external?),
- // don't send to the filter (xml_parse didn't)
- break;
-
- case XMLReader::COMMENT:
- // Don't send to the filter (xml_parse didn't)
- break;
-
- case XMLReader::PI:
- // Processing instructions can happen after the header too
- $this->processingInstructionHandler(
- $reader->name,
- $reader->value
- );
- break;
- case XMLReader::DOC_TYPE:
- // We should never see a doctype after first
- // element.
- $this->wellFormed = false;
- break;
- default:
- // One of DOC, ENTITY, END_ENTITY,
- // NOTATION, or XML_DECLARATION
- // xml_parse didn't send these to the filter, so we won't.
- }
-
- } while ( $this->readNext( $reader ) );
-
- if ( $this->stackDepth !== 0 ) {
- $this->wellFormed = false;
- } elseif ( $this->wellFormed === null ) {
- $this->wellFormed = true;
- }
-
- }
-
- /**
- * Get all of the attributes for an XMLReader's current node
- * @param $r XMLReader
- * @return array of attributes
- */
- private function getAttributesArray( XMLReader $r ) {
- $attrs = [];
- while ( $r->moveToNextAttribute() ) {
- if ( $r->namespaceURI === 'http://www.w3.org/2000/xmlns/' ) {
- // XMLReader treats xmlns attributes as normal
- // attributes, while xml_parse doesn't
- continue;
- }
- $name = $this->expandNS( $r->name, $r->namespaceURI );
- $attrs[$name] = $r->value;
- }
- return $attrs;
- }
-
- /**
- * @param $name element or attribute name, maybe with a full or short prefix
- * @param $namespaceURI the namespaceURI
- * @return string the name prefixed with namespaceURI
- */
- private function expandNS( $name, $namespaceURI ) {
- if ( $namespaceURI ) {
- $parts = explode( ':', $name );
- $localname = array_pop( $parts );
- return "$namespaceURI:$localname";
- }
- return $name;
- }
-
- /**
- * @param $name
- * @param $attribs
- */
- private function elementOpen( $name, $attribs ) {
- $this->elementDataContext[] = [ $name, $attribs ];
- $this->elementData[] = '';
- $this->stackDepth++;
- }
-
- /**
- */
- private function elementClose() {
- list( $name, $attribs ) = array_pop( $this->elementDataContext );
- $data = array_pop( $this->elementData );
- $this->stackDepth--;
- $callbackReturn = false;
-
- if ( is_callable( $this->filterCallback ) ) {
- $callbackReturn = call_user_func(
- $this->filterCallback,
- $name,
- $attribs,
- $data
- );
- }
- if ( $callbackReturn ) {
- // Filter hit!
- $this->filterMatch = true;
- $this->filterMatchType = $callbackReturn;
- }
- }
-
- /**
- * @param $data
- */
- private function elementData( $data ) {
- // Collect any data here, and we'll run the callback in elementClose
- $this->elementData[ $this->stackDepth - 1 ] .= trim( $data );
- }
-
- /**
- * @param $target
- * @param $data
- */
- private function processingInstructionHandler( $target, $data ) {
- $callbackReturn = false;
- if ( $this->parserOptions['processing_instruction_handler'] ) {
- $callbackReturn = call_user_func(
- $this->parserOptions['processing_instruction_handler'],
- $target,
- $data
- );
- }
- if ( $callbackReturn ) {
- // Filter hit!
- $this->filterMatch = true;
- $this->filterMatchType = $callbackReturn;
- }
- }
- /**
- * Handle coming across a <!DOCTYPE declaration.
- *
- * @param XMLReader $reader Reader currently pointing at DOCTYPE node.
- */
- private function DTDHandler( XMLReader $reader ) {
- $externalCallback = $this->parserOptions['external_dtd_handler'];
- $generalCallback = $this->parserOptions['dtd_handler'];
- $checkIfSafe = $this->parserOptions['require_safe_dtd'];
- if ( !$externalCallback && !$generalCallback && !$checkIfSafe ) {
- return;
- }
- $dtd = $reader->readOuterXML();
- $callbackReturn = false;
-
- if ( $generalCallback ) {
- $callbackReturn = call_user_func( $generalCallback, $dtd );
- }
- if ( $callbackReturn ) {
- // Filter hit!
- $this->filterMatch = true;
- $this->filterMatchType = $callbackReturn;
- $callbackReturn = false;
- }
-
- $parsedDTD = $this->parseDTD( $dtd );
- if ( $externalCallback && isset( $parsedDTD['type'] ) ) {
- $callbackReturn = call_user_func(
- $externalCallback,
- $parsedDTD['type'],
- isset( $parsedDTD['publicid'] ) ? $parsedDTD['publicid'] : null,
- isset( $parsedDTD['systemid'] ) ? $parsedDTD['systemid'] : null
- );
- }
- if ( $callbackReturn ) {
- // Filter hit!
- $this->filterMatch = true;
- $this->filterMatchType = $callbackReturn;
- $callbackReturn = false;
- }
-
- if ( $checkIfSafe && isset( $parsedDTD['internal'] ) ) {
- if ( !$this->checkDTDIsSafe( $parsedDTD['internal'] ) ) {
- $this->wellFormed = false;
- }
- }
- }
-
- /**
- * Check if the internal subset of the DTD is safe.
- *
- * We whitelist an extremely restricted subset of DTD features.
- *
- * Safe is defined as:
- * * Only contains entity defintions (e.g. No <!ATLIST )
- * * Entity definitions are not "system" entities
- * * Entity definitions are not "parameter" (i.e. %) entities
- * * Entity definitions do not reference other entites except &amp;
- * and quotes. Entity aliases (where the entity contains only
- * another entity are allowed)
- * * Entity references aren't overly long (>255 bytes).
- * * <!ATTLIST svg xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink">
- * allowed if matched exactly for compatibility with graphviz
- * * Comments.
- *
- * @param string $internalSubset The internal subset of the DTD
- * @return bool true if safe.
- */
- private function checkDTDIsSafe( $internalSubset ) {
- $offset = 0;
- $res = preg_match(
- '/^(?:\s*<!ENTITY\s+\S+\s+' .
- '(?:"(?:&[^"%&;]{1,64};|(?:[^"%&]|&amp;|&quot;){0,255})"' .
- '|\'(?:&[^"%&;]{1,64};|(?:[^\'%&]|&amp;|&apos;){0,255})\')\s*>' .
- '|\s*<!--(?:[^-]|-[^-])*-->' .
- '|\s*<!ATTLIST svg xmlns:xlink CDATA #FIXED ' .
- '"http:\/\/www.w3.org\/1999\/xlink">)*\s*$/',
- $internalSubset
- );
-
- return (bool)$res;
- }
-
- /**
- * Parse DTD into parts.
- *
- * If there is an error parsing the dtd, sets wellFormed to false.
- *
- * @param $dtd string
- * @return array Possibly containing keys publicid, systemid, type and internal.
- */
- private function parseDTD( $dtd ) {
- $m = [];
- $res = preg_match(
- '/^<!DOCTYPE\s*\S+\s*' .
- '(?:(?P<typepublic>PUBLIC)\s*' .
- '(?:"(?P<pubquote>[^"]*)"|\'(?P<pubapos>[^\']*)\')' . // public identifer
- '\s*"(?P<pubsysquote>[^"]*)"|\'(?P<pubsysapos>[^\']*)\'' . // system identifier
- '|(?P<typesystem>SYSTEM)\s*' .
- '(?:"(?P<sysquote>[^"]*)"|\'(?P<sysapos>[^\']*)\')' .
- ')?\s*' .
- '(?:\[\s*(?P<internal>.*)\])?\s*>$/s',
- $dtd,
- $m
- );
- if ( !$res ) {
- $this->wellFormed = false;
- return [];
- }
- $parsed = [];
- foreach ( $m as $field => $value ) {
- if ( $value === '' || is_numeric( $field ) ) {
- continue;
- }
- switch ( $field ) {
- case 'typepublic':
- case 'typesystem':
- $parsed['type'] = $value;
- break;
- case 'pubquote':
- case 'pubapos':
- $parsed['publicid'] = $value;
- break;
- case 'pubsysquote':
- case 'pubsysapos':
- case 'sysquote':
- case 'sysapos':
- $parsed['systemid'] = $value;
- break;
- case 'internal':
- $parsed['internal'] = $value;
- break;
- }
- }
- return $parsed;
- }
-}
diff --git a/www/wiki/includes/libs/filebackend/FSFileBackend.php b/www/wiki/includes/libs/filebackend/FSFileBackend.php
index 30548ef0..b257e279 100644
--- a/www/wiki/includes/libs/filebackend/FSFileBackend.php
+++ b/www/wiki/includes/libs/filebackend/FSFileBackend.php
@@ -99,7 +99,13 @@ class FSFileBackend extends FileBackendStore {
}
public function getFeatures() {
- return !$this->isWindows ? FileBackend::ATTR_UNICODE_PATHS : 0;
+ if ( $this->isWindows && version_compare( PHP_VERSION, '7.1', 'lt' ) ) {
+ // PHP before 7.1 used 8-bit code page for filesystem paths on Windows;
+ // See http://php.net/manual/en/migration71.windows-support.php
+ return 0;
+ } else {
+ return FileBackend::ATTR_UNICODE_PATHS;
+ }
}
protected function resolveContainerPath( $container, $relStoragePath ) {
diff --git a/www/wiki/includes/libs/filebackend/FileBackend.php b/www/wiki/includes/libs/filebackend/FileBackend.php
index 51308c13..08f960a6 100644
--- a/www/wiki/includes/libs/filebackend/FileBackend.php
+++ b/www/wiki/includes/libs/filebackend/FileBackend.php
@@ -175,7 +175,7 @@ abstract class FileBackend implements LoggerAwareInterface {
: new NullLockManager( [] );
$this->fileJournal = isset( $config['fileJournal'] )
? $config['fileJournal']
- : FileJournal::factory( [ 'class' => 'NullFileJournal' ], $this->name );
+ : FileJournal::factory( [ 'class' => NullFileJournal::class ], $this->name );
$this->readOnly = isset( $config['readOnly'] )
? (string)$config['readOnly']
: '';
@@ -1597,7 +1597,7 @@ abstract class FileBackend implements LoggerAwareInterface {
final protected function newStatus() {
$args = func_get_args();
if ( count( $args ) ) {
- $sv = call_user_func_array( [ 'StatusValue', 'newFatal' ], $args );
+ $sv = call_user_func_array( [ StatusValue::class, 'newFatal' ], $args );
} else {
$sv = StatusValue::newGood();
}
diff --git a/www/wiki/includes/libs/filebackend/FileBackendMultiWrite.php b/www/wiki/includes/libs/filebackend/FileBackendMultiWrite.php
index f8ca7e5a..9c367af5 100644
--- a/www/wiki/includes/libs/filebackend/FileBackendMultiWrite.php
+++ b/www/wiki/includes/libs/filebackend/FileBackendMultiWrite.php
@@ -87,6 +87,9 @@ class FileBackendMultiWrite extends FileBackend {
* This will apply such updates post-send for web requests. Note that
* any checks from "syncChecks" are still synchronous.
*
+ * Bogus warning
+ * @suppress PhanAccessMethodProtected
+ *
* @param array $config
* @throws FileBackendError
*/
diff --git a/www/wiki/includes/libs/filebackend/FileBackendStore.php b/www/wiki/includes/libs/filebackend/FileBackendStore.php
index b8eec3f0..06b263a8 100644
--- a/www/wiki/includes/libs/filebackend/FileBackendStore.php
+++ b/www/wiki/includes/libs/filebackend/FileBackendStore.php
@@ -60,7 +60,7 @@ abstract class FileBackendStore extends FileBackend {
/**
* @see FileBackend::__construct()
* Additional $config params include:
- * - srvCache : BagOStuff cache to APC/XCache or the like.
+ * - srvCache : BagOStuff cache to APC or the like.
* - wanCache : WANObjectCache object to use for persistent caching.
* - mimeCallback : Callback that takes (storage path, content, file system path) and
* returns the MIME type of the file or 'unknown/unknown'. The file
@@ -378,9 +378,9 @@ abstract class FileBackendStore extends FileBackend {
unset( $params['latest'] ); // sanity
// Check that the specified temp file is valid...
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$ok ) { // not present or not empty
$status->fatal( 'backend-fail-opentemp', $tmpPath );
@@ -696,9 +696,9 @@ abstract class FileBackendStore extends FileBackend {
protected function doGetFileContentsMulti( array $params ) {
$contents = [];
foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
return $contents;
@@ -1008,13 +1008,13 @@ abstract class FileBackendStore extends FileBackend {
*/
final public function getOperationsInternal( array $ops ) {
$supportedOps = [
- 'store' => 'StoreFileOp',
- 'copy' => 'CopyFileOp',
- 'move' => 'MoveFileOp',
- 'delete' => 'DeleteFileOp',
- 'create' => 'CreateFileOp',
- 'describe' => 'DescribeFileOp',
- 'null' => 'NullFileOp'
+ 'store' => StoreFileOp::class,
+ 'copy' => CopyFileOp::class,
+ 'move' => MoveFileOp::class,
+ 'delete' => DeleteFileOp::class,
+ 'create' => CreateFileOp::class,
+ 'describe' => DescribeFileOp::class,
+ 'null' => NullFileOp::class
];
$performOps = []; // array of FileOp objects
diff --git a/www/wiki/includes/libs/filebackend/HTTPFileStreamer.php b/www/wiki/includes/libs/filebackend/HTTPFileStreamer.php
index 9730acb8..9f8959cb 100644
--- a/www/wiki/includes/libs/filebackend/HTTPFileStreamer.php
+++ b/www/wiki/includes/libs/filebackend/HTTPFileStreamer.php
@@ -84,9 +84,9 @@ class HTTPFileStreamer {
is_int( $header ) ? HttpStatus::header( $header ) : header( $header );
};
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$info = stat( $this->path );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !is_array( $info ) ) {
if ( $sendErrors ) {
diff --git a/www/wiki/includes/libs/filebackend/MemoryFileBackend.php b/www/wiki/includes/libs/filebackend/MemoryFileBackend.php
index 0341a2af..548c85c8 100644
--- a/www/wiki/includes/libs/filebackend/MemoryFileBackend.php
+++ b/www/wiki/includes/libs/filebackend/MemoryFileBackend.php
@@ -70,9 +70,9 @@ class MemoryFileBackend extends FileBackendStore {
return $status;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$data = file_get_contents( $params['src'] );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $data === false ) { // source doesn't exist?
$status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
diff --git a/www/wiki/includes/libs/filebackend/SwiftFileBackend.php b/www/wiki/includes/libs/filebackend/SwiftFileBackend.php
index de5a1038..5695d828 100644
--- a/www/wiki/includes/libs/filebackend/SwiftFileBackend.php
+++ b/www/wiki/includes/libs/filebackend/SwiftFileBackend.php
@@ -50,6 +50,14 @@ class SwiftFileBackend extends FileBackendStore {
protected $rgwS3AccessKey;
/** @var string S3 authentication key (RADOS Gateway) */
protected $rgwS3SecretKey;
+ /** @var array Additional users (account:user) with read permissions on public containers */
+ protected $readUsers;
+ /** @var array Additional users (account:user) with write permissions on public containers */
+ protected $writeUsers;
+ /** @var array Additional users (account:user) with read permissions on private containers */
+ protected $secureReadUsers;
+ /** @var array Additional users (account:user) with write permissions on private containers */
+ protected $secureWriteUsers;
/** @var BagOStuff */
protected $srvCache;
@@ -83,7 +91,7 @@ class SwiftFileBackend extends FileBackendStore {
* - levels : the number of hash levels (and digits)
* - repeat : hash subdirectories are prefixed with all the
* parent hash directory names (e.g. "a/ab/abc")
- * - cacheAuthInfo : Whether to cache authentication tokens in APC, XCache, ect.
+ * - cacheAuthInfo : Whether to cache authentication tokens in APC, etc.
* If those are not available, then the main cache will be used.
* This is probably insecure in shared hosting environments.
* - rgwS3AccessKey : Rados Gateway S3 "access key" value on the account.
@@ -96,6 +104,10 @@ class SwiftFileBackend extends FileBackendStore {
* This is used for generating expiring pre-authenticated URLs.
* Only use this when using rgw and to work around
* http://tracker.newdream.net/issues/3454.
+ * - readUsers : Swift users with read access to public containers (account:username)
+ * - writeUsers : Swift users with write access to public containers (account:username)
+ * - secureReadUsers : Swift users with read access to private containers (account:username)
+ * - secureWriteUsers : Swift users with write access to private containers (account:username)
*/
public function __construct( array $config ) {
parent::__construct( $config );
@@ -136,6 +148,18 @@ class SwiftFileBackend extends FileBackendStore {
} else {
$this->srvCache = new EmptyBagOStuff();
}
+ $this->readUsers = isset( $config['readUsers'] )
+ ? $config['readUsers']
+ : [];
+ $this->writeUsers = isset( $config['writeUsers'] )
+ ? $config['writeUsers']
+ : [];
+ $this->secureReadUsers = isset( $config['secureReadUsers'] )
+ ? $config['secureReadUsers']
+ : [];
+ $this->secureWriteUsers = isset( $config['secureWriteUsers'] )
+ ? $config['secureWriteUsers']
+ : [];
}
public function getFeatures() {
@@ -146,7 +170,7 @@ class SwiftFileBackend extends FileBackendStore {
protected function resolveContainerPath( $container, $relStoragePath ) {
if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) {
return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
- } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
+ } elseif ( strlen( rawurlencode( $relStoragePath ) ) > 1024 ) {
return null; // too long for Swift
}
@@ -169,6 +193,29 @@ class SwiftFileBackend extends FileBackendStore {
* @param array $params
* @return array Sanitized value of 'headers' field in $params
*/
+ protected function sanitizeHdrsStrict( array $params ) {
+ if ( !isset( $params['headers'] ) ) {
+ return [];
+ }
+
+ $headers = $this->getCustomHeaders( $params['headers'] );
+ unset( $headers[ 'content-type' ] );
+
+ return $headers;
+ }
+
+ /**
+ * Sanitize and filter the custom headers from a $params array.
+ * Only allows certain "standard" Content- and X-Content- headers.
+ *
+ * When POSTing data, libcurl adds Content-Type: application/x-www-form-urlencoded
+ * if Content-Type is not set, which overwrites the stored Content-Type header
+ * in Swift - therefore for POSTing data do not strip the Content-Type header (the
+ * previously-stored header that has been already read back from swift is sent)
+ *
+ * @param array $params
+ * @return array Sanitized value of 'headers' field in $params
+ */
protected function sanitizeHdrs( array $params ) {
return isset( $params['headers'] )
? $this->getCustomHeaders( $params['headers'] )
@@ -185,7 +232,7 @@ class SwiftFileBackend extends FileBackendStore {
// Normalize casing, and strip out illegal headers
foreach ( $rawHeaders as $name => $value ) {
$name = strtolower( $name );
- if ( preg_match( '/^content-(type|length)$/', $name ) ) {
+ if ( preg_match( '/^content-length$/', $name ) ) {
continue; // blacklisted
} elseif ( preg_match( '/^(x-)?content-/', $name ) ) {
$headers[$name] = $value; // allowed
@@ -264,7 +311,7 @@ class SwiftFileBackend extends FileBackendStore {
'etag' => md5( $params['content'] ),
'content-type' => $contentType,
'x-object-meta-sha1base36' => $sha1Hash
- ] + $this->sanitizeHdrs( $params ),
+ ] + $this->sanitizeHdrsStrict( $params ),
'body' => $params['content']
] ];
@@ -300,9 +347,9 @@ class SwiftFileBackend extends FileBackendStore {
return $status;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$sha1Hash = sha1_file( $params['src'] );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $sha1Hash === false ) { // source doesn't exist?
$status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
@@ -328,7 +375,7 @@ class SwiftFileBackend extends FileBackendStore {
'etag' => md5_file( $params['src'] ),
'content-type' => $contentType,
'x-object-meta-sha1base36' => $sha1Hash
- ] + $this->sanitizeHdrs( $params ),
+ ] + $this->sanitizeHdrsStrict( $params ),
'body' => $handle // resource
] ];
@@ -379,7 +426,7 @@ class SwiftFileBackend extends FileBackendStore {
'headers' => [
'x-copy-from' => '/' . rawurlencode( $srcCont ) .
'/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
- ] + $this->sanitizeHdrs( $params ), // extra headers merged into object
+ ] + $this->sanitizeHdrsStrict( $params ), // extra headers merged into object
] ];
$method = __METHOD__;
@@ -428,7 +475,7 @@ class SwiftFileBackend extends FileBackendStore {
'headers' => [
'x-copy-from' => '/' . rawurlencode( $srcCont ) .
'/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
- ] + $this->sanitizeHdrs( $params ) // extra headers merged into object
+ ] + $this->sanitizeHdrsStrict( $params ) // extra headers merged into object
]
];
if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) {
@@ -590,11 +637,13 @@ class SwiftFileBackend extends FileBackendStore {
$stat = $this->getContainerStat( $fullCont );
if ( is_array( $stat ) ) {
+ $readUsers = array_merge( $this->secureReadUsers, [ $this->swiftUser ] );
+ $writeUsers = array_merge( $this->secureWriteUsers, [ $this->swiftUser ] );
// Make container private to end-users...
$status->merge( $this->setContainerAccess(
$fullCont,
- [ $this->swiftUser ], // read
- [ $this->swiftUser ] // write
+ $readUsers,
+ $writeUsers
) );
} elseif ( $stat === false ) {
$status->fatal( 'backend-fail-usable', $params['dir'] );
@@ -611,11 +660,14 @@ class SwiftFileBackend extends FileBackendStore {
$stat = $this->getContainerStat( $fullCont );
if ( is_array( $stat ) ) {
+ $readUsers = array_merge( $this->readUsers, [ $this->swiftUser, '.r:*' ] );
+ $writeUsers = array_merge( $this->writeUsers, [ $this->swiftUser ] );
+
// Make container public to end-users...
$status->merge( $this->setContainerAccess(
$fullCont,
- [ $this->swiftUser, '.r:*' ], // read
- [ $this->swiftUser ] // write
+ $readUsers,
+ $writeUsers
) );
} elseif ( $stat === false ) {
$status->fatal( 'backend-fail-usable', $params['dir'] );
@@ -697,7 +749,8 @@ class SwiftFileBackend extends FileBackendStore {
/** @noinspection PhpUnusedLocalVariableInspection */
$ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
- $this->logger->error( __METHOD__ . ": $path was not stored with SHA-1 metadata." );
+ $this->logger->error( __METHOD__ . ": {path} was not stored with SHA-1 metadata.",
+ [ 'path' => $path ] );
$objHdrs['x-object-meta-sha1base36'] = false;
@@ -737,7 +790,8 @@ class SwiftFileBackend extends FileBackendStore {
}
}
- $this->logger->error( __METHOD__ . ": unable to set SHA-1 metadata for $path" );
+ $this->logger->error( __METHOD__ . ': unable to set SHA-1 metadata for {path}',
+ [ 'path' => $path ] );
return $objHdrs; // failed
}
@@ -1309,7 +1363,7 @@ class SwiftFileBackend extends FileBackendStore {
* (lists are truncated to 10000 item with no way to page), and is just a performance risk.
*
* @param string $container Resolved Swift container
- * @param array $readGrps List of the possible criteria for a request to have
+ * @param array $readUsers List of the possible criteria for a request to have
* access to read a container. Each item is one of the following formats:
* - account:user : Grants access if the request is by the given user
* - ".r:<regex>" : Grants access if the request is from a referrer host that
@@ -1317,12 +1371,12 @@ class SwiftFileBackend extends FileBackendStore {
* Setting this to '*' effectively makes a container public.
* -".rlistings:<regex>" : Grants access if the request is from a referrer host that
* matches the expression and the request is for a listing.
- * @param array $writeGrps A list of the possible criteria for a request to have
+ * @param array $writeUsers A list of the possible criteria for a request to have
* access to write to a container. Each item is of the following format:
* - account:user : Grants access if the request is by the given user
* @return StatusValue
*/
- protected function setContainerAccess( $container, array $readGrps, array $writeGrps ) {
+ protected function setContainerAccess( $container, array $readUsers, array $writeUsers ) {
$status = $this->newStatus();
$auth = $this->getAuthentication();
@@ -1336,14 +1390,15 @@ class SwiftFileBackend extends FileBackendStore {
'method' => 'POST',
'url' => $this->storageUrl( $auth, $container ),
'headers' => $this->authTokenHeaders( $auth ) + [
- 'x-container-read' => implode( ',', $readGrps ),
- 'x-container-write' => implode( ',', $writeGrps )
+ 'x-container-read' => implode( ',', $readUsers ),
+ 'x-container-write' => implode( ',', $writeUsers )
]
] );
if ( $rcode != 204 && $rcode !== 202 ) {
$status->fatal( 'backend-fail-internal', $this->name );
- $this->logger->error( __METHOD__ . ': unexpected rcode value (' . $rcode . ')' );
+ $this->logger->error( __METHOD__ . ': unexpected rcode value ({rcode})',
+ [ 'rcode' => $rcode ] );
}
return $status;
@@ -1420,18 +1475,21 @@ class SwiftFileBackend extends FileBackendStore {
// @see SwiftFileBackend::setContainerAccess()
if ( empty( $params['noAccess'] ) ) {
- $readGrps = [ '.r:*', $this->swiftUser ]; // public
+ // public
+ $readUsers = array_merge( $this->readUsers, [ '.r:*', $this->swiftUser ] );
+ $writeUsers = array_merge( $this->writeUsers, [ $this->swiftUser ] );
} else {
- $readGrps = [ $this->swiftUser ]; // private
+ // private
+ $readUsers = array_merge( $this->secureReadUsers, [ $this->swiftUser ] );
+ $writeUsers = array_merge( $this->secureWriteUsers, [ $this->swiftUser ] );
}
- $writeGrps = [ $this->swiftUser ]; // sanity
list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
'method' => 'PUT',
'url' => $this->storageUrl( $auth, $container ),
'headers' => $this->authTokenHeaders( $auth ) + [
- 'x-container-read' => implode( ',', $readGrps ),
- 'x-container-write' => implode( ',', $writeGrps )
+ 'x-container-read' => implode( ',', $readUsers ),
+ 'x-container-write' => implode( ',', $writeUsers )
]
] );
@@ -1753,10 +1811,18 @@ class SwiftFileBackend extends FileBackendStore {
if ( $code == 401 ) { // possibly a stale token
$this->srvCache->delete( $this->getCredsCacheKey( $this->swiftUser ) );
}
- $this->logger->error(
- "HTTP $code ($desc) in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
- ( $err ? ": $err" : "" )
- );
+ $msg = "HTTP {code} ({desc}) in '{func}' (given '{params}')";
+ $msgParams = [
+ 'code' => $code,
+ 'desc' => $desc,
+ 'func' => $func,
+ 'req_params' => FormatJson::encode( $params ),
+ ];
+ if ( $err ) {
+ $msg .= ': {err}';
+ $msgParams['err'] = $err;
+ }
+ $this->logger->error( $msg, $msgParams );
}
}
diff --git a/www/wiki/includes/libs/filebackend/fileop/StoreFileOp.php b/www/wiki/includes/libs/filebackend/fileop/StoreFileOp.php
index bba762f0..69ae47f4 100644
--- a/www/wiki/includes/libs/filebackend/fileop/StoreFileOp.php
+++ b/www/wiki/includes/libs/filebackend/fileop/StoreFileOp.php
@@ -77,9 +77,9 @@ class StoreFileOp extends FileOp {
}
protected function getSourceSha1Base36() {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$hash = sha1_file( $this->params['src'] );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $hash !== false ) {
$hash = Wikimedia\base_convert( $hash, 16, 36, 31 );
}
diff --git a/www/wiki/includes/libs/filebackend/fsfile/FSFile.php b/www/wiki/includes/libs/filebackend/fsfile/FSFile.php
index dacad1cb..553c9aaf 100644
--- a/www/wiki/includes/libs/filebackend/fsfile/FSFile.php
+++ b/www/wiki/includes/libs/filebackend/fsfile/FSFile.php
@@ -75,9 +75,9 @@ class FSFile {
* @return string|bool TS_MW timestamp or false on failure
*/
public function getTimestamp() {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$timestamp = filemtime( $this->path );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $timestamp !== false ) {
$timestamp = wfTimestamp( TS_MW, $timestamp );
}
@@ -168,9 +168,9 @@ class FSFile {
return $this->sha1Base36;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$this->sha1Base36 = sha1_file( $this->path );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $this->sha1Base36 !== false ) {
$this->sha1Base36 = Wikimedia\base_convert( $this->sha1Base36, 16, 36, 31 );
diff --git a/www/wiki/includes/libs/filebackend/fsfile/TempFSFile.php b/www/wiki/includes/libs/filebackend/fsfile/TempFSFile.php
index fed6812f..00d20287 100644
--- a/www/wiki/includes/libs/filebackend/fsfile/TempFSFile.php
+++ b/www/wiki/includes/libs/filebackend/fsfile/TempFSFile.php
@@ -62,9 +62,9 @@ class TempFSFile extends FSFile {
$tmpDirectory = self::getUsableTempDirectory();
}
$path = wfTempDir() . '/' . $prefix . $hex . $ext;
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$newFileHandle = fopen( $path, 'x' );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $newFileHandle ) {
fclose( $newFileHandle );
$tmpFile = new self( $path );
@@ -119,9 +119,9 @@ class TempFSFile extends FSFile {
*/
public function purge() {
$this->canDelete = false; // done
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ok = unlink( $this->path );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
unset( self::$pathsCollect[$this->path] );
@@ -179,9 +179,9 @@ class TempFSFile extends FSFile {
*/
public static function purgeAllOnShutdown() {
foreach ( self::$pathsCollect as $path ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
unlink( $path );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
}
diff --git a/www/wiki/includes/libs/http/HttpAcceptNegotiator.php b/www/wiki/includes/libs/http/HttpAcceptNegotiator.php
index 5f8d9a69..4de8e77e 100644
--- a/www/wiki/includes/libs/http/HttpAcceptNegotiator.php
+++ b/www/wiki/includes/libs/http/HttpAcceptNegotiator.php
@@ -1,5 +1,7 @@
<?php
+namespace Wikimedia\Http;
+
/**
* Utility for negotiating a value from a set of supported values using a preference list.
* This is intended for use with HTTP headers like Accept, Accept-Language, Accept-Encoding, etc.
@@ -8,13 +10,10 @@
* To use this with a request header, first parse the header value into an array of weights
* using HttpAcceptParser, then call getBestSupportedKey.
*
- * @license GPL-2.0+
+ * @license GPL-2.0-or-later
* @author Daniel Kinzler
- * @author Thiemo Mättig
+ * @author Thiemo Kreuz
*/
-
-namespace Wikimedia\Http;
-
class HttpAcceptNegotiator {
/**
diff --git a/www/wiki/includes/libs/http/HttpAcceptParser.php b/www/wiki/includes/libs/http/HttpAcceptParser.php
index bce071e7..df22b414 100644
--- a/www/wiki/includes/libs/http/HttpAcceptParser.php
+++ b/www/wiki/includes/libs/http/HttpAcceptParser.php
@@ -4,7 +4,7 @@
* Utility for parsing a HTTP Accept header value into a weight map. May also be used with
* other, similar headers like Accept-Language, Accept-Encoding, etc.
*
- * @license GPL-2.0+
+ * @license GPL-2.0-or-later
* @author Daniel Kinzler
*/
diff --git a/www/wiki/includes/libs/jsminplus.php b/www/wiki/includes/libs/jsminplus.php
index 7feac7d1..e3c2d758 100644
--- a/www/wiki/includes/libs/jsminplus.php
+++ b/www/wiki/includes/libs/jsminplus.php
@@ -1,5 +1,5 @@
<?php
-// @codingStandardsIgnoreFile File external to MediaWiki. Ignore coding conventions checks.
+// phpcs:ignoreFile -- File external to MediaWiki. Ignore coding conventions checks.
/**
* JSMinPlus version 1.4
*
diff --git a/www/wiki/includes/libs/lockmanager/FSLockManager.php b/www/wiki/includes/libs/lockmanager/FSLockManager.php
index 7f33a0ab..f2624e72 100644
--- a/www/wiki/includes/libs/lockmanager/FSLockManager.php
+++ b/www/wiki/includes/libs/lockmanager/FSLockManager.php
@@ -122,13 +122,13 @@ class FSLockManager extends LockManager {
if ( isset( $this->handles[$path] ) ) {
$handle = $this->handles[$path];
} else {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$handle = fopen( $this->getLockPath( $path ), 'a+' );
if ( !$handle ) { // lock dir missing?
mkdir( $this->lockDir, 0777, true );
$handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
if ( $handle ) {
// Either a shared or exclusive lock
diff --git a/www/wiki/includes/libs/lockmanager/LockManager.php b/www/wiki/includes/libs/lockmanager/LockManager.php
index a6257bfd..6b3cfb44 100644
--- a/www/wiki/includes/libs/lockmanager/LockManager.php
+++ b/www/wiki/includes/libs/lockmanager/LockManager.php
@@ -83,7 +83,7 @@ abstract class LockManager {
$this->domain = isset( $config['domain'] ) ? $config['domain'] : 'global';
if ( isset( $config['lockTTL'] ) ) {
$this->lockTTL = max( 5, $config['lockTTL'] );
- } elseif ( PHP_SAPI === 'cli' ) {
+ } elseif ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) {
$this->lockTTL = 3600;
} else {
$met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
diff --git a/www/wiki/includes/libs/lockmanager/MemcLockManager.php b/www/wiki/includes/libs/lockmanager/MemcLockManager.php
index aecdf60c..6274d605 100644
--- a/www/wiki/includes/libs/lockmanager/MemcLockManager.php
+++ b/www/wiki/includes/libs/lockmanager/MemcLockManager.php
@@ -69,10 +69,10 @@ class MemcLockManager extends QuorumLockManager {
$this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
$memcConfig = isset( $config['memcConfig'] ) ? $config['memcConfig'] : [];
- $memcConfig += [ 'class' => 'MemcachedPhpBagOStuff' ]; // default
+ $memcConfig += [ 'class' => MemcachedPhpBagOStuff::class ]; // default
$class = $memcConfig['class'];
- if ( !is_subclass_of( $class, 'MemcachedBagOStuff' ) ) {
+ if ( !is_subclass_of( $class, MemcachedBagOStuff::class ) ) {
throw new InvalidArgumentException( "$class is not of type MemcachedBagOStuff." );
}
diff --git a/www/wiki/includes/libs/lockmanager/ScopedLock.php b/www/wiki/includes/libs/lockmanager/ScopedLock.php
index 2ad8ac87..6261335e 100644
--- a/www/wiki/includes/libs/lockmanager/ScopedLock.php
+++ b/www/wiki/includes/libs/lockmanager/ScopedLock.php
@@ -70,7 +70,7 @@ class ScopedLock {
public static function factory(
LockManager $manager, array $paths, $type, StatusValue $status, $timeout = 0
) {
- $pathsByType = is_integer( $type ) ? [ $type => $paths ] : $paths;
+ $pathsByType = is_int( $type ) ? [ $type => $paths ] : $paths;
$lockStatus = $manager->lockByType( $pathsByType, $timeout );
$status->merge( $lockStatus );
if ( $lockStatus->isOK() ) {
diff --git a/www/wiki/includes/libs/mime/MimeAnalyzer.php b/www/wiki/includes/libs/mime/MimeAnalyzer.php
index 4d860bb5..8d842cbb 100644
--- a/www/wiki/includes/libs/mime/MimeAnalyzer.php
+++ b/www/wiki/includes/libs/mime/MimeAnalyzer.php
@@ -643,9 +643,9 @@ EOT;
*/
private function doGuessMimeType( $file, $ext ) {
// Read a chunk of the file
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$f = fopen( $file, 'rb' );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$f ) {
return 'unknown/unknown';
@@ -833,9 +833,9 @@ EOT;
}
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$gis = getimagesize( $file );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $gis && isset( $gis['mime'] ) ) {
$mime = $gis['mime'];
diff --git a/www/wiki/includes/libs/mime/XmlTypeCheck.php b/www/wiki/includes/libs/mime/XmlTypeCheck.php
index ea7f9a6c..9770515e 100644
--- a/www/wiki/includes/libs/mime/XmlTypeCheck.php
+++ b/www/wiki/includes/libs/mime/XmlTypeCheck.php
@@ -294,7 +294,7 @@ class XmlTypeCheck {
/**
* @param string $name element or attribute name, maybe with a full or short prefix
- * @param string $namespaceURI the namespaceURI
+ * @param string $namespaceURI
* @return string the name prefixed with namespaceURI
*/
private function expandNS( $name, $namespaceURI ) {
@@ -376,7 +376,7 @@ class XmlTypeCheck {
if ( !$externalCallback && !$generalCallback && !$checkIfSafe ) {
return;
}
- $dtd = $reader->readOuterXML();
+ $dtd = $reader->readOuterXml();
$callbackReturn = false;
if ( $generalCallback ) {
@@ -479,23 +479,23 @@ class XmlTypeCheck {
continue;
}
switch ( $field ) {
- case 'typepublic':
- case 'typesystem':
- $parsed['type'] = $value;
- break;
- case 'pubquote':
- case 'pubapos':
- $parsed['publicid'] = $value;
- break;
- case 'pubsysquote':
- case 'pubsysapos':
- case 'sysquote':
- case 'sysapos':
- $parsed['systemid'] = $value;
- break;
- case 'internal':
- $parsed['internal'] = $value;
- break;
+ case 'typepublic':
+ case 'typesystem':
+ $parsed['type'] = $value;
+ break;
+ case 'pubquote':
+ case 'pubapos':
+ $parsed['publicid'] = $value;
+ break;
+ case 'pubsysquote':
+ case 'pubsysapos':
+ case 'sysquote':
+ case 'sysapos':
+ $parsed['systemid'] = $value;
+ break;
+ case 'internal':
+ $parsed['internal'] = $value;
+ break;
}
}
return $parsed;
diff --git a/www/wiki/includes/libs/mime/mime.info b/www/wiki/includes/libs/mime/mime.info
index d8b8be77..36702439 100644
--- a/www/wiki/includes/libs/mime/mime.info
+++ b/www/wiki/includes/libs/mime/mime.info
@@ -87,6 +87,7 @@ application/x-tcsh [EXECUTABLE]
application/x-tcl [EXECUTABLE]
application/x-perl [EXECUTABLE]
application/x-python [EXECUTABLE]
+application/wasm [EXECUTABLE]
application/pdf application/acrobat [OFFICE]
application/msword [OFFICE]
diff --git a/www/wiki/includes/libs/mime/mime.types b/www/wiki/includes/libs/mime/mime.types
index f1cd59d1..ef6854ce 100644
--- a/www/wiki/includes/libs/mime/mime.types
+++ b/www/wiki/includes/libs/mime/mime.types
@@ -187,3 +187,4 @@ chemical/x-mdl-rdfile rd
chemical/x-mdl-rgfile rg
application/x-amf amf
application/sla stl
+application/wasm wasm
diff --git a/www/wiki/includes/libs/objectcache/BagOStuff.php b/www/wiki/includes/libs/objectcache/BagOStuff.php
index 8a23db51..0ea57a1e 100644
--- a/www/wiki/includes/libs/objectcache/BagOStuff.php
+++ b/www/wiki/includes/libs/objectcache/BagOStuff.php
@@ -70,6 +70,9 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
/** @var callable[] */
protected $busyCallbacks = [];
+ /** @var float|null */
+ private $wallClockOverride;
+
/** @var int[] Map of (ATTR_* class constant => QOS_* class constant) */
protected $attrMap = [];
@@ -473,11 +476,11 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
return null;
}
- $lSince = microtime( true ); // lock timestamp
+ $lSince = $this->getCurrentTime(); // lock timestamp
return new ScopedCallback( function () use ( $key, $lSince, $expiry ) {
$latency = 0.050; // latency skew (err towards keeping lock present)
- $age = ( microtime( true ) - $lSince + $latency );
+ $age = ( $this->getCurrentTime() - $lSince + $latency );
if ( ( $age + $latency ) >= $expiry ) {
$this->logger->warning( "Lock for $key held too long ($age sec)." );
return; // expired; it's not "safe" to delete the key
@@ -691,7 +694,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
*/
protected function convertExpiry( $exptime ) {
if ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) ) {
- return time() + $exptime;
+ return (int)$this->getCurrentTime() + $exptime;
} else {
return $exptime;
}
@@ -706,7 +709,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
*/
protected function convertToRelative( $exptime ) {
if ( $exptime >= ( 10 * self::TTL_YEAR ) ) {
- $exptime -= time();
+ $exptime -= (int)$this->getCurrentTime();
if ( $exptime <= 0 ) {
$exptime = 1;
}
@@ -732,7 +735,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
* @since 1.27
* @param string $keyspace
* @param array $args
- * @return string
+ * @return string Colon-delimited list of $keyspace followed by escaped components of $args
*/
public function makeKeyInternal( $keyspace, $args ) {
$key = $keyspace;
@@ -747,10 +750,11 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
* Make a global cache key.
*
* @since 1.27
- * @param string $keys,... Key component
- * @return string
+ * @param string $class Key class
+ * @param string $component [optional] Key component (starting with a key collection name)
+ * @return string Colon-delimited list of $keyspace followed by escaped components of $args
*/
- public function makeGlobalKey() {
+ public function makeGlobalKey( $class, $component = null ) {
return $this->makeKeyInternal( 'global', func_get_args() );
}
@@ -758,10 +762,11 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
* Make a cache key, scoped to this instance's keyspace.
*
* @since 1.27
- * @param string $keys,... Key component
- * @return string
+ * @param string $class Key class
+ * @param string $component [optional] Key component (starting with a key collection name)
+ * @return string Colon-delimited list of $keyspace followed by escaped components of $args
*/
- public function makeKey() {
+ public function makeKey( $class, $component = null ) {
return $this->makeKeyInternal( $this->keyspace, func_get_args() );
}
@@ -794,4 +799,20 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
return $map;
}
+
+ /**
+ * @return float UNIX timestamp
+ * @codeCoverageIgnore
+ */
+ protected function getCurrentTime() {
+ return $this->wallClockOverride ?: microtime( true );
+ }
+
+ /**
+ * @param float|null &$time Mock UNIX timestamp for testing
+ * @codeCoverageIgnore
+ */
+ public function setMockTime( &$time ) {
+ $this->wallClockOverride =& $time;
+ }
}
diff --git a/www/wiki/includes/libs/objectcache/CachedBagOStuff.php b/www/wiki/includes/libs/objectcache/CachedBagOStuff.php
index c85a82ea..ae434c17 100644
--- a/www/wiki/includes/libs/objectcache/CachedBagOStuff.php
+++ b/www/wiki/includes/libs/objectcache/CachedBagOStuff.php
@@ -86,11 +86,11 @@ class CachedBagOStuff extends HashBagOStuff {
return $this->backend->deleteObjectsExpiringBefore( $date, $progressCallback );
}
- public function makeKey() {
+ public function makeKey( $class, $component = null ) {
return call_user_func_array( [ $this->backend, __FUNCTION__ ], func_get_args() );
}
- public function makeGlobalKey() {
+ public function makeGlobalKey( $class, $component = null ) {
return call_user_func_array( [ $this->backend, __FUNCTION__ ], func_get_args() );
}
diff --git a/www/wiki/includes/libs/objectcache/HashBagOStuff.php b/www/wiki/includes/libs/objectcache/HashBagOStuff.php
index 6d583da0..c6d357b5 100644
--- a/www/wiki/includes/libs/objectcache/HashBagOStuff.php
+++ b/www/wiki/includes/libs/objectcache/HashBagOStuff.php
@@ -52,7 +52,7 @@ class HashBagOStuff extends BagOStuff {
protected function expire( $key ) {
$et = $this->bag[$key][self::KEY_EXP];
- if ( $et == self::TTL_INDEFINITE || $et > time() ) {
+ if ( $et == self::TTL_INDEFINITE || $et > $this->getCurrentTime() ) {
return false;
}
diff --git a/www/wiki/includes/libs/objectcache/IExpiringStore.php b/www/wiki/includes/libs/objectcache/IExpiringStore.php
index 0e09f16f..7bab20a3 100644
--- a/www/wiki/includes/libs/objectcache/IExpiringStore.php
+++ b/www/wiki/includes/libs/objectcache/IExpiringStore.php
@@ -30,6 +30,7 @@
*/
interface IExpiringStore {
// Constants for TTL values, in seconds
+ const TTL_SECOND = 1;
const TTL_MINUTE = 60;
const TTL_HOUR = 3600;
const TTL_DAY = 86400; // 24 * 3600
diff --git a/www/wiki/includes/libs/objectcache/MemcachedBagOStuff.php b/www/wiki/includes/libs/objectcache/MemcachedBagOStuff.php
index 0188991a..f7bf86b9 100644
--- a/www/wiki/includes/libs/objectcache/MemcachedBagOStuff.php
+++ b/www/wiki/includes/libs/objectcache/MemcachedBagOStuff.php
@@ -137,7 +137,7 @@ class MemcachedBagOStuff extends BagOStuff {
);
if ( $charsLeft < 0 ) {
- return $keyspace . ':##' . md5( implode( ':', $args ) );
+ return $keyspace . ':BagOStuff-long-key:##' . md5( implode( ':', $args ) );
}
return $keyspace . ':' . implode( ':', $args );
diff --git a/www/wiki/includes/libs/objectcache/MemcachedClient.php b/www/wiki/includes/libs/objectcache/MemcachedClient.php
index 5cb49a99..59131b97 100644
--- a/www/wiki/includes/libs/objectcache/MemcachedClient.php
+++ b/www/wiki/includes/libs/objectcache/MemcachedClient.php
@@ -1,5 +1,5 @@
<?php
-// @codingStandardsIgnoreFile It's an external lib and it isn't. Let's not bother.
+// phpcs:ignoreFile -- It's an external lib and it isn't. Let's not bother.
/**
* Memcached client for PHP.
*
@@ -363,7 +363,7 @@ class MemcachedClient {
/**
* Changes the TTL on a key from the server to $time
*
- * @param string $key Key
+ * @param string $key
* @param int $time TTL in seconds
*
* @return bool True on success, false on failure
@@ -788,13 +788,13 @@ class MemcachedClient {
$timeout = $this->_connect_timeout;
$errno = $errstr = null;
for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
if ( $this->_persistent == 1 ) {
$sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout );
} else {
$sock = fsockopen( $ip, $port, $errno, $errstr, $timeout );
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
if ( !$sock ) {
$this->_error_log( "Error connecting to $host: $errstr" );
diff --git a/www/wiki/includes/libs/objectcache/MemcachedPhpBagOStuff.php b/www/wiki/includes/libs/objectcache/MemcachedPhpBagOStuff.php
index 971406cf..3ff390b8 100644
--- a/www/wiki/includes/libs/objectcache/MemcachedPhpBagOStuff.php
+++ b/www/wiki/includes/libs/objectcache/MemcachedPhpBagOStuff.php
@@ -62,14 +62,12 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff {
public function incr( $key, $value = 1 ) {
$this->validateKeyEncoding( $key );
- $ret = $this->client->incr( $key, $value );
- return $ret !== null ? $ret : false;
+ return $this->client->incr( $key, $value ) ?? false;
}
public function decr( $key, $value = 1 ) {
$this->validateKeyEncoding( $key );
- $ret = $this->client->decr( $key, $value );
- return $ret !== null ? $ret : false;
+ return $this->client->decr( $key, $value ) ?? false;
}
}
diff --git a/www/wiki/includes/libs/objectcache/MultiWriteBagOStuff.php b/www/wiki/includes/libs/objectcache/MultiWriteBagOStuff.php
index 200ab796..b0305226 100644
--- a/www/wiki/includes/libs/objectcache/MultiWriteBagOStuff.php
+++ b/www/wiki/includes/libs/objectcache/MultiWriteBagOStuff.php
@@ -20,6 +20,7 @@
* @file
* @ingroup Cache
*/
+use Wikimedia\ObjectFactory;
/**
* A cache class that replicates all writes to multiple child caches. Reads
@@ -233,11 +234,11 @@ class MultiWriteBagOStuff extends BagOStuff {
return $ret;
}
- public function makeKey() {
+ public function makeKey( $class, $component = null ) {
return call_user_func_array( [ $this->caches[0], __FUNCTION__ ], func_get_args() );
}
- public function makeGlobalKey() {
+ public function makeGlobalKey( $class, $component = null ) {
return call_user_func_array( [ $this->caches[0], __FUNCTION__ ], func_get_args() );
}
}
diff --git a/www/wiki/includes/libs/objectcache/RedisBagOStuff.php b/www/wiki/includes/libs/objectcache/RedisBagOStuff.php
index 583ec377..f720010f 100644
--- a/www/wiki/includes/libs/objectcache/RedisBagOStuff.php
+++ b/www/wiki/includes/libs/objectcache/RedisBagOStuff.php
@@ -23,7 +23,10 @@
/**
* Redis-based caching module for redis server >= 2.6.12
*
- * @note: avoid use of Redis::MULTI transactions for twemproxy support
+ * @note Avoid use of Redis::MULTI transactions for twemproxy support
+ *
+ * @ingroup Cache
+ * @ingroup Redis
*/
class RedisBagOStuff extends BagOStuff {
/** @var RedisConnectionPool */
diff --git a/www/wiki/includes/libs/objectcache/ReplicatedBagOStuff.php b/www/wiki/includes/libs/objectcache/ReplicatedBagOStuff.php
index 8239491f..56b6e0ec 100644
--- a/www/wiki/includes/libs/objectcache/ReplicatedBagOStuff.php
+++ b/www/wiki/includes/libs/objectcache/ReplicatedBagOStuff.php
@@ -18,6 +18,7 @@
* @file
* @ingroup Cache
*/
+use Wikimedia\ObjectFactory;
/**
* A cache class that directs writes to one set of servers and reads to
diff --git a/www/wiki/includes/libs/objectcache/WANObjectCache.php b/www/wiki/includes/libs/objectcache/WANObjectCache.php
index 1f757a41..658b225b 100644
--- a/www/wiki/includes/libs/objectcache/WANObjectCache.php
+++ b/www/wiki/includes/libs/objectcache/WANObjectCache.php
@@ -19,6 +19,7 @@
* @ingroup Cache
*/
+use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
@@ -46,17 +47,23 @@ use Psr\Log\NullLogger;
* There are three supported ways to handle broadcasted operations:
* - a) Configure the 'purge' EventRelayer to point to a valid PubSub endpoint
* that has subscribed listeners on the cache servers applying the cache updates.
- * - b) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer)
- * and set up mcrouter as the underlying cache backend, using one of the memcached
- * BagOStuff classes as 'cache'. Use OperationSelectorRoute in the mcrouter settings
- * to configure 'set' and 'delete' operations to go to all DCs via AllAsyncRoute and
- * configure other operations to go to the local DC via PoolRoute (for reference,
- * see https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles).
- * - c) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer)
- * and set up dynomite as cache middleware between the web servers and either
- * memcached or redis. This will also broadcast all key setting operations, not just purges,
- * which can be useful for cache warming. Writes are eventually consistent via the
- * Dynamo replication model (see https://github.com/Netflix/dynomite).
+ * - b) Ommit the 'purge' EventRelayer parameter and set up mcrouter as the underlying cache
+ * backend, using a memcached BagOStuff class for the 'cache' parameter. The 'region'
+ * and 'cluster' parameters must be provided and 'mcrouterAware' must be set to 'true'.
+ * Configure mcrouter as follows:
+ * - 1) Use Route Prefixing based on region (datacenter) and cache cluster.
+ * See https://github.com/facebook/mcrouter/wiki/Routing-Prefix and
+ * https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
+ * - 2) To increase the consistency of delete() and touchCheckKey() during cache
+ * server membership changes, you can use the OperationSelectorRoute to
+ * configure 'set' and 'delete' operations to go to all servers in the cache
+ * cluster, instead of just one server determined by hashing.
+ * See https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles
+ * - c) Ommit the 'purge' EventRelayer parameter and set up dynomite as cache middleware
+ * between the web servers and either memcached or redis. This will also broadcast all
+ * key setting operations, not just purges, which can be useful for cache warming.
+ * Writes are eventually consistent via the Dynamo replication model.
+ * See https://github.com/Netflix/dynomite
*
* Broadcasted operations like delete() and touchCheckKey() are done asynchronously
* in all datacenters this way, though the local one should likely be near immediate.
@@ -86,8 +93,20 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
protected $purgeChannel;
/** @var EventRelayer Bus that handles purge broadcasts */
protected $purgeRelayer;
+ /** @bar bool Whether to use mcrouter key prefixing for routing */
+ protected $mcrouterAware;
+ /** @var string Physical region for mcrouter use */
+ protected $region;
+ /** @var string Cache cluster name for mcrouter use */
+ protected $cluster;
/** @var LoggerInterface */
protected $logger;
+ /** @var StatsdDataFactoryInterface */
+ protected $stats;
+ /** @var bool Whether to use "interim" caching while keys are tombstoned */
+ protected $useInterimHoldOffCaching = true;
+ /** @var callable|null Function that takes a WAN cache callback and runs it later */
+ protected $asyncHandler;
/** @var int ERR_* constant for the "last error" registry */
protected $lastRelayError = self::ERR_NONE;
@@ -99,6 +118,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
/** @var int Key fetched */
private $warmupKeyMisses = 0;
+ /** @var float|null */
+ private $wallClockOverride;
+
/** Max time expected to pass between delete() and DB commit finishing */
const MAX_COMMIT_DELAY = 3;
/** Max replication+snapshot lag before applying TTL_LAGGED or disallowing set() */
@@ -108,12 +130,13 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
/** Seconds to keep dependency purge keys around */
const CHECK_KEY_TTL = self::TTL_YEAR;
+ /** Seconds to keep interim value keys for tombstoned keys around */
+ const INTERIM_KEY_TTL = 1;
+
/** Seconds to keep lock keys around */
const LOCK_TTL = 10;
/** Default remaining TTL at which to consider pre-emptive regeneration */
const LOW_TTL = 30;
- /** Default time-since-expiry on a miss that makes a key "hot" */
- const LOCK_TSE = 1;
/** Never consider performing "popularity" refreshes until a key reaches this age */
const AGE_NEW = 60;
@@ -132,6 +155,11 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
const TTL_LAGGED = 30;
/** Idiom for delete() for "no hold-off" */
const HOLDOFF_NONE = 0;
+ /** Idiom for set()/getWithSetCallback() for "do not augment the storage medium TTL" */
+ const STALE_TTL_NONE = 0;
+ /** Idiom for set()/getWithSetCallback() for "no post-expired grace period" */
+ const GRACE_TTL_NONE = 0;
+
/** Idiom for getWithSetCallback() for "no minimum required as-of timestamp" */
const MIN_TIMESTAMP_NONE = 0.0;
@@ -177,6 +205,24 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* - channels : Map of (action => channel string). Actions include "purge".
* - relayers : Map of (action => EventRelayer object). Actions include "purge".
* - logger : LoggerInterface object
+ * - stats : LoggerInterface object
+ * - asyncHandler : A function that takes a callback and runs it later. If supplied,
+ * whenever a preemptive refresh would be triggered in getWithSetCallback(), the
+ * current cache value is still used instead. However, the async-handler function
+ * receives a WAN cache callback that, when run, will execute the value generation
+ * callback supplied by the getWithSetCallback() caller. The result will be saved
+ * as normal. The handler is expected to call the WAN cache callback at an opportune
+ * time (e.g. HTTP post-send), though generally within a few 100ms. [optional]
+ * - region: the current physical region. This is required when using mcrouter as the
+ * backing store proxy. [optional]
+ * - cluster: name of the cache cluster used by this WAN cache. The name must be the
+ * same in all datacenters; the ("region","cluster") tuple is what distinguishes
+ * the counterpart cache clusters among all the datacenter. The contents of
+ * https://github.com/facebook/mcrouter/wiki/Config-Files give background on this.
+ * This is required when using mcrouter as the backing store proxy. [optional]
+ * - mcrouterAware: set as true if mcrouter is the backing store proxy and mcrouter
+ * is configured to interpret /<region>/<cluster>/ key prefixes as routes. This
+ * requires that "region" and "cluster" are both set above. [optional]
*/
public function __construct( array $params ) {
$this->cache = $params['cache'];
@@ -186,9 +232,18 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
$this->purgeRelayer = isset( $params['relayers']['purge'] )
? $params['relayers']['purge']
: new EventRelayerNull( [] );
+ $this->region = isset( $params['region'] ) ? $params['region'] : 'main';
+ $this->cluster = isset( $params['cluster'] ) ? $params['cluster'] : 'wan-main';
+ $this->mcrouterAware = !empty( $params['mcrouterAware'] );
+
$this->setLogger( isset( $params['logger'] ) ? $params['logger'] : new NullLogger() );
+ $this->stats = isset( $params['stats'] ) ? $params['stats'] : new NullStatsdDataFactory();
+ $this->asyncHandler = isset( $params['asyncHandler'] ) ? $params['asyncHandler'] : null;
}
+ /**
+ * @param LoggerInterface $logger
+ */
public function setLogger( LoggerInterface $logger ) {
$this->logger = $logger;
}
@@ -199,9 +254,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* @return WANObjectCache
*/
public static function newEmpty() {
- return new self( [
- 'cache' => new EmptyBagOStuff(),
- 'pool' => 'empty'
+ return new static( [
+ 'cache' => new EmptyBagOStuff()
] );
}
@@ -231,7 +285,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* (e.g. the default REPEATABLE-READ in innoDB). Even for mutable data, that
* isolation can largely be maintained by doing the following:
* - a) Calling delete() on entity change *and* creation, before DB commit
- * - b) Keeping transaction duration shorter than delete() hold-off TTL
+ * - b) Keeping transaction duration shorter than the delete() hold-off TTL
+ * - c) Disabling interim key caching via useInterimHoldOffCaching() before get() calls
*
* However, pre-snapshot values might still be seen if an update was made
* in a remote datacenter but the purge from delete() didn't relay yet.
@@ -239,7 +294,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* Consider using getWithSetCallback() instead of get() and set() cycles.
* That method has cache slam avoiding features for hot/expensive keys.
*
- * @param string $key Cache key
+ * @param string $key Cache key made from makeKey() or makeGlobalKey()
* @param mixed &$curTTL Approximate TTL left on the key if present/tombstoned [returned]
* @param array $checkKeys List of "check" keys
* @param float &$asOf UNIX timestamp of cached value; null on failure [returned]
@@ -260,7 +315,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
*
* @see WANObjectCache::get()
*
- * @param array $keys List of cache keys
+ * @param array $keys List of cache keys made from makeKey() or makeGlobalKey()
* @param array &$curTTLs Map of (key => approximate TTL left) for existing keys [returned]
* @param array $checkKeys List of check keys to apply to all $keys. May also apply "check"
* keys to specific cache keys only by using cache keys as keys in the $checkKeys array.
@@ -306,7 +361,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
$wrappedValues += $this->cache->getMulti( $keysGet );
}
// Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
- $now = microtime( true );
+ $now = $this->getCurrentTime();
// Collect timestamps from all "check" keys
$purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
@@ -362,13 +417,13 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
$purgeValues = [];
foreach ( $timeKeys as $timeKey ) {
$purge = isset( $wrappedValues[$timeKey] )
- ? self::parsePurgeValue( $wrappedValues[$timeKey] )
+ ? $this->parsePurgeValue( $wrappedValues[$timeKey] )
: false;
if ( $purge === false ) {
// Key is not set or invalid; regenerate
$newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL );
$this->cache->add( $timeKey, $newVal, self::CHECK_KEY_TTL );
- $purge = self::parsePurgeValue( $newVal );
+ $purge = $this->parsePurgeValue( $newVal );
}
$purgeValues[] = $purge;
}
@@ -392,6 +447,12 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
*
* Setting 'lag' and 'since' help avoids keys getting stuck in stale states.
*
+ * Be aware that this does not update the process cache for getWithSetCallback()
+ * callers. Keys accessed via that method are not generally meant to also be set
+ * using this primitive method.
+ *
+ * Do not use this method on versioned keys accessed via getWithSetCallback().
+ *
* Example usage:
* @code
* $dbr = wfGetDB( DB_REPLICA );
@@ -421,28 +482,30 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* they certainly should not see ones that ended up getting rolled back.
* Default: false
* - lockTSE : if excessive replication/snapshot lag is detected, then store the value
- * with this TTL and flag it as stale. This is only useful if the reads for
- * this key use getWithSetCallback() with "lockTSE" set.
+ * with this TTL and flag it as stale. This is only useful if the reads for this key
+ * use getWithSetCallback() with "lockTSE" set. Note that if "staleTTL" is set
+ * then it will still add on to this TTL in the excessive lag scenario.
* Default: WANObjectCache::TSE_NONE
* - staleTTL : Seconds to keep the key around if it is stale. The get()/getMulti()
* methods return such stale values with a $curTTL of 0, and getWithSetCallback()
* will call the regeneration callback in such cases, passing in the old value
* and its as-of time to the callback. This is useful if adaptiveTTL() is used
* on the old value's as-of time when it is verified as still being correct.
- * Default: 0.
+ * Default: WANObjectCache::STALE_TTL_NONE.
* @note Options added in 1.28: staleTTL
* @return bool Success
*/
final public function set( $key, $value, $ttl = 0, array $opts = [] ) {
- $now = microtime( true );
+ $now = $this->getCurrentTime();
$lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
+ $staleTTL = isset( $opts['staleTTL'] ) ? $opts['staleTTL'] : self::STALE_TTL_NONE;
$age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
$lag = isset( $opts['lag'] ) ? $opts['lag'] : 0;
- $staleTTL = isset( $opts['staleTTL'] ) ? $opts['staleTTL'] : 0;
// Do not cache potentially uncommitted data as it might get rolled back
if ( !empty( $opts['pending'] ) ) {
- $this->logger->info( "Rejected set() for $key due to pending writes." );
+ $this->logger->info( 'Rejected set() for {cachekey} due to pending writes.',
+ [ 'cachekey' => $key ] );
return true; // no-op the write for being unsafe
}
@@ -456,16 +519,19 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
$wrapExtra[self::FLD_FLAGS] = self::FLG_STALE; // mark as stale
// Case B: any long-running transaction; ignore this set()
} elseif ( $age > self::MAX_READ_LAG ) {
- $this->logger->info( "Rejected set() for $key due to snapshot lag." );
+ $this->logger->info( 'Rejected set() for {cachekey} due to snapshot lag.',
+ [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ] );
return true; // no-op the write for being unsafe
// Case C: high replication lag; lower TTL instead of ignoring all set()s
} elseif ( $lag === false || $lag > self::MAX_READ_LAG ) {
$ttl = $ttl ? min( $ttl, self::TTL_LAGGED ) : self::TTL_LAGGED;
- $this->logger->warning( "Lowered set() TTL for $key due to replication lag." );
+ $this->logger->warning( 'Lowered set() TTL for {cachekey} due to replication lag.',
+ [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ] );
// Case D: medium length request with medium replication lag; ignore this set()
} else {
- $this->logger->info( "Rejected set() for $key due to high read lag." );
+ $this->logger->info( 'Rejected set() for {cachekey} due to high read lag.',
+ [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ] );
return true; // no-op the write for being unsafe
}
@@ -503,6 +569,10 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
*
* Note that set() can also be lag-aware and lower the TTL if it's high.
*
+ * Be aware that this does not clear the process cache. Even if it did, callbacks
+ * used by getWithSetCallback() might still return stale data in the case of either
+ * uncommitted or not-yet-replicated changes (callback generally use replica DBs).
+ *
* When using potentially long-running ACID transactions, a good pattern is
* to use a pre-commit hook to issue the delete. This means that immediately
* after commit, callers will see the tombstone in cache upon purge relay.
@@ -571,25 +641,102 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* Note that "check" keys won't collide with other regular keys.
*
* @param string $key
- * @return float UNIX timestamp of the check key
+ * @return float UNIX timestamp
*/
final public function getCheckKeyTime( $key ) {
- $key = self::TIME_KEY_PREFIX . $key;
+ return $this->getMultiCheckKeyTime( [ $key ] )[$key];
+ }
- $purge = self::parsePurgeValue( $this->cache->get( $key ) );
- if ( $purge !== false ) {
- $time = $purge[self::FLD_TIME];
- } else {
- // Casting assures identical floats for the next getCheckKeyTime() calls
- $now = (string)microtime( true );
- $this->cache->add( $key,
- $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
- self::CHECK_KEY_TTL
- );
- $time = (float)$now;
+ /**
+ * Fetch the values of each timestamp "check" key
+ *
+ * This works like getCheckKeyTime() except it takes a list of keys
+ * and returns a map of timestamps instead of just that of one key
+ *
+ * This might be useful if both:
+ * - a) a class of entities each depend on hundreds of other entities
+ * - b) these other entities are depended upon by millions of entities
+ *
+ * The later entities can each use a "check" key to invalidate their dependee entities.
+ * However, it is expensive for the former entities to verify against all of the relevant
+ * "check" keys during each getWithSetCallback() call. A less expensive approach is to do
+ * these verifications only after a "time-till-verify" (TTV) has passed. This is a middle
+ * ground between using blind TTLs and using constant verification. The adaptiveTTL() method
+ * can be used to dynamically adjust the TTV. Also, the initial TTV can make use of the
+ * last-modified times of the dependant entities (either from the DB or the "check" keys).
+ *
+ * Example usage:
+ * @code
+ * $value = $cache->getWithSetCallback(
+ * $cache->makeGlobalKey( 'wikibase-item', $id ),
+ * self::INITIAL_TTV, // initial time-till-verify
+ * function ( $oldValue, &$ttv, &$setOpts, $oldAsOf ) use ( $checkKeys, $cache ) {
+ * $now = microtime( true );
+ * // Use $oldValue if it passes max ultimate age and "check" key comparisons
+ * if ( $oldValue &&
+ * $oldAsOf > max( $cache->getMultiCheckKeyTime( $checkKeys ) ) &&
+ * ( $now - $oldValue['ctime'] ) <= self::MAX_CACHE_AGE
+ * ) {
+ * // Increase time-till-verify by 50% of last time to reduce overhead
+ * $ttv = $cache->adaptiveTTL( $oldAsOf, self::MAX_TTV, self::MIN_TTV, 1.5 );
+ * // Unlike $oldAsOf, "ctime" is the ultimate age of the cached data
+ * return $oldValue;
+ * }
+ *
+ * $mtimes = []; // dependency last-modified times; passed by reference
+ * $value = [ 'data' => $this->fetchEntityData( $mtimes ), 'ctime' => $now ];
+ * // Guess time-till-change among the dependencies, e.g. 1/(total change rate)
+ * $ttc = 1 / array_sum( array_map(
+ * function ( $mtime ) use ( $now ) {
+ * return 1 / ( $mtime ? ( $now - $mtime ) : 900 );
+ * },
+ * $mtimes
+ * ) );
+ * // The time-to-verify should not be overly pessimistic nor optimistic
+ * $ttv = min( max( $ttc, self::MIN_TTV ), self::MAX_TTV );
+ *
+ * return $value;
+ * },
+ * [ 'staleTTL' => $cache::TTL_DAY ] // keep around to verify and re-save
+ * );
+ * @endcode
+ *
+ * @see WANObjectCache::getCheckKeyTime()
+ * @see WANObjectCache::getWithSetCallback()
+ *
+ * @param array $keys
+ * @return float[] Map of (key => UNIX timestamp)
+ * @since 1.31
+ */
+ final public function getMultiCheckKeyTime( array $keys ) {
+ $rawKeys = [];
+ foreach ( $keys as $key ) {
+ $rawKeys[$key] = self::TIME_KEY_PREFIX . $key;
+ }
+
+ $rawValues = $this->cache->getMulti( $rawKeys );
+ $rawValues += array_fill_keys( $rawKeys, false );
+
+ $times = [];
+ foreach ( $rawKeys as $key => $rawKey ) {
+ $purge = $this->parsePurgeValue( $rawValues[$rawKey] );
+ if ( $purge !== false ) {
+ $time = $purge[self::FLD_TIME];
+ } else {
+ // Casting assures identical floats for the next getCheckKeyTime() calls
+ $now = (string)$this->getCurrentTime();
+ $this->cache->add(
+ $rawKey,
+ $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
+ self::CHECK_KEY_TTL
+ );
+ $time = (float)$now;
+ }
+
+ $times[$key] = $time;
}
- return $time;
+ return $times;
}
/**
@@ -600,20 +747,21 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* on all keys that should be changed. When get() is called on those
* keys, the relevant "check" keys must be supplied for this to work.
*
- * The "check" key essentially represents a last-modified field.
- * When touched, the field will be updated on all cache servers.
- * Keys using it via get(), getMulti(), or getWithSetCallback() will
- * be invalidated. It is treated as being HOLDOFF_TTL seconds in the future
- * by those methods to avoid race conditions where dependent keys get updated
- * with stale values (e.g. from a DB replica DB).
- *
- * This is typically useful for keys with hardcoded names or in some cases
- * dynamically generated names where a low number of combinations exist.
- * When a few important keys get a large number of hits, a high cache
- * time is usually desired as well as "lockTSE" logic. The resetCheckKey()
- * method is less appropriate in such cases since the "time since expiry"
- * cannot be inferred, causing any get() after the reset to treat the key
- * as being "hot", resulting in more stale value usage.
+ * The "check" key essentially represents a last-modified time of an entity.
+ * When the key is touched, the timestamp will be updated to the current time.
+ * Keys using the "check" key via get(), getMulti(), or getWithSetCallback() will
+ * be invalidated. This approach is useful if many keys depend on a single entity.
+ *
+ * The timestamp of the "check" key is treated as being HOLDOFF_TTL seconds in the
+ * future by get*() methods in order to avoid race conditions where keys are updated
+ * with stale values (e.g. from a lagged replica DB). A high TTL is set on the "check"
+ * key, making it possible to know the timestamp of the last change to the corresponding
+ * entities in most cases. This might use more cache space than resetCheckKey().
+ *
+ * When a few important keys get a large number of hits, a high cache time is usually
+ * desired as well as "lockTSE" logic. The resetCheckKey() method is less appropriate
+ * in such cases since the "time since expiry" cannot be inferred, causing any get()
+ * after the reset to treat the key as being "hot", resulting in more stale value usage.
*
* Note that "check" keys won't collide with other regular keys.
*
@@ -644,12 +792,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* to, any temporary ejection of that server will cause the value to be
* seen as purged as a new server will initialize the "check" key.
*
- * The advantage is that this does not place high TTL keys on every cache
- * server, making it better for code that will cache many different keys
- * and either does not use lockTSE or uses a low enough TTL anyway.
- *
- * This is typically useful for keys with dynamically generated names
- * where a high number of combinations exist.
+ * The advantage here is that the "check" keys, which have high TTLs, will only
+ * be created when a get*() method actually uses that key. This is better when
+ * a large number of "check" keys are invalided in a short period of time.
*
* Note that "check" keys won't collide with other regular keys.
*
@@ -686,8 +831,11 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* having to inspect a "current time left" variable (e.g. $curTTL, $curTTLs), a cache
* regeneration will automatically be triggered using the callback.
*
- * The simplest way to avoid stampedes for hot keys is to use
- * the 'lockTSE' option in $opts. If cache purges are needed, also:
+ * The $ttl argument and "hotTTR" option (in $opts) use time-dependant randomization
+ * to avoid stampedes. Keys that are slow to regenerate and either heavily used
+ * or subject to explicit (unpredictable) purges, may need additional mechanisms.
+ * The simplest way to avoid stampedes for such keys is to use 'lockTSE' (in $opts).
+ * If explicit purges are needed, also:
* - a) Pass $key into $checkKeys
* - b) Use touchCheckKey( $key ) instead of delete( $key )
*
@@ -793,18 +941,60 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* );
* @endcode
*
+ * Example usage (key holding an LRU subkey:value map; this can avoid flooding cache with
+ * keys for an unlimited set of (constraint,situation) pairs, thereby avoiding elevated
+ * cache evictions and wasted memory):
+ * @code
+ * $catSituationTolerabilityCache = $this->cache->getWithSetCallback(
+ * // Group by constraint ID/hash, cat family ID/hash, or something else useful
+ * $this->cache->makeKey( 'cat-situation-tolerablity-checks', $groupKey ),
+ * WANObjectCache::TTL_DAY, // rarely used groups should fade away
+ * // The $scenarioKey format is $constraintId:<ID/hash of $situation>
+ * function ( $cacheMap ) use ( $scenarioKey, $constraintId, $situation ) {
+ * $lruCache = MapCacheLRU::newFromArray( $cacheMap ?: [], self::CACHE_SIZE );
+ * $result = $lruCache->get( $scenarioKey ); // triggers LRU bump if present
+ * if ( $result === null || $this->isScenarioResultExpired( $result ) ) {
+ * $result = $this->checkScenarioTolerability( $constraintId, $situation );
+ * $lruCache->set( $scenarioKey, $result, 3 / 8 );
+ * }
+ * // Save the new LRU cache map and reset the map's TTL
+ * return $lruCache->toArray();
+ * },
+ * [
+ * // Once map is > 1 sec old, consider refreshing
+ * 'ageNew' => 1,
+ * // Update within 5 seconds after "ageNew" given a 1hz cache check rate
+ * 'hotTTR' => 5,
+ * // Avoid querying cache servers multiple times in a request; this also means
+ * // that a request can only alter the value of any given constraint key once
+ * 'pcTTL' => WANObjectCache::TTL_PROC_LONG
+ * ]
+ * );
+ * $tolerability = isset( $catSituationTolerabilityCache[$scenarioKey] )
+ * ? $catSituationTolerabilityCache[$scenarioKey]
+ * : $this->checkScenarioTolerability( $constraintId, $situation );
+ * @endcode
+ *
* @see WANObjectCache::get()
* @see WANObjectCache::set()
*
- * @param string $key Cache key
+ * @param string $key Cache key made from makeKey() or makeGlobalKey()
* @param int $ttl Seconds to live for key updates. Special values are:
- * - WANObjectCache::TTL_INDEFINITE: Cache forever
- * - WANObjectCache::TTL_UNCACHEABLE: Do not cache at all
+ * - WANObjectCache::TTL_INDEFINITE: Cache forever (subject to LRU-style evictions)
+ * - WANObjectCache::TTL_UNCACHEABLE: Do not cache (if the key exists, it is not deleted)
* @param callable $callback Value generation function
* @param array $opts Options map:
* - checkKeys: List of "check" keys. The key at $key will be seen as invalid when either
- * touchCheckKey() or resetCheckKey() is called on any of these keys.
+ * touchCheckKey() or resetCheckKey() is called on any of the keys in this list. This
+ * is useful if thousands or millions of keys depend on the same entity. The entity can
+ * simply have its "check" key updated whenever the entity is modified.
* Default: [].
+ * - graceTTL: Consider reusing expired values instead of refreshing them if they expired
+ * less than this many seconds ago. The odds of a refresh becomes more likely over time,
+ * becoming certain once the grace period is reached. This can reduce traffic spikes
+ * when millions of keys are compared to the same "check" key and touchCheckKey()
+ * or resetCheckKey() is called on that "check" key.
+ * Default: WANObjectCache::GRACE_TTL_NONE.
* - lockTSE: If the key is tombstoned or expired (by checkKeys) less than this many seconds
* ago, then try to have a single thread handle cache regeneration at any given time.
* Other threads will try to use stale values if possible. If, on miss, the time since
@@ -839,19 +1029,29 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* This is useful if the source of a key is suspected of having possibly changed
* recently, and the caller wants any such changes to be reflected.
* Default: WANObjectCache::MIN_TIMESTAMP_NONE.
- * - hotTTR: Expected time-till-refresh for keys that average ~1 hit/second.
- * This should be greater than "ageNew". Keys with higher hit rates will regenerate
- * more often. This is useful when a popular key is changed but the cache purge was
- * delayed or lost. Seldom used keys are rarely affected by this setting, unless an
- * extremely low "hotTTR" value is passed in.
+ * - hotTTR: Expected time-till-refresh (TTR) in seconds for keys that average ~1 hit per
+ * second (e.g. 1Hz). Keys with a hit rate higher than 1Hz will refresh sooner than this
+ * TTR and vise versa. Such refreshes won't happen until keys are "ageNew" seconds old.
+ * This uses randomization to avoid triggering cache stampedes. The TTR is useful at
+ * reducing the impact of missed cache purges, since the effect of a heavily referenced
+ * key being stale is worse than that of a rarely referenced key. Unlike simply lowering
+ * $ttl, seldomly used keys are largely unaffected by this option, which makes it
+ * possible to have a high hit rate for the "long-tail" of less-used keys.
* Default: WANObjectCache::HOT_TTR.
* - lowTTL: Consider pre-emptive updates when the current TTL (seconds) of the key is less
* than this. It becomes more likely over time, becoming certain once the key is expired.
+ * This helps avoid cache stampedes that might be triggered due to the key expiring.
* Default: WANObjectCache::LOW_TTL.
* - ageNew: Consider popularity refreshes only once a key reaches this age in seconds.
* Default: WANObjectCache::AGE_NEW.
+ * - staleTTL: Seconds to keep the key around if it is stale. This means that on cache
+ * miss the callback may get $oldValue/$oldAsOf values for keys that have already been
+ * expired for this specified time. This is useful if adaptiveTTL() is used on the old
+ * value's as-of time when it is verified as still being correct.
+ * Default: WANObjectCache::STALE_TTL_NONE
* @return mixed Value found or written to the key
* @note Options added in 1.28: version, busyValue, hotTTR, ageNew, pcGroup, minAsOf
+ * @note Options added in 1.31: staleTTL, graceTTL
* @note Callable type hints are not used to avoid class-autoloading
*/
final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
@@ -881,11 +1081,14 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
use ( $callback, $version ) {
if ( is_array( $oldValue )
&& array_key_exists( self::VFLD_DATA, $oldValue )
+ && array_key_exists( self::VFLD_VERSION, $oldValue )
+ && $oldValue[self::VFLD_VERSION] === $version
) {
$oldData = $oldValue[self::VFLD_DATA];
} else {
// VFLD_DATA is not set if an old, unversioned, key is present
$oldData = false;
+ $oldAsOf = null;
}
return [
@@ -903,7 +1106,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
// Value existed before with a different version; use variant key.
// Reflect purges to $key by requiring that this key value be newer.
$value = $this->doGetWithSetCallback(
- 'cache-variant:' . md5( $key ) . ":$version",
+ $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
$ttl,
$callback,
// Regenerate value if not newer than $key
@@ -939,6 +1142,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
protected function doGetWithSetCallback( $key, $ttl, $callback, array $opts, &$asOf = null ) {
$lowTTL = isset( $opts['lowTTL'] ) ? $opts['lowTTL'] : min( self::LOW_TTL, $ttl );
$lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
+ $staleTTL = isset( $opts['staleTTL'] ) ? $opts['staleTTL'] : self::STALE_TTL_NONE;
+ $graceTTL = isset( $opts['graceTTL'] ) ? $opts['graceTTL'] : self::GRACE_TTL_NONE;
$checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
$busyValue = isset( $opts['busyValue'] ) ? $opts['busyValue'] : null;
$popWindow = isset( $opts['hotTTR'] ) ? $opts['hotTTR'] : self::HOT_TTR;
@@ -946,24 +1151,48 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
$minTime = isset( $opts['minAsOf'] ) ? $opts['minAsOf'] : self::MIN_TIMESTAMP_NONE;
$versioned = isset( $opts['version'] );
+ // Get a collection name to describe this class of key
+ $kClass = $this->determineKeyClass( $key );
+
// Get the current key value
$curTTL = null;
$cValue = $this->get( $key, $curTTL, $checkKeys, $asOf ); // current value
$value = $cValue; // return value
- $preCallbackTime = microtime( true );
+ $preCallbackTime = $this->getCurrentTime();
// Determine if a cached value regeneration is needed or desired
if ( $value !== false
- && $curTTL > 0
+ && $this->isAliveOrInGracePeriod( $curTTL, $graceTTL )
&& $this->isValid( $value, $versioned, $asOf, $minTime )
- && !$this->worthRefreshExpiring( $curTTL, $lowTTL )
- && !$this->worthRefreshPopular( $asOf, $ageNew, $popWindow, $preCallbackTime )
) {
- return $value;
+ $preemptiveRefresh = (
+ $this->worthRefreshExpiring( $curTTL, $lowTTL ) ||
+ $this->worthRefreshPopular( $asOf, $ageNew, $popWindow, $preCallbackTime )
+ );
+
+ if ( !$preemptiveRefresh ) {
+ $this->stats->increment( "wanobjectcache.$kClass.hit.good" );
+
+ return $value;
+ } elseif ( $this->asyncHandler ) {
+ // Update the cache value later, such during post-send of an HTTP request
+ $func = $this->asyncHandler;
+ $func( function () use ( $key, $ttl, $callback, $opts, $asOf ) {
+ $opts['minAsOf'] = INF; // force a refresh
+ $this->doGetWithSetCallback( $key, $ttl, $callback, $opts, $asOf );
+ } );
+ $this->stats->increment( "wanobjectcache.$kClass.hit.refresh" );
+
+ return $value;
+ }
}
// A deleted key with a negative TTL left must be tombstoned
$isTombstone = ( $curTTL !== null && $value === false );
+ if ( $isTombstone && $lockTSE <= 0 ) {
+ // Use the INTERIM value for tombstoned keys to reduce regeneration load
+ $lockTSE = self::INTERIM_KEY_TTL;
+ }
// Assume a key is hot if requested soon after invalidation
$isHot = ( $curTTL !== null && $curTTL <= 0 && abs( $curTTL ) <= $lockTSE );
// Use the mutex if there is no value and a busy fallback is given
@@ -981,21 +1210,23 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
// Lock acquired; this thread should update the key
$lockAcquired = true;
} elseif ( $value !== false && $this->isValid( $value, $versioned, $asOf, $minTime ) ) {
+ $this->stats->increment( "wanobjectcache.$kClass.hit.stale" );
// If it cannot be acquired; then the stale value can be used
return $value;
} else {
// Use the INTERIM value for tombstoned keys to reduce regeneration load.
// For hot keys, either another thread has the lock or the lock failed;
// use the INTERIM value from the last thread that regenerated it.
- $wrapped = $this->cache->get( self::INTERIM_KEY_PREFIX . $key );
- list( $value ) = $this->unwrap( $wrapped, microtime( true ) );
- if ( $value !== false && $this->isValid( $value, $versioned, $asOf, $minTime ) ) {
- $asOf = $wrapped[self::FLD_TIME];
+ $value = $this->getInterimValue( $key, $versioned, $minTime, $asOf );
+ if ( $value !== false ) {
+ $this->stats->increment( "wanobjectcache.$kClass.hit.volatile" );
return $value;
}
// Use the busy fallback value if nothing else
if ( $busyValue !== null ) {
+ $this->stats->increment( "wanobjectcache.$kClass.miss.busy" );
+
return is_callable( $busyValue ) ? $busyValue() : $busyValue;
}
}
@@ -1013,25 +1244,21 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
} finally {
--$this->callbackDepth;
}
+ $valueIsCacheable = ( $value !== false && $ttl >= 0 );
+
// When delete() is called, writes are write-holed by the tombstone,
// so use a special INTERIM key to pass the new value around threads.
- if ( ( $isTombstone && $lockTSE > 0 ) && $value !== false && $ttl >= 0 ) {
+ if ( ( $isTombstone && $lockTSE > 0 ) && $valueIsCacheable ) {
$tempTTL = max( 1, (int)$lockTSE ); // set() expects seconds
- $newAsOf = microtime( true );
+ $newAsOf = $this->getCurrentTime();
$wrapped = $this->wrap( $value, $tempTTL, $newAsOf );
// Avoid using set() to avoid pointless mcrouter broadcasting
- $this->cache->merge(
- self::INTERIM_KEY_PREFIX . $key,
- function () use ( $wrapped ) {
- return $wrapped;
- },
- $tempTTL,
- 1
- );
+ $this->setInterimValue( $key, $wrapped, $tempTTL );
}
- if ( $value !== false && $ttl >= 0 ) {
+ if ( $valueIsCacheable ) {
$setOpts['lockTSE'] = $lockTSE;
+ $setOpts['staleTTL'] = $staleTTL;
// Use best known "since" timestamp if not provided
$setOpts += [ 'since' => $preCallbackTime ];
// Update the cache; this will fail if the key is tombstoned
@@ -1040,13 +1267,54 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
if ( $lockAcquired ) {
// Avoid using delete() to avoid pointless mcrouter broadcasting
- $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, 1 );
+ $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, (int)$preCallbackTime - 60 );
}
+ $this->stats->increment( "wanobjectcache.$kClass.miss.compute" );
+
return $value;
}
/**
+ * @param string $key
+ * @param bool $versioned
+ * @param float $minTime
+ * @param mixed &$asOf
+ * @return mixed
+ */
+ protected function getInterimValue( $key, $versioned, $minTime, &$asOf ) {
+ if ( !$this->useInterimHoldOffCaching ) {
+ return false; // disabled
+ }
+
+ $wrapped = $this->cache->get( self::INTERIM_KEY_PREFIX . $key );
+ list( $value ) = $this->unwrap( $wrapped, $this->getCurrentTime() );
+ if ( $value !== false && $this->isValid( $value, $versioned, $asOf, $minTime ) ) {
+ $asOf = $wrapped[self::FLD_TIME];
+
+ return $value;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param string $key
+ * @param array $wrapped
+ * @param int $tempTTL
+ */
+ protected function setInterimValue( $key, $wrapped, $tempTTL ) {
+ $this->cache->merge(
+ self::INTERIM_KEY_PREFIX . $key,
+ function () use ( $wrapped ) {
+ return $wrapped;
+ },
+ $tempTTL,
+ 1
+ );
+ }
+
+ /**
* Method to fetch multiple cache keys at once with regeneration
*
* This works the same as getWithSetCallback() except:
@@ -1083,7 +1351,15 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* $setOpts += Database::getCacheSetOptions( $dbr );
*
* // Load the row for this file
- * $row = $dbr->selectRow( 'file', '*', [ 'id' => $id ], __METHOD__ );
+ * $queryInfo = File::getQueryInfo();
+ * $row = $dbr->selectRow(
+ * $queryInfo['tables'],
+ * $queryInfo['fields'],
+ * [ 'id' => $id ],
+ * __METHOD__,
+ * [],
+ * $queryInfo['joins']
+ * );
*
* return $row ? (array)$row : false;
* },
@@ -1139,8 +1415,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* This works the same as getWithSetCallback() except:
* - a) The $keys argument expects the result of WANObjectCache::makeMultiKeys()
* - b) The $callback argument expects a callback returning a map of (ID => new value)
- * for all entity IDs in $regenById and it takes the following arguments:
- * - $ids: a list of entity IDs to regenerate
+ * for all entity IDs in $ids and it takes the following arguments:
+ * - $ids: a list of entity IDs that require cache regeneration
* - &$ttls: a reference to the (entity ID => new TTL) map
* - &$setOpts: a reference to options for set() which can be altered
* - c) The return value is a map of (cache key => value) in the order of $keyedIds
@@ -1169,7 +1445,15 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
*
* // Load the rows for these files
* $rows = [];
- * $res = $dbr->select( 'file', '*', [ 'id' => $ids ], __METHOD__ );
+ * $queryInfo = File::getQueryInfo();
+ * $res = $dbr->select(
+ * $queryInfo['tables'],
+ * $queryInfo['fields'],
+ * [ 'id' => $ids ],
+ * __METHOD__,
+ * [],
+ * $queryInfo['joins']
+ * );
* foreach ( $res as $row ) {
* $rows[$row->id] = $row;
* $mtime = wfTimestamp( TS_UNIX, $row->timestamp );
@@ -1255,7 +1539,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
}
/**
- * Locally set a key to expire soon if it is stale based on $purgeTimestamp
+ * Set a key to soon expire in the local cluster if it pre-dates $purgeTimestamp
*
* This sets stale keys' time-to-live at HOLDOFF_TTL seconds, which both avoids
* broadcasting in mcrouter setups and also avoids races with new tombstones.
@@ -1266,7 +1550,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* @return bool Success
* @since 1.28
*/
- public function reap( $key, $purgeTimestamp, &$isStale = false ) {
+ final public function reap( $key, $purgeTimestamp, &$isStale = false ) {
$minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
$wrapped = $this->cache->get( self::VALUE_KEY_PREFIX . $key );
if ( is_array( $wrapped ) && $wrapped[self::FLD_TIME] < $minAsOf ) {
@@ -1287,7 +1571,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
}
/**
- * Locally set a "check" key to expire soon if it is stale based on $purgeTimestamp
+ * Set a "check" key to soon expire in the local cluster if it pre-dates $purgeTimestamp
*
* @param string $key Cache key
* @param int $purgeTimestamp UNIX timestamp of purge
@@ -1295,12 +1579,12 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* @return bool Success
* @since 1.28
*/
- public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
+ final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
$purge = $this->parsePurgeValue( $this->cache->get( self::TIME_KEY_PREFIX . $key ) );
if ( $purge && $purge[self::FLD_TIME] < $purgeTimestamp ) {
$isStale = true;
$this->logger->warning( "Reaping stale check key '$key'." );
- $ok = $this->cache->changeTTL( self::TIME_KEY_PREFIX . $key, 1 );
+ $ok = $this->cache->changeTTL( self::TIME_KEY_PREFIX . $key, self::TTL_SECOND );
if ( !$ok ) {
$this->logger->error( "Could not complete reap of check key '$key'." );
}
@@ -1315,21 +1599,23 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
/**
* @see BagOStuff::makeKey()
- * @param string $keys,... Key component
- * @return string
+ * @param string $class Key class
+ * @param string $component [optional] Key component (starting with a key collection name)
+ * @return string Colon-delimited list of $keyspace followed by escaped components of $args
* @since 1.27
*/
- public function makeKey() {
+ public function makeKey( $class, $component = null ) {
return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
}
/**
* @see BagOStuff::makeGlobalKey()
- * @param string $keys,... Key component
- * @return string
+ * @param string $class Key class
+ * @param string $component [optional] Key component (starting with a key collection name)
+ * @return string Colon-delimited list of $keyspace followed by escaped components of $args
* @since 1.27
*/
- public function makeGlobalKey() {
+ public function makeGlobalKey( $class, $component = null ) {
return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
}
@@ -1339,7 +1625,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* @return ArrayIterator Iterator yielding (cache key => entity ID) in $entities order
* @since 1.28
*/
- public function makeMultiKeys( array $entities, callable $keyFunc ) {
+ final public function makeMultiKeys( array $entities, callable $keyFunc ) {
$map = [];
foreach ( $entities as $entity ) {
$map[$keyFunc( $entity, $this )] = $entity;
@@ -1393,6 +1679,30 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
}
/**
+ * Enable or disable the use of brief caching for tombstoned keys
+ *
+ * When a key is purged via delete(), there normally is a period where caching
+ * is hold-off limited to an extremely short time. This method will disable that
+ * caching, forcing the callback to run for any of:
+ * - WANObjectCache::getWithSetCallback()
+ * - WANObjectCache::getMultiWithSetCallback()
+ * - WANObjectCache::getMultiWithUnionSetCallback()
+ *
+ * This is useful when both:
+ * - a) the database used by the callback is known to be up-to-date enough
+ * for some particular purpose (e.g. replica DB has applied transaction X)
+ * - b) the caller needs to exploit that fact, and therefore needs to avoid the
+ * use of inherently volatile and possibly stale interim keys
+ *
+ * @see WANObjectCache::delete()
+ * @param bool $enabled Whether to enable interim caching
+ * @since 1.31
+ */
+ final public function useInterimHoldOffCaching( $enabled ) {
+ $this->useInterimHoldOffCaching = $enabled;
+ }
+
+ /**
* @param int $flag ATTR_* class constant
* @return int QOS_* class constant
* @since 1.28
@@ -1417,6 +1727,46 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* $ttl = $cache->adaptiveTTL( $mtime, $cache::TTL_DAY );
* @endcode
*
+ * Another use case is when there are no applicable "last modified" fields in the DB,
+ * and there are too many dependencies for explicit purges to be viable, and the rate of
+ * change to relevant content is unstable, and it is highly valued to have the cached value
+ * be as up-to-date as possible.
+ *
+ * Example usage:
+ * @code
+ * $query = "<some complex query>";
+ * $idListFromComplexQuery = $cache->getWithSetCallback(
+ * $cache->makeKey( 'complex-graph-query', $hashOfQuery ),
+ * GraphQueryClass::STARTING_TTL,
+ * function ( $oldValue, &$ttl, array &$setOpts, $oldAsOf ) use ( $query, $cache ) {
+ * $gdb = $this->getReplicaGraphDbConnection();
+ * // Account for any snapshot/replica DB lag
+ * $setOpts += GraphDatabase::getCacheSetOptions( $gdb );
+ *
+ * $newList = iterator_to_array( $gdb->query( $query ) );
+ * sort( $newList, SORT_NUMERIC ); // normalize
+ *
+ * $minTTL = GraphQueryClass::MIN_TTL;
+ * $maxTTL = GraphQueryClass::MAX_TTL;
+ * if ( $oldValue !== false ) {
+ * // Note that $oldAsOf is the last time this callback ran
+ * $ttl = ( $newList === $oldValue )
+ * // No change: cache for 150% of the age of $oldValue
+ * ? $cache->adaptiveTTL( $oldAsOf, $maxTTL, $minTTL, 1.5 )
+ * // Changed: cache for 50% of the age of $oldValue
+ * : $cache->adaptiveTTL( $oldAsOf, $maxTTL, $minTTL, .5 );
+ * }
+ *
+ * return $newList;
+ * },
+ * [
+ * // Keep stale values around for doing comparisons for TTL calculations.
+ * // High values improve long-tail keys hit-rates, though might waste space.
+ * 'staleTTL' => GraphQueryClass::GRACE_TTL
+ * ]
+ * );
+ * @endcode
+ *
* @param int|float $mtime UNIX timestamp
* @param int $maxTTL Maximum TTL (seconds)
* @param int $minTTL Minimum TTL (seconds); Default: 30
@@ -1433,7 +1783,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
return $minTTL; // no last-modified time provided
}
- $age = time() - $mtime;
+ $age = $this->getCurrentTime() - $mtime;
return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
}
@@ -1442,7 +1792,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* @return int Number of warmup key cache misses last round
* @since 1.30
*/
- public function getWarmupKeyMisses() {
+ final public function getWarmupKeyMisses() {
return $this->warmupKeyMisses;
}
@@ -1457,10 +1807,19 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* @return bool Success
*/
protected function relayPurge( $key, $ttl, $holdoff ) {
- if ( $this->purgeRelayer instanceof EventRelayerNull ) {
+ if ( $this->mcrouterAware ) {
+ // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
+ // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
+ $ok = $this->cache->set(
+ "/*/{$this->cluster}/{$key}",
+ $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_NONE ),
+ $ttl
+ );
+ } elseif ( $this->purgeRelayer instanceof EventRelayerNull ) {
// This handles the mcrouter and the single-DC case
- $ok = $this->cache->set( $key,
- $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
+ $ok = $this->cache->set(
+ $key,
+ $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_NONE ),
$ttl
);
} else {
@@ -1468,7 +1827,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
'cmd' => 'set',
'key' => $key,
'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
- 'ttl' => max( $ttl, 1 ),
+ 'ttl' => max( $ttl, self::TTL_SECOND ),
'sbt' => true, // substitute $UNIXTIME$ with actual microtime
] );
@@ -1488,8 +1847,12 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* @return bool Success
*/
protected function relayDelete( $key ) {
- if ( $this->purgeRelayer instanceof EventRelayerNull ) {
- // This handles the mcrouter and the single-DC case
+ if ( $this->mcrouterAware ) {
+ // See https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup
+ // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
+ $ok = $this->cache->delete( "/*/{$this->cluster}/{$key}" );
+ } elseif ( $this->purgeRelayer instanceof EventRelayerNull ) {
+ // Some other proxy handles broadcasting or there is only one datacenter
$ok = $this->cache->delete( $key );
} else {
$event = $this->cache->modifySimpleRelayEvent( [
@@ -1507,22 +1870,55 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
}
/**
- * Check if a key should be regenerated (using random probability)
+ * Check if a key is fresh or in the grace window and thus due for randomized reuse
+ *
+ * If $curTTL > 0 (e.g. not expired) this returns true. Otherwise, the chance of returning
+ * true decrease steadily from 100% to 0% as the |$curTTL| moves from 0 to $graceTTL seconds.
+ * This handles widely varying levels of cache access traffic.
+ *
+ * If $curTTL <= -$graceTTL (e.g. already expired), then this returns false.
+ *
+ * @param float $curTTL Approximate TTL left on the key if present
+ * @param int $graceTTL Consider using stale values if $curTTL is greater than this
+ * @return bool
+ */
+ protected function isAliveOrInGracePeriod( $curTTL, $graceTTL ) {
+ if ( $curTTL > 0 ) {
+ return true;
+ } elseif ( $graceTTL <= 0 ) {
+ return false;
+ }
+
+ $ageStale = abs( $curTTL ); // seconds of staleness
+ $curGTTL = ( $graceTTL - $ageStale ); // current grace-time-to-live
+ if ( $curGTTL <= 0 ) {
+ return false; // already out of grace period
+ }
+
+ // Chance of using a stale value is the complement of the chance of refreshing it
+ return !$this->worthRefreshExpiring( $curGTTL, $graceTTL );
+ }
+
+ /**
+ * Check if a key is nearing expiration and thus due for randomized regeneration
*
- * This returns false if $curTTL >= $lowTTL. Otherwise, the chance
- * of returning true increases steadily from 0% to 100% as the $curTTL
- * moves from $lowTTL to 0 seconds. This handles widely varying
- * levels of cache access traffic.
+ * This returns false if $curTTL >= $lowTTL. Otherwise, the chance of returning true
+ * increases steadily from 0% to 100% as the $curTTL moves from $lowTTL to 0 seconds.
+ * This handles widely varying levels of cache access traffic.
+ *
+ * If $curTTL <= 0 (e.g. already expired), then this returns false.
*
* @param float $curTTL Approximate TTL left on the key if present
* @param float $lowTTL Consider a refresh when $curTTL is less than this
* @return bool
*/
protected function worthRefreshExpiring( $curTTL, $lowTTL ) {
- if ( $curTTL >= $lowTTL ) {
+ if ( $lowTTL <= 0 ) {
+ return false;
+ } elseif ( $curTTL >= $lowTTL ) {
return false;
} elseif ( $curTTL <= 0 ) {
- return true;
+ return false;
}
$chance = ( 1 - $curTTL / $lowTTL );
@@ -1546,6 +1942,10 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
* @return bool
*/
protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
+ if ( $ageNew < 0 || $timeTillRefresh <= 0 ) {
+ return false;
+ }
+
$age = $now - $asOf;
$timeOld = $age - $ageNew;
if ( $timeOld <= 0 ) {
@@ -1612,7 +2012,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
*/
protected function unwrap( $wrapped, $now ) {
// Check if the value is a tombstone
- $purge = self::parsePurgeValue( $wrapped );
+ $purge = $this->parsePurgeValue( $wrapped );
if ( $purge !== false ) {
// Purged values should always have a negative current $ttl
$curTTL = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
@@ -1658,11 +2058,21 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
}
/**
+ * @param string $key String of the format <scope>:<class>[:<class or variable>]...
+ * @return string
+ */
+ protected function determineKeyClass( $key ) {
+ $parts = explode( ':', $key );
+
+ return isset( $parts[1] ) ? $parts[1] : $parts[0]; // sanity
+ }
+
+ /**
* @param string $value Wrapped value like "PURGED:<timestamp>:<holdoff>"
* @return array|bool Array containing a UNIX timestamp (float) and holdoff period (integer),
* or false if value isn't a valid purge value
*/
- protected static function parsePurgeValue( $value ) {
+ protected function parsePurgeValue( $value ) {
if ( !is_string( $value ) ) {
return false;
}
@@ -1724,7 +2134,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
return array_diff( $keys, $keysFound );
}
- /**
+ /**
* @param array $keys
* @param array $checkKeys
* @return array Map of (cache key => mixed)
@@ -1758,4 +2168,21 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
return $warmupCache;
}
+
+ /**
+ * @return float UNIX timestamp
+ * @codeCoverageIgnore
+ */
+ protected function getCurrentTime() {
+ return $this->wallClockOverride ?: microtime( true );
+ }
+
+ /**
+ * @param float|null &$time Mock UNIX timestamp for testing
+ * @codeCoverageIgnore
+ */
+ public function setMockTime( &$time ) {
+ $this->wallClockOverride =& $time;
+ $this->cache->setMockTime( $time );
+ }
}
diff --git a/www/wiki/includes/libs/objectcache/XCacheBagOStuff.php b/www/wiki/includes/libs/objectcache/XCacheBagOStuff.php
deleted file mode 100644
index 47c29064..00000000
--- a/www/wiki/includes/libs/objectcache/XCacheBagOStuff.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-/**
- * Object caching using XCache.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Cache
- */
-
-/**
- * Wrapper for XCache object caching functions; identical interface
- * to the APC wrapper
- *
- * @ingroup Cache
- */
-class XCacheBagOStuff extends BagOStuff {
- protected function doGet( $key, $flags = 0 ) {
- $val = xcache_get( $key );
-
- if ( is_string( $val ) ) {
- if ( $this->isInteger( $val ) ) {
- $val = intval( $val );
- } else {
- $val = unserialize( $val );
- }
- } elseif ( is_null( $val ) ) {
- return false;
- }
-
- return $val;
- }
-
- public function set( $key, $value, $expire = 0, $flags = 0 ) {
- if ( !$this->isInteger( $value ) ) {
- $value = serialize( $value );
- }
-
- xcache_set( $key, $value, $expire );
- return true;
- }
-
- public function delete( $key ) {
- xcache_unset( $key );
- return true;
- }
-
- public function incr( $key, $value = 1 ) {
- return xcache_inc( $key, $value );
- }
-
- public function decr( $key, $value = 1 ) {
- return xcache_dec( $key, $value );
- }
-}
diff --git a/www/wiki/includes/libs/rdbms/ChronologyProtector.php b/www/wiki/includes/libs/rdbms/ChronologyProtector.php
index 8121654e..e1152865 100644
--- a/www/wiki/includes/libs/rdbms/ChronologyProtector.php
+++ b/www/wiki/includes/libs/rdbms/ChronologyProtector.php
@@ -43,10 +43,10 @@ class ChronologyProtector implements LoggerAwareInterface {
protected $key;
/** @var string Hash of client parameters */
protected $clientId;
- /** @var float|null Minimum UNIX timestamp of 1+ expected startup positions */
- protected $waitForPosTime;
+ /** @var int|null Expected minimum index of the last write to the position store */
+ protected $waitForPosIndex;
/** @var int Max seconds to wait on positions to appear */
- protected $waitForPosTimeout = self::POS_WAIT_TIMEOUT;
+ protected $waitForPosStoreTimeout = self::POS_STORE_WAIT_TIMEOUT;
/** @var bool Whether to no-op all method calls */
protected $enabled = true;
/** @var bool Whether to check and wait on positions */
@@ -64,19 +64,19 @@ class ChronologyProtector implements LoggerAwareInterface {
/** @var int Seconds to store positions */
const POSITION_TTL = 60;
/** @var int Max time to wait for positions to appear */
- const POS_WAIT_TIMEOUT = 5;
+ const POS_STORE_WAIT_TIMEOUT = 5;
/**
* @param BagOStuff $store
- * @param array $client Map of (ip: <IP>, agent: <user-agent>)
- * @param float $posTime UNIX timestamp
+ * @param array[] $client Map of (ip: <IP>, agent: <user-agent>)
+ * @param int|null $posIndex Write counter index [optional]
* @since 1.27
*/
- public function __construct( BagOStuff $store, array $client, $posTime = null ) {
+ public function __construct( BagOStuff $store, array $client, $posIndex = null ) {
$this->store = $store;
$this->clientId = md5( $client['ip'] . "\n" . $client['agent'] );
- $this->key = $store->makeGlobalKey( __CLASS__, $this->clientId, 'v1' );
- $this->waitForPosTime = $posTime;
+ $this->key = $store->makeGlobalKey( __CLASS__, $this->clientId, 'v2' );
+ $this->waitForPosIndex = $posIndex;
$this->logger = new NullLogger();
}
@@ -124,7 +124,7 @@ class ChronologyProtector implements LoggerAwareInterface {
$this->startupPositions[$masterName] instanceof DBMasterPos
) {
$pos = $this->startupPositions[$masterName];
- $this->logger->info( __METHOD__ . ": LB for '$masterName' set to pos $pos\n" );
+ $this->logger->debug( __METHOD__ . ": LB for '$masterName' set to pos $pos\n" );
$lb->waitFor( $pos );
}
}
@@ -147,10 +147,12 @@ class ChronologyProtector implements LoggerAwareInterface {
$masterName = $lb->getServerName( $lb->getWriterIndex() );
if ( $lb->getServerCount() > 1 ) {
$pos = $lb->getMasterPos();
- $this->logger->info( __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
- $this->shutdownPositions[$masterName] = $pos;
+ if ( $pos ) {
+ $this->logger->debug( __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
+ $this->shutdownPositions[$masterName] = $pos;
+ }
} else {
- $this->logger->info( __METHOD__ . ": DB '$masterName' touched\n" );
+ $this->logger->debug( __METHOD__ . ": DB '$masterName' touched\n" );
}
$this->shutdownTouchDBs[$masterName] = 1;
}
@@ -161,9 +163,10 @@ class ChronologyProtector implements LoggerAwareInterface {
*
* @param callable|null $workCallback Work to do instead of waiting on syncing positions
* @param string $mode One of (sync, async); whether to wait on remote datacenters
+ * @param int|null &$cpIndex DB position key write counter; incremented on update
* @return DBMasterPos[] Empty on success; returns the (db name => position) map on failure
*/
- public function shutdown( callable $workCallback = null, $mode = 'sync' ) {
+ public function shutdown( callable $workCallback = null, $mode = 'sync', &$cpIndex = null ) {
if ( !$this->enabled ) {
return [];
}
@@ -183,7 +186,7 @@ class ChronologyProtector implements LoggerAwareInterface {
return []; // nothing to save
}
- $this->logger->info( __METHOD__ . ": saving master pos for " .
+ $this->logger->debug( __METHOD__ . ": saving master pos for " .
implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
);
@@ -198,13 +201,18 @@ class ChronologyProtector implements LoggerAwareInterface {
}
$ok = $store->set(
$this->key,
- self::mergePositions( $store->get( $this->key ), $this->shutdownPositions ),
+ $this->mergePositions(
+ $store->get( $this->key ),
+ $this->shutdownPositions,
+ $cpIndex
+ ),
self::POSITION_TTL,
( $mode === 'sync' ) ? $store::WRITE_SYNC : 0
);
$store->unlock( $this->key );
} else {
$ok = false;
+ $cpIndex = null; // nothing saved
}
if ( !$ok ) {
@@ -254,28 +262,36 @@ class ChronologyProtector implements LoggerAwareInterface {
$this->initialized = true;
if ( $this->wait ) {
- // If there is an expectation to see master positions with a certain min
- // timestamp, then block until they appear, or until a timeout is reached.
- if ( $this->waitForPosTime > 0.0 ) {
+ // If there is an expectation to see master positions from a certain write
+ // index or higher, then block until it appears, or until a timeout is reached.
+ // Since the write index restarts each time the key is created, it is possible that
+ // a lagged store has a matching key write index. However, in that case, it should
+ // already be expired and thus treated as non-existing, maintaining correctness.
+ if ( $this->waitForPosIndex > 0 ) {
$data = null;
$loop = new WaitConditionLoop(
function () use ( &$data ) {
$data = $this->store->get( $this->key );
+ if ( !is_array( $data ) ) {
+ return WaitConditionLoop::CONDITION_CONTINUE; // not found yet
+ } elseif ( !isset( $data['writeIndex'] ) ) {
+ return WaitConditionLoop::CONDITION_REACHED; // b/c
+ }
- return ( self::minPosTime( $data ) >= $this->waitForPosTime )
+ return ( $data['writeIndex'] >= $this->waitForPosIndex )
? WaitConditionLoop::CONDITION_REACHED
: WaitConditionLoop::CONDITION_CONTINUE;
},
- $this->waitForPosTimeout
+ $this->waitForPosStoreTimeout
);
$result = $loop->invoke();
$waitedMs = $loop->getLastWaitTime() * 1e3;
if ( $result == $loop::CONDITION_REACHED ) {
- $msg = "expected and found pos time {$this->waitForPosTime} ({$waitedMs}ms)";
+ $msg = "expected and found pos index {$this->waitForPosIndex} ({$waitedMs}ms)";
$this->logger->debug( $msg );
} else {
- $msg = "expected but missed pos time {$this->waitForPosTime} ({$waitedMs}ms)";
+ $msg = "expected but missed pos index {$this->waitForPosIndex} ({$waitedMs}ms)";
$this->logger->info( $msg );
}
} else {
@@ -283,55 +299,38 @@ class ChronologyProtector implements LoggerAwareInterface {
}
$this->startupPositions = $data ? $data['positions'] : [];
- $this->logger->info( __METHOD__ . ": key is {$this->key} (read)\n" );
+ $this->logger->debug( __METHOD__ . ": key is {$this->key} (read)\n" );
} else {
$this->startupPositions = [];
- $this->logger->info( __METHOD__ . ": key is {$this->key} (unread)\n" );
+ $this->logger->debug( __METHOD__ . ": key is {$this->key} (unread)\n" );
}
}
/**
- * @param array|bool $data
- * @return float|null
- */
- private static function minPosTime( $data ) {
- if ( !isset( $data['positions'] ) ) {
- return null;
- }
-
- $min = null;
- foreach ( $data['positions'] as $pos ) {
- if ( $pos instanceof DBMasterPos ) {
- $min = $min ? min( $pos->asOfTime(), $min ) : $pos->asOfTime();
- }
- }
-
- return $min;
- }
-
- /**
* @param array|bool $curValue
* @param DBMasterPos[] $shutdownPositions
+ * @param int|null &$cpIndex
* @return array
*/
- private static function mergePositions( $curValue, array $shutdownPositions ) {
+ protected function mergePositions( $curValue, array $shutdownPositions, &$cpIndex = null ) {
/** @var DBMasterPos[] $curPositions */
- if ( $curValue === false ) {
- $curPositions = $shutdownPositions;
- } else {
- $curPositions = $curValue['positions'];
- // Use the newest positions for each DB master
- foreach ( $shutdownPositions as $db => $pos ) {
- if (
- !isset( $curPositions[$db] ) ||
- !( $curPositions[$db] instanceof DBMasterPos ) ||
- $pos->asOfTime() > $curPositions[$db]->asOfTime()
- ) {
- $curPositions[$db] = $pos;
- }
+ $curPositions = isset( $curValue['positions'] ) ? $curValue['positions'] : [];
+ // Use the newest positions for each DB master
+ foreach ( $shutdownPositions as $db => $pos ) {
+ if (
+ !isset( $curPositions[$db] ) ||
+ !( $curPositions[$db] instanceof DBMasterPos ) ||
+ $pos->asOfTime() > $curPositions[$db]->asOfTime()
+ ) {
+ $curPositions[$db] = $pos;
}
}
- return [ 'positions' => $curPositions ];
+ $cpIndex = isset( $curValue['writeIndex'] ) ? $curValue['writeIndex'] : 0;
+
+ return [
+ 'positions' => $curPositions,
+ 'writeIndex' => ++$cpIndex
+ ];
}
}
diff --git a/www/wiki/includes/libs/rdbms/TransactionProfiler.php b/www/wiki/includes/libs/rdbms/TransactionProfiler.php
index 57a12a44..c353a224 100644
--- a/www/wiki/includes/libs/rdbms/TransactionProfiler.php
+++ b/www/wiki/includes/libs/rdbms/TransactionProfiler.php
@@ -82,7 +82,7 @@ class TransactionProfiler implements LoggerAwareInterface {
}
/**
- * @param bool $value New value
+ * @param bool $value
* @return bool Old value
* @since 1.28
*/
@@ -177,7 +177,7 @@ class TransactionProfiler implements LoggerAwareInterface {
public function transactionWritingIn( $server, $db, $id ) {
$name = "{$server} ({$db}) (TRX#$id)";
if ( isset( $this->dbTrxHoldingLocks[$name] ) ) {
- $this->logger->info( "Nested transaction for '$name' - out of sync." );
+ $this->logger->warning( "Nested transaction for '$name' - out of sync." );
}
$this->dbTrxHoldingLocks[$name] = [
'start' => microtime( true ),
@@ -206,7 +206,7 @@ class TransactionProfiler implements LoggerAwareInterface {
$elapsed = ( $eTime - $sTime );
if ( $isWrite && $n > $this->expect['maxAffected'] ) {
- $this->logger->info(
+ $this->logger->warning(
"Query affected $n row(s):\n" . $query . "\n" .
( new RuntimeException() )->getTraceAsString() );
}
@@ -271,7 +271,7 @@ class TransactionProfiler implements LoggerAwareInterface {
public function transactionWritingOut( $server, $db, $id, $writeTime = 0.0, $affected = 0 ) {
$name = "{$server} ({$db}) (TRX#$id)";
if ( !isset( $this->dbTrxMethodTimes[$name] ) ) {
- $this->logger->info( "Detected no transaction for '$name' - out of sync." );
+ $this->logger->warning( "Detected no transaction for '$name' - out of sync." );
return;
}
@@ -317,7 +317,7 @@ class TransactionProfiler implements LoggerAwareInterface {
list( $query, $sTime, $end ) = $info;
$trace .= sprintf( "%d\t%.6f\t%s\n", $i, ( $end - $sTime ), $query );
}
- $this->logger->info( "Sub-optimal transaction on DB(s) [{dbs}]: \n{trace}", [
+ $this->logger->warning( "Sub-optimal transaction on DB(s) [{dbs}]: \n{trace}", [
'dbs' => implode( ', ', array_keys( $this->dbTrxHoldingLocks[$name]['conns'] ) ),
'trace' => $trace
] );
@@ -336,7 +336,7 @@ class TransactionProfiler implements LoggerAwareInterface {
return;
}
- $this->logger->info(
+ $this->logger->warning(
"Expectation ({measure} <= {max}) by {by} not met (actual: {actual}):\n{query}\n" .
( new RuntimeException() )->getTraceAsString(),
[
diff --git a/www/wiki/includes/libs/rdbms/connectionmanager/ConnectionManager.php b/www/wiki/includes/libs/rdbms/connectionmanager/ConnectionManager.php
index 212ff315..4a497b08 100644
--- a/www/wiki/includes/libs/rdbms/connectionmanager/ConnectionManager.php
+++ b/www/wiki/includes/libs/rdbms/connectionmanager/ConnectionManager.php
@@ -1,4 +1,23 @@
<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
namespace Wikimedia\Rdbms;
@@ -11,7 +30,6 @@ use InvalidArgumentException;
*
* @since 1.29
*
- * @license GPL-2.0+
* @author Addshore
*/
class ConnectionManager {
diff --git a/www/wiki/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php b/www/wiki/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php
index 30b1fb4c..aa3bea8f 100644
--- a/www/wiki/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php
+++ b/www/wiki/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php
@@ -1,4 +1,23 @@
<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
namespace Wikimedia\Rdbms;
@@ -20,7 +39,6 @@ namespace Wikimedia\Rdbms;
*
* @since 1.29
*
- * @license GPL-2.0+
* @author Daniel Kinzler
* @author Addshore
*/
diff --git a/www/wiki/includes/compat/RunningStatCompat.php b/www/wiki/includes/libs/rdbms/database/AtomicSectionIdentifier.php
index ac82f44d..c6e3d44c 100644
--- a/www/wiki/includes/compat/RunningStatCompat.php
+++ b/www/wiki/includes/libs/rdbms/database/AtomicSectionIdentifier.php
@@ -16,13 +16,12 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
+ * @ingroup Database
*/
+namespace Wikimedia\Rdbms;
/**
- * Backward-compatibility alias for RunningStat, which was moved out
- * into an external library and namespaced.
- *
- * @deprecated since 1.27 use RunningStat\RunningStat directly
+ * Class used for token representing identifiers for atomic sections from IDatabase instances
*/
-class RunningStat extends RunningStat\RunningStat {
+class AtomicSectionIdentifier {
}
diff --git a/www/wiki/includes/libs/rdbms/database/DBConnRef.php b/www/wiki/includes/libs/rdbms/database/DBConnRef.php
index ef2953ec..decd9bfa 100644
--- a/www/wiki/includes/libs/rdbms/database/DBConnRef.php
+++ b/www/wiki/includes/libs/rdbms/database/DBConnRef.php
@@ -33,7 +33,7 @@ class DBConnRef implements IDatabase {
$this->lb = $lb;
if ( $conn instanceof Database ) {
$this->conn = $conn; // live handle
- } elseif ( count( $conn ) >= 4 && $conn[self::FLD_DOMAIN] !== false ) {
+ } elseif ( is_array( $conn ) && count( $conn ) >= 4 && $conn[self::FLD_DOMAIN] !== false ) {
$this->params = $conn;
} else {
throw new InvalidArgumentException( "Missing lazy connection arguments." );
@@ -211,10 +211,6 @@ class DBConnRef implements IDatabase {
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function fieldInfo( $table, $field ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
public function affectedRows() {
return $this->__call( __FUNCTION__, func_get_args() );
}
@@ -231,18 +227,10 @@ class DBConnRef implements IDatabase {
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function reportConnectionError( $error = 'Unknown error' ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
public function freeResult( $res ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
@@ -281,7 +269,7 @@ class DBConnRef implements IDatabase {
}
public function estimateRowCount(
- $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
+ $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
) {
return $this->__call( __FUNCTION__, func_get_args() );
}
@@ -304,10 +292,6 @@ class DBConnRef implements IDatabase {
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function indexUnique( $table, $index ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
@@ -350,10 +334,25 @@ class DBConnRef implements IDatabase {
return $this->__call( __FUNCTION__, func_get_args() );
}
+ public function buildSubstring( $input, $startPosition, $length = null ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
public function buildStringCast( $field ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
+ public function buildIntegerCast( $field ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function buildSelectSubquery(
+ $table, $vars, $conds = '', $fname = __METHOD__,
+ $options = [], $join_conds = []
+ ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
public function databasesAreIndependent() {
return $this->__call( __FUNCTION__, func_get_args() );
}
@@ -452,7 +451,7 @@ class DBConnRef implements IDatabase {
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function wasErrorReissuable() {
+ public function wasConnectionLoss() {
return $this->__call( __FUNCTION__, func_get_args() );
}
@@ -460,6 +459,10 @@ class DBConnRef implements IDatabase {
return $this->__call( __FUNCTION__, func_get_args() );
}
+ public function wasErrorReissuable() {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
public function masterPosWait( DBMasterPos $pos, $timeout ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
@@ -492,7 +495,9 @@ class DBConnRef implements IDatabase {
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function startAtomic( $fname = __METHOD__ ) {
+ public function startAtomic(
+ $fname = __METHOD__, $cancelable = IDatabase::ATOMIC_NOT_CANCELABLE
+ ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
@@ -500,7 +505,13 @@ class DBConnRef implements IDatabase {
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function doAtomicSection( $fname, callable $callback ) {
+ public function cancelAtomic( $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function doAtomicSection(
+ $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
+ ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
@@ -520,10 +531,6 @@ class DBConnRef implements IDatabase {
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function listTables( $prefix = null, $fname = __METHOD__ ) {
- return $this->__call( __FUNCTION__, func_get_args() );
- }
-
public function timestamp( $ts = 0 ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
@@ -610,6 +617,10 @@ class DBConnRef implements IDatabase {
return $this->__call( __FUNCTION__, func_get_args() );
}
+ public function setIndexAliases( array $aliases ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
/**
* Clean up the connection when out of scope
*/
diff --git a/www/wiki/includes/libs/rdbms/database/Database.php b/www/wiki/includes/libs/rdbms/database/Database.php
index c9040928..2b5aaf59 100644
--- a/www/wiki/includes/libs/rdbms/database/Database.php
+++ b/www/wiki/includes/libs/rdbms/database/Database.php
@@ -27,12 +27,15 @@ namespace Wikimedia\Rdbms;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
use Wikimedia\ScopedCallback;
use Wikimedia\Timestamp\ConvertibleTimestamp;
-use MediaWiki;
+use Wikimedia;
use BagOStuff;
use HashBagOStuff;
+use LogicException;
use InvalidArgumentException;
+use UnexpectedValueException;
use Exception;
use RuntimeException;
@@ -58,27 +61,38 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
const SLOW_WRITE_SEC = 0.500;
const SMALL_WRITE_ROWS = 100;
+ /** @var string Whether lock granularity is on the level of the entire database */
+ const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
+
+ /** @var int New Database instance will not be connected yet when returned */
+ const NEW_UNCONNECTED = 0;
+ /** @var int New Database instance will already be connected when returned */
+ const NEW_CONNECTED = 1;
+
/** @var string SQL query */
- protected $mLastQuery = '';
+ protected $lastQuery = '';
/** @var float|bool UNIX timestamp of last write query */
- protected $mLastWriteTime = false;
+ protected $lastWriteTime = false;
/** @var string|bool */
- protected $mPHPError = false;
- /** @var string */
- protected $mServer;
- /** @var string */
- protected $mUser;
- /** @var string */
- protected $mPassword;
- /** @var string */
- protected $mDBname;
- /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
+ protected $phpError = false;
+ /** @var string Server that this instance is currently connected to */
+ protected $server;
+ /** @var string User that this instance is currently connected under the name of */
+ protected $user;
+ /** @var string Password used to establish the current connection */
+ protected $password;
+ /** @var string Database that this instance is currently connected to */
+ protected $dbName;
+ /** @var array[] Map of (table => (dbname, schema, prefix) map) */
protected $tableAliases = [];
+ /** @var string[] Map of (index alias => index) */
+ protected $indexAliases = [];
/** @var bool Whether this PHP instance is for a CLI script */
protected $cliMode;
/** @var string Agent name for query profiling */
protected $agent;
-
+ /** @var array Parameters used by initConnection() to establish a connection */
+ protected $connectionParams = [];
/** @var BagOStuff APC cache */
protected $srvCache;
/** @var LoggerInterface */
@@ -87,37 +101,37 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
protected $queryLogger;
/** @var callback Error logging callback */
protected $errorLogger;
+ /** @var callback Deprecation logging callback */
+ protected $deprecationLogger;
/** @var resource|null Database connection */
- protected $mConn = null;
+ protected $conn = null;
/** @var bool */
- protected $mOpened = false;
-
- /** @var array[] List of (callable, method name) */
- protected $mTrxIdleCallbacks = [];
- /** @var array[] List of (callable, method name) */
- protected $mTrxPreCommitCallbacks = [];
- /** @var array[] List of (callable, method name) */
- protected $mTrxEndCallbacks = [];
+ protected $opened = false;
+
+ /** @var array[] List of (callable, method name, atomic section id) */
+ protected $trxIdleCallbacks = [];
+ /** @var array[] List of (callable, method name, atomic section id) */
+ protected $trxPreCommitCallbacks = [];
+ /** @var array[] List of (callable, method name, atomic section id) */
+ protected $trxEndCallbacks = [];
/** @var callable[] Map of (name => callable) */
- protected $mTrxRecurringCallbacks = [];
+ protected $trxRecurringCallbacks = [];
/** @var bool Whether to suppress triggering of transaction end callbacks */
- protected $mTrxEndCallbacksSuppressed = false;
+ protected $trxEndCallbacksSuppressed = false;
/** @var string */
- protected $mTablePrefix = '';
+ protected $tablePrefix = '';
/** @var string */
- protected $mSchema = '';
+ protected $schema = '';
/** @var int */
- protected $mFlags;
+ protected $flags;
/** @var array */
- protected $mLBInfo = [];
- /** @var bool|null */
- protected $mDefaultBigSelects = null;
+ protected $lbInfo = [];
/** @var array|bool */
- protected $mSchemaVars = false;
+ protected $schemaVars = false;
/** @var array */
- protected $mSessionVars = [];
+ protected $sessionVars = [];
/** @var array|null */
protected $preparedArgs;
/** @var string|bool|null Stashed value of html_errors INI setting */
@@ -126,101 +140,122 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
protected $delimiter = ';';
/** @var DatabaseDomain */
protected $currentDomain;
+ /** @var integer|null Rows affected by the last query to query() or its CRUD wrappers */
+ protected $affectedRowCount;
/**
+ * @var int Transaction status
+ */
+ protected $trxStatus = self::STATUS_TRX_NONE;
+ /**
+ * @var Exception|null The last error that caused the status to become STATUS_TRX_ERROR
+ */
+ protected $trxStatusCause;
+ /**
+ * @var array|null If wasKnownStatementRollbackError() prevented trxStatus from being set,
+ * the relevant details are stored here.
+ */
+ protected $trxStatusIgnoredCause;
+ /**
* Either 1 if a transaction is active or 0 otherwise.
* The other Trx fields may not be meaningfull if this is 0.
*
* @var int
*/
- protected $mTrxLevel = 0;
+ protected $trxLevel = 0;
/**
* Either a short hexidecimal string if a transaction is active or ""
*
* @var string
- * @see Database::mTrxLevel
+ * @see Database::trxLevel
*/
- protected $mTrxShortId = '';
+ protected $trxShortId = '';
/**
* The UNIX time that the transaction started. Callers can assume that if
* snapshot isolation is used, then the data is *at least* up to date to that
* point (possibly more up-to-date since the first SELECT defines the snapshot).
*
* @var float|null
- * @see Database::mTrxLevel
+ * @see Database::trxLevel
*/
- private $mTrxTimestamp = null;
+ private $trxTimestamp = null;
/** @var float Lag estimate at the time of BEGIN */
- private $mTrxReplicaLag = null;
+ private $trxReplicaLag = null;
/**
* Remembers the function name given for starting the most recent transaction via begin().
* Used to provide additional context for error reporting.
*
* @var string
- * @see Database::mTrxLevel
+ * @see Database::trxLevel
*/
- private $mTrxFname = null;
+ private $trxFname = null;
/**
* Record if possible write queries were done in the last transaction started
*
* @var bool
- * @see Database::mTrxLevel
+ * @see Database::trxLevel
*/
- private $mTrxDoneWrites = false;
+ private $trxDoneWrites = false;
/**
* Record if the current transaction was started implicitly due to DBO_TRX being set.
*
* @var bool
- * @see Database::mTrxLevel
+ * @see Database::trxLevel
+ */
+ private $trxAutomatic = false;
+ /**
+ * Counter for atomic savepoint identifiers. Reset when a new transaction begins.
+ *
+ * @var int
*/
- private $mTrxAutomatic = false;
+ private $trxAtomicCounter = 0;
/**
* Array of levels of atomicity within transactions
*
- * @var array
+ * @var array List of (name, unique ID, savepoint ID)
*/
- private $mTrxAtomicLevels = [];
+ private $trxAtomicLevels = [];
/**
* Record if the current transaction was started implicitly by Database::startAtomic
*
* @var bool
*/
- private $mTrxAutomaticAtomic = false;
+ private $trxAutomaticAtomic = false;
/**
* Track the write query callers of the current transaction
*
* @var string[]
*/
- private $mTrxWriteCallers = [];
+ private $trxWriteCallers = [];
/**
* @var float Seconds spent in write queries for the current transaction
*/
- private $mTrxWriteDuration = 0.0;
+ private $trxWriteDuration = 0.0;
/**
* @var int Number of write queries for the current transaction
*/
- private $mTrxWriteQueryCount = 0;
+ private $trxWriteQueryCount = 0;
/**
* @var int Number of rows affected by write queries for the current transaction
*/
- private $mTrxWriteAffectedRows = 0;
+ private $trxWriteAffectedRows = 0;
/**
- * @var float Like mTrxWriteQueryCount but excludes lock-bound, easy to replicate, queries
+ * @var float Like trxWriteQueryCount but excludes lock-bound, easy to replicate, queries
*/
- private $mTrxWriteAdjDuration = 0.0;
+ private $trxWriteAdjDuration = 0.0;
/**
- * @var int Number of write queries counted in mTrxWriteAdjDuration
+ * @var int Number of write queries counted in trxWriteAdjDuration
*/
- private $mTrxWriteAdjQueryCount = 0;
+ private $trxWriteAdjQueryCount = 0;
/**
* @var float RTT time estimate
*/
- private $mRTTEstimate = 0.0;
+ private $rttEstimate = 0.0;
/** @var array Map of (name => 1) for locks obtained via lock() */
- private $mNamedLocksHeld = [];
+ private $namedLocksHeld = [];
/** @var array Map of (table name => 1) for TEMPORARY tables */
- protected $mSessionTempTables = [];
+ protected $sessionTempTables = [];
/** @var IDatabase|null Lazy handle to the master DB this server replicates from */
private $lazyMasterHandle;
@@ -228,7 +263,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
/** @var float UNIX timestamp */
protected $lastPing = 0.0;
- /** @var int[] Prior mFlags values */
+ /** @var int[] Prior flags member variable values */
private $priorFlags = [];
/** @var object|string Class name or object With profileIn/profileOut methods */
@@ -236,37 +271,49 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
/** @var TransactionProfiler */
protected $trxProfiler;
+ /** @var int */
+ protected $nonNativeInsertSelectBatchSize = 10000;
+
+ /** @var string Idiom used when a cancelable atomic section started the transaction */
+ private static $NOT_APPLICABLE = 'n/a';
+ /** @var string Prefix to the atomic section counter used to make savepoint IDs */
+ private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
+
+ /** @var int Transaction is in a error state requiring a full or savepoint rollback */
+ const STATUS_TRX_ERROR = 1;
+ /** @var int Transaction is active and in a normal state */
+ const STATUS_TRX_OK = 2;
+ /** @var int No transaction is active */
+ const STATUS_TRX_NONE = 3;
+
/**
- * Constructor and database handle and attempt to connect to the DB server
- *
- * IDatabase classes should not be constructed directly in external
- * code. Database::factory() should be used instead.
- *
+ * @note: exceptions for missing libraries/drivers should be thrown in initConnection()
* @param array $params Parameters passed from Database::factory()
*/
- function __construct( array $params ) {
- $server = $params['host'];
- $user = $params['user'];
- $password = $params['password'];
- $dbName = $params['dbname'];
+ protected function __construct( array $params ) {
+ foreach ( [ 'host', 'user', 'password', 'dbname' ] as $name ) {
+ $this->connectionParams[$name] = $params[$name];
+ }
- $this->mSchema = $params['schema'];
- $this->mTablePrefix = $params['tablePrefix'];
+ $this->schema = $params['schema'];
+ $this->tablePrefix = $params['tablePrefix'];
$this->cliMode = $params['cliMode'];
// Agent name is added to SQL queries in a comment, so make sure it can't break out
$this->agent = str_replace( '/', '-', $params['agent'] );
- $this->mFlags = $params['flags'];
- if ( $this->mFlags & self::DBO_DEFAULT ) {
+ $this->flags = $params['flags'];
+ if ( $this->flags & self::DBO_DEFAULT ) {
if ( $this->cliMode ) {
- $this->mFlags &= ~self::DBO_TRX;
+ $this->flags &= ~self::DBO_TRX;
} else {
- $this->mFlags |= self::DBO_TRX;
+ $this->flags |= self::DBO_TRX;
}
}
+ // Disregard deprecated DBO_IGNORE flag (T189999)
+ $this->flags &= ~self::DBO_IGNORE;
- $this->mSessionVars = $params['variables'];
+ $this->sessionVars = $params['variables'];
$this->srvCache = isset( $params['srvCache'] )
? $params['srvCache']
@@ -277,20 +324,54 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$this->connLogger = $params['connLogger'];
$this->queryLogger = $params['queryLogger'];
$this->errorLogger = $params['errorLogger'];
+ $this->deprecationLogger = $params['deprecationLogger'];
+
+ if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
+ $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
+ }
// Set initial dummy domain until open() sets the final DB/prefix
$this->currentDomain = DatabaseDomain::newUnspecified();
+ }
- if ( $user ) {
- $this->open( $server, $user, $password, $dbName );
- } elseif ( $this->requiresDatabaseUser() ) {
- throw new InvalidArgumentException( "No database user provided." );
+ /**
+ * Initialize the connection to the database over the wire (or to local files)
+ *
+ * @throws LogicException
+ * @throws InvalidArgumentException
+ * @throws DBConnectionError
+ * @since 1.31
+ */
+ final public function initConnection() {
+ if ( $this->isOpen() ) {
+ throw new LogicException( __METHOD__ . ': already connected.' );
}
-
+ // Establish the connection
+ $this->doInitConnection();
// Set the domain object after open() sets the relevant fields
- if ( $this->mDBname != '' ) {
+ if ( $this->dbName != '' ) {
// Domains with server scope but a table prefix are not used by IDatabase classes
- $this->currentDomain = new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix );
+ $this->currentDomain = new DatabaseDomain( $this->dbName, null, $this->tablePrefix );
+ }
+ }
+
+ /**
+ * Actually connect to the database over the wire (or to local files)
+ *
+ * @throws InvalidArgumentException
+ * @throws DBConnectionError
+ * @since 1.31
+ */
+ protected function doInitConnection() {
+ if ( strlen( $this->connectionParams['user'] ) ) {
+ $this->open(
+ $this->connectionParams['host'],
+ $this->connectionParams['user'],
+ $this->connectionParams['password'],
+ $this->connectionParams['dbname']
+ );
+ } else {
+ throw new InvalidArgumentException( "No database user provided." );
}
}
@@ -299,7 +380,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
*
* This also connects to the database immediately upon object construction
*
- * @param string $dbType A possible DB type (sqlite, mysql, postgres)
+ * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
* @param array $p Parameter map with keys:
* - host : The hostname of the DB server
* - user : The name of the database user the client operates under
@@ -317,8 +398,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* - flags : Optional bitfield of DBO_* constants that define connection, protocol,
* buffering, and transaction behavior. It is STRONGLY adviced to leave the DBO_DEFAULT
* flag in place UNLESS this this database simply acts as a key/value store.
- * - driver: Optional name of a specific DB client driver. For MySQL, there is the old
- * 'mysql' driver and the newer 'mysqli' driver.
+ * - driver: Optional name of a specific DB client driver. For MySQL, there is only the
+ * 'mysqli' driver; the old one 'mysql' has been removed.
* - variables: Optional map of session variables to set after connecting. This can be
* used to adjust lock timeouts or encoding modes and the like.
* - connLogger: Optional PSR-3 logger interface instance.
@@ -328,61 +409,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* includes the agent as a SQL comment.
* - trxProfiler: Optional TransactionProfiler instance.
* - errorLogger: Optional callback that takes an Exception and logs it.
+ * - deprecationLogger: Optional callback that takes a string and logs it.
* - cliMode: Whether to consider the execution context that of a CLI script.
* - agent: Optional name used to identify the end-user in query profiling/logging.
* - srvCache: Optional BagOStuff instance to an APC-style cache.
+ * - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT emulation.
+ * @param int $connect One of the class constants (NEW_CONNECTED, NEW_UNCONNECTED) [optional]
* @return Database|null If the database driver or extension cannot be found
* @throws InvalidArgumentException If the database driver or extension cannot be found
* @since 1.18
*/
- final public static function factory( $dbType, $p = [] ) {
- static $canonicalDBTypes = [
- 'mysql' => [ 'mysqli', 'mysql' ],
- 'postgres' => [],
- 'sqlite' => [],
- 'oracle' => [],
- 'mssql' => [],
- ];
- static $classAliases = [
- 'DatabaseMssql' => DatabaseMssql::class,
- 'DatabaseMysql' => DatabaseMysql::class,
- 'DatabaseMysqli' => DatabaseMysqli::class,
- 'DatabaseSqlite' => DatabaseSqlite::class,
- 'DatabasePostgres' => DatabasePostgres::class
- ];
-
- $driver = false;
- $dbType = strtolower( $dbType );
- if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
- $possibleDrivers = $canonicalDBTypes[$dbType];
- if ( !empty( $p['driver'] ) ) {
- if ( in_array( $p['driver'], $possibleDrivers ) ) {
- $driver = $p['driver'];
- } else {
- throw new InvalidArgumentException( __METHOD__ .
- " type '$dbType' does not support driver '{$p['driver']}'" );
- }
- } else {
- foreach ( $possibleDrivers as $posDriver ) {
- if ( extension_loaded( $posDriver ) ) {
- $driver = $posDriver;
- break;
- }
- }
- }
- } else {
- $driver = $dbType;
- }
-
- if ( $driver === false || $driver === '' ) {
- throw new InvalidArgumentException( __METHOD__ .
- " no viable database extension found for type '$dbType'" );
- }
-
- $class = 'Database' . ucfirst( $driver );
- if ( isset( $classAliases[$class] ) ) {
- $class = $classAliases[$class];
- }
+ final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
+ $class = self::getClass( $dbType, isset( $p['driver'] ) ? $p['driver'] : null );
if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
// Resolve some defaults for b/c
@@ -394,13 +432,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
$p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : '';
$p['schema'] = isset( $p['schema'] ) ? $p['schema'] : '';
- $p['cliMode'] = isset( $p['cliMode'] ) ? $p['cliMode'] : ( PHP_SAPI === 'cli' );
+ $p['cliMode'] = isset( $p['cliMode'] )
+ ? $p['cliMode']
+ : ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
$p['agent'] = isset( $p['agent'] ) ? $p['agent'] : '';
if ( !isset( $p['connLogger'] ) ) {
- $p['connLogger'] = new \Psr\Log\NullLogger();
+ $p['connLogger'] = new NullLogger();
}
if ( !isset( $p['queryLogger'] ) ) {
- $p['queryLogger'] = new \Psr\Log\NullLogger();
+ $p['queryLogger'] = new NullLogger();
}
$p['profiler'] = isset( $p['profiler'] ) ? $p['profiler'] : null;
if ( !isset( $p['trxProfiler'] ) ) {
@@ -411,8 +451,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
};
}
+ if ( !isset( $p['deprecationLogger'] ) ) {
+ $p['deprecationLogger'] = function ( $msg ) {
+ trigger_error( $msg, E_USER_DEPRECATED );
+ };
+ }
+ /** @var Database $conn */
$conn = new $class( $p );
+ if ( $connect == self::NEW_CONNECTED ) {
+ $conn->initConnection();
+ }
} else {
$conn = null;
}
@@ -421,6 +470,85 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
/**
+ * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
+ * @param string|null $driver Optional name of a specific DB client driver
+ * @return array Map of (Database::ATTRIBUTE_* constant => value) for all such constants
+ * @throws InvalidArgumentException
+ * @since 1.31
+ */
+ final public static function attributesFromType( $dbType, $driver = null ) {
+ static $defaults = [ self::ATTR_DB_LEVEL_LOCKING => false ];
+
+ $class = self::getClass( $dbType, $driver );
+
+ return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
+ }
+
+ /**
+ * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
+ * @param string|null $driver Optional name of a specific DB client driver
+ * @return string Database subclass name to use
+ * @throws InvalidArgumentException
+ */
+ private static function getClass( $dbType, $driver = null ) {
+ // For database types with built-in support, the below maps type to IDatabase
+ // implementations. For types with multipe driver implementations (PHP extensions),
+ // an array can be used, keyed by extension name. In case of an array, the
+ // optional 'driver' parameter can be used to force a specific driver. Otherwise,
+ // we auto-detect the first available driver. For types without built-in support,
+ // an class named "Database<Type>" us used, eg. DatabaseFoo for type 'foo'.
+ static $builtinTypes = [
+ 'mssql' => DatabaseMssql::class,
+ 'mysql' => [ 'mysqli' => DatabaseMysqli::class ],
+ 'sqlite' => DatabaseSqlite::class,
+ 'postgres' => DatabasePostgres::class,
+ ];
+
+ $dbType = strtolower( $dbType );
+ $class = false;
+
+ if ( isset( $builtinTypes[$dbType] ) ) {
+ $possibleDrivers = $builtinTypes[$dbType];
+ if ( is_string( $possibleDrivers ) ) {
+ $class = $possibleDrivers;
+ } else {
+ if ( (string)$driver !== '' ) {
+ if ( !isset( $possibleDrivers[$driver] ) ) {
+ throw new InvalidArgumentException( __METHOD__ .
+ " type '$dbType' does not support driver '{$driver}'" );
+ } else {
+ $class = $possibleDrivers[$driver];
+ }
+ } else {
+ foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
+ if ( extension_loaded( $posDriver ) ) {
+ $class = $possibleClass;
+ break;
+ }
+ }
+ }
+ }
+ } else {
+ $class = 'Database' . ucfirst( $dbType );
+ }
+
+ if ( $class === false ) {
+ throw new InvalidArgumentException( __METHOD__ .
+ " no viable database extension found for type '$dbType'" );
+ }
+
+ return $class;
+ }
+
+ /**
+ * @return array Map of (Database::ATTRIBUTE_* constant => value
+ * @since 1.31
+ */
+ protected static function getAttributes() {
+ return [];
+ }
+
+ /**
* Set the PSR-3 logger interface to use for query logging. (The logger
* interfaces for connection logging and error logging can be set with the
* constructor.)
@@ -446,43 +574,28 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
return $res;
}
- /**
- * Turns on (false) or off (true) the automatic generation and sending
- * of a "we're sorry, but there has been a database error" page on
- * database errors. Default is on (false). When turned off, the
- * code should use lastErrno() and lastError() to handle the
- * situation as appropriate.
- *
- * Do not use this function outside of the Database classes.
- *
- * @param null|bool $ignoreErrors
- * @return bool The previous value of the flag.
- */
- protected function ignoreErrors( $ignoreErrors = null ) {
- $res = $this->getFlag( self::DBO_IGNORE );
- if ( $ignoreErrors !== null ) {
- $ignoreErrors
- ? $this->setFlag( self::DBO_IGNORE )
- : $this->clearFlag( self::DBO_IGNORE );
- }
-
- return $res;
- }
-
public function trxLevel() {
- return $this->mTrxLevel;
+ return $this->trxLevel;
}
public function trxTimestamp() {
- return $this->mTrxLevel ? $this->mTrxTimestamp : null;
+ return $this->trxLevel ? $this->trxTimestamp : null;
+ }
+
+ /**
+ * @return int One of the STATUS_TRX_* class constants
+ * @since 1.31
+ */
+ public function trxStatus() {
+ return $this->trxStatus;
}
public function tablePrefix( $prefix = null ) {
- $old = $this->mTablePrefix;
+ $old = $this->tablePrefix;
if ( $prefix !== null ) {
- $this->mTablePrefix = $prefix;
- $this->currentDomain = ( $this->mDBname != '' )
- ? new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix )
+ $this->tablePrefix = $prefix;
+ $this->currentDomain = ( $this->dbName != '' )
+ ? new DatabaseDomain( $this->dbName, null, $this->tablePrefix )
: DatabaseDomain::newUnspecified();
}
@@ -490,9 +603,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
public function dbSchema( $schema = null ) {
- $old = $this->mSchema;
+ $old = $this->schema;
if ( $schema !== null ) {
- $this->mSchema = $schema;
+ $this->schema = $schema;
}
return $old;
@@ -500,10 +613,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
public function getLBInfo( $name = null ) {
if ( is_null( $name ) ) {
- return $this->mLBInfo;
+ return $this->lbInfo;
} else {
- if ( array_key_exists( $name, $this->mLBInfo ) ) {
- return $this->mLBInfo[$name];
+ if ( array_key_exists( $name, $this->lbInfo ) ) {
+ return $this->lbInfo[$name];
} else {
return null;
}
@@ -512,9 +625,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
public function setLBInfo( $name, $value = null ) {
if ( is_null( $value ) ) {
- $this->mLBInfo = $name;
+ $this->lbInfo = $name;
} else {
- $this->mLBInfo[$name] = $value;
+ $this->lbInfo[$name] = $value;
}
}
@@ -540,55 +653,72 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
public function lastQuery() {
- return $this->mLastQuery;
+ return $this->lastQuery;
}
public function doneWrites() {
- return (bool)$this->mLastWriteTime;
+ return (bool)$this->lastWriteTime;
}
public function lastDoneWrites() {
- return $this->mLastWriteTime ?: false;
+ return $this->lastWriteTime ?: false;
}
public function writesPending() {
- return $this->mTrxLevel && $this->mTrxDoneWrites;
+ return $this->trxLevel && $this->trxDoneWrites;
}
public function writesOrCallbacksPending() {
- return $this->mTrxLevel && (
- $this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
+ return $this->trxLevel && (
+ $this->trxDoneWrites ||
+ $this->trxIdleCallbacks ||
+ $this->trxPreCommitCallbacks ||
+ $this->trxEndCallbacks
);
}
+ /**
+ * @return string|null
+ */
+ final protected function getTransactionRoundId() {
+ // If transaction round participation is enabled, see if one is active
+ if ( $this->getFlag( self::DBO_TRX ) ) {
+ $id = $this->getLBInfo( 'trxRoundId' );
+
+ return is_string( $id ) ? $id : null;
+ }
+
+ return null;
+ }
+
public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
- if ( !$this->mTrxLevel ) {
+ if ( !$this->trxLevel ) {
return false;
- } elseif ( !$this->mTrxDoneWrites ) {
+ } elseif ( !$this->trxDoneWrites ) {
return 0.0;
}
switch ( $type ) {
case self::ESTIMATE_DB_APPLY:
$this->ping( $rtt );
- $rttAdjTotal = $this->mTrxWriteAdjQueryCount * $rtt;
- $applyTime = max( $this->mTrxWriteAdjDuration - $rttAdjTotal, 0 );
+ $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
+ $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
// For omitted queries, make them count as something at least
- $omitted = $this->mTrxWriteQueryCount - $this->mTrxWriteAdjQueryCount;
+ $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
$applyTime += self::TINY_WRITE_SEC * $omitted;
return $applyTime;
default: // everything
- return $this->mTrxWriteDuration;
+ return $this->trxWriteDuration;
}
}
public function pendingWriteCallers() {
- return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
+ return $this->trxLevel ? $this->trxWriteCallers : [];
}
public function pendingWriteRowsAffected() {
- return $this->mTrxWriteAffectedRows;
+ return $this->trxWriteAffectedRows;
}
/**
@@ -598,15 +728,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @return array
*/
protected function pendingWriteAndCallbackCallers() {
- if ( !$this->mTrxLevel ) {
+ if ( !$this->trxLevel ) {
return [];
}
- $fnames = $this->mTrxWriteCallers;
+ $fnames = $this->trxWriteCallers;
foreach ( [
- $this->mTrxIdleCallbacks,
- $this->mTrxPreCommitCallbacks,
- $this->mTrxEndCallbacks
+ $this->trxIdleCallbacks,
+ $this->trxPreCommitCallbacks,
+ $this->trxEndCallbacks
] as $callbacks ) {
foreach ( $callbacks as $callback ) {
$fnames[] = $callback[1];
@@ -616,22 +746,39 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
return $fnames;
}
+ /**
+ * @return string
+ */
+ private function flatAtomicSectionList() {
+ return array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
+ return $accum === null ? $v[0] : "$accum, " . $v[0];
+ } );
+ }
+
public function isOpen() {
- return $this->mOpened;
+ return $this->opened;
}
public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
+ if ( ( $flag & self::DBO_IGNORE ) ) {
+ throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
+ }
+
if ( $remember === self::REMEMBER_PRIOR ) {
- array_push( $this->priorFlags, $this->mFlags );
+ array_push( $this->priorFlags, $this->flags );
}
- $this->mFlags |= $flag;
+ $this->flags |= $flag;
}
public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
+ if ( ( $flag & self::DBO_IGNORE ) ) {
+ throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
+ }
+
if ( $remember === self::REMEMBER_PRIOR ) {
- array_push( $this->priorFlags, $this->mFlags );
+ array_push( $this->priorFlags, $this->flags );
}
- $this->mFlags &= ~$flag;
+ $this->flags &= ~$flag;
}
public function restoreFlags( $state = self::RESTORE_PRIOR ) {
@@ -640,15 +787,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
if ( $state === self::RESTORE_INITIAL ) {
- $this->mFlags = reset( $this->priorFlags );
+ $this->flags = reset( $this->priorFlags );
$this->priorFlags = [];
} else {
- $this->mFlags = array_pop( $this->priorFlags );
+ $this->flags = array_pop( $this->priorFlags );
}
}
public function getFlag( $flag ) {
- return !!( $this->mFlags & $flag );
+ return !!( $this->flags & $flag );
}
/**
@@ -689,7 +836,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* Set a custom error handler for logging errors during database connection
*/
protected function installErrorHandler() {
- $this->mPHPError = false;
+ $this->phpError = false;
$this->htmlErrors = ini_set( 'html_errors', '0' );
set_error_handler( [ $this, 'connectionErrorLogger' ] );
}
@@ -712,8 +859,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @return string|bool Last PHP error for this DB (typically connection errors)
*/
protected function getLastPHPError() {
- if ( $this->mPHPError ) {
- $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
+ if ( $this->phpError ) {
+ $error = preg_replace( '!\[<a.*</a>\]!', '', $this->phpError );
$error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
return $error;
@@ -730,7 +877,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @param string $errstr
*/
public function connectionErrorLogger( $errno, $errstr ) {
- $this->mPHPError = $errstr;
+ $this->phpError = $errstr;
}
/**
@@ -742,32 +889,85 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
protected function getLogContext( array $extras = [] ) {
return array_merge(
[
- 'db_server' => $this->mServer,
- 'db_name' => $this->mDBname,
- 'db_user' => $this->mUser,
+ 'db_server' => $this->server,
+ 'db_name' => $this->dbName,
+ 'db_user' => $this->user,
],
$extras
);
}
- public function close() {
- if ( $this->mConn ) {
- if ( $this->trxLevel() ) {
- $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
+ final public function close() {
+ $exception = null; // error to throw after disconnecting
+
+ if ( $this->conn ) {
+ // Resolve any dangling transaction first
+ if ( $this->trxLevel ) {
+ if ( $this->trxAtomicLevels ) {
+ // Cannot let incomplete atomic sections be committed
+ $levels = $this->flatAtomicSectionList();
+ $exception = new DBUnexpectedError(
+ $this,
+ __METHOD__ . ": atomic sections $levels are still open."
+ );
+ } elseif ( $this->trxAutomatic ) {
+ // Only the connection manager can commit non-empty DBO_TRX transactions
+ if ( $this->writesOrCallbacksPending() ) {
+ $exception = new DBUnexpectedError(
+ $this,
+ __METHOD__ .
+ ": mass commit/rollback of peer transaction required (DBO_TRX set)."
+ );
+ }
+ } elseif ( $this->trxLevel ) {
+ // Commit explicit transactions as if this was commit()
+ $this->queryLogger->warning(
+ __METHOD__ . ": writes or callbacks still pending.",
+ [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
+ );
+ }
+
+ if ( $this->trxEndCallbacksSuppressed ) {
+ $exception = $exception ?: new DBUnexpectedError(
+ $this,
+ __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
+ );
+ }
+
+ // Commit or rollback the changes and run any callbacks as needed
+ if ( $this->trxStatus === self::STATUS_TRX_OK && !$exception ) {
+ $this->commit(
+ __METHOD__,
+ $this->trxAutomatic ? self::FLUSHING_INTERNAL : self::FLUSHING_ONE
+ );
+ } else {
+ $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
+ }
}
+ // Close the actual connection in the binding handle
$closed = $this->closeConnection();
- $this->mConn = false;
- } elseif (
- $this->mTrxIdleCallbacks ||
- $this->mTrxPreCommitCallbacks ||
- $this->mTrxEndCallbacks
- ) { // sanity
- throw new RuntimeException( "Transaction callbacks still pending." );
+ $this->conn = false;
} else {
- $closed = true;
+ $closed = true; // already closed; nothing to do
+ }
+
+ $this->opened = false;
+
+ // Throw any unexpected errors after having disconnected
+ if ( $exception instanceof Exception ) {
+ throw $exception;
+ }
+
+ // Sanity check that no callbacks are dangling
+ if (
+ $this->trxIdleCallbacks || $this->trxPreCommitCallbacks || $this->trxEndCallbacks
+ ) {
+ throw new RuntimeException(
+ "Transaction callbacks are still pending:\n" .
+ implode( ', ', $this->pendingWriteAndCallbackCallers() )
+ );
}
- $this->mOpened = false;
return $closed;
}
@@ -790,6 +990,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
*/
abstract protected function closeConnection();
+ /**
+ * @param string $error Fallback error message, used if none is given by DB
+ * @throws DBConnectionError
+ */
public function reportConnectionError( $error = 'Unknown error' ) {
$myError = $this->lastError();
if ( $myError ) {
@@ -801,11 +1005,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
/**
- * The DBMS-dependent part of query()
+ * Run a query and return a DBMS-dependent wrapper (that has all IResultWrapper methods)
+ *
+ * This might return things, such as mysqli_result, that do not formally implement
+ * IResultWrapper, but nonetheless implement all of its methods correctly
*
* @param string $sql SQL query.
- * @return ResultWrapper|bool Result object to feed to fetchObject,
- * fetchRow, ...; or false on failure
+ * @return IResultWrapper|bool Iterator to feed to fetchObject/fetchRow; false on failure
*/
abstract protected function doQuery( $sql );
@@ -856,7 +1062,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$sql,
$matches
) ) {
- $this->mSessionTempTables[$matches[1]] = 1;
+ $this->sessionTempTables[$matches[1]] = 1;
return true;
} elseif ( preg_match(
@@ -864,8 +1070,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$sql,
$matches
) ) {
- $isTemp = isset( $this->mSessionTempTables[$matches[1]] );
- unset( $this->mSessionTempTables[$matches[1]] );
+ $isTemp = isset( $this->sessionTempTables[$matches[1]] );
+ unset( $this->sessionTempTables[$matches[1]] );
return $isTemp;
} elseif ( preg_match(
@@ -873,21 +1079,26 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$sql,
$matches
) ) {
- return isset( $this->mSessionTempTables[$matches[1]] );
+ return isset( $this->sessionTempTables[$matches[1]] );
} elseif ( preg_match(
'/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
$sql,
$matches
) ) {
- return isset( $this->mSessionTempTables[$matches[1]] );
+ return isset( $this->sessionTempTables[$matches[1]] );
}
return false;
}
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
+ $this->assertTransactionStatus( $sql, $fname );
+
+ # Avoid fatals if close() was called
+ $this->assertOpen();
+
$priorWritesPending = $this->writesOrCallbacksPending();
- $this->mLastQuery = $sql;
+ $this->lastQuery = $sql;
$isWrite = $this->isWriteQuery( $sql );
if ( $isWrite ) {
@@ -897,6 +1108,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
if ( $isWrite ) {
+ if ( $this->getLBInfo( 'replica' ) === true ) {
+ throw new DBError(
+ $this,
+ 'Write operations are not allowed on replica database connections.'
+ );
+ }
# In theory, non-persistent writes are allowed in read-only mode, but due to things
# like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
$reason = $this->getReadOnlyReason();
@@ -904,7 +1121,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
}
# Set a flag indicating that writes have been done
- $this->mLastWriteTime = microtime( true );
+ $this->lastWriteTime = microtime( true );
}
# Add trace comment to the begin of the sql string, right after the operator.
@@ -912,81 +1129,82 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
# Start implicit transactions that wrap the request if DBO_TRX is enabled
- if ( !$this->mTrxLevel && $this->getFlag( self::DBO_TRX )
+ if ( !$this->trxLevel && $this->getFlag( self::DBO_TRX )
&& $this->isTransactableQuery( $sql )
) {
$this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
- $this->mTrxAutomatic = true;
+ $this->trxAutomatic = true;
}
# Keep track of whether the transaction has write queries pending
- if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
- $this->mTrxDoneWrites = true;
+ if ( $this->trxLevel && !$this->trxDoneWrites && $isWrite ) {
+ $this->trxDoneWrites = true;
$this->trxProfiler->transactionWritingIn(
- $this->mServer, $this->mDBname, $this->mTrxShortId );
+ $this->server, $this->dbName, $this->trxShortId );
}
if ( $this->getFlag( self::DBO_DEBUG ) ) {
- $this->queryLogger->debug( "{$this->mDBname} {$commentedSql}" );
+ $this->queryLogger->debug( "{$this->dbName} {$commentedSql}" );
}
- # Avoid fatals if close() was called
- $this->assertOpen();
-
- # Send the query to the server
+ # Send the query to the server and fetch any corresponding errors
$ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
+ $lastError = $this->lastError();
+ $lastErrno = $this->lastErrno();
# Try reconnecting if the connection was lost
- if ( false === $ret && $this->wasErrorReissuable() ) {
+ if ( $ret === false && $this->wasConnectionLoss() ) {
+ # Check if any meaningful session state was lost
$recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
- # Stash the last error values before anything might clear them
- $lastError = $this->lastError();
- $lastErrno = $this->lastErrno();
- # Update state tracking to reflect transaction loss due to disconnection
- $this->handleSessionLoss();
- if ( $this->reconnect() ) {
- $msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
- $this->connLogger->warning( $msg );
- $this->queryLogger->warning(
- "$msg:\n" . ( new RuntimeException() )->getTraceAsString() );
-
- if ( !$recoverable ) {
- # Callers may catch the exception and continue to use the DB
- $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
- } else {
- # Should be safe to silently retry the query
- $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
+ # Update session state tracking and try to restore the connection
+ $reconnected = $this->replaceLostConnection( __METHOD__ );
+ # Silently resend the query to the server if it is safe and possible
+ if ( $reconnected && $recoverable ) {
+ $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
+ $lastError = $this->lastError();
+ $lastErrno = $this->lastErrno();
+
+ if ( $ret === false && $this->wasConnectionLoss() ) {
+ # Query probably causes disconnects; reconnect and do not re-run it
+ $this->replaceLostConnection( __METHOD__ );
}
- } else {
- $msg = __METHOD__ . ": lost connection to {$this->getServer()} permanently";
- $this->connLogger->error( $msg );
}
}
- if ( false === $ret ) {
- # Deadlocks cause the entire transaction to abort, not just the statement.
- # https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
- # https://www.postgresql.org/docs/9.1/static/explicit-locking.html
- if ( $this->wasDeadlock() ) {
- if ( $this->explicitTrxActive() || $priorWritesPending ) {
- $tempIgnore = false; // not recoverable
+ if ( $ret === false ) {
+ if ( $this->trxLevel ) {
+ if ( !$this->wasKnownStatementRollbackError() ) {
+ # Either the query was aborted or all queries after BEGIN where aborted.
+ if ( $this->explicitTrxActive() || $priorWritesPending ) {
+ # In the first case, the only options going forward are (a) ROLLBACK, or
+ # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
+ # option is ROLLBACK, since the snapshots would have been released.
+ $this->trxStatus = self::STATUS_TRX_ERROR;
+ $this->trxStatusCause =
+ $this->makeQueryException( $lastError, $lastErrno, $sql, $fname );
+ $tempIgnore = false; // cannot recover
+ } else {
+ # Nothing prior was there to lose from the transaction,
+ # so just roll it back.
+ $this->rollback( __METHOD__ . " ($fname)", self::FLUSHING_INTERNAL );
+ }
+ $this->trxStatusIgnoredCause = null;
+ } else {
+ # We're ignoring an error that caused just the current query to be aborted.
+ # But log the cause so we can log a deprecation notice if a
+ # caller actually does ignore it.
+ $this->trxStatusIgnoredCause = [ $lastError, $lastErrno, $fname ];
}
- # Update state tracking to reflect transaction loss
- $this->handleSessionLoss();
}
- $this->reportQueryError(
- $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
+ $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
}
- $res = $this->resultObject( $ret );
-
- return $res;
+ return $this->resultObject( $ret );
}
/**
- * Helper method for query() that handles profiling and logging and sends
- * the query to doQuery()
+ * Wrapper for query() that also handles profiling, logging, and affected row count updates
*
* @param string $sql Original SQL query
* @param string $commentedSql SQL query with debugging/trace comment
@@ -1006,13 +1224,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
# Include query transaction state
- $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
+ $queryProf .= $this->trxShortId ? " [TRX#{$this->trxShortId}]" : "";
$startTime = microtime( true );
if ( $this->profiler ) {
call_user_func( [ $this->profiler, 'profileIn' ], $queryProf );
}
+ $this->affectedRowCount = null;
$ret = $this->doQuery( $commentedSql );
+ $this->affectedRowCount = $this->affectedRows();
if ( $this->profiler ) {
call_user_func( [ $this->profiler, 'profileOut' ], $queryProf );
}
@@ -1022,14 +1242,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
if ( $ret !== false ) {
$this->lastPing = $startTime;
- if ( $isWrite && $this->mTrxLevel ) {
+ if ( $isWrite && $this->trxLevel ) {
$this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
- $this->mTrxWriteCallers[] = $fname;
+ $this->trxWriteCallers[] = $fname;
}
}
if ( $sql === self::PING_QUERY ) {
- $this->mRTTEstimate = $queryRuntime;
+ $this->rttEstimate = $queryRuntime;
}
$this->trxProfiler->recordQueryCompletion(
@@ -1069,12 +1289,39 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
}
- $this->mTrxWriteDuration += $runtime;
- $this->mTrxWriteQueryCount += 1;
- $this->mTrxWriteAffectedRows += $affected;
+ $this->trxWriteDuration += $runtime;
+ $this->trxWriteQueryCount += 1;
+ $this->trxWriteAffectedRows += $affected;
if ( $indicativeOfReplicaRuntime ) {
- $this->mTrxWriteAdjDuration += $runtime;
- $this->mTrxWriteAdjQueryCount += 1;
+ $this->trxWriteAdjDuration += $runtime;
+ $this->trxWriteAdjQueryCount += 1;
+ }
+ }
+
+ /**
+ * @param string $sql
+ * @param string $fname
+ * @throws DBTransactionStateError
+ */
+ private function assertTransactionStatus( $sql, $fname ) {
+ if ( $this->getQueryVerb( $sql ) === 'ROLLBACK' ) { // transaction/savepoint
+ return;
+ }
+
+ if ( $this->trxStatus < self::STATUS_TRX_OK ) {
+ throw new DBTransactionStateError(
+ $this,
+ "Cannot execute query from $fname while transaction status is ERROR.",
+ [],
+ $this->trxStatusCause
+ );
+ } elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
+ list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause;
+ call_user_func( $this->deprecationLogger,
+ "Caller from $fname ignored an error originally raised from $iFname: " .
+ "[$iLastErrno] $iLastError"
+ );
+ $this->trxStatusIgnoredCause = null;
}
}
@@ -1093,14 +1340,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
# Dropped connections also mean that named locks are automatically released.
# Only allow error suppression in autocommit mode or when the lost transaction
# didn't matter anyway (aside from DBO_TRX snapshot loss).
- if ( $this->mNamedLocksHeld ) {
+ if ( $this->namedLocksHeld ) {
return false; // possible critical section violation
+ } elseif ( $this->sessionTempTables ) {
+ return false; // tables might be queried latter
} elseif ( $sql === 'COMMIT' ) {
return !$priorWritesPending; // nothing written anyway? (T127428)
} elseif ( $sql === 'ROLLBACK' ) {
return true; // transaction lost...which is also what was requested :)
} elseif ( $this->explicitTrxActive() ) {
- return false; // don't drop atomocity
+ return false; // don't drop atomocity and explicit snapshots
} elseif ( $priorWritesPending ) {
return false; // prior writes lost from implicit transaction
}
@@ -1109,45 +1358,106 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
/**
- * Clean things up after transaction loss due to disconnection
- *
- * @return null|Exception
+ * Clean things up after session (and thus transaction) loss
*/
private function handleSessionLoss() {
- $this->mTrxLevel = 0;
- $this->mTrxIdleCallbacks = []; // T67263
- $this->mTrxPreCommitCallbacks = []; // T67263
- $this->mSessionTempTables = [];
- $this->mNamedLocksHeld = [];
+ // Clean up tracking of session-level things...
+ // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
+ // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
+ $this->sessionTempTables = [];
+ // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
+ // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ $this->namedLocksHeld = [];
+ // Session loss implies transaction loss
+ $this->handleTransactionLoss();
+ }
+
+ /**
+ * Clean things up after transaction loss
+ */
+ private function handleTransactionLoss() {
+ $this->trxLevel = 0;
+ $this->trxAtomicCounter = 0;
+ $this->trxIdleCallbacks = []; // T67263; transaction already lost
+ $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
try {
- // Handle callbacks in mTrxEndCallbacks
+ // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
+ // If callback suppression is set then the array will remain unhandled.
$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+ } catch ( Exception $ex ) {
+ // Already logged; move on...
+ }
+ try {
+ // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
- return null;
- } catch ( Exception $e ) {
+ } catch ( Exception $ex ) {
// Already logged; move on...
- return $e;
}
}
+ /**
+ * Checks whether the cause of the error is detected to be a timeout.
+ *
+ * It returns false by default, and not all engines support detecting this yet.
+ * If this returns false, it will be treated as a generic query error.
+ *
+ * @param string $error Error text
+ * @param int $errno Error number
+ * @return bool
+ */
+ protected function wasQueryTimeout( $error, $errno ) {
+ return false;
+ }
+
+ /**
+ * Report a query error. Log the error, and if neither the object ignore
+ * flag nor the $tempIgnore flag is set, throw a DBQueryError.
+ *
+ * @param string $error
+ * @param int $errno
+ * @param string $sql
+ * @param string $fname
+ * @param bool $tempIgnore
+ * @throws DBQueryError
+ */
public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- if ( $this->ignoreErrors() || $tempIgnore ) {
+ if ( $tempIgnore ) {
$this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
} else {
- $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
- $this->queryLogger->error(
- "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
- $this->getLogContext( [
- 'method' => __METHOD__,
- 'errno' => $errno,
- 'error' => $error,
- 'sql1line' => $sql1line,
- 'fname' => $fname,
- ] )
- );
- $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
- throw new DBQueryError( $this, $error, $errno, $sql, $fname );
+ $exception = $this->makeQueryException( $error, $errno, $sql, $fname );
+
+ throw $exception;
+ }
+ }
+
+ /**
+ * @param string $error
+ * @param string|int $errno
+ * @param string $sql
+ * @param string $fname
+ * @return DBError
+ */
+ private function makeQueryException( $error, $errno, $sql, $fname ) {
+ $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
+ $this->queryLogger->error(
+ "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
+ $this->getLogContext( [
+ 'method' => __METHOD__,
+ 'errno' => $errno,
+ 'error' => $error,
+ 'sql1line' => $sql1line,
+ 'fname' => $fname,
+ ] )
+ );
+ $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
+ $wasQueryTimeout = $this->wasQueryTimeout( $error, $errno );
+ if ( $wasQueryTimeout ) {
+ $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
+ } else {
+ $e = new DBQueryError( $this, $error, $errno, $sql, $fname );
}
+
+ return $e;
}
public function freeResult( $res ) {
@@ -1193,14 +1503,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$options = [ $options ];
}
- $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
+ $res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
if ( $res === false ) {
return false;
}
$values = [];
foreach ( $res as $row ) {
- $values[] = $row->$var;
+ $values[] = $row->value;
}
return $values;
@@ -1366,13 +1676,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$this->tableNamesWithIndexClauseOrJOIN(
$table, $useIndexes, $ignoreIndexes, $join_conds );
} elseif ( $table != '' ) {
- if ( $table[0] == ' ' ) {
- $from = ' FROM ' . $table;
- } else {
- $from = ' FROM ' .
- $this->tableNamesWithIndexClauseOrJOIN(
- [ $table ], $useIndexes, $ignoreIndexes, [] );
- }
+ $from = ' FROM ' .
+ $this->tableNamesWithIndexClauseOrJOIN(
+ [ $table ], $useIndexes, $ignoreIndexes, [] );
} else {
$from = '';
}
@@ -1380,14 +1686,27 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
$this->makeSelectOptions( $options );
- if ( !empty( $conds ) ) {
- if ( is_array( $conds ) ) {
- $conds = $this->makeList( $conds, self::LIST_AND );
- }
+ if ( is_array( $conds ) ) {
+ $conds = $this->makeList( $conds, self::LIST_AND );
+ }
+
+ if ( $conds === null || $conds === false ) {
+ $this->queryLogger->warning(
+ __METHOD__
+ . ' called from '
+ . $fname
+ . ' with incorrect parameters: $conds must be a string or an array'
+ );
+ $conds = '';
+ }
+
+ if ( $conds === '' ) {
+ $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
+ } elseif ( is_string( $conds ) ) {
$sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
"WHERE $conds $preLimitTail";
} else {
- $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
+ throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
}
if ( isset( $options['LIMIT'] ) ) {
@@ -1424,35 +1743,93 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
public function estimateRowCount(
- $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
+ $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
) {
- $rows = 0;
- $res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
-
- if ( $res ) {
- $row = $this->fetchRow( $res );
- $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
+ $conds = $this->normalizeConditions( $conds, $fname );
+ $column = $this->extractSingleFieldFromList( $var );
+ if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
+ $conds[] = "$column IS NOT NULL";
}
- return $rows;
+ $res = $this->select(
+ $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
+ );
+ $row = $res ? $this->fetchRow( $res ) : [];
+
+ return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
}
public function selectRowCount(
- $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
+ $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
) {
- $rows = 0;
- $sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
- // The identifier quotes is primarily for MSSQL.
- $rowCountCol = $this->addIdentifierQuotes( "rowcount" );
- $tableName = $this->addIdentifierQuotes( "tmp_count" );
- $res = $this->query( "SELECT COUNT(*) AS $rowCountCol FROM ($sql) $tableName", $fname );
+ $conds = $this->normalizeConditions( $conds, $fname );
+ $column = $this->extractSingleFieldFromList( $var );
+ if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
+ $conds[] = "$column IS NOT NULL";
+ }
+
+ $res = $this->select(
+ [
+ 'tmp_count' => $this->buildSelectSubquery(
+ $tables,
+ '1',
+ $conds,
+ $fname,
+ $options,
+ $join_conds
+ )
+ ],
+ [ 'rowcount' => 'COUNT(*)' ],
+ [],
+ $fname
+ );
+ $row = $res ? $this->fetchRow( $res ) : [];
+
+ return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
+ }
+
+ /**
+ * @param array|string $conds
+ * @param string $fname
+ * @return array
+ */
+ final protected function normalizeConditions( $conds, $fname ) {
+ if ( $conds === null || $conds === false ) {
+ $this->queryLogger->warning(
+ __METHOD__
+ . ' called from '
+ . $fname
+ . ' with incorrect parameters: $conds must be a string or an array'
+ );
+ $conds = '';
+ }
- if ( $res ) {
- $row = $this->fetchRow( $res );
- $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
+ if ( !is_array( $conds ) ) {
+ $conds = ( $conds === '' ) ? [] : [ $conds ];
}
- return $rows;
+ return $conds;
+ }
+
+ /**
+ * @param array|string $var Field parameter in the style of select()
+ * @return string|null Column name or null; ignores aliases
+ * @throws DBUnexpectedError Errors out if multiple columns are given
+ */
+ final protected function extractSingleFieldFromList( $var ) {
+ if ( is_array( $var ) ) {
+ if ( !$var ) {
+ $column = null;
+ } elseif ( count( $var ) == 1 ) {
+ $column = isset( $var[0] ) ? $var[0] : reset( $var );
+ } else {
+ throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns.' );
+ }
+ } else {
+ $column = $var;
+ }
+
+ return $column;
}
/**
@@ -1506,7 +1883,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
public function tableExists( $table, $fname = __METHOD__ ) {
$tableRaw = $this->tableName( $table, 'raw' );
- if ( isset( $this->mSessionTempTables[$tableRaw] ) ) {
+ if ( isset( $this->sessionTempTables[$tableRaw] ) ) {
return true; // already known to exist
}
@@ -1760,10 +2137,57 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
}
+ public function buildSubstring( $input, $startPosition, $length = null ) {
+ $this->assertBuildSubstringParams( $startPosition, $length );
+ $functionBody = "$input FROM $startPosition";
+ if ( $length !== null ) {
+ $functionBody .= " FOR $length";
+ }
+ return 'SUBSTRING(' . $functionBody . ')';
+ }
+
+ /**
+ * Check type and bounds for parameters to self::buildSubstring()
+ *
+ * All supported databases have substring functions that behave the same for
+ * positive $startPosition and non-negative $length, but behaviors differ when
+ * given 0 or negative $startPosition or negative $length. The simplest
+ * solution to that is to just forbid those values.
+ *
+ * @param int $startPosition
+ * @param int|null $length
+ * @since 1.31
+ */
+ protected function assertBuildSubstringParams( $startPosition, $length ) {
+ if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
+ throw new InvalidArgumentException(
+ '$startPosition must be a positive integer'
+ );
+ }
+ if ( !( is_int( $length ) && $length >= 0 || $length === null ) ) {
+ throw new InvalidArgumentException(
+ '$length must be null or an integer greater than or equal to 0'
+ );
+ }
+ }
+
public function buildStringCast( $field ) {
return $field;
}
+ public function buildIntegerCast( $field ) {
+ return 'CAST( ' . $field . ' AS INTEGER )';
+ }
+
+ public function buildSelectSubquery(
+ $table, $vars, $conds = '', $fname = __METHOD__,
+ $options = [], $join_conds = []
+ ) {
+ return new Subquery(
+ $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
+ );
+ }
+
public function databasesAreIndependent() {
return false;
}
@@ -1772,20 +2196,27 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
# Stub. Shouldn't cause serious problems if it's not overridden, but
# if your database engine supports a concept similar to MySQL's
# databases you may as well.
- $this->mDBname = $db;
+ $this->dbName = $db;
return true;
}
public function getDBname() {
- return $this->mDBname;
+ return $this->dbName;
}
public function getServer() {
- return $this->mServer;
+ return $this->server;
}
public function tableName( $name, $format = 'quoted' ) {
+ if ( $name instanceof Subquery ) {
+ throw new DBUnexpectedError(
+ $this,
+ __METHOD__ . ': got Subquery instance when expecting a string.'
+ );
+ }
+
# Skip the entire process when we have a string quoted on both ends.
# Note that we check the end so that we will still quote any use of
# use of `database`.table. But won't break things if someone wants
@@ -1802,6 +2233,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
# any remote case where a word like on may be inside of a table name
# surrounded by symbols which may be considered word breaks.
if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
+ $this->queryLogger->warning(
+ __METHOD__ . ": use of subqueries is not supported this way.",
+ [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
+ );
+
return $name;
}
@@ -1851,14 +2287,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$database = $this->tableAliases[$table]['dbname'];
$schema = is_string( $this->tableAliases[$table]['schema'] )
? $this->tableAliases[$table]['schema']
- : $this->mSchema;
+ : $this->schema;
$prefix = is_string( $this->tableAliases[$table]['prefix'] )
? $this->tableAliases[$table]['prefix']
- : $this->mTablePrefix;
+ : $this->tablePrefix;
} else {
$database = '';
- $schema = $this->mSchema; # Default schema
- $prefix = $this->mTablePrefix; # Default prefix
+ $schema = $this->schema; # Default schema
+ $prefix = $this->tablePrefix; # Default prefix
}
}
@@ -1906,17 +2342,32 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
/**
* Get an aliased table name
- * e.g. tableName AS newTableName
*
- * @param string $name Table name, see tableName()
- * @param string|bool $alias Alias (optional)
+ * This returns strings like "tableName AS newTableName" for aliased tables
+ * and "(SELECT * from tableA) newTablename" for subqueries (e.g. derived tables)
+ *
+ * @see Database::tableName()
+ * @param string|Subquery $table Table name or object with a 'sql' field
+ * @param string|bool $alias Table alias (optional)
* @return string SQL name for aliased table. Will not alias a table to its own name
*/
- protected function tableNameWithAlias( $name, $alias = false ) {
- if ( !$alias || $alias == $name ) {
- return $this->tableName( $name );
+ protected function tableNameWithAlias( $table, $alias = false ) {
+ if ( is_string( $table ) ) {
+ $quotedTable = $this->tableName( $table );
+ } elseif ( $table instanceof Subquery ) {
+ $quotedTable = (string)$table;
} else {
- return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
+ throw new InvalidArgumentException( "Table must be a string or Subquery." );
+ }
+
+ if ( !strlen( $alias ) || $alias === $table ) {
+ if ( $table instanceof Subquery ) {
+ throw new InvalidArgumentException( "Subquery table missing alias." );
+ }
+
+ return $quotedTable;
+ } else {
+ return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
}
}
@@ -1996,11 +2447,31 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
// No alias? Set it equal to the table name
$alias = $table;
}
+
+ if ( is_array( $table ) ) {
+ // A parenthesized group
+ if ( count( $table ) > 1 ) {
+ $joinedTable = '(' .
+ $this->tableNamesWithIndexClauseOrJOIN(
+ $table, $use_index, $ignore_index, $join_conds ) . ')';
+ } else {
+ // Degenerate case
+ $innerTable = reset( $table );
+ $innerAlias = key( $table );
+ $joinedTable = $this->tableNameWithAlias(
+ $innerTable,
+ is_string( $innerAlias ) ? $innerAlias : $innerTable
+ );
+ }
+ } else {
+ $joinedTable = $this->tableNameWithAlias( $table, $alias );
+ }
+
// Is there a JOIN clause for this table?
if ( isset( $join_conds[$alias] ) ) {
list( $joinType, $conds ) = $join_conds[$alias];
$tableClause = $joinType;
- $tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
+ $tableClause .= ' ' . $joinedTable;
if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
if ( $use != '' ) {
@@ -2022,7 +2493,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$retJOIN[] = $tableClause;
} elseif ( isset( $use_index[$alias] ) ) {
// Is there an INDEX clause for this table?
- $tableClause = $this->tableNameWithAlias( $table, $alias );
+ $tableClause = $joinedTable;
$tableClause .= ' ' . $this->useIndexClause(
implode( ',', (array)$use_index[$alias] )
);
@@ -2030,22 +2501,22 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$ret[] = $tableClause;
} elseif ( isset( $ignore_index[$alias] ) ) {
// Is there an INDEX clause for this table?
- $tableClause = $this->tableNameWithAlias( $table, $alias );
+ $tableClause = $joinedTable;
$tableClause .= ' ' . $this->ignoreIndexClause(
implode( ',', (array)$ignore_index[$alias] )
);
$ret[] = $tableClause;
} else {
- $tableClause = $this->tableNameWithAlias( $table, $alias );
+ $tableClause = $joinedTable;
$ret[] = $tableClause;
}
}
// We can't separate explicit JOIN clauses with ',', use ' ' for those
- $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
- $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
+ $implicitJoins = $ret ? implode( ',', $ret ) : "";
+ $explicitJoins = $retJOIN ? implode( ' ', $retJOIN ) : "";
// Compile our final table clause
return implode( ' ', [ $implicitJoins, $explicitJoins ] );
@@ -2058,7 +2529,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @return string
*/
protected function indexName( $index ) {
- return $index;
+ return isset( $this->indexAliases[$index] )
+ ? $this->indexAliases[$index]
+ : $index;
}
public function addQuotes( $s ) {
@@ -2138,7 +2611,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
}
- return ' LIKE ' . $this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
+ return ' LIKE ' .
+ $this->addQuotes( $s ) . ' ESCAPE ' . $this->addQuotes( $escapeChar ) . ' ';
}
public function anyChar() {
@@ -2182,50 +2656,54 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
- $quotedTable = $this->tableName( $table );
-
if ( count( $rows ) == 0 ) {
return;
}
- # Single row case
+ // Single row case
if ( !is_array( reset( $rows ) ) ) {
$rows = [ $rows ];
}
- // @FXIME: this is not atomic, but a trx would break affectedRows()
- foreach ( $rows as $row ) {
- # Delete rows which collide
- if ( $uniqueIndexes ) {
- $sql = "DELETE FROM $quotedTable WHERE ";
- $first = true;
+ try {
+ $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
+ $affectedRowCount = 0;
+ foreach ( $rows as $row ) {
+ // Delete rows which collide with this one
+ $indexWhereClauses = [];
foreach ( $uniqueIndexes as $index ) {
- if ( $first ) {
- $first = false;
- $sql .= '( ';
- } else {
- $sql .= ' ) OR ( ';
- }
- if ( is_array( $index ) ) {
- $first2 = true;
- foreach ( $index as $col ) {
- if ( $first2 ) {
- $first2 = false;
- } else {
- $sql .= ' AND ';
- }
- $sql .= $col . '=' . $this->addQuotes( $row[$col] );
- }
- } else {
- $sql .= $index . '=' . $this->addQuotes( $row[$index] );
+ $indexColumns = (array)$index;
+ $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
+ if ( count( $indexRowValues ) != count( $indexColumns ) ) {
+ throw new DBUnexpectedError(
+ $this,
+ 'New record does not provide all values for unique key (' .
+ implode( ', ', $indexColumns ) . ')'
+ );
+ } elseif ( in_array( null, $indexRowValues, true ) ) {
+ throw new DBUnexpectedError(
+ $this,
+ 'New record has a null value for unique key (' .
+ implode( ', ', $indexColumns ) . ')'
+ );
}
+ $indexWhereClauses[] = $this->makeList( $indexRowValues, LIST_AND );
+ }
+
+ if ( $indexWhereClauses ) {
+ $this->delete( $table, $this->makeList( $indexWhereClauses, LIST_OR ), $fname );
+ $affectedRowCount += $this->affectedRows();
}
- $sql .= ' )';
- $this->query( $sql, $fname );
- }
- # Now insert the row
- $this->insert( $table, $row, $fname );
+ // Now insert the row
+ $this->insert( $table, $row, $fname );
+ $affectedRowCount += $this->affectedRows();
+ }
+ $this->endAtomic( $fname );
+ $this->affectedRowCount = $affectedRowCount;
+ } catch ( Exception $e ) {
+ $this->cancelAtomic( $fname );
+ throw $e;
}
}
@@ -2291,28 +2769,25 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$where = false;
}
- $useTrx = !$this->mTrxLevel;
- if ( $useTrx ) {
- $this->begin( $fname, self::TRANSACTION_INTERNAL );
- }
+ $affectedRowCount = 0;
try {
+ $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
# Update any existing conflicting row(s)
if ( $where !== false ) {
$ok = $this->update( $table, $set, $where, $fname );
+ $affectedRowCount += $this->affectedRows();
} else {
$ok = true;
}
# Now insert any non-conflicting row(s)
$ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
+ $affectedRowCount += $this->affectedRows();
+ $this->endAtomic( $fname );
+ $this->affectedRowCount = $affectedRowCount;
} catch ( Exception $e ) {
- if ( $useTrx ) {
- $this->rollback( $fname, self::FLUSHING_INTERNAL );
- }
+ $this->cancelAtomic( $fname );
throw $e;
}
- if ( $useTrx ) {
- $this->commit( $fname, self::FLUSHING_INTERNAL );
- }
return $ok;
}
@@ -2370,11 +2845,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
return $this->query( $sql, $fname );
}
- public function insertSelect(
+ final public function insertSelect(
$destTable, $srcTable, $varMap, $conds,
$fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
) {
- if ( $this->cliMode ) {
+ static $hints = [ 'NO_AUTO_COLUMNS' ];
+
+ $insertOptions = (array)$insertOptions;
+ $selectOptions = (array)$selectOptions;
+
+ if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
// For massive migrations with downtime, we don't want to select everything
// into memory and OOM, so do all this native on the server side if possible.
return $this->nativeInsertSelect(
@@ -2383,12 +2863,53 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$varMap,
$conds,
$fname,
- $insertOptions,
+ array_diff( $insertOptions, $hints ),
$selectOptions,
$selectJoinConds
);
}
+ return $this->nonNativeInsertSelect(
+ $destTable,
+ $srcTable,
+ $varMap,
+ $conds,
+ $fname,
+ array_diff( $insertOptions, $hints ),
+ $selectOptions,
+ $selectJoinConds
+ );
+ }
+
+ /**
+ * @param array $insertOptions INSERT options
+ * @param array $selectOptions SELECT options
+ * @return bool Whether an INSERT SELECT with these options will be replication safe
+ * @since 1.31
+ */
+ protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
+ return true;
+ }
+
+ /**
+ * Implementation of insertSelect() based on select() and insert()
+ *
+ * @see IDatabase::insertSelect()
+ * @since 1.30
+ * @param string $destTable
+ * @param string|array $srcTable
+ * @param array $varMap
+ * @param array $conds
+ * @param string $fname
+ * @param array $insertOptions
+ * @param array $selectOptions
+ * @param array $selectJoinConds
+ * @return bool
+ */
+ protected function nonNativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
+ $fname = __METHOD__,
+ $insertOptions = [], $selectOptions = [], $selectJoinConds = []
+ ) {
// For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
// on only the master (without needing row-based-replication). It also makes it easy to
// know how big the INSERT is going to be.
@@ -2404,12 +2925,41 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
return false;
}
- $rows = [];
- foreach ( $res as $row ) {
- $rows[] = (array)$row;
- }
+ try {
+ $affectedRowCount = 0;
+ $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
+ $rows = [];
+ $ok = true;
+ foreach ( $res as $row ) {
+ $rows[] = (array)$row;
- return $this->insert( $destTable, $rows, $fname, $insertOptions );
+ // Avoid inserts that are too huge
+ if ( count( $rows ) >= $this->nonNativeInsertSelectBatchSize ) {
+ $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
+ if ( !$ok ) {
+ break;
+ }
+ $affectedRowCount += $this->affectedRows();
+ $rows = [];
+ }
+ }
+ if ( $rows && $ok ) {
+ $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
+ if ( $ok ) {
+ $affectedRowCount += $this->affectedRows();
+ }
+ }
+ if ( $ok ) {
+ $this->endAtomic( $fname );
+ $this->affectedRowCount = $affectedRowCount;
+ } else {
+ $this->cancelAtomic( $fname );
+ }
+ return $ok;
+ } catch ( Exception $e ) {
+ $this->cancelAtomic( $fname );
+ throw $e;
+ }
}
/**
@@ -2590,14 +3140,22 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
return false;
}
- public function wasErrorReissuable() {
- return false;
+ public function wasConnectionLoss() {
+ return $this->wasConnectionError( $this->lastErrno() );
}
public function wasReadOnlyError() {
return false;
}
+ public function wasErrorReissuable() {
+ return (
+ $this->wasDeadlock() ||
+ $this->wasLockTimeout() ||
+ $this->wasConnectionLoss()
+ );
+ }
+
/**
* Do not use this method outside of Database/DBError classes
*
@@ -2608,6 +3166,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
return false;
}
+ /**
+ * @return bool Whether it is safe to assume the given error only caused statement rollback
+ * @note This is for backwards compatibility for callers catching DBError exceptions in
+ * order to ignore problems like duplicate key errors or foriegn key violations
+ * @since 1.31
+ */
+ protected function wasKnownStatementRollbackError() {
+ return false; // don't know; it could have caused a transaction rollback
+ }
+
public function deadlockLoop() {
$args = func_get_args();
$function = array_shift( $args );
@@ -2664,43 +3232,118 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
- if ( !$this->mTrxLevel ) {
+ if ( !$this->trxLevel ) {
throw new DBUnexpectedError( $this, "No transaction is active." );
}
- $this->mTrxEndCallbacks[] = [ $callback, $fname ];
+ $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
}
final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
- $this->mTrxIdleCallbacks[] = [ $callback, $fname ];
- if ( !$this->mTrxLevel ) {
+ if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
+ // Start an implicit transaction similar to how query() does
+ $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
+ $this->trxAutomatic = true;
+ }
+
+ $this->trxIdleCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
+ if ( !$this->trxLevel ) {
$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
}
}
final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
- if ( $this->mTrxLevel || $this->getFlag( self::DBO_TRX ) ) {
- // As long as DBO_TRX is set, writes will accumulate until the load balancer issues
- // an implicit commit of all peer databases. This is true even if a transaction has
- // not yet been triggered by writes; make sure $callback runs *after* any such writes.
- $this->mTrxPreCommitCallbacks[] = [ $callback, $fname ];
+ if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
+ // Start an implicit transaction similar to how query() does
+ $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
+ $this->trxAutomatic = true;
+ }
+
+ if ( $this->trxLevel ) {
+ $this->trxPreCommitCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
} else {
// No transaction is active nor will start implicitly, so make one for this callback
- $this->startAtomic( __METHOD__ );
+ $this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
try {
call_user_func( $callback );
$this->endAtomic( __METHOD__ );
} catch ( Exception $e ) {
- $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
+ $this->cancelAtomic( __METHOD__ );
throw $e;
}
}
}
+ /**
+ * @return AtomicSectionIdentifier|null ID of the topmost atomic section level
+ */
+ private function currentAtomicSectionId() {
+ if ( $this->trxLevel && $this->trxAtomicLevels ) {
+ $levelInfo = end( $this->trxAtomicLevels );
+
+ return $levelInfo[1];
+ }
+
+ return null;
+ }
+
+ /**
+ * @param AtomicSectionIdentifier $old
+ * @param AtomicSectionIdentifier $new
+ */
+ private function reassignCallbacksForSection(
+ AtomicSectionIdentifier $old, AtomicSectionIdentifier $new
+ ) {
+ foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
+ if ( $info[2] === $old ) {
+ $this->trxPreCommitCallbacks[$key][2] = $new;
+ }
+ }
+ foreach ( $this->trxIdleCallbacks as $key => $info ) {
+ if ( $info[2] === $old ) {
+ $this->trxIdleCallbacks[$key][2] = $new;
+ }
+ }
+ foreach ( $this->trxEndCallbacks as $key => $info ) {
+ if ( $info[2] === $old ) {
+ $this->trxEndCallbacks[$key][2] = $new;
+ }
+ }
+ }
+
+ /**
+ * @param AtomicSectionIdentifier[] $sectionIds ID of an actual savepoint
+ * @throws UnexpectedValueException
+ */
+ private function modifyCallbacksForCancel( array $sectionIds ) {
+ // Cancel the "on commit" callbacks owned by this savepoint
+ $this->trxIdleCallbacks = array_filter(
+ $this->trxIdleCallbacks,
+ function ( $entry ) use ( $sectionIds ) {
+ return !in_array( $entry[2], $sectionIds, true );
+ }
+ );
+ $this->trxPreCommitCallbacks = array_filter(
+ $this->trxPreCommitCallbacks,
+ function ( $entry ) use ( $sectionIds ) {
+ return !in_array( $entry[2], $sectionIds, true );
+ }
+ );
+ // Make "on resolution" callbacks owned by this savepoint to perceive a rollback
+ foreach ( $this->trxEndCallbacks as $key => $entry ) {
+ if ( in_array( $entry[2], $sectionIds, true ) ) {
+ $callback = $entry[0];
+ $this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
+ return $callback( self::TRIGGER_ROLLBACK );
+ };
+ }
+ }
+ }
+
final public function setTransactionListener( $name, callable $callback = null ) {
if ( $callback ) {
- $this->mTrxRecurringCallbacks[$name] = $callback;
+ $this->trxRecurringCallbacks[$name] = $callback;
} else {
- unset( $this->mTrxRecurringCallbacks[$name] );
+ unset( $this->trxRecurringCallbacks[$name] );
}
}
@@ -2713,7 +3356,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @since 1.28
*/
final public function setTrxEndCallbackSuppression( $suppress ) {
- $this->mTrxEndCallbacksSuppressed = $suppress;
+ $this->trxEndCallbacksSuppressed = $suppress;
}
/**
@@ -2726,7 +3369,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @throws Exception
*/
public function runOnTransactionIdleCallbacks( $trigger ) {
- if ( $this->mTrxEndCallbacksSuppressed ) {
+ if ( $this->trxEndCallbacksSuppressed ) {
return;
}
@@ -2735,11 +3378,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$e = null; // first exception
do { // callbacks may add callbacks :)
$callbacks = array_merge(
- $this->mTrxIdleCallbacks,
- $this->mTrxEndCallbacks // include "transaction resolution" callbacks
+ $this->trxIdleCallbacks,
+ $this->trxEndCallbacks // include "transaction resolution" callbacks
);
- $this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
- $this->mTrxEndCallbacks = []; // consumed (recursion guard)
+ $this->trxIdleCallbacks = []; // consumed (and recursion guard)
+ $this->trxEndCallbacks = []; // consumed (recursion guard)
foreach ( $callbacks as $callback ) {
try {
list( $phpCallback ) = $callback;
@@ -2760,7 +3403,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
}
}
- } while ( count( $this->mTrxIdleCallbacks ) );
+ } while ( count( $this->trxIdleCallbacks ) );
if ( $e instanceof Exception ) {
throw $e; // re-throw any first exception
@@ -2778,8 +3421,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
public function runOnTransactionPreCommitCallbacks() {
$e = null; // first exception
do { // callbacks may add callbacks :)
- $callbacks = $this->mTrxPreCommitCallbacks;
- $this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
+ $callbacks = $this->trxPreCommitCallbacks;
+ $this->trxPreCommitCallbacks = []; // consumed (and recursion guard)
foreach ( $callbacks as $callback ) {
try {
list( $phpCallback ) = $callback;
@@ -2789,7 +3432,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$e = $e ?: $ex;
}
}
- } while ( count( $this->mTrxPreCommitCallbacks ) );
+ } while ( count( $this->trxPreCommitCallbacks ) );
if ( $e instanceof Exception ) {
throw $e; // re-throw any first exception
@@ -2806,14 +3449,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @since 1.20
*/
public function runTransactionListenerCallbacks( $trigger ) {
- if ( $this->mTrxEndCallbacksSuppressed ) {
+ if ( $this->trxEndCallbacksSuppressed ) {
return;
}
/** @var Exception $e */
$e = null; // first exception
- foreach ( $this->mTrxRecurringCallbacks as $phpCallback ) {
+ foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
try {
$phpCallback( $trigger, $this );
} catch ( Exception $ex ) {
@@ -2827,40 +3470,201 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
}
- final public function startAtomic( $fname = __METHOD__ ) {
- if ( !$this->mTrxLevel ) {
+ /**
+ * Create a savepoint
+ *
+ * This is used internally to implement atomic sections. It should not be
+ * used otherwise.
+ *
+ * @since 1.31
+ * @param string $identifier Identifier for the savepoint
+ * @param string $fname Calling function name
+ */
+ protected function doSavepoint( $identifier, $fname ) {
+ $this->query( 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
+ }
+
+ /**
+ * Release a savepoint
+ *
+ * This is used internally to implement atomic sections. It should not be
+ * used otherwise.
+ *
+ * @since 1.31
+ * @param string $identifier Identifier for the savepoint
+ * @param string $fname Calling function name
+ */
+ protected function doReleaseSavepoint( $identifier, $fname ) {
+ $this->query( 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
+ }
+
+ /**
+ * Rollback to a savepoint
+ *
+ * This is used internally to implement atomic sections. It should not be
+ * used otherwise.
+ *
+ * @since 1.31
+ * @param string $identifier Identifier for the savepoint
+ * @param string $fname Calling function name
+ */
+ protected function doRollbackToSavepoint( $identifier, $fname ) {
+ $this->query( 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
+ }
+
+ /**
+ * @param string $fname
+ * @return string
+ */
+ private function nextSavepointId( $fname ) {
+ $savepointId = self::$SAVEPOINT_PREFIX . ++$this->trxAtomicCounter;
+ if ( strlen( $savepointId ) > 30 ) {
+ // 30 == Oracle's identifier length limit (pre 12c)
+ // With a 22 character prefix, that puts the highest number at 99999999.
+ throw new DBUnexpectedError(
+ $this,
+ 'There have been an excessively large number of atomic sections in a transaction'
+ . " started by $this->trxFname (at $fname)"
+ );
+ }
+
+ return $savepointId;
+ }
+
+ final public function startAtomic(
+ $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
+ ) {
+ $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null;
+
+ if ( !$this->trxLevel ) {
$this->begin( $fname, self::TRANSACTION_INTERNAL );
// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
// in all changes being in one transaction to keep requests transactional.
- if ( !$this->getFlag( self::DBO_TRX ) ) {
- $this->mTrxAutomaticAtomic = true;
+ if ( $this->getFlag( self::DBO_TRX ) ) {
+ // Since writes could happen in between the topmost atomic sections as part
+ // of the transaction, those sections will need savepoints.
+ $savepointId = $this->nextSavepointId( $fname );
+ $this->doSavepoint( $savepointId, $fname );
+ } else {
+ $this->trxAutomaticAtomic = true;
}
+ } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
+ $savepointId = $this->nextSavepointId( $fname );
+ $this->doSavepoint( $savepointId, $fname );
}
- $this->mTrxAtomicLevels[] = $fname;
+ $sectionId = new AtomicSectionIdentifier;
+ $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
+
+ return $sectionId;
}
final public function endAtomic( $fname = __METHOD__ ) {
- if ( !$this->mTrxLevel ) {
- throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
+ if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
+ throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
}
- if ( !$this->mTrxAtomicLevels ||
- array_pop( $this->mTrxAtomicLevels ) !== $fname
- ) {
- throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
+
+ // Check if the current section matches $fname
+ $pos = count( $this->trxAtomicLevels ) - 1;
+ list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
+
+ if ( $savedFname !== $fname ) {
+ throw new DBUnexpectedError(
+ $this,
+ "Invalid atomic section ended (got $fname but expected $savedFname)."
+ );
}
- if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
+ // Remove the last section (no need to re-index the array)
+ array_pop( $this->trxAtomicLevels );
+
+ if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
$this->commit( $fname, self::FLUSHING_INTERNAL );
+ } elseif ( $savepointId !== null && $savepointId !== self::$NOT_APPLICABLE ) {
+ $this->doReleaseSavepoint( $savepointId, $fname );
+ }
+
+ // Hoist callback ownership for callbacks in the section that just ended;
+ // all callbacks should have an owner that is present in trxAtomicLevels.
+ $currentSectionId = $this->currentAtomicSectionId();
+ if ( $currentSectionId ) {
+ $this->reassignCallbacksForSection( $sectionId, $currentSectionId );
+ }
+ }
+
+ final public function cancelAtomic(
+ $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null
+ ) {
+ if ( !$this->trxLevel || !$this->trxAtomicLevels ) {
+ throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
+ }
+
+ if ( $sectionId !== null ) {
+ // Find the (last) section with the given $sectionId
+ $pos = -1;
+ foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
+ if ( $asId === $sectionId ) {
+ $pos = $i;
+ }
+ }
+ if ( $pos < 0 ) {
+ throw new DBUnexpectedError( "Atomic section not found (for $fname)" );
+ }
+ // Remove all descendant sections and re-index the array
+ $excisedIds = [];
+ $len = count( $this->trxAtomicLevels );
+ for ( $i = $pos + 1; $i < $len; ++$i ) {
+ $excisedIds[] = $this->trxAtomicLevels[$i][1];
+ }
+ $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
+ $this->modifyCallbacksForCancel( $excisedIds );
+ }
+
+ // Check if the current section matches $fname
+ $pos = count( $this->trxAtomicLevels ) - 1;
+ list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
+
+ if ( $savedFname !== $fname ) {
+ throw new DBUnexpectedError(
+ $this,
+ "Invalid atomic section ended (got $fname but expected $savedFname)."
+ );
}
+
+ // Remove the last section (no need to re-index the array)
+ array_pop( $this->trxAtomicLevels );
+ $this->modifyCallbacksForCancel( [ $savedSectionId ] );
+
+ if ( $savepointId !== null ) {
+ // Rollback the transaction to the state just before this atomic section
+ if ( $savepointId === self::$NOT_APPLICABLE ) {
+ $this->rollback( $fname, self::FLUSHING_INTERNAL );
+ } else {
+ $this->doRollbackToSavepoint( $savepointId, $fname );
+ $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered
+ $this->trxStatusIgnoredCause = null;
+ }
+ } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
+ // Put the transaction into an error state if it's not already in one
+ $this->trxStatus = self::STATUS_TRX_ERROR;
+ $this->trxStatusCause = new DBUnexpectedError(
+ $this,
+ "Uncancelable atomic section canceled (got $fname)."
+ );
+ }
+
+ $this->affectedRowCount = 0; // for the sake of consistency
}
- final public function doAtomicSection( $fname, callable $callback ) {
- $this->startAtomic( $fname );
+ final public function doAtomicSection(
+ $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
+ ) {
+ $sectionId = $this->startAtomic( $fname, $cancelable );
try {
$res = call_user_func_array( $callback, [ $this, $fname ] );
} catch ( Exception $e ) {
- $this->rollback( $fname, self::FLUSHING_INTERNAL );
+ $this->cancelAtomic( $fname, $sectionId );
+
throw $e;
}
$this->endAtomic( $fname );
@@ -2869,53 +3673,56 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
+ static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
+ if ( !in_array( $mode, $modes, true ) ) {
+ throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'." );
+ }
+
// Protect against mismatched atomic section, transaction nesting, and snapshot loss
- if ( $this->mTrxLevel ) {
- if ( $this->mTrxAtomicLevels ) {
- $levels = implode( ', ', $this->mTrxAtomicLevels );
+ if ( $this->trxLevel ) {
+ if ( $this->trxAtomicLevels ) {
+ $levels = $this->flatAtomicSectionList();
$msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
throw new DBUnexpectedError( $this, $msg );
- } elseif ( !$this->mTrxAutomatic ) {
- $msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
+ } elseif ( !$this->trxAutomatic ) {
+ $msg = "$fname: Explicit transaction already active (from {$this->trxFname}).";
throw new DBUnexpectedError( $this, $msg );
} else {
- // @TODO: make this an exception at some point
- $msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
- $this->queryLogger->error( $msg );
- return; // join the main transaction set
+ $msg = "$fname: Implicit transaction already active (from {$this->trxFname}).";
+ throw new DBUnexpectedError( $this, $msg );
}
} elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
- // @TODO: make this an exception at some point
$msg = "$fname: Implicit transaction expected (DBO_TRX set).";
- $this->queryLogger->error( $msg );
- return; // let any writes be in the main transaction
+ throw new DBUnexpectedError( $this, $msg );
}
// Avoid fatals if close() was called
$this->assertOpen();
$this->doBegin( $fname );
- $this->mTrxTimestamp = microtime( true );
- $this->mTrxFname = $fname;
- $this->mTrxDoneWrites = false;
- $this->mTrxAutomaticAtomic = false;
- $this->mTrxAtomicLevels = [];
- $this->mTrxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
- $this->mTrxWriteDuration = 0.0;
- $this->mTrxWriteQueryCount = 0;
- $this->mTrxWriteAffectedRows = 0;
- $this->mTrxWriteAdjDuration = 0.0;
- $this->mTrxWriteAdjQueryCount = 0;
- $this->mTrxWriteCallers = [];
+ $this->trxStatus = self::STATUS_TRX_OK;
+ $this->trxStatusIgnoredCause = null;
+ $this->trxAtomicCounter = 0;
+ $this->trxTimestamp = microtime( true );
+ $this->trxFname = $fname;
+ $this->trxDoneWrites = false;
+ $this->trxAutomaticAtomic = false;
+ $this->trxAtomicLevels = [];
+ $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
+ $this->trxWriteDuration = 0.0;
+ $this->trxWriteQueryCount = 0;
+ $this->trxWriteAffectedRows = 0;
+ $this->trxWriteAdjDuration = 0.0;
+ $this->trxWriteAdjQueryCount = 0;
+ $this->trxWriteCallers = [];
// First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
- // Get an estimate of the replica DB lag before then, treating estimate staleness
- // as lag itself just to be safe
- $status = $this->getApproximateLagStatus();
- $this->mTrxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] );
+ // Get an estimate of the replication lag before any such queries.
+ $this->trxReplicaLag = null; // clear cached value first
+ $this->trxReplicaLag = $this->getApproximateLagStatus()['lag'];
// T147697: make explicitTrxActive() return true until begin() finishes. This way, no
// caller will think its OK to muck around with the transaction just because startAtomic()
- // has not yet completed (e.g. setting mTrxAtomicLevels).
- $this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
+ // has not yet completed (e.g. setting trxAtomicLevels).
+ $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
}
/**
@@ -2926,13 +3733,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
*/
protected function doBegin( $fname ) {
$this->query( 'BEGIN', $fname );
- $this->mTrxLevel = 1;
+ $this->trxLevel = 1;
}
- final public function commit( $fname = __METHOD__, $flush = '' ) {
- if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
- // There are still atomic sections open. This cannot be ignored
- $levels = implode( ', ', $this->mTrxAtomicLevels );
+ final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
+ static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
+ if ( !in_array( $flush, $modes, true ) ) {
+ throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." );
+ }
+
+ if ( $this->trxLevel && $this->trxAtomicLevels ) {
+ // There are still atomic sections open; this cannot be ignored
+ $levels = $this->flatAtomicSectionList();
throw new DBUnexpectedError(
$this,
"$fname: Got COMMIT while atomic sections $levels are still open."
@@ -2940,24 +3752,24 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
- if ( !$this->mTrxLevel ) {
+ if ( !$this->trxLevel ) {
return; // nothing to do
- } elseif ( !$this->mTrxAutomatic ) {
+ } elseif ( !$this->trxAutomatic ) {
throw new DBUnexpectedError(
$this,
"$fname: Flushing an explicit transaction, getting out of sync."
);
}
} else {
- if ( !$this->mTrxLevel ) {
+ if ( !$this->trxLevel ) {
$this->queryLogger->error(
"$fname: No transaction to commit, something got out of sync." );
return; // nothing to do
- } elseif ( $this->mTrxAutomatic ) {
- // @TODO: make this an exception at some point
- $msg = "$fname: Explicit commit of implicit transaction.";
- $this->queryLogger->error( $msg );
- return; // wait for the main transaction set commit round
+ } elseif ( $this->trxAutomatic ) {
+ throw new DBUnexpectedError(
+ $this,
+ "$fname: Expected mass commit of all peer transactions (DBO_TRX set)."
+ );
}
}
@@ -2967,14 +3779,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$this->runOnTransactionPreCommitCallbacks();
$writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
$this->doCommit( $fname );
- if ( $this->mTrxDoneWrites ) {
- $this->mLastWriteTime = microtime( true );
+ $this->trxStatus = self::STATUS_TRX_NONE;
+ if ( $this->trxDoneWrites ) {
+ $this->lastWriteTime = microtime( true );
$this->trxProfiler->transactionWritingOut(
- $this->mServer,
- $this->mDBname,
- $this->mTrxShortId,
+ $this->server,
+ $this->dbName,
+ $this->trxShortId,
$writeTime,
- $this->mTrxWriteAffectedRows
+ $this->trxWriteAffectedRows
);
}
@@ -2989,47 +3802,59 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @param string $fname
*/
protected function doCommit( $fname ) {
- if ( $this->mTrxLevel ) {
+ if ( $this->trxLevel ) {
$this->query( 'COMMIT', $fname );
- $this->mTrxLevel = 0;
+ $this->trxLevel = 0;
}
}
final public function rollback( $fname = __METHOD__, $flush = '' ) {
- if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
- if ( !$this->mTrxLevel ) {
- return; // nothing to do
- }
- } else {
- if ( !$this->mTrxLevel ) {
- $this->queryLogger->error(
- "$fname: No transaction to rollback, something got out of sync." );
- return; // nothing to do
- } elseif ( $this->getFlag( self::DBO_TRX ) ) {
+ $trxActive = $this->trxLevel;
+
+ if ( $flush !== self::FLUSHING_INTERNAL && $flush !== self::FLUSHING_ALL_PEERS ) {
+ if ( $this->getFlag( self::DBO_TRX ) ) {
throw new DBUnexpectedError(
$this,
- "$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
+ "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
);
}
}
- // Avoid fatals if close() was called
- $this->assertOpen();
+ if ( $trxActive ) {
+ // Avoid fatals if close() was called
+ $this->assertOpen();
- $this->doRollback( $fname );
- $this->mTrxAtomicLevels = [];
- if ( $this->mTrxDoneWrites ) {
- $this->trxProfiler->transactionWritingOut(
- $this->mServer,
- $this->mDBname,
- $this->mTrxShortId
- );
+ $this->doRollback( $fname );
+ $this->trxStatus = self::STATUS_TRX_NONE;
+ $this->trxAtomicLevels = [];
+ if ( $this->trxDoneWrites ) {
+ $this->trxProfiler->transactionWritingOut(
+ $this->server,
+ $this->dbName,
+ $this->trxShortId
+ );
+ }
}
- $this->mTrxIdleCallbacks = []; // clear
- $this->mTrxPreCommitCallbacks = []; // clear
- $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
- $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
+ // Clear any commit-dependant callbacks. They might even be present
+ // only due to transaction rounds, with no SQL transaction being active
+ $this->trxIdleCallbacks = [];
+ $this->trxPreCommitCallbacks = [];
+
+ if ( $trxActive ) {
+ try {
+ $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+ } catch ( Exception $e ) {
+ // already logged; finish and let LoadBalancer move on during mass-rollback
+ }
+ try {
+ $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
+ } catch ( Exception $e ) {
+ // already logged; let LoadBalancer move on during mass-rollback
+ }
+
+ $this->affectedRowCount = 0; // for the sake of consistency
+ }
}
/**
@@ -3039,11 +3864,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @param string $fname
*/
protected function doRollback( $fname ) {
- if ( $this->mTrxLevel ) {
+ if ( $this->trxLevel ) {
# Disconnects cause rollback anyway, so ignore those errors
$ignoreErrors = true;
$this->query( 'ROLLBACK', $fname, $ignoreErrors );
- $this->mTrxLevel = 0;
+ $this->trxLevel = 0;
}
}
@@ -3061,7 +3886,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
public function explicitTrxActive() {
- return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
+ return $this->trxLevel && ( $this->trxAtomicLevels || !$this->trxAutomatic );
}
public function duplicateTableStructure(
@@ -3092,6 +3917,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
}
+ public function affectedRows() {
+ return ( $this->affectedRowCount === null )
+ ? $this->fetchAffectedRowCount() // default to driver value
+ : $this->affectedRowCount;
+ }
+
+ /**
+ * @return int Number of retrieved rows according to the driver
+ */
+ abstract protected function fetchAffectedRowCount();
+
/**
* Take the result from a query, and wrap it in a ResultWrapper if
* necessary. Boolean values are passed through as is, to indicate success
@@ -3121,8 +3957,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
public function ping( &$rtt = null ) {
// Avoid hitting the server if it was hit recently
if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
- if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
- $rtt = $this->mRTTEstimate;
+ if ( !func_num_args() || $this->rttEstimate > 0 ) {
+ $rtt = $this->rttEstimate;
return true; // don't care about $rtt
}
}
@@ -3133,34 +3969,50 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$this->restoreFlags( self::RESTORE_PRIOR );
if ( $ok ) {
- $rtt = $this->mRTTEstimate;
+ $rtt = $this->rttEstimate;
}
return $ok;
}
/**
- * Close existing database connection and open a new connection
+ * Close any existing (dead) database connection and open a new connection
*
+ * @param string $fname
* @return bool True if new connection is opened successfully, false if error
*/
- protected function reconnect() {
+ protected function replaceLostConnection( $fname ) {
$this->closeConnection();
- $this->mOpened = false;
- $this->mConn = false;
+ $this->opened = false;
+ $this->conn = false;
try {
- $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+ $this->open( $this->server, $this->user, $this->password, $this->dbName );
$this->lastPing = microtime( true );
$ok = true;
+
+ $this->connLogger->warning(
+ $fname . ': lost connection to {dbserver}; reconnected',
+ [
+ 'dbserver' => $this->getServer(),
+ 'trace' => ( new RuntimeException() )->getTraceAsString()
+ ]
+ );
} catch ( DBConnectionError $e ) {
$ok = false;
+
+ $this->connLogger->error(
+ $fname . ': lost connection to {dbserver} permanently',
+ [ 'dbserver' => $this->getServer() ]
+ );
}
+ $this->handleSessionLoss();
+
return $ok;
}
public function getSessionLagStatus() {
- return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
+ return $this->getRecordedTransactionLagStatus() ?: $this->getApproximateLagStatus();
}
/**
@@ -3171,12 +4023,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* is this lag plus transaction duration. If they don't, it is still
* safe to be pessimistic. This returns null if there is no transaction.
*
+ * This returns null if the lag status for this transaction was not yet recorded.
+ *
* @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
* @since 1.27
*/
- protected function getTransactionLagStatus() {
- return $this->mTrxLevel
- ? [ 'lag' => $this->mTrxReplicaLag, 'since' => $this->trxTimestamp() ]
+ final protected function getRecordedTransactionLagStatus() {
+ return ( $this->trxLevel && $this->trxReplicaLag !== null )
+ ? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ]
: null;
}
@@ -3204,14 +4058,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @see WANObjectCache::getWithSetCallback()
*
* @param IDatabase $db1
- * @param IDatabase $dbs,...
+ * @param IDatabase $db2 [optional]
* @return array Map of values:
* - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
* - since: oldest UNIX timestamp of any of the DB lag estimates
* - pending: whether any of the DBs have uncommitted changes
+ * @throws DBError
* @since 1.27
*/
- public static function getCacheSetOptions( IDatabase $db1 ) {
+ public static function getCacheSetOptions( IDatabase $db1, IDatabase $db2 = null ) {
$res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
foreach ( func_get_args() as $db ) {
/** @var IDatabase $db */
@@ -3257,9 +4112,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$fname = false,
callable $inputCallback = null
) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$fp = fopen( $filename, 'r' );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( false === $fp ) {
throw new RuntimeException( "Could not open \"{$filename}\".\n" );
@@ -3283,7 +4138,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
public function setSchemaVars( $vars ) {
- $this->mSchemaVars = $vars;
+ $this->schemaVars = $vars;
}
public function sourceStream(
@@ -3293,6 +4148,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$fname = __METHOD__,
callable $inputCallback = null
) {
+ $delimiterReset = new ScopedCallback(
+ function ( $delimiter ) {
+ $this->delimiter = $delimiter;
+ },
+ [ $this->delimiter ]
+ );
$cmd = '';
while ( !feof( $fp ) ) {
@@ -3321,7 +4182,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
if ( $done || feof( $fp ) ) {
$cmd = $this->replaceVars( $cmd );
- if ( !$inputCallback || call_user_func( $inputCallback, $cmd ) ) {
+ if ( $inputCallback ) {
+ $callbackResult = call_user_func( $inputCallback, $cmd );
+
+ if ( is_string( $callbackResult ) || !$callbackResult ) {
+ $cmd = $callbackResult;
+ }
+ }
+
+ if ( $cmd ) {
$res = $this->query( $cmd, $fname );
if ( $resultCallback ) {
@@ -3338,6 +4207,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
}
+ ScopedCallback::consume( $delimiterReset );
return true;
}
@@ -3420,8 +4290,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @return array
*/
protected function getSchemaVars() {
- if ( $this->mSchemaVars ) {
- return $this->mSchemaVars;
+ if ( $this->schemaVars ) {
+ return $this->schemaVars;
} else {
return $this->getDefaultSchemaVars();
}
@@ -3440,17 +4310,20 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
public function lockIsFree( $lockName, $method ) {
- return true;
+ // RDBMs methods for checking named locks may or may not count this thread itself.
+ // In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
+ // the behavior choosen by the interface for this method.
+ return !isset( $this->namedLocksHeld[$lockName] );
}
public function lock( $lockName, $method, $timeout = 5 ) {
- $this->mNamedLocksHeld[$lockName] = 1;
+ $this->namedLocksHeld[$lockName] = 1;
return true;
}
public function unlock( $lockName, $method ) {
- unset( $this->mNamedLocksHeld[$lockName] );
+ unset( $this->namedLocksHeld[$lockName] );
return true;
}
@@ -3597,34 +4470,30 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$this->tableAliases = $aliases;
}
- /**
- * @return bool Whether a DB user is required to access the DB
- * @since 1.28
- */
- protected function requiresDatabaseUser() {
- return true;
+ public function setIndexAliases( array $aliases ) {
+ $this->indexAliases = $aliases;
}
/**
- * Get the underlying binding handle, mConn
+ * Get the underlying binding connection handle
*
- * Makes sure that mConn is set (disconnects and ping() failure can unset it).
+ * Makes sure the connection resource is set (disconnects and ping() failure can unset it).
* This catches broken callers than catch and ignore disconnection exceptions.
* Unlike checking isOpen(), this is safe to call inside of open().
*
- * @return resource|object
+ * @return mixed
* @throws DBUnexpectedError
* @since 1.26
*/
protected function getBindingHandle() {
- if ( !$this->mConn ) {
+ if ( !$this->conn ) {
throw new DBUnexpectedError(
$this,
'DB connection was already closed or the connection dropped.'
);
}
- return $this->mConn;
+ return $this->conn;
}
/**
@@ -3632,7 +4501,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @return string
*/
public function __toString() {
- return (string)$this->mConn;
+ return (string)$this->conn;
}
/**
@@ -3647,11 +4516,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
if ( $this->isOpen() ) {
// Open a new connection resource without messing with the old one
- $this->mOpened = false;
- $this->mConn = false;
- $this->mTrxEndCallbacks = []; // don't copy
+ $this->opened = false;
+ $this->conn = false;
+ $this->trxEndCallbacks = []; // don't copy
$this->handleSessionLoss(); // no trx or locks anymore
- $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+ $this->open( $this->server, $this->user, $this->password, $this->dbName );
$this->lastPing = microtime( true );
}
}
@@ -3670,8 +4539,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* Run a few simple sanity checks and close dangling connections
*/
public function __destruct() {
- if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
- trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
+ if ( $this->trxLevel && $this->trxDoneWrites ) {
+ trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})." );
}
$danglingWriters = $this->pendingWriteAndCallbackCallers();
@@ -3680,14 +4549,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
}
- if ( $this->mConn ) {
+ if ( $this->conn ) {
// Avoid connection leaks for sanity. Normally, resources close at script completion.
// The connection might already be closed in zend/hhvm by now, so suppress warnings.
- \MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$this->closeConnection();
- \MediaWiki\restoreWarnings();
- $this->mConn = false;
- $this->mOpened = false;
+ Wikimedia\restoreWarnings();
+ $this->conn = false;
+ $this->opened = false;
}
}
}
diff --git a/www/wiki/includes/libs/rdbms/database/DatabaseMssql.php b/www/wiki/includes/libs/rdbms/database/DatabaseMssql.php
index 8a69eec4..4c187f23 100644
--- a/www/wiki/includes/libs/rdbms/database/DatabaseMssql.php
+++ b/www/wiki/includes/libs/rdbms/database/DatabaseMssql.php
@@ -27,7 +27,7 @@
namespace Wikimedia\Rdbms;
-use MediaWiki;
+use Wikimedia;
use Exception;
use stdClass;
@@ -35,19 +35,28 @@ use stdClass;
* @ingroup Database
*/
class DatabaseMssql extends Database {
- protected $mPort;
- protected $mUseWindowsAuth = false;
-
- protected $mInsertId = null;
- protected $mLastResult = null;
- protected $mAffectedRows = null;
- protected $mSubqueryId = 0;
- protected $mScrollableCursor = true;
- protected $mPrepareStatements = true;
- protected $mBinaryColumnCache = null;
- protected $mBitColumnCache = null;
- protected $mIgnoreDupKeyErrors = false;
- protected $mIgnoreErrors = [];
+ /** @var int */
+ protected $serverPort;
+ /** @var bool */
+ protected $useWindowsAuth = false;
+ /** @var int|null */
+ protected $lastInsertId = null;
+ /** @var int|null */
+ protected $lastAffectedRowCount = null;
+ /** @var int */
+ protected $subqueryId = 0;
+ /** @var bool */
+ protected $scrollableCursor = true;
+ /** @var bool */
+ protected $prepareStatements = true;
+ /** @var stdClass[][]|null */
+ protected $binaryColumnCache = null;
+ /** @var stdClass[][]|null */
+ protected $bitColumnCache = null;
+ /** @var bool */
+ protected $ignoreDupKeyErrors = false;
+ /** @var string[] */
+ protected $ignoreErrors = [];
public function implicitGroupby() {
return false;
@@ -62,8 +71,8 @@ class DatabaseMssql extends Database {
}
public function __construct( array $params ) {
- $this->mPort = $params['port'];
- $this->mUseWindowsAuth = $params['UseWindowsAuth'];
+ $this->serverPort = $params['port'];
+ $this->useWindowsAuth = $params['UseWindowsAuth'];
parent::__construct( $params );
}
@@ -93,10 +102,10 @@ class DatabaseMssql extends Database {
}
$this->close();
- $this->mServer = $server;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
+ $this->server = $server;
+ $this->user = $user;
+ $this->password = $password;
+ $this->dbName = $dbName;
$connectionInfo = [];
@@ -106,22 +115,22 @@ class DatabaseMssql extends Database {
// Decide which auth scenerio to use
// if we are using Windows auth, then don't add credentials to $connectionInfo
- if ( !$this->mUseWindowsAuth ) {
+ if ( !$this->useWindowsAuth ) {
$connectionInfo['UID'] = $user;
$connectionInfo['PWD'] = $password;
}
- MediaWiki\suppressWarnings();
- $this->mConn = sqlsrv_connect( $server, $connectionInfo );
- MediaWiki\restoreWarnings();
+ Wikimedia\suppressWarnings();
+ $this->conn = sqlsrv_connect( $server, $connectionInfo );
+ Wikimedia\restoreWarnings();
- if ( $this->mConn === false ) {
+ if ( $this->conn === false ) {
throw new DBConnectionError( $this, $this->lastError() );
}
- $this->mOpened = true;
+ $this->opened = true;
- return $this->mConn;
+ return $this->conn;
}
/**
@@ -130,7 +139,7 @@ class DatabaseMssql extends Database {
* @return bool
*/
protected function closeConnection() {
- return sqlsrv_close( $this->mConn );
+ return sqlsrv_close( $this->conn );
}
/**
@@ -178,25 +187,25 @@ class DatabaseMssql extends Database {
// needed if we want to be able to seek around the result set), however CLIENT_BUFFERED
// has a bug in the sqlsrv driver where wchar_t types (such as nvarchar) that are empty
// strings make php throw a fatal error "Severe error translating Unicode"
- if ( $this->mScrollableCursor ) {
+ if ( $this->scrollableCursor ) {
$scrollArr = [ 'Scrollable' => SQLSRV_CURSOR_STATIC ];
} else {
$scrollArr = [];
}
- if ( $this->mPrepareStatements ) {
+ if ( $this->prepareStatements ) {
// we do prepare + execute so we can get its field metadata for later usage if desired
- $stmt = sqlsrv_prepare( $this->mConn, $sql, [], $scrollArr );
+ $stmt = sqlsrv_prepare( $this->conn, $sql, [], $scrollArr );
$success = sqlsrv_execute( $stmt );
} else {
- $stmt = sqlsrv_query( $this->mConn, $sql, [], $scrollArr );
+ $stmt = sqlsrv_query( $this->conn, $sql, [], $scrollArr );
$success = (bool)$stmt;
}
// Make a copy to ensure what we add below does not get reflected in future queries
- $ignoreErrors = $this->mIgnoreErrors;
+ $ignoreErrors = $this->ignoreErrors;
- if ( $this->mIgnoreDupKeyErrors ) {
+ if ( $this->ignoreDupKeyErrors ) {
// ignore duplicate key errors
// this emulates INSERT IGNORE in MySQL
$ignoreErrors[] = '2601'; // duplicate key error caused by unique index
@@ -220,7 +229,7 @@ class DatabaseMssql extends Database {
}
}
// remember number of rows affected
- $this->mAffectedRows = sqlsrv_rows_affected( $stmt );
+ $this->lastAffectedRowCount = sqlsrv_rows_affected( $stmt );
return $stmt;
}
@@ -300,7 +309,7 @@ class DatabaseMssql extends Database {
* @return int|null
*/
public function insertId() {
- return $this->mInsertId;
+ return $this->lastInsertId;
}
/**
@@ -350,11 +359,33 @@ class DatabaseMssql extends Database {
}
}
+ protected function wasKnownStatementRollbackError() {
+ $errors = sqlsrv_errors( SQLSRV_ERR_ALL );
+ if ( !$errors ) {
+ return false;
+ }
+ // The transaction vs statement rollback behavior depends on XACT_ABORT, so make sure
+ // that the "statement has been terminated" error (3621) is specifically present.
+ // https://docs.microsoft.com/en-us/sql/t-sql/statements/set-xact-abort-transact-sql
+ $statementOnly = false;
+ $codeWhitelist = [ '2601', '2627', '547' ];
+ foreach ( $errors as $error ) {
+ if ( $error['code'] == '3621' ) {
+ $statementOnly = true;
+ } elseif ( !in_array( $error['code'], $codeWhitelist ) ) {
+ $statementOnly = false;
+ break;
+ }
+ }
+
+ return $statementOnly;
+ }
+
/**
* @return int
*/
- public function affectedRows() {
- return $this->mAffectedRows;
+ protected function fetchAffectedRowCount() {
+ return $this->lastAffectedRowCount;
}
/**
@@ -381,8 +412,8 @@ class DatabaseMssql extends Database {
$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
if ( isset( $options['EXPLAIN'] ) ) {
try {
- $this->mScrollableCursor = false;
- $this->mPrepareStatements = false;
+ $this->scrollableCursor = false;
+ $this->prepareStatements = false;
$this->query( "SET SHOWPLAN_ALL ON" );
$ret = $this->query( $sql, $fname );
$this->query( "SET SHOWPLAN_ALL OFF" );
@@ -402,13 +433,13 @@ class DatabaseMssql extends Database {
} else {
// someone actually wanted the query plan instead of an est row count
// let them know of the error
- $this->mScrollableCursor = true;
- $this->mPrepareStatements = true;
+ $this->scrollableCursor = true;
+ $this->prepareStatements = true;
throw $dqe;
}
}
- $this->mScrollableCursor = true;
- $this->mPrepareStatements = true;
+ $this->scrollableCursor = true;
+ $this->prepareStatements = true;
return $ret;
}
return $this->query( $sql, $fname );
@@ -440,8 +471,14 @@ class DatabaseMssql extends Database {
if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) {
$bitColumns = [];
if ( is_array( $table ) ) {
- foreach ( $table as $t ) {
- $bitColumns += $this->getBitColumns( $this->tableName( $t ) );
+ $tables = $table;
+ while ( $tables ) {
+ $t = array_pop( $tables );
+ if ( is_array( $t ) ) {
+ $tables = array_merge( $tables, $t );
+ } else {
+ $bitColumns += $this->getBitColumns( $this->tableName( $t ) );
+ }
}
} else {
$bitColumns = $this->getBitColumns( $this->tableName( $table ) );
@@ -462,25 +499,25 @@ class DatabaseMssql extends Database {
public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
$fname = __METHOD__
) {
- $this->mScrollableCursor = false;
+ $this->scrollableCursor = false;
try {
parent::deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname );
} catch ( Exception $e ) {
- $this->mScrollableCursor = true;
+ $this->scrollableCursor = true;
throw $e;
}
- $this->mScrollableCursor = true;
+ $this->scrollableCursor = true;
}
public function delete( $table, $conds, $fname = __METHOD__ ) {
- $this->mScrollableCursor = false;
+ $this->scrollableCursor = false;
try {
parent::delete( $table, $conds, $fname );
} catch ( Exception $e ) {
- $this->mScrollableCursor = true;
+ $this->scrollableCursor = true;
throw $e;
}
- $this->mScrollableCursor = true;
+ $this->scrollableCursor = true;
}
/**
@@ -490,19 +527,26 @@ class DatabaseMssql extends Database {
* Returns -1 if count cannot be found
* Takes same arguments as Database::select()
* @param string $table
- * @param string $vars
+ * @param string $var
* @param string $conds
* @param string $fname
* @param array $options
+ * @param array $join_conds
* @return int
*/
- public function estimateRowCount( $table, $vars = '*', $conds = '',
- $fname = __METHOD__, $options = []
+ public function estimateRowCount( $table, $var = '*', $conds = '',
+ $fname = __METHOD__, $options = [], $join_conds = []
) {
+ $conds = $this->normalizeConditions( $conds, $fname );
+ $column = $this->extractSingleFieldFromList( $var );
+ if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
+ $conds[] = "$column IS NOT NULL";
+ }
+
// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
$options['EXPLAIN'] = true;
$options['FOR COUNT'] = true;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
+ $res = $this->select( $table, $var, $conds, $fname, $options, $join_conds );
$rows = -1;
if ( $res ) {
@@ -554,7 +598,7 @@ class DatabaseMssql extends Database {
}
}
- return empty( $result ) ? false : $result;
+ return $result ?: false;
}
/**
@@ -611,7 +655,7 @@ class DatabaseMssql extends Database {
// remove IGNORE from options list and set ignore flag to true
if ( in_array( 'IGNORE', $options ) ) {
$options = array_diff( $options, [ 'IGNORE' ] );
- $this->mIgnoreDupKeyErrors = true;
+ $this->ignoreDupKeyErrors = true;
}
$ret = null;
@@ -675,32 +719,32 @@ class DatabaseMssql extends Database {
$sql .= ')' . $sqlPost;
// Run the query
- $this->mScrollableCursor = false;
+ $this->scrollableCursor = false;
try {
$ret = $this->query( $sql );
} catch ( Exception $e ) {
- $this->mScrollableCursor = true;
- $this->mIgnoreDupKeyErrors = false;
+ $this->scrollableCursor = true;
+ $this->ignoreDupKeyErrors = false;
throw $e;
}
- $this->mScrollableCursor = true;
+ $this->scrollableCursor = true;
if ( $ret instanceof ResultWrapper && !is_null( $identity ) ) {
// Then we want to get the identity column value we were assigned and save it off
$row = $ret->fetchObject();
if ( is_object( $row ) ) {
- $this->mInsertId = $row->$identity;
+ $this->lastInsertId = $row->$identity;
// It seems that mAffectedRows is -1 sometimes when OUTPUT INSERTED.identity is
// used if we got an identity back, we know for sure a row was affected, so
// adjust that here
- if ( $this->mAffectedRows == -1 ) {
- $this->mAffectedRows = 1;
+ if ( $this->lastAffectedRowCount == -1 ) {
+ $this->lastAffectedRowCount = 1;
}
}
}
}
- $this->mIgnoreDupKeyErrors = false;
+ $this->ignoreDupKeyErrors = false;
return $ret;
}
@@ -718,13 +762,13 @@ class DatabaseMssql extends Database {
* @param array $insertOptions
* @param array $selectOptions
* @param array $selectJoinConds
- * @return null|ResultWrapper
+ * @return bool
* @throws Exception
*/
public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
$insertOptions = [], $selectOptions = [], $selectJoinConds = []
) {
- $this->mScrollableCursor = false;
+ $this->scrollableCursor = false;
try {
$ret = parent::nativeInsertSelect(
$destTable,
@@ -737,10 +781,10 @@ class DatabaseMssql extends Database {
$selectJoinConds
);
} catch ( Exception $e ) {
- $this->mScrollableCursor = true;
+ $this->scrollableCursor = true;
throw $e;
}
- $this->mScrollableCursor = true;
+ $this->scrollableCursor = true;
return $ret;
}
@@ -781,14 +825,14 @@ class DatabaseMssql extends Database {
$sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns );
}
- $this->mScrollableCursor = false;
+ $this->scrollableCursor = false;
try {
$this->query( $sql );
} catch ( Exception $e ) {
- $this->mScrollableCursor = true;
+ $this->scrollableCursor = true;
throw $e;
}
- $this->mScrollableCursor = true;
+ $this->scrollableCursor = true;
return true;
}
@@ -881,9 +925,9 @@ class DatabaseMssql extends Database {
$postOrder = '';
$first = $offset + 1;
$last = $offset + $limit;
- $sub1 = 'sub_' . $this->mSubqueryId;
- $sub2 = 'sub_' . ( $this->mSubqueryId + 1 );
- $this->mSubqueryId += 2;
+ $sub1 = 'sub_' . $this->subqueryId;
+ $sub2 = 'sub_' . ( $this->subqueryId + 1 );
+ $this->subqueryId += 2;
if ( !$s1 ) {
// wat
throw new DBUnexpectedError( $this, "Attempting to LIMIT a non-SELECT query\n" );
@@ -947,7 +991,7 @@ class DatabaseMssql extends Database {
* @return string Version information from the database
*/
public function getServerVersion() {
- $server_info = sqlsrv_server_info( $this->mConn );
+ $server_info = sqlsrv_server_info( $this->conn );
$version = 'Error';
if ( isset( $server_info['SQLServerVersion'] ) ) {
$version = $server_info['SQLServerVersion'];
@@ -971,7 +1015,7 @@ class DatabaseMssql extends Database {
}
if ( $schema === false ) {
- $schema = $this->mSchema;
+ $schema = $this->schema;
}
$res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES
@@ -1031,13 +1075,26 @@ class DatabaseMssql extends Database {
return false;
}
+ protected function doSavepoint( $identifier, $fname ) {
+ $this->query( 'SAVE TRANSACTION ' . $this->addIdentifierQuotes( $identifier ), $fname );
+ }
+
+ protected function doReleaseSavepoint( $identifier, $fname ) {
+ // Not supported. Also not really needed, a new doSavepoint() for the
+ // same identifier will overwrite the old.
+ }
+
+ protected function doRollbackToSavepoint( $identifier, $fname ) {
+ $this->query( 'ROLLBACK TRANSACTION ' . $this->addIdentifierQuotes( $identifier ), $fname );
+ }
+
/**
* Begin a transaction, committing any previously open transaction
* @param string $fname
*/
protected function doBegin( $fname = __METHOD__ ) {
- sqlsrv_begin_transaction( $this->mConn );
- $this->mTrxLevel = 1;
+ sqlsrv_begin_transaction( $this->conn );
+ $this->trxLevel = 1;
}
/**
@@ -1045,8 +1102,8 @@ class DatabaseMssql extends Database {
* @param string $fname
*/
protected function doCommit( $fname = __METHOD__ ) {
- sqlsrv_commit( $this->mConn );
- $this->mTrxLevel = 0;
+ sqlsrv_commit( $this->conn );
+ $this->trxLevel = 0;
}
/**
@@ -1055,8 +1112,8 @@ class DatabaseMssql extends Database {
* @param string $fname
*/
protected function doRollback( $fname = __METHOD__ ) {
- sqlsrv_rollback( $this->mConn );
- $this->mTrxLevel = 0;
+ sqlsrv_rollback( $this->conn );
+ $this->trxLevel = 0;
}
/**
@@ -1125,7 +1182,7 @@ class DatabaseMssql extends Database {
*/
public function selectDB( $db ) {
try {
- $this->mDBname = $db;
+ $this->dbName = $db;
$this->query( "USE $db" );
return true;
} catch ( Exception $e ) {
@@ -1198,8 +1255,8 @@ class DatabaseMssql extends Database {
public function buildGroupConcatField( $delim, $table, $field, $conds = '',
$join_conds = []
) {
- $gcsq = 'gcsq_' . $this->mSubqueryId;
- $this->mSubqueryId++;
+ $gcsq = 'gcsq_' . $this->subqueryId;
+ $this->subqueryId++;
$delimLen = strlen( $delim );
$fld = "{$field} + {$this->addQuotes( $delim )}";
@@ -1210,6 +1267,19 @@ class DatabaseMssql extends Database {
return $sql;
}
+ public function buildSubstring( $input, $startPosition, $length = null ) {
+ $this->assertBuildSubstringParams( $startPosition, $length );
+ if ( $length === null ) {
+ /**
+ * MSSQL doesn't allow an empty length parameter, so when we don't want to limit the
+ * length returned use the default maximum size of text.
+ * @see https://docs.microsoft.com/en-us/sql/t-sql/statements/set-textsize-transact-sql
+ */
+ $length = 2147483647;
+ }
+ return 'SUBSTRING(' . implode( ',', [ $input, $startPosition, $length ] ) . ')';
+ }
+
/**
* Returns an associative array for fields that are of type varbinary, binary, or image
* $table can be either a raw table name or passed through tableName() first
@@ -1220,12 +1290,12 @@ class DatabaseMssql extends Database {
$tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
$tableRaw = array_pop( $tableRawArr );
- if ( $this->mBinaryColumnCache === null ) {
+ if ( $this->binaryColumnCache === null ) {
$this->populateColumnCaches();
}
- return isset( $this->mBinaryColumnCache[$tableRaw] )
- ? $this->mBinaryColumnCache[$tableRaw]
+ return isset( $this->binaryColumnCache[$tableRaw] )
+ ? $this->binaryColumnCache[$tableRaw]
: [];
}
@@ -1237,30 +1307,30 @@ class DatabaseMssql extends Database {
$tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
$tableRaw = array_pop( $tableRawArr );
- if ( $this->mBitColumnCache === null ) {
+ if ( $this->bitColumnCache === null ) {
$this->populateColumnCaches();
}
- return isset( $this->mBitColumnCache[$tableRaw] )
- ? $this->mBitColumnCache[$tableRaw]
+ return isset( $this->bitColumnCache[$tableRaw] )
+ ? $this->bitColumnCache[$tableRaw]
: [];
}
private function populateColumnCaches() {
$res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*',
[
- 'TABLE_CATALOG' => $this->mDBname,
- 'TABLE_SCHEMA' => $this->mSchema,
+ 'TABLE_CATALOG' => $this->dbName,
+ 'TABLE_SCHEMA' => $this->schema,
'DATA_TYPE' => [ 'varbinary', 'binary', 'image', 'bit' ]
] );
- $this->mBinaryColumnCache = [];
- $this->mBitColumnCache = [];
+ $this->binaryColumnCache = [];
+ $this->bitColumnCache = [];
foreach ( $res as $row ) {
if ( $row->DATA_TYPE == 'bit' ) {
- $this->mBitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
+ $this->bitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
} else {
- $this->mBinaryColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
+ $this->binaryColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
}
}
}
@@ -1324,9 +1394,9 @@ class DatabaseMssql extends Database {
* @return bool|null
*/
public function prepareStatements( $value = null ) {
- $old = $this->mPrepareStatements;
+ $old = $this->prepareStatements;
if ( $value !== null ) {
- $this->mPrepareStatements = $value;
+ $this->prepareStatements = $value;
}
return $old;
@@ -1339,9 +1409,9 @@ class DatabaseMssql extends Database {
* @return bool|null
*/
public function scrollableCursor( $value = null ) {
- $old = $this->mScrollableCursor;
+ $old = $this->scrollableCursor;
if ( $value !== null ) {
- $this->mScrollableCursor = $value;
+ $this->scrollableCursor = $value;
}
return $old;
diff --git a/www/wiki/includes/libs/rdbms/database/DatabaseMysql.php b/www/wiki/includes/libs/rdbms/database/DatabaseMysql.php
deleted file mode 100644
index 58b09266..00000000
--- a/www/wiki/includes/libs/rdbms/database/DatabaseMysql.php
+++ /dev/null
@@ -1,210 +0,0 @@
-<?php
-/**
- * This is the MySQL database abstraction layer.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-namespace Wikimedia\Rdbms;
-
-/**
- * Database abstraction object for PHP extension mysql.
- *
- * @deprecated 1.30 PHP extension 'mysql' was deprecated in PHP 5.5 and removed in PHP 7.0.
- * @see PHP extension 'mysqli' and DatabaseMysqli
- *
- * @ingroup Database
- * @see Database
- */
-class DatabaseMysql extends DatabaseMysqlBase {
- /**
- * @param string $sql
- * @return resource False on error
- */
- protected function doQuery( $sql ) {
- $conn = $this->getBindingHandle();
-
- if ( $this->bufferResults() ) {
- $ret = mysql_query( $sql, $conn );
- } else {
- $ret = mysql_unbuffered_query( $sql, $conn );
- }
-
- return $ret;
- }
-
- /**
- * @param string $realServer
- * @return bool|resource MySQL Database connection or false on failure to connect
- * @throws DBConnectionError
- */
- protected function mysqlConnect( $realServer ) {
- # Avoid a suppressed fatal error, which is very hard to track down
- if ( !extension_loaded( 'mysql' ) ) {
- throw new DBConnectionError(
- $this,
- "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n"
- );
- }
-
- $connFlags = 0;
- if ( $this->mFlags & self::DBO_SSL ) {
- $connFlags |= MYSQL_CLIENT_SSL;
- }
- if ( $this->mFlags & self::DBO_COMPRESS ) {
- $connFlags |= MYSQL_CLIENT_COMPRESS;
- }
-
- if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) {
- $numAttempts = 2;
- } else {
- $numAttempts = 1;
- }
-
- $conn = false;
-
- # The kernel's default SYN retransmission period is far too slow for us,
- # so we use a short timeout plus a manual retry. Retrying means that a small
- # but finite rate of SYN packet loss won't cause user-visible errors.
- for ( $i = 0; $i < $numAttempts && !$conn; $i++ ) {
- if ( $i > 1 ) {
- usleep( 1000 );
- }
- if ( $this->mFlags & self::DBO_PERSISTENT ) {
- $conn = mysql_pconnect( $realServer, $this->mUser, $this->mPassword, $connFlags );
- } else {
- # Create a new connection...
- $conn = mysql_connect( $realServer, $this->mUser, $this->mPassword, true, $connFlags );
- }
- }
-
- return $conn;
- }
-
- /**
- * @param string $charset
- * @return bool
- */
- protected function mysqlSetCharset( $charset ) {
- $conn = $this->getBindingHandle();
-
- if ( function_exists( 'mysql_set_charset' ) ) {
- return mysql_set_charset( $charset, $conn );
- } else {
- return $this->query( 'SET NAMES ' . $charset, __METHOD__ );
- }
- }
-
- /**
- * @return bool
- */
- protected function closeConnection() {
- $conn = $this->getBindingHandle();
-
- return mysql_close( $conn );
- }
-
- /**
- * @return int
- */
- function insertId() {
- $conn = $this->getBindingHandle();
-
- return mysql_insert_id( $conn );
- }
-
- /**
- * @return int
- */
- function lastErrno() {
- if ( $this->mConn ) {
- return mysql_errno( $this->mConn );
- } else {
- return mysql_errno();
- }
- }
-
- /**
- * @return int
- */
- function affectedRows() {
- $conn = $this->getBindingHandle();
-
- return mysql_affected_rows( $conn );
- }
-
- /**
- * @param string $db
- * @return bool
- */
- function selectDB( $db ) {
- $conn = $this->getBindingHandle();
-
- $this->mDBname = $db;
-
- return mysql_select_db( $db, $conn );
- }
-
- protected function mysqlFreeResult( $res ) {
- return mysql_free_result( $res );
- }
-
- protected function mysqlFetchObject( $res ) {
- return mysql_fetch_object( $res );
- }
-
- protected function mysqlFetchArray( $res ) {
- return mysql_fetch_array( $res );
- }
-
- protected function mysqlNumRows( $res ) {
- return mysql_num_rows( $res );
- }
-
- protected function mysqlNumFields( $res ) {
- return mysql_num_fields( $res );
- }
-
- protected function mysqlFetchField( $res, $n ) {
- return mysql_fetch_field( $res, $n );
- }
-
- protected function mysqlFieldName( $res, $n ) {
- return mysql_field_name( $res, $n );
- }
-
- protected function mysqlFieldType( $res, $n ) {
- return mysql_field_type( $res, $n );
- }
-
- protected function mysqlDataSeek( $res, $row ) {
- return mysql_data_seek( $res, $row );
- }
-
- protected function mysqlError( $conn = null ) {
- return ( $conn !== null ) ? mysql_error( $conn ) : mysql_error(); // avoid warning
- }
-
- protected function mysqlRealEscapeString( $s ) {
- $conn = $this->getBindingHandle();
-
- return mysql_real_escape_string( (string)$s, $conn );
- }
-}
-
-class_alias( DatabaseMysql::class, 'DatabaseMysql' );
diff --git a/www/wiki/includes/libs/rdbms/database/DatabaseMysqlBase.php b/www/wiki/includes/libs/rdbms/database/DatabaseMysqlBase.php
index 3c4cda55..3e6190ce 100644
--- a/www/wiki/includes/libs/rdbms/database/DatabaseMysqlBase.php
+++ b/www/wiki/includes/libs/rdbms/database/DatabaseMysqlBase.php
@@ -24,7 +24,7 @@ namespace Wikimedia\Rdbms;
use DateTime;
use DateTimeZone;
-use MediaWiki;
+use Wikimedia;
use InvalidArgumentException;
use Exception;
use stdClass;
@@ -60,9 +60,21 @@ abstract class DatabaseMysqlBase extends Database {
protected $sqlMode;
/** @var bool Use experimental UTF-8 transmission encoding */
protected $utf8Mode;
+ /** @var bool|null */
+ protected $defaultBigSelects = null;
/** @var string|null */
private $serverVersion = null;
+ /** @var bool|null */
+ private $insertSelectIsSafe = null;
+ /** @var stdClass|null */
+ private $replicationInfoRow = null;
+
+ // Cache getServerId() for 24 hours
+ const SERVER_ID_CACHE_TTL = 86400;
+
+ /** @var float Warn if lag estimates are made for transactions older than this many seconds */
+ const LAG_STALE_WARN_THRESHOLD = 0.100;
/**
* Additional $params include:
@@ -75,6 +87,7 @@ abstract class DatabaseMysqlBase extends Database {
* ID of this server's master will be used. Set the "conds" field to
* override the query conditions, e.g. ['shard' => 's1'].
* - useGTIDs : use GTID methods like MASTER_GTID_WAIT() when possible.
+ * - insertSelectIsSafe : force that native INSERT SELECT is or is not safe [default: null]
* - sslKeyPath : path to key file [default: null]
* - sslCertPath : path to certificate file [default: null]
* - sslCAFile: path to a single certificate authority PEM file [default: null]
@@ -98,6 +111,8 @@ abstract class DatabaseMysqlBase extends Database {
}
$this->sqlMode = isset( $params['sqlMode'] ) ? $params['sqlMode'] : '';
$this->utf8Mode = !empty( $params['utf8Mode'] );
+ $this->insertSelectIsSafe = isset( $params['insertSelectIsSafe'] )
+ ? (bool)$params['insertSelectIsSafe'] : null;
parent::__construct( $params );
}
@@ -121,14 +136,14 @@ abstract class DatabaseMysqlBase extends Database {
# Close/unset connection handle
$this->close();
- $this->mServer = $server;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
+ $this->server = $server;
+ $this->user = $user;
+ $this->password = $password;
+ $this->dbName = $dbName;
$this->installErrorHandler();
try {
- $this->mConn = $this->mysqlConnect( $this->mServer );
+ $this->conn = $this->mysqlConnect( $this->server );
} catch ( Exception $ex ) {
$this->restoreErrorHandler();
throw $ex;
@@ -136,7 +151,7 @@ abstract class DatabaseMysqlBase extends Database {
$error = $this->restoreErrorHandler();
# Always log connection errors
- if ( !$this->mConn ) {
+ if ( !$this->conn ) {
if ( !$error ) {
$error = $this->lastError();
}
@@ -154,10 +169,10 @@ abstract class DatabaseMysqlBase extends Database {
$this->reportConnectionError( $error );
}
- if ( $dbName != '' ) {
- MediaWiki\suppressWarnings();
+ if ( strlen( $dbName ) ) {
+ Wikimedia\suppressWarnings();
$success = $this->selectDB( $dbName );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$success ) {
$this->queryLogger->error(
"Error selecting database {db_name} on server {db_server}",
@@ -166,7 +181,7 @@ abstract class DatabaseMysqlBase extends Database {
] )
);
$this->queryLogger->debug(
- "Error selecting database $dbName on server {$this->mServer}" );
+ "Error selecting database $dbName on server {$this->server}" );
$this->reportConnectionError( "Error selecting database $dbName" );
}
@@ -185,7 +200,7 @@ abstract class DatabaseMysqlBase extends Database {
}
// Set any custom settings defined by site config
// (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html)
- foreach ( $this->mSessionVars as $var => $val ) {
+ foreach ( $this->sessionVars as $var => $val ) {
// Escape strings but not numbers to avoid MySQL complaining
if ( !is_int( $val ) && !is_float( $val ) ) {
$val = $this->addQuotes( $val );
@@ -208,7 +223,7 @@ abstract class DatabaseMysqlBase extends Database {
}
}
- $this->mOpened = true;
+ $this->opened = true;
return true;
}
@@ -252,9 +267,9 @@ abstract class DatabaseMysqlBase extends Database {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ok = $this->mysqlFreeResult( $res );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$ok ) {
throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
}
@@ -277,9 +292,9 @@ abstract class DatabaseMysqlBase extends Database {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$row = $this->mysqlFetchObject( $res );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
$errno = $this->lastErrno();
// Unfortunately, mysql_fetch_object does not reset the last errno.
@@ -305,7 +320,7 @@ abstract class DatabaseMysqlBase extends Database {
abstract protected function mysqlFetchObject( $res );
/**
- * @param ResultWrapper|resource $res
+ * @param IResultWrapper|resource $res
* @return array|bool
* @throws DBUnexpectedError
*/
@@ -313,9 +328,9 @@ abstract class DatabaseMysqlBase extends Database {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$row = $this->mysqlFetchArray( $res );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
$errno = $this->lastErrno();
// Unfortunately, mysql_fetch_array does not reset the last errno.
@@ -349,9 +364,9 @@ abstract class DatabaseMysqlBase extends Database {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$n = $this->mysqlNumRows( $res );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
// Unfortunately, mysql_num_rows does not reset the last errno.
// We are not checking for any errors here, since
@@ -460,19 +475,19 @@ abstract class DatabaseMysqlBase extends Database {
* @return string
*/
public function lastError() {
- if ( $this->mConn ) {
+ if ( $this->conn ) {
# Even if it's non-zero, it can still be invalid
- MediaWiki\suppressWarnings();
- $error = $this->mysqlError( $this->mConn );
+ Wikimedia\suppressWarnings();
+ $error = $this->mysqlError( $this->conn );
if ( !$error ) {
$error = $this->mysqlError();
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
} else {
$error = $this->mysqlError();
}
if ( $error ) {
- $error .= ' (' . $this->mServer . ')';
+ $error .= ' (' . $this->server . ')';
}
return $error;
@@ -486,6 +501,10 @@ abstract class DatabaseMysqlBase extends Database {
*/
abstract protected function mysqlError( $conn = null );
+ protected function wasQueryTimeout( $error, $errno ) {
+ return $errno == 2062;
+ }
+
/**
* @param string $table
* @param array $uniqueIndexes
@@ -497,23 +516,72 @@ abstract class DatabaseMysqlBase extends Database {
return $this->nativeReplace( $table, $rows, $fname );
}
+ protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
+ $row = $this->getReplicationSafetyInfo();
+ // For row-based-replication, the resulting changes will be relayed, not the query
+ if ( $row->binlog_format === 'ROW' ) {
+ return true;
+ }
+ // LIMIT requires ORDER BY on a unique key or it is non-deterministic
+ if ( isset( $selectOptions['LIMIT'] ) ) {
+ return false;
+ }
+ // In MySQL, an INSERT SELECT is only replication safe with row-based
+ // replication or if innodb_autoinc_lock_mode is 0. When those
+ // conditions aren't met, use non-native mode.
+ // While we could try to determine if the insert is safe anyway by
+ // checking if the target table has an auto-increment column that
+ // isn't set in $varMap, that seems unlikely to be worth the extra
+ // complexity.
+ return (
+ in_array( 'NO_AUTO_COLUMNS', $insertOptions ) ||
+ (int)$row->innodb_autoinc_lock_mode === 0
+ );
+ }
+
+ /**
+ * @return stdClass Process cached row
+ */
+ protected function getReplicationSafetyInfo() {
+ if ( $this->replicationInfoRow === null ) {
+ $this->replicationInfoRow = $this->selectRow(
+ false,
+ [
+ 'innodb_autoinc_lock_mode' => '@@innodb_autoinc_lock_mode',
+ 'binlog_format' => '@@binlog_format',
+ ],
+ [],
+ __METHOD__
+ );
+ }
+
+ return $this->replicationInfoRow;
+ }
+
/**
* Estimate rows in dataset
* Returns estimated count, based on EXPLAIN output
* Takes same arguments as Database::select()
*
* @param string|array $table
- * @param string|array $vars
+ * @param string|array $var
* @param string|array $conds
* @param string $fname
* @param string|array $options
+ * @param array $join_conds
* @return bool|int
*/
- public function estimateRowCount( $table, $vars = '*', $conds = '',
- $fname = __METHOD__, $options = []
+ public function estimateRowCount( $table, $var = '*', $conds = '',
+ $fname = __METHOD__, $options = [], $join_conds = []
) {
+ $conds = $this->normalizeConditions( $conds, $fname );
+ $column = $this->extractSingleFieldFromList( $var );
+ if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
+ $conds[] = "$column IS NOT NULL";
+ }
+
$options['EXPLAIN'] = true;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
+ $res = $this->select( $table, $var, $conds, $fname, $options, $join_conds );
if ( $res === false ) {
return false;
}
@@ -535,7 +603,7 @@ abstract class DatabaseMysqlBase extends Database {
list( $database, , $prefix, $table ) = $this->qualifiedTableComponents( $table );
$tableName = "{$prefix}{$table}";
- if ( isset( $this->mSessionTempTables[$tableName] ) ) {
+ if ( isset( $this->sessionTempTables[$tableName] ) ) {
return true; // already known to exist and won't show in SHOW TABLES anyway
}
@@ -616,7 +684,7 @@ abstract class DatabaseMysqlBase extends Database {
}
}
- return empty( $result ) ? false : $result;
+ return $result ?: false;
}
/**
@@ -684,6 +752,7 @@ abstract class DatabaseMysqlBase extends Database {
protected function getLagFromSlaveStatus() {
$res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
$row = $res ? $res->fetchObject() : false;
+ // If the server is not replicating, there will be no row
if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
return intval( $row->Seconds_Behind_Master );
}
@@ -697,6 +766,22 @@ abstract class DatabaseMysqlBase extends Database {
protected function getLagFromPtHeartbeat() {
$options = $this->lagDetectionOptions;
+ $currentTrxInfo = $this->getRecordedTransactionLagStatus();
+ if ( $currentTrxInfo ) {
+ // There is an active transaction and the initial lag was already queried
+ $staleness = microtime( true ) - $currentTrxInfo['since'];
+ if ( $staleness > self::LAG_STALE_WARN_THRESHOLD ) {
+ // Avoid returning higher and higher lag value due to snapshot age
+ // given that the isolation level will typically be REPEATABLE-READ
+ $this->queryLogger->warning(
+ "Using cached lag value for {db_server} due to active transaction",
+ $this->getLogContext( [ 'method' => __METHOD__, 'age' => $staleness ] )
+ );
+ }
+
+ return $currentTrxInfo['lag'];
+ }
+
if ( isset( $options['conds'] ) ) {
// Best method for multi-DC setups: use logical channel names
$data = $this->getHeartbeatData( $options['conds'] );
@@ -791,7 +876,8 @@ abstract class DatabaseMysqlBase extends Database {
// Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
// percision field is not supported in MySQL <= 5.5.
$res = $this->query(
- "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1"
+ "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1",
+ __METHOD__
);
$row = $res ? $res->fetchObject() : false;
} finally {
@@ -831,34 +917,55 @@ abstract class DatabaseMysqlBase extends Database {
}
// Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
- if ( $this->useGTIDs && $pos->gtids ) {
+ if ( $pos->getGTIDs() ) {
+ // Ignore GTIDs from domains exclusive to the master DB (presumably inactive)
+ $rpos = $this->getReplicaPos();
+ $gtidsWait = $rpos ? MySQLMasterPos::getCommonDomainGTIDs( $pos, $rpos ) : [];
+ if ( !$gtidsWait ) {
+ $this->queryLogger->error(
+ "No GTIDs with the same domain between master ($pos) and replica ($rpos)",
+ $this->getLogContext( [
+ 'method' => __METHOD__,
+ ] )
+ );
+
+ return -1; // $pos is from the wrong cluster?
+ }
// Wait on the GTID set (MariaDB only)
- $gtidArg = $this->addQuotes( implode( ',', $pos->gtids ) );
- $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
+ $gtidArg = $this->addQuotes( implode( ',', $gtidsWait ) );
+ if ( strpos( $gtidArg, ':' ) !== false ) {
+ // MySQL GTIDs, e.g "source_id:transaction_id"
+ $res = $this->doQuery( "SELECT WAIT_FOR_EXECUTED_GTID_SET($gtidArg, $timeout)" );
+ } else {
+ // MariaDB GTIDs, e.g."domain:server:sequence"
+ $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
+ }
} else {
// Wait on the binlog coordinates
- $encFile = $this->addQuotes( $pos->file );
- $encPos = intval( $pos->pos );
+ $encFile = $this->addQuotes( $pos->getLogFile() );
+ $encPos = intval( $pos->getLogPosition()[$pos::CORD_EVENT] );
$res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
}
$row = $res ? $this->fetchRow( $res ) : false;
if ( !$row ) {
- throw new DBExpectedError( $this,
- "MASTER_POS_WAIT() or MASTER_GTID_WAIT() failed: {$this->lastError()}" );
+ throw new DBExpectedError( $this, "Replication wait failed: {$this->lastError()}" );
}
// Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
$status = ( $row[0] !== null ) ? intval( $row[0] ) : null;
if ( $status === null ) {
- // T126436: jobs programmed to wait on master positions might be referencing binlogs
- // with an old master hostname. Such calls make MASTER_POS_WAIT() return null. Try
- // to detect this and treat the replica DB as having reached the position; a proper master
- // switchover already requires that the new master be caught up before the switch.
- $replicationPos = $this->getReplicaPos();
- if ( $replicationPos && !$replicationPos->channelsMatch( $pos ) ) {
- $this->lastKnownReplicaPos = $replicationPos;
- $status = 0;
+ if ( !$pos->getGTIDs() ) {
+ // T126436: jobs programmed to wait on master positions might be referencing
+ // binlogs with an old master hostname; this makes MASTER_POS_WAIT() return null.
+ // Try to detect this case and treat the replica DB as having reached the given
+ // position (any master switchover already requires that the new master be caught
+ // up before the switch).
+ $replicationPos = $this->getReplicaPos();
+ if ( $replicationPos && !$replicationPos->channelsMatch( $pos ) ) {
+ $this->lastKnownReplicaPos = $replicationPos;
+ $status = 0;
+ }
}
} elseif ( $status >= 0 ) {
// Remember that this position was reached to save queries next time
@@ -874,26 +981,28 @@ abstract class DatabaseMysqlBase extends Database {
* @return MySQLMasterPos|bool
*/
public function getReplicaPos() {
- $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
- $row = $this->fetchObject( $res );
-
- if ( $row ) {
- $pos = isset( $row->Exec_master_log_pos )
- ? $row->Exec_master_log_pos
- : $row->Exec_Master_Log_Pos;
- // Also fetch the last-applied GTID set (MariaDB)
- if ( $this->useGTIDs ) {
- $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_slave_pos'", __METHOD__ );
- $gtidRow = $this->fetchObject( $res );
- $gtidSet = $gtidRow ? $gtidRow->Value : '';
- } else {
- $gtidSet = '';
+ $now = microtime( true ); // as-of-time *before* fetching GTID variables
+
+ if ( $this->useGTIDs() ) {
+ // Try to use GTIDs, fallbacking to binlog positions if not possible
+ $data = $this->getServerGTIDs( __METHOD__ );
+ // Use gtid_slave_pos for MariaDB and gtid_executed for MySQL
+ foreach ( [ 'gtid_slave_pos', 'gtid_executed' ] as $name ) {
+ if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
+ return new MySQLMasterPos( $data[$name], $now );
+ }
}
+ }
- return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos, $gtidSet );
- } else {
- return false;
+ $data = $this->getServerRoleStatus( 'SLAVE', __METHOD__ );
+ if ( $data && strlen( $data['Relay_Master_Log_File'] ) ) {
+ return new MySQLMasterPos(
+ "{$data['Relay_Master_Log_File']}/{$data['Exec_Master_Log_Pos']}",
+ $now
+ );
}
+
+ return false;
}
/**
@@ -902,23 +1011,97 @@ abstract class DatabaseMysqlBase extends Database {
* @return MySQLMasterPos|bool
*/
public function getMasterPos() {
- $res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
- $row = $this->fetchObject( $res );
+ $now = microtime( true ); // as-of-time *before* fetching GTID variables
+
+ $pos = false;
+ if ( $this->useGTIDs() ) {
+ // Try to use GTIDs, fallbacking to binlog positions if not possible
+ $data = $this->getServerGTIDs( __METHOD__ );
+ // Use gtid_binlog_pos for MariaDB and gtid_executed for MySQL
+ foreach ( [ 'gtid_binlog_pos', 'gtid_executed' ] as $name ) {
+ if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
+ $pos = new MySQLMasterPos( $data[$name], $now );
+ break;
+ }
+ }
+ // Filter domains that are inactive or not relevant to the session
+ if ( $pos ) {
+ $pos->setActiveOriginServerId( $this->getServerId() );
+ $pos->setActiveOriginServerUUID( $this->getServerUUID() );
+ if ( isset( $data['gtid_domain_id'] ) ) {
+ $pos->setActiveDomain( $data['gtid_domain_id'] );
+ }
+ }
+ }
- if ( $row ) {
- // Also fetch the last-written GTID set (MariaDB)
- if ( $this->useGTIDs ) {
- $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_binlog_pos'", __METHOD__ );
- $gtidRow = $this->fetchObject( $res );
- $gtidSet = $gtidRow ? $gtidRow->Value : '';
- } else {
- $gtidSet = '';
+ if ( !$pos ) {
+ $data = $this->getServerRoleStatus( 'MASTER', __METHOD__ );
+ if ( $data && strlen( $data['File'] ) ) {
+ $pos = new MySQLMasterPos( "{$data['File']}/{$data['Position']}", $now );
}
+ }
- return new MySQLMasterPos( $row->File, $row->Position, $gtidSet );
- } else {
- return false;
+ return $pos;
+ }
+
+ /**
+ * @return int
+ * @throws DBQueryError If the variable doesn't exist for some reason
+ */
+ protected function getServerId() {
+ return $this->srvCache->getWithSetCallback(
+ $this->srvCache->makeGlobalKey( 'mysql-server-id', $this->getServer() ),
+ self::SERVER_ID_CACHE_TTL,
+ function () {
+ $res = $this->query( "SELECT @@server_id AS id", __METHOD__ );
+ return intval( $this->fetchObject( $res )->id );
+ }
+ );
+ }
+
+ /**
+ * @return string|null
+ */
+ protected function getServerUUID() {
+ return $this->srvCache->getWithSetCallback(
+ $this->srvCache->makeGlobalKey( 'mysql-server-uuid', $this->getServer() ),
+ self::SERVER_ID_CACHE_TTL,
+ function () {
+ $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'server_uuid'" );
+ $row = $this->fetchObject( $res );
+
+ return $row ? $row->Value : null;
+ }
+ );
+ }
+
+ /**
+ * @param string $fname
+ * @return string[]
+ */
+ protected function getServerGTIDs( $fname = __METHOD__ ) {
+ $map = [];
+ // Get global-only variables like gtid_executed
+ $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_%'", $fname );
+ foreach ( $res as $row ) {
+ $map[$row->Variable_name] = $row->Value;
+ }
+ // Get session-specific (e.g. gtid_domain_id since that is were writes will log)
+ $res = $this->query( "SHOW SESSION VARIABLES LIKE 'gtid_%'", $fname );
+ foreach ( $res as $row ) {
+ $map[$row->Variable_name] = $row->Value;
}
+
+ return $map;
+ }
+
+ /**
+ * @param string $role One of "MASTER"/"SLAVE"
+ * @param string $fname
+ * @return string[] Latest available server status row
+ */
+ protected function getServerRoleStatus( $role, $fname = __METHOD__ ) {
+ return $this->query( "SHOW $role STATUS", $fname )->fetchRow() ?: [];
}
public function serverIsReadOnly() {
@@ -1017,6 +1200,10 @@ abstract class DatabaseMysqlBase extends Database {
* @since 1.20
*/
public function lockIsFree( $lockName, $method ) {
+ if ( !parent::lockIsFree( $lockName, $method ) ) {
+ return false; // already held
+ }
+
$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
$result = $this->query( "SELECT IS_FREE_LOCK($encName) AS lockstatus", $method );
$row = $this->fetchObject( $result );
@@ -1040,7 +1227,8 @@ abstract class DatabaseMysqlBase extends Database {
return true;
}
- $this->queryLogger->warning( __METHOD__ . " failed to acquire lock '$lockName'\n" );
+ $this->queryLogger->info( __METHOD__ . " failed to acquire lock '{lockname}'",
+ [ 'lockname' => $lockName ] );
return false;
}
@@ -1107,14 +1295,14 @@ abstract class DatabaseMysqlBase extends Database {
*/
public function setBigSelects( $value = true ) {
if ( $value === 'default' ) {
- if ( $this->mDefaultBigSelects === null ) {
+ if ( $this->defaultBigSelects === null ) {
# Function hasn't been called before so it must already be set to the default
return;
} else {
- $value = $this->mDefaultBigSelects;
+ $value = $this->defaultBigSelects;
}
- } elseif ( $this->mDefaultBigSelects === null ) {
- $this->mDefaultBigSelects =
+ } elseif ( $this->defaultBigSelects === null ) {
+ $this->defaultBigSelects =
(bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ );
}
$encValue = $value ? '1' : '0';
@@ -1212,10 +1400,6 @@ abstract class DatabaseMysqlBase extends Database {
return $this->lastErrno() == 1205;
}
- public function wasErrorReissuable() {
- return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
- }
-
/**
* Determines if the last failure was due to the database being read-only.
*
@@ -1230,6 +1414,26 @@ abstract class DatabaseMysqlBase extends Database {
return $errno == 2013 || $errno == 2006;
}
+ protected function wasKnownStatementRollbackError() {
+ $errno = $this->lastErrno();
+
+ if ( $errno === 1205 ) { // lock wait timeout
+ // Note that this is uncached to avoid stale values of SET is used
+ $row = $this->selectRow(
+ false,
+ [ 'innodb_rollback_on_timeout' => '@@innodb_rollback_on_timeout' ],
+ [],
+ __METHOD__
+ );
+ // https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
+ // https://dev.mysql.com/doc/refman/5.5/en/innodb-parameters.html
+ return $row->innodb_rollback_on_timeout ? false : true;
+ }
+
+ // See https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
+ return in_array( $errno, [ 1022, 1216, 1217, 1137 ], true );
+ }
+
/**
* @param string $oldName
* @param string $newName
@@ -1313,7 +1517,7 @@ abstract class DatabaseMysqlBase extends Database {
*/
public function listViews( $prefix = null, $fname = __METHOD__ ) {
// The name of the column containing the name of the VIEW
- $propertyName = 'Tables_in_' . $this->mDBname;
+ $propertyName = 'Tables_in_' . $this->dbName;
// Query for the VIEWS
$res = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
@@ -1349,38 +1553,24 @@ abstract class DatabaseMysqlBase extends Database {
return in_array( $name, $this->listViews( $prefix ) );
}
+ protected function isTransactableQuery( $sql ) {
+ return parent::isTransactableQuery( $sql ) &&
+ !preg_match( '/^SELECT\s+(GET|RELEASE|IS_FREE)_LOCK\(/', $sql );
+ }
+
/**
- * Allows for index remapping in queries where this is not consistent across DBMS
- *
- * @param string $index
+ * @param string $field Field or column to cast
* @return string
*/
- protected function indexName( $index ) {
- /**
- * When SQLite indexes were introduced in r45764, it was noted that
- * SQLite requires index names to be unique within the whole database,
- * not just within a schema. As discussed in CR r45819, to avoid the
- * need for a schema change on existing installations, the indexes
- * were implicitly mapped from the new names to the old names.
- *
- * This mapping can be removed if DB patches are introduced to alter
- * the relevant tables in existing installations. Note that because
- * this index mapping applies to table creation, even new installations
- * of MySQL have the old names (except for installations created during
- * a period where this mapping was inappropriately removed, see
- * T154872).
- */
- $renamed = [
- 'ar_usertext_timestamp' => 'usertext_timestamp',
- 'un_user_id' => 'user_id',
- 'un_user_ip' => 'user_ip',
- ];
-
- if ( isset( $renamed[$index] ) ) {
- return $renamed[$index];
- } else {
- return $index;
- }
+ public function buildIntegerCast( $field ) {
+ return 'CAST( ' . $field . ' AS SIGNED )';
+ }
+
+ /*
+ * @return bool Whether GTID support is used (mockable for testing)
+ */
+ protected function useGTIDs() {
+ return $this->useGTIDs;
}
}
diff --git a/www/wiki/includes/libs/rdbms/database/DatabaseMysqli.php b/www/wiki/includes/libs/rdbms/database/DatabaseMysqli.php
index c1a56988..0a5450cc 100644
--- a/www/wiki/includes/libs/rdbms/database/DatabaseMysqli.php
+++ b/www/wiki/includes/libs/rdbms/database/DatabaseMysqli.php
@@ -25,6 +25,7 @@ namespace Wikimedia\Rdbms;
use mysqli;
use mysqli_result;
use IP;
+use stdClass;
/**
* Database abstraction object for PHP extension mysqli.
@@ -34,11 +35,9 @@ use IP;
* @see Database
*/
class DatabaseMysqli extends DatabaseMysqlBase {
- /** @var mysqli $mConn */
-
/**
* @param string $sql
- * @return resource
+ * @return mysqli_result
*/
protected function doQuery( $sql ) {
$conn = $this->getBindingHandle();
@@ -86,7 +85,7 @@ class DatabaseMysqli extends DatabaseMysqlBase {
$mysqli = mysqli_init();
$connFlags = 0;
- if ( $this->mFlags & self::DBO_SSL ) {
+ if ( $this->flags & self::DBO_SSL ) {
$connFlags |= MYSQLI_CLIENT_SSL;
$mysqli->ssl_set(
$this->sslKeyPath,
@@ -96,10 +95,10 @@ class DatabaseMysqli extends DatabaseMysqlBase {
$this->sslCiphers
);
}
- if ( $this->mFlags & self::DBO_COMPRESS ) {
+ if ( $this->flags & self::DBO_COMPRESS ) {
$connFlags |= MYSQLI_CLIENT_COMPRESS;
}
- if ( $this->mFlags & self::DBO_PERSISTENT ) {
+ if ( $this->flags & self::DBO_PERSISTENT ) {
$realServer = 'p:' . $realServer;
}
@@ -112,8 +111,8 @@ class DatabaseMysqli extends DatabaseMysqlBase {
}
$mysqli->options( MYSQLI_OPT_CONNECT_TIMEOUT, 3 );
- if ( $mysqli->real_connect( $realServer, $this->mUser,
- $this->mPassword, $this->mDBname, $port, $socket, $connFlags )
+ if ( $mysqli->real_connect( $realServer, $this->user,
+ $this->password, $this->dbName, $port, $socket, $connFlags )
) {
return $mysqli;
}
@@ -162,8 +161,8 @@ class DatabaseMysqli extends DatabaseMysqlBase {
* @return int
*/
function lastErrno() {
- if ( $this->mConn ) {
- return $this->mConn->errno;
+ if ( $this->conn instanceof mysqli ) {
+ return $this->conn->errno;
} else {
return mysqli_connect_errno();
}
@@ -172,7 +171,7 @@ class DatabaseMysqli extends DatabaseMysqlBase {
/**
* @return int
*/
- function affectedRows() {
+ protected function fetchAffectedRowCount() {
$conn = $this->getBindingHandle();
return $conn->affected_rows;
@@ -185,7 +184,7 @@ class DatabaseMysqli extends DatabaseMysqlBase {
function selectDB( $db ) {
$conn = $this->getBindingHandle();
- $this->mDBname = $db;
+ $this->dbName = $db;
return $conn->select_db( $db );
}
@@ -202,7 +201,7 @@ class DatabaseMysqli extends DatabaseMysqlBase {
/**
* @param mysqli_result $res
- * @return bool
+ * @return stdClass|bool
*/
protected function mysqlFetchObject( $res ) {
$object = $res->fetch_object();
@@ -235,7 +234,7 @@ class DatabaseMysqli extends DatabaseMysqlBase {
}
/**
- * @param mysqli $res
+ * @param mysqli_result $res
* @return mixed
*/
protected function mysqlNumFields( $res ) {
@@ -243,7 +242,7 @@ class DatabaseMysqli extends DatabaseMysqlBase {
}
/**
- * @param mysqli $res
+ * @param mysqli_result $res
* @param int $n
* @return mixed
*/
@@ -266,7 +265,7 @@ class DatabaseMysqli extends DatabaseMysqlBase {
}
/**
- * @param mysqli $res
+ * @param mysqli_result $res
* @param int $n
* @return mixed
*/
@@ -277,7 +276,7 @@ class DatabaseMysqli extends DatabaseMysqlBase {
}
/**
- * @param mysqli $res
+ * @param mysqli_result $res
* @param int $n
* @return mixed
*/
@@ -326,13 +325,20 @@ class DatabaseMysqli extends DatabaseMysqlBase {
* @return string
*/
public function __toString() {
- if ( $this->mConn instanceof mysqli ) {
- return (string)$this->mConn->thread_id;
+ if ( $this->conn instanceof mysqli ) {
+ return (string)$this->conn->thread_id;
} else {
// mConn might be false or something.
- return (string)$this->mConn;
+ return (string)$this->conn;
}
}
+
+ /**
+ * @return mysqli
+ */
+ protected function getBindingHandle() {
+ return parent::getBindingHandle();
+ }
}
class_alias( DatabaseMysqli::class, 'DatabaseMysqli' );
diff --git a/www/wiki/includes/libs/rdbms/database/DatabasePostgres.php b/www/wiki/includes/libs/rdbms/database/DatabasePostgres.php
index ac509088..94509a3c 100644
--- a/www/wiki/includes/libs/rdbms/database/DatabasePostgres.php
+++ b/www/wiki/includes/libs/rdbms/database/DatabasePostgres.php
@@ -24,7 +24,7 @@ namespace Wikimedia\Rdbms;
use Wikimedia\Timestamp\ConvertibleTimestamp;
use Wikimedia\WaitConditionLoop;
-use MediaWiki;
+use Wikimedia;
use Exception;
/**
@@ -35,16 +35,16 @@ class DatabasePostgres extends Database {
protected $port;
/** @var resource */
- protected $mLastResult = null;
- /** @var int The number of rows affected as an integer */
- protected $mAffectedRows = null;
+ protected $lastResultHandle = null;
/** @var float|string */
private $numericVersion = null;
/** @var string Connect string to open a PostgreSQL connection */
private $connectString;
/** @var string */
- private $mCoreSchema;
+ private $coreSchema;
+ /** @var string */
+ private $tempSchema;
/** @var string[] Map of (reserved table name => alternate table name) */
private $keywordTableMap = [];
@@ -75,15 +75,17 @@ class DatabasePostgres extends Database {
}
public function hasConstraint( $name ) {
- $conn = $this->getBindingHandle();
-
- $sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " .
- "WHERE c.connamespace = n.oid AND conname = '" .
- pg_escape_string( $conn, $name ) . "' AND n.nspname = '" .
- pg_escape_string( $conn, $this->getCoreSchema() ) . "'";
- $res = $this->doQuery( $sql );
-
- return $this->numRows( $res );
+ foreach ( $this->getCoreSchemas() as $schema ) {
+ $sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " .
+ "WHERE c.connamespace = n.oid AND conname = " .
+ $this->addQuotes( $name ) . " AND n.nspname = " .
+ $this->addQuotes( $schema );
+ $res = $this->doQuery( $sql );
+ if ( $res && $this->numRows( $res ) ) {
+ return true;
+ }
+ }
+ return false;
}
public function open( $server, $user, $password, $dbName ) {
@@ -97,10 +99,10 @@ class DatabasePostgres extends Database {
);
}
- $this->mServer = $server;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
+ $this->server = $server;
+ $this->user = $user;
+ $this->password = $password;
+ $this->dbName = $dbName;
$connectVars = [
// pg_connect() user $user as the default database. Since a database is *required*,
@@ -116,7 +118,7 @@ class DatabasePostgres extends Database {
if ( (int)$this->port > 0 ) {
$connectVars['port'] = (int)$this->port;
}
- if ( $this->mFlags & self::DBO_SSL ) {
+ if ( $this->flags & self::DBO_SSL ) {
$connectVars['sslmode'] = 1;
}
@@ -126,7 +128,7 @@ class DatabasePostgres extends Database {
try {
// Use new connections to let LoadBalancer/LBFactory handle reuse
- $this->mConn = pg_connect( $this->connectString, PGSQL_CONNECT_FORCE_NEW );
+ $this->conn = pg_connect( $this->connectString, PGSQL_CONNECT_FORCE_NEW );
} catch ( Exception $ex ) {
$this->restoreErrorHandler();
throw $ex;
@@ -134,7 +136,7 @@ class DatabasePostgres extends Database {
$phpError = $this->restoreErrorHandler();
- if ( !$this->mConn ) {
+ if ( !$this->conn ) {
$this->queryLogger->debug(
"DB connection error\n" .
"Server: $server, Database: $dbName, User: $user, Password: " .
@@ -144,7 +146,7 @@ class DatabasePostgres extends Database {
throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
}
- $this->mOpened = true;
+ $this->opened = true;
# If called from the command-line (e.g. importDump), only show errors
if ( $this->cliMode ) {
@@ -155,15 +157,13 @@ class DatabasePostgres extends Database {
$this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ );
$this->query( "SET timezone = 'GMT'", __METHOD__ );
$this->query( "SET standard_conforming_strings = on", __METHOD__ );
- if ( $this->getServerVersion() >= 9.0 ) {
- $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
- }
+ $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
- $this->determineCoreSchema( $this->mSchema );
+ $this->determineCoreSchema( $this->schema );
// The schema to be used is now in the search path; no need for explicit qualification
- $this->mSchema = '';
+ $this->schema = '';
- return $this->mConn;
+ return $this->conn;
}
public function databasesAreIndependent() {
@@ -178,8 +178,8 @@ class DatabasePostgres extends Database {
* @throws DBUnexpectedError
*/
public function selectDB( $db ) {
- if ( $this->mDBname !== $db ) {
- return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
+ if ( $this->dbName !== $db ) {
+ return (bool)$this->open( $this->server, $this->user, $this->password, $db );
} else {
return true;
}
@@ -199,7 +199,12 @@ class DatabasePostgres extends Database {
}
protected function closeConnection() {
- return $this->mConn ? pg_close( $this->mConn ) : true;
+ return $this->conn ? pg_close( $this->conn ) : true;
+ }
+
+ protected function isTransactableQuery( $sql ) {
+ return parent::isTransactableQuery( $sql ) &&
+ !preg_match( '/^SELECT\s+pg_(try_|)advisory_\w+\(/', $sql );
}
public function doQuery( $sql ) {
@@ -213,13 +218,12 @@ class DatabasePostgres extends Database {
if ( pg_send_query( $conn, $sql ) === false ) {
throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
}
- $this->mLastResult = pg_get_result( $conn );
- $this->mAffectedRows = null;
- if ( pg_result_error( $this->mLastResult ) ) {
+ $this->lastResultHandle = pg_get_result( $conn );
+ if ( pg_result_error( $this->lastResultHandle ) ) {
return false;
}
- return $this->mLastResult;
+ return $this->lastResultHandle;
}
protected function dumpError() {
@@ -239,36 +243,17 @@ class DatabasePostgres extends Database {
];
foreach ( $diags as $d ) {
$this->queryLogger->debug( sprintf( "PgSQL ERROR(%d): %s\n",
- $d, pg_result_error_field( $this->mLastResult, $d ) ) );
+ $d, pg_result_error_field( $this->lastResultHandle, $d ) ) );
}
}
- public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- if ( $tempIgnore ) {
- /* Check for constraint violation */
- if ( $errno === '23505' ) {
- parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore );
-
- return;
- }
- }
- /* Transaction stays in the ERROR state until rolled back */
- if ( $this->mTrxLevel ) {
- // Throw away the transaction state, then raise the error as normal.
- // Note that if this connection is managed by LBFactory, it's already expected
- // that the other transactions LBFactory manages will be rolled back.
- $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
- }
- parent::reportQueryError( $error, $errno, $sql, $fname, false );
- }
-
public function freeResult( $res ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ok = pg_free_result( $res );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$ok ) {
throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
}
@@ -278,9 +263,9 @@ class DatabasePostgres extends Database {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$row = pg_fetch_object( $res );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
# @todo FIXME: HACK HACK HACK HACK debug
# @todo hashar: not sure if the following test really trigger if the object
@@ -300,9 +285,9 @@ class DatabasePostgres extends Database {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$row = pg_fetch_array( $res );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
$conn = $this->getBindingHandle();
if ( pg_last_error( $conn ) ) {
@@ -319,9 +304,9 @@ class DatabasePostgres extends Database {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$n = pg_num_rows( $res );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
$conn = $this->getBindingHandle();
if ( pg_last_error( $conn ) ) {
@@ -365,9 +350,9 @@ class DatabasePostgres extends Database {
}
public function lastError() {
- if ( $this->mConn ) {
- if ( $this->mLastResult ) {
- return pg_result_error( $this->mLastResult );
+ if ( $this->conn ) {
+ if ( $this->lastResultHandle ) {
+ return pg_result_error( $this->lastResultHandle );
} else {
return pg_last_error();
}
@@ -377,23 +362,19 @@ class DatabasePostgres extends Database {
}
public function lastErrno() {
- if ( $this->mLastResult ) {
- return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
+ if ( $this->lastResultHandle ) {
+ return pg_result_error_field( $this->lastResultHandle, PGSQL_DIAG_SQLSTATE );
} else {
return false;
}
}
- public function affectedRows() {
- if ( !is_null( $this->mAffectedRows ) ) {
- // Forced result for simulated queries
- return $this->mAffectedRows;
- }
- if ( empty( $this->mLastResult ) ) {
+ protected function fetchAffectedRowCount() {
+ if ( !$this->lastResultHandle ) {
return 0;
}
- return pg_affected_rows( $this->mLastResult );
+ return pg_affected_rows( $this->lastResultHandle );
}
/**
@@ -404,17 +385,24 @@ class DatabasePostgres extends Database {
* Takes same arguments as Database::select()
*
* @param string $table
- * @param string $vars
+ * @param string $var
* @param string $conds
* @param string $fname
* @param array $options
+ * @param array $join_conds
* @return int
*/
- public function estimateRowCount( $table, $vars = '*', $conds = '',
- $fname = __METHOD__, $options = []
+ public function estimateRowCount( $table, $var = '*', $conds = '',
+ $fname = __METHOD__, $options = [], $join_conds = []
) {
+ $conds = $this->normalizeConditions( $conds, $fname );
+ $column = $this->extractSingleFieldFromList( $var );
+ if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
+ $conds[] = "$column IS NOT NULL";
+ }
+
$options['EXPLAIN'] = true;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
+ $res = $this->select( $table, $var, $conds, $fname, $options, $join_conds );
$rows = -1;
if ( $res ) {
$row = $this->fetchRow( $res );
@@ -444,59 +432,65 @@ class DatabasePostgres extends Database {
public function indexAttributes( $index, $schema = false ) {
if ( $schema === false ) {
- $schema = $this->getCoreSchema();
- }
- /*
- * A subquery would be not needed if we didn't care about the order
- * of attributes, but we do
- */
- $sql = <<<__INDEXATTR__
-
- SELECT opcname,
- attname,
- i.indoption[s.g] as option,
- pg_am.amname
- FROM
- (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g
- FROM
- pg_index isub
- JOIN pg_class cis
- ON cis.oid=isub.indexrelid
- JOIN pg_namespace ns
- ON cis.relnamespace = ns.oid
- WHERE cis.relname='$index' AND ns.nspname='$schema') AS s,
- pg_attribute,
- pg_opclass opcls,
- pg_am,
- pg_class ci
- JOIN pg_index i
- ON ci.oid=i.indexrelid
- JOIN pg_class ct
- ON ct.oid = i.indrelid
- JOIN pg_namespace n
- ON ci.relnamespace = n.oid
- WHERE
- ci.relname='$index' AND n.nspname='$schema'
- AND attrelid = ct.oid
- AND i.indkey[s.g] = attnum
- AND i.indclass[s.g] = opcls.oid
- AND pg_am.oid = opcls.opcmethod
+ $schemas = $this->getCoreSchemas();
+ } else {
+ $schemas = [ $schema ];
+ }
+
+ $eindex = $this->addQuotes( $index );
+
+ foreach ( $schemas as $schema ) {
+ $eschema = $this->addQuotes( $schema );
+ /*
+ * A subquery would be not needed if we didn't care about the order
+ * of attributes, but we do
+ */
+ $sql = <<<__INDEXATTR__
+
+ SELECT opcname,
+ attname,
+ i.indoption[s.g] as option,
+ pg_am.amname
+ FROM
+ (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g
+ FROM
+ pg_index isub
+ JOIN pg_class cis
+ ON cis.oid=isub.indexrelid
+ JOIN pg_namespace ns
+ ON cis.relnamespace = ns.oid
+ WHERE cis.relname=$eindex AND ns.nspname=$eschema) AS s,
+ pg_attribute,
+ pg_opclass opcls,
+ pg_am,
+ pg_class ci
+ JOIN pg_index i
+ ON ci.oid=i.indexrelid
+ JOIN pg_class ct
+ ON ct.oid = i.indrelid
+ JOIN pg_namespace n
+ ON ci.relnamespace = n.oid
+ WHERE
+ ci.relname=$eindex AND n.nspname=$eschema
+ AND attrelid = ct.oid
+ AND i.indkey[s.g] = attnum
+ AND i.indclass[s.g] = opcls.oid
+ AND pg_am.oid = opcls.opcmethod
__INDEXATTR__;
- $res = $this->query( $sql, __METHOD__ );
- $a = [];
- if ( $res ) {
- foreach ( $res as $row ) {
- $a[] = [
- $row->attname,
- $row->opcname,
- $row->amname,
- $row->option ];
+ $res = $this->query( $sql, __METHOD__ );
+ $a = [];
+ if ( $res ) {
+ foreach ( $res as $row ) {
+ $a[] = [
+ $row->attname,
+ $row->opcname,
+ $row->amname,
+ $row->option ];
+ }
+ return $a;
}
- } else {
- return null;
}
-
- return $a;
+ return null;
}
public function indexUnique( $table, $index, $fname = __METHOD__ ) {
@@ -532,26 +526,30 @@ __INDEXATTR__;
unset( $options[$forUpdateKey] );
$options['FOR UPDATE'] = [];
- // All tables not in $join_conds are good
- foreach ( $table as $alias => $name ) {
- if ( is_numeric( $alias ) ) {
+ $toCheck = $table;
+ reset( $toCheck );
+ while ( $toCheck ) {
+ $alias = key( $toCheck );
+ $name = $toCheck[$alias];
+ unset( $toCheck[$alias] );
+
+ $hasAlias = !is_numeric( $alias );
+ if ( !$hasAlias && is_string( $name ) ) {
$alias = $name;
}
- if ( !isset( $join_conds[$alias] ) ) {
- $options['FOR UPDATE'][] = $alias;
- }
- }
- foreach ( $join_conds as $table_cond => $join_cond ) {
- if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
- $options['FOR UPDATE'][] = $table_cond;
+ if ( !isset( $join_conds[$alias] ) ||
+ !preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_conds[$alias][0] )
+ ) {
+ if ( is_array( $name ) ) {
+ // It's a parenthesized group, process all the tables inside the group.
+ $toCheck = array_merge( $toCheck, $name );
+ } else {
+ // Quote alias names so $this->tableName() won't mangle them
+ $options['FOR UPDATE'][] = $hasAlias ? $this->addIdentifierQuotes( $alias ) : $alias;
+ }
}
}
-
- // Quote alias names so $this->tableName() won't mangle them
- $options['FOR UPDATE'] = array_map( function ( $name ) use ( $table ) {
- return isset( $table[$name] ) ? $this->addIdentifierQuotes( $name ) : $name;
- }, $options['FOR UPDATE'] );
}
if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) {
@@ -562,18 +560,7 @@ __INDEXATTR__;
return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
}
- /**
- * INSERT wrapper, inserts an array into a table
- *
- * $args may be a single associative array, or an array of these with numeric keys,
- * for multi-row insert (Postgres version 8.2 and above only).
- *
- * @param string $table Name of the table to insert to.
- * @param array $args Items to insert into the table.
- * @param string $fname Name of the function, for profiling
- * @param array|string $options String or array. Valid options: IGNORE
- * @return bool Success of insert operation. IGNORE always returns true.
- */
+ /** @inheritDoc */
public function insert( $table, $args, $fname = __METHOD__, $options = [] ) {
if ( !count( $args ) ) {
return true;
@@ -589,97 +576,80 @@ __INDEXATTR__;
}
if ( isset( $args[0] ) && is_array( $args[0] ) ) {
- $multi = true;
+ $rows = $args;
$keys = array_keys( $args[0] );
} else {
- $multi = false;
+ $rows = [ $args ];
$keys = array_keys( $args );
}
- // If IGNORE is set, we use savepoints to emulate mysql's behavior
- $savepoint = $olde = null;
- $numrowsinserted = 0;
- if ( in_array( 'IGNORE', $options ) ) {
- $savepoint = new SavepointPostgres( $this, 'mw', $this->queryLogger );
- $olde = error_reporting( 0 );
- // For future use, we may want to track the number of actual inserts
- // Right now, insert (all writes) simply return true/false
- }
+ $ignore = in_array( 'IGNORE', $options );
$sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
- if ( $multi ) {
- if ( $this->numericVersion >= 8.2 && !$savepoint ) {
- $first = true;
- foreach ( $args as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeList( $row ) . ')';
+ if ( $this->numericVersion >= 9.5 || !$ignore ) {
+ // No IGNORE or our PG has "ON CONFLICT DO NOTHING"
+ $first = true;
+ foreach ( $rows as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
}
- $res = (bool)$this->query( $sql, $fname, $savepoint );
- } else {
- $res = true;
- $origsql = $sql;
- foreach ( $args as $row ) {
- $tempsql = $origsql;
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
+ if ( $ignore ) {
+ $sql .= ' ON CONFLICT DO NOTHING';
+ }
+ $this->query( $sql, $fname );
+ } else {
+ // Emulate IGNORE by doing each row individually, with savepoints
+ // to roll back as necessary.
+ $numrowsinserted = 0;
+
+ $tok = $this->startAtomic( "$fname (outer)", self::ATOMIC_CANCELABLE );
+ try {
+ foreach ( $rows as $row ) {
+ $tempsql = $sql;
$tempsql .= '(' . $this->makeList( $row ) . ')';
- if ( $savepoint ) {
- $savepoint->savepoint();
- }
-
- $tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
-
- if ( $savepoint ) {
- $bar = pg_result_error( $this->mLastResult );
- if ( $bar != false ) {
- $savepoint->rollback();
- } else {
- $savepoint->release();
- $numrowsinserted++;
+ $this->startAtomic( "$fname (inner)", self::ATOMIC_CANCELABLE );
+ try {
+ $this->query( $tempsql, $fname );
+ $this->endAtomic( "$fname (inner)" );
+ $numrowsinserted++;
+ } catch ( DBQueryError $e ) {
+ $this->cancelAtomic( "$fname (inner)" );
+ // Our IGNORE is supposed to ignore duplicate key errors, but not others.
+ // (even though MySQL's version apparently ignores all errors)
+ if ( $e->errno !== '23505' ) {
+ throw $e;
}
}
-
- // If any of them fail, we fail overall for this function call
- // Note that this will be ignored if IGNORE is set
- if ( !$tempres ) {
- $res = false;
- }
}
+ } catch ( Exception $e ) {
+ $this->cancelAtomic( "$fname (outer)", $tok );
+ throw $e;
}
- } else {
- // Not multi, just a lone insert
- if ( $savepoint ) {
- $savepoint->savepoint();
- }
+ $this->endAtomic( "$fname (outer)" );
- $sql .= '(' . $this->makeList( $args ) . ')';
- $res = (bool)$this->query( $sql, $fname, $savepoint );
- if ( $savepoint ) {
- $bar = pg_result_error( $this->mLastResult );
- if ( $bar != false ) {
- $savepoint->rollback();
- } else {
- $savepoint->release();
- $numrowsinserted++;
- }
- }
+ // Set the affected row count for the whole operation
+ $this->affectedRowCount = $numrowsinserted;
}
- if ( $savepoint ) {
- error_reporting( $olde );
- $savepoint->commit();
- // Set the affected row count for the whole operation
- $this->mAffectedRows = $numrowsinserted;
+ return true;
+ }
- // IGNORE always returns true
- return true;
+ protected function makeUpdateOptionsArray( $options ) {
+ if ( !is_array( $options ) ) {
+ $options = [ $options ];
}
- return $res;
+ // PostgreSQL doesn't support anything like "ignore" for
+ // UPDATE.
+ $options = array_diff( $options, [ 'IGNORE' ] );
+
+ return parent::makeUpdateOptionsArray( $options );
}
/**
@@ -709,40 +679,35 @@ __INDEXATTR__;
$insertOptions = [ $insertOptions ];
}
- /*
- * If IGNORE is set, we use savepoints to emulate mysql's behavior
- * Ignore LOW PRIORITY option, since it is MySQL-specific
- */
- $savepoint = $olde = null;
- $numrowsinserted = 0;
if ( in_array( 'IGNORE', $insertOptions ) ) {
- $savepoint = new SavepointPostgres( $this, 'mw', $this->queryLogger );
- $olde = error_reporting( 0 );
- $savepoint->savepoint();
- }
-
- $res = parent::nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname,
- $insertOptions, $selectOptions, $selectJoinConds );
-
- if ( $savepoint ) {
- $bar = pg_result_error( $this->mLastResult );
- if ( $bar != false ) {
- $savepoint->rollback();
+ if ( $this->getServerVersion() >= 9.5 ) {
+ // Use ON CONFLICT DO NOTHING if we have it for IGNORE
+ $destTable = $this->tableName( $destTable );
+
+ $selectSql = $this->selectSQLText(
+ $srcTable,
+ array_values( $varMap ),
+ $conds,
+ $fname,
+ $selectOptions,
+ $selectJoinConds
+ );
+
+ $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' .
+ $selectSql . ' ON CONFLICT DO NOTHING';
+
+ return $this->query( $sql, $fname );
} else {
- $savepoint->release();
- $numrowsinserted++;
+ // IGNORE and we don't have ON CONFLICT DO NOTHING, so just use the non-native version
+ return $this->nonNativeInsertSelect(
+ $destTable, $srcTable, $varMap, $conds, $fname,
+ $insertOptions, $selectOptions, $selectJoinConds
+ );
}
- error_reporting( $olde );
- $savepoint->commit();
-
- // Set the affected row count for the whole operation
- $this->mAffectedRows = $numrowsinserted;
-
- // IGNORE always returns true
- return true;
}
- return $res;
+ return parent::nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname,
+ $insertOptions, $selectOptions, $selectJoinConds );
}
public function tableName( $name, $format = 'quoted' ) {
@@ -810,23 +775,106 @@ __INDEXATTR__;
}
public function wasDeadlock() {
- return $this->lastErrno() == '40P01';
+ // https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html
+ return $this->lastErrno() === '40P01';
+ }
+
+ public function wasLockTimeout() {
+ // https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html
+ return $this->lastErrno() === '55P03';
+ }
+
+ public function wasConnectionError( $errno ) {
+ // https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html
+ static $codes = [ '08000', '08003', '08006', '08001', '08004', '57P01', '57P03', '53300' ];
+
+ return in_array( $errno, $codes, true );
+ }
+
+ protected function wasKnownStatementRollbackError() {
+ return false; // transaction has to be rolled-back from error state
}
public function duplicateTableStructure(
$oldName, $newName, $temporary = false, $fname = __METHOD__
) {
- $newName = $this->addIdentifierQuotes( $newName );
- $oldName = $this->addIdentifierQuotes( $oldName );
+ $newNameE = $this->addIdentifierQuotes( $newName );
+ $oldNameE = $this->addIdentifierQuotes( $oldName );
+
+ $temporary = $temporary ? 'TEMPORARY' : '';
+
+ $ret = $this->query( "CREATE $temporary TABLE $newNameE " .
+ "(LIKE $oldNameE INCLUDING DEFAULTS INCLUDING INDEXES)", $fname );
+ if ( !$ret ) {
+ return $ret;
+ }
+
+ $res = $this->query( 'SELECT attname FROM pg_class c'
+ . ' JOIN pg_namespace n ON (n.oid = c.relnamespace)'
+ . ' JOIN pg_attribute a ON (a.attrelid = c.oid)'
+ . ' JOIN pg_attrdef d ON (c.oid=d.adrelid and a.attnum=d.adnum)'
+ . ' WHERE relkind = \'r\''
+ . ' AND nspname = ' . $this->addQuotes( $this->getCoreSchema() )
+ . ' AND relname = ' . $this->addQuotes( $oldName )
+ . ' AND adsrc LIKE \'nextval(%\'',
+ $fname
+ );
+ $row = $this->fetchObject( $res );
+ if ( $row ) {
+ $field = $row->attname;
+ $newSeq = "{$newName}_{$field}_seq";
+ $fieldE = $this->addIdentifierQuotes( $field );
+ $newSeqE = $this->addIdentifierQuotes( $newSeq );
+ $newSeqQ = $this->addQuotes( $newSeq );
+ $this->query( "CREATE $temporary SEQUENCE $newSeqE OWNED BY $newNameE.$fieldE", $fname );
+ $this->query(
+ "ALTER TABLE $newNameE ALTER COLUMN $fieldE SET DEFAULT nextval({$newSeqQ}::regclass)",
+ $fname
+ );
+ }
- return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName " .
- "(LIKE $oldName INCLUDING DEFAULTS INCLUDING INDEXES)", $fname );
+ return $ret;
+ }
+
+ public function resetSequenceForTable( $table, $fname = __METHOD__ ) {
+ $table = $this->tableName( $table, 'raw' );
+ foreach ( $this->getCoreSchemas() as $schema ) {
+ $res = $this->query(
+ 'SELECT c.oid FROM pg_class c JOIN pg_namespace n ON (n.oid = c.relnamespace)'
+ . ' WHERE relkind = \'r\''
+ . ' AND nspname = ' . $this->addQuotes( $schema )
+ . ' AND relname = ' . $this->addQuotes( $table ),
+ $fname
+ );
+ if ( !$res || !$this->numRows( $res ) ) {
+ continue;
+ }
+
+ $oid = $this->fetchObject( $res )->oid;
+ $res = $this->query( 'SELECT adsrc FROM pg_attribute a'
+ . ' JOIN pg_attrdef d ON (a.attrelid=d.adrelid and a.attnum=d.adnum)'
+ . " WHERE a.attrelid = $oid"
+ . ' AND adsrc LIKE \'nextval(%\'',
+ $fname
+ );
+ $row = $this->fetchObject( $res );
+ if ( $row ) {
+ $this->query(
+ 'SELECT ' . preg_replace( '/^nextval\((.+)\)$/', 'setval($1,1,false)', $row->adsrc ),
+ $fname
+ );
+ return true;
+ }
+ return false;
+ }
+
+ return false;
}
public function listTables( $prefix = null, $fname = __METHOD__ ) {
- $eschema = $this->addQuotes( $this->getCoreSchema() );
+ $eschemas = implode( ',', array_map( [ $this, 'addQuotes' ], $this->getCoreSchemas() ) );
$result = $this->query(
- "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
+ "SELECT DISTINCT tablename FROM pg_tables WHERE schemaname IN ($eschemas)", $fname );
$endArray = [];
foreach ( $result as $table ) {
@@ -980,7 +1028,7 @@ __INDEXATTR__;
$this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
if ( $this->schemaExists( $desiredSchema ) ) {
if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
- $this->mCoreSchema = $desiredSchema;
+ $this->coreSchema = $desiredSchema;
$this->queryLogger->debug(
"Schema \"" . $desiredSchema . "\" already in the search path\n" );
} else {
@@ -993,15 +1041,15 @@ __INDEXATTR__;
array_unshift( $search_path,
$this->addIdentifierQuotes( $desiredSchema ) );
$this->setSearchPath( $search_path );
- $this->mCoreSchema = $desiredSchema;
+ $this->coreSchema = $desiredSchema;
$this->queryLogger->debug(
"Schema \"" . $desiredSchema . "\" added to the search path\n" );
}
} else {
- $this->mCoreSchema = $this->getCurrentSchema();
+ $this->coreSchema = $this->getCurrentSchema();
$this->queryLogger->debug(
"Schema \"" . $desiredSchema . "\" not found, using current \"" .
- $this->mCoreSchema . "\"\n" );
+ $this->coreSchema . "\"\n" );
}
/* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
$this->commit( __METHOD__, self::FLUSHING_INTERNAL );
@@ -1014,7 +1062,30 @@ __INDEXATTR__;
* @return string Core schema name
*/
public function getCoreSchema() {
- return $this->mCoreSchema;
+ return $this->coreSchema;
+ }
+
+ /**
+ * Return schema names for temporary tables and core application tables
+ *
+ * @since 1.31
+ * @return string[] schema names
+ */
+ public function getCoreSchemas() {
+ if ( $this->tempSchema ) {
+ return [ $this->tempSchema, $this->getCoreSchema() ];
+ }
+
+ $res = $this->query(
+ "SELECT nspname FROM pg_catalog.pg_namespace n WHERE n.oid = pg_my_temp_schema()", __METHOD__
+ );
+ $row = $this->fetchObject( $res );
+ if ( $row ) {
+ $this->tempSchema = $row->nspname;
+ return [ $this->tempSchema, $this->getCoreSchema() ];
+ }
+
+ return [ $this->getCoreSchema() ];
}
public function getServerVersion() {
@@ -1049,18 +1120,24 @@ __INDEXATTR__;
$types = [ $types ];
}
if ( $schema === false ) {
- $schema = $this->getCoreSchema();
+ $schemas = $this->getCoreSchemas();
+ } else {
+ $schemas = [ $schema ];
}
$table = $this->realTableName( $table, 'raw' );
$etable = $this->addQuotes( $table );
- $eschema = $this->addQuotes( $schema );
- $sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
- . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
- . "AND c.relkind IN ('" . implode( "','", $types ) . "')";
- $res = $this->query( $sql );
- $count = $res ? $res->numRows() : 0;
+ foreach ( $schemas as $schema ) {
+ $eschema = $this->addQuotes( $schema );
+ $sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
+ . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
+ . "AND c.relkind IN ('" . implode( "','", $types ) . "')";
+ $res = $this->query( $sql );
+ if ( $res && $res->numRows() ) {
+ return true;
+ }
+ }
- return (bool)$count;
+ return false;
}
/**
@@ -1085,20 +1162,21 @@ __INDEXATTR__;
AND tgrelid=pg_class.oid
AND nspname=%s AND relname=%s AND tgname=%s
SQL;
- $res = $this->query(
- sprintf(
- $q,
- $this->addQuotes( $this->getCoreSchema() ),
- $this->addQuotes( $table ),
- $this->addQuotes( $trigger )
- )
- );
- if ( !$res ) {
- return null;
+ foreach ( $this->getCoreSchemas() as $schema ) {
+ $res = $this->query(
+ sprintf(
+ $q,
+ $this->addQuotes( $schema ),
+ $this->addQuotes( $table ),
+ $this->addQuotes( $trigger )
+ )
+ );
+ if ( $res && $res->numRows() ) {
+ return true;
+ }
}
- $rows = $res->numRows();
- return $rows;
+ return false;
}
public function ruleExists( $table, $rule ) {
@@ -1106,7 +1184,7 @@ SQL;
[
'rulename' => $rule,
'tablename' => $table,
- 'schemaname' => $this->getCoreSchema()
+ 'schemaname' => $this->getCoreSchemas()
]
);
@@ -1114,19 +1192,19 @@ SQL;
}
public function constraintExists( $table, $constraint ) {
- $sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
- "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
- $this->addQuotes( $this->getCoreSchema() ),
- $this->addQuotes( $table ),
- $this->addQuotes( $constraint )
- );
- $res = $this->query( $sql );
- if ( !$res ) {
- return null;
+ foreach ( $this->getCoreSchemas() as $schema ) {
+ $sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
+ "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
+ $this->addQuotes( $schema ),
+ $this->addQuotes( $table ),
+ $this->addQuotes( $constraint )
+ );
+ $res = $this->query( $sql );
+ if ( $res && $res->numRows() ) {
+ return true;
+ }
}
- $rows = $res->numRows();
-
- return $rows;
+ return false;
}
/**
@@ -1220,28 +1298,6 @@ SQL;
return "'" . pg_escape_string( $conn, (string)$s ) . "'";
}
- /**
- * Postgres specific version of replaceVars.
- * Calls the parent version in Database.php
- *
- * @param string $ins SQL string, read from a stream (usually tables.sql)
- * @return string SQL string
- */
- protected function replaceVars( $ins ) {
- $ins = parent::replaceVars( $ins );
-
- if ( $this->numericVersion >= 8.3 ) {
- // Thanks for not providing backwards-compatibility, 8.3
- $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
- }
-
- if ( $this->numericVersion <= 8.1 ) { // Our minimum version
- $ins = str_replace( 'USING gin', 'USING gist', $ins );
- }
-
- return $ins;
- }
-
public function makeSelectOptions( $options ) {
$preLimitTail = $postLimitTail = '';
$startOpts = $useIndex = $ignoreIndex = '';
@@ -1272,11 +1328,11 @@ SQL;
}
public function getDBname() {
- return $this->mDBname;
+ return $this->dbName;
}
public function getServer() {
- return $this->mServer;
+ return $this->server;
}
public function buildConcat( $stringList ) {
@@ -1336,7 +1392,10 @@ SQL;
}
public function lockIsFree( $lockName, $method ) {
- // http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ if ( !parent::lockIsFree( $lockName, $method ) ) {
+ return false; // already held
+ }
+ // http://www.postgresql.org/docs/9.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
$key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
$result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key))
WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method );
@@ -1346,7 +1405,7 @@ SQL;
}
public function lock( $lockName, $method, $timeout = 5 ) {
- // http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ // http://www.postgresql.org/docs/9.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
$key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
$loop = new WaitConditionLoop(
function () use ( $lockName, $key, $timeout, $method ) {
@@ -1366,7 +1425,7 @@ SQL;
}
public function unlock( $lockName, $method ) {
- // http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ // http://www.postgresql.org/docs/9.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
$key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
$result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method );
$row = $this->fetchObject( $result );
diff --git a/www/wiki/includes/libs/rdbms/database/DatabaseSqlite.php b/www/wiki/includes/libs/rdbms/database/DatabaseSqlite.php
index 168eef9b..f282b17a 100644
--- a/www/wiki/includes/libs/rdbms/database/DatabaseSqlite.php
+++ b/www/wiki/includes/libs/rdbms/database/DatabaseSqlite.php
@@ -46,16 +46,19 @@ class DatabaseSqlite extends Database {
protected $trxMode;
/** @var int The number of rows affected as an integer */
- protected $mAffectedRows;
+ protected $lastAffectedRowCount;
/** @var resource */
- protected $mLastResult;
+ protected $lastResultHandle;
/** @var PDO */
- protected $mConn;
+ protected $conn;
/** @var FSLockManager (hopefully on the same server as the DB) */
protected $lockMgr;
+ /** @var array List of shared database already attached to this connection */
+ private $alreadyAttached = [];
+
/**
* Additional params include:
* - dbDirectory : directory containing the DB and the lock file directory
@@ -66,33 +69,13 @@ class DatabaseSqlite extends Database {
*/
function __construct( array $p ) {
if ( isset( $p['dbFilePath'] ) ) {
- parent::__construct( $p );
- // Standalone .sqlite file mode.
- // Super doesn't open when $user is false, but we can work with $dbName,
- // which is derived from the file path in this case.
- $this->openFile( $p['dbFilePath'] );
- $lockDomain = md5( $p['dbFilePath'] );
- } elseif ( !isset( $p['dbDirectory'] ) ) {
- throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
- } else {
+ $this->dbPath = $p['dbFilePath'];
+ $lockDomain = md5( $this->dbPath );
+ } elseif ( isset( $p['dbDirectory'] ) ) {
$this->dbDir = $p['dbDirectory'];
- $this->mDBname = $p['dbname'];
- $lockDomain = $this->mDBname;
- // Stock wiki mode using standard file names per DB.
- parent::__construct( $p );
- // Super doesn't open when $user is false, but we can work with $dbName
- if ( $p['dbname'] && !$this->isOpen() ) {
- if ( $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] ) ) {
- $done = [];
- foreach ( $this->tableAliases as $params ) {
- if ( isset( $done[$params['dbname']] ) ) {
- continue;
- }
- $this->attachDatabase( $params['dbname'] );
- $done[$params['dbname']] = 1;
- }
- }
- }
+ $lockDomain = $p['dbname'];
+ } else {
+ throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
}
$this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null;
@@ -107,6 +90,12 @@ class DatabaseSqlite extends Database {
'domain' => $lockDomain,
'lockDirectory' => "{$this->dbDir}/locks"
] );
+
+ parent::__construct( $p );
+ }
+
+ protected static function getAttributes() {
+ return [ self::ATTR_DB_LEVEL_LOCKING => true ];
}
/**
@@ -128,6 +117,28 @@ class DatabaseSqlite extends Database {
return $db;
}
+ protected function doInitConnection() {
+ if ( $this->dbPath !== null ) {
+ // Standalone .sqlite file mode.
+ $this->openFile( $this->dbPath, $this->connectionParams['dbname'] );
+ } elseif ( $this->dbDir !== null ) {
+ // Stock wiki mode using standard file names per DB
+ if ( strlen( $this->connectionParams['dbname'] ) ) {
+ $this->open(
+ $this->connectionParams['host'],
+ $this->connectionParams['user'],
+ $this->connectionParams['password'],
+ $this->connectionParams['dbname']
+ );
+ } else {
+ // Caller will manually call open() later?
+ $this->connLogger->debug( __METHOD__ . ': no database opened.' );
+ }
+ } else {
+ throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
+ }
+ }
+
/**
* @return string
*/
@@ -148,7 +159,7 @@ class DatabaseSqlite extends Database {
* NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
*
* @param string $server
- * @param string $user
+ * @param string $user Unused
* @param string $pass
* @param string $dbName
*
@@ -159,49 +170,51 @@ class DatabaseSqlite extends Database {
$this->close();
$fileName = self::generateFileName( $this->dbDir, $dbName );
if ( !is_readable( $fileName ) ) {
- $this->mConn = false;
+ $this->conn = false;
throw new DBConnectionError( $this, "SQLite database not accessible" );
}
- $this->openFile( $fileName );
+ $this->openFile( $fileName, $dbName );
- return (bool)$this->mConn;
+ return (bool)$this->conn;
}
/**
* Opens a database file
*
* @param string $fileName
+ * @param string $dbName
* @throws DBConnectionError
* @return PDO|bool SQL connection or false if failed
*/
- protected function openFile( $fileName ) {
+ protected function openFile( $fileName, $dbName ) {
$err = false;
$this->dbPath = $fileName;
try {
- if ( $this->mFlags & self::DBO_PERSISTENT ) {
- $this->mConn = new PDO( "sqlite:$fileName", '', '',
+ if ( $this->flags & self::DBO_PERSISTENT ) {
+ $this->conn = new PDO( "sqlite:$fileName", '', '',
[ PDO::ATTR_PERSISTENT => true ] );
} else {
- $this->mConn = new PDO( "sqlite:$fileName", '', '' );
+ $this->conn = new PDO( "sqlite:$fileName", '', '' );
}
} catch ( PDOException $e ) {
$err = $e->getMessage();
}
- if ( !$this->mConn ) {
+ if ( !$this->conn ) {
$this->queryLogger->debug( "DB connection error: $err\n" );
throw new DBConnectionError( $this, $err );
}
- $this->mOpened = !!$this->mConn;
- if ( $this->mOpened ) {
+ $this->opened = is_object( $this->conn );
+ if ( $this->opened ) {
+ $this->dbName = $dbName;
# Set error codes only, don't raise exceptions
- $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
+ $this->conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
# Enforce LIKE to be case sensitive, just like MySQL
$this->query( 'PRAGMA case_sensitive_like = 1' );
- return $this->mConn;
+ return $this->conn;
}
return false;
@@ -220,7 +233,7 @@ class DatabaseSqlite extends Database {
* @return bool
*/
protected function closeConnection() {
- $this->mConn = null;
+ $this->conn = null;
return true;
}
@@ -298,6 +311,14 @@ class DatabaseSqlite extends Database {
return parent::isWriteQuery( $sql ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql );
}
+ protected function isTransactableQuery( $sql ) {
+ return parent::isTransactableQuery( $sql ) && !in_array(
+ $this->getQueryVerb( $sql ),
+ [ 'ATTACH', 'PRAGMA' ],
+ true
+ );
+ }
+
/**
* SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
*
@@ -305,13 +326,13 @@ class DatabaseSqlite extends Database {
* @return bool|ResultWrapper
*/
protected function doQuery( $sql ) {
- $res = $this->mConn->query( $sql );
+ $res = $this->getBindingHandle()->query( $sql );
if ( $res === false ) {
return false;
}
$r = $res instanceof ResultWrapper ? $res->result : $res;
- $this->mAffectedRows = $r->rowCount();
+ $this->lastAffectedRowCount = $r->rowCount();
$res = new ResultWrapper( $this, $r->fetchAll() );
return $res;
@@ -441,7 +462,7 @@ class DatabaseSqlite extends Database {
*/
function insertId() {
// PDO::lastInsertId yields a string :(
- return intval( $this->mConn->lastInsertId() );
+ return intval( $this->getBindingHandle()->lastInsertId() );
}
/**
@@ -466,10 +487,10 @@ class DatabaseSqlite extends Database {
* @return string
*/
function lastError() {
- if ( !is_object( $this->mConn ) ) {
+ if ( !is_object( $this->conn ) ) {
return "Cannot return last error, no db connection";
}
- $e = $this->mConn->errorInfo();
+ $e = $this->conn->errorInfo();
return isset( $e[2] ) ? $e[2] : '';
}
@@ -478,10 +499,10 @@ class DatabaseSqlite extends Database {
* @return string
*/
function lastErrno() {
- if ( !is_object( $this->mConn ) ) {
+ if ( !is_object( $this->conn ) ) {
return "Cannot return last error, no db connection";
} else {
- $info = $this->mConn->errorInfo();
+ $info = $this->conn->errorInfo();
return $info[1];
}
@@ -490,8 +511,8 @@ class DatabaseSqlite extends Database {
/**
* @return int
*/
- function affectedRows() {
- return $this->mAffectedRows;
+ protected function fetchAffectedRowCount() {
+ return $this->lastAffectedRowCount;
}
/**
@@ -692,17 +713,22 @@ class DatabaseSqlite extends Database {
/**
* @return bool
*/
- function wasErrorReissuable() {
- return $this->lastErrno() == 17; // SQLITE_SCHEMA;
- }
-
- /**
- * @return bool
- */
function wasReadOnlyError() {
return $this->lastErrno() == 8; // SQLITE_READONLY;
}
+ public function wasConnectionError( $errno ) {
+ return $errno == 17; // SQLITE_SCHEMA;
+ }
+
+ protected function wasKnownStatementRollbackError() {
+ // ON CONFLICT ROLLBACK clauses make it so that SQLITE_CONSTRAINT error is
+ // ambiguous with regard to whether it implies a ROLLBACK or an ABORT happened.
+ // https://sqlite.org/lang_createtable.html#uniqueconst
+ // https://sqlite.org/lang_conflict.html
+ return false;
+ }
+
/**
* @return string Wikitext of a link to the server software's web site
*/
@@ -714,7 +740,7 @@ class DatabaseSqlite extends Database {
* @return string Version information from the database
*/
function getServerVersion() {
- $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
+ $ver = $this->getBindingHandle()->getAttribute( PDO::ATTR_SERVER_VERSION );
return $ver;
}
@@ -746,7 +772,7 @@ class DatabaseSqlite extends Database {
} else {
$this->query( 'BEGIN', $fname );
}
- $this->mTrxLevel = 1;
+ $this->trxLevel = 1;
}
/**
@@ -804,8 +830,17 @@ class DatabaseSqlite extends Database {
);
return "x'" . bin2hex( (string)$s ) . "'";
} else {
- return $this->mConn->quote( (string)$s );
+ return $this->getBindingHandle()->quote( (string)$s );
+ }
+ }
+
+ public function buildSubstring( $input, $startPosition, $length = null ) {
+ $this->assertBuildSubstringParams( $startPosition, $length );
+ $params = [ $input, $startPosition ];
+ if ( $length !== null ) {
+ $params[] = $length;
}
+ return 'SUBSTR(' . implode( ',', $params ) . ')';
}
/**
@@ -1029,15 +1064,41 @@ class DatabaseSqlite extends Database {
return $this->query( $sql, $fName );
}
- protected function requiresDatabaseUser() {
- return false; // just a file
+ public function setTableAliases( array $aliases ) {
+ parent::setTableAliases( $aliases );
+ foreach ( $this->tableAliases as $params ) {
+ if ( isset( $this->alreadyAttached[$params['dbname']] ) ) {
+ continue;
+ }
+ $this->attachDatabase( $params['dbname'] );
+ $this->alreadyAttached[$params['dbname']] = true;
+ }
+ }
+
+ public function resetSequenceForTable( $table, $fname = __METHOD__ ) {
+ $encTable = $this->addIdentifierQuotes( 'sqlite_sequence' );
+ $encName = $this->addQuotes( $this->tableName( $table, 'raw' ) );
+ $this->query( "DELETE FROM $encTable WHERE name = $encName", $fname );
+ }
+
+ public function databasesAreIndependent() {
+ return true;
}
/**
* @return string
*/
public function __toString() {
- return 'SQLite ' . (string)$this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
+ return is_object( $this->conn )
+ ? 'SQLite ' . (string)$this->conn->getAttribute( PDO::ATTR_SERVER_VERSION )
+ : '(not connected)';
+ }
+
+ /**
+ * @return PDO
+ */
+ protected function getBindingHandle() {
+ return parent::getBindingHandle();
}
}
diff --git a/www/wiki/includes/libs/rdbms/database/IDatabase.php b/www/wiki/includes/libs/rdbms/database/IDatabase.php
index 5d0e03fc..675ba7f2 100644
--- a/www/wiki/includes/libs/rdbms/database/IDatabase.php
+++ b/www/wiki/includes/libs/rdbms/database/IDatabase.php
@@ -1,10 +1,5 @@
<?php
/**
- * @defgroup Database Database
- *
- * This file deals with database interface functions
- * and query specifics/optimisations.
- *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
@@ -21,17 +16,20 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup Database
*/
namespace Wikimedia\Rdbms;
+use InvalidArgumentException;
use Wikimedia\ScopedCallback;
-use Exception;
use RuntimeException;
-use UnexpectedValueException;
use stdClass;
/**
+ * @defgroup Database Database
+ * This group deals with database interface functions
+ * and query specifics/optimisations.
+ */
+/**
* Basic database interface for live and lazy-loaded relation database handles
*
* @note: IDatabase and DBConnRef should be updated to reflect any changes
@@ -50,9 +48,16 @@ interface IDatabase {
/** @var string Transaction is requested internally via DBO_TRX/startAtomic() */
const TRANSACTION_INTERNAL = 'implicit';
- /** @var string Transaction operation comes from service managing all DBs */
+ /** @var string Atomic section is not cancelable */
+ const ATOMIC_NOT_CANCELABLE = '';
+ /** @var string Atomic section is cancelable */
+ const ATOMIC_CANCELABLE = 'cancelable';
+
+ /** @var string Commit/rollback is from outside the IDatabase handle and connection manager */
+ const FLUSHING_ONE = '';
+ /** @var string Commit/rollback is from the connection manager for the IDatabase handle */
const FLUSHING_ALL_PEERS = 'flush';
- /** @var string Transaction operation comes from the database class internally */
+ /** @var string Commit/rollback is from the IDatabase handle internally */
const FLUSHING_INTERNAL = 'flush';
/** @var string Do not remember the prior flags */
@@ -86,7 +91,7 @@ interface IDatabase {
const DBO_NOBUFFER = 2;
/** @var int Ignore query errors (internal use only!) */
const DBO_IGNORE = 4;
- /** @var int Autoatically start transaction on first query (work with ILoadBalancer rounds) */
+ /** @var int Automatically start a transaction before running a query if none is active */
const DBO_TRX = 8;
/** @var int Use DBO_TRX in non-CLI mode */
const DBO_DEFAULT = 16;
@@ -229,6 +234,7 @@ interface IDatabase {
* Should return true if unsure.
*
* @return bool
+ * @deprecated Since 1.31; use lastDoneWrites()
*/
public function doneWrites();
@@ -248,7 +254,7 @@ interface IDatabase {
public function writesPending();
/**
- * Returns true if there is a transaction open with possible write
+ * Returns true if there is a transaction/round open with possible write
* queries or transaction pre-commit/idle callbacks waiting on it to finish.
* This does *not* count recurring callbacks, e.g. from setTransactionListener().
*
@@ -358,7 +364,7 @@ interface IDatabase {
public function getType();
/**
- * Open a connection to the database. Usually aborts on failure
+ * Open a new connection to the database (closing any existing one)
*
* @param string $server Database server host
* @param string $user Database user name
@@ -456,17 +462,6 @@ interface IDatabase {
public function lastError();
/**
- * mysql_fetch_field() wrapper
- * Returns false if the field doesn't exist
- *
- * @param string $table Table name
- * @param string $field Field name
- *
- * @return Field
- */
- public function fieldInfo( $table, $field );
-
- /**
* Get the number of rows affected by the last write query
* @see https://secure.php.net/mysql_affected_rows
*
@@ -493,8 +488,11 @@ interface IDatabase {
public function getServerVersion();
/**
- * Closes a database connection.
- * if it is open : commits any open transactions
+ * Close the database connection
+ *
+ * This should only be called after any transactions have been resolved,
+ * aside from read-only transactions (assuming no callbacks are registered).
+ * If a transaction is still open anyway, it will be committed if possible.
*
* @throws DBError
* @return bool Operation success. true if already closed.
@@ -502,15 +500,13 @@ interface IDatabase {
public function close();
/**
- * @param string $error Fallback error message, used if none is given by DB
- * @throws DBConnectionError
- */
- public function reportConnectionError( $error = 'Unknown error' );
-
- /**
* Run an SQL query and return the result. Normally throws a DBQueryError
* on failure. If errors are ignored, returns false instead.
*
+ * If a connection loss is detected, then an attempt to reconnect will be made.
+ * For queries that involve no larger transactions or locks, they will be re-issued
+ * for convenience, provided the connection was re-established.
+ *
* In new code, the query wrappers select(), insert(), update(), delete(),
* etc. should be used where possible, since they give much better DBMS
* independence and automatically quote or validate user input in a variety
@@ -525,26 +521,13 @@ interface IDatabase {
* comment (you can use __METHOD__ or add some extra info)
* @param bool $tempIgnore Whether to avoid throwing an exception on errors...
* maybe best to catch the exception instead?
- * @throws DBError
* @return bool|IResultWrapper True for a successful write query, IResultWrapper object
* for a successful read query, or false on failure if $tempIgnore set
+ * @throws DBError
*/
public function query( $sql, $fname = __METHOD__, $tempIgnore = false );
/**
- * Report a query error. Log the error, and if neither the object ignore
- * flag nor the $tempIgnore flag is set, throw a DBQueryError.
- *
- * @param string $error
- * @param int $errno
- * @param string $sql
- * @param string $fname
- * @param bool $tempIgnore
- * @throws DBQueryError
- */
- public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false );
-
- /**
* Free a result object returned by query() or select(). It's usually not
* necessary to call this, just use unset() or let the variable holding
* the result object go out of scope.
@@ -569,7 +552,8 @@ interface IDatabase {
* @param string|array $options The query options. See IDatabase::select() for details.
* @param string|array $join_conds The query join conditions. See IDatabase::select() for details.
*
- * @return bool|mixed The value from the field, or false on failure.
+ * @return mixed The value from the field
+ * @throws DBError
*/
public function selectField(
$table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
@@ -591,7 +575,8 @@ interface IDatabase {
* @param string|array $options The query options. See IDatabase::select() for details.
* @param string|array $join_conds The query join conditions. See IDatabase::select() for details.
*
- * @return bool|array The values from the field, or false on failure
+ * @return array The values from the field
+ * @throws DBError
* @since 1.25
*/
public function selectFieldValues(
@@ -620,6 +605,24 @@ interface IDatabase {
* This includes the user table in the query, with the alias "a" available
* for use in field names (e.g. a.user_name).
*
+ * A derived table, defined by the result of selectSQLText(), requires an alias
+ * key and a Subquery instance value which wraps the SQL query, for example:
+ *
+ * [ 'c' => new Subquery( 'SELECT ...' ) ]
+ *
+ * Joins using parentheses for grouping (since MediaWiki 1.31) may be
+ * constructed using nested arrays. For example,
+ *
+ * [ 'tableA', 'nestedB' => [ 'tableB', 'b2' => 'tableB2' ] ]
+ *
+ * along with `$join_conds` like
+ *
+ * [ 'b2' => [ 'JOIN', 'b_id = b2_id' ], 'nestedB' => [ 'LEFT JOIN', 'b_a = a_id' ] ]
+ *
+ * will produce SQL something like
+ *
+ * FROM tableA LEFT JOIN (tableB JOIN tableB2 AS b2 ON (b_id = b2_id)) ON (b_a = a_id)
+ *
* All of the table names given here are automatically run through
* Database::tableName(), which causes the table prefix (if any) to be
* added, and various other table name mappings to be performed.
@@ -742,10 +745,8 @@ interface IDatabase {
*
* [ 'page' => [ 'LEFT JOIN', 'page_latest=rev_id' ] ]
*
- * @return IResultWrapper|bool If the query returned no rows, a IResultWrapper
- * with no rows in it will be returned. If there was a query error, a
- * DBQueryError exception will be thrown, except if the "ignore errors"
- * option was set, in which case false will be returned.
+ * @return IResultWrapper Resulting rows
+ * @throws DBError
*/
public function select(
$table, $vars, $conds = '', $fname = __METHOD__,
@@ -758,15 +759,15 @@ interface IDatabase {
* doing UNION queries, where the SQL text of each query is needed. In general,
* however, callers outside of Database classes should just use select().
*
+ * @see IDatabase::select()
+ *
* @param string|array $table Table name
* @param string|array $vars Field names
* @param string|array $conds Conditions
* @param string $fname Caller function name
* @param string|array $options Query options
* @param string|array $join_conds Join conditions
- *
- * @return string SQL query string.
- * @see IDatabase::select()
+ * @return string SQL query string
*/
public function selectSQLText(
$table, $vars, $conds = '', $fname = __METHOD__,
@@ -786,6 +787,7 @@ interface IDatabase {
* @param array|string $join_conds Join conditions
*
* @return stdClass|bool
+ * @throws DBError
*/
public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
$options = [], $join_conds = []
@@ -805,14 +807,16 @@ interface IDatabase {
* Takes the same arguments as IDatabase::select().
*
* @param string $table Table name
- * @param string $vars Unused
+ * @param string $var Column for which NULL values are not counted [default "*"]
* @param array|string $conds Filters on the table
* @param string $fname Function name for profiling
* @param array $options Options for select
+ * @param array|string $join_conds Join conditions
* @return int Row count
+ * @throws DBError
*/
public function estimateRowCount(
- $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
+ $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
);
/**
@@ -825,15 +829,16 @@ interface IDatabase {
* @since 1.27 Added $join_conds parameter
*
* @param array|string $tables Table names
- * @param string $vars Unused
+ * @param string $var Column for which NULL values are not counted [default "*"]
* @param array|string $conds Filters on the table
* @param string $fname Function name for profiling
* @param array $options Options for select
* @param array $join_conds Join conditions (since 1.27)
* @return int Row count
+ * @throws DBError
*/
public function selectRowCount(
- $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
+ $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
);
/**
@@ -843,6 +848,7 @@ interface IDatabase {
* @param string $field Filed to check on that table
* @param string $fname Calling function name (optional)
* @return bool Whether $table has filed $field
+ * @throws DBError
*/
public function fieldExists( $table, $field, $fname = __METHOD__ );
@@ -855,6 +861,7 @@ interface IDatabase {
* @param string $index
* @param string $fname
* @return bool|null
+ * @throws DBError
*/
public function indexExists( $table, $index, $fname = __METHOD__ );
@@ -864,20 +871,11 @@ interface IDatabase {
* @param string $table
* @param string $fname
* @return bool
+ * @throws DBError
*/
public function tableExists( $table, $fname = __METHOD__ );
/**
- * Determines if a given index is unique
- *
- * @param string $table
- * @param string $index
- *
- * @return bool
- */
- public function indexUnique( $table, $index );
-
- /**
* INSERT wrapper, inserts an array into a table.
*
* $a may be either:
@@ -909,6 +907,7 @@ interface IDatabase {
* @param array $options Array of options
*
* @return bool
+ * @throws DBError
*/
public function insert( $table, $a, $fname = __METHOD__, $options = [] );
@@ -931,6 +930,7 @@ interface IDatabase {
* - IGNORE: Ignore unique key conflicts
* - LOW_PRIORITY: MySQL-specific, see MySQL manual.
* @return bool
+ * @throws DBError
*/
public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] );
@@ -943,11 +943,11 @@ interface IDatabase {
* Example usage:
* @code
* $sql = $db->makeList( [
- * 'rev_user' => $id,
+ * 'rev_page' => $id,
* $db->makeList( [ 'rev_minor' => 1, 'rev_len' < 500 ], $db::LIST_OR ] )
* ], $db::LIST_AND );
* @endcode
- * This would set $sql to "rev_user = '$id' AND (rev_minor = '1' OR rev_len < '500')"
+ * This would set $sql to "rev_page = '$id' AND (rev_minor = '1' OR rev_len < '500')"
*
* @param array $a Containing the data
* @param int $mode IDatabase class constant:
@@ -1032,6 +1032,20 @@ interface IDatabase {
);
/**
+ * Build a SUBSTRING function.
+ *
+ * Behavior for non-ASCII values is undefined.
+ *
+ * @param string $input Field name
+ * @param int $startPosition Positive integer
+ * @param int|null $length Non-negative integer length or null for no limit
+ * @throws InvalidArgumentException
+ * @return string SQL text
+ * @since 1.31
+ */
+ public function buildSubString( $input, $startPosition, $length = null );
+
+ /**
* @param string $field Field or column to cast
* @return string
* @since 1.28
@@ -1039,6 +1053,32 @@ interface IDatabase {
public function buildStringCast( $field );
/**
+ * @param string $field Field or column to cast
+ * @return string
+ * @since 1.31
+ */
+ public function buildIntegerCast( $field );
+
+ /**
+ * Equivalent to IDatabase::selectSQLText() except wraps the result in Subqyery
+ *
+ * @see IDatabase::selectSQLText()
+ *
+ * @param string|array $table Table name
+ * @param string|array $vars Field names
+ * @param string|array $conds Conditions
+ * @param string $fname Caller function name
+ * @param string|array $options Query options
+ * @param string|array $join_conds Join conditions
+ * @return Subquery
+ * @since 1.31
+ */
+ public function buildSelectSubquery(
+ $table, $vars, $conds = '', $fname = __METHOD__,
+ $options = [], $join_conds = []
+ );
+
+ /**
* Returns true if DBs are assumed to be on potentially different servers
*
* In systems like mysql/mariadb, different databases can easily be referenced on a single
@@ -1151,6 +1191,7 @@ interface IDatabase {
* @param array $rows Can be either a single row to insert, or multiple rows,
* in the same format as for IDatabase::insert()
* @param string $fname Calling function name (use __METHOD__) for logs/profiling
+ * @throws DBError
*/
public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ );
@@ -1187,7 +1228,7 @@ interface IDatabase {
* Values with integer keys form unquoted SET statements, which can be used for
* things like "field = field + 1" or similar computed values.
* @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @throws Exception
+ * @throws DBError
* @return bool
*/
public function upsert(
@@ -1212,7 +1253,7 @@ interface IDatabase {
* @param array $conds Condition array of field names mapped to variables,
* ANDed together in the WHERE clause
* @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @throws DBUnexpectedError
+ * @throws DBError
*/
public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
$fname = __METHOD__
@@ -1227,6 +1268,7 @@ interface IDatabase {
* @param string $fname Name of the calling function
* @throws DBUnexpectedError
* @return bool|IResultWrapper
+ * @throws DBError
*/
public function delete( $table, $conds, $fname = __METHOD__ );
@@ -1234,6 +1276,11 @@ interface IDatabase {
* INSERT SELECT wrapper. Takes data from a SELECT query and inserts it
* into another table.
*
+ * @warning If the insert will use an auto-increment or sequence to
+ * determine the value of a column, this may break replication on
+ * databases using statement-based replication if the SELECT is not
+ * deterministically ordered.
+ *
* @param string $destTable The table name to insert into
* @param string|array $srcTable May be either a table name, or an array of table names
* to include in a join.
@@ -1250,13 +1297,16 @@ interface IDatabase {
* @param string $fname The function name of the caller, from __METHOD__
*
* @param array $insertOptions Options for the INSERT part of the query, see
- * IDatabase::insert() for details.
+ * IDatabase::insert() for details. Also, one additional option is
+ * available: pass 'NO_AUTO_COLUMNS' to hint that the query does not use
+ * an auto-increment or sequence to determine any column values.
* @param array $selectOptions Options for the SELECT part of the query, see
* IDatabase::select() for details.
* @param array $selectJoinConds Join conditions for the SELECT part of the query, see
* IDatabase::select() for details.
*
* @return bool
+ * @throws DBError
*/
public function insertSelect( $destTable, $srcTable, $varMap, $conds,
$fname = __METHOD__,
@@ -1338,12 +1388,15 @@ interface IDatabase {
* Determines how long the server has been up
*
* @return int
+ * @throws DBError
*/
public function getServerUptime();
/**
* Determines if the last failure was due to a deadlock
*
+ * Note that during a deadlock, the prior transaction will have been lost
+ *
* @return bool
*/
public function wasDeadlock();
@@ -1351,17 +1404,21 @@ interface IDatabase {
/**
* Determines if the last failure was due to a lock timeout
*
+ * Note that during a lock wait timeout, the prior transaction will have been lost
+ *
* @return bool
*/
public function wasLockTimeout();
/**
- * Determines if the last query error was due to a dropped connection and should
- * be dealt with by pinging the connection and reissuing the query.
+ * Determines if the last query error was due to a dropped connection
+ *
+ * Note that during a connection loss, the prior transaction will have been lost
*
* @return bool
+ * @since 1.31
*/
- public function wasErrorReissuable();
+ public function wasConnectionLoss();
/**
* Determines if the last failure was due to the database being read-only.
@@ -1371,6 +1428,15 @@ interface IDatabase {
public function wasReadOnlyError();
/**
+ * Determines if the last query error was due to something outside of the query itself
+ *
+ * Note that the transaction may have been lost, discarding prior writes and results
+ *
+ * @return bool
+ */
+ public function wasErrorReissuable();
+
+ /**
* Wait for the replica DB to catch up to a given master position
*
* @param DBMasterPos $pos
@@ -1378,13 +1444,15 @@ interface IDatabase {
* @return int|null Zero if the replica DB was past that position already,
* greater than zero if we waited for some period of time, less than
* zero if it timed out, and null on error
+ * @throws DBError
*/
public function masterPosWait( DBMasterPos $pos, $timeout );
/**
* Get the replication position of this replica DB
*
- * @return DBMasterPos|bool False if this is not a replica DB.
+ * @return DBMasterPos|bool False if this is not a replica DB
+ * @throws DBError
*/
public function getReplicaPos();
@@ -1392,6 +1460,7 @@ interface IDatabase {
* Get the position of this master
*
* @return DBMasterPos|bool False if this is not a master
+ * @throws DBError
*/
public function getMasterPos();
@@ -1404,11 +1473,13 @@ interface IDatabase {
/**
* Run a callback as soon as the current transaction commits or rolls back.
* An error is thrown if no transaction is pending. Queries in the function will run in
- * AUTO-COMMIT mode unless there are begin() calls. Callbacks must commit any transactions
+ * AUTOCOMMIT mode unless there are begin() calls. Callbacks must commit any transactions
* that they begin.
*
* This is useful for combining cooperative locks and DB transactions.
*
+ * @note: do not assume that *other* IDatabase instances will be AUTOCOMMIT mode
+ *
* The callback takes one argument:
* - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_ROLLBACK)
*
@@ -1422,16 +1493,24 @@ interface IDatabase {
/**
* Run a callback as soon as there is no transaction pending.
* If there is a transaction and it is rolled back, then the callback is cancelled.
- * Queries in the function will run in AUTO-COMMIT mode unless there are begin() calls.
+ *
+ * When transaction round mode (DBO_TRX) is set, the callback will run at the end
+ * of the round, just after all peer transactions COMMIT. If the transaction round
+ * is rolled back, then the callback is cancelled.
+ *
+ * Queries in the function will run in AUTOCOMMIT mode unless there are begin() calls.
* Callbacks must commit any transactions that they begin.
*
* This is useful for updates to different systems or when separate transactions are needed.
* For example, one might want to enqueue jobs into a system outside the database, but only
* after the database is updated so that the jobs will see the data when they actually run.
- * It can also be used for updates that easily cause deadlocks if locks are held too long.
+ * It can also be used for updates that easily suffer from lock timeouts and deadlocks,
+ * but where atomicity is not essential.
*
* Updates will execute in the order they were enqueued.
*
+ * @note: do not assume that *other* IDatabase instances will be AUTOCOMMIT mode
+ *
* The callback takes one argument:
* - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_IDLE)
*
@@ -1444,10 +1523,15 @@ interface IDatabase {
/**
* Run a callback before the current transaction commits or now if there is none.
* If there is a transaction and it is rolled back, then the callback is cancelled.
+ *
+ * When transaction round mode (DBO_TRX) is set, the callback will run at the end
+ * of the round, just before all peer transactions COMMIT. If the transaction round
+ * is rolled back, then the callback is cancelled.
+ *
* Callbacks must not start nor commit any transactions. If no transaction is active,
* then a transaction will wrap the callback.
*
- * This is useful for updates that easily cause deadlocks if locks are held too long
+ * This is useful for updates that easily suffer from lock timeouts and deadlocks,
* but where atomicity is strongly desired for these updates and some related updates.
*
* Updates will execute in the order they were enqueued.
@@ -1476,27 +1560,80 @@ interface IDatabase {
public function setTransactionListener( $name, callable $callback = null );
/**
- * Begin an atomic section of statements
+ * Begin an atomic section of SQL statements
*
- * If a transaction has been started already, just keep track of the given
- * section name to make sure the transaction is not committed pre-maturely.
- * This function can be used in layers (with sub-sections), so use a stack
- * to keep track of the different atomic sections. If there is no transaction,
- * start one implicitly.
+ * Start an implicit transaction if no transaction is already active, set a savepoint
+ * (if $cancelable is ATOMIC_CANCELABLE), and track the given section name to enforce
+ * that the transaction is not committed prematurely. The end of the section must be
+ * signified exactly once, either by endAtomic() or cancelAtomic(). Sections can have
+ * have layers of inner sections (sub-sections), but all sections must be ended in order
+ * of innermost to outermost. Transactions cannot be started or committed until all
+ * atomic sections are closed.
*
- * The goal of this function is to create an atomic section of SQL queries
- * without having to start a new transaction if it already exists.
+ * ATOMIC_CANCELABLE is useful when the caller needs to handle specific failure cases
+ * by discarding the section's writes. This should not be used for failures when:
+ * - upsert() could easily be used instead
+ * - insert() with IGNORE could easily be used instead
+ * - select() with FOR UPDATE could be checked before issuing writes instead
+ * - The failure is from code that runs after the first write but doesn't need to
+ * - The failures are from contention solvable via onTransactionPreCommitOrIdle()
+ * - The failures are deadlocks; the RDBMs usually discard the whole transaction
*
- * All atomic levels *must* be explicitly closed using IDatabase::endAtomic(),
- * and any database transactions cannot be began or committed until all atomic
- * levels are closed. There is no such thing as implicitly opening or closing
- * an atomic section.
+ * @note: callers must use additional measures for situations involving two or more
+ * (peer) transactions (e.g. updating two database servers at once). The transaction
+ * and savepoint logic of this method only applies to this specific IDatabase instance.
+ *
+ * Example usage:
+ * @code
+ * // Start a transaction if there isn't one already
+ * $dbw->startAtomic( __METHOD__ );
+ * // Serialize these thread table updates
+ * $dbw->select( 'thread', '1', [ 'td_id' => $tid ], __METHOD__, 'FOR UPDATE' );
+ * // Add a new comment for the thread
+ * $dbw->insert( 'comment', $row, __METHOD__ );
+ * $cid = $db->insertId();
+ * // Update thread reference to last comment
+ * $dbw->update( 'thread', [ 'td_latest' => $cid ], [ 'td_id' => $tid ], __METHOD__ );
+ * // Demark the end of this conceptual unit of updates
+ * $dbw->endAtomic( __METHOD__ );
+ * @endcode
+ *
+ * Example usage (atomic changes that might have to be discarded):
+ * @code
+ * // Start a transaction if there isn't one already
+ * $sectionId = $dbw->startAtomic( __METHOD__, $dbw::ATOMIC_CANCELABLE );
+ * // Create new record metadata row
+ * $dbw->insert( 'records', $row, __METHOD__ );
+ * // Figure out where to store the data based on the new row's ID
+ * $path = $recordDirectory . '/' . $dbw->insertId();
+ * // Write the record data to the storage system
+ * $status = $fileBackend->create( [ 'dst' => $path, 'content' => $data ] );
+ * if ( $status->isOK() ) {
+ * // Try to cleanup files orphaned by transaction rollback
+ * $dbw->onTransactionResolution(
+ * function ( $type ) use ( $fileBackend, $path ) {
+ * if ( $type === IDatabase::TRIGGER_ROLLBACK ) {
+ * $fileBackend->delete( [ 'src' => $path ] );
+ * }
+ * },
+ * __METHOD__
+ * );
+ * // Demark the end of this conceptual unit of updates
+ * $dbw->endAtomic( __METHOD__ );
+ * } else {
+ * // Discard these writes from the transaction (preserving prior writes)
+ * $dbw->cancelAtomic( __METHOD__, $sectionId );
+ * }
+ * @endcode
*
* @since 1.23
* @param string $fname
+ * @param string $cancelable Pass self::ATOMIC_CANCELABLE to use a
+ * savepoint and enable self::cancelAtomic() for this section.
+ * @return AtomicSectionIdentifier section ID token
* @throws DBError
*/
- public function startAtomic( $fname = __METHOD__ );
+ public function startAtomic( $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE );
/**
* Ends an atomic section of SQL statements
@@ -1512,33 +1649,107 @@ interface IDatabase {
public function endAtomic( $fname = __METHOD__ );
/**
- * Run a callback to do an atomic set of updates for this database
+ * Cancel an atomic section of SQL statements
+ *
+ * This will roll back only the statements executed since the start of the
+ * most recent atomic section, and close that section. If a transaction was
+ * open before the corresponding startAtomic() call, any statements before
+ * that call are *not* rolled back and the transaction remains open. If the
+ * corresponding startAtomic() implicitly started a transaction, that
+ * transaction is rolled back.
+ *
+ * @note: callers must use additional measures for situations involving two or more
+ * (peer) transactions (e.g. updating two database servers at once). The transaction
+ * and savepoint logic of startAtomic() are bound to specific IDatabase instances.
+ *
+ * Note that a call to IDatabase::rollback() will also roll back any open atomic sections.
+ *
+ * @note As a micro-optimization to save a few DB calls, this method may only
+ * be called when startAtomic() was called with the ATOMIC_CANCELABLE flag.
+ * @since 1.31
+ * @see IDatabase::startAtomic
+ * @param string $fname
+ * @param AtomicSectionIdentifier $sectionId Section ID from startAtomic();
+ * passing this enables cancellation of unclosed nested sections [optional]
+ * @throws DBError
+ */
+ public function cancelAtomic( $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null );
+
+ /**
+ * Perform an atomic section of reversable SQL statements from a callback
*
* The $callback takes the following arguments:
* - This database object
* - The value of $fname
*
- * If any exception occurs in the callback, then rollback() will be called and the error will
- * be re-thrown. It may also be that the rollback itself fails with an exception before then.
- * In any case, such errors are expected to terminate the request, without any outside caller
- * attempting to catch errors and commit anyway. Note that any rollback undoes all prior
- * atomic section and uncommitted updates, which trashes the current request, requiring an
- * error to be displayed.
+ * This will execute the callback inside a pair of startAtomic()/endAtomic() calls.
+ * If any exception occurs during execution of the callback, it will be handled as follows:
+ * - If $cancelable is ATOMIC_CANCELABLE, cancelAtomic() will be called to back out any
+ * (and only) statements executed during the atomic section. If that succeeds, then the
+ * exception will be re-thrown; if it fails, then a different exception will be thrown
+ * and any further query attempts will fail until rollback() is called.
+ * - If $cancelable is ATOMIC_NOT_CANCELABLE, cancelAtomic() will be called to mark the
+ * end of the section and the error will be re-thrown. Any further query attempts will
+ * fail until rollback() is called.
+ *
+ * This method is convenient for letting calls to the caller of this method be wrapped
+ * in a try/catch blocks for exception types that imply that the caller failed but was
+ * able to properly discard the changes it made in the transaction. This method can be
+ * an alternative to explicit calls to startAtomic()/endAtomic()/cancelAtomic().
+ *
+ * Example usage, "RecordStore::save" method:
+ * @code
+ * $dbw->doAtomicSection( __METHOD__, function ( $dbw ) use ( $record ) {
+ * // Create new record metadata row
+ * $dbw->insert( 'records', $record->toArray(), __METHOD__ );
+ * // Figure out where to store the data based on the new row's ID
+ * $path = $this->recordDirectory . '/' . $dbw->insertId();
+ * // Write the record data to the storage system;
+ * // blob store throughs StoreFailureException on failure
+ * $this->blobStore->create( $path, $record->getJSON() );
+ * // Try to cleanup files orphaned by transaction rollback
+ * $dbw->onTransactionResolution(
+ * function ( $type ) use ( $path ) {
+ * if ( $type === IDatabase::TRIGGER_ROLLBACK ) {
+ * $this->blobStore->delete( $path );
+ * }
+ * },
+ * __METHOD__
+ * );
+ * }, $dbw::ATOMIC_CANCELABLE );
+ * @endcode
*
- * This can be an alternative to explicit startAtomic()/endAtomic() calls.
+ * Example usage, caller of the "RecordStore::save" method:
+ * @code
+ * $dbw->startAtomic( __METHOD__ );
+ * // ...various SQL writes happen...
+ * try {
+ * $recordStore->save( $record );
+ * } catch ( StoreFailureException $e ) {
+ * // ...various SQL writes happen...
+ * }
+ * // ...various SQL writes happen...
+ * $dbw->endAtomic( __METHOD__ );
+ * @endcode
*
* @see Database::startAtomic
* @see Database::endAtomic
+ * @see Database::cancelAtomic
*
* @param string $fname Caller name (usually __METHOD__)
* @param callable $callback Callback that issues DB updates
+ * @param string $cancelable Pass self::ATOMIC_CANCELABLE to use a
+ * savepoint and enable self::cancelAtomic() for this section.
* @return mixed $res Result of the callback (since 1.28)
* @throws DBError
* @throws RuntimeException
- * @throws UnexpectedValueException
- * @since 1.27
+ * @since 1.27; prior to 1.31 this did a rollback() instead of
+ * cancelAtomic(), and assumed no callers up the stack would ever try to
+ * catch the exception.
*/
- public function doAtomicSection( $fname, callable $callback );
+ public function doAtomicSection(
+ $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
+ );
/**
* Begin a transaction. If a transaction is already in progress,
@@ -1580,7 +1791,7 @@ interface IDatabase {
* Only set the flush flag if you are sure that these warnings are not applicable,
* and no explicit transactions are open.
*
- * @throws DBUnexpectedError
+ * @throws DBError
*/
public function commit( $fname = __METHOD__, $flush = '' );
@@ -1594,12 +1805,14 @@ interface IDatabase {
* throwing an Exception is preferrable, using a pre-installed error handler to trigger
* rollback (in any case, failure to issue COMMIT will cause rollback server-side).
*
+ * Query, connection, and onTransaction* callback errors will be suppressed and logged.
+ *
* @param string $fname Calling function name
* @param string $flush Flush flag, set to a situationally valid IDatabase::FLUSHING_*
* constant to disable warnings about calling rollback when no transaction is in
* progress. This will silently break any ongoing explicit transaction. Only set the
* flush flag if you are sure that it is safe to ignore these warnings in your context.
- * @throws DBUnexpectedError
+ * @throws DBError
* @since 1.23 Added $flush parameter
*/
public function rollback( $fname = __METHOD__, $flush = '' );
@@ -1613,22 +1826,12 @@ interface IDatabase {
* useful to call on a replica DB after waiting on replication to catch up to the master.
*
* @param string $fname Calling function name
- * @throws DBUnexpectedError
+ * @throws DBError
* @since 1.28
*/
public function flushSnapshot( $fname = __METHOD__ );
/**
- * List all tables on the database
- *
- * @param string $prefix Only show tables with this prefix, e.g. mw_
- * @param string $fname Calling function name
- * @throws DBError
- * @return array
- */
- public function listTables( $prefix = null, $fname = __METHOD__ );
-
- /**
* Convert a timestamp in one of the formats accepted by wfTimestamp()
* to the format used for inserting into timestamp fields in this DBMS.
*
@@ -1665,13 +1868,12 @@ interface IDatabase {
public function ping( &$rtt = null );
/**
- * Get replica DB lag. Currently supported only by MySQL.
+ * Get the amount of replication lag for this database server
*
- * Note that this function will generate a fatal error on many
- * installations. Most callers should use LoadBalancer::safeGetLag()
- * instead.
+ * Callers should avoid using this method while a transaction is active
*
* @return int|bool Database replication lag in seconds or false on error
+ * @throws DBError
*/
public function getLag();
@@ -1682,10 +1884,11 @@ interface IDatabase {
* This is useful when transactions might use snapshot isolation
* (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
* is this lag plus transaction duration. If they don't, it is still
- * safe to be pessimistic. In AUTO-COMMIT mode, this still gives an
+ * safe to be pessimistic. In AUTOCOMMIT mode, this still gives an
* indication of the staleness of subsequent reads.
*
* @return array ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
+ * @throws DBError
* @since 1.27
*/
public function getSessionLagStatus();
@@ -1727,6 +1930,7 @@ interface IDatabase {
*
* @param array $options
* @return void
+ * @throws DBError
*/
public function setSessionOptions( array $options );
@@ -1740,11 +1944,12 @@ interface IDatabase {
public function setSchemaVars( $vars );
/**
- * Check to see if a named lock is available (non-blocking)
+ * Check to see if a named lock is not locked by any thread (non-blocking)
*
* @param string $lockName Name of lock to poll
* @param string $method Name of method calling us
* @return bool
+ * @throws DBError
* @since 1.20
*/
public function lockIsFree( $lockName, $method );
@@ -1758,6 +1963,7 @@ interface IDatabase {
* @param string $method Name of the calling method
* @param int $timeout Acquisition timeout in seconds
* @return bool
+ * @throws DBError
*/
public function lock( $lockName, $method, $timeout = 5 );
@@ -1770,8 +1976,10 @@ interface IDatabase {
* @param string $method Name of the calling method
*
* @return int Returns 1 if the lock was released, 0 if the lock was not established
- * by this thread (in which case the lock is not released), and NULL if the named
- * lock did not exist
+ * by this thread (in which case the lock is not released), and NULL if the named lock
+ * did not exist
+ *
+ * @throws DBError
*/
public function unlock( $lockName, $method );
@@ -1793,7 +2001,7 @@ interface IDatabase {
* @param string $fname Name of the calling method
* @param int $timeout Acquisition timeout in seconds
* @return ScopedCallback|null
- * @throws DBUnexpectedError
+ * @throws DBError
* @since 1.27
*/
public function getScopedLockAndFlush( $lockKey, $fname, $timeout );
@@ -1863,6 +2071,21 @@ interface IDatabase {
* @since 1.28
*/
public function setTableAliases( array $aliases );
+
+ /**
+ * Convert certain index names to alternative names before querying the DB
+ *
+ * Note that this applies to indexes regardless of the table they belong to.
+ *
+ * This can be employed when an index was renamed X => Y in code, but the new Y-named
+ * indexes were not yet built on all DBs. After all the Y-named ones are added by the DBA,
+ * the aliases can be removed, and then the old X-named indexes dropped.
+ *
+ * @param string[] $aliases
+ * @return mixed
+ * @since 1.31
+ */
+ public function setIndexAliases( array $aliases );
}
class_alias( IDatabase::class, 'IDatabase' );
diff --git a/www/wiki/includes/libs/rdbms/database/IMaintainableDatabase.php b/www/wiki/includes/libs/rdbms/database/IMaintainableDatabase.php
index fbc2774b..18e3cbbc 100644
--- a/www/wiki/includes/libs/rdbms/database/IMaintainableDatabase.php
+++ b/www/wiki/includes/libs/rdbms/database/IMaintainableDatabase.php
@@ -20,7 +20,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup Database
*/
namespace Wikimedia\Rdbms;
@@ -63,8 +62,8 @@ interface IMaintainableDatabase extends IDatabase {
* This is handy when you need to construct SQL for joins
*
* Example:
- * extract( $dbr->tableNames( 'user', 'watchlist' ) );
- * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+ * list( $user, $watchlist ) = $dbr->tableNames( 'user', 'watchlist' ) );
+ * $sql = "SELECT wl_namespace, wl_title FROM $watchlist, $user
* WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
*
* @return array
@@ -276,6 +275,37 @@ interface IMaintainableDatabase extends IDatabase {
* @since 1.29
*/
public function unlockTables( $method );
+
+ /**
+ * List all tables on the database
+ *
+ * @param string $prefix Only show tables with this prefix, e.g. mw_
+ * @param string $fname Calling function name
+ * @throws DBError
+ * @return array
+ */
+ public function listTables( $prefix = null, $fname = __METHOD__ );
+
+ /**
+ * Determines if a given index is unique
+ *
+ * @param string $table
+ * @param string $index
+ *
+ * @return bool
+ */
+ public function indexUnique( $table, $index );
+
+ /**
+ * mysql_fetch_field() wrapper
+ * Returns false if the field doesn't exist
+ *
+ * @param string $table Table name
+ * @param string $field Field name
+ *
+ * @return Field
+ */
+ public function fieldInfo( $table, $field );
}
class_alias( IMaintainableDatabase::class, 'IMaintainableDatabase' );
diff --git a/www/wiki/includes/libs/rdbms/database/MaintainableDBConnRef.php b/www/wiki/includes/libs/rdbms/database/MaintainableDBConnRef.php
index 6c94eb9a..ff4b0509 100644
--- a/www/wiki/includes/libs/rdbms/database/MaintainableDBConnRef.php
+++ b/www/wiki/includes/libs/rdbms/database/MaintainableDBConnRef.php
@@ -80,6 +80,18 @@ class MaintainableDBConnRef extends DBConnRef implements IMaintainableDatabase {
public function unlockTables( $method ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
+
+ public function indexUnique( $table, $index ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function listTables( $prefix = null, $fname = __METHOD__ ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function fieldInfo( $table, $field ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
}
class_alias( MaintainableDBConnRef::class, 'MaintainableDBConnRef' );
diff --git a/www/wiki/includes/libs/rdbms/database/position/DBMasterPos.php b/www/wiki/includes/libs/rdbms/database/position/DBMasterPos.php
index 2f79ea9a..28d2a1b8 100644
--- a/www/wiki/includes/libs/rdbms/database/position/DBMasterPos.php
+++ b/www/wiki/includes/libs/rdbms/database/position/DBMasterPos.php
@@ -2,12 +2,14 @@
namespace Wikimedia\Rdbms;
+use Serializable;
+
/**
* An object representing a master or replica DB position in a replicated setup.
*
* The implementation details of this opaque type are up to the database subclass.
*/
-interface DBMasterPos {
+interface DBMasterPos extends Serializable {
/**
* @return float UNIX timestamp
* @since 1.25
diff --git a/www/wiki/includes/libs/rdbms/database/position/MySQLMasterPos.php b/www/wiki/includes/libs/rdbms/database/position/MySQLMasterPos.php
index 0657cf3d..54eca79a 100644
--- a/www/wiki/includes/libs/rdbms/database/position/MySQLMasterPos.php
+++ b/www/wiki/includes/libs/rdbms/database/position/MySQLMasterPos.php
@@ -3,6 +3,7 @@
namespace Wikimedia\Rdbms;
use InvalidArgumentException;
+use UnexpectedValueException;
/**
* DBMasterPos class for MySQL/MariaDB
@@ -11,57 +12,115 @@ use InvalidArgumentException;
* - Binlog-based usage assumes single-source replication and non-hierarchical replication.
* - GTID-based usage allows getting/syncing with multi-source replication. It is assumed
* that GTID sets are complete (e.g. include all domains on the server).
+ *
+ * @see https://mariadb.com/kb/en/library/gtid/
+ * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
*/
class MySQLMasterPos implements DBMasterPos {
- /** @var string Binlog file */
- public $file;
- /** @var int Binglog file position */
- public $pos;
- /** @var string[] GTID list */
- public $gtids = [];
+ /** @var int One of (BINARY_LOG, GTID_MYSQL, GTID_MARIA) */
+ private $style;
+ /** @var string|null Base name of all Binary Log files */
+ private $binLog;
+ /** @var int[]|null Binary Log position tuple (index number, event number) */
+ private $logPos;
+ /** @var string[] Map of (server_uuid/gtid_domain_id => GTID) */
+ private $gtids = [];
+ /** @var int|null Active GTID domain ID */
+ private $activeDomain;
+ /** @var int|null ID of the server were DB writes originate */
+ private $activeServerId;
+ /** @var string|null UUID of the server were DB writes originate */
+ private $activeServerUUID;
/** @var float UNIX timestamp */
- public $asOfTime = 0.0;
+ private $asOfTime = 0.0;
+
+ const BINARY_LOG = 'binary-log';
+ const GTID_MARIA = 'gtid-maria';
+ const GTID_MYSQL = 'gtid-mysql';
+
+ /** @var int Key name of the binary log index number of a position tuple */
+ const CORD_INDEX = 0;
+ /** @var int Key name of the binary log event number of a position tuple */
+ const CORD_EVENT = 1;
/**
- * @param string $file Binlog file name
- * @param int $pos Binlog position
- * @param string $gtid Comma separated GTID set [optional]
+ * @param string $position One of (comma separated GTID list, <binlog file>/<integer>)
+ * @param float $asOfTime UNIX timestamp
*/
- function __construct( $file, $pos, $gtid = '' ) {
- $this->file = $file;
- $this->pos = $pos;
- $this->gtids = array_map( 'trim', explode( ',', $gtid ) );
- $this->asOfTime = microtime( true );
+ public function __construct( $position, $asOfTime ) {
+ $this->init( $position, $asOfTime );
}
/**
- * @return string <binlog file>/<position>, e.g db1034-bin.000976/843431247
+ * @param string $position
+ * @param float $asOfTime
*/
- function __toString() {
- return "{$this->file}/{$this->pos}";
+ protected function init( $position, $asOfTime ) {
+ $m = [];
+ if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', $position, $m ) ) {
+ $this->binLog = $m[1]; // ideally something like host name
+ $this->logPos = [ self::CORD_INDEX => (int)$m[2], self::CORD_EVENT => (int)$m[3] ];
+ $this->style = self::BINARY_LOG;
+ } else {
+ $gtids = array_filter( array_map( 'trim', explode( ',', $position ) ) );
+ foreach ( $gtids as $gtid ) {
+ $components = self::parseGTID( $gtid );
+ if ( !$components ) {
+ throw new InvalidArgumentException( "Invalid GTID '$gtid'." );
+ }
+
+ list( $domain, $pos ) = $components;
+ if ( isset( $this->gtids[$domain] ) ) {
+ // For MySQL, handle the case where some past issue caused a gap in the
+ // executed GTID set, e.g. [last_purged+1,N-1] and [N+1,N+2+K]. Ignore the
+ // gap by using the GTID with the highest ending sequence number.
+ list( , $otherPos ) = self::parseGTID( $this->gtids[$domain] );
+ if ( $pos > $otherPos ) {
+ $this->gtids[$domain] = $gtid;
+ }
+ } else {
+ $this->gtids[$domain] = $gtid;
+ }
+
+ if ( is_int( $domain ) ) {
+ $this->style = self::GTID_MARIA; // gtid_domain_id
+ } else {
+ $this->style = self::GTID_MYSQL; // server_uuid
+ }
+ }
+ if ( !$this->gtids ) {
+ throw new InvalidArgumentException( "GTID set cannot be empty." );
+ }
+ }
+
+ $this->asOfTime = $asOfTime;
}
- function asOfTime() {
+ public function asOfTime() {
return $this->asOfTime;
}
- function hasReached( DBMasterPos $pos ) {
+ public function hasReached( DBMasterPos $pos ) {
if ( !( $pos instanceof self ) ) {
throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
}
// Prefer GTID comparisons, which work with multi-tier replication
- $thisPosByDomain = $this->getGtidCoordinates();
- $thatPosByDomain = $pos->getGtidCoordinates();
+ $thisPosByDomain = $this->getActiveGtidCoordinates();
+ $thatPosByDomain = $pos->getActiveGtidCoordinates();
if ( $thisPosByDomain && $thatPosByDomain ) {
- $reached = true;
- // Check that this has positions GTE all of those in $pos for all domains in $pos
+ $comparisons = [];
+ // Check that this has positions reaching those in $pos for all domains in common
foreach ( $thatPosByDomain as $domain => $thatPos ) {
- $thisPos = isset( $thisPosByDomain[$domain] ) ? $thisPosByDomain[$domain] : -1;
- $reached = $reached && ( $thatPos <= $thisPos );
+ if ( isset( $thisPosByDomain[$domain] ) ) {
+ $comparisons[] = ( $thatPos <= $thisPosByDomain[$domain] );
+ }
}
-
- return $reached;
+ // Check that $this has a GTID for at least one domain also in $pos; due to MariaDB
+ // quirks, prior master switch-overs may result in inactive garbage GTIDs that cannot
+ // be cleaned up. Assume that the domains in both this and $pos cover the relevant
+ // active channels.
+ return ( $comparisons && !in_array( false, $comparisons, true ) );
}
// Fallback to the binlog file comparisons
@@ -75,17 +134,20 @@ class MySQLMasterPos implements DBMasterPos {
return false;
}
- function channelsMatch( DBMasterPos $pos ) {
+ public function channelsMatch( DBMasterPos $pos ) {
if ( !( $pos instanceof self ) ) {
throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
}
// Prefer GTID comparisons, which work with multi-tier replication
- $thisPosDomains = array_keys( $this->getGtidCoordinates() );
- $thatPosDomains = array_keys( $pos->getGtidCoordinates() );
+ $thisPosDomains = array_keys( $this->getActiveGtidCoordinates() );
+ $thatPosDomains = array_keys( $pos->getActiveGtidCoordinates() );
if ( $thisPosDomains && $thatPosDomains ) {
- // Check that this has GTIDs for all domains in $pos
- return !array_diff( $thatPosDomains, $thisPosDomains );
+ // Check that $this has a GTID for at least one domain also in $pos; due to MariaDB
+ // quirks, prior master switch-overs may result in inactive garbage GTIDs that cannot
+ // easily be cleaned up. Assume that the domains in both this and $pos cover the
+ // relevant active channels.
+ return array_intersect( $thatPosDomains, $thisPosDomains ) ? true : false;
}
// Fallback to the binlog file comparisons
@@ -96,42 +158,169 @@ class MySQLMasterPos implements DBMasterPos {
}
/**
- * @note: this returns false for multi-source replication GTID sets
+ * @return string|null Base name of binary log files
+ * @since 1.31
+ */
+ public function getLogName() {
+ return $this->gtids ? null : $this->binLog;
+ }
+
+ /**
+ * @return int[]|null Tuple of (binary log file number, event number)
+ * @since 1.31
+ */
+ public function getLogPosition() {
+ return $this->gtids ? null : $this->logPos;
+ }
+
+ /**
+ * @return string|null Name of the binary log file for this position
+ * @since 1.31
+ */
+ public function getLogFile() {
+ return $this->gtids ? null : "{$this->binLog}.{$this->logPos[self::CORD_INDEX]}";
+ }
+
+ /**
+ * @return string[] Map of (server_uuid/gtid_domain_id => GTID)
+ * @since 1.31
+ */
+ public function getGTIDs() {
+ return $this->gtids;
+ }
+
+ /**
+ * @param int|null $id @@gtid_domain_id of the active replication stream
+ * @since 1.31
+ */
+ public function setActiveDomain( $id ) {
+ $this->activeDomain = (int)$id;
+ }
+
+ /**
+ * @param int|null $id @@server_id of the server were writes originate
+ * @since 1.31
+ */
+ public function setActiveOriginServerId( $id ) {
+ $this->activeServerId = (int)$id;
+ }
+
+ /**
+ * @param string|null $id @@server_uuid of the server were writes originate
+ * @since 1.31
+ */
+ public function setActiveOriginServerUUID( $id ) {
+ $this->activeServerUUID = $id;
+ }
+
+ /**
+ * @param MySQLMasterPos $pos
+ * @param MySQLMasterPos $refPos
+ * @return string[] List of GTIDs from $pos that have domains in $refPos
+ * @since 1.31
+ */
+ public static function getCommonDomainGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) {
+ return array_values(
+ array_intersect_key( $pos->gtids, $refPos->getActiveGtidCoordinates() )
+ );
+ }
+
+ /**
* @see https://mariadb.com/kb/en/mariadb/gtid
* @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
- * @return array Map of (domain => integer position) or false
+ * @return array Map of (server_uuid/gtid_domain_id => integer position); possibly empty
*/
- protected function getGtidCoordinates() {
+ protected function getActiveGtidCoordinates() {
$gtidInfos = [];
- foreach ( $this->gtids as $gtid ) {
- $m = [];
- // MariaDB style: <domain>-<server id>-<sequence number>
- if ( preg_match( '!^(\d+)-\d+-(\d+)$!', $gtid, $m ) ) {
- $gtidInfos[(int)$m[1]] = (int)$m[2];
- // MySQL style: <UUID domain>:<sequence number>
- } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(\d+)$!', $gtid, $m ) ) {
- $gtidInfos[$m[1]] = (int)$m[2];
- } else {
- $gtidInfos = [];
- break; // unrecognized GTID
+
+ foreach ( $this->gtids as $domain => $gtid ) {
+ list( $domain, $pos, $server ) = self::parseGTID( $gtid );
+
+ $ignore = false;
+ // Filter out GTIDs from non-active replication domains
+ if ( $this->style === self::GTID_MARIA && $this->activeDomain !== null ) {
+ $ignore |= ( $domain !== $this->activeDomain );
+ }
+ // Likewise for GTIDs from non-active replication origin servers
+ if ( $this->style === self::GTID_MARIA && $this->activeServerId !== null ) {
+ $ignore |= ( $server !== $this->activeServerId );
+ } elseif ( $this->style === self::GTID_MYSQL && $this->activeServerUUID !== null ) {
+ $ignore |= ( $server !== $this->activeServerUUID );
}
+ if ( !$ignore ) {
+ $gtidInfos[$domain] = $pos;
+ }
}
return $gtidInfos;
}
/**
+ * @param string $id GTID
+ * @return array|null [domain ID or server UUID, sequence number, server ID/UUID] or null
+ */
+ protected static function parseGTID( $id ) {
+ $m = [];
+ if ( preg_match( '!^(\d+)-(\d+)-(\d+)$!', $id, $m ) ) {
+ // MariaDB style: <domain>-<server id>-<sequence number>
+ return [ (int)$m[1], (int)$m[3], (int)$m[2] ];
+ } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(?:\d+-|)(\d+)$!', $id, $m ) ) {
+ // MySQL style: <server UUID>:<sequence number>-<sequence number>
+ // Normally, the first number should reflect the point (gtid_purged) where older
+ // binary logs where purged to save space. When doing comparisons, it may as well
+ // be 1 in that case. Assume that this is generally the situation.
+ return [ $m[1], (int)$m[2], $m[1] ];
+ }
+
+ return null;
+ }
+
+ /**
* @see https://dev.mysql.com/doc/refman/5.7/en/show-master-status.html
* @see https://dev.mysql.com/doc/refman/5.7/en/show-slave-status.html
- * @return array|bool (binlog, (integer file number, integer position)) or false
+ * @return array|bool Map of (binlog:<string>, pos:(<integer>, <integer>)) or false
*/
protected function getBinlogCoordinates() {
- $m = [];
- if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', (string)$this, $m ) ) {
- return [ 'binlog' => $m[1], 'pos' => [ (int)$m[2], (int)$m[3] ] ];
+ return ( $this->binLog !== null && $this->logPos !== null )
+ ? [ 'binlog' => $this->binLog, 'pos' => $this->logPos ]
+ : false;
+ }
+
+ public function serialize() {
+ return serialize( [
+ 'position' => $this->__toString(),
+ 'activeDomain' => $this->activeDomain,
+ 'activeServerId' => $this->activeServerId,
+ 'activeServerUUID' => $this->activeServerUUID,
+ 'asOfTime' => $this->asOfTime
+ ] );
+ }
+
+ public function unserialize( $serialized ) {
+ $data = unserialize( $serialized );
+ if ( !is_array( $data ) ) {
+ throw new UnexpectedValueException( __METHOD__ . ": cannot unserialize position" );
}
- return false;
+ $this->init( $data['position'], $data['asOfTime'] );
+ if ( isset( $data['activeDomain'] ) ) {
+ $this->setActiveDomain( $data['activeDomain'] );
+ }
+ if ( isset( $data['activeServerId'] ) ) {
+ $this->setActiveOriginServerId( $data['activeServerId'] );
+ }
+ if ( isset( $data['activeServerUUID'] ) ) {
+ $this->setActiveOriginServerUUID( $data['activeServerUUID'] );
+ }
+ }
+
+ /**
+ * @return string GTID set or <binary log file>/<position> (e.g db1034-bin.000976/843431247)
+ */
+ public function __toString() {
+ return $this->gtids
+ ? implode( ',', $this->gtids )
+ : $this->getLogFile() . "/{$this->logPos[self::CORD_EVENT]}";
}
}
diff --git a/www/wiki/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php b/www/wiki/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php
index 298ec619..ba79be14 100644
--- a/www/wiki/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php
+++ b/www/wiki/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php
@@ -6,7 +6,7 @@ use stdClass;
class MssqlResultWrapper extends ResultWrapper {
/** @var int|null */
- private $mSeekTo = null;
+ private $seekTo = null;
/**
* @return stdClass|bool
@@ -14,10 +14,10 @@ class MssqlResultWrapper extends ResultWrapper {
public function fetchObject() {
$res = $this->result;
- if ( $this->mSeekTo !== null ) {
- $result = sqlsrv_fetch_object( $res, 'stdClass', [],
- SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo );
- $this->mSeekTo = null;
+ if ( $this->seekTo !== null ) {
+ $result = sqlsrv_fetch_object( $res, stdClass::class, [],
+ SQLSRV_SCROLL_ABSOLUTE, $this->seekTo );
+ $this->seekTo = null;
} else {
$result = sqlsrv_fetch_object( $res );
}
@@ -36,10 +36,10 @@ class MssqlResultWrapper extends ResultWrapper {
public function fetchRow() {
$res = $this->result;
- if ( $this->mSeekTo !== null ) {
+ if ( $this->seekTo !== null ) {
$result = sqlsrv_fetch_array( $res, SQLSRV_FETCH_BOTH,
- SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo );
- $this->mSeekTo = null;
+ SQLSRV_SCROLL_ABSOLUTE, $this->seekTo );
+ $this->seekTo = null;
} else {
$result = sqlsrv_fetch_array( $res );
}
@@ -70,7 +70,7 @@ class MssqlResultWrapper extends ResultWrapper {
}
// Unlike MySQL, the seek actually happens on the next access
- $this->mSeekTo = $row;
+ $this->seekTo = $row;
return true;
}
}
diff --git a/www/wiki/includes/libs/rdbms/database/utils/SavepointPostgres.php b/www/wiki/includes/libs/rdbms/database/utils/SavepointPostgres.php
index cf5060e4..edbcdfe1 100644
--- a/www/wiki/includes/libs/rdbms/database/utils/SavepointPostgres.php
+++ b/www/wiki/includes/libs/rdbms/database/utils/SavepointPostgres.php
@@ -27,6 +27,7 @@ use Psr\Log\LoggerInterface;
* Manage savepoints within a transaction
* @ingroup Database
* @since 1.19
+ * @deprecated since 1.31, use IDatabase::startAtomic() and such instead.
*/
class SavepointPostgres {
/** @var DatabasePostgres Establish a savepoint within a transaction */
diff --git a/www/wiki/includes/libs/rdbms/encasing/Blob.php b/www/wiki/includes/libs/rdbms/encasing/Blob.php
index e2d685cb..7bc3eacf 100644
--- a/www/wiki/includes/libs/rdbms/encasing/Blob.php
+++ b/www/wiki/includes/libs/rdbms/encasing/Blob.php
@@ -4,17 +4,17 @@ namespace Wikimedia\Rdbms;
class Blob implements IBlob {
/** @var string */
- protected $mData;
+ protected $data;
/**
* @param string $data
*/
public function __construct( $data ) {
- $this->mData = $data;
+ $this->data = $data;
}
public function fetch() {
- return $this->mData;
+ return $this->data;
}
}
diff --git a/www/wiki/includes/libs/rdbms/encasing/MssqlBlob.php b/www/wiki/includes/libs/rdbms/encasing/MssqlBlob.php
index aacdf402..8e68aba0 100644
--- a/www/wiki/includes/libs/rdbms/encasing/MssqlBlob.php
+++ b/www/wiki/includes/libs/rdbms/encasing/MssqlBlob.php
@@ -12,11 +12,11 @@ class MssqlBlob extends Blob {
if ( $data instanceof MssqlBlob ) {
return $data;
} elseif ( $data instanceof Blob ) {
- $this->mData = $data->fetch();
+ $this->data = $data->fetch();
} elseif ( is_array( $data ) && is_object( $data ) ) {
- $this->mData = serialize( $data );
+ $this->data = serialize( $data );
} else {
- $this->mData = $data;
+ $this->data = $data;
}
}
@@ -26,14 +26,14 @@ class MssqlBlob extends Blob {
* @return string
*/
public function fetch() {
- if ( $this->mData === null ) {
+ if ( $this->data === null ) {
return 'null';
}
$ret = '0x';
- $dataLength = strlen( $this->mData );
+ $dataLength = strlen( $this->data );
for ( $i = 0; $i < $dataLength; $i++ ) {
- $ret .= bin2hex( pack( 'C', ord( $this->mData[$i] ) ) );
+ $ret .= bin2hex( pack( 'C', ord( $this->data[$i] ) ) );
}
return $ret;
diff --git a/www/wiki/includes/libs/rdbms/encasing/Subquery.php b/www/wiki/includes/libs/rdbms/encasing/Subquery.php
new file mode 100644
index 00000000..fc118d0a
--- /dev/null
+++ b/www/wiki/includes/libs/rdbms/encasing/Subquery.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * This file deals with database interface functions
+ * and query specifics/optimisations.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+
+namespace Wikimedia\Rdbms;
+
+class Subquery {
+ /** @var string */
+ private $sql;
+
+ /**
+ * @param string $sql SQL query defining the table
+ */
+ public function __construct( $sql ) {
+ $this->sql = $sql;
+ }
+
+ /**
+ * @return string Original SQL query
+ */
+ public function __toString() {
+ return '(' . $this->sql . ')';
+ }
+}
diff --git a/www/wiki/includes/libs/rdbms/exception/DBError.php b/www/wiki/includes/libs/rdbms/exception/DBError.php
index 2f7499bc..aad219dd 100644
--- a/www/wiki/includes/libs/rdbms/exception/DBError.php
+++ b/www/wiki/includes/libs/rdbms/exception/DBError.php
@@ -21,13 +21,13 @@
namespace Wikimedia\Rdbms;
-use Exception;
+use RuntimeException;
/**
* Database error base class
* @ingroup Database
*/
-class DBError extends Exception {
+class DBError extends RuntimeException {
/** @var IDatabase|null */
public $db;
@@ -35,10 +35,11 @@ class DBError extends Exception {
* Construct a database error
* @param IDatabase $db Object which threw the error
* @param string $error A simple error message to be used for debugging
+ * @param \Exception|\Throwable|null $prev Previous exception
*/
- public function __construct( IDatabase $db = null, $error ) {
+ public function __construct( IDatabase $db = null, $error, $prev = null ) {
+ parent::__construct( $error, 0, $prev );
$this->db = $db;
- parent::__construct( $error );
}
}
diff --git a/www/wiki/includes/libs/rdbms/exception/DBExpectedError.php b/www/wiki/includes/libs/rdbms/exception/DBExpectedError.php
index 31d8c27d..7e46420d 100644
--- a/www/wiki/includes/libs/rdbms/exception/DBExpectedError.php
+++ b/www/wiki/includes/libs/rdbms/exception/DBExpectedError.php
@@ -16,14 +16,11 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup Database
*/
namespace Wikimedia\Rdbms;
use MessageSpecifier;
-use ILocalizedException;
-use Message;
/**
* Base class for the more common types of database errors. These are known to occur
@@ -32,12 +29,20 @@ use Message;
* @ingroup Database
* @since 1.23
*/
-class DBExpectedError extends DBError implements MessageSpecifier, ILocalizedException {
+class DBExpectedError extends DBError implements MessageSpecifier {
/** @var string[] Message parameters */
protected $params;
- public function __construct( IDatabase $db = null, $error, array $params = [] ) {
- parent::__construct( $db, $error );
+ /**
+ * @param IDatabase|null $db
+ * @param string $error
+ * @param array $params
+ * @param \Exception|\Throwable|null $prev
+ */
+ public function __construct(
+ IDatabase $db = null, $error, array $params = [], $prev = null
+ ) {
+ parent::__construct( $db, $error, $prev );
$this->params = $params;
}
@@ -48,14 +53,6 @@ class DBExpectedError extends DBError implements MessageSpecifier, ILocalizedExc
public function getParams() {
return $this->params;
}
-
- /**
- * @inheritDoc
- * @since 1.29
- */
- public function getMessageObject() {
- return Message::newFromSpecifier( $this );
- }
}
class_alias( DBExpectedError::class, 'DBExpectedError' );
diff --git a/www/wiki/includes/libs/rdbms/exception/DBQueryError.php b/www/wiki/includes/libs/rdbms/exception/DBQueryError.php
index a8ea3ade..e6870a7e 100644
--- a/www/wiki/includes/libs/rdbms/exception/DBQueryError.php
+++ b/www/wiki/includes/libs/rdbms/exception/DBQueryError.php
@@ -40,19 +40,22 @@ class DBQueryError extends DBExpectedError {
* @param int|string $errno
* @param string $sql
* @param string $fname
+ * @param string $message Optional message, intended for subclases (optional)
*/
- public function __construct( IDatabase $db, $error, $errno, $sql, $fname ) {
- if ( $db instanceof Database && $db->wasConnectionError( $errno ) ) {
- $message = "A connection error occured. \n" .
- "Query: $sql\n" .
- "Function: $fname\n" .
- "Error: $errno $error\n";
- } else {
- $message = "A database query error has occurred. Did you forget to run " .
- "your application's database schema updater after upgrading? \n" .
- "Query: $sql\n" .
- "Function: $fname\n" .
- "Error: $errno $error\n";
+ public function __construct( IDatabase $db, $error, $errno, $sql, $fname, $message = null ) {
+ if ( $message === null ) {
+ if ( $db instanceof Database && $db->wasConnectionError( $errno ) ) {
+ $message = "A connection error occured. \n" .
+ "Query: $sql\n" .
+ "Function: $fname\n" .
+ "Error: $errno $error\n";
+ } else {
+ $message = "A database query error has occurred. Did you forget to run " .
+ "your application's database schema updater after upgrading? \n" .
+ "Query: $sql\n" .
+ "Function: $fname\n" .
+ "Error: $errno $error\n";
+ }
}
parent::__construct( $db, $message );
diff --git a/www/wiki/includes/compat/CdbCompat.php b/www/wiki/includes/libs/rdbms/exception/DBQueryTimeoutError.php
index 0074cc96..ea91d955 100644
--- a/www/wiki/includes/compat/CdbCompat.php
+++ b/www/wiki/includes/libs/rdbms/exception/DBQueryTimeoutError.php
@@ -16,30 +16,23 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
+ * @ingroup Database
*/
-/***
- * This file contains a set of backwards-compatability class names
- * after the cdb functions were moved out into a separate library
- * and put under a proper namespace
- *
- * @since 1.25
- */
-
-/**
- * @deprecated since 1.25
- */
-abstract class CdbReader extends \Cdb\Reader {
-}
+namespace Wikimedia\Rdbms;
/**
- * @deprecated since 1.25
+ * Error thrown when a query times out
+ *
+ * @ingroup Database
*/
-abstract class CdbWriter extends \Cdb\Writer {
-}
+class DBQueryTimeoutError extends DBQueryError {
+ public function __construct( IDatabase $db, $error, $errno, $sql, $fname ) {
+ $message = "A database query timeout has occurred. \n" .
+ "Query: $sql\n" .
+ "Function: $fname\n" .
+ "Error: $errno $error\n";
-/**
- * @deprecated since 1.25
- */
-class CdbException extends \Cdb\Exception {
+ parent::__construct( $db, $error, $errno, $sql, $fname, $message );
+ }
}
diff --git a/www/wiki/includes/HtmlFormatter.php b/www/wiki/includes/libs/rdbms/exception/DBTransactionStateError.php
index 9bae8b5f..3e218482 100644
--- a/www/wiki/includes/HtmlFormatter.php
+++ b/www/wiki/includes/libs/rdbms/exception/DBTransactionStateError.php
@@ -1,8 +1,5 @@
<?php
/**
- * Stub for extensions that haven't switched to Composer-based version of this class
- * @todo: remove in 1.28
- *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
@@ -19,7 +16,13 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @deprecated since 1.27, use HtmlFormatter\HtmlFormatter
+ * @ingroup Database
+ */
+
+namespace Wikimedia\Rdbms;
+
+/**
+ * @ingroup Database
*/
-class HtmlFormatter extends HtmlFormatter\HtmlFormatter {
+class DBTransactionStateError extends DBTransactionError {
}
diff --git a/www/wiki/includes/libs/rdbms/field/PostgresField.php b/www/wiki/includes/libs/rdbms/field/PostgresField.php
index 600f34a4..53c3d335 100644
--- a/www/wiki/includes/libs/rdbms/field/PostgresField.php
+++ b/www/wiki/includes/libs/rdbms/field/PostgresField.php
@@ -38,30 +38,34 @@ AND attname=%s;
SQL;
$table = $db->remappedTableName( $table );
- $res = $db->query(
- sprintf( $q,
- $db->addQuotes( $db->getCoreSchema() ),
- $db->addQuotes( $table ),
- $db->addQuotes( $field )
- )
- );
- $row = $db->fetchObject( $res );
- if ( !$row ) {
- return null;
+ foreach ( $db->getCoreSchemas() as $schema ) {
+ $res = $db->query(
+ sprintf( $q,
+ $db->addQuotes( $schema ),
+ $db->addQuotes( $table ),
+ $db->addQuotes( $field )
+ )
+ );
+ $row = $db->fetchObject( $res );
+ if ( !$row ) {
+ continue;
+ }
+ $n = new PostgresField;
+ $n->type = $row->typname;
+ $n->nullable = ( $row->attnotnull == 'f' );
+ $n->name = $field;
+ $n->tablename = $table;
+ $n->max_length = $row->attlen;
+ $n->deferrable = ( $row->deferrable == 't' );
+ $n->deferred = ( $row->deferred == 't' );
+ $n->conname = $row->conname;
+ $n->has_default = ( $row->atthasdef === 't' );
+ $n->default = $row->adsrc;
+
+ return $n;
}
- $n = new PostgresField;
- $n->type = $row->typname;
- $n->nullable = ( $row->attnotnull == 'f' );
- $n->name = $field;
- $n->tablename = $table;
- $n->max_length = $row->attlen;
- $n->deferrable = ( $row->deferrable == 't' );
- $n->deferred = ( $row->deferred == 't' );
- $n->conname = $row->conname;
- $n->has_default = ( $row->atthasdef === 't' );
- $n->default = $row->adsrc;
- return $n;
+ return null;
}
function name() {
diff --git a/www/wiki/includes/libs/rdbms/lbfactory/ILBFactory.php b/www/wiki/includes/libs/rdbms/lbfactory/ILBFactory.php
index f6d080e4..1e8838e7 100644
--- a/www/wiki/includes/libs/rdbms/lbfactory/ILBFactory.php
+++ b/www/wiki/includes/libs/rdbms/lbfactory/ILBFactory.php
@@ -42,19 +42,20 @@ interface ILBFactory {
*
* @param array $conf Array with keys:
* - localDomain: A DatabaseDomain or domain ID string.
- * - readOnlyReason : Reason the master DB is read-only if so [optional]
- * - srvCache : BagOStuff object for server cache [optional]
- * - memStash : BagOStuff object for cross-datacenter memory storage [optional]
- * - wanCache : WANObjectCache object [optional]
- * - hostname : The name of the current server [optional]
+ * - readOnlyReason: Reason the master DB is read-only if so [optional]
+ * - srvCache: BagOStuff object for server cache [optional]
+ * - memStash: BagOStuff object for cross-datacenter memory storage [optional]
+ * - wanCache: WANObjectCache object [optional]
+ * - hostname: The name of the current server [optional]
* - cliMode: Whether the execution context is a CLI script. [optional]
- * - profiler : Class name or instance with profileIn()/profileOut() methods. [optional]
+ * - profiler: Class name or instance with profileIn()/profileOut() methods. [optional]
* - trxProfiler: TransactionProfiler instance. [optional]
* - replLogger: PSR-3 logger instance. [optional]
* - connLogger: PSR-3 logger instance. [optional]
* - queryLogger: PSR-3 logger instance. [optional]
* - perfLogger: PSR-3 logger instance. [optional]
- * - errorLogger : Callback that takes an Exception and logs it. [optional]
+ * - errorLogger: Callback that takes an Exception and logs it. [optional]
+ * - deprecationLogger: Callback to log a deprecation warning. [optional]
* @throws InvalidArgumentException
*/
public function __construct( array $conf );
@@ -140,9 +141,10 @@ interface ILBFactory {
* Prepare all tracked load balancers for shutdown
* @param int $mode One of the class SHUTDOWN_* constants
* @param callable|null $workCallback Work to mask ChronologyProtector writes
+ * @param int|null &$cpIndex Position key write counter for ChronologyProtector
*/
public function shutdown(
- $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
+ $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null, &$cpIndex = null
);
/**
@@ -304,7 +306,7 @@ interface ILBFactory {
public function setAgentName( $agent );
/**
- * Append ?cpPosTime parameter to a URL for ChronologyProtector purposes if needed
+ * Append ?cpPosIndex parameter to a URL for ChronologyProtector purposes if needed
*
* Note that unlike cookies, this works accross domains
*
@@ -312,13 +314,44 @@ interface ILBFactory {
* @param float $time UNIX timestamp just before shutdown() was called
* @return string
*/
- public function appendPreShutdownTimeAsQuery( $url, $time );
+ public function appendShutdownCPIndexAsQuery( $url, $time );
/**
* @param array $info Map of fields, including:
* - IPAddress : IP address
* - UserAgent : User-Agent HTTP header
* - ChronologyProtection : cookie/header value specifying ChronologyProtector usage
+ * - ChronologyPositionIndex: timestamp used to get up-to-date DB positions for the agent
*/
public function setRequestInfo( array $info );
+
+ /**
+ * Make certain table names use their own database, schema, and table prefix
+ * when passed into SQL queries pre-escaped and without a qualified database name
+ *
+ * For example, "user" can be converted to "myschema.mydbname.user" for convenience.
+ * Appearances like `user`, somedb.user, somedb.someschema.user will used literally.
+ *
+ * Calling this twice will completely clear any old table aliases. Also, note that
+ * callers are responsible for making sure the schemas and databases actually exist.
+ *
+ * @param array[] $aliases Map of (table => (dbname, schema, prefix) map)
+ * @since 1.31
+ */
+ public function setTableAliases( array $aliases );
+
+ /**
+ * Convert certain index names to alternative names before querying the DB
+ *
+ * Note that this applies to indexes regardless of the table they belong to.
+ *
+ * This can be employed when an index was renamed X => Y in code, but the new Y-named
+ * indexes were not yet built on all DBs. After all the Y-named ones are added by the DBA,
+ * the aliases can be removed, and then the old X-named indexes dropped.
+ *
+ * @param string[] $aliases
+ * @return mixed
+ * @since 1.31
+ */
+ public function setIndexAliases( array $aliases );
}
diff --git a/www/wiki/includes/libs/rdbms/lbfactory/LBFactory.php b/www/wiki/includes/libs/rdbms/lbfactory/LBFactory.php
index c891fb6b..401e9b36 100644
--- a/www/wiki/includes/libs/rdbms/lbfactory/LBFactory.php
+++ b/www/wiki/includes/libs/rdbms/lbfactory/LBFactory.php
@@ -30,6 +30,7 @@ use EmptyBagOStuff;
use WANObjectCache;
use Exception;
use RuntimeException;
+use LogicException;
/**
* An interface for generating database load balancers
@@ -52,6 +53,8 @@ abstract class LBFactory implements ILBFactory {
protected $perfLogger;
/** @var callable Error logger */
protected $errorLogger;
+ /** @var callable Deprecation logger */
+ protected $deprecationLogger;
/** @var BagOStuff */
protected $srvCache;
/** @var BagOStuff */
@@ -75,6 +78,11 @@ abstract class LBFactory implements ILBFactory {
/** @var callable[] */
protected $replicationWaitCallbacks = [];
+ /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
+ protected $tableAliases = [];
+ /** @var string[] Map of (index alias => index) */
+ protected $indexAliases = [];
+
/** @var bool Whether this PHP instance is for a CLI script */
protected $cliMode;
/** @var string Agent name for query profiling */
@@ -104,7 +112,12 @@ abstract class LBFactory implements ILBFactory {
$this->errorLogger = isset( $conf['errorLogger'] )
? $conf['errorLogger']
: function ( Exception $e ) {
- trigger_error( E_USER_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
+ trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
+ };
+ $this->deprecationLogger = isset( $conf['deprecationLogger'] )
+ ? $conf['deprecationLogger']
+ : function ( $msg ) {
+ trigger_error( $msg, E_USER_DEPRECATED );
};
$this->profiler = isset( $conf['profiler'] ) ? $conf['profiler'] : null;
@@ -115,10 +128,13 @@ abstract class LBFactory implements ILBFactory {
$this->requestInfo = [
'IPAddress' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
'UserAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
- 'ChronologyProtection' => 'true'
+ 'ChronologyProtection' => 'true',
+ 'ChronologyPositionIndex' => isset( $_GET['cpPosIndex'] ) ? $_GET['cpPosIndex'] : null
];
- $this->cliMode = isset( $conf['cliMode'] ) ? $conf['cliMode'] : PHP_SAPI === 'cli';
+ $this->cliMode = isset( $conf['cliMode'] )
+ ? $conf['cliMode']
+ : ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
$this->hostname = isset( $conf['hostname'] ) ? $conf['hostname'] : gethostname();
$this->agent = isset( $conf['agent'] ) ? $conf['agent'] : '';
@@ -131,13 +147,13 @@ abstract class LBFactory implements ILBFactory {
}
public function shutdown(
- $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
+ $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null, &$cpIndex = null
) {
$chronProt = $this->getChronologyProtector();
if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
- $this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync' );
+ $this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync', $cpIndex );
} elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
- $this->shutdownChronologyProtector( $chronProt, null, 'async' );
+ $this->shutdownChronologyProtector( $chronProt, null, 'async', $cpIndex );
}
$this->commitMasterChanges( __METHOD__ ); // sanity
@@ -307,7 +323,7 @@ abstract class LBFactory implements ILBFactory {
$opts += [
'domain' => false,
'cluster' => false,
- 'timeout' => 60,
+ 'timeout' => $this->cliMode ? 60 : 10,
'ifWritesSince' => null
];
@@ -357,7 +373,7 @@ abstract class LBFactory implements ILBFactory {
$failed = [];
foreach ( $lbs as $i => $lb ) {
if ( $masterPositions[$i] ) {
- // The DBMS may not support getMasterPos()
+ // The RDBMS may not support getMasterPos()
if ( !$lb->waitForAll( $masterPositions[$i], $opts['timeout'] ) ) {
$failed[] = $lb->getServerName( $lb->getWriterIndex() );
}
@@ -440,7 +456,7 @@ abstract class LBFactory implements ILBFactory {
'ip' => $this->requestInfo['IPAddress'],
'agent' => $this->requestInfo['UserAgent'],
],
- isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null
+ $this->requestInfo['ChronologyPositionIndex']
);
$this->chronProt->setLogger( $this->replLogger );
@@ -450,6 +466,10 @@ abstract class LBFactory implements ILBFactory {
// Request opted out of using position wait logic. This is useful for requests
// done by the job queue or background ETL that do not have a meaningful session.
$this->chronProt->setWaitEnabled( false );
+ } elseif ( $this->memStash instanceof EmptyBagOStuff ) {
+ // No where to store any DB positions and wait for them to appear
+ $this->chronProt->setEnabled( false );
+ $this->replLogger->info( 'Cannot use ChronologyProtector with EmptyBagOStuff.' );
}
$this->replLogger->debug( __METHOD__ . ': using request info ' .
@@ -464,9 +484,10 @@ abstract class LBFactory implements ILBFactory {
* @param ChronologyProtector $cp
* @param callable|null $workCallback Work to do instead of waiting on syncing positions
* @param string $mode One of (sync, async); whether to wait on remote datacenters
+ * @param int|null &$cpIndex DB position key write counter; incremented on update
*/
protected function shutdownChronologyProtector(
- ChronologyProtector $cp, $workCallback, $mode
+ ChronologyProtector $cp, $workCallback, $mode, &$cpIndex = null
) {
// Record all the master positions needed
$this->forEachLB( function ( ILoadBalancer $lb ) use ( $cp ) {
@@ -474,7 +495,7 @@ abstract class LBFactory implements ILBFactory {
} );
// Write them to the persistent stash. Try to do something useful by running $work
// while ChronologyProtector waits for the stash write to replicate to all DCs.
- $unsavedPositions = $cp->shutdown( $workCallback, $mode );
+ $unsavedPositions = $cp->shutdown( $workCallback, $mode, $cpIndex );
if ( $unsavedPositions && $workCallback ) {
// Invoke callback in case it did not cache the result yet
$workCallback(); // work now to block for less time in waitForAll()
@@ -507,10 +528,15 @@ abstract class LBFactory implements ILBFactory {
'connLogger' => $this->connLogger,
'replLogger' => $this->replLogger,
'errorLogger' => $this->errorLogger,
+ 'deprecationLogger' => $this->deprecationLogger,
'hostname' => $this->hostname,
'cliMode' => $this->cliMode,
'agent' => $this->agent,
- 'chronologyProtector' => $this->getChronologyProtector()
+ 'chronologyCallback' => function ( ILoadBalancer $lb ) {
+ // Defer ChronologyProtector construction in case setRequestInfo() ends up
+ // being called later (but before the first connection attempt) (T192611)
+ $this->getChronologyProtector()->initLB( $lb );
+ }
];
}
@@ -521,6 +547,17 @@ abstract class LBFactory implements ILBFactory {
if ( $this->trxRoundId !== false ) {
$lb->beginMasterChanges( $this->trxRoundId ); // set DBO_TRX
}
+
+ $lb->setTableAliases( $this->tableAliases );
+ $lb->setIndexAliases( $this->indexAliases );
+ }
+
+ public function setTableAliases( array $aliases ) {
+ $this->tableAliases = $aliases;
+ }
+
+ public function setIndexAliases( array $aliases ) {
+ $this->indexAliases = $aliases;
}
public function setDomainPrefix( $prefix ) {
@@ -543,7 +580,7 @@ abstract class LBFactory implements ILBFactory {
$this->agent = $agent;
}
- public function appendPreShutdownTimeAsQuery( $url, $time ) {
+ public function appendShutdownCPIndexAsQuery( $url, $index ) {
$usedCluster = 0;
$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$usedCluster ) {
$usedCluster |= ( $lb->getServerCount() > 1 );
@@ -553,10 +590,14 @@ abstract class LBFactory implements ILBFactory {
return $url; // no master/replica clusters touched
}
- return strpos( $url, '?' ) === false ? "$url?cpPosTime=$time" : "$url&cpPosTime=$time";
+ return strpos( $url, '?' ) === false ? "$url?cpPosIndex=$index" : "$url&cpPosIndex=$index";
}
public function setRequestInfo( array $info ) {
+ if ( $this->chronProt ) {
+ throw new LogicException( 'ChronologyProtector already initialized.' );
+ }
+
$this->requestInfo = $info + $this->requestInfo;
}
diff --git a/www/wiki/includes/libs/rdbms/lbfactory/LBFactoryMulti.php b/www/wiki/includes/libs/rdbms/lbfactory/LBFactoryMulti.php
index 0384588d..cfa26479 100644
--- a/www/wiki/includes/libs/rdbms/lbfactory/LBFactoryMulti.php
+++ b/www/wiki/includes/libs/rdbms/lbfactory/LBFactoryMulti.php
@@ -107,6 +107,12 @@ class LBFactoryMulti extends LBFactory {
/** @var string */
private $lastSection;
+ /** @var int */
+ private $maxLag = self::MAX_LAG_DEFAULT;
+
+ /** @var int Default 'maxLag' when unspecified */
+ const MAX_LAG_DEFAULT = 10;
+
/**
* @see LBFactory::__construct()
*
@@ -160,6 +166,7 @@ class LBFactoryMulti extends LBFactory {
* storage cluster.
* - masterTemplateOverrides Server configuration map overrides for all master servers.
* - loadMonitorClass Name of the LoadMonitor class to always use.
+ * - maxLag Avoid replica DBs with more lag than this many seconds.
* - readOnlyBySection A map of section name to read-only message.
* Missing or false for read/write.
*/
@@ -171,7 +178,7 @@ class LBFactoryMulti extends LBFactory {
$optional = [ 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
'templateOverridesByCluster', 'templateOverridesBySection', 'masterTemplateOverrides',
- 'readOnlyBySection', 'loadMonitorClass' ];
+ 'readOnlyBySection', 'maxLag', 'loadMonitorClass' ];
foreach ( $required as $key ) {
if ( !isset( $conf[$key] ) ) {
@@ -319,6 +326,7 @@ class LBFactoryMulti extends LBFactory {
$this->baseLoadBalancerParams(),
[
'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
+ 'maxLag' => $this->maxLag,
'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
'readOnlyReason' => $readOnlyReason
]
diff --git a/www/wiki/includes/libs/rdbms/lbfactory/LBFactorySimple.php b/www/wiki/includes/libs/rdbms/lbfactory/LBFactorySimple.php
index df0a806b..9a6aa3a7 100644
--- a/www/wiki/includes/libs/rdbms/lbfactory/LBFactorySimple.php
+++ b/www/wiki/includes/libs/rdbms/lbfactory/LBFactorySimple.php
@@ -41,6 +41,11 @@ class LBFactorySimple extends LBFactory {
/** @var string */
private $loadMonitorClass;
+ /** @var int */
+ private $maxLag;
+
+ /** @var int Default 'maxLag' when unspecified */
+ const MAX_LAG_DEFAULT = 10;
/**
* @see LBFactory::__construct()
@@ -72,6 +77,7 @@ class LBFactorySimple extends LBFactory {
$this->loadMonitorClass = isset( $conf['loadMonitorClass'] )
? $conf['loadMonitorClass']
: 'LoadMonitor';
+ $this->maxLag = isset( $conf['maxLag'] ) ? $conf['maxLag'] : self::MAX_LAG_DEFAULT;
}
/**
@@ -128,6 +134,7 @@ class LBFactorySimple extends LBFactory {
$this->baseLoadBalancerParams(),
[
'servers' => $servers,
+ 'maxLag' => $this->maxLag,
'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
]
) );
diff --git a/www/wiki/includes/libs/rdbms/lbfactory/LBFactorySingle.php b/www/wiki/includes/libs/rdbms/lbfactory/LBFactorySingle.php
index cd998c3e..587ab23c 100644
--- a/www/wiki/includes/libs/rdbms/lbfactory/LBFactorySingle.php
+++ b/www/wiki/includes/libs/rdbms/lbfactory/LBFactorySingle.php
@@ -102,6 +102,8 @@ class LBFactorySingle extends LBFactory {
* @param array $params
*/
public function forEachLB( $callback, array $params = [] ) {
- call_user_func_array( $callback, array_merge( [ $this->lb ], $params ) );
+ if ( isset( $this->lb ) ) { // may not be set during _destruct()
+ call_user_func_array( $callback, array_merge( [ $this->lb ], $params ) );
+ }
}
}
diff --git a/www/wiki/includes/libs/rdbms/loadbalancer/ILoadBalancer.php b/www/wiki/includes/libs/rdbms/loadbalancer/ILoadBalancer.php
index 22a58055..d6b7e3bd 100644
--- a/www/wiki/includes/libs/rdbms/loadbalancer/ILoadBalancer.php
+++ b/www/wiki/includes/libs/rdbms/loadbalancer/ILoadBalancer.php
@@ -85,6 +85,8 @@ interface ILoadBalancer {
const DOMAIN_ANY = '';
/** @var int DB handle should have DBO_TRX disabled and the caller will leave it as such */
+ const CONN_TRX_AUTOCOMMIT = 1;
+ /** @var int Alias for CONN_TRX_AUTOCOMMIT for b/c; deprecated since 1.31 */
const CONN_TRX_AUTO = 1;
/**
@@ -96,9 +98,10 @@ interface ILoadBalancer {
* - loadMonitor : Name of a class used to fetch server lag and load.
* - readOnlyReason : Reason the master DB is read-only if so [optional]
* - waitTimeout : Maximum time to wait for replicas for consistency [optional]
+ * - maxLag: Avoid replica DB servers with more lag than this [optional]
* - srvCache : BagOStuff object for server cache [optional]
* - wanCache : WANObjectCache object [optional]
- * - chronologyProtector: ChronologyProtector object [optional]
+ * - chronologyCallback: Callback to run before the first connection attempt [optional]
* - hostname : The name of the current server [optional]
* - cliMode: Whether the execution context is a CLI script. [optional]
* - profiler : Class name or instance with profileIn()/profileOut() methods. [optional]
@@ -108,6 +111,7 @@ interface ILoadBalancer {
* - queryLogger: PSR-3 logger instance. [optional]
* - perfLogger: PSR-3 logger instance. [optional]
* - errorLogger : Callback that takes an Exception and logs it. [optional]
+ * - deprecationLogger: Callback to log a deprecation warning. [optional]
* @throws InvalidArgumentException
*/
public function __construct( array $params );
@@ -163,21 +167,32 @@ interface ILoadBalancer {
/**
* Get any open connection to a given server index, local or foreign
*
+ * Use CONN_TRX_AUTOCOMMIT to only look for connections opened with that flag
+ *
* @param int $i Server index or DB_MASTER/DB_REPLICA
+ * @param int $flags Bitfield of CONN_* class constants
* @return Database|bool False if no such connection is open
*/
- public function getAnyOpenConnection( $i );
+ public function getAnyOpenConnection( $i, $flags = 0 );
/**
- * Get a connection by index
+ * Get a connection handle by server index
*
- * Avoid using CONN_TRX_AUTO with sqlite (e.g. check getServerType() first)
+ * The CONN_TRX_AUTOCOMMIT flag is ignored for databases with ATTR_DB_LEVEL_LOCKING
+ * (e.g. sqlite) in order to avoid deadlocks. ILoadBalancer::getServerAttributes()
+ * can be used to check such flags beforehand.
*
- * @param int $i Server index or DB_MASTER/DB_REPLICA
+ * If the caller uses $domain or sets CONN_TRX_AUTOCOMMIT in $flags, then it must also
+ * call ILoadBalancer::reuseConnection() on the handle when finished using it.
+ * In all other cases, this is not necessary, though not harmful either.
+ *
+ * @param int $i Server index (overrides $groups) or DB_MASTER/DB_REPLICA
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $domain Domain ID, or false for the current domain
* @param int $flags Bitfield of CONN_* class constants
*
+ * @note This method throws DBAccessError if ILoadBalancer::disable() was called
+ *
* @throws DBError
* @return Database
*/
@@ -192,21 +207,23 @@ interface ILoadBalancer {
* @param IDatabase $conn
* @throws InvalidArgumentException
*/
- public function reuseConnection( $conn );
+ public function reuseConnection( IDatabase $conn );
/**
* Get a database connection handle reference
*
* The handle's methods simply wrap those of a Database handle
*
- * Avoid using CONN_TRX_AUTO with sqlite (e.g. check getServerType() first)
+ * The CONN_TRX_AUTOCOMMIT flag is ignored for databases with ATTR_DB_LEVEL_LOCKING
+ * (e.g. sqlite) in order to avoid deadlocks. ILoadBalancer::getServerAttributes()
+ * can be used to check such flags beforehand.
*
* @see ILoadBalancer::getConnection() for parameter information
*
* @param int $i Server index or DB_MASTER/DB_REPLICA
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $domain Domain ID, or false for the current domain
- * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTO)
+ * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTOCOMMIT)
* @return DBConnRef
*/
public function getConnectionRef( $i, $groups = [], $domain = false, $flags = 0 );
@@ -216,14 +233,16 @@ interface ILoadBalancer {
*
* The handle's methods simply wrap those of a Database handle
*
- * Avoid using CONN_TRX_AUTO with sqlite (e.g. check getServerType() first)
+ * The CONN_TRX_AUTOCOMMIT flag is ignored for databases with ATTR_DB_LEVEL_LOCKING
+ * (e.g. sqlite) in order to avoid deadlocks. ILoadBalancer::getServerAttributes()
+ * can be used to check such flags beforehand.
*
* @see ILoadBalancer::getConnection() for parameter information
*
* @param int $i Server index or DB_MASTER/DB_REPLICA
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $domain Domain ID, or false for the current domain
- * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTO)
+ * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTOCOMMIT)
* @return DBConnRef
*/
public function getLazyConnectionRef( $i, $groups = [], $domain = false, $flags = 0 );
@@ -233,14 +252,16 @@ interface ILoadBalancer {
*
* The handle's methods simply wrap those of a Database handle
*
- * Avoid using CONN_TRX_AUTO with sqlite (e.g. check getServerType() first)
+ * The CONN_TRX_AUTOCOMMIT flag is ignored for databases with ATTR_DB_LEVEL_LOCKING
+ * (e.g. sqlite) in order to avoid deadlocks. ILoadBalancer::getServerAttributes()
+ * can be used to check such flags beforehand.
*
* @see ILoadBalancer::getConnection() for parameter information
*
* @param int $db Server index or DB_MASTER/DB_REPLICA
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $domain Domain ID, or false for the current domain
- * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTO)
+ * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTOCOMMIT)
* @return MaintainableDBConnRef
*/
public function getMaintenanceConnectionRef( $db, $groups = [], $domain = false, $flags = 0 );
@@ -251,13 +272,19 @@ interface ILoadBalancer {
* The index must be an actual index into the array. If a connection to the server is
* already open and not considered an "in use" foreign connection, this simply returns it.
*
- * Avoid using CONN_TRX_AUTO with sqlite (e.g. check getServerType() first)
+ * Avoid using CONN_TRX_AUTOCOMMIT for databases with ATTR_DB_LEVEL_LOCKING (e.g. sqlite) in
+ * order to avoid deadlocks. ILoadBalancer::getServerAttributes() can be used to check
+ * such flags beforehand.
+ *
+ * If the caller uses $domain or sets CONN_TRX_AUTOCOMMIT in $flags, then it must also
+ * call ILoadBalancer::reuseConnection() on the handle when finished using it.
+ * In all other cases, this is not necessary, though not harmful either.
*
- * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
+ * @note This method throws DBAccessError if ILoadBalancer::disable() was called
*
* @param int $i Server index (does not support DB_MASTER/DB_REPLICA)
* @param string|bool $domain Domain ID, or false for the current domain
- * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTO)
+ * @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTOCOMMIT)
* @return Database|bool Returns false on errors
* @throws DBAccessError
*/
@@ -312,20 +339,16 @@ interface ILoadBalancer {
* Return the server info structure for a given index, or false if the index is invalid.
* @param int $i
* @return array|bool
- *
- * @deprecated Since 1.30, no alternative
+ * @since 1.31
*/
public function getServerInfo( $i );
/**
- * Sets the server info structure for the given index. Entry at index $i
- * is created if it doesn't exist
- * @param int $i
- * @param array $serverInfo
- *
- * @deprecated Since 1.30, construct new object
+ * @param int $i Server index
+ * @return array (Database::ATTRIBUTE_* constant => value) for all such constants
+ * @since 1.31
*/
- public function setServerInfo( $i, array $serverInfo );
+ public function getServerAttributes( $i );
/**
* Get the current master position for chronology control purposes
@@ -606,4 +629,19 @@ interface ILoadBalancer {
* @param array[] $aliases Map of (table => (dbname, schema, prefix) map)
*/
public function setTableAliases( array $aliases );
+
+ /**
+ * Convert certain index names to alternative names before querying the DB
+ *
+ * Note that this applies to indexes regardless of the table they belong to.
+ *
+ * This can be employed when an index was renamed X => Y in code, but the new Y-named
+ * indexes were not yet built on all DBs. After all the Y-named ones are added by the DBA,
+ * the aliases can be removed, and then the old X-named indexes dropped.
+ *
+ * @param string[] $aliases
+ * @return mixed
+ * @since 1.31
+ */
+ public function setIndexAliases( array $aliases );
}
diff --git a/www/wiki/includes/libs/rdbms/loadbalancer/LoadBalancer.php b/www/wiki/includes/libs/rdbms/loadbalancer/LoadBalancer.php
index 8393e2bc..d5e65cdd 100644
--- a/www/wiki/includes/libs/rdbms/loadbalancer/LoadBalancer.php
+++ b/www/wiki/includes/libs/rdbms/loadbalancer/LoadBalancer.php
@@ -18,7 +18,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup Database
*/
namespace Wikimedia\Rdbms;
@@ -40,26 +39,28 @@ use Exception;
*/
class LoadBalancer implements ILoadBalancer {
/** @var array[] Map of (server index => server config array) */
- private $mServers;
+ private $servers;
/** @var Database[][][] Map of (connection category => server index => IDatabase[]) */
- private $mConns;
+ private $conns;
/** @var float[] Map of (server index => weight) */
- private $mLoads;
+ private $loads;
/** @var array[] Map of (group => server index => weight) */
- private $mGroupLoads;
+ private $groupLoads;
/** @var bool Whether to disregard replica DB lag as a factor in replica DB selection */
- private $mAllowLagged;
+ private $allowLagged;
/** @var int Seconds to spend waiting on replica DB lag to resolve */
- private $mWaitTimeout;
+ private $waitTimeout;
/** @var array The LoadMonitor configuration */
private $loadMonitorConfig;
/** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
private $tableAliases = [];
+ /** @var string[] Map of (index alias => index) */
+ private $indexAliases = [];
/** @var ILoadMonitor */
private $loadMonitor;
- /** @var ChronologyProtector|null */
- private $chronProt;
+ /** @var callable|null Callback to run before the first connection attempt */
+ private $chronologyCallback;
/** @var BagOStuff */
private $srvCache;
/** @var WANObjectCache */
@@ -80,15 +81,15 @@ class LoadBalancer implements ILoadBalancer {
/** @var Database DB connection object that caused a problem */
private $errorConnection;
/** @var int The generic (not query grouped) replica DB index (of $mServers) */
- private $mReadIndex;
+ private $readIndex;
/** @var bool|DBMasterPos False if not set */
- private $mWaitForPos;
+ private $waitForPos;
/** @var bool Whether the generic reader fell back to a lagged replica DB */
private $laggedReplicaMode = false;
/** @var bool Whether the generic reader fell back to a lagged replica DB */
private $allReplicasDownMode = false;
/** @var string The last DB selection or connection error */
- private $mLastError = 'Unknown error';
+ private $lastError = 'Unknown error';
/** @var string|bool Reason the LB is read-only or false if not */
private $readOnlyReason = false;
/** @var int Total connections opened */
@@ -110,17 +111,23 @@ class LoadBalancer implements ILoadBalancer {
/** @var callable Exception logger */
private $errorLogger;
+ /** @var callable Deprecation logger */
+ private $deprecationLogger;
/** @var bool */
private $disabled = false;
- /** @var bool */
- private $chronProtInitialized = false;
+ /** @var bool Whether any connection has been attempted yet */
+ private $connectionAttempted = false;
+ /** @var int */
+ private $maxLag = self::MAX_LAG_DEFAULT;
/** @var int Warn when this many connection are held */
const CONN_HELD_WARN_THRESHOLD = 10;
- /** @var int Default 'max lag' when unspecified */
+ /** @var int Default 'maxLag' when unspecified */
const MAX_LAG_DEFAULT = 10;
+ /** @var int Default 'waitTimeout' when unspecified */
+ const MAX_WAIT_DEFAULT = 10;
/** @var int Seconds to cache master server read-only status */
const TTL_CACHE_READONLY = 5;
@@ -136,31 +143,26 @@ class LoadBalancer implements ILoadBalancer {
if ( !isset( $params['servers'] ) ) {
throw new InvalidArgumentException( __CLASS__ . ': missing servers parameter' );
}
- $this->mServers = $params['servers'];
- foreach ( $this->mServers as $i => $server ) {
+ $this->servers = $params['servers'];
+ foreach ( $this->servers as $i => $server ) {
if ( $i == 0 ) {
- $this->mServers[$i]['master'] = true;
+ $this->servers[$i]['master'] = true;
} else {
- $this->mServers[$i]['replica'] = true;
+ $this->servers[$i]['replica'] = true;
}
}
- $this->localDomain = isset( $params['localDomain'] )
+ $localDomain = isset( $params['localDomain'] )
? DatabaseDomain::newFromId( $params['localDomain'] )
: DatabaseDomain::newUnspecified();
- // In case a caller assumes that the domain ID is simply <db>-<prefix>, which is almost
- // always true, gracefully handle the case when they fail to account for escaping.
- if ( $this->localDomain->getTablePrefix() != '' ) {
- $this->localDomainIdAlias =
- $this->localDomain->getDatabase() . '-' . $this->localDomain->getTablePrefix();
- } else {
- $this->localDomainIdAlias = $this->localDomain->getDatabase();
- }
+ $this->setLocalDomain( $localDomain );
- $this->mWaitTimeout = isset( $params['waitTimeout'] ) ? $params['waitTimeout'] : 10;
+ $this->waitTimeout = isset( $params['waitTimeout'] )
+ ? $params['waitTimeout']
+ : self::MAX_WAIT_DEFAULT;
- $this->mReadIndex = -1;
- $this->mConns = [
+ $this->readIndex = -1;
+ $this->conns = [
// Connection were transaction rounds may be applied
self::KEY_LOCAL => [],
self::KEY_FOREIGN_INUSE => [],
@@ -170,28 +172,33 @@ class LoadBalancer implements ILoadBalancer {
self::KEY_FOREIGN_INUSE_NOROUND => [],
self::KEY_FOREIGN_FREE_NOROUND => []
];
- $this->mLoads = [];
- $this->mWaitForPos = false;
- $this->mAllowLagged = false;
+ $this->loads = [];
+ $this->waitForPos = false;
+ $this->allowLagged = false;
if ( isset( $params['readOnlyReason'] ) && is_string( $params['readOnlyReason'] ) ) {
$this->readOnlyReason = $params['readOnlyReason'];
}
+ if ( isset( $params['maxLag'] ) ) {
+ $this->maxLag = $params['maxLag'];
+ }
+
if ( isset( $params['loadMonitor'] ) ) {
$this->loadMonitorConfig = $params['loadMonitor'];
} else {
$this->loadMonitorConfig = [ 'class' => 'LoadMonitorNull' ];
}
+ $this->loadMonitorConfig += [ 'lagWarnThreshold' => $this->maxLag ];
foreach ( $params['servers'] as $i => $server ) {
- $this->mLoads[$i] = $server['load'];
+ $this->loads[$i] = $server['load'];
if ( isset( $server['groupLoads'] ) ) {
foreach ( $server['groupLoads'] as $group => $ratio ) {
- if ( !isset( $this->mGroupLoads[$group] ) ) {
- $this->mGroupLoads[$group] = [];
+ if ( !isset( $this->groupLoads[$group] ) ) {
+ $this->groupLoads[$group] = [];
}
- $this->mGroupLoads[$group][$i] = $ratio;
+ $this->groupLoads[$group][$i] = $ratio;
}
}
}
@@ -218,6 +225,11 @@ class LoadBalancer implements ILoadBalancer {
: function ( Exception $e ) {
trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
};
+ $this->deprecationLogger = isset( $params['deprecationLogger'] )
+ ? $params['deprecationLogger']
+ : function ( $msg ) {
+ trigger_error( $msg, E_USER_DEPRECATED );
+ };
foreach ( [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ] as $key ) {
$this->$key = isset( $params[$key] ) ? $params[$key] : new NullLogger();
@@ -226,15 +238,28 @@ class LoadBalancer implements ILoadBalancer {
$this->host = isset( $params['hostname'] )
? $params['hostname']
: ( gethostname() ?: 'unknown' );
- $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli';
+ $this->cliMode = isset( $params['cliMode'] )
+ ? $params['cliMode']
+ : ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
$this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
- if ( isset( $params['chronologyProtector'] ) ) {
- $this->chronProt = $params['chronologyProtector'];
+ if ( isset( $params['chronologyCallback'] ) ) {
+ $this->chronologyCallback = $params['chronologyCallback'];
}
}
/**
+ * Get the local (and default) database domain ID of connection handles
+ *
+ * @see DatabaseDomain
+ * @return string Database domain ID; this specifies DB name, schema, and table prefix
+ * @since 1.31
+ */
+ public function getLocalDomainID() {
+ return $this->localDomain->getId();
+ }
+
+ /**
* Get a LoadMonitor instance
*
* @return ILoadMonitor
@@ -273,20 +298,22 @@ class LoadBalancer implements ILoadBalancer {
foreach ( $lags as $i => $lag ) {
if ( $i != 0 ) {
# How much lag this server nominally is allowed to have
- $maxServerLag = isset( $this->mServers[$i]['max lag'] )
- ? $this->mServers[$i]['max lag']
- : self::MAX_LAG_DEFAULT; // default
+ $maxServerLag = isset( $this->servers[$i]['max lag'] )
+ ? $this->servers[$i]['max lag']
+ : $this->maxLag; // default
# Constrain that futher by $maxLag argument
$maxServerLag = min( $maxServerLag, $maxLag );
$host = $this->getServerName( $i );
if ( $lag === false && !is_infinite( $maxServerLag ) ) {
$this->replLogger->error(
- "Server {host} is not replicating?", [ 'host' => $host ] );
+ __METHOD__ .
+ ": server {host} is not replicating?", [ 'host' => $host ] );
unset( $loads[$i] );
} elseif ( $lag > $maxServerLag ) {
- $this->replLogger->warning(
- "Server {host} has {lag} seconds of lag (>= {maxlag})",
+ $this->replLogger->debug(
+ __METHOD__ .
+ ": server {host} has {lag} seconds of lag (>= {maxlag})",
[ 'host' => $host, 'lag' => $lag, 'maxlag' => $maxServerLag ]
);
unset( $loads[$i] );
@@ -316,18 +343,18 @@ class LoadBalancer implements ILoadBalancer {
}
public function getReaderIndex( $group = false, $domain = false ) {
- if ( count( $this->mServers ) == 1 ) {
+ if ( count( $this->servers ) == 1 ) {
// Skip the load balancing if there's only one server
return $this->getWriterIndex();
- } elseif ( $group === false && $this->mReadIndex >= 0 ) {
+ } elseif ( $group === false && $this->readIndex >= 0 ) {
// Shortcut if the generic reader index was already cached
- return $this->mReadIndex;
+ return $this->readIndex;
}
if ( $group !== false ) {
// Use the server weight array for this load group
- if ( isset( $this->mGroupLoads[$group] ) ) {
- $loads = $this->mGroupLoads[$group];
+ if ( isset( $this->groupLoads[$group] ) ) {
+ $loads = $this->groupLoads[$group];
} else {
// No loads for this group, return false and the caller can use some other group
$this->connLogger->info( __METHOD__ . ": no loads for group $group" );
@@ -336,32 +363,32 @@ class LoadBalancer implements ILoadBalancer {
}
} else {
// Use the generic load group
- $loads = $this->mLoads;
+ $loads = $this->loads;
}
// Scale the configured load ratios according to each server's load and state
$this->getLoadMonitor()->scaleLoads( $loads, $domain );
- // Pick a server to use, accounting for weights, load, lag, and mWaitForPos
+ // Pick a server to use, accounting for weights, load, lag, and "waitForPos"
list( $i, $laggedReplicaMode ) = $this->pickReaderIndex( $loads, $domain );
if ( $i === false ) {
// Replica DB connection unsuccessful
return false;
}
- if ( $this->mWaitForPos && $i != $this->getWriterIndex() ) {
+ if ( $this->waitForPos && $i != $this->getWriterIndex() ) {
// Before any data queries are run, wait for the server to catch up to the
// specified position. This is used to improve session consistency. Note that
- // when LoadBalancer::waitFor() sets mWaitForPos, the waiting triggers here,
+ // when LoadBalancer::waitFor() sets "waitForPos", the waiting triggers here,
// so update laggedReplicaMode as needed for consistency.
if ( !$this->doWait( $i ) ) {
$laggedReplicaMode = true;
}
}
- if ( $this->mReadIndex <= 0 && $this->mLoads[$i] > 0 && $group === false ) {
+ if ( $this->readIndex <= 0 && $this->loads[$i] > 0 && $group === false ) {
// Cache the generic reader index for future ungrouped DB_REPLICA handles
- $this->mReadIndex = $i;
+ $this->readIndex = $i;
// Record if the generic reader index is in "lagged replica DB" mode
if ( $laggedReplicaMode ) {
$this->laggedReplicaMode = true;
@@ -392,15 +419,15 @@ class LoadBalancer implements ILoadBalancer {
// Quickly look through the available servers for a server that meets criteria...
$currentLoads = $loads;
while ( count( $currentLoads ) ) {
- if ( $this->mAllowLagged || $laggedReplicaMode ) {
+ if ( $this->allowLagged || $laggedReplicaMode ) {
$i = ArrayUtils::pickRandom( $currentLoads );
} else {
$i = false;
- if ( $this->mWaitForPos && $this->mWaitForPos->asOfTime() ) {
- // ChronologyProtecter sets mWaitForPos for session consistency.
+ if ( $this->waitForPos && $this->waitForPos->asOfTime() ) {
+ // "chronologyCallback" sets "waitForPos" for session consistency.
// This triggers doWait() after connect, so it's especially good to
// avoid lagged servers so as to avoid excessive delay in that method.
- $ago = microtime( true ) - $this->mWaitForPos->asOfTime();
+ $ago = microtime( true ) - $this->waitForPos->asOfTime();
// Aim for <= 1 second of waiting (being too picky can backfire)
$i = $this->getRandomNonLagged( $currentLoads, $domain, $ago + 1 );
}
@@ -410,7 +437,8 @@ class LoadBalancer implements ILoadBalancer {
}
if ( $i === false && count( $currentLoads ) != 0 ) {
// All replica DBs lagged. Switch to read-only mode
- $this->replLogger->error( "All replica DBs lagged. Switch to read-only mode" );
+ $this->replLogger->error(
+ __METHOD__ . ": all replica DBs lagged. Switch to read-only mode" );
$i = ArrayUtils::pickRandom( $currentLoads );
$laggedReplicaMode = true;
}
@@ -448,18 +476,18 @@ class LoadBalancer implements ILoadBalancer {
// If all servers were down, quit now
if ( !count( $currentLoads ) ) {
- $this->connLogger->error( "All servers down" );
+ $this->connLogger->error( __METHOD__ . ": all servers down" );
}
return [ $i, $laggedReplicaMode ];
}
public function waitFor( $pos ) {
- $oldPos = $this->mWaitForPos;
+ $oldPos = $this->waitForPos;
try {
- $this->mWaitForPos = $pos;
+ $this->waitForPos = $pos;
// If a generic reader connection was already established, then wait now
- $i = $this->mReadIndex;
+ $i = $this->readIndex;
if ( $i > 0 ) {
if ( !$this->doWait( $i ) ) {
$this->laggedReplicaMode = true;
@@ -472,14 +500,14 @@ class LoadBalancer implements ILoadBalancer {
}
public function waitForOne( $pos, $timeout = null ) {
- $oldPos = $this->mWaitForPos;
+ $oldPos = $this->waitForPos;
try {
- $this->mWaitForPos = $pos;
+ $this->waitForPos = $pos;
- $i = $this->mReadIndex;
+ $i = $this->readIndex;
if ( $i <= 0 ) {
// Pick a generic replica DB if there isn't one yet
- $readLoads = $this->mLoads;
+ $readLoads = $this->loads;
unset( $readLoads[$this->getWriterIndex()] ); // replica DBs only
$readLoads = array_filter( $readLoads ); // with non-zero load
$i = ArrayUtils::pickRandom( $readLoads );
@@ -492,27 +520,34 @@ class LoadBalancer implements ILoadBalancer {
}
} finally {
# Restore the old position, as this is not used for lag-protection but for throttling
- $this->mWaitForPos = $oldPos;
+ $this->waitForPos = $oldPos;
}
return $ok;
}
public function waitForAll( $pos, $timeout = null ) {
- $oldPos = $this->mWaitForPos;
+ $timeout = $timeout ?: $this->waitTimeout;
+
+ $oldPos = $this->waitForPos;
try {
- $this->mWaitForPos = $pos;
- $serverCount = count( $this->mServers );
+ $this->waitForPos = $pos;
+ $serverCount = count( $this->servers );
$ok = true;
for ( $i = 1; $i < $serverCount; $i++ ) {
- if ( $this->mLoads[$i] > 0 ) {
+ if ( $this->loads[$i] > 0 ) {
+ $start = microtime( true );
$ok = $this->doWait( $i, true, $timeout ) && $ok;
+ $timeout -= intval( microtime( true ) - $start );
+ if ( $timeout <= 0 ) {
+ break; // timeout reached
+ }
}
}
} finally {
# Restore the old position, as this is not used for lag-protection but for throttling
- $this->mWaitForPos = $oldPos;
+ $this->waitForPos = $oldPos;
}
return $ok;
@@ -526,22 +561,22 @@ class LoadBalancer implements ILoadBalancer {
return;
}
- if ( !$this->mWaitForPos || $pos->hasReached( $this->mWaitForPos ) ) {
- $this->mWaitForPos = $pos;
+ if ( !$this->waitForPos || $pos->hasReached( $this->waitForPos ) ) {
+ $this->waitForPos = $pos;
}
}
- /**
- * @param int $i
- * @return IDatabase|bool
- */
- public function getAnyOpenConnection( $i ) {
- foreach ( $this->mConns as $connsByServer ) {
- if ( !empty( $connsByServer[$i] ) ) {
- /** @var IDatabase[] $serverConns */
- $serverConns = $connsByServer[$i];
+ public function getAnyOpenConnection( $i, $flags = 0 ) {
+ $autocommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
+ foreach ( $this->conns as $connsByServer ) {
+ if ( !isset( $connsByServer[$i] ) ) {
+ continue;
+ }
- return reset( $serverConns );
+ foreach ( $connsByServer[$i] as $conn ) {
+ if ( !$autocommit || $conn->getLBInfo( 'autoCommitOnly' ) ) {
+ return $conn;
+ }
}
}
@@ -552,11 +587,11 @@ class LoadBalancer implements ILoadBalancer {
* Wait for a given replica DB to catch up to the master pos stored in $this
* @param int $index Server index
* @param bool $open Check the server even if a new connection has to be made
- * @param int $timeout Max seconds to wait; default is mWaitTimeout
+ * @param int $timeout Max seconds to wait; default is "waitTimeout" given to __construct()
* @return bool
*/
protected function doWait( $index, $open = false, $timeout = null ) {
- $close = false; // close the connection afterwards
+ $timeout = max( 1, intval( $timeout ?: $this->waitTimeout ) );
// Check if we already know that the DB has reached this point
$server = $this->getServerName( $index );
@@ -565,24 +600,34 @@ class LoadBalancer implements ILoadBalancer {
$knownReachedPos = $this->srvCache->get( $key );
if (
$knownReachedPos instanceof DBMasterPos &&
- $knownReachedPos->hasReached( $this->mWaitForPos )
+ $knownReachedPos->hasReached( $this->waitForPos )
) {
- $this->replLogger->debug( __METHOD__ .
- ": replica DB $server known to be caught up (pos >= $knownReachedPos)." );
+ $this->replLogger->debug(
+ __METHOD__ .
+ ': replica DB {dbserver} known to be caught up (pos >= $knownReachedPos).',
+ [ 'dbserver' => $server ]
+ );
return true;
}
// Find a connection to wait on, creating one if needed and allowed
+ $close = false; // close the connection afterwards
$conn = $this->getAnyOpenConnection( $index );
if ( !$conn ) {
if ( !$open ) {
- $this->replLogger->debug( __METHOD__ . ": no connection open for $server" );
+ $this->replLogger->debug(
+ __METHOD__ . ': no connection open for {dbserver}',
+ [ 'dbserver' => $server ]
+ );
return false;
} else {
$conn = $this->openConnection( $index, self::DOMAIN_ANY );
if ( !$conn ) {
- $this->replLogger->warning( __METHOD__ . ": failed to connect to $server" );
+ $this->replLogger->warning(
+ __METHOD__ . ': failed to connect to {dbserver}',
+ [ 'dbserver' => $server ]
+ );
return false;
}
@@ -592,22 +637,39 @@ class LoadBalancer implements ILoadBalancer {
}
}
- $this->replLogger->info( __METHOD__ . ": Waiting for replica DB $server to catch up..." );
- $timeout = $timeout ?: $this->mWaitTimeout;
- $result = $conn->masterPosWait( $this->mWaitForPos, $timeout );
+ $this->replLogger->info(
+ __METHOD__ .
+ ': waiting for replica DB {dbserver} to catch up...',
+ [ 'dbserver' => $server ]
+ );
+
+ $result = $conn->masterPosWait( $this->waitForPos, $timeout );
- if ( $result == -1 || is_null( $result ) ) {
- // Timed out waiting for replica DB, use master instead
+ if ( $result === null ) {
+ $this->replLogger->warning(
+ __METHOD__ . ': Errored out waiting on {host} pos {pos}',
+ [
+ 'host' => $server,
+ 'pos' => $this->waitForPos,
+ 'trace' => ( new RuntimeException() )->getTraceAsString()
+ ]
+ );
+ $ok = false;
+ } elseif ( $result == -1 ) {
$this->replLogger->warning(
- __METHOD__ . ": Timed out waiting on {host} pos {$this->mWaitForPos}",
- [ 'host' => $server ]
+ __METHOD__ . ': Timed out waiting on {host} pos {pos}',
+ [
+ 'host' => $server,
+ 'pos' => $this->waitForPos,
+ 'trace' => ( new RuntimeException() )->getTraceAsString()
+ ]
);
$ok = false;
} else {
- $this->replLogger->info( __METHOD__ . ": Done" );
+ $this->replLogger->debug( __METHOD__ . ": done waiting" );
$ok = true;
// Remember that the DB reached this point
- $this->srvCache->set( $key, $this->mWaitForPos, BagOStuff::TTL_DAY );
+ $this->srvCache->set( $key, $this->waitForPos, BagOStuff::TTL_DAY );
}
if ( $close ) {
@@ -627,6 +689,23 @@ class LoadBalancer implements ILoadBalancer {
$domain = false; // local connection requested
}
+ if ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) === self::CONN_TRX_AUTOCOMMIT ) {
+ // Assuming all servers are of the same type (or similar), which is overwhelmingly
+ // the case, use the master server information to get the attributes. The information
+ // for $i cannot be used since it might be DB_REPLICA, which might require connection
+ // attempts in order to be resolved into a real server index.
+ $attributes = $this->getServerAttributes( $this->getWriterIndex() );
+ if ( $attributes[Database::ATTR_DB_LEVEL_LOCKING] ) {
+ // Callers sometimes want to (a) escape REPEATABLE-READ stateness without locking
+ // rows (e.g. FOR UPDATE) or (b) make small commits during a larger transactions
+ // to reduce lock contention. None of these apply for sqlite and using separate
+ // connections just causes self-deadlocks.
+ $flags &= ~self::CONN_TRX_AUTOCOMMIT;
+ $this->connLogger->info( __METHOD__ .
+ ': ignoring CONN_TRX_AUTOCOMMIT to avoid deadlocks.' );
+ }
+ }
+
$groups = ( $groups === false || $groups === [] )
? [ false ] // check one "group": the generic pool
: (array)$groups;
@@ -636,7 +715,7 @@ class LoadBalancer implements ILoadBalancer {
if ( $i == self::DB_MASTER ) {
$i = $this->getWriterIndex();
- } else {
+ } elseif ( $i == self::DB_REPLICA ) {
# Try to find an available server in any the query groups (in order)
foreach ( $groups as $group ) {
$groupIndex = $this->getReaderIndex( $group, $domain );
@@ -649,14 +728,14 @@ class LoadBalancer implements ILoadBalancer {
# Operation-based index
if ( $i == self::DB_REPLICA ) {
- $this->mLastError = 'Unknown error'; // reset error string
+ $this->lastError = 'Unknown error'; // reset error string
# Try the general server pool if $groups are unavailable.
$i = ( $groups === [ false ] )
? false // don't bother with this if that is what was tried above
: $this->getReaderIndex( false, $domain );
# Couldn't find a working server in getReaderIndex()?
if ( $i === false ) {
- $this->mLastError = 'No working replica DB server: ' . $this->mLastError;
+ $this->lastError = 'No working replica DB server: ' . $this->lastError;
// Throw an exception
$this->reportConnectionError();
return null; // not reached
@@ -686,7 +765,7 @@ class LoadBalancer implements ILoadBalancer {
return $conn;
}
- public function reuseConnection( $conn ) {
+ public function reuseConnection( IDatabase $conn ) {
$serverIndex = $conn->getLBInfo( 'serverIndex' );
$refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
if ( $serverIndex === null || $refCount === null ) {
@@ -704,7 +783,8 @@ class LoadBalancer implements ILoadBalancer {
} elseif ( $conn instanceof DBConnRef ) {
// DBConnRef already handles calling reuseConnection() and only passes the live
// Database instance to this method. Any caller passing in a DBConnRef is broken.
- $this->connLogger->error( __METHOD__ . ": got DBConnRef instance.\n" .
+ $this->connLogger->error(
+ __METHOD__ . ": got DBConnRef instance.\n" .
( new RuntimeException() )->getTraceAsString() );
return;
@@ -723,20 +803,20 @@ class LoadBalancer implements ILoadBalancer {
}
$domain = $conn->getDomainID();
- if ( !isset( $this->mConns[$connInUseKey][$serverIndex][$domain] ) ) {
+ if ( !isset( $this->conns[$connInUseKey][$serverIndex][$domain] ) ) {
throw new InvalidArgumentException( __METHOD__ .
": connection $serverIndex/$domain not found; it may have already been freed." );
- } elseif ( $this->mConns[$connInUseKey][$serverIndex][$domain] !== $conn ) {
+ } elseif ( $this->conns[$connInUseKey][$serverIndex][$domain] !== $conn ) {
throw new InvalidArgumentException( __METHOD__ .
": connection $serverIndex/$domain mismatched; it may have already been freed." );
}
$conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
if ( $refCount <= 0 ) {
- $this->mConns[$connFreeKey][$serverIndex][$domain] = $conn;
- unset( $this->mConns[$connInUseKey][$serverIndex][$domain] );
- if ( !$this->mConns[$connInUseKey][$serverIndex] ) {
- unset( $this->mConns[$connInUseKey][$serverIndex] ); // clean up
+ $this->conns[$connFreeKey][$serverIndex][$domain] = $conn;
+ unset( $this->conns[$connInUseKey][$serverIndex][$domain] );
+ if ( !$this->conns[$connInUseKey][$serverIndex] ) {
+ unset( $this->conns[$connInUseKey][$serverIndex] ); // clean up
}
$this->connLogger->debug( __METHOD__ . ": freed connection $serverIndex/$domain" );
} else {
@@ -769,18 +849,18 @@ class LoadBalancer implements ILoadBalancer {
$domain = false; // local connection requested
}
- if ( !$this->chronProtInitialized && $this->chronProt ) {
+ if ( !$this->connectionAttempted && $this->chronologyCallback ) {
$this->connLogger->debug( __METHOD__ . ': calling initLB() before first connection.' );
- // Load CP positions before connecting so that doWait() triggers later if needed
- $this->chronProtInitialized = true;
- $this->chronProt->initLB( $this );
+ // Load any "waitFor" positions before connecting so that doWait() is triggered
+ $this->connectionAttempted = true;
+ call_user_func( $this->chronologyCallback, $this );
}
// Check if an auto-commit connection is being requested. If so, it will not reuse the
// main set of DB connections but rather its own pool since:
// a) those are usually set to implicitly use transaction rounds via DBO_TRX
// b) those must support the use of explicit transaction rounds via beginMasterChanges()
- $autoCommit = ( ( $flags & self::CONN_TRX_AUTO ) == self::CONN_TRX_AUTO );
+ $autoCommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
if ( $domain !== false ) {
// Connection is to a foreign domain
@@ -788,23 +868,29 @@ class LoadBalancer implements ILoadBalancer {
} else {
// Connection is to the local domain
$connKey = $autoCommit ? self::KEY_LOCAL_NOROUND : self::KEY_LOCAL;
- if ( isset( $this->mConns[$connKey][$i][0] ) ) {
- $conn = $this->mConns[$connKey][$i][0];
+ if ( isset( $this->conns[$connKey][$i][0] ) ) {
+ $conn = $this->conns[$connKey][$i][0];
} else {
- if ( !isset( $this->mServers[$i] ) || !is_array( $this->mServers[$i] ) ) {
+ if ( !isset( $this->servers[$i] ) || !is_array( $this->servers[$i] ) ) {
throw new InvalidArgumentException( "No server with index '$i'." );
}
// Open a new connection
- $server = $this->mServers[$i];
+ $server = $this->servers[$i];
$server['serverIndex'] = $i;
$server['autoCommitOnly'] = $autoCommit;
- $conn = $this->reallyOpenConnection( $server, false );
+ if ( $this->localDomain->getDatabase() !== null ) {
+ // Use the local domain table prefix if the local domain is specified
+ $server['tablePrefix'] = $this->localDomain->getTablePrefix();
+ }
+ $conn = $this->reallyOpenConnection( $server, $this->localDomain );
$host = $this->getServerName( $i );
if ( $conn->isOpen() ) {
- $this->connLogger->debug( "Connected to database $i at '$host'." );
- $this->mConns[$connKey][$i][0] = $conn;
+ $this->connLogger->debug(
+ __METHOD__ . ": connected to database $i at '$host'." );
+ $this->conns[$connKey][$i][0] = $conn;
} else {
- $this->connLogger->warning( "Failed to connect to database $i at '$host'." );
+ $this->connLogger->warning(
+ __METHOD__ . ": failed to connect to database $i at '$host'." );
$this->errorConnection = $conn;
$conn = false;
}
@@ -852,7 +938,7 @@ class LoadBalancer implements ILoadBalancer {
$domainInstance = DatabaseDomain::newFromId( $domain );
$dbName = $domainInstance->getDatabase();
$prefix = $domainInstance->getTablePrefix();
- $autoCommit = ( ( $flags & self::CONN_TRX_AUTO ) == self::CONN_TRX_AUTO );
+ $autoCommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
if ( $autoCommit ) {
$connFreeKey = self::KEY_FOREIGN_FREE_NOROUND;
@@ -862,52 +948,52 @@ class LoadBalancer implements ILoadBalancer {
$connInUseKey = self::KEY_FOREIGN_INUSE;
}
- if ( isset( $this->mConns[$connInUseKey][$i][$domain] ) ) {
+ if ( isset( $this->conns[$connInUseKey][$i][$domain] ) ) {
// Reuse an in-use connection for the same domain
- $conn = $this->mConns[$connInUseKey][$i][$domain];
+ $conn = $this->conns[$connInUseKey][$i][$domain];
$this->connLogger->debug( __METHOD__ . ": reusing connection $i/$domain" );
- } elseif ( isset( $this->mConns[$connFreeKey][$i][$domain] ) ) {
+ } elseif ( isset( $this->conns[$connFreeKey][$i][$domain] ) ) {
// Reuse a free connection for the same domain
- $conn = $this->mConns[$connFreeKey][$i][$domain];
- unset( $this->mConns[$connFreeKey][$i][$domain] );
- $this->mConns[$connInUseKey][$i][$domain] = $conn;
+ $conn = $this->conns[$connFreeKey][$i][$domain];
+ unset( $this->conns[$connFreeKey][$i][$domain] );
+ $this->conns[$connInUseKey][$i][$domain] = $conn;
$this->connLogger->debug( __METHOD__ . ": reusing free connection $i/$domain" );
- } elseif ( !empty( $this->mConns[$connFreeKey][$i] ) ) {
+ } elseif ( !empty( $this->conns[$connFreeKey][$i] ) ) {
// Reuse a free connection from another domain
- $conn = reset( $this->mConns[$connFreeKey][$i] );
- $oldDomain = key( $this->mConns[$connFreeKey][$i] );
- // The empty string as a DB name means "don't care".
- // DatabaseMysqlBase::open() already handle this on connection.
+ $conn = reset( $this->conns[$connFreeKey][$i] );
+ $oldDomain = key( $this->conns[$connFreeKey][$i] );
if ( strlen( $dbName ) && !$conn->selectDB( $dbName ) ) {
- $this->mLastError = "Error selecting database '$dbName' on server " .
+ $this->lastError = "Error selecting database '$dbName' on server " .
$conn->getServer() . " from client host {$this->host}";
$this->errorConnection = $conn;
$conn = false;
} else {
$conn->tablePrefix( $prefix );
- unset( $this->mConns[$connFreeKey][$i][$oldDomain] );
- $this->mConns[$connInUseKey][$i][$domain] = $conn;
+ unset( $this->conns[$connFreeKey][$i][$oldDomain] );
+ // Note that if $domain is an empty string, getDomainID() might not match it
+ $this->conns[$connInUseKey][$i][$conn->getDomainId()] = $conn;
$this->connLogger->debug( __METHOD__ .
": reusing free connection from $oldDomain for $domain" );
}
} else {
- if ( !isset( $this->mServers[$i] ) || !is_array( $this->mServers[$i] ) ) {
+ if ( !isset( $this->servers[$i] ) || !is_array( $this->servers[$i] ) ) {
throw new InvalidArgumentException( "No server with index '$i'." );
}
// Open a new connection
- $server = $this->mServers[$i];
+ $server = $this->servers[$i];
$server['serverIndex'] = $i;
$server['foreignPoolRefCount'] = 0;
$server['foreign'] = true;
$server['autoCommitOnly'] = $autoCommit;
- $conn = $this->reallyOpenConnection( $server, $dbName );
+ $conn = $this->reallyOpenConnection( $server, $domainInstance );
if ( !$conn->isOpen() ) {
$this->connLogger->warning( __METHOD__ . ": connection error for $i/$domain" );
$this->errorConnection = $conn;
$conn = false;
} else {
- $conn->tablePrefix( $prefix );
- $this->mConns[$connInUseKey][$i][$domain] = $conn;
+ $conn->tablePrefix( $prefix ); // as specified
+ // Note that if $domain is an empty string, getDomainID() might not match it
+ $this->conns[$connInUseKey][$i][$conn->getDomainID()] = $conn;
$this->connLogger->debug( __METHOD__ . ": opened new connection for $i/$domain" );
}
}
@@ -921,6 +1007,13 @@ class LoadBalancer implements ILoadBalancer {
return $conn;
}
+ public function getServerAttributes( $i ) {
+ return Database::attributesFromType(
+ $this->getServerType( $i ),
+ isset( $this->servers[$i]['driver'] ) ? $this->servers[$i]['driver'] : null
+ );
+ }
+
/**
* Test if the specified index represents an open connection
*
@@ -929,7 +1022,7 @@ class LoadBalancer implements ILoadBalancer {
* @return bool
*/
private function isOpen( $index ) {
- if ( !is_integer( $index ) ) {
+ if ( !is_int( $index ) ) {
return false;
}
@@ -937,23 +1030,34 @@ class LoadBalancer implements ILoadBalancer {
}
/**
- * Really opens a connection. Uncached.
+ * Open a new network connection to a server (uncached)
+ *
* Returns a Database object whether or not the connection was successful.
- * @access private
*
* @param array $server
- * @param string|bool $dbNameOverride Use "" to not select any database
+ * @param DatabaseDomain $domainOverride Use an unspecified domain to not select any database
* @return Database
* @throws DBAccessError
* @throws InvalidArgumentException
*/
- protected function reallyOpenConnection( array $server, $dbNameOverride = false ) {
+ protected function reallyOpenConnection( array $server, DatabaseDomain $domainOverride ) {
if ( $this->disabled ) {
throw new DBAccessError();
}
- if ( $dbNameOverride !== false ) {
- $server['dbname'] = $dbNameOverride;
+ // Handle $domainOverride being a specified or an unspecified domain
+ if ( $domainOverride->getDatabase() === null ) {
+ // Normally, an RDBMS requires a DB name specified on connection and the $server
+ // configuration array is assumed to already specify an appropriate DB name.
+ if ( $server['type'] === 'mysql' ) {
+ // For MySQL, DATABASE and SCHEMA are synonyms, connections need not specify a DB,
+ // and the DB name in $server might not exist due to legacy reasons (the default
+ // domain used to ignore the local LB domain, even when mismatched).
+ $server['dbname'] = null;
+ }
+ } else {
+ $server['dbname'] = $domainOverride->getDatabase();
+ $server['schema'] = $domainOverride->getSchema();
}
// Let the handle know what the cluster master is (e.g. "db1052")
@@ -971,6 +1075,7 @@ class LoadBalancer implements ILoadBalancer {
$server['connLogger'] = $this->connLogger;
$server['queryLogger'] = $this->queryLogger;
$server['errorLogger'] = $this->errorLogger;
+ $server['deprecationLogger'] = $this->deprecationLogger;
$server['profiler'] = $this->profiler;
$server['trxProfiler'] = $this->trxProfiler;
// Use the same agent and PHP mode for all DB handles
@@ -994,6 +1099,7 @@ class LoadBalancer implements ILoadBalancer {
$this->getLazyConnectionRef( self::DB_MASTER, [], $db->getDomainID() )
);
$db->setTableAliases( $this->tableAliases );
+ $db->setIndexAliases( $this->indexAliases );
if ( $server['serverIndex'] === $this->getWriterIndex() ) {
if ( $this->trxRoundId !== false ) {
@@ -1014,27 +1120,28 @@ class LoadBalancer implements ILoadBalancer {
$conn = $this->errorConnection; // the connection which caused the error
$context = [
'method' => __METHOD__,
- 'last_error' => $this->mLastError,
+ 'last_error' => $this->lastError,
];
if ( $conn instanceof IDatabase ) {
$context['db_server'] = $conn->getServer();
$this->connLogger->warning(
- "Connection error: {last_error} ({db_server})",
+ __METHOD__ . ": connection error: {last_error} ({db_server})",
$context
);
// throws DBConnectionError
- $conn->reportConnectionError( "{$this->mLastError} ({$context['db_server']})" );
+ $conn->reportConnectionError( "{$this->lastError} ({$context['db_server']})" );
} else {
// No last connection, probably due to all servers being too busy
$this->connLogger->error(
- "LB failure with no last connection. Connection error: {last_error}",
+ __METHOD__ .
+ ": LB failure with no last connection. Connection error: {last_error}",
$context
);
- // If all servers were busy, mLastError will contain something sensible
- throw new DBConnectionError( null, $this->mLastError );
+ // If all servers were busy, "lastError" will contain something sensible
+ throw new DBConnectionError( null, $this->lastError );
}
}
@@ -1043,22 +1150,22 @@ class LoadBalancer implements ILoadBalancer {
}
public function haveIndex( $i ) {
- return array_key_exists( $i, $this->mServers );
+ return array_key_exists( $i, $this->servers );
}
public function isNonZeroLoad( $i ) {
- return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
+ return array_key_exists( $i, $this->servers ) && $this->loads[$i] != 0;
}
public function getServerCount() {
- return count( $this->mServers );
+ return count( $this->servers );
}
public function getServerName( $i ) {
- if ( isset( $this->mServers[$i]['hostName'] ) ) {
- $name = $this->mServers[$i]['hostName'];
- } elseif ( isset( $this->mServers[$i]['host'] ) ) {
- $name = $this->mServers[$i]['host'];
+ if ( isset( $this->servers[$i]['hostName'] ) ) {
+ $name = $this->servers[$i]['hostName'];
+ } elseif ( isset( $this->servers[$i]['host'] ) ) {
+ $name = $this->servers[$i]['host'];
} else {
$name = '';
}
@@ -1066,28 +1173,16 @@ class LoadBalancer implements ILoadBalancer {
return ( $name != '' ) ? $name : 'localhost';
}
- public function getServerType( $i ) {
- return isset( $this->mServers[$i]['type'] ) ? $this->mServers[$i]['type'] : 'unknown';
- }
-
- /**
- * @deprecated Since 1.30, no alternative
- */
public function getServerInfo( $i ) {
- wfDeprecated( __METHOD__, '1.30' );
- if ( isset( $this->mServers[$i] ) ) {
- return $this->mServers[$i];
+ if ( isset( $this->servers[$i] ) ) {
+ return $this->servers[$i];
} else {
return false;
}
}
- /**
- * @deprecated Since 1.30, construct new object
- */
- public function setServerInfo( $i, array $serverInfo ) {
- wfDeprecated( __METHOD__, '1.30' );
- $this->mServers[$i] = $serverInfo;
+ public function getServerType( $i ) {
+ return isset( $this->servers[$i]['type'] ) ? $this->servers[$i]['type'] : 'unknown';
}
public function getMasterPos() {
@@ -1095,7 +1190,7 @@ class LoadBalancer implements ILoadBalancer {
# master (however unlikely that may be), then we can fetch the position from the replica DB.
$masterConn = $this->getAnyOpenConnection( $this->getWriterIndex() );
if ( !$masterConn ) {
- $serverCount = count( $this->mServers );
+ $serverCount = count( $this->servers );
for ( $i = 1; $i < $serverCount; $i++ ) {
$conn = $this->getAnyOpenConnection( $i );
if ( $conn ) {
@@ -1117,11 +1212,12 @@ class LoadBalancer implements ILoadBalancer {
public function closeAll() {
$this->forEachOpenConnection( function ( IDatabase $conn ) {
$host = $conn->getServer();
- $this->connLogger->debug( "Closing connection to database '$host'." );
+ $this->connLogger->debug(
+ __METHOD__ . ": closing connection to database '$host'." );
$conn->close();
} );
- $this->mConns = [
+ $this->conns = [
self::KEY_LOCAL => [],
self::KEY_FOREIGN_INUSE => [],
self::KEY_FOREIGN_FREE => [],
@@ -1133,8 +1229,8 @@ class LoadBalancer implements ILoadBalancer {
}
public function closeConnection( IDatabase $conn ) {
- $serverIndex = $conn->getLBInfo( 'serverIndex' ); // second index level of mConns
- foreach ( $this->mConns as $type => $connsByServer ) {
+ $serverIndex = $conn->getLBInfo( 'serverIndex' );
+ foreach ( $this->conns as $type => $connsByServer ) {
if ( !isset( $connsByServer[$serverIndex] ) ) {
continue;
}
@@ -1142,8 +1238,9 @@ class LoadBalancer implements ILoadBalancer {
foreach ( $connsByServer[$serverIndex] as $i => $trackedConn ) {
if ( $conn === $trackedConn ) {
$host = $this->getServerName( $i );
- $this->connLogger->debug( "Closing connection to database $i at '$host'." );
- unset( $this->mConns[$type][$serverIndex][$i] );
+ $this->connLogger->debug(
+ __METHOD__ . ": closing connection to database $i at '$host'." );
+ unset( $this->conns[$type][$serverIndex][$i] );
--$this->connsOpened;
break 2;
}
@@ -1293,11 +1390,12 @@ class LoadBalancer implements ILoadBalancer {
$e = null; // first exception
$this->forEachOpenMasterConnection( function ( Database $conn ) use ( $type, &$e ) {
$conn->setTrxEndCallbackSuppression( false );
- if ( $conn->writesOrCallbacksPending() ) {
- // This happens if onTransactionIdle() callbacks leave callbacks on *another* DB
- // (which finished its callbacks already). Warn and recover in this case. Let the
- // callbacks run in the final commitMasterChanges() in LBFactory::shutdown().
- $this->queryLogger->info( __METHOD__ . ": found writes/callbacks pending." );
+ // Callbacks run in AUTO-COMMIT mode, so make sure no transactions are pending...
+ if ( $conn->writesPending() ) {
+ // This happens if onTransactionIdle() callbacks write to *other* handles
+ // (which already finished their callbacks). Let any callbacks run in the final
+ // commitMasterChanges() in LBFactory::shutdown(), when the transaction is gone.
+ $this->queryLogger->warning( __METHOD__ . ": found writes pending." );
return;
} elseif ( $conn->trxLevel() ) {
// This happens for single-DB setups where DB_REPLICA uses the master DB,
@@ -1327,9 +1425,7 @@ class LoadBalancer implements ILoadBalancer {
$this->trxRoundId = false;
$this->forEachOpenMasterConnection(
function ( IDatabase $conn ) use ( $fname, $restore ) {
- if ( $conn->writesOrCallbacksPending() ) {
- $conn->rollback( $fname, $conn::FLUSHING_ALL_PEERS );
- }
+ $conn->rollback( $fname, $conn::FLUSHING_ALL_PEERS );
if ( $restore ) {
$this->undoTransactionRoundFlags( $conn );
}
@@ -1344,6 +1440,12 @@ class LoadBalancer implements ILoadBalancer {
}
/**
+ * Make all DB servers with DBO_DEFAULT/DBO_TRX set join the transaction round
+ *
+ * Some servers may have neither flag enabled, meaning that they opt out of such
+ * transaction rounds and remain in auto-commit mode. Such behavior might be desired
+ * when a DB server is used for something like simple key/value storage.
+ *
* @param IDatabase $conn
*/
private function applyTransactionRoundFlags( IDatabase $conn ) {
@@ -1355,9 +1457,10 @@ class LoadBalancer implements ILoadBalancer {
// DBO_TRX is controlled entirely by CLI mode presence with DBO_DEFAULT.
// Force DBO_TRX even in CLI mode since a commit round is expected soon.
$conn->setFlag( $conn::DBO_TRX, $conn::REMEMBER_PRIOR );
- // If config has explicitly requested DBO_TRX be either on or off by not
- // setting DBO_DEFAULT, then respect that. Forcing no transactions is useful
- // for things like blob stores (ExternalStore) which want auto-commit mode.
+ }
+
+ if ( $conn->getFlag( $conn::DBO_TRX ) ) {
+ $conn->setLBInfo( 'trxRoundId', $this->trxRoundId );
}
}
@@ -1369,6 +1472,10 @@ class LoadBalancer implements ILoadBalancer {
return; // transaction rounds do not apply to these connections
}
+ if ( $conn->getFlag( $conn::DBO_TRX ) ) {
+ $conn->setLBInfo( 'trxRoundId', false );
+ }
+
if ( $conn->getFlag( $conn::DBO_DEFAULT ) ) {
$conn->restoreFlags( $conn::RESTORE_PRIOR );
}
@@ -1403,7 +1510,7 @@ class LoadBalancer implements ILoadBalancer {
}
public function hasOrMadeRecentMasterChanges( $age = null ) {
- $age = ( $age === null ) ? $this->mWaitTimeout : $age;
+ $age = ( $age === null ) ? $this->waitTimeout : $age;
return ( $this->hasMasterChanges()
|| $this->lastMasterChangeTimestamp() > microtime( true ) - $age );
@@ -1507,11 +1614,11 @@ class LoadBalancer implements ILoadBalancer {
public function allowLagged( $mode = null ) {
if ( $mode === null ) {
- return $this->mAllowLagged;
+ return $this->allowLagged;
}
- $this->mAllowLagged = $mode;
+ $this->allowLagged = $mode;
- return $this->mAllowLagged;
+ return $this->allowLagged;
}
public function pingAll() {
@@ -1526,7 +1633,7 @@ class LoadBalancer implements ILoadBalancer {
}
public function forEachOpenConnection( $callback, array $params = [] ) {
- foreach ( $this->mConns as $connsByServer ) {
+ foreach ( $this->conns as $connsByServer ) {
foreach ( $connsByServer as $serverConns ) {
foreach ( $serverConns as $conn ) {
$mergedParams = array_merge( [ $conn ], $params );
@@ -1538,7 +1645,7 @@ class LoadBalancer implements ILoadBalancer {
public function forEachOpenMasterConnection( $callback, array $params = [] ) {
$masterIndex = $this->getWriterIndex();
- foreach ( $this->mConns as $connsByServer ) {
+ foreach ( $this->conns as $connsByServer ) {
if ( isset( $connsByServer[$masterIndex] ) ) {
/** @var IDatabase $conn */
foreach ( $connsByServer[$masterIndex] as $conn ) {
@@ -1550,7 +1657,7 @@ class LoadBalancer implements ILoadBalancer {
}
public function forEachOpenReplicaConnection( $callback, array $params = [] ) {
- foreach ( $this->mConns as $connsByServer ) {
+ foreach ( $this->conns as $connsByServer ) {
foreach ( $connsByServer as $i => $serverConns ) {
if ( $i === $this->getWriterIndex() ) {
continue; // skip master
@@ -1574,9 +1681,9 @@ class LoadBalancer implements ILoadBalancer {
$lagTimes = $this->getLagTimes( $domain );
foreach ( $lagTimes as $i => $lag ) {
- if ( $this->mLoads[$i] > 0 && $lag > $maxLag ) {
+ if ( $this->loads[$i] > 0 && $lag > $maxLag ) {
$maxLag = $lag;
- $host = $this->mServers[$i]['host'];
+ $host = $this->servers[$i]['host'];
$maxIndex = $i;
}
}
@@ -1591,7 +1698,7 @@ class LoadBalancer implements ILoadBalancer {
$knownLagTimes = []; // map of (server index => 0 seconds)
$indexesWithLag = [];
- foreach ( $this->mServers as $i => $server ) {
+ foreach ( $this->servers as $i => $server ) {
if ( empty( $server['is static'] ) ) {
$indexesWithLag[] = $i; // DB server might have replication lag
} else {
@@ -1613,10 +1720,12 @@ class LoadBalancer implements ILoadBalancer {
/**
* @param IDatabase $conn
* @param DBMasterPos|bool $pos
- * @param int $timeout
+ * @param int|null $timeout
* @return bool
*/
- public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ) {
+ public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = null ) {
+ $timeout = max( 1, $timeout ?: $this->waitTimeout );
+
if ( $this->getServerCount() <= 1 || !$conn->getLBInfo( 'replica' ) ) {
return true; // server is not a replica DB
}
@@ -1636,16 +1745,26 @@ class LoadBalancer implements ILoadBalancer {
if ( $pos instanceof DBMasterPos ) {
$result = $conn->masterPosWait( $pos, $timeout );
if ( $result == -1 || is_null( $result ) ) {
- $msg = __METHOD__ . ": Timed out waiting on {$conn->getServer()} pos {$pos}";
- $this->replLogger->warning( "$msg" );
+ $msg = __METHOD__ . ': timed out waiting on {host} pos {pos}';
+ $this->replLogger->warning( $msg, [
+ 'host' => $conn->getServer(),
+ 'pos' => $pos,
+ 'trace' => ( new RuntimeException() )->getTraceAsString()
+ ] );
$ok = false;
} else {
- $this->replLogger->info( __METHOD__ . ": Done" );
+ $this->replLogger->debug( __METHOD__ . ': done waiting' );
$ok = true;
}
} else {
$ok = false; // something is misconfigured
- $this->replLogger->error( "Could not get master pos for {$conn->getServer()}." );
+ $this->replLogger->error(
+ __METHOD__ . ': could not get master pos for {host}',
+ [
+ 'host' => $conn->getServer(),
+ 'trace' => ( new RuntimeException() )->getTraceAsString()
+ ]
+ );
}
return $ok;
@@ -1668,6 +1787,10 @@ class LoadBalancer implements ILoadBalancer {
$this->tableAliases = $aliases;
}
+ public function setIndexAliases( array $aliases ) {
+ $this->indexAliases = $aliases;
+ }
+
public function setDomainPrefix( $prefix ) {
// Find connections to explicit foreign domains still marked as in-use...
$domainsInUse = [];
@@ -1686,18 +1809,36 @@ class LoadBalancer implements ILoadBalancer {
"Foreign domain connections are still in use ($domains)." );
}
- $this->localDomain = new DatabaseDomain(
+ $oldDomain = $this->localDomain->getId();
+ $this->setLocalDomain( new DatabaseDomain(
$this->localDomain->getDatabase(),
- null,
+ $this->localDomain->getSchema(),
$prefix
- );
+ ) );
- $this->forEachOpenConnection( function ( IDatabase $db ) use ( $prefix ) {
- $db->tablePrefix( $prefix );
+ $this->forEachOpenConnection( function ( IDatabase $db ) use ( $prefix, $oldDomain ) {
+ if ( !$db->getLBInfo( 'foreign' ) ) {
+ $db->tablePrefix( $prefix );
+ }
} );
}
/**
+ * @param DatabaseDomain $domain
+ */
+ private function setLocalDomain( DatabaseDomain $domain ) {
+ $this->localDomain = $domain;
+ // In case a caller assumes that the domain ID is simply <db>-<prefix>, which is almost
+ // always true, gracefully handle the case when they fail to account for escaping.
+ if ( $this->localDomain->getTablePrefix() != '' ) {
+ $this->localDomainIdAlias =
+ $this->localDomain->getDatabase() . '-' . $this->localDomain->getTablePrefix();
+ } else {
+ $this->localDomainIdAlias = $this->localDomain->getDatabase();
+ }
+ }
+
+ /**
* Make PHP ignore user aborts/disconnects until the returned
* value leaves scope. This returns null and does nothing in CLI mode.
*
diff --git a/www/wiki/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php b/www/wiki/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php
index 79d250f6..be80cc5e 100644
--- a/www/wiki/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php
+++ b/www/wiki/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php
@@ -72,9 +72,9 @@ class LoadBalancerSingle extends LoadBalancer {
return new static( [ 'connection' => $db ] + $params );
}
- protected function reallyOpenConnection( array $server, $dbNameOverride = false ) {
+ protected function reallyOpenConnection( array $server, DatabaseDomain $domainOverride ) {
return $this->db;
}
}
-class_alias( 'Wikimedia\Rdbms\LoadBalancerSingle', 'LoadBalancerSingle' );
+class_alias( LoadBalancerSingle::class, 'LoadBalancerSingle' );
diff --git a/www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitor.php b/www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitor.php
index 8292c036..c7f807a0 100644
--- a/www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitor.php
+++ b/www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitor.php
@@ -45,9 +45,22 @@ class LoadMonitor implements ILoadMonitor {
/** @var float Moving average ratio (e.g. 0.1 for 10% weight to new weight) */
private $movingAveRatio;
+ /** @var int Amount of replication lag in seconds before warnings are logged */
+ private $lagWarnThreshold;
- const VERSION = 1; // cache key version
+ /** @var int cache key version */
+ const VERSION = 1;
+ /** @var int Default 'max lag' in seconds when unspecified */
+ const LAG_WARN_THRESHOLD = 10;
+ /**
+ * @param ILoadBalancer $lb
+ * @param BagOStuff $srvCache
+ * @param WANObjectCache $wCache
+ * @param array $options
+ * - movingAveRatio: moving average constant for server weight updates based on lag
+ * - lagWarnThreshold: how many seconds of lag trigger warnings
+ */
public function __construct(
ILoadBalancer $lb, BagOStuff $srvCache, WANObjectCache $wCache, array $options = []
) {
@@ -59,19 +72,22 @@ class LoadMonitor implements ILoadMonitor {
$this->movingAveRatio = isset( $options['movingAveRatio'] )
? $options['movingAveRatio']
: 0.1;
+ $this->lagWarnThreshold = isset( $options['lagWarnThreshold'] )
+ ? $options['lagWarnThreshold']
+ : self::LAG_WARN_THRESHOLD;
}
public function setLogger( LoggerInterface $logger ) {
$this->replLogger = $logger;
}
- public function scaleLoads( array &$weightByServer, $domain ) {
+ final public function scaleLoads( array &$weightByServer, $domain ) {
$serverIndexes = array_keys( $weightByServer );
$states = $this->getServerStates( $serverIndexes, $domain );
- $coefficientsByServer = $states['weightScales'];
+ $newScalesByServer = $states['weightScales'];
foreach ( $weightByServer as $i => $weight ) {
- if ( isset( $coefficientsByServer[$i] ) ) {
- $weightByServer[$i] = $weight * $coefficientsByServer[$i];
+ if ( isset( $newScalesByServer[$i] ) ) {
+ $weightByServer[$i] = $weight * $newScalesByServer[$i];
} else { // server recently added to config?
$host = $this->parent->getServerName( $i );
$this->replLogger->error( __METHOD__ . ": host $host not in cache" );
@@ -79,10 +95,8 @@ class LoadMonitor implements ILoadMonitor {
}
}
- public function getLagTimes( array $serverIndexes, $domain ) {
- $states = $this->getServerStates( $serverIndexes, $domain );
-
- return $states['lagTimes'];
+ final public function getLagTimes( array $serverIndexes, $domain ) {
+ return $this->getServerStates( $serverIndexes, $domain )['lagTimes'];
}
protected function getServerStates( array $serverIndexes, $domain ) {
@@ -142,11 +156,14 @@ class LoadMonitor implements ILoadMonitor {
continue;
}
- $conn = $this->parent->getAnyOpenConnection( $i );
+ # Handles with open transactions are avoided since they might be subject
+ # to REPEATABLE-READ snapshots, which could affect the lag estimate query.
+ $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
+ $conn = $this->parent->getAnyOpenConnection( $i, $flags );
if ( $conn ) {
$close = false; // already open
} else {
- $conn = $this->parent->openConnection( $i, '' );
+ $conn = $this->parent->openConnection( $i, ILoadBalancer::DOMAIN_ANY, $flags );
$close = true; // new connection
}
@@ -159,9 +176,10 @@ class LoadMonitor implements ILoadMonitor {
// Scale from 10% to 100% of nominal weight
$weightScales[$i] = max( $newWeight, 0.10 );
+ $host = $this->parent->getServerName( $i );
+
if ( !$conn ) {
$lagTimes[$i] = false;
- $host = $this->parent->getServerName( $i );
$this->replLogger->error(
__METHOD__ . ": host {db_server} is unreachable",
[ 'db_server' => $host ]
@@ -174,11 +192,19 @@ class LoadMonitor implements ILoadMonitor {
} else {
$lagTimes[$i] = $conn->getLag();
if ( $lagTimes[$i] === false ) {
- $host = $this->parent->getServerName( $i );
$this->replLogger->error(
__METHOD__ . ": host {db_server} is not replicating?",
[ 'db_server' => $host ]
);
+ } elseif ( $lagTimes[$i] > $this->lagWarnThreshold ) {
+ $this->replLogger->error(
+ "Server {host} has {lag} seconds of lag (>= {maxlag})",
+ [
+ 'host' => $host,
+ 'lag' => $lagTimes[$i],
+ 'maxlag' => $this->lagWarnThreshold
+ ]
+ );
}
}
diff --git a/www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php b/www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php
index f8ad329b..1fbc117c 100644
--- a/www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php
+++ b/www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php
@@ -46,12 +46,12 @@ class LoadMonitorMySQL extends LoadMonitor {
protected function getWeightScale( $index, IDatabase $conn = null ) {
if ( !$conn ) {
- return 0.0;
+ return parent::getWeightScale( $index, $conn );
}
$weight = 1.0;
if ( $this->warmCacheRatio > 0 ) {
- $res = $conn->query( 'SHOW STATUS', false );
+ $res = $conn->query( 'SHOW STATUS', __METHOD__ );
$s = $res ? $conn->fetchObject( $res ) : false;
if ( $s === false ) {
$host = $this->parent->getServerName( $index );
diff --git a/www/wiki/includes/libs/stats/BufferingStatsdDataFactory.php b/www/wiki/includes/libs/stats/BufferingStatsdDataFactory.php
index d75d9c0b..06915b2d 100644
--- a/www/wiki/includes/libs/stats/BufferingStatsdDataFactory.php
+++ b/www/wiki/includes/libs/stats/BufferingStatsdDataFactory.php
@@ -99,27 +99,22 @@ class BufferingStatsdDataFactory extends StatsdDataFactory implements IBuffering
return $this->buffer;
}
- /**
- * Check whether this data factory has any data.
- * @return bool
- */
public function hasData() {
return !empty( $this->buffer );
}
- /**
- * Return data from the factory.
- * @return StatsdData[]
- */
public function getData() {
return $this->buffer;
}
- /**
- * Set collection enable status.
- * @param bool $enabled Will collection be enabled?
- * @return void
- */
+ public function clearData() {
+ $this->buffer = [];
+ }
+
+ public function getDataCount() {
+ return count( $this->buffer );
+ }
+
public function setEnabled( $enabled ) {
$this->enabled = $enabled;
}
diff --git a/www/wiki/includes/libs/stats/IBufferingStatsdDataFactory.php b/www/wiki/includes/libs/stats/IBufferingStatsdDataFactory.php
index f77b26ce..77b4c352 100644
--- a/www/wiki/includes/libs/stats/IBufferingStatsdDataFactory.php
+++ b/www/wiki/includes/libs/stats/IBufferingStatsdDataFactory.php
@@ -9,22 +9,34 @@ use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
*/
interface IBufferingStatsdDataFactory extends StatsdDataFactoryInterface {
/**
- * Check whether this data factory has any data.
+ * Check whether this data factory has any buffered data.
* @return bool
*/
public function hasData();
/**
- * Return data from the factory.
+ * Return the buffered data from the factory.
* @return StatsdData[]
*/
public function getData();
/**
+ * Clear all buffered data from the factory
+ * @since 1.31
+ */
+ public function clearData();
+
+ /**
+ * Return the number of buffered statsd data entries
+ * @return int
+ * @since 1.31
+ */
+ public function getDataCount();
+
+ /**
* Set collection enable status.
* @param bool $enabled Will collection be enabled?
* @return void
*/
public function setEnabled( $enabled );
-
}
diff --git a/www/wiki/includes/libs/stats/NullStatsdDataFactory.php b/www/wiki/includes/libs/stats/NullStatsdDataFactory.php
index d346f651..63de8f2f 100644
--- a/www/wiki/includes/libs/stats/NullStatsdDataFactory.php
+++ b/www/wiki/includes/libs/stats/NullStatsdDataFactory.php
@@ -2,7 +2,6 @@
use Liuggio\StatsdClient\Entity\StatsdData;
use Liuggio\StatsdClient\Entity\StatsdDataInterface;
-use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
/**
* @author Addshore
@@ -106,27 +105,22 @@ class NullStatsdDataFactory implements IBufferingStatsdDataFactory {
return $data;
}
- /**
- * Check whether this data factory has any data.
- * @return bool
- */
public function hasData() {
return false;
}
- /**
- * Return data from the factory.
- * @return StatsdData[]
- */
public function getData() {
return [];
}
- /**
- * Set collection enable status.
- * @param bool $enabled Will collection be enabled?
- * @return void
- */
+ public function clearData() {
+ // Nothing to do, always empty
+ }
+
+ public function getDataCount() {
+ return 0;
+ }
+
public function setEnabled( $enabled ) {
// Nothing to do, null factory is always disabled.
}
diff --git a/www/wiki/includes/libs/virtualrest/SwiftVirtualRESTService.php b/www/wiki/includes/libs/virtualrest/SwiftVirtualRESTService.php
index 679d51c5..e00bee36 100644
--- a/www/wiki/includes/libs/virtualrest/SwiftVirtualRESTService.php
+++ b/www/wiki/includes/libs/virtualrest/SwiftVirtualRESTService.php
@@ -22,7 +22,7 @@
/**
* Example virtual rest service for OpenStack Swift
- * @TODO: caching support (APC/memcached)
+ * @todo caching support (APC/memcached)
* @since 1.23
*/
class SwiftVirtualRESTService extends VirtualRESTService {
diff --git a/www/wiki/includes/libs/xmp/XMP.php b/www/wiki/includes/libs/xmp/XMP.php
index c46acc69..6a4d7c42 100644
--- a/www/wiki/includes/libs/xmp/XMP.php
+++ b/www/wiki/includes/libs/xmp/XMP.php
@@ -130,10 +130,16 @@ class XMPReader implements LoggerAwareInterface {
private $logger;
/**
+ * @var string
+ */
+ private $filename;
+
+ /**
* Primary job is to initialize the XMLParser
* @param LoggerInterface|null $logger
+ * @param string $filename
*/
- function __construct( LoggerInterface $logger = null ) {
+ function __construct( LoggerInterface $logger = null, $filename = 'unknown' ) {
if ( !function_exists( 'xml_parser_create_ns' ) ) {
// this should already be checked by this point
throw new RuntimeException( 'XMP support requires XML Parser' );
@@ -143,6 +149,7 @@ class XMPReader implements LoggerAwareInterface {
} else {
$this->setLogger( new NullLogger() );
}
+ $this->filename = $filename;
$this->items = XMPInfo::getItems();
@@ -336,9 +343,9 @@ class XMPReader implements LoggerAwareInterface {
}
if ( $this->charset !== 'UTF-8' ) {
// don't convert if already utf-8
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$content = iconv( $this->charset, 'UTF-8//IGNORE', $content );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
// Ensure the XMP block does not have an xml doctype declaration, which
@@ -370,13 +377,15 @@ class XMPReader implements LoggerAwareInterface {
$col = xml_get_current_column_number( $this->xmlParser );
$offset = xml_get_current_byte_index( $this->xmlParser );
- $this->logger->warning(
+ $this->logger->info(
'{method} : Error reading XMP content: {error} ' .
- '(line: {line} column: {column} byte offset: {offset})',
+ '(file: {file}, line: {line} column: {column} ' .
+ 'byte offset: {offset})',
[
'method' => __METHOD__,
'error_code' => $code,
'error' => $error,
+ 'file' => $this->filename,
'line' => $line,
'column' => $col,
'offset' => $offset,
@@ -388,10 +397,11 @@ class XMPReader implements LoggerAwareInterface {
}
} catch ( Exception $e ) {
$this->logger->warning(
- '{method} Exception caught while parsing: ' . $e->getMessage(),
+ '{method} {exception}',
[
'method' => __METHOD__,
'exception' => $e,
+ 'file' => $this->filename,
'content' => $content,
]
);
@@ -420,7 +430,12 @@ class XMPReader implements LoggerAwareInterface {
|| $this->results['xmp-special']['HasExtendedXMP'] !== $guid
) {
$this->logger->info( __METHOD__ .
- " Ignoring XMPExtended block due to wrong guid (guid= '$guid')" );
+ " Ignoring XMPExtended block due to wrong guid (guid= '{guid}')",
+ [
+ 'guid' => $guid,
+ 'file' => $this->filename,
+ ]
+ );
return false;
}
@@ -432,7 +447,8 @@ class XMPReader implements LoggerAwareInterface {
$len['offset'] > $len['length']
) {
$this->logger->info(
- __METHOD__ . 'Error reading extended XMP block, invalid length or offset.'
+ __METHOD__ . 'Error reading extended XMP block, invalid length or offset.',
+ [ 'file' => $this->filename ]
);
return false;
@@ -450,7 +466,9 @@ class XMPReader implements LoggerAwareInterface {
if ( $len['offset'] !== $this->extendedXMPOffset ) {
$this->logger->info( __METHOD__ . 'Ignoring XMPExtended block due to wrong order. (Offset was '
- . $len['offset'] . ' but expected ' . $this->extendedXMPOffset . ')' );
+ . $len['offset'] . ' but expected ' . $this->extendedXMPOffset . ')',
+ [ 'file' => $this->filename ]
+ );
return false;
}
@@ -471,7 +489,10 @@ class XMPReader implements LoggerAwareInterface {
$atEnd = false;
}
- $this->logger->debug( __METHOD__ . 'Parsing a XMPExtended block' );
+ $this->logger->debug(
+ __METHOD__ . 'Parsing a XMPExtended block',
+ [ 'file' => $this->filename ]
+ );
return $this->parse( $actualContent, $atEnd );
}
@@ -550,7 +571,7 @@ class XMPReader implements LoggerAwareInterface {
// Even with LIBXML_NOWARNING set, XMLReader::read gives a warning
// when parsing truncated XML, which causes unit tests to fail.
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
while ( $reader->read() ) {
if ( $reader->nodeType === XMLReader::ELEMENT ) {
// Reached the first element without hitting a doctype declaration
@@ -564,7 +585,7 @@ class XMPReader implements LoggerAwareInterface {
break;
}
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !is_null( $result ) ) {
return $result;
@@ -667,19 +688,28 @@ class XMPReader implements LoggerAwareInterface {
if ( !isset( $this->results['xmp-' . $info['map_group']][$finalName] ) ) {
// This can happen if all the members of the struct failed validation.
- $this->logger->debug( __METHOD__ . " <$ns:$tag> has no valid members." );
+ $this->logger->debug(
+ __METHOD__ . " <$ns:$tag> has no valid members.",
+ [ 'file' => $this->filename ]
+ );
} elseif ( is_callable( $validate ) ) {
$val =& $this->results['xmp-' . $info['map_group']][$finalName];
call_user_func_array( $validate, [ $info, &$val, false ] );
if ( is_null( $val ) ) {
// the idea being the validation function will unset the variable if
// its invalid.
- $this->logger->info( __METHOD__ . " <$ns:$tag> failed validation." );
+ $this->logger->info(
+ __METHOD__ . " <$ns:$tag> failed validation.",
+ [ 'file' => $this->filename ]
+ );
unset( $this->results['xmp-' . $info['map_group']][$finalName] );
}
} else {
- $this->logger->warning( __METHOD__ . " Validation function for $finalName ("
- . $validate[0] . '::' . $validate[1] . '()) is not callable.' );
+ $this->logger->warning(
+ __METHOD__ . " Validation function for $finalName (" .
+ $validate[0] . '::' . $validate[1] . '()) is not callable.',
+ [ 'file' => $this->filename ]
+ );
}
}
@@ -718,7 +748,10 @@ class XMPReader implements LoggerAwareInterface {
array_shift( $this->mode );
if ( !isset( $this->results['xmp-' . $info['map_group']][$finalName] ) ) {
- $this->logger->debug( __METHOD__ . " Empty compund element $finalName." );
+ $this->logger->debug(
+ __METHOD__ . " Empty compund element $finalName.",
+ [ 'file' => $this->filename ]
+ );
return;
}
@@ -786,7 +819,10 @@ class XMPReader implements LoggerAwareInterface {
if ( $elm === self::NS_RDF . ' type' ) {
// these aren't really supported properly yet.
// However, it appears they almost never used.
- $this->logger->info( __METHOD__ . ' encountered <rdf:type>' );
+ $this->logger->info(
+ __METHOD__ . ' encountered <rdf:type>',
+ [ 'file' => $this->filename ]
+ );
}
if ( strpos( $elm, ' ' ) === false ) {
@@ -794,12 +830,15 @@ class XMPReader implements LoggerAwareInterface {
// However, there is a bug in an adobe product
// that forgets the namespace on some things.
// (Luckily they are unimportant things).
- $this->logger->info( __METHOD__ . " Encountered </$elm> which has no namespace. Skipping." );
+ $this->logger->info(
+ __METHOD__ . " Encountered </$elm> which has no namespace. Skipping.",
+ [ 'file' => $this->filename ]
+ );
return;
}
- if ( count( $this->mode[0] ) === 0 ) {
+ if ( count( $this->mode ) === 0 ) {
// This should never ever happen and means
// there is a pretty major bug in this class.
throw new RuntimeException( 'Encountered end element with no mode' );
@@ -840,7 +879,10 @@ class XMPReader implements LoggerAwareInterface {
$this->endElementModeQDesc( $elm );
break;
default:
- $this->logger->warning( __METHOD__ . " no mode (elm = $elm)" );
+ $this->logger->info(
+ __METHOD__ . " no mode (elm = $elm)",
+ [ 'file' => $this->filename ]
+ );
break;
}
}
@@ -890,8 +932,11 @@ class XMPReader implements LoggerAwareInterface {
array_unshift( $this->mode, self::MODE_LI );
} elseif ( $elm === self::NS_RDF . ' Bag' ) {
# T29105
- $this->logger->info( __METHOD__ . ' Expected an rdf:Seq, but got an rdf:Bag. Pretending'
- . ' it is a Seq, since some buggy software is known to screw this up.' );
+ $this->logger->info(
+ __METHOD__ . ' Expected an rdf:Seq, but got an rdf:Bag. Pretending' .
+ ' it is a Seq, since some buggy software is known to screw this up.',
+ [ 'file' => $this->filename ]
+ );
array_unshift( $this->mode, self::MODE_LI );
} else {
throw new RuntimeException( "Expected <rdf:Seq> but got $elm." );
@@ -954,8 +999,13 @@ class XMPReader implements LoggerAwareInterface {
} else {
// something else we don't recognize, like a qualifier maybe.
$this->logger->info( __METHOD__ .
- " Encountered element <$elm> where only expecting character data as value of " .
- $this->curItem[0] );
+ " Encountered element <{element}> where only expecting character data as value of {curitem}",
+ [
+ 'element' => $elm,
+ 'curitem' => $this->curItem[0],
+ 'file' => $this->filename,
+ ]
+ );
array_unshift( $this->mode, self::MODE_IGNORE );
array_unshift( $this->curItem, $elm );
}
@@ -1005,8 +1055,10 @@ class XMPReader implements LoggerAwareInterface {
// a child of a struct), then something weird is
// happening, so ignore this element and its children.
- $this->logger->warning( "Encountered <$ns:$tag> outside"
- . " of its expected parent. Ignoring." );
+ $this->logger->info(
+ 'Encountered <{element}> outside of its expected parent. Ignoring.',
+ [ 'element' => "$ns:$tag", 'file' => $this->filename ]
+ );
array_unshift( $this->mode, self::MODE_IGNORE );
array_unshift( $this->curItem, $ns . ' ' . $tag );
@@ -1027,7 +1079,8 @@ class XMPReader implements LoggerAwareInterface {
}
} else {
// This element is not on our list of allowed elements so ignore.
- $this->logger->debug( __METHOD__ . " Ignoring unrecognized element <$ns:$tag>." );
+ $this->logger->debug( __METHOD__ . ' Ignoring unrecognized element <{element}>.',
+ [ 'element' => "$ns:$tag", 'file' => $this->filename ] );
array_unshift( $this->mode, self::MODE_IGNORE );
array_unshift( $this->curItem, $ns . ' ' . $tag );
@@ -1204,12 +1257,18 @@ class XMPReader implements LoggerAwareInterface {
// on page 25 of part 1 of the xmp standard.
// Also it seems as if exiv2 and exiftool do not support
// this either (That or I misunderstand the standard)
- $this->logger->info( __METHOD__ . ' Encountered <rdf:type> which isn\'t currently supported' );
+ $this->logger->info(
+ __METHOD__ . ' Encountered <rdf:type> which isn\'t currently supported',
+ [ 'file' => $this->filename ]
+ );
}
if ( strpos( $elm, ' ' ) === false ) {
// This probably shouldn't happen.
- $this->logger->info( __METHOD__ . " Encountered <$elm> which has no namespace. Skipping." );
+ $this->logger->info(
+ __METHOD__ . " Encountered <$elm> which has no namespace. Skipping.",
+ [ 'file' => $this->filename ]
+ );
return;
}
@@ -1259,7 +1318,7 @@ class XMPReader implements LoggerAwareInterface {
}
}
- // @codingStandardsIgnoreStart Generic.Files.LineLength
+ // phpcs:disable Generic.Files.LineLength
/**
* Process attributes.
* Simple values can be stored as either a tag or attribute
@@ -1275,7 +1334,7 @@ class XMPReader implements LoggerAwareInterface {
* @param array $attribs Array attribute=>value
* @throws RuntimeException
*/
- // @codingStandardsIgnoreEnd
+ // phpcs:enable
private function doAttribs( $attribs ) {
// first check for rdf:parseType attribute, as that can change
// how the attributes are interperted.
@@ -1291,8 +1350,11 @@ class XMPReader implements LoggerAwareInterface {
if ( strpos( $name, ' ' ) === false ) {
// This shouldn't happen, but so far some old software forgets namespace
// on rdf:about.
- $this->logger->info( __METHOD__ . ' Encountered non-namespaced attribute: '
- . " $name=\"$val\". Skipping. " );
+ $this->logger->info(
+ __METHOD__ . ' Encountered non-namespaced attribute: ' .
+ " $name=\"$val\". Skipping. ",
+ [ 'file' => $this->filename ]
+ );
continue;
}
list( $ns, $tag ) = explode( ' ', $name, 2 );
@@ -1309,7 +1371,10 @@ class XMPReader implements LoggerAwareInterface {
}
$this->saveValue( $ns, $tag, $val );
} else {
- $this->logger->debug( __METHOD__ . " Ignoring unrecognized element <$ns:$tag>." );
+ $this->logger->debug(
+ __METHOD__ . " Ignoring unrecognized element <$ns:$tag>.",
+ [ 'file' => $this->filename ]
+ );
}
}
}
@@ -1342,13 +1407,19 @@ class XMPReader implements LoggerAwareInterface {
// the reasoning behind using &$val instead of using the return value
// is to be consistent between here and validating structures.
if ( is_null( $val ) ) {
- $this->logger->info( __METHOD__ . " <$ns:$tag> failed validation." );
+ $this->logger->info(
+ __METHOD__ . " <$ns:$tag> failed validation.",
+ [ 'file' => $this->filename ]
+ );
return;
}
} else {
- $this->logger->warning( __METHOD__ . " Validation function for $finalName ("
- . $validate[0] . '::' . $validate[1] . '()) is not callable.' );
+ $this->logger->warning(
+ __METHOD__ . " Validation function for $finalName (" .
+ $validate[0] . '::' . $validate[1] . '()) is not callable.',
+ [ 'file' => $this->filename ]
+ );
}
}
diff --git a/www/wiki/includes/libs/xmp/XMPValidate.php b/www/wiki/includes/libs/xmp/XMPValidate.php
index 76ae279f..9fe3e335 100644
--- a/www/wiki/includes/libs/xmp/XMPValidate.php
+++ b/www/wiki/includes/libs/xmp/XMPValidate.php
@@ -206,7 +206,7 @@ class XMPValidate implements LoggerAwareInterface {
$this->logger->info( __METHOD__ . " Flash structure did not have all the required components" );
$val = null;
} else {
- $val = ( "\0" | ( $val['Fired'] === 'True' )
+ $val = ( 0 | ( $val['Fired'] === 'True' )
| ( intval( $val['Return'] ) << 1 )
| ( intval( $val['Mode'] ) << 3 )
| ( ( $val['Function'] === 'True' ) << 5 )
@@ -262,14 +262,12 @@ class XMPValidate implements LoggerAwareInterface {
return;
}
$res = [];
- // @codingStandardsIgnoreStart Long line that cannot be broken
if ( !preg_match(
/* ahh! scary regex... */
+ // phpcs:ignore Generic.Files.LineLength
'/^([0-3]\d{3})(?:-([01]\d)(?:-([0-3]\d)(?:T([0-2]\d):([0-6]\d)(?::([0-6]\d)(?:\.\d+)?)?([-+]\d{2}:\d{2}|Z)?)?)?)?$/D',
$val, $res )
) {
- // @codingStandardsIgnoreEnd
-
$this->logger->info( __METHOD__ . " Expected date but got $val" );
$val = null;
} else {
diff --git a/www/wiki/includes/limit.sh b/www/wiki/includes/limit.sh
deleted file mode 100755
index d71e6603..00000000
--- a/www/wiki/includes/limit.sh
+++ /dev/null
@@ -1,122 +0,0 @@
-#!/bin/bash
-#
-# Resource limiting wrapper for command execution
-#
-# Why is this in shell script? Because bash has a setrlimit() wrapper
-# and is available on most Linux systems. If Perl was distributed with
-# BSD::Resource included, we would happily use that instead, but it isn't.
-
-# Clean up cgroup
-cleanup() {
- # First we have to move the current task into a "garbage" group, otherwise
- # the cgroup will not be empty, and attempting to remove it will fail with
- # "Device or resource busy"
- if [ -w "$MW_CGROUP"/tasks ]; then
- GARBAGE="$MW_CGROUP"
- else
- GARBAGE="$MW_CGROUP"/garbage-`id -un`
- if [ ! -e "$GARBAGE" ]; then
- mkdir -m 0700 "$GARBAGE"
- fi
- fi
- echo $BASHPID > "$GARBAGE"/tasks
-
- # Suppress errors in case the cgroup has disappeared due to a release script
- rmdir "$MW_CGROUP"/$$ 2>/dev/null
-}
-
-updateTaskCount() {
- # There are lots of ways to count lines in a file in shell script, but this
- # is one of the few that doesn't create another process, which would
- # increase the returned number of tasks.
- readarray < "$MW_CGROUP"/$$/tasks
- NUM_TASKS=${#MAPFILE[*]}
-}
-
-log() {
- echo limit.sh: "$*" >&3
- echo limit.sh: "$*" >&2
-}
-
-MW_INCLUDE_STDERR=
-MW_USE_LOG_PIPE=
-MW_CPU_LIMIT=0
-MW_CGROUP=
-MW_MEM_LIMIT=0
-MW_FILE_SIZE_LIMIT=0
-MW_WALL_CLOCK_LIMIT=0
-
-# Override settings
-eval "$2"
-
-if [ -n "$MW_INCLUDE_STDERR" ]; then
- exec 2>&1
-fi
-if [ -z "$MW_USE_LOG_PIPE" ]; then
- # Open a dummy log FD
- exec 3>/dev/null
-fi
-
-if [ "$MW_CPU_LIMIT" -gt 0 ]; then
- ulimit -t "$MW_CPU_LIMIT"
-fi
-if [ "$MW_MEM_LIMIT" -gt 0 ]; then
- if [ -n "$MW_CGROUP" ]; then
- # Create cgroup
- if ! mkdir -m 0700 "$MW_CGROUP"/$$; then
- log "failed to create the cgroup."
- MW_CGROUP=""
- fi
- fi
- if [ -n "$MW_CGROUP" ]; then
- echo $$ > "$MW_CGROUP"/$$/tasks
- if [ -n "$MW_CGROUP_NOTIFY" ]; then
- echo "1" > "$MW_CGROUP"/$$/notify_on_release
- fi
- # Memory
- echo $(($MW_MEM_LIMIT*1024)) > "$MW_CGROUP"/$$/memory.limit_in_bytes
- # Memory+swap
- # This will be missing if there is no swap
- if [ -e "$MW_CGROUP"/$$/memory.memsw.limit_in_bytes ]; then
- echo $(($MW_MEM_LIMIT*1024)) > "$MW_CGROUP"/$$/memory.memsw.limit_in_bytes
- fi
- else
- ulimit -v "$MW_MEM_LIMIT"
- fi
-else
- MW_CGROUP=""
-fi
-if [ "$MW_FILE_SIZE_LIMIT" -gt 0 ]; then
- ulimit -f "$MW_FILE_SIZE_LIMIT"
-fi
-if [ "$MW_WALL_CLOCK_LIMIT" -gt 0 -a -x "/usr/bin/timeout" ]; then
- /usr/bin/timeout $MW_WALL_CLOCK_LIMIT /bin/bash -c "$1" 3>&-
- STATUS="$?"
- if [ "$STATUS" == 124 ]; then
- log "timed out executing command \"$1\""
- fi
-else
- eval "$1" 3>&-
- STATUS="$?"
-fi
-
-if [ -n "$MW_CGROUP" ]; then
- updateTaskCount
-
- if [ $NUM_TASKS -gt 1 ]; then
- # Spawn a monitor process which will continue to poll for completion
- # of all processes in the cgroup after termination of the parent shell
- (
- while [ $NUM_TASKS -gt 1 ]; do
- sleep 10
- updateTaskCount
- done
- cleanup
- ) >&/dev/null < /dev/null 3>&- &
- disown -a
- else
- cleanup
- fi
-fi
-exit "$STATUS"
-
diff --git a/www/wiki/includes/linkeddata/PageDataRequestHandler.php b/www/wiki/includes/linkeddata/PageDataRequestHandler.php
index 43cb44c8..03ab8ea2 100644
--- a/www/wiki/includes/linkeddata/PageDataRequestHandler.php
+++ b/www/wiki/includes/linkeddata/PageDataRequestHandler.php
@@ -1,4 +1,22 @@
<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
use Wikimedia\Http\HttpAcceptParser;
use Wikimedia\Http\HttpAcceptNegotiator;
@@ -6,11 +24,9 @@ use Wikimedia\Http\HttpAcceptNegotiator;
/**
* Request handler implementing a data interface for mediawiki pages.
*
- * @license GPL-2.0+
* @author Daniel Kinzler
* @author Amir Sarabadanai
*/
-
class PageDataRequestHandler {
/**
diff --git a/www/wiki/includes/linker/LinkRenderer.php b/www/wiki/includes/linker/LinkRenderer.php
index c203a16b..160d2d11 100644
--- a/www/wiki/includes/linker/LinkRenderer.php
+++ b/www/wiki/includes/linker/LinkRenderer.php
@@ -16,7 +16,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL-2.0+
* @author Kunal Mehta <legoktm@member.fsf.org>
*/
namespace MediaWiki\Linker;
diff --git a/www/wiki/includes/linker/LinkRendererFactory.php b/www/wiki/includes/linker/LinkRendererFactory.php
index b7c05c2f..240ea09b 100644
--- a/www/wiki/includes/linker/LinkRendererFactory.php
+++ b/www/wiki/includes/linker/LinkRendererFactory.php
@@ -16,7 +16,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL-2.0+
* @author Kunal Mehta <legoktm@member.fsf.org>
*/
namespace MediaWiki\Linker;
diff --git a/www/wiki/includes/linker/LinkTarget.php b/www/wiki/includes/linker/LinkTarget.php
index dbd97a7a..56407aec 100644
--- a/www/wiki/includes/linker/LinkTarget.php
+++ b/www/wiki/includes/linker/LinkTarget.php
@@ -16,7 +16,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Addshore
*/
namespace MediaWiki\Linker;
@@ -103,4 +102,15 @@ interface LinkTarget {
* @return string
*/
public function getInterwiki();
+
+ /**
+ * Returns an informative human readable representation of the link target,
+ * for use in logging and debugging. There is no requirement for the return
+ * value to have any relationship with the input of TitleParser.
+ * @since 1.31
+ *
+ * @return string
+ */
+ public function __toString();
+
}
diff --git a/www/wiki/includes/logging/BlockLogFormatter.php b/www/wiki/includes/logging/BlockLogFormatter.php
index 1ed18cd0..a5af0269 100644
--- a/www/wiki/includes/logging/BlockLogFormatter.php
+++ b/www/wiki/includes/logging/BlockLogFormatter.php
@@ -22,8 +22,6 @@
* @since 1.25
*/
-use MediaWiki\MediaWikiServices;
-
/**
* This class formats block log entries.
*
@@ -99,7 +97,7 @@ class BlockLogFormatter extends LogFormatter {
public function getActionLinks() {
$subtype = $this->entry->getSubtype();
- $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+ $linkRenderer = $this->getLinkRenderer();
if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden
|| !( $subtype === 'block' || $subtype === 'reblock' )
|| !$this->context->getUser()->isAllowed( 'block' )
diff --git a/www/wiki/includes/logging/ContentModelLogFormatter.php b/www/wiki/includes/logging/ContentModelLogFormatter.php
index 861ea302..e05357cd 100644
--- a/www/wiki/includes/logging/ContentModelLogFormatter.php
+++ b/www/wiki/includes/logging/ContentModelLogFormatter.php
@@ -1,7 +1,5 @@
<?php
-use MediaWiki\MediaWikiServices;
-
class ContentModelLogFormatter extends LogFormatter {
protected function getMessageParameters() {
$lang = $this->context->getLanguage();
@@ -20,7 +18,7 @@ class ContentModelLogFormatter extends LogFormatter {
}
$params = $this->extractParameters();
- $revert = MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
+ $revert = $this->getLinkRenderer()->makeKnownLink(
SpecialPage::getTitleFor( 'ChangeContentModel' ),
$this->msg( 'logentry-contentmodel-change-revertlink' )->text(),
[],
diff --git a/www/wiki/includes/logging/DeleteLogFormatter.php b/www/wiki/includes/logging/DeleteLogFormatter.php
index ceb00520..ef006345 100644
--- a/www/wiki/includes/logging/DeleteLogFormatter.php
+++ b/www/wiki/includes/logging/DeleteLogFormatter.php
@@ -23,8 +23,6 @@
* @since 1.22
*/
-use MediaWiki\MediaWikiServices;
-
/**
* This class formats delete log entries.
*
@@ -135,7 +133,7 @@ class DeleteLogFormatter extends LogFormatter {
public function getActionLinks() {
$user = $this->context->getUser();
- $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+ $linkRenderer = $this->getLinkRenderer();
if ( !$user->isAllowed( 'deletedhistory' )
|| $this->entry->isDeleted( LogPage::DELETED_ACTION )
) {
diff --git a/www/wiki/includes/logging/LogEntry.php b/www/wiki/includes/logging/LogEntry.php
index 8b51932b..e17ac032 100644
--- a/www/wiki/includes/logging/LogEntry.php
+++ b/www/wiki/includes/logging/LogEntry.php
@@ -170,21 +170,23 @@ class DatabaseLogEntry extends LogEntryBase {
* @return array
*/
public static function getSelectQueryData() {
- $commentQuery = CommentStore::newKey( 'log_comment' )->getJoin();
+ $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
- $tables = [ 'logging', 'user' ] + $commentQuery['tables'];
+ $tables = array_merge(
+ [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ]
+ );
$fields = [
'log_id', 'log_type', 'log_action', 'log_timestamp',
- 'log_user', 'log_user_text',
'log_namespace', 'log_title', // unused log_page
'log_params', 'log_deleted',
'user_id', 'user_name', 'user_editcount',
- ] + $commentQuery['fields'];
+ ] + $commentQuery['fields'] + $actorQuery['fields'];
$joins = [
// IPs don't have an entry in user table
- 'user' => [ 'LEFT JOIN', 'log_user=user_id' ],
- ] + $commentQuery['joins'];
+ 'user' => [ 'LEFT JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
+ ] + $commentQuery['joins'] + $actorQuery['joins'];
return [
'tables' => $tables,
@@ -211,6 +213,30 @@ class DatabaseLogEntry extends LogEntryBase {
}
}
+ /**
+ * Loads a LogEntry with the given id from database
+ *
+ * @param int $id
+ * @param IDatabase $db
+ * @return DatabaseLogEntry|null
+ */
+ public static function newFromId( $id, IDatabase $db ) {
+ $queryInfo = self::getSelectQueryData();
+ $queryInfo['conds'] += [ 'log_id' => $id ];
+ $row = $db->selectRow(
+ $queryInfo['tables'],
+ $queryInfo['fields'],
+ $queryInfo['conds'],
+ __METHOD__,
+ $queryInfo['options'],
+ $queryInfo['join_conds']
+ );
+ if ( !$row ) {
+ return null;
+ }
+ return self::newFromRow( $row );
+ }
+
/** @var stdClass Database result row. */
protected $row;
@@ -265,9 +291,9 @@ class DatabaseLogEntry extends LogEntryBase {
public function getParameters() {
if ( !isset( $this->params ) ) {
$blob = $this->getRawParameters();
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$params = LogEntryBase::extractParams( $blob );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $params !== false ) {
$this->params = $params;
$this->legacy = false;
@@ -293,11 +319,14 @@ class DatabaseLogEntry extends LogEntryBase {
public function getPerformer() {
if ( !$this->performer ) {
+ $actorId = isset( $this->row->log_actor ) ? (int)$this->row->log_actor : 0;
$userId = (int)$this->row->log_user;
- if ( $userId !== 0 ) {
+ if ( $userId !== 0 || $actorId !== 0 ) {
// logged-in users
if ( isset( $this->row->user_name ) ) {
$this->performer = User::newFromRow( $this->row );
+ } elseif ( $actorId !== 0 ) {
+ $this->performer = User::newFromActorId( $actorId );
} else {
$this->performer = User::newFromId( $userId );
}
@@ -324,7 +353,7 @@ class DatabaseLogEntry extends LogEntryBase {
}
public function getComment() {
- return CommentStore::newKey( 'log_comment' )->getComment( $this->row )->text;
+ return CommentStore::getStore()->getComment( 'log_comment', $this->row )->text;
}
public function getDeleted() {
@@ -356,8 +385,11 @@ class RCDatabaseLogEntry extends DatabaseLogEntry {
public function getPerformer() {
if ( !$this->performer ) {
+ $actorId = isset( $this->row->rc_actor ) ? (int)$this->row->rc_actor : 0;
$userId = (int)$this->row->rc_user;
- if ( $userId !== 0 ) {
+ if ( $actorId !== 0 ) {
+ $this->performer = User::newFromActorId( $actorId );
+ } elseif ( $userId !== 0 ) {
$this->performer = User::newFromId( $userId );
} else {
$userText = $this->row->rc_user_text;
@@ -382,9 +414,9 @@ class RCDatabaseLogEntry extends DatabaseLogEntry {
}
public function getComment() {
- return CommentStore::newKey( 'rc_comment' )
- // Legacy because the row probably used RecentChange::selectFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $this->row )->text;
+ return CommentStore::getStore()
+ // Legacy because the row may have used RecentChange::selectFields()
+ ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $this->row )->text;
}
public function getDeleted() {
@@ -593,6 +625,8 @@ class ManualLogEntry extends LogEntryBase {
* @throws MWException
*/
public function insert( IDatabase $dbw = null ) {
+ global $wgActorTableSchemaMigrationStage;
+
$dbw = $dbw ?: wfGetDB( DB_MASTER );
if ( $this->timestamp === null ) {
@@ -605,6 +639,31 @@ class ManualLogEntry extends LogEntryBase {
$params = $this->getParameters();
$relations = $this->relations;
+ // Ensure actor relations are set
+ if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH &&
+ empty( $relations['target_author_actor'] )
+ ) {
+ $actorIds = [];
+ if ( !empty( $relations['target_author_id'] ) ) {
+ foreach ( $relations['target_author_id'] as $id ) {
+ $actorIds[] = User::newFromId( $id )->getActorId( $dbw );
+ }
+ }
+ if ( !empty( $relations['target_author_ip'] ) ) {
+ foreach ( $relations['target_author_ip'] as $ip ) {
+ $actorIds[] = User::newFromName( $ip, false )->getActorId( $dbw );
+ }
+ }
+ if ( $actorIds ) {
+ $relations['target_author_actor'] = $actorIds;
+ $params['authorActors'] = $actorIds;
+ }
+ }
+ if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_NEW ) {
+ unset( $relations['target_author_id'], $relations['target_author_ip'] );
+ unset( $params['authorIds'], $params['authorIPs'] );
+ }
+
// Additional fields for which there's no space in the database table schema
$revId = $this->getAssociatedRevId();
if ( $revId ) {
@@ -616,8 +675,6 @@ class ManualLogEntry extends LogEntryBase {
'log_type' => $this->getType(),
'log_action' => $this->getSubtype(),
'log_timestamp' => $dbw->timestamp( $this->getTimestamp() ),
- 'log_user' => $this->getPerformer()->getId(),
- 'log_user_text' => $this->getPerformer()->getName(),
'log_namespace' => $this->getTarget()->getNamespace(),
'log_title' => $this->getTarget()->getDBkey(),
'log_page' => $this->getTarget()->getArticleID(),
@@ -626,7 +683,9 @@ class ManualLogEntry extends LogEntryBase {
if ( isset( $this->deleted ) ) {
$data['log_deleted'] = $this->deleted;
}
- $data += CommentStore::newKey( 'log_comment' )->insert( $dbw, $comment );
+ $data += CommentStore::getStore()->insert( $dbw, 'log_comment', $comment );
+ $data += ActorMigration::newMigration()
+ ->getInsertValues( $dbw, 'log_user', $this->getPerformer() );
$dbw->insert( 'logging', $data, __METHOD__ );
$this->id = $dbw->insertId();
@@ -723,13 +782,6 @@ class ManualLogEntry extends LogEntryBase {
if ( $to === 'udp' || $to === 'rcandudp' ) {
$rc->notifyRCFeeds();
}
-
- // Log the autopatrol if the log entry is patrollable
- if ( $this->getIsPatrollable() &&
- $rc->getAttribute( 'rc_patrolled' ) === 1
- ) {
- PatrolLog::record( $rc, true, $this->getPerformer() );
- }
}
},
DeferredUpdates::POSTSEND,
diff --git a/www/wiki/includes/logging/LogEventsList.php b/www/wiki/includes/logging/LogEventsList.php
index 00d3bd33..d97ddfdd 100644
--- a/www/wiki/includes/logging/LogEventsList.php
+++ b/www/wiki/includes/logging/LogEventsList.php
@@ -98,8 +98,9 @@ class LogEventsList extends ContextSource {
* @param string $user
* @param string $page
* @param string $pattern
- * @param int $year Year
- * @param int $month Month
+ * @param int|string $year Use 0 to start with no year preselected.
+ * @param int|string $month A month in the 1..12 range. Use 0 to start with no month
+ * preselected.
* @param array $filter
* @param string $tagFilter Tag to select by default
* @param string $action
@@ -426,7 +427,7 @@ class LogEventsList extends ContextSource {
}
/**
- * @param stdClass $row Row
+ * @param stdClass $row
* @return string
*/
private function getShowHideLinks( $row ) {
@@ -496,7 +497,7 @@ class LogEventsList extends ContextSource {
}
/**
- * @param stdClass $row Row
+ * @param stdClass $row
* @param string|array $type
* @param string|array $action
* @param string $right
@@ -519,15 +520,16 @@ class LogEventsList extends ContextSource {
/**
* Determine if the current user is allowed to view a particular
- * field of this log row, if it's marked as deleted.
+ * field of this log row, if it's marked as deleted and/or restricted log type.
*
- * @param stdClass $row Row
+ * @param stdClass $row
* @param int $field
* @param User $user User to check, or null to use $wgUser
* @return bool
*/
public static function userCan( $row, $field, User $user = null ) {
- return self::userCanBitfield( $row->log_deleted, $field, $user );
+ return self::userCanBitfield( $row->log_deleted, $field, $user ) &&
+ self::userCanViewLogType( $row->log_type, $user );
}
/**
@@ -558,7 +560,27 @@ class LogEventsList extends ContextSource {
}
/**
- * @param stdClass $row Row
+ * Determine if the current user is allowed to view a particular
+ * field of this log row, if it's marked as restricted log type.
+ *
+ * @param stdClass $type
+ * @param User|null $user User to check, or null to use $wgUser
+ * @return bool
+ */
+ public static function userCanViewLogType( $type, User $user = null ) {
+ if ( $user === null ) {
+ global $wgUser;
+ $user = $wgUser;
+ }
+ $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( 'LogRestrictions' );
+ if ( isset( $logRestrictions[$type] ) && !$user->isAllowed( $logRestrictions[$type] ) ) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @param stdClass $row
* @param int $field One of DELETED_* bitfield constants
* @return bool
*/
diff --git a/www/wiki/includes/logging/LogFormatter.php b/www/wiki/includes/logging/LogFormatter.php
index 2a47943a..72f3c54b 100644
--- a/www/wiki/includes/logging/LogFormatter.php
+++ b/www/wiki/includes/logging/LogFormatter.php
@@ -193,7 +193,7 @@ class LogFormatter {
}
/**
- * Even uglier hack to maintain backwards compatibilty with IRC bots
+ * Even uglier hack to maintain backwards compatibility with IRC bots
* (T36508).
* @see getActionText()
* @return string Text
@@ -214,7 +214,7 @@ class LogFormatter {
}
/**
- * Even uglier hack to maintain backwards compatibilty with IRC bots
+ * Even uglier hack to maintain backwards compatibility with IRC bots
* (T36508).
* @see getActionText()
* @return string Text
@@ -261,19 +261,15 @@ class LogFormatter {
$text = wfMessage( 'undeletedarticle' )
->rawParams( $target )->inContentLanguage()->escaped();
break;
- // @codingStandardsIgnoreStart Long line
//case 'revision': // Revision deletion
//case 'event': // Log deletion
// see https://github.com/wikimedia/mediawiki/commit/a9c243b7b5289dad204278dbe7ed571fd914e395
//default:
- // @codingStandardsIgnoreEnd
}
break;
case 'patrol':
- // @codingStandardsIgnoreStart Long line
// https://github.com/wikimedia/mediawiki/commit/1a05f8faf78675dc85984f27f355b8825b43efff
- // @codingStandardsIgnoreEnd
// Create a diff link to the patrolled revision
if ( $entry->getSubtype() === 'patrol' ) {
$diffLink = htmlspecialchars(
@@ -640,16 +636,22 @@ class LogFormatter {
* @param Title $title The page
* @param array $parameters Query parameters
* @param string|null $html Linktext of the link as raw html
- * @throws MWException
* @return string
*/
protected function makePageLink( Title $title = null, $parameters = [], $html = null ) {
+ if ( !$title instanceof Title ) {
+ $msg = $this->msg( 'invalidtitle' )->text();
+ if ( !$this->plaintext ) {
+ return Html::element( 'span', [ 'class' => 'mw-invalidtitle' ], $msg );
+ } else {
+ return $msg;
+ }
+ }
+
if ( !$this->plaintext ) {
- $link = Linker::link( $title, $html, [], $parameters );
+ $html = $html !== null ? new HtmlArmor( $html ) : $html;
+ $link = $this->getLinkRenderer()->makeLink( $title, $html, [], $parameters );
} else {
- if ( !$title instanceof Title ) {
- throw new MWException( "Expected title, got null" );
- }
$link = '[[' . $title->getPrefixedText() . ']]';
}
@@ -866,10 +868,12 @@ class LogFormatter {
case 'title':
case 'title-link':
$title = Title::newFromText( $value );
- if ( $title ) {
- $value = [];
- ApiQueryBase::addTitleInfo( $value, $title, "{$name}_" );
+ if ( !$title ) {
+ // Huh? Do something halfway sane.
+ $title = SpecialPage::getTitleFor( 'Badtitle', $value );
}
+ $value = [];
+ ApiQueryBase::addTitleInfo( $value, $title, "{$name}_" );
return $value;
case 'user':
diff --git a/www/wiki/includes/logging/LogPage.php b/www/wiki/includes/logging/LogPage.php
index e4212092..28c1a873 100644
--- a/www/wiki/includes/logging/LogPage.php
+++ b/www/wiki/includes/logging/LogPage.php
@@ -97,14 +97,13 @@ class LogPage {
'log_type' => $this->type,
'log_action' => $this->action,
'log_timestamp' => $dbw->timestamp( $now ),
- 'log_user' => $this->doer->getId(),
- 'log_user_text' => $this->doer->getName(),
'log_namespace' => $this->target->getNamespace(),
'log_title' => $this->target->getDBkey(),
'log_page' => $this->target->getArticleID(),
'log_params' => $this->params
];
- $data += CommentStore::newKey( 'log_comment' )->insert( $dbw, $this->comment );
+ $data += CommentStore::getStore()->insert( $dbw, 'log_comment', $this->comment );
+ $data += ActorMigration::newMigration()->getInsertValues( $dbw, 'log_user', $this->doer );
$dbw->insert( 'logging', $data, __METHOD__ );
$newId = $dbw->insertId();
@@ -319,7 +318,7 @@ class LogPage {
*
* @param string $action One of '', 'block', 'protect', 'rights', 'delete',
* 'upload', 'move', 'move_redir'
- * @param Title $target Title object
+ * @param Title $target
* @param string $comment Description associated
* @param array $params Parameters passed later to wfMessage function
* @param null|int|User $doer The user doing the action. null for $wgUser
diff --git a/www/wiki/includes/logging/LogPager.php b/www/wiki/includes/logging/LogPager.php
index e62a7453..c047e96e 100644
--- a/www/wiki/includes/logging/LogPager.php
+++ b/www/wiki/includes/logging/LogPager.php
@@ -45,6 +45,12 @@ class LogPager extends ReverseChronologicalPager {
/** @var string */
private $action = '';
+ /** @var bool */
+ private $performerRestrictionsEnforced = false;
+
+ /** @var bool */
+ private $actionRestrictionsEnforced = false;
+
/** @var LogEventsList */
public $mLogEventsList;
@@ -99,13 +105,11 @@ class LogPager extends ReverseChronologicalPager {
return $filters;
}
foreach ( $wgFilterLogTypes as $type => $default ) {
- // Avoid silly filtering
- if ( $type !== 'patrol' || $this->getUser()->useNPPatrol() ) {
- $hide = $this->getRequest()->getInt( "hide_{$type}_log", $default );
- $filters[$type] = $hide;
- if ( $hide ) {
- $this->mConds[] = 'log_type != ' . $this->mDb->addQuotes( $type );
- }
+ $hide = $this->getRequest()->getInt( "hide_{$type}_log", $default );
+
+ $filters[$type] = $hide;
+ if ( $hide ) {
+ $this->mConds[] = 'log_type != ' . $this->mDb->addQuotes( $type );
}
}
@@ -174,21 +178,13 @@ class LogPager extends ReverseChronologicalPager {
// Normalize username first so that non-existent users used
// in maintenance scripts work
$name = $usertitle->getText();
- /* Fetch userid at first, if known, provides awesome query plan afterwards */
- $userid = User::idFromName( $name );
- if ( !$userid ) {
- $this->mConds['log_user_text'] = IP::sanitizeIP( $name );
- } else {
- $this->mConds['log_user'] = $userid;
- }
- // Paranoia: avoid brute force searches (T19342)
- $user = $this->getUser();
- if ( !$user->isAllowed( 'deletedhistory' ) ) {
- $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0';
- } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
- $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) .
- ' != ' . LogPage::SUPPRESSED_USER;
- }
+
+ // Assume no joins required for log_user
+ $this->mConds[] = ActorMigration::newMigration()->getWhere(
+ wfGetDB( DB_REPLICA ), 'log_user', User::newFromName( $name, false )
+ )['conds'];
+
+ $this->enforcePerformerRestrictions();
$this->performer = $name;
}
@@ -256,14 +252,7 @@ class LogPager extends ReverseChronologicalPager {
} else {
$this->mConds['log_title'] = $title->getDBkey();
}
- // Paranoia: avoid brute force searches (T19342)
- $user = $this->getUser();
- if ( !$user->isAllowed( 'deletedhistory' ) ) {
- $this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0';
- } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
- $this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) .
- ' != ' . LogPage::SUPPRESSED_ACTION;
- }
+ $this->enforceActionRestrictions();
}
/**
@@ -435,4 +424,39 @@ class LogPager extends ReverseChronologicalPager {
parent::doQuery();
$this->mDb->setBigSelects( 'default' );
}
+
+ /**
+ * Paranoia: avoid brute force searches (T19342)
+ */
+ private function enforceActionRestrictions() {
+ if ( $this->actionRestrictionsEnforced ) {
+ return;
+ }
+ $this->actionRestrictionsEnforced = true;
+ $user = $this->getUser();
+ if ( !$user->isAllowed( 'deletedhistory' ) ) {
+ $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0';
+ } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+ $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) .
+ ' != ' . LogPage::SUPPRESSED_USER;
+ }
+ }
+
+ /**
+ * Paranoia: avoid brute force searches (T19342)
+ */
+ private function enforcePerformerRestrictions() {
+ // Same as enforceActionRestrictions(), except for _USER instead of _ACTION bits.
+ if ( $this->performerRestrictionsEnforced ) {
+ return;
+ }
+ $this->performerRestrictionsEnforced = true;
+ $user = $this->getUser();
+ if ( !$user->isAllowed( 'deletedhistory' ) ) {
+ $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0';
+ } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+ $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) .
+ ' != ' . LogPage::SUPPRESSED_ACTION;
+ }
+ }
}
diff --git a/www/wiki/includes/logging/MergeLogFormatter.php b/www/wiki/includes/logging/MergeLogFormatter.php
index b0edd4c0..8775097d 100644
--- a/www/wiki/includes/logging/MergeLogFormatter.php
+++ b/www/wiki/includes/logging/MergeLogFormatter.php
@@ -54,9 +54,9 @@ class MergeLogFormatter extends LogFormatter {
// Show unmerge link
$params = $this->extractParameters();
- $revert = Linker::linkKnown(
+ $revert = $this->getLinkRenderer()->makeKnownLink(
SpecialPage::getTitleFor( 'MergeHistory' ),
- $this->msg( 'revertmerge' )->escaped(),
+ $this->msg( 'revertmerge' )->text(),
[],
[
'target' => $params[3],
diff --git a/www/wiki/includes/logging/MoveLogFormatter.php b/www/wiki/includes/logging/MoveLogFormatter.php
index afbf8e95..43ca0ea1 100644
--- a/www/wiki/includes/logging/MoveLogFormatter.php
+++ b/www/wiki/includes/logging/MoveLogFormatter.php
@@ -71,9 +71,9 @@ class MoveLogFormatter extends LogFormatter {
return '';
}
- $revert = Linker::linkKnown(
+ $revert = $this->getLinkRenderer()->makeKnownLink(
SpecialPage::getTitleFor( 'Movepage' ),
- $this->msg( 'revertmove' )->escaped(),
+ $this->msg( 'revertmove' )->text(),
[],
[
'wpOldTitle' => $destTitle->getPrefixedDBkey(),
diff --git a/www/wiki/includes/logging/PatrolLog.php b/www/wiki/includes/logging/PatrolLog.php
index d1de2cd3..9b2e0982 100644
--- a/www/wiki/includes/logging/PatrolLog.php
+++ b/www/wiki/includes/logging/PatrolLog.php
@@ -27,6 +27,7 @@
* logs of patrol events
*/
class PatrolLog {
+
/**
* Record a log event for a change being patrolled
*
@@ -39,10 +40,8 @@ class PatrolLog {
* @return bool
*/
public static function record( $rc, $auto = false, User $user = null, $tags = null ) {
- global $wgLogAutopatrol;
-
- // do not log autopatrolled edits if setting disables it
- if ( $auto && !$wgLogAutopatrol ) {
+ // Do not log autopatrol actions: T184485
+ if ( $auto ) {
return false;
}
diff --git a/www/wiki/includes/logging/PatrolLogFormatter.php b/www/wiki/includes/logging/PatrolLogFormatter.php
index bbd8badc..894f59b0 100644
--- a/www/wiki/includes/logging/PatrolLogFormatter.php
+++ b/www/wiki/includes/logging/PatrolLogFormatter.php
@@ -22,7 +22,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
* @since 1.22
*/
-use MediaWiki\MediaWikiServices;
/**
* This class formats patrol log entries.
@@ -55,8 +54,7 @@ class PatrolLogFormatter extends LogFormatter {
'oldid' => $oldid,
'diff' => 'prev'
];
- $revlink = MediaWikiServices::getInstance()->getLinkRenderer()->makeLink(
- $target, $revision, [], $query );
+ $revlink = $this->getLinkRenderer()->makeLink( $target, $revision, [], $query );
} else {
$revlink = htmlspecialchars( $revision );
}
diff --git a/www/wiki/includes/logging/ProtectLogFormatter.php b/www/wiki/includes/logging/ProtectLogFormatter.php
index 9e5eea54..64ec6269 100644
--- a/www/wiki/includes/logging/ProtectLogFormatter.php
+++ b/www/wiki/includes/logging/ProtectLogFormatter.php
@@ -21,7 +21,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
* @since 1.26
*/
-use MediaWiki\MediaWikiServices;
/**
* This class formats protect log entries.
@@ -78,7 +77,7 @@ class ProtectLogFormatter extends LogFormatter {
}
public function getActionLinks() {
- $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+ $linkRenderer = $this->getLinkRenderer();
$subtype = $this->entry->getSubtype();
if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden
|| $subtype === 'move_prot' // the move log entry has the right action link
diff --git a/www/wiki/includes/utils/iterators/NotRecursiveIterator.php b/www/wiki/includes/logging/WikitextLogFormatter.php
index 52ca61b4..13b55595 100644
--- a/www/wiki/includes/utils/iterators/NotRecursiveIterator.php
+++ b/www/wiki/includes/logging/WikitextLogFormatter.php
@@ -1,10 +1,6 @@
<?php
/**
- * Wraps a non-recursive iterator with methods to be recursive
- * without children.
- *
- * Alternatively wraps a recursive iterator to prevent recursing deeper
- * than the wrapped iterator.
+ * Formatter to allow log entries to contain formatted wikitext.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,14 +18,18 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup Maintenance
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
*/
-class NotRecursiveIterator extends IteratorDecorator implements RecursiveIterator {
- public function hasChildren() {
- return false;
- }
- public function getChildren() {
- return null;
+/**
+ * Log formatter specifically for log entries containing wikitext.
+ * @since 1.31
+ */
+class WikitextLogFormatter extends LogFormatter {
+ /**
+ * @return string
+ */
+ public function getActionMessage() {
+ return parent::getActionMessage()->parse();
}
}
diff --git a/www/wiki/includes/mail/EmailNotification.php b/www/wiki/includes/mail/EmailNotification.php
index 2931d9dd..67b7142a 100644
--- a/www/wiki/includes/mail/EmailNotification.php
+++ b/www/wiki/includes/mail/EmailNotification.php
@@ -90,7 +90,7 @@ class EmailNotification {
LinkTarget $linkTarget,
$timestamp
) {
- // wfDeprecated( __METHOD__, '1.27' );
+ wfDeprecated( __METHOD__, '1.27' );
$config = RequestContext::getMain()->getConfig();
if ( !$config->get( 'EnotifWatchlist' ) && !$config->get( 'ShowUpdatedMarker' ) ) {
return [];
diff --git a/www/wiki/includes/mail/MailAddress.php b/www/wiki/includes/mail/MailAddress.php
index ce1df0db..b9d94143 100644
--- a/www/wiki/includes/mail/MailAddress.php
+++ b/www/wiki/includes/mail/MailAddress.php
@@ -30,7 +30,6 @@
* header format when requested.
*/
class MailAddress {
-
/**
* @var string
*/
@@ -89,8 +88,9 @@ class MailAddress {
global $wgEnotifUseRealName;
$name = ( $wgEnotifUseRealName && $this->realName !== '' ) ? $this->realName : $this->name;
$quoted = UserMailer::quotedPrintable( $name );
- if ( strpos( $quoted, '.' ) !== false || strpos( $quoted, ',' ) !== false ) {
- $quoted = '"' . $quoted . '"';
+ // Must only be quoted if string does not use =? encoding (T191931)
+ if ( $quoted === $name ) {
+ $quoted = '"' . addslashes( $quoted ) . '"';
}
return "$quoted <{$this->address}>";
} else {
diff --git a/www/wiki/includes/mail/UserMailer.php b/www/wiki/includes/mail/UserMailer.php
index 93cbdf43..fcb61731 100644
--- a/www/wiki/includes/mail/UserMailer.php
+++ b/www/wiki/includes/mail/UserMailer.php
@@ -198,14 +198,8 @@ class UserMailer {
private static function isMailMimeUsable() {
static $usable = null;
if ( $usable === null ) {
- // If the class is not already loaded, and it's in the include path,
- // try requiring it.
- if ( !class_exists( 'Mail_mime' ) && stream_resolve_include_path( 'Mail/mime.php' ) ) {
- require_once 'Mail/mime.php';
- }
$usable = class_exists( 'Mail_mime' );
}
-
return $usable;
}
@@ -218,11 +212,6 @@ class UserMailer {
private static function isMailUsable() {
static $usable = null;
if ( $usable === null ) {
- // If the class is not already loaded, and it's in the include path,
- // try requiring it.
- if ( !class_exists( 'Mail' ) && stream_resolve_include_path( 'Mail.php' ) ) {
- require_once 'Mail.php';
- }
$usable = class_exists( 'Mail' );
}
@@ -393,13 +382,13 @@ class UserMailer {
throw new MWException( 'PEAR mail package is not installed' );
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
// Create the mail object using the Mail::factory method
- $mail_object =& Mail::factory( 'smtp', $wgSMTP );
+ $mail_object = Mail::factory( 'smtp', $wgSMTP );
if ( PEAR::isError( $mail_object ) ) {
wfDebug( "PEAR::Mail factory failed: " . $mail_object->getMessage() . "\n" );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return Status::newFatal( 'pear-mail-error', $mail_object->getMessage() );
}
@@ -419,11 +408,11 @@ class UserMailer {
$status = self::sendWithPear( $mail_object, $chunk, $headers, $body );
// FIXME : some chunks might be sent while others are not!
if ( !$status->isOK() ) {
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return $status;
}
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return Status::newGood();
} else {
// PHP mail()
@@ -442,7 +431,7 @@ class UserMailer {
try {
foreach ( $to as $recip ) {
$sent = mail(
- $recip,
+ $recip->toString(),
self::quotedPrintable( $subject ),
$body,
$headers,
diff --git a/www/wiki/includes/media/Bitmap.php b/www/wiki/includes/media/Bitmap.php
index ac39e6f3..b88a34dc 100644
--- a/www/wiki/includes/media/Bitmap.php
+++ b/www/wiki/includes/media/Bitmap.php
@@ -112,14 +112,14 @@ class BitmapHandler extends TransformationalImageHandler {
*/
protected function imageMagickSubsampling( $pixelFormat ) {
switch ( $pixelFormat ) {
- case 'yuv444':
- return [ '1x1', '1x1', '1x1' ];
- case 'yuv422':
- return [ '2x1', '1x1', '1x1' ];
- case 'yuv420':
- return [ '2x2', '1x1', '1x1' ];
- default:
- throw new MWException( 'Invalid pixel format for JPEG output' );
+ case 'yuv444':
+ return [ '1x1', '1x1', '1x1' ];
+ case 'yuv422':
+ return [ '2x1', '1x1', '1x1' ];
+ case 'yuv420':
+ return [ '2x2', '1x1', '1x1' ];
+ default:
+ throw new MWException( 'Invalid pixel format for JPEG output' );
}
}
@@ -203,9 +203,9 @@ class BitmapHandler extends TransformationalImageHandler {
'-layers', 'merge',
'-background', 'white',
];
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$xcfMeta = unserialize( $image->getMetadata() );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $xcfMeta
&& isset( $xcfMeta['colorType'] )
&& $xcfMeta['colorType'] === 'greyscale-alpha'
@@ -440,6 +440,14 @@ class BitmapHandler extends TransformationalImageHandler {
return $this->getMediaTransformError( $params, $errMsg );
}
+ if ( filesize( $params['srcPath'] ) === 0 ) {
+ $err = "Image file size seems to be zero.";
+ wfDebug( "$err\n" );
+ $errMsg = wfMessage( 'thumbnail_image-size-zero', $params['srcPath'] )->text();
+
+ return $this->getMediaTransformError( $params, $errMsg );
+ }
+
$src_image = call_user_func( $loader, $params['srcPath'] );
$rotation = function_exists( 'imagerotate' ) && !isset( $params['disableRotation'] ) ?
diff --git a/www/wiki/includes/media/BitmapMetadataHandler.php b/www/wiki/includes/media/BitmapMetadataHandler.php
index 35c97518..9e0fc3db 100644
--- a/www/wiki/includes/media/BitmapMetadataHandler.php
+++ b/www/wiki/includes/media/BitmapMetadataHandler.php
@@ -170,7 +170,7 @@ class BitmapMetadataHandler {
}
}
if ( isset( $seg['XMP'] ) && $showXMP ) {
- $xmp = new XMPReader( LoggerFactory::getInstance( 'XMP' ) );
+ $xmp = new XMPReader( LoggerFactory::getInstance( 'XMP' ), $filename );
$xmp->parse( $seg['XMP'] );
foreach ( $seg['XMP_ext'] as $xmpExt ) {
/* Support for extended xmp in jpeg files
@@ -205,7 +205,7 @@ class BitmapMetadataHandler {
if ( isset( $array['text']['xmp']['x-default'] )
&& $array['text']['xmp']['x-default'] !== '' && $showXMP
) {
- $xmp = new XMPReader( LoggerFactory::getInstance( 'XMP' ) );
+ $xmp = new XMPReader( LoggerFactory::getInstance( 'XMP' ), $filename );
$xmp->parse( $array['text']['xmp']['x-default'] );
$xmpRes = $xmp->getResults();
foreach ( $xmpRes as $type => $xmpSection ) {
@@ -238,7 +238,7 @@ class BitmapMetadataHandler {
}
if ( $baseArray['xmp'] !== '' && XMPReader::isSupported() ) {
- $xmp = new XMPReader( LoggerFactory::getInstance( 'XMP' ) );
+ $xmp = new XMPReader( LoggerFactory::getInstance( 'XMP' ), $filename );
$xmp->parse( $baseArray['xmp'] );
$xmpRes = $xmp->getResults();
foreach ( $xmpRes as $type => $xmpSection ) {
@@ -292,7 +292,7 @@ class BitmapMetadataHandler {
* Read the first 2 bytes of a tiff file to figure out
* Little Endian or Big Endian. Needed for exif stuff.
*
- * @param string $filename The filename
+ * @param string $filename
* @return string 'BE' or 'LE' or false
*/
static function getTiffByteOrder( $filename ) {
diff --git a/www/wiki/includes/media/Bitmap_ClientOnly.php b/www/wiki/includes/media/Bitmap_ClientOnly.php
index 3ec87723..fa5b0a61 100644
--- a/www/wiki/includes/media/Bitmap_ClientOnly.php
+++ b/www/wiki/includes/media/Bitmap_ClientOnly.php
@@ -29,9 +29,8 @@
*
* @ingroup Media
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class BitmapHandler_ClientOnly extends BitmapHandler {
- // @codingStandardsIgnoreEnd
/**
* @param File $image
diff --git a/www/wiki/includes/media/DjVu.php b/www/wiki/includes/media/DjVu.php
index aae66d37..2541e35b 100644
--- a/www/wiki/includes/media/DjVu.php
+++ b/www/wiki/includes/media/DjVu.php
@@ -265,9 +265,9 @@ class DjVuHandler extends ImageHandler {
return $metadata;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$unser = unserialize( $metadata );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( is_array( $unser ) ) {
if ( isset( $unser['error'] ) ) {
return false;
@@ -321,7 +321,7 @@ class DjVuHandler extends ImageHandler {
* @return array
*/
protected function extractTreesFromMetadata( $metadata ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
try {
// Set to false rather than null to avoid further attempts
$metaTree = false;
@@ -344,7 +344,7 @@ class DjVuHandler extends ImageHandler {
} catch ( Exception $e ) {
wfDebug( "Bogus multipage XML metadata\n" );
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return [ 'MetaTree' => $metaTree, 'TextTree' => $textTree ];
}
@@ -357,7 +357,7 @@ class DjVuHandler extends ImageHandler {
global $wgDjvuOutputExtension;
static $mime;
if ( !isset( $mime ) ) {
- $magic = MimeMagic::singleton();
+ $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
$mime = $magic->guessTypesForExtension( $wgDjvuOutputExtension );
}
diff --git a/www/wiki/includes/media/DjVuImage.php b/www/wiki/includes/media/DjVuImage.php
index d25111c4..adcac252 100644
--- a/www/wiki/includes/media/DjVuImage.php
+++ b/www/wiki/includes/media/DjVuImage.php
@@ -117,9 +117,9 @@ class DjVuImage {
}
function getInfo() {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$file = fopen( $this->mFilename, 'rb' );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $file === false ) {
wfDebug( __METHOD__ . ": missing or failed file read\n" );
diff --git a/www/wiki/includes/media/Exif.php b/www/wiki/includes/media/Exif.php
index cd457f0b..70f8d5c0 100644
--- a/www/wiki/includes/media/Exif.php
+++ b/www/wiki/includes/media/Exif.php
@@ -292,9 +292,9 @@ class Exif {
$this->debugFile( $this->basename, __FUNCTION__, true );
if ( function_exists( 'exif_read_data' ) ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$data = exif_read_data( $this->file, 0, true );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
} else {
throw new MWException( "Internal error: exif_read_data not present. " .
"\$wgShowEXIF may be incorrectly set or not checked by an extension." );
@@ -467,17 +467,17 @@ class Exif {
break;
}
if ( $charset ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$val = iconv( $charset, 'UTF-8//IGNORE', $val );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
} else {
// if valid utf-8, assume that, otherwise assume windows-1252
$valCopy = $val;
UtfNormal\Validator::quickIsNFCVerify( $valCopy ); // validates $valCopy.
if ( $valCopy !== $val ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$val = iconv( 'Windows-1252', 'UTF-8//IGNORE', $val );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
}
@@ -742,12 +742,16 @@ class Exif {
$ecount = 1; // checking individual elements
}
}
- $count = count( $val );
- if ( $ecount != $count ) {
- $this->debug( $val, __FUNCTION__, "Expected $ecount elements for $tag but got $count" );
- return false;
+ $count = 1;
+ if ( is_array( $val ) ) {
+ $count = count( $val );
+ if ( $ecount != $count ) {
+ $this->debug( $val, __FUNCTION__, "Expected $ecount elements for $tag but got $count" );
+ return false;
+ }
}
+ // If there are multiple values, recursively validate each of them.
if ( $count > 1 ) {
foreach ( $val as $v ) {
if ( !$this->validate( $section, $tag, $v, true ) ) {
diff --git a/www/wiki/includes/media/ExifBitmap.php b/www/wiki/includes/media/ExifBitmap.php
index 0e10abb9..42672109 100644
--- a/www/wiki/includes/media/ExifBitmap.php
+++ b/www/wiki/includes/media/ExifBitmap.php
@@ -99,9 +99,9 @@ class ExifBitmapHandler extends BitmapHandler {
if ( $metadata === self::BROKEN_FILE ) {
return self::METADATA_GOOD;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$exif = unserialize( $metadata );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] )
|| $exif['MEDIAWIKI_EXIF_VERSION'] != Exif::version()
) {
@@ -223,9 +223,9 @@ class ExifBitmapHandler extends BitmapHandler {
if ( !$data ) {
return 0;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$data = unserialize( $data );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( isset( $data['Orientation'] ) ) {
# See http://sylvana.net/jpegcrop/exif_orientation.html
switch ( $data['Orientation'] ) {
diff --git a/www/wiki/includes/media/FormatMetadata.php b/www/wiki/includes/media/FormatMetadata.php
index 79032232..6cb33ac4 100644
--- a/www/wiki/includes/media/FormatMetadata.php
+++ b/www/wiki/includes/media/FormatMetadata.php
@@ -271,7 +271,7 @@ class FormatMetadata extends ContextSource {
// TODO: YCbCrCoefficients #p27 (see annex E)
case 'ExifVersion':
case 'FlashpixVersion':
- $val = "$val" / 100;
+ $val = (int)$val / 100;
break;
case 'ColorSpace':
@@ -740,8 +740,13 @@ class FormatMetadata extends ContextSource {
case 'Software':
if ( is_array( $val ) ) {
- // if its a software, version array.
- $val = $this->msg( 'exif-software-version-value', $val[0], $val[1] )->text();
+ if ( count( $val ) > 1 ) {
+ // if its a software, version array.
+ $val = $this->msg( 'exif-software-version-value', $val[0], $val[1] )->text();
+ } else {
+ // https://phabricator.wikimedia.org/T178130
+ $val = $this->exifMsg( $tag, '', $val[0] );
+ }
} else {
$val = $this->exifMsg( $tag, '', $val );
}
@@ -1761,9 +1766,9 @@ class FormatMetadata extends ContextSource {
}
return $newValue;
} else { // _type is 'ul' or 'ol' or missing in which case it defaults to 'ul'
- list( $k, $v ) = each( $value );
- if ( $k === '_type' ) {
- $v = current( $value );
+ $v = reset( $value );
+ if ( key( $value ) === '_type' ) {
+ $v = next( $value );
}
return $v;
}
@@ -1854,9 +1859,9 @@ class FormatMetadata extends ContextSource {
// drop all characters which are not valid in an XML tag name
// a bunch of non-ASCII letters would be valid but probably won't
// be used so we take the easy way
- $key = preg_replace( '/[^a-zA-z0-9_:.-]/', '', $key );
+ $key = preg_replace( '/[^a-zA-z0-9_:.\-]/', '', $key );
// drop characters which are invalid at the first position
- $key = preg_replace( '/^[\d-.]+/', '', $key );
+ $key = preg_replace( '/^[\d\-.]+/', '', $key );
if ( $key == '' ) {
$key = '_';
diff --git a/www/wiki/includes/media/GIF.php b/www/wiki/includes/media/GIF.php
index 5f23855c..d65f8726 100644
--- a/www/wiki/includes/media/GIF.php
+++ b/www/wiki/includes/media/GIF.php
@@ -131,9 +131,9 @@ class GIFHandler extends BitmapHandler {
return self::METADATA_GOOD;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$data = unserialize( $metadata );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$data || !is_array( $data ) ) {
wfDebug( __METHOD__ . " invalid GIF metadata\n" );
@@ -161,9 +161,9 @@ class GIFHandler extends BitmapHandler {
$original = parent::getLongDesc( $image );
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$metadata = unserialize( $image->getMetadata() );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$metadata || $metadata['frameCount'] <= 1 ) {
return $original;
@@ -198,9 +198,9 @@ class GIFHandler extends BitmapHandler {
*/
public function getLength( $file ) {
$serMeta = $file->getMetadata();
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$metadata = unserialize( $serMeta );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$metadata || !isset( $metadata['duration'] ) || !$metadata['duration'] ) {
return 0.0;
diff --git a/www/wiki/includes/media/GIFMetadataExtractor.php b/www/wiki/includes/media/GIFMetadataExtractor.php
index ac5fc81c..a26539a8 100644
--- a/www/wiki/includes/media/GIFMetadataExtractor.php
+++ b/www/wiki/includes/media/GIFMetadataExtractor.php
@@ -161,9 +161,9 @@ class GIFMetadataExtractor {
UtfNormal\Validator::quickIsNFCVerify( $dataCopy );
if ( $dataCopy !== $data ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$data = iconv( 'windows-1252', 'UTF-8', $data );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
$commentCount = count( $comment );
diff --git a/www/wiki/includes/media/IPTC.php b/www/wiki/includes/media/IPTC.php
index 343adc20..441c515f 100644
--- a/www/wiki/includes/media/IPTC.php
+++ b/www/wiki/includes/media/IPTC.php
@@ -75,7 +75,7 @@ class IPTC {
* Title, person. Not sure if this is best
* approach since we no longer have the two fields
* separate. each byline title entry corresponds to a
- * specific byline. */
+ * specific byline. */
$bylines = self::convIPTC( $val, $c );
if ( isset( $parsed['2#085'] ) ) {
@@ -353,20 +353,20 @@ class IPTC {
* @todo Potentially this should also capture the timezone offset.
* @param array $date The date tag
* @param array $time The time tag
- * @param string $c The charset
+ * @param string $charset
* @return string Date in EXIF format.
*/
- private static function timeHelper( $date, $time, $c ) {
+ private static function timeHelper( $date, $time, $charset ) {
if ( count( $date ) === 1 ) {
// the standard says this should always be 1
// just double checking.
- list( $date ) = self::convIPTC( $date, $c );
+ list( $date ) = self::convIPTC( $date, $charset );
} else {
return null;
}
if ( count( $time ) === 1 ) {
- list( $time ) = self::convIPTC( $time, $c );
+ list( $time ) = self::convIPTC( $time, $charset );
$dateOnly = false;
} else {
$time = '000000+0000'; // placeholder
@@ -420,7 +420,7 @@ class IPTC {
/**
* Helper function to convert charset for iptc values.
* @param string|array $data The iptc string
- * @param string $charset The charset
+ * @param string $charset
*
* @return string|array
*/
@@ -439,15 +439,15 @@ class IPTC {
/**
* Helper function of a helper function to convert charset for iptc values.
* @param string|array $data The IPTC string
- * @param string $charset The charset
+ * @param string $charset
*
* @return string
*/
private static function convIPTCHelper( $data, $charset ) {
if ( $charset ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$data = iconv( $charset, "UTF-8//IGNORE", $data );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $data === false ) {
$data = "";
wfDebugLog( 'iptc', __METHOD__ . " Error converting iptc data charset $charset to utf-8" );
diff --git a/www/wiki/includes/media/ImageHandler.php b/www/wiki/includes/media/ImageHandler.php
index 1eefddbd..a0a16032 100644
--- a/www/wiki/includes/media/ImageHandler.php
+++ b/www/wiki/includes/media/ImageHandler.php
@@ -201,9 +201,9 @@ abstract class ImageHandler extends MediaHandler {
}
function getImageSize( $image, $path ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$gis = getimagesize( $path );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return $gis;
}
diff --git a/www/wiki/includes/media/JpegMetadataExtractor.php b/www/wiki/includes/media/JpegMetadataExtractor.php
index 211845cc..3c778f36 100644
--- a/www/wiki/includes/media/JpegMetadataExtractor.php
+++ b/www/wiki/includes/media/JpegMetadataExtractor.php
@@ -82,7 +82,7 @@ class JpegMetadataExtractor {
// this is just a sanity check
throw new MWException( 'Too many jpeg segments. Aborting' );
}
- while ( $buffer !== "\xFF" ) {
+ while ( $buffer !== "\xFF" && !feof( $fh ) ) {
// In theory JPEG files are not allowed to contain anything between the sections,
// but in practice they sometimes do. It's customary to ignore the garbage data.
$buffer = fread( $fh, 1 );
@@ -102,9 +102,9 @@ class JpegMetadataExtractor {
// turns $com to valid utf-8.
// thus if no change, its utf-8, otherwise its something else.
if ( $com !== $oldCom ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$com = $oldCom = iconv( 'windows-1252', 'UTF-8//IGNORE', $oldCom );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
// Try it again, if its still not a valid string, then probably
// binary junk or some really weird encoding, so don't extract.
@@ -158,6 +158,8 @@ class JpegMetadataExtractor {
if ( $size['int'] < 2 ) {
throw new MWException( "invalid marker size in jpeg" );
}
+ // Note it's possible to seek beyond end of file if truncated.
+ // fseek doesn't report a failure in this case.
fseek( $fh, $size['int'] - 2, SEEK_CUR );
}
}
diff --git a/www/wiki/includes/media/MediaHandler.php b/www/wiki/includes/media/MediaHandler.php
index aa7c62be..c76930cf 100644
--- a/www/wiki/includes/media/MediaHandler.php
+++ b/www/wiki/includes/media/MediaHandler.php
@@ -158,9 +158,9 @@ abstract class MediaHandler {
function convertMetadataVersion( $metadata, $version = 1 ) {
if ( !is_array( $metadata ) ) {
// unserialize to keep return parameter consistent.
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ret = unserialize( $metadata );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return $ret;
}
@@ -255,7 +255,7 @@ abstract class MediaHandler {
* Get a MediaTransformOutput object representing the transformed output. Does not
* actually do the transform.
*
- * @param File $image The image object
+ * @param File $image
* @param string $dstPath Filesystem destination path
* @param string $dstUrl Destination URL to use in output HTML
* @param array $params Arbitrary set of parameters validated by $this->validateParam()
@@ -269,7 +269,7 @@ abstract class MediaHandler {
* Get a MediaTransformOutput object representing the transformed output. Does the
* transform unless $flags contains self::TRANSFORM_LATER.
*
- * @param File $image The image object
+ * @param File $image
* @param string $dstPath Filesystem destination path
* @param string $dstUrl Destination URL to use in output HTML
* @param array $params Arbitrary set of parameters validated by $this->validateParam()
@@ -288,7 +288,7 @@ abstract class MediaHandler {
* @return array Thumbnail extension and MIME type
*/
function getThumbType( $ext, $mime, $params = null ) {
- $magic = MimeMagic::singleton();
+ $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
if ( !$ext || $magic->isMatchingExtension( $ext, $mime ) === false ) {
// The extension is not valid for this MIME type and we do
// recognize the MIME type
@@ -497,7 +497,7 @@ abstract class MediaHandler {
*
* This is used by the media handlers that use the FormatMetadata class
*
- * @param array $metadataArray Metadata array
+ * @param array $metadataArray
* @param bool|IContextSource $context Context to use (optional)
* @return array Array for use displaying metadata.
*/
@@ -921,6 +921,6 @@ abstract class MediaHandler {
* @since 1.30
*/
public function getContentHeaders( $metadata ) {
- return [];
+ return [ 'X-Content-Dimensions' => '' ]; // T175689
}
}
diff --git a/www/wiki/includes/media/MediaTransformOutput.php b/www/wiki/includes/media/MediaTransformOutput.php
index 5366c4fa..3506684a 100644
--- a/www/wiki/includes/media/MediaTransformOutput.php
+++ b/www/wiki/includes/media/MediaTransformOutput.php
@@ -47,13 +47,13 @@ abstract class MediaTransformOutput {
/** @var bool|string */
protected $page;
- /** @var bool|string Filesystem path to the thumb */
+ /** @var bool|string Filesystem path to the thumb */
protected $path;
/** @var bool|string Language code, false if not set */
protected $lang;
- /** @var bool|string Permanent storage path */
+ /** @var bool|string Permanent storage path */
protected $storagePath = false;
/**
diff --git a/www/wiki/includes/media/PNG.php b/www/wiki/includes/media/PNG.php
index b6288bc3..6748b26b 100644
--- a/www/wiki/includes/media/PNG.php
+++ b/www/wiki/includes/media/PNG.php
@@ -117,9 +117,9 @@ class PNGHandler extends BitmapHandler {
return self::METADATA_GOOD;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$data = unserialize( $metadata );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$data || !is_array( $data ) ) {
wfDebug( __METHOD__ . " invalid png metadata\n" );
@@ -146,9 +146,9 @@ class PNGHandler extends BitmapHandler {
global $wgLang;
$original = parent::getLongDesc( $image );
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$metadata = unserialize( $image->getMetadata() );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$metadata || $metadata['frameCount'] <= 0 ) {
return $original;
@@ -184,9 +184,9 @@ class PNGHandler extends BitmapHandler {
*/
public function getLength( $file ) {
$serMeta = $file->getMetadata();
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$metadata = unserialize( $serMeta );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$metadata || !isset( $metadata['duration'] ) || !$metadata['duration'] ) {
return 0.0;
diff --git a/www/wiki/includes/media/PNGMetadataExtractor.php b/www/wiki/includes/media/PNGMetadataExtractor.php
index c12ca0bf..78ed0bcd 100644
--- a/www/wiki/includes/media/PNGMetadataExtractor.php
+++ b/www/wiki/includes/media/PNGMetadataExtractor.php
@@ -202,9 +202,9 @@ class PNGMetadataExtractor {
// if compressed
if ( $items[2] == "\x01" ) {
if ( function_exists( 'gzuncompress' ) && $items[4] === "\x00" ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$items[5] = gzuncompress( $items[5] );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $items[5] === false ) {
// decompression failed
@@ -246,9 +246,9 @@ class PNGMetadataExtractor {
fseek( $fh, self::$crcSize, SEEK_CUR );
continue;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$content = iconv( 'ISO-8859-1', 'UTF-8', $content );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $content === false ) {
throw new Exception( __METHOD__ . ": Read error (error with iconv)" );
@@ -286,9 +286,9 @@ class PNGMetadataExtractor {
continue;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$content = gzuncompress( $content );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $content === false ) {
// decompression failed
@@ -297,9 +297,9 @@ class PNGMetadataExtractor {
continue;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$content = iconv( 'ISO-8859-1', 'UTF-8', $content );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $content === false ) {
throw new Exception( __METHOD__ . ": Read error (error with iconv)" );
diff --git a/www/wiki/includes/media/SVG.php b/www/wiki/includes/media/SVG.php
index bd78b49e..9085421a 100644
--- a/www/wiki/includes/media/SVG.php
+++ b/www/wiki/includes/media/SVG.php
@@ -97,19 +97,50 @@ class SvgHandler extends ImageHandler {
if ( isset( $metadata['translations'] ) ) {
foreach ( $metadata['translations'] as $lang => $langType ) {
if ( $langType === SVGReader::LANG_FULL_MATCH ) {
- $langList[] = $lang;
+ $langList[] = strtolower( $lang );
}
}
}
}
- return $langList;
+ return array_unique( $langList );
}
/**
- * What language to render file in if none selected.
+ * SVG's systemLanguage matching rules state:
+ * 'The `systemLanguage` attribute ... [e]valuates to "true" if one of the languages indicated
+ * by user preferences exactly equals one of the languages given in the value of this parameter,
+ * or if one of the languages indicated by user preferences exactly equals a prefix of one of
+ * the languages given in the value of this parameter such that the first tag character
+ * following the prefix is "-".'
*
- * @param File $file
- * @return string Language code.
+ * Return the first element of $svgLanguages that matches $userPreferredLanguage
+ *
+ * @see https://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute
+ * @param string $userPreferredLanguage
+ * @param array $svgLanguages
+ * @return string|null
+ */
+ public function getMatchedLanguage( $userPreferredLanguage, array $svgLanguages ) {
+ foreach ( $svgLanguages as $svgLang ) {
+ if ( strcasecmp( $svgLang, $userPreferredLanguage ) === 0 ) {
+ return $svgLang;
+ }
+ $trimmedSvgLang = $svgLang;
+ while ( strpos( $trimmedSvgLang, '-' ) !== false ) {
+ $trimmedSvgLang = substr( $trimmedSvgLang, 0, strrpos( $trimmedSvgLang, '-' ) );
+ if ( strcasecmp( $trimmedSvgLang, $userPreferredLanguage ) === 0 ) {
+ return $svgLang;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * What language to render file in if none selected
+ *
+ * @param File $file Language code
+ * @return string
*/
public function getDefaultRenderLanguage( File $file ) {
return 'en';
@@ -218,10 +249,10 @@ class SvgHandler extends ImageHandler {
$ok = symlink( $srcPath, $lnPath );
/** @noinspection PhpUnusedLocalVariableInspection */
$cleaner = new ScopedCallback( function () use ( $tmpDir, $lnPath ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
unlink( $lnPath );
rmdir( $tmpDir );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
} );
if ( !$ok ) {
wfDebugLog( 'thumbnail',
@@ -387,9 +418,9 @@ class SvgHandler extends ImageHandler {
}
function unpackMetadata( $metadata ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$unser = unserialize( $metadata );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) {
return $unser;
} else {
@@ -479,9 +510,7 @@ class SvgHandler extends ImageHandler {
return ( $value > 0 );
} elseif ( $name == 'lang' ) {
// Validate $code
- if ( $value === '' || !Language::isValidBuiltInCode( $value ) ) {
- wfDebug( "Invalid user language code\n" );
-
+ if ( $value === '' || !Language::isValidCode( $value ) ) {
return false;
}
@@ -499,8 +528,7 @@ class SvgHandler extends ImageHandler {
public function makeParamString( $params ) {
$lang = '';
if ( isset( $params['lang'] ) && $params['lang'] !== 'en' ) {
- $params['lang'] = strtolower( $params['lang'] );
- $lang = "lang{$params['lang']}-";
+ $lang = 'lang' . strtolower( $params['lang'] ) . '-';
}
if ( !isset( $params['width'] ) ) {
return false;
@@ -511,7 +539,7 @@ class SvgHandler extends ImageHandler {
public function parseParamString( $str ) {
$m = false;
- if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) {
+ if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/i', $str, $m ) ) {
return [ 'width' => array_pop( $m ), 'lang' => $m[1] ];
} elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
return [ 'width' => $m[1], 'lang' => 'en' ];
diff --git a/www/wiki/includes/media/SVGMetadataExtractor.php b/www/wiki/includes/media/SVGMetadataExtractor.php
index 9b22cbee..fc93b233 100644
--- a/www/wiki/includes/media/SVGMetadataExtractor.php
+++ b/www/wiki/includes/media/SVGMetadataExtractor.php
@@ -106,17 +106,17 @@ class SVGReader {
// Because we cut off the end of the svg making an invalid one. Complicated
// try catch thing to make sure warnings get restored. Seems like there should
// be a better way.
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
try {
$this->read();
} catch ( Exception $e ) {
// Note, if this happens, the width/height will be taken to be 0x0.
// Should we consider it the default 512x512 instead?
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
libxml_disable_entity_loader( $oldDisable );
throw $e;
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
libxml_disable_entity_loader( $oldDisable );
}
diff --git a/www/wiki/includes/media/TransformationalImageHandler.php b/www/wiki/includes/media/TransformationalImageHandler.php
index de438da2..85430d2c 100644
--- a/www/wiki/includes/media/TransformationalImageHandler.php
+++ b/www/wiki/includes/media/TransformationalImageHandler.php
@@ -512,7 +512,7 @@ abstract class TransformationalImageHandler extends ImageHandler {
$cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
$method = __METHOD__;
return $cache->getWithSetCallback(
- 'imagemagick-version',
+ $cache->makeGlobalKey( 'imagemagick-version' ),
$cache::TTL_HOUR,
function () use ( $method ) {
global $wgImageMagickConvertCommand;
diff --git a/www/wiki/includes/media/WebP.php b/www/wiki/includes/media/WebP.php
index e23989df..295a9785 100644
--- a/www/wiki/includes/media/WebP.php
+++ b/www/wiki/includes/media/WebP.php
@@ -63,9 +63,9 @@ class WebPHandler extends BitmapHandler {
return self::METADATA_GOOD;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$data = unserialize( $metadata );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$data || !is_array( $data ) ) {
wfDebug( __METHOD__ . " invalid WebP metadata\n" );
@@ -154,7 +154,7 @@ class WebPHandler extends BitmapHandler {
/**
* Decodes a lossy chunk header
- * @param string $header Header string
+ * @param string $header First few bytes of the header, expected to be at least 18 bytes long
* @return bool|array See WebPHandler::decodeHeader
*/
protected static function decodeLossyChunkHeader( $header ) {
@@ -180,7 +180,7 @@ class WebPHandler extends BitmapHandler {
/**
* Decodes a lossless chunk header
- * @param string $header Header string
+ * @param string $header First few bytes of the header, expected to be at least 13 bytes long
* @return bool|array See WebPHandler::decodeHeader
*/
public static function decodeLosslessChunkHeader( $header ) {
@@ -205,7 +205,7 @@ class WebPHandler extends BitmapHandler {
/**
* Decodes an extended chunk header
- * @param string $header Header string
+ * @param string $header First few bytes of the header, expected to be at least 18 bytes long
* @return bool|array See WebPHandler::decodeHeader
*/
public static function decodeExtendedChunkHeader( $header ) {
@@ -235,9 +235,9 @@ class WebPHandler extends BitmapHandler {
$metadata = $file->getMetadata();
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$metadata = unserialize( $metadata );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $metadata == false ) {
return false;
diff --git a/www/wiki/includes/media/XCF.php b/www/wiki/includes/media/XCF.php
index c4195240..491fef21 100644
--- a/www/wiki/includes/media/XCF.php
+++ b/www/wiki/includes/media/XCF.php
@@ -151,7 +151,7 @@ class XCFHandler extends BitmapHandler {
*
* @param File|FSFile $file The image object, or false if there isn't one.
* Warning, FSFile::getPropsFromPath might pass an (object)array() instead (!)
- * @param string $filename The filename
+ * @param string $filename
* @return string
*/
public function getMetadata( $file, $filename ) {
@@ -162,18 +162,17 @@ class XCFHandler extends BitmapHandler {
// Unclear from base media type if it has an alpha layer,
// so just assume that it does since it "potentially" could.
switch ( $header['base_type'] ) {
- case 0:
- $metadata['colorType'] = 'truecolour-alpha';
- break;
- case 1:
- $metadata['colorType'] = 'greyscale-alpha';
- break;
- case 2:
- $metadata['colorType'] = 'index-coloured';
- break;
- default:
- $metadata['colorType'] = 'unknown';
-
+ case 0:
+ $metadata['colorType'] = 'truecolour-alpha';
+ break;
+ case 1:
+ $metadata['colorType'] = 'greyscale-alpha';
+ break;
+ case 2:
+ $metadata['colorType'] = 'index-coloured';
+ break;
+ default:
+ $metadata['colorType'] = 'unknown';
}
} else {
// Marker to prevent repeated attempted extraction
@@ -218,9 +217,9 @@ class XCFHandler extends BitmapHandler {
* @return bool
*/
public function canRender( $file ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$xcfMeta = unserialize( $file->getMetadata() );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( isset( $xcfMeta['colorType'] ) && $xcfMeta['colorType'] === 'index-coloured' ) {
return false;
}
diff --git a/www/wiki/includes/media/XMP.php b/www/wiki/includes/media/XMP.php
deleted file mode 100644
index 70f67b78..00000000
--- a/www/wiki/includes/media/XMP.php
+++ /dev/null
@@ -1,1383 +0,0 @@
-<?php
-/**
- * Reader for XMP data containing properties relevant to images.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Media
- */
-
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-
-/**
- * Class for reading xmp data containing properties relevant to
- * images, and spitting out an array that FormatMetadata accepts.
- *
- * Note, this is not meant to recognize every possible thing you can
- * encode in XMP. It should recognize all the properties we want.
- * For example it doesn't have support for structures with multiple
- * nesting levels, as none of the properties we're supporting use that
- * feature. If it comes across properties it doesn't recognize, it should
- * ignore them.
- *
- * The public methods one would call in this class are
- * - parse( $content )
- * Reads in xmp content.
- * Can potentially be called multiple times with partial data each time.
- * - parseExtended( $content )
- * Reads XMPExtended blocks (jpeg files only).
- * - getResults
- * Outputs a results array.
- *
- * Note XMP kind of looks like rdf. They are not the same thing - XMP is
- * encoded as a specific subset of rdf. This class can read XMP. It cannot
- * read rdf.
- *
- */
-class XMPReader implements LoggerAwareInterface {
- /** @var array XMP item configuration array */
- protected $items;
-
- /** @var array Array to hold the current element (and previous element, and so on) */
- private $curItem = [];
-
- /** @var bool|string The structure name when processing nested structures. */
- private $ancestorStruct = false;
-
- /** @var bool|string Temporary holder for character data that appears in xmp doc. */
- private $charContent = false;
-
- /** @var array Stores the state the xmpreader is in (see MODE_FOO constants) */
- private $mode = [];
-
- /** @var array Array to hold results */
- private $results = [];
-
- /** @var bool If we're doing a seq or bag. */
- private $processingArray = false;
-
- /** @var bool|string Used for lang alts only */
- private $itemLang = false;
-
- /** @var resource A resource handle for the XML parser */
- private $xmlParser;
-
- /** @var bool|string Character set like 'UTF-8' */
- private $charset = false;
-
- /** @var int */
- private $extendedXMPOffset = 0;
-
- /** @var int Flag determining if the XMP is safe to parse **/
- private $parsable = 0;
-
- /** @var string Buffer of XML to parse **/
- private $xmlParsableBuffer = '';
-
- /**
- * These are various mode constants.
- * they are used to figure out what to do
- * with an element when its encountered.
- *
- * For example, MODE_IGNORE is used when processing
- * a property we're not interested in. So if a new
- * element pops up when we're in that mode, we ignore it.
- */
- const MODE_INITIAL = 0;
- const MODE_IGNORE = 1;
- const MODE_LI = 2;
- const MODE_LI_LANG = 3;
- const MODE_QDESC = 4;
-
- // The following MODE constants are also used in the
- // $items array to denote what type of property the item is.
- const MODE_SIMPLE = 10;
- const MODE_STRUCT = 11; // structure (associative array)
- const MODE_SEQ = 12; // ordered list
- const MODE_BAG = 13; // unordered list
- const MODE_LANG = 14;
- const MODE_ALT = 15; // non-language alt. Currently not implemented, and not needed atm.
- const MODE_BAGSTRUCT = 16; // A BAG of Structs.
-
- const NS_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
- const NS_XML = 'http://www.w3.org/XML/1998/namespace';
-
- // States used while determining if XML is safe to parse
- const PARSABLE_UNKNOWN = 0;
- const PARSABLE_OK = 1;
- const PARSABLE_BUFFERING = 2;
- const PARSABLE_NO = 3;
-
- /**
- * @var LoggerInterface
- */
- private $logger;
-
- /**
- * Constructor.
- *
- * Primary job is to initialize the XMLParser
- */
- function __construct( LoggerInterface $logger = null ) {
-
- if ( !function_exists( 'xml_parser_create_ns' ) ) {
- // this should already be checked by this point
- throw new RuntimeException( 'XMP support requires XML Parser' );
- }
- if ( $logger ) {
- $this->setLogger( $logger );
- } else {
- $this->setLogger( new NullLogger() );
- }
-
- $this->items = XMPInfo::getItems();
-
- $this->resetXMLParser();
- }
-
- public function setLogger( LoggerInterface $logger ) {
- $this->logger = $logger;
- }
-
- /**
- * free the XML parser.
- *
- * @note It is unclear to me if we really need to do this ourselves
- * or if php garbage collection will automatically free the xmlParser
- * when it is no longer needed.
- */
- private function destroyXMLParser() {
- if ( $this->xmlParser ) {
- xml_parser_free( $this->xmlParser );
- $this->xmlParser = null;
- }
- }
-
- /**
- * Main use is if a single item has multiple xmp documents describing it.
- * For example in jpeg's with extendedXMP
- */
- private function resetXMLParser() {
-
- $this->destroyXMLParser();
-
- $this->xmlParser = xml_parser_create_ns( 'UTF-8', ' ' );
- xml_parser_set_option( $this->xmlParser, XML_OPTION_CASE_FOLDING, 0 );
- xml_parser_set_option( $this->xmlParser, XML_OPTION_SKIP_WHITE, 1 );
-
- xml_set_element_handler( $this->xmlParser,
- [ $this, 'startElement' ],
- [ $this, 'endElement' ] );
-
- xml_set_character_data_handler( $this->xmlParser, [ $this, 'char' ] );
-
- $this->parsable = self::PARSABLE_UNKNOWN;
- $this->xmlParsableBuffer = '';
- }
-
- /**
- * Check if this instance supports using this class
- */
- public static function isSupported() {
- return function_exists( 'xml_parser_create_ns' ) && class_exists( 'XMLReader' );
- }
-
- /** Get the result array. Do some post-processing before returning
- * the array, and transform any metadata that is special-cased.
- *
- * @return array Array of results as an array of arrays suitable for
- * FormatMetadata::getFormattedData().
- */
- public function getResults() {
- // xmp-special is for metadata that affects how stuff
- // is extracted. For example xmpNote:HasExtendedXMP.
-
- // It is also used to handle photoshop:AuthorsPosition
- // which is weird and really part of another property,
- // see 2:85 in IPTC. See also pg 21 of IPTC4XMP standard.
- // The location fields also use it.
-
- $data = $this->results;
-
- if ( isset( $data['xmp-special']['AuthorsPosition'] )
- && is_string( $data['xmp-special']['AuthorsPosition'] )
- && isset( $data['xmp-general']['Artist'][0] )
- ) {
- // Note, if there is more than one creator,
- // this only applies to first. This also will
- // only apply to the dc:Creator prop, not the
- // exif:Artist prop.
-
- $data['xmp-general']['Artist'][0] =
- $data['xmp-special']['AuthorsPosition'] . ', '
- . $data['xmp-general']['Artist'][0];
- }
-
- // Go through the LocationShown and LocationCreated
- // changing it to the non-hierarchal form used by
- // the other location fields.
-
- if ( isset( $data['xmp-special']['LocationShown'][0] )
- && is_array( $data['xmp-special']['LocationShown'][0] )
- ) {
- // the is_array is just paranoia. It should always
- // be an array.
- foreach ( $data['xmp-special']['LocationShown'] as $loc ) {
- if ( !is_array( $loc ) ) {
- // To avoid copying over the _type meta-fields.
- continue;
- }
- foreach ( $loc as $field => $val ) {
- $data['xmp-general'][$field . 'Dest'][] = $val;
- }
- }
- }
- if ( isset( $data['xmp-special']['LocationCreated'][0] )
- && is_array( $data['xmp-special']['LocationCreated'][0] )
- ) {
- // the is_array is just paranoia. It should always
- // be an array.
- foreach ( $data['xmp-special']['LocationCreated'] as $loc ) {
- if ( !is_array( $loc ) ) {
- // To avoid copying over the _type meta-fields.
- continue;
- }
- foreach ( $loc as $field => $val ) {
- $data['xmp-general'][$field . 'Created'][] = $val;
- }
- }
- }
-
- // We don't want to return the special values, since they're
- // special and not info to be stored about the file.
- unset( $data['xmp-special'] );
-
- // Convert GPSAltitude to negative if below sea level.
- if ( isset( $data['xmp-exif']['GPSAltitudeRef'] )
- && isset( $data['xmp-exif']['GPSAltitude'] )
- ) {
-
- // Must convert to a real before multiplying by -1
- // XMPValidate guarantees there will always be a '/' in this value.
- list( $nom, $denom ) = explode( '/', $data['xmp-exif']['GPSAltitude'] );
- $data['xmp-exif']['GPSAltitude'] = $nom / $denom;
-
- if ( $data['xmp-exif']['GPSAltitudeRef'] == '1' ) {
- $data['xmp-exif']['GPSAltitude'] *= -1;
- }
- unset( $data['xmp-exif']['GPSAltitudeRef'] );
- }
-
- return $data;
- }
-
- /**
- * Main function to call to parse XMP. Use getResults to
- * get results.
- *
- * Also catches any errors during processing, writes them to
- * debug log, blanks result array and returns false.
- *
- * @param string $content XMP data
- * @param bool $allOfIt If this is all the data (true) or if its split up (false). Default true
- * @throws RuntimeException
- * @return bool Success.
- */
- public function parse( $content, $allOfIt = true ) {
- if ( !$this->xmlParser ) {
- $this->resetXMLParser();
- }
- try {
-
- // detect encoding by looking for BOM which is supposed to be in processing instruction.
- // see page 12 of http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart3.pdf
- if ( !$this->charset ) {
- $bom = [];
- if ( preg_match( '/\xEF\xBB\xBF|\xFE\xFF|\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\xFF\xFE/',
- $content, $bom )
- ) {
- switch ( $bom[0] ) {
- case "\xFE\xFF":
- $this->charset = 'UTF-16BE';
- break;
- case "\xFF\xFE":
- $this->charset = 'UTF-16LE';
- break;
- case "\x00\x00\xFE\xFF":
- $this->charset = 'UTF-32BE';
- break;
- case "\xFF\xFE\x00\x00":
- $this->charset = 'UTF-32LE';
- break;
- case "\xEF\xBB\xBF":
- $this->charset = 'UTF-8';
- break;
- default:
- // this should be impossible to get to
- throw new RuntimeException( "Invalid BOM" );
- }
- } else {
- // standard specifically says, if no bom assume utf-8
- $this->charset = 'UTF-8';
- }
- }
- if ( $this->charset !== 'UTF-8' ) {
- // don't convert if already utf-8
- MediaWiki\suppressWarnings();
- $content = iconv( $this->charset, 'UTF-8//IGNORE', $content );
- MediaWiki\restoreWarnings();
- }
-
- // Ensure the XMP block does not have an xml doctype declaration, which
- // could declare entities unsafe to parse with xml_parse (T85848/T71210).
- if ( $this->parsable !== self::PARSABLE_OK ) {
- if ( $this->parsable === self::PARSABLE_NO ) {
- throw new RuntimeException( 'Unsafe doctype declaration in XML.' );
- }
-
- $content = $this->xmlParsableBuffer . $content;
- if ( !$this->checkParseSafety( $content ) ) {
- if ( !$allOfIt && $this->parsable !== self::PARSABLE_NO ) {
- // parse wasn't Unsuccessful yet, so return true
- // in this case.
- return true;
- }
- $msg = ( $this->parsable === self::PARSABLE_NO ) ?
- 'Unsafe doctype declaration in XML.' :
- 'No root element found in XML.';
- throw new RuntimeException( $msg );
- }
- }
-
- $ok = xml_parse( $this->xmlParser, $content, $allOfIt );
- if ( !$ok ) {
- $code = xml_get_error_code( $this->xmlParser );
- $error = xml_error_string( $code );
- $line = xml_get_current_line_number( $this->xmlParser );
- $col = xml_get_current_column_number( $this->xmlParser );
- $offset = xml_get_current_byte_index( $this->xmlParser );
-
- $this->logger->warning(
- '{method} : Error reading XMP content: {error} ' .
- '(line: {line} column: {column} byte offset: {offset})',
- [
- 'method' => __METHOD__,
- 'error_code' => $code,
- 'error' => $error,
- 'line' => $line,
- 'column' => $col,
- 'offset' => $offset,
- 'content' => $content,
- ] );
- $this->results = []; // blank if error.
- $this->destroyXMLParser();
- return false;
- }
- } catch ( Exception $e ) {
- $this->logger->warning(
- '{method} Exception caught while parsing: ' . $e->getMessage(),
- [
- 'method' => __METHOD__,
- 'exception' => $e,
- 'content' => $content,
- ]
- );
- $this->results = [];
- return false;
- }
- if ( $allOfIt ) {
- $this->destroyXMLParser();
- }
-
- return true;
- }
-
- /** Entry point for XMPExtended blocks in jpeg files
- *
- * @todo In serious need of testing
- * @see http://www.adobe.ge/devnet/xmp/pdfs/XMPSpecificationPart3.pdf XMP spec part 3 page 20
- * @param string $content XMPExtended block minus the namespace signature
- * @return bool If it succeeded.
- */
- public function parseExtended( $content ) {
- // @todo FIXME: This is untested. Hard to find example files
- // or programs that make such files..
- $guid = substr( $content, 0, 32 );
- if ( !isset( $this->results['xmp-special']['HasExtendedXMP'] )
- || $this->results['xmp-special']['HasExtendedXMP'] !== $guid
- ) {
- $this->logger->info( __METHOD__ .
- " Ignoring XMPExtended block due to wrong guid (guid= '$guid')" );
-
- return false;
- }
- $len = unpack( 'Nlength/Noffset', substr( $content, 32, 8 ) );
-
- if ( !$len ||
- $len['length'] < 4 ||
- $len['offset'] < 0 ||
- $len['offset'] > $len['length']
- ) {
- $this->logger->info(
- __METHOD__ . 'Error reading extended XMP block, invalid length or offset.'
- );
-
- return false;
- }
-
- // we're not very robust here. we should accept it in the wrong order.
- // To quote the XMP standard:
- // "A JPEG writer should write the ExtendedXMP marker segments in order,
- // immediately following the StandardXMP. However, the JPEG standard
- // does not require preservation of marker segment order. A robust JPEG
- // reader should tolerate the marker segments in any order."
- // On the other hand, the probability that an image will have more than
- // 128k of metadata is rather low... so the probability that it will have
- // > 128k, and be in the wrong order is very low...
-
- if ( $len['offset'] !== $this->extendedXMPOffset ) {
- $this->logger->info( __METHOD__ . 'Ignoring XMPExtended block due to wrong order. (Offset was '
- . $len['offset'] . ' but expected ' . $this->extendedXMPOffset . ')' );
-
- return false;
- }
-
- if ( $len['offset'] === 0 ) {
- // if we're starting the extended block, we've probably already
- // done the XMPStandard block, so reset.
- $this->resetXMLParser();
- }
-
- $this->extendedXMPOffset += $len['length'];
-
- $actualContent = substr( $content, 40 );
-
- if ( $this->extendedXMPOffset === strlen( $actualContent ) ) {
- $atEnd = true;
- } else {
- $atEnd = false;
- }
-
- $this->logger->debug( __METHOD__ . 'Parsing a XMPExtended block' );
-
- return $this->parse( $actualContent, $atEnd );
- }
-
- /**
- * Character data handler
- * Called whenever character data is found in the xmp document.
- *
- * does nothing if we're in MODE_IGNORE or if the data is whitespace
- * throws an error if we're not in MODE_SIMPLE (as we're not allowed to have character
- * data in the other modes).
- *
- * As an example, this happens when we encounter XMP like:
- * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio>
- * and are processing the 0/10 bit.
- *
- * @param XMLParser $parser XMLParser reference to the xml parser
- * @param string $data Character data
- * @throws RuntimeException On invalid data
- */
- function char( $parser, $data ) {
-
- $data = trim( $data );
- if ( trim( $data ) === "" ) {
- return;
- }
-
- if ( !isset( $this->mode[0] ) ) {
- throw new RuntimeException( 'Unexpected character data before first rdf:Description element' );
- }
-
- if ( $this->mode[0] === self::MODE_IGNORE ) {
- return;
- }
-
- if ( $this->mode[0] !== self::MODE_SIMPLE
- && $this->mode[0] !== self::MODE_QDESC
- ) {
- throw new RuntimeException( 'character data where not expected. (mode ' . $this->mode[0] . ')' );
- }
-
- // to check, how does this handle w.s.
- if ( $this->charContent === false ) {
- $this->charContent = $data;
- } else {
- $this->charContent .= $data;
- }
- }
-
- /**
- * Check if a block of XML is safe to pass to xml_parse, i.e. doesn't
- * contain a doctype declaration which could contain a dos attack if we
- * parse it and expand internal entities (T85848).
- *
- * @param string $content xml string to check for parse safety
- * @return bool true if the xml is safe to parse, false otherwise
- */
- private function checkParseSafety( $content ) {
- $reader = new XMLReader();
- $result = null;
-
- // For XMLReader to parse incomplete/invalid XML, it has to be open()'ed
- // instead of using XML().
- $reader->open(
- 'data://text/plain,' . urlencode( $content ),
- null,
- LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET
- );
-
- $oldDisable = libxml_disable_entity_loader( true );
- /** @noinspection PhpUnusedLocalVariableInspection */
- $reset = new ScopedCallback(
- 'libxml_disable_entity_loader',
- [ $oldDisable ]
- );
- $reader->setParserProperty( XMLReader::SUBST_ENTITIES, false );
-
- // Even with LIBXML_NOWARNING set, XMLReader::read gives a warning
- // when parsing truncated XML, which causes unit tests to fail.
- MediaWiki\suppressWarnings();
- while ( $reader->read() ) {
- if ( $reader->nodeType === XMLReader::ELEMENT ) {
- // Reached the first element without hitting a doctype declaration
- $this->parsable = self::PARSABLE_OK;
- $result = true;
- break;
- }
- if ( $reader->nodeType === XMLReader::DOC_TYPE ) {
- $this->parsable = self::PARSABLE_NO;
- $result = false;
- break;
- }
- }
- MediaWiki\restoreWarnings();
-
- if ( !is_null( $result ) ) {
- return $result;
- }
-
- // Reached the end of the parsable xml without finding an element
- // or doctype. Buffer and try again.
- $this->parsable = self::PARSABLE_BUFFERING;
- $this->xmlParsableBuffer = $content;
- return false;
- }
-
- /** When we hit a closing element in MODE_IGNORE
- * Check to see if this is the element we started to ignore,
- * in which case we get out of MODE_IGNORE
- *
- * @param string $elm Namespace of element followed by a space and then tag name of element.
- */
- private function endElementModeIgnore( $elm ) {
- if ( $this->curItem[0] === $elm ) {
- array_shift( $this->curItem );
- array_shift( $this->mode );
- }
- }
-
- /**
- * Hit a closing element when in MODE_SIMPLE.
- * This generally means that we finished processing a
- * property value, and now have to save the result to the
- * results array
- *
- * For example, when processing:
- * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio>
- * this deals with when we hit </exif:DigitalZoomRatio>.
- *
- * Or it could be if we hit the end element of a property
- * of a compound data structure (like a member of an array).
- *
- * @param string $elm Namespace, space, and tag name.
- */
- private function endElementModeSimple( $elm ) {
- if ( $this->charContent !== false ) {
- if ( $this->processingArray ) {
- // if we're processing an array, use the original element
- // name instead of rdf:li.
- list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
- } else {
- list( $ns, $tag ) = explode( ' ', $elm, 2 );
- }
- $this->saveValue( $ns, $tag, $this->charContent );
-
- $this->charContent = false; // reset
- }
- array_shift( $this->curItem );
- array_shift( $this->mode );
- }
-
- /**
- * Hit a closing element in MODE_STRUCT, MODE_SEQ, MODE_BAG
- * generally means we've finished processing a nested structure.
- * resets some internal variables to indicate that.
- *
- * Note this means we hit the closing element not the "</rdf:Seq>".
- *
- * @par For example, when processing:
- * @code{,xml}
- * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
- * </rdf:Seq> </exif:ISOSpeedRatings>
- * @endcode
- *
- * This method is called when we hit the "</exif:ISOSpeedRatings>" tag.
- *
- * @param string $elm Namespace . space . tag name.
- * @throws RuntimeException
- */
- private function endElementNested( $elm ) {
-
- /* cur item must be the same as $elm, unless if in MODE_STRUCT
- in which case it could also be rdf:Description */
- if ( $this->curItem[0] !== $elm
- && !( $elm === self::NS_RDF . ' Description'
- && $this->mode[0] === self::MODE_STRUCT )
- ) {
- throw new RuntimeException( "nesting mismatch. got a </$elm> but expected a </" .
- $this->curItem[0] . '>' );
- }
-
- // Validate structures.
- list( $ns, $tag ) = explode( ' ', $elm, 2 );
- if ( isset( $this->items[$ns][$tag]['validate'] ) ) {
- $info =& $this->items[$ns][$tag];
- $finalName = isset( $info['map_name'] )
- ? $info['map_name'] : $tag;
-
- if ( is_array( $info['validate'] ) ) {
- $validate = $info['validate'];
- } else {
- $validator = new XMPValidate( $this->logger );
- $validate = [ $validator, $info['validate'] ];
- }
-
- if ( !isset( $this->results['xmp-' . $info['map_group']][$finalName] ) ) {
- // This can happen if all the members of the struct failed validation.
- $this->logger->debug( __METHOD__ . " <$ns:$tag> has no valid members." );
- } elseif ( is_callable( $validate ) ) {
- $val =& $this->results['xmp-' . $info['map_group']][$finalName];
- call_user_func_array( $validate, [ $info, &$val, false ] );
- if ( is_null( $val ) ) {
- // the idea being the validation function will unset the variable if
- // its invalid.
- $this->logger->info( __METHOD__ . " <$ns:$tag> failed validation." );
- unset( $this->results['xmp-' . $info['map_group']][$finalName] );
- }
- } else {
- $this->logger->warning( __METHOD__ . " Validation function for $finalName ("
- . $validate[0] . '::' . $validate[1] . '()) is not callable.' );
- }
- }
-
- array_shift( $this->curItem );
- array_shift( $this->mode );
- $this->ancestorStruct = false;
- $this->processingArray = false;
- $this->itemLang = false;
- }
-
- /**
- * Hit a closing element in MODE_LI (either rdf:Seq, or rdf:Bag )
- * Add information about what type of element this is.
- *
- * Note we still have to hit the outer "</property>"
- *
- * @par For example, when processing:
- * @code{,xml}
- * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
- * </rdf:Seq> </exif:ISOSpeedRatings>
- * @endcode
- *
- * This method is called when we hit the "</rdf:Seq>".
- * (For comparison, we call endElementModeSimple when we
- * hit the "</rdf:li>")
- *
- * @param string $elm Namespace . ' ' . element name
- * @throws RuntimeException
- */
- private function endElementModeLi( $elm ) {
- list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
- $info = $this->items[$ns][$tag];
- $finalName = isset( $info['map_name'] )
- ? $info['map_name'] : $tag;
-
- array_shift( $this->mode );
-
- if ( !isset( $this->results['xmp-' . $info['map_group']][$finalName] ) ) {
- $this->logger->debug( __METHOD__ . " Empty compund element $finalName." );
-
- return;
- }
-
- if ( $elm === self::NS_RDF . ' Seq' ) {
- $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'ol';
- } elseif ( $elm === self::NS_RDF . ' Bag' ) {
- $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'ul';
- } elseif ( $elm === self::NS_RDF . ' Alt' ) {
- // extra if needed as you could theoretically have a non-language alt.
- if ( $info['mode'] === self::MODE_LANG ) {
- $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'lang';
- }
- } else {
- throw new RuntimeException(
- __METHOD__ . " expected </rdf:seq> or </rdf:bag> but instead got $elm."
- );
- }
- }
-
- /**
- * End element while in MODE_QDESC
- * mostly when ending an element when we have a simple value
- * that has qualifiers.
- *
- * Qualifiers aren't all that common, and we don't do anything
- * with them.
- *
- * @param string $elm Namespace and element
- */
- private function endElementModeQDesc( $elm ) {
-
- if ( $elm === self::NS_RDF . ' value' ) {
- list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
- $this->saveValue( $ns, $tag, $this->charContent );
-
- return;
- } else {
- array_shift( $this->mode );
- array_shift( $this->curItem );
- }
- }
-
- /**
- * Handler for hitting a closing element.
- *
- * generally just calls a helper function depending on what
- * mode we're in.
- *
- * Ignores the outer wrapping elements that are optional in
- * xmp and have no meaning.
- *
- * @param XMLParser $parser
- * @param string $elm Namespace . ' ' . element name
- * @throws RuntimeException
- */
- function endElement( $parser, $elm ) {
- if ( $elm === ( self::NS_RDF . ' RDF' )
- || $elm === 'adobe:ns:meta/ xmpmeta'
- || $elm === 'adobe:ns:meta/ xapmeta'
- ) {
- // ignore these.
- return;
- }
-
- if ( $elm === self::NS_RDF . ' type' ) {
- // these aren't really supported properly yet.
- // However, it appears they almost never used.
- $this->logger->info( __METHOD__ . ' encountered <rdf:type>' );
- }
-
- if ( strpos( $elm, ' ' ) === false ) {
- // This probably shouldn't happen.
- // However, there is a bug in an adobe product
- // that forgets the namespace on some things.
- // (Luckily they are unimportant things).
- $this->logger->info( __METHOD__ . " Encountered </$elm> which has no namespace. Skipping." );
-
- return;
- }
-
- if ( count( $this->mode[0] ) === 0 ) {
- // This should never ever happen and means
- // there is a pretty major bug in this class.
- throw new RuntimeException( 'Encountered end element with no mode' );
- }
-
- if ( count( $this->curItem ) == 0 && $this->mode[0] !== self::MODE_INITIAL ) {
- // just to be paranoid. Should always have a curItem, except for initially
- // (aka during MODE_INITAL).
- throw new RuntimeException( "Hit end element </$elm> but no curItem" );
- }
-
- switch ( $this->mode[0] ) {
- case self::MODE_IGNORE:
- $this->endElementModeIgnore( $elm );
- break;
- case self::MODE_SIMPLE:
- $this->endElementModeSimple( $elm );
- break;
- case self::MODE_STRUCT:
- case self::MODE_SEQ:
- case self::MODE_BAG:
- case self::MODE_LANG:
- case self::MODE_BAGSTRUCT:
- $this->endElementNested( $elm );
- break;
- case self::MODE_INITIAL:
- if ( $elm === self::NS_RDF . ' Description' ) {
- array_shift( $this->mode );
- } else {
- throw new RuntimeException( 'Element ended unexpectedly while in MODE_INITIAL' );
- }
- break;
- case self::MODE_LI:
- case self::MODE_LI_LANG:
- $this->endElementModeLi( $elm );
- break;
- case self::MODE_QDESC:
- $this->endElementModeQDesc( $elm );
- break;
- default:
- $this->logger->warning( __METHOD__ . " no mode (elm = $elm)" );
- break;
- }
- }
-
- /**
- * Hit an opening element while in MODE_IGNORE
- *
- * XMP is extensible, so ignore any tag we don't understand.
- *
- * Mostly ignores, unless we encounter the element that we are ignoring.
- * in which case we add it to the item stack, so we can ignore things
- * that are nested, correctly.
- *
- * @param string $elm Namespace . ' ' . tag name
- */
- private function startElementModeIgnore( $elm ) {
- if ( $elm === $this->curItem[0] ) {
- array_unshift( $this->curItem, $elm );
- array_unshift( $this->mode, self::MODE_IGNORE );
- }
- }
-
- /**
- * Start element in MODE_BAG (unordered array)
- * this should always be <rdf:Bag>
- *
- * @param string $elm Namespace . ' ' . tag
- * @throws RuntimeException If we have an element that's not <rdf:Bag>
- */
- private function startElementModeBag( $elm ) {
- if ( $elm === self::NS_RDF . ' Bag' ) {
- array_unshift( $this->mode, self::MODE_LI );
- } else {
- throw new RuntimeException( "Expected <rdf:Bag> but got $elm." );
- }
- }
-
- /**
- * Start element in MODE_SEQ (ordered array)
- * this should always be <rdf:Seq>
- *
- * @param string $elm Namespace . ' ' . tag
- * @throws RuntimeException If we have an element that's not <rdf:Seq>
- */
- private function startElementModeSeq( $elm ) {
- if ( $elm === self::NS_RDF . ' Seq' ) {
- array_unshift( $this->mode, self::MODE_LI );
- } elseif ( $elm === self::NS_RDF . ' Bag' ) {
- # bug 27105
- $this->logger->info( __METHOD__ . ' Expected an rdf:Seq, but got an rdf:Bag. Pretending'
- . ' it is a Seq, since some buggy software is known to screw this up.' );
- array_unshift( $this->mode, self::MODE_LI );
- } else {
- throw new RuntimeException( "Expected <rdf:Seq> but got $elm." );
- }
- }
-
- /**
- * Start element in MODE_LANG (language alternative)
- * this should always be <rdf:Alt>
- *
- * This tag tends to be used for metadata like describe this
- * picture, which can be translated into multiple languages.
- *
- * XMP supports non-linguistic alternative selections,
- * which are really only used for thumbnails, which
- * we don't care about.
- *
- * @param string $elm Namespace . ' ' . tag
- * @throws RuntimeException If we have an element that's not <rdf:Alt>
- */
- private function startElementModeLang( $elm ) {
- if ( $elm === self::NS_RDF . ' Alt' ) {
- array_unshift( $this->mode, self::MODE_LI_LANG );
- } else {
- throw new RuntimeException( "Expected <rdf:Seq> but got $elm." );
- }
- }
-
- /**
- * Handle an opening element when in MODE_SIMPLE
- *
- * This should not happen often. This is for if a simple element
- * already opened has a child element. Could happen for a
- * qualified element.
- *
- * For example:
- * <exif:DigitalZoomRatio><rdf:Description><rdf:value>0/10</rdf:value>
- * <foo:someQualifier>Bar</foo:someQualifier> </rdf:Description>
- * </exif:DigitalZoomRatio>
- *
- * This method is called when processing the <rdf:Description> element
- *
- * @param string $elm Namespace and tag names separated by space.
- * @param array $attribs Attributes of the element.
- * @throws RuntimeException
- */
- private function startElementModeSimple( $elm, $attribs ) {
- if ( $elm === self::NS_RDF . ' Description' ) {
- // If this value has qualifiers
- array_unshift( $this->mode, self::MODE_QDESC );
- array_unshift( $this->curItem, $this->curItem[0] );
-
- if ( isset( $attribs[self::NS_RDF . ' value'] ) ) {
- list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
- $this->saveValue( $ns, $tag, $attribs[self::NS_RDF . ' value'] );
- }
- } elseif ( $elm === self::NS_RDF . ' value' ) {
- // This should not be here.
- throw new RuntimeException( __METHOD__ . ' Encountered <rdf:value> where it was unexpected.' );
- } else {
- // something else we don't recognize, like a qualifier maybe.
- $this->logger->info( __METHOD__ .
- " Encountered element <$elm> where only expecting character data as value of " .
- $this->curItem[0] );
- array_unshift( $this->mode, self::MODE_IGNORE );
- array_unshift( $this->curItem, $elm );
- }
- }
-
- /**
- * Start an element when in MODE_QDESC.
- * This generally happens when a simple element has an inner
- * rdf:Description to hold qualifier elements.
- *
- * For example in:
- * <exif:DigitalZoomRatio><rdf:Description><rdf:value>0/10</rdf:value>
- * <foo:someQualifier>Bar</foo:someQualifier> </rdf:Description>
- * </exif:DigitalZoomRatio>
- * Called when processing the <rdf:value> or <foo:someQualifier>.
- *
- * @param string $elm Namespace and tag name separated by a space.
- *
- */
- private function startElementModeQDesc( $elm ) {
- if ( $elm === self::NS_RDF . ' value' ) {
- return; // do nothing
- } else {
- // otherwise its a qualifier, which we ignore
- array_unshift( $this->mode, self::MODE_IGNORE );
- array_unshift( $this->curItem, $elm );
- }
- }
-
- /**
- * Starting an element when in MODE_INITIAL
- * This usually happens when we hit an element inside
- * the outer rdf:Description
- *
- * This is generally where most properties start.
- *
- * @param string $ns Namespace
- * @param string $tag Tag name (without namespace prefix)
- * @param array $attribs Array of attributes
- * @throws RuntimeException
- */
- private function startElementModeInitial( $ns, $tag, $attribs ) {
- if ( $ns !== self::NS_RDF ) {
-
- if ( isset( $this->items[$ns][$tag] ) ) {
- if ( isset( $this->items[$ns][$tag]['structPart'] ) ) {
- // If this element is supposed to appear only as
- // a child of a structure, but appears here (not as
- // a child of a struct), then something weird is
- // happening, so ignore this element and its children.
-
- $this->logger->warning( "Encountered <$ns:$tag> outside"
- . " of its expected parent. Ignoring." );
-
- array_unshift( $this->mode, self::MODE_IGNORE );
- array_unshift( $this->curItem, $ns . ' ' . $tag );
-
- return;
- }
- $mode = $this->items[$ns][$tag]['mode'];
- array_unshift( $this->mode, $mode );
- array_unshift( $this->curItem, $ns . ' ' . $tag );
- if ( $mode === self::MODE_STRUCT ) {
- $this->ancestorStruct = isset( $this->items[$ns][$tag]['map_name'] )
- ? $this->items[$ns][$tag]['map_name'] : $tag;
- }
- if ( $this->charContent !== false ) {
- // Something weird.
- // Should not happen in valid XMP.
- throw new RuntimeException( 'tag nested in non-whitespace characters.' );
- }
- } else {
- // This element is not on our list of allowed elements so ignore.
- $this->logger->debug( __METHOD__ . " Ignoring unrecognized element <$ns:$tag>." );
- array_unshift( $this->mode, self::MODE_IGNORE );
- array_unshift( $this->curItem, $ns . ' ' . $tag );
-
- return;
- }
- }
- // process attributes
- $this->doAttribs( $attribs );
- }
-
- /**
- * Hit an opening element when in a Struct (MODE_STRUCT)
- * This is generally for fields of a compound property.
- *
- * Example of a struct (abbreviated; flash has more properties):
- *
- * <exif:Flash> <rdf:Description> <exif:Fired>True</exif:Fired>
- * <exif:Mode>1</exif:Mode></rdf:Description></exif:Flash>
- *
- * or:
- *
- * <exif:Flash rdf:parseType='Resource'> <exif:Fired>True</exif:Fired>
- * <exif:Mode>1</exif:Mode></exif:Flash>
- *
- * @param string $ns Namespace
- * @param string $tag Tag name (no ns)
- * @param array $attribs Array of attribs w/ values.
- * @throws RuntimeException
- */
- private function startElementModeStruct( $ns, $tag, $attribs ) {
- if ( $ns !== self::NS_RDF ) {
-
- if ( isset( $this->items[$ns][$tag] ) ) {
- if ( isset( $this->items[$ns][$this->ancestorStruct]['children'] )
- && !isset( $this->items[$ns][$this->ancestorStruct]['children'][$tag] )
- ) {
- // This assumes that we don't have inter-namespace nesting
- // which we don't in all the properties we're interested in.
- throw new RuntimeException( " <$tag> appeared nested in <" . $this->ancestorStruct
- . "> where it is not allowed." );
- }
- array_unshift( $this->mode, $this->items[$ns][$tag]['mode'] );
- array_unshift( $this->curItem, $ns . ' ' . $tag );
- if ( $this->charContent !== false ) {
- // Something weird.
- // Should not happen in valid XMP.
- throw new RuntimeException( "tag <$tag> nested in non-whitespace characters (" .
- $this->charContent . ")." );
- }
- } else {
- array_unshift( $this->mode, self::MODE_IGNORE );
- array_unshift( $this->curItem, $elm );
-
- return;
- }
- }
-
- if ( $ns === self::NS_RDF && $tag === 'Description' ) {
- $this->doAttribs( $attribs );
- array_unshift( $this->mode, self::MODE_STRUCT );
- array_unshift( $this->curItem, $this->curItem[0] );
- }
- }
-
- /**
- * opening element in MODE_LI
- * process elements of arrays.
- *
- * Example:
- * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
- * </rdf:Seq> </exif:ISOSpeedRatings>
- * This method is called when we hit the <rdf:li> element.
- *
- * @param string $elm Namespace . ' ' . tagname
- * @param array $attribs Attributes. (needed for BAGSTRUCTS)
- * @throws RuntimeException If gets a tag other than <rdf:li>
- */
- private function startElementModeLi( $elm, $attribs ) {
- if ( ( $elm ) !== self::NS_RDF . ' li' ) {
- throw new RuntimeException( "<rdf:li> expected but got $elm." );
- }
-
- if ( !isset( $this->mode[1] ) ) {
- // This should never ever ever happen. Checking for it
- // to be paranoid.
- throw new RuntimeException( 'In mode Li, but no 2xPrevious mode!' );
- }
-
- if ( $this->mode[1] === self::MODE_BAGSTRUCT ) {
- // This list item contains a compound (STRUCT) value.
- array_unshift( $this->mode, self::MODE_STRUCT );
- array_unshift( $this->curItem, $elm );
- $this->processingArray = true;
-
- if ( !isset( $this->curItem[1] ) ) {
- // be paranoid.
- throw new RuntimeException( 'Can not find parent of BAGSTRUCT.' );
- }
- list( $curNS, $curTag ) = explode( ' ', $this->curItem[1] );
- $this->ancestorStruct = isset( $this->items[$curNS][$curTag]['map_name'] )
- ? $this->items[$curNS][$curTag]['map_name'] : $curTag;
-
- $this->doAttribs( $attribs );
- } else {
- // Normal BAG or SEQ containing simple values.
- array_unshift( $this->mode, self::MODE_SIMPLE );
- // need to add curItem[0] on again since one is for the specific item
- // and one is for the entire group.
- array_unshift( $this->curItem, $this->curItem[0] );
- $this->processingArray = true;
- }
- }
-
- /**
- * Opening element in MODE_LI_LANG.
- * process elements of language alternatives
- *
- * Example:
- * <dc:title> <rdf:Alt> <rdf:li xml:lang="x-default">My house
- * </rdf:li> </rdf:Alt> </dc:title>
- *
- * This method is called when we hit the <rdf:li> element.
- *
- * @param string $elm Namespace . ' ' . tag
- * @param array $attribs Array of elements (most importantly xml:lang)
- * @throws RuntimeException If gets a tag other than <rdf:li> or if no xml:lang
- */
- private function startElementModeLiLang( $elm, $attribs ) {
- if ( $elm !== self::NS_RDF . ' li' ) {
- throw new RuntimeException( __METHOD__ . " <rdf:li> expected but got $elm." );
- }
- if ( !isset( $attribs[self::NS_XML . ' lang'] )
- || !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $attribs[self::NS_XML . ' lang'] )
- ) {
- throw new RuntimeException( __METHOD__
- . " <rdf:li> did not contain, or has invalid xml:lang attribute in lang alternative" );
- }
-
- // Lang is case-insensitive.
- $this->itemLang = strtolower( $attribs[self::NS_XML . ' lang'] );
-
- // need to add curItem[0] on again since one is for the specific item
- // and one is for the entire group.
- array_unshift( $this->curItem, $this->curItem[0] );
- array_unshift( $this->mode, self::MODE_SIMPLE );
- $this->processingArray = true;
- }
-
- /**
- * Hits an opening element.
- * Generally just calls a helper based on what MODE we're in.
- * Also does some initial set up for the wrapper element
- *
- * @param XMLParser $parser
- * @param string $elm Namespace "<space>" element
- * @param array $attribs Attribute name => value
- * @throws RuntimeException
- */
- function startElement( $parser, $elm, $attribs ) {
-
- if ( $elm === self::NS_RDF . ' RDF'
- || $elm === 'adobe:ns:meta/ xmpmeta'
- || $elm === 'adobe:ns:meta/ xapmeta'
- ) {
- /* ignore. */
- return;
- } elseif ( $elm === self::NS_RDF . ' Description' ) {
- if ( count( $this->mode ) === 0 ) {
- // outer rdf:desc
- array_unshift( $this->mode, self::MODE_INITIAL );
- }
- } elseif ( $elm === self::NS_RDF . ' type' ) {
- // This doesn't support rdf:type properly.
- // In practise I have yet to see a file that
- // uses this element, however it is mentioned
- // on page 25 of part 1 of the xmp standard.
- // Also it seems as if exiv2 and exiftool do not support
- // this either (That or I misunderstand the standard)
- $this->logger->info( __METHOD__ . ' Encountered <rdf:type> which isn\'t currently supported' );
- }
-
- if ( strpos( $elm, ' ' ) === false ) {
- // This probably shouldn't happen.
- $this->logger->info( __METHOD__ . " Encountered <$elm> which has no namespace. Skipping." );
-
- return;
- }
-
- list( $ns, $tag ) = explode( ' ', $elm, 2 );
-
- if ( count( $this->mode ) === 0 ) {
- // This should not happen.
- throw new RuntimeException( 'Error extracting XMP, '
- . "encountered <$elm> with no mode" );
- }
-
- switch ( $this->mode[0] ) {
- case self::MODE_IGNORE:
- $this->startElementModeIgnore( $elm );
- break;
- case self::MODE_SIMPLE:
- $this->startElementModeSimple( $elm, $attribs );
- break;
- case self::MODE_INITIAL:
- $this->startElementModeInitial( $ns, $tag, $attribs );
- break;
- case self::MODE_STRUCT:
- $this->startElementModeStruct( $ns, $tag, $attribs );
- break;
- case self::MODE_BAG:
- case self::MODE_BAGSTRUCT:
- $this->startElementModeBag( $elm );
- break;
- case self::MODE_SEQ:
- $this->startElementModeSeq( $elm );
- break;
- case self::MODE_LANG:
- $this->startElementModeLang( $elm );
- break;
- case self::MODE_LI_LANG:
- $this->startElementModeLiLang( $elm, $attribs );
- break;
- case self::MODE_LI:
- $this->startElementModeLi( $elm, $attribs );
- break;
- case self::MODE_QDESC:
- $this->startElementModeQDesc( $elm );
- break;
- default:
- throw new RuntimeException( 'StartElement in unknown mode: ' . $this->mode[0] );
- }
- }
-
- // @codingStandardsIgnoreStart Generic.Files.LineLength
- /**
- * Process attributes.
- * Simple values can be stored as either a tag or attribute
- *
- * Often the initial "<rdf:Description>" tag just has all the simple
- * properties as attributes.
- *
- * @par Example:
- * @code
- * <rdf:Description rdf:about="" xmlns:exif="http://ns.adobe.com/exif/1.0/" exif:DigitalZoomRatio="0/10">
- * @endcode
- *
- * @param array $attribs Array attribute=>value
- * @throws RuntimeException
- */
- // @codingStandardsIgnoreEnd
- private function doAttribs( $attribs ) {
- // first check for rdf:parseType attribute, as that can change
- // how the attributes are interperted.
-
- if ( isset( $attribs[self::NS_RDF . ' parseType'] )
- && $attribs[self::NS_RDF . ' parseType'] === 'Resource'
- && $this->mode[0] === self::MODE_SIMPLE
- ) {
- // this is equivalent to having an inner rdf:Description
- $this->mode[0] = self::MODE_QDESC;
- }
- foreach ( $attribs as $name => $val ) {
- if ( strpos( $name, ' ' ) === false ) {
- // This shouldn't happen, but so far some old software forgets namespace
- // on rdf:about.
- $this->logger->info( __METHOD__ . ' Encountered non-namespaced attribute: '
- . " $name=\"$val\". Skipping. " );
- continue;
- }
- list( $ns, $tag ) = explode( ' ', $name, 2 );
- if ( $ns === self::NS_RDF ) {
- if ( $tag === 'value' || $tag === 'resource' ) {
- // resource is for url.
- // value attribute is a weird way of just putting the contents.
- $this->char( $this->xmlParser, $val );
- }
- } elseif ( isset( $this->items[$ns][$tag] ) ) {
- if ( $this->mode[0] === self::MODE_SIMPLE ) {
- throw new RuntimeException( __METHOD__
- . " $ns:$tag found as attribute where not allowed" );
- }
- $this->saveValue( $ns, $tag, $val );
- } else {
- $this->logger->debug( __METHOD__ . " Ignoring unrecognized element <$ns:$tag>." );
- }
- }
- }
-
- /**
- * Given an extracted value, save it to results array
- *
- * note also uses $this->ancestorStruct and
- * $this->processingArray to determine what name to
- * save the value under. (in addition to $tag).
- *
- * @param string $ns Namespace of tag this is for
- * @param string $tag Tag name
- * @param string $val Value to save
- */
- private function saveValue( $ns, $tag, $val ) {
-
- $info =& $this->items[$ns][$tag];
- $finalName = isset( $info['map_name'] )
- ? $info['map_name'] : $tag;
- if ( isset( $info['validate'] ) ) {
- if ( is_array( $info['validate'] ) ) {
- $validate = $info['validate'];
- } else {
- $validator = new XMPValidate( $this->logger );
- $validate = [ $validator, $info['validate'] ];
- }
-
- if ( is_callable( $validate ) ) {
- call_user_func_array( $validate, [ $info, &$val, true ] );
- // the reasoning behind using &$val instead of using the return value
- // is to be consistent between here and validating structures.
- if ( is_null( $val ) ) {
- $this->logger->info( __METHOD__ . " <$ns:$tag> failed validation." );
-
- return;
- }
- } else {
- $this->logger->warning( __METHOD__ . " Validation function for $finalName ("
- . $validate[0] . '::' . $validate[1] . '()) is not callable.' );
- }
- }
-
- if ( $this->ancestorStruct && $this->processingArray ) {
- // Aka both an array and a struct. ( self::MODE_BAGSTRUCT )
- $this->results['xmp-' . $info['map_group']][$this->ancestorStruct][][$finalName] = $val;
- } elseif ( $this->ancestorStruct ) {
- $this->results['xmp-' . $info['map_group']][$this->ancestorStruct][$finalName] = $val;
- } elseif ( $this->processingArray ) {
- if ( $this->itemLang === false ) {
- // normal array
- $this->results['xmp-' . $info['map_group']][$finalName][] = $val;
- } else {
- // lang array.
- $this->results['xmp-' . $info['map_group']][$finalName][$this->itemLang] = $val;
- }
- } else {
- $this->results['xmp-' . $info['map_group']][$finalName] = $val;
- }
- }
-}
diff --git a/www/wiki/includes/media/XMPInfo.php b/www/wiki/includes/media/XMPInfo.php
deleted file mode 100644
index 052be33a..00000000
--- a/www/wiki/includes/media/XMPInfo.php
+++ /dev/null
@@ -1,1168 +0,0 @@
-<?php
-/**
- * Definitions for XMPReader class.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Media
- */
-
-/**
- * This class is just a container for a big array
- * used by XMPReader to determine which XMP items to
- * extract.
- */
-class XMPInfo {
- /** Get the items array
- * @return array XMP item configuration array.
- */
- public static function getItems() {
- return self::$items;
- }
-
- /**
- * XMPInfo::$items keeps a list of all the items
- * we are interested to extract, as well as
- * information about the item like what type
- * it is.
- *
- * Format is an array of namespaces,
- * each containing an array of tags
- * each tag is an array of information about the
- * tag, including:
- * * map_group - What group (used for precedence during conflicts).
- * * mode - What type of item (self::MODE_SIMPLE usually, see above for
- * all values).
- * * validate - Method to validate input. Could also post-process the
- * input. A string value is assumed to be a method of
- * XMPValidate. Can also take a array( 'className', 'methodName' ).
- * * choices - Array of potential values (format of 'value' => true ).
- * Only used with validateClosed.
- * * rangeLow and rangeHigh - Alternative to choices for numeric ranges.
- * Again for validateClosed only.
- * * children - For MODE_STRUCT items, allowed children.
- * * structPart - Indicates that this element can only appear as a member
- * of a structure.
- *
- * Currently this just has a bunch of EXIF values as this class is only half-done.
- */
- static private $items = [
- 'http://ns.adobe.com/exif/1.0/' => [
- 'ApertureValue' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'BrightnessValue' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'CompressedBitsPerPixel' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'DigitalZoomRatio' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'ExposureBiasValue' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'ExposureIndex' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'ExposureTime' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'FlashEnergy' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational',
- ],
- 'FNumber' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'FocalLength' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'FocalPlaneXResolution' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'FocalPlaneYResolution' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'GPSAltitude' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational',
- ],
- 'GPSDestBearing' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'GPSDestDistance' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'GPSDOP' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'GPSImgDirection' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'GPSSpeed' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'GPSTrack' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'MaxApertureValue' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'ShutterSpeedValue' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- 'SubjectDistance' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational'
- ],
- /* Flash */
- 'Flash' => [
- 'mode' => XMPReader::MODE_STRUCT,
- 'children' => [
- 'Fired' => true,
- 'Function' => true,
- 'Mode' => true,
- 'RedEyeMode' => true,
- 'Return' => true,
- ],
- 'validate' => 'validateFlash',
- 'map_group' => 'exif',
- ],
- 'Fired' => [
- 'map_group' => 'exif',
- 'validate' => 'validateBoolean',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- 'Function' => [
- 'map_group' => 'exif',
- 'validate' => 'validateBoolean',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- 'Mode' => [
- 'map_group' => 'exif',
- 'validate' => 'validateClosed',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'choices' => [ '0' => true, '1' => true,
- '2' => true, '3' => true ],
- 'structPart' => true,
- ],
- 'Return' => [
- 'map_group' => 'exif',
- 'validate' => 'validateClosed',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'choices' => [ '0' => true,
- '2' => true, '3' => true ],
- 'structPart' => true,
- ],
- 'RedEyeMode' => [
- 'map_group' => 'exif',
- 'validate' => 'validateBoolean',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- /* End Flash */
- 'ISOSpeedRatings' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SEQ,
- 'validate' => 'validateInteger'
- ],
- /* end rational things */
- 'ColorSpace' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '1' => true, '65535' => true ],
- ],
- 'ComponentsConfiguration' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SEQ,
- 'validate' => 'validateClosed',
- 'choices' => [ '1' => true, '2' => true, '3' => true, '4' => true,
- '5' => true, '6' => true ]
- ],
- 'Contrast' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '0' => true, '1' => true, '2' => true ]
- ],
- 'CustomRendered' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '0' => true, '1' => true ]
- ],
- 'DateTimeOriginal' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateDate',
- ],
- 'DateTimeDigitized' => [ /* xmp:CreateDate */
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateDate',
- ],
- /* todo: there might be interesting information in
- * exif:DeviceSettingDescription, but need to find an
- * example
- */
- 'ExifVersion' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'ExposureMode' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'rangeLow' => 0,
- 'rangeHigh' => 2,
- ],
- 'ExposureProgram' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'rangeLow' => 0,
- 'rangeHigh' => 8,
- ],
- 'FileSource' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '3' => true ]
- ],
- 'FlashpixVersion' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'FocalLengthIn35mmFilm' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateInteger',
- ],
- 'FocalPlaneResolutionUnit' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '2' => true, '3' => true ],
- ],
- 'GainControl' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'rangeLow' => 0,
- 'rangeHigh' => 4,
- ],
- /* this value is post-processed out later */
- 'GPSAltitudeRef' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '0' => true, '1' => true ],
- ],
- 'GPSAreaInformation' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'GPSDestBearingRef' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ 'T' => true, 'M' => true ],
- ],
- 'GPSDestDistanceRef' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ 'K' => true, 'M' => true,
- 'N' => true ],
- ],
- 'GPSDestLatitude' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateGPS',
- ],
- 'GPSDestLongitude' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateGPS',
- ],
- 'GPSDifferential' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '0' => true, '1' => true ],
- ],
- 'GPSImgDirectionRef' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ 'T' => true, 'M' => true ],
- ],
- 'GPSLatitude' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateGPS',
- ],
- 'GPSLongitude' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateGPS',
- ],
- 'GPSMapDatum' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'GPSMeasureMode' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '2' => true, '3' => true ]
- ],
- 'GPSProcessingMethod' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'GPSSatellites' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'GPSSpeedRef' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ 'K' => true, 'M' => true,
- 'N' => true ],
- ],
- 'GPSStatus' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ 'A' => true, 'V' => true ]
- ],
- 'GPSTimeStamp' => [
- 'map_group' => 'exif',
- // Note: in exif, GPSDateStamp does not include
- // the time, where here it does.
- 'map_name' => 'GPSDateStamp',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateDate',
- ],
- 'GPSTrackRef' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ 'T' => true, 'M' => true ]
- ],
- 'GPSVersionID' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'ImageUniqueID' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'LightSource' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- /* can't use a range, as it skips... */
- 'choices' => [ '0' => true, '1' => true,
- '2' => true, '3' => true, '4' => true,
- '9' => true, '10' => true, '11' => true,
- '12' => true, '13' => true,
- '14' => true, '15' => true,
- '17' => true, '18' => true,
- '19' => true, '20' => true,
- '21' => true, '22' => true,
- '23' => true, '24' => true,
- '255' => true,
- ],
- ],
- 'MeteringMode' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'rangeLow' => 0,
- 'rangeHigh' => 6,
- 'choices' => [ '255' => true ],
- ],
- /* Pixel(X|Y)Dimension are rather useless, but for
- * completeness since we do it with exif.
- */
- 'PixelXDimension' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateInteger',
- ],
- 'PixelYDimension' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateInteger',
- ],
- 'Saturation' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'rangeLow' => 0,
- 'rangeHigh' => 2,
- ],
- 'SceneCaptureType' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'rangeLow' => 0,
- 'rangeHigh' => 3,
- ],
- 'SceneType' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '1' => true ],
- ],
- // Note, 6 is not valid SensingMethod.
- 'SensingMethod' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'rangeLow' => 1,
- 'rangeHigh' => 5,
- 'choices' => [ '7' => true, 8 => true ],
- ],
- 'Sharpness' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'rangeLow' => 0,
- 'rangeHigh' => 2,
- ],
- 'SpectralSensitivity' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- // This tag should perhaps be displayed to user better.
- 'SubjectArea' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SEQ,
- 'validate' => 'validateInteger',
- ],
- 'SubjectDistanceRange' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'rangeLow' => 0,
- 'rangeHigh' => 3,
- ],
- 'SubjectLocation' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SEQ,
- 'validate' => 'validateInteger',
- ],
- 'UserComment' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_LANG,
- ],
- 'WhiteBalance' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '0' => true, '1' => true ]
- ],
- ],
- 'http://ns.adobe.com/tiff/1.0/' => [
- 'Artist' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'BitsPerSample' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SEQ,
- 'validate' => 'validateInteger',
- ],
- 'Compression' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '1' => true, '6' => true ],
- ],
- /* this prop should not be used in XMP. dc:rights is the correct prop */
- 'Copyright' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_LANG,
- ],
- 'DateTime' => [ /* proper prop is xmp:ModifyDate */
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateDate',
- ],
- 'ImageDescription' => [ /* proper one is dc:description */
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_LANG,
- ],
- 'ImageLength' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateInteger',
- ],
- 'ImageWidth' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateInteger',
- ],
- 'Make' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'Model' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- /**** Do not extract this property
- * It interferes with auto exif rotation.
- * 'Orientation' => array(
- * 'map_group' => 'exif',
- * 'mode' => XMPReader::MODE_SIMPLE,
- * 'validate' => 'validateClosed',
- * 'choices' => array( '1' => true, '2' => true, '3' => true, '4' => true, 5 => true,
- * '6' => true, '7' => true, '8' => true ),
- *),
- ******/
- 'PhotometricInterpretation' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '2' => true, '6' => true ],
- ],
- 'PlanerConfiguration' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '1' => true, '2' => true ],
- ],
- 'PrimaryChromaticities' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SEQ,
- 'validate' => 'validateRational',
- ],
- 'ReferenceBlackWhite' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SEQ,
- 'validate' => 'validateRational',
- ],
- 'ResolutionUnit' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '2' => true, '3' => true ],
- ],
- 'SamplesPerPixel' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateInteger',
- ],
- 'Software' => [ /* see xmp:CreatorTool */
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- /* ignore TransferFunction */
- 'WhitePoint' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SEQ,
- 'validate' => 'validateRational',
- ],
- 'XResolution' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational',
- ],
- 'YResolution' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRational',
- ],
- 'YCbCrCoefficients' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SEQ,
- 'validate' => 'validateRational',
- ],
- 'YCbCrPositioning' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateClosed',
- 'choices' => [ '1' => true, '2' => true ],
- ],
- /********
- * Disable extracting this property (bug 31944)
- * Several files have a string instead of a Seq
- * for this property. XMPReader doesn't handle
- * mismatched types very gracefully (it marks
- * the entire file as invalid, instead of just
- * the relavent prop). Since this prop
- * doesn't communicate all that useful information
- * just disable this prop for now, until such
- * XMPReader is more graceful (bug 32172)
- * 'YCbCrSubSampling' => array(
- * 'map_group' => 'exif',
- * 'mode' => XMPReader::MODE_SEQ,
- * 'validate' => 'validateClosed',
- * 'choices' => array( '1' => true, '2' => true ),
- * ),
- */
- ],
- 'http://ns.adobe.com/exif/1.0/aux/' => [
- 'Lens' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'SerialNumber' => [
- 'map_group' => 'exif',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'OwnerName' => [
- 'map_group' => 'exif',
- 'map_name' => 'CameraOwnerName',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- ],
- 'http://purl.org/dc/elements/1.1/' => [
- 'title' => [
- 'map_group' => 'general',
- 'map_name' => 'ObjectName',
- 'mode' => XMPReader::MODE_LANG
- ],
- 'description' => [
- 'map_group' => 'general',
- 'map_name' => 'ImageDescription',
- 'mode' => XMPReader::MODE_LANG
- ],
- 'contributor' => [
- 'map_group' => 'general',
- 'map_name' => 'dc-contributor',
- 'mode' => XMPReader::MODE_BAG
- ],
- 'coverage' => [
- 'map_group' => 'general',
- 'map_name' => 'dc-coverage',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'creator' => [
- 'map_group' => 'general',
- 'map_name' => 'Artist', // map with exif Artist, iptc byline (2:80)
- 'mode' => XMPReader::MODE_SEQ,
- ],
- 'date' => [
- 'map_group' => 'general',
- // Note, not mapped with other date properties, as this type of date is
- // non-specific: "A point or period of time associated with an event in
- // the lifecycle of the resource"
- 'map_name' => 'dc-date',
- 'mode' => XMPReader::MODE_SEQ,
- 'validate' => 'validateDate',
- ],
- /* Do not extract dc:format, as we've got better ways to determine MIME type */
- 'identifier' => [
- 'map_group' => 'deprecated',
- 'map_name' => 'Identifier',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'language' => [
- 'map_group' => 'general',
- 'map_name' => 'LanguageCode', /* mapped with iptc 2:135 */
- 'mode' => XMPReader::MODE_BAG,
- 'validate' => 'validateLangCode',
- ],
- 'publisher' => [
- 'map_group' => 'general',
- 'map_name' => 'dc-publisher',
- 'mode' => XMPReader::MODE_BAG,
- ],
- // for related images/resources
- 'relation' => [
- 'map_group' => 'general',
- 'map_name' => 'dc-relation',
- 'mode' => XMPReader::MODE_BAG,
- ],
- 'rights' => [
- 'map_group' => 'general',
- 'map_name' => 'Copyright',
- 'mode' => XMPReader::MODE_LANG,
- ],
- // Note: source is not mapped with iptc source, since iptc
- // source describes the source of the image in terms of a person
- // who provided the image, where this is to describe an image that the
- // current one is based on.
- 'source' => [
- 'map_group' => 'general',
- 'map_name' => 'dc-source',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'subject' => [
- 'map_group' => 'general',
- 'map_name' => 'Keywords', /* maps to iptc 2:25 */
- 'mode' => XMPReader::MODE_BAG,
- ],
- 'type' => [
- 'map_group' => 'general',
- 'map_name' => 'dc-type',
- 'mode' => XMPReader::MODE_BAG,
- ],
- ],
- 'http://ns.adobe.com/xap/1.0/' => [
- 'CreateDate' => [
- 'map_group' => 'general',
- 'map_name' => 'DateTimeDigitized',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateDate',
- ],
- 'CreatorTool' => [
- 'map_group' => 'general',
- 'map_name' => 'Software',
- 'mode' => XMPReader::MODE_SIMPLE
- ],
- 'Identifier' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_BAG,
- ],
- 'Label' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'ModifyDate' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'map_name' => 'DateTime',
- 'validate' => 'validateDate',
- ],
- 'MetadataDate' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- // map_name to be consistent with other date names.
- 'map_name' => 'DateTimeMetadata',
- 'validate' => 'validateDate',
- ],
- 'Nickname' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'Rating' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateRating',
- ],
- ],
- 'http://ns.adobe.com/xap/1.0/rights/' => [
- 'Certificate' => [
- 'map_group' => 'general',
- 'map_name' => 'RightsCertificate',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'Marked' => [
- 'map_group' => 'general',
- 'map_name' => 'Copyrighted',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateBoolean',
- ],
- 'Owner' => [
- 'map_group' => 'general',
- 'map_name' => 'CopyrightOwner',
- 'mode' => XMPReader::MODE_BAG,
- ],
- // this seems similar to dc:rights.
- 'UsageTerms' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_LANG,
- ],
- 'WebStatement' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- ],
- // XMP media management.
- 'http://ns.adobe.com/xap/1.0/mm/' => [
- // if we extract the exif UniqueImageID, might
- // as well do this too.
- 'OriginalDocumentID' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- // It might also be useful to do xmpMM:LastURL
- // and xmpMM:DerivedFrom as you can potentially,
- // get the url of this document/source for this
- // document. However whats more likely is you'd
- // get a file:// url for the path of the doc,
- // which is somewhat of a privacy issue.
- ],
- 'http://creativecommons.org/ns#' => [
- 'license' => [
- 'map_name' => 'LicenseUrl',
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'morePermissions' => [
- 'map_name' => 'MorePermissionsUrl',
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'attributionURL' => [
- 'map_group' => 'general',
- 'map_name' => 'AttributionUrl',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'attributionName' => [
- 'map_group' => 'general',
- 'map_name' => 'PreferredAttributionName',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- ],
- // Note, this property affects how jpeg metadata is extracted.
- 'http://ns.adobe.com/xmp/note/' => [
- 'HasExtendedXMP' => [
- 'map_group' => 'special',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- ],
- /* Note, in iptc schemas, the legacy properties are denoted
- * as deprecated, since other properties should used instead,
- * and properties marked as deprecated in the standard are
- * are marked as general here as they don't have replacements
- */
- 'http://ns.adobe.com/photoshop/1.0/' => [
- 'City' => [
- 'map_group' => 'deprecated',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'map_name' => 'CityDest',
- ],
- 'Country' => [
- 'map_group' => 'deprecated',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'map_name' => 'CountryDest',
- ],
- 'State' => [
- 'map_group' => 'deprecated',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'map_name' => 'ProvinceOrStateDest',
- ],
- 'DateCreated' => [
- 'map_group' => 'deprecated',
- // marking as deprecated as the xmp prop preferred
- 'mode' => XMPReader::MODE_SIMPLE,
- 'map_name' => 'DateTimeOriginal',
- 'validate' => 'validateDate',
- // note this prop is an XMP, not IPTC date
- ],
- 'CaptionWriter' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'map_name' => 'Writer',
- ],
- 'Instructions' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'map_name' => 'SpecialInstructions',
- ],
- 'TransmissionReference' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'map_name' => 'OriginalTransmissionRef',
- ],
- 'AuthorsPosition' => [
- /* This corresponds with 2:85
- * By-line Title, which needs to be
- * handled weirdly to correspond
- * with iptc/exif. */
- 'map_group' => 'special',
- 'mode' => XMPReader::MODE_SIMPLE
- ],
- 'Credit' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'Source' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'Urgency' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'Category' => [
- // Note, this prop is deprecated, but in general
- // group since it doesn't have a replacement.
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'map_name' => 'iimCategory',
- ],
- 'SupplementalCategories' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_BAG,
- 'map_name' => 'iimSupplementalCategory',
- ],
- 'Headline' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE
- ],
- ],
- 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/' => [
- 'CountryCode' => [
- 'map_group' => 'deprecated',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'map_name' => 'CountryCodeDest',
- ],
- 'IntellectualGenre' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- // Note, this is a six digit code.
- // See: http://cv.iptc.org/newscodes/scene/
- // Since these aren't really all that common,
- // we just show the number.
- 'Scene' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_BAG,
- 'validate' => 'validateInteger',
- 'map_name' => 'SceneCode',
- ],
- /* Note: SubjectCode should be an 8 ascii digits.
- * it is not really an integer (has leading 0's,
- * cannot have a +/- sign), but validateInteger
- * will let it through.
- */
- 'SubjectCode' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_BAG,
- 'map_name' => 'SubjectNewsCode',
- 'validate' => 'validateInteger'
- ],
- 'Location' => [
- 'map_group' => 'deprecated',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'map_name' => 'SublocationDest',
- ],
- 'CreatorContactInfo' => [
- /* Note this maps to 2:118 in iim
- * (Contact) field. However those field
- * types are slightly different - 2:118
- * is free form text field, where this
- * is more structured.
- */
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_STRUCT,
- 'map_name' => 'Contact',
- 'children' => [
- 'CiAdrExtadr' => true,
- 'CiAdrCity' => true,
- 'CiAdrCtry' => true,
- 'CiEmailWork' => true,
- 'CiTelWork' => true,
- 'CiAdrPcode' => true,
- 'CiAdrRegion' => true,
- 'CiUrlWork' => true,
- ],
- ],
- 'CiAdrExtadr' => [ /* address */
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- 'CiAdrCity' => [ /* city */
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- 'CiAdrCtry' => [ /* country */
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- 'CiEmailWork' => [ /* email (possibly separated by ',') */
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- 'CiTelWork' => [ /* telephone */
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- 'CiAdrPcode' => [ /* postal code */
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- 'CiAdrRegion' => [ /* province/state */
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- 'CiUrlWork' => [ /* url. Multiple may be separated by comma. */
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- /* End contact info struct properties */
- ],
- 'http://iptc.org/std/Iptc4xmpExt/2008-02-29/' => [
- 'Event' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- ],
- 'OrganisationInImageName' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_BAG,
- 'map_name' => 'OrganisationInImage'
- ],
- 'PersonInImage' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_BAG,
- ],
- 'MaxAvailHeight' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateInteger',
- 'map_name' => 'OriginalImageHeight',
- ],
- 'MaxAvailWidth' => [
- 'map_group' => 'general',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'validate' => 'validateInteger',
- 'map_name' => 'OriginalImageWidth',
- ],
- // LocationShown and LocationCreated are handled
- // specially because they are hierarchical, but we
- // also want to merge with the old non-hierarchical.
- 'LocationShown' => [
- 'map_group' => 'special',
- 'mode' => XMPReader::MODE_BAGSTRUCT,
- 'children' => [
- 'WorldRegion' => true,
- 'CountryCode' => true, /* iso code */
- 'CountryName' => true,
- 'ProvinceState' => true,
- 'City' => true,
- 'Sublocation' => true,
- ],
- ],
- 'LocationCreated' => [
- 'map_group' => 'special',
- 'mode' => XMPReader::MODE_BAGSTRUCT,
- 'children' => [
- 'WorldRegion' => true,
- 'CountryCode' => true, /* iso code */
- 'CountryName' => true,
- 'ProvinceState' => true,
- 'City' => true,
- 'Sublocation' => true,
- ],
- ],
- 'WorldRegion' => [
- 'map_group' => 'special',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- 'CountryCode' => [
- 'map_group' => 'special',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- 'CountryName' => [
- 'map_group' => 'special',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- 'map_name' => 'Country',
- ],
- 'ProvinceState' => [
- 'map_group' => 'special',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- 'map_name' => 'ProvinceOrState',
- ],
- 'City' => [
- 'map_group' => 'special',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
- 'Sublocation' => [
- 'map_group' => 'special',
- 'mode' => XMPReader::MODE_SIMPLE,
- 'structPart' => true,
- ],
-
- /* Other props that might be interesting but
- * Not currently extracted:
- * ArtworkOrObject, (info about objects in picture)
- * DigitalSourceType
- * RegistryId
- */
- ],
-
- /* Plus props we might want to consider:
- * (Note: some of these have unclear/incomplete definitions
- * from the iptc4xmp standard).
- * ImageSupplier (kind of like iptc source field)
- * ImageSupplierId (id code for image from supplier)
- * CopyrightOwner
- * ImageCreator
- * Licensor
- * Various model release fields
- * Property release fields.
- */
- ];
-}
diff --git a/www/wiki/includes/media/XMPValidate.php b/www/wiki/includes/media/XMPValidate.php
deleted file mode 100644
index fe47f474..00000000
--- a/www/wiki/includes/media/XMPValidate.php
+++ /dev/null
@@ -1,398 +0,0 @@
-<?php
-/**
- * Methods for validating XMP properties.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Media
- */
-
-use Psr\Log\LoggerInterface;
-use Psr\Log\LoggerAwareInterface;
-
-/**
- * This contains some static methods for
- * validating XMP properties. See XMPInfo and XMPReader classes.
- *
- * Each of these functions take the same parameters
- * * an info array which is a subset of the XMPInfo::items array
- * * A value (passed as reference) to validate. This can be either a
- * simple value or an array
- * * A boolean to determine if this is validating a simple or complex values
- *
- * It should be noted that when an array is being validated, typically the validation
- * function is called once for each value, and then once at the end for the entire array.
- *
- * These validation functions can also be used to modify the data. See the gps and flash one's
- * for example.
- *
- * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart1.pdf starting at pg 28
- * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf starting at pg 11
- */
-class XMPValidate implements LoggerAwareInterface {
-
- /**
- * @var LoggerInterface
- */
- private $logger;
-
- public function __construct( LoggerInterface $logger ) {
- $this->setLogger( $logger );
- }
-
- public function setLogger( LoggerInterface $logger ) {
- $this->logger = $logger;
- }
- /**
- * Function to validate boolean properties ( True or False )
- *
- * @param array $info Information about current property
- * @param mixed &$val Current value to validate
- * @param bool $standalone If this is a simple property or array
- */
- public function validateBoolean( $info, &$val, $standalone ) {
- if ( !$standalone ) {
- // this only validates standalone properties, not arrays, etc
- return;
- }
- if ( $val !== 'True' && $val !== 'False' ) {
- $this->logger->info( __METHOD__ . " Expected True or False but got $val" );
- $val = null;
- }
- }
-
- /**
- * function to validate rational properties ( 12/10 )
- *
- * @param array $info Information about current property
- * @param mixed &$val Current value to validate
- * @param bool $standalone If this is a simple property or array
- */
- public function validateRational( $info, &$val, $standalone ) {
- if ( !$standalone ) {
- // this only validates standalone properties, not arrays, etc
- return;
- }
- if ( !preg_match( '/^(?:-?\d+)\/(?:\d+[1-9]|[1-9]\d*)$/D', $val ) ) {
- $this->logger->info( __METHOD__ . " Expected rational but got $val" );
- $val = null;
- }
- }
-
- /**
- * function to validate rating properties -1, 0-5
- *
- * if its outside of range put it into range.
- *
- * @see MWG spec
- * @param array $info Information about current property
- * @param mixed &$val Current value to validate
- * @param bool $standalone If this is a simple property or array
- */
- public function validateRating( $info, &$val, $standalone ) {
- if ( !$standalone ) {
- // this only validates standalone properties, not arrays, etc
- return;
- }
- if ( !preg_match( '/^[-+]?\d*(?:\.?\d*)$/D', $val )
- || !is_numeric( $val )
- ) {
- $this->logger->info( __METHOD__ . " Expected rating but got $val" );
- $val = null;
-
- return;
- } else {
- $nVal = (float)$val;
- if ( $nVal < 0 ) {
- // We do < 0 here instead of < -1 here, since
- // the values between 0 and -1 are also illegal
- // as -1 is meant as a special reject rating.
- $this->logger->info( __METHOD__ . " Rating too low, setting to -1 (Rejected)" );
- $val = '-1';
-
- return;
- }
- if ( $nVal > 5 ) {
- $this->logger->info( __METHOD__ . " Rating too high, setting to 5" );
- $val = '5';
-
- return;
- }
- }
- }
-
- /**
- * function to validate integers
- *
- * @param array $info Information about current property
- * @param mixed &$val Current value to validate
- * @param bool $standalone If this is a simple property or array
- */
- public function validateInteger( $info, &$val, $standalone ) {
- if ( !$standalone ) {
- // this only validates standalone properties, not arrays, etc
- return;
- }
- if ( !preg_match( '/^[-+]?\d+$/D', $val ) ) {
- $this->logger->info( __METHOD__ . " Expected integer but got $val" );
- $val = null;
- }
- }
-
- /**
- * function to validate properties with a fixed number of allowed
- * choices. (closed choice)
- *
- * @param array $info Information about current property
- * @param mixed &$val Current value to validate
- * @param bool $standalone If this is a simple property or array
- */
- public function validateClosed( $info, &$val, $standalone ) {
- if ( !$standalone ) {
- // this only validates standalone properties, not arrays, etc
- return;
- }
-
- // check if its in a numeric range
- $inRange = false;
- if ( isset( $info['rangeLow'] )
- && isset( $info['rangeHigh'] )
- && is_numeric( $val )
- && ( intval( $val ) <= $info['rangeHigh'] )
- && ( intval( $val ) >= $info['rangeLow'] )
- ) {
- $inRange = true;
- }
-
- if ( !isset( $info['choices'][$val] ) && !$inRange ) {
- $this->logger->info( __METHOD__ . " Expected closed choice, but got $val" );
- $val = null;
- }
- }
-
- /**
- * function to validate and modify flash structure
- *
- * @param array $info Information about current property
- * @param mixed &$val Current value to validate
- * @param bool $standalone If this is a simple property or array
- */
- public function validateFlash( $info, &$val, $standalone ) {
- if ( $standalone ) {
- // this only validates flash structs, not individual properties
- return;
- }
- if ( !( isset( $val['Fired'] )
- && isset( $val['Function'] )
- && isset( $val['Mode'] )
- && isset( $val['RedEyeMode'] )
- && isset( $val['Return'] )
- ) ) {
- $this->logger->info( __METHOD__ . " Flash structure did not have all the required components" );
- $val = null;
- } else {
- $val = ( "\0" | ( $val['Fired'] === 'True' )
- | ( intval( $val['Return'] ) << 1 )
- | ( intval( $val['Mode'] ) << 3 )
- | ( ( $val['Function'] === 'True' ) << 5 )
- | ( ( $val['RedEyeMode'] === 'True' ) << 6 ) );
- }
- }
-
- /**
- * function to validate LangCode properties ( en-GB, etc )
- *
- * This is just a naive check to make sure it somewhat looks like a lang code.
- *
- * @see BCP 47
- * @see https://wwwimages2.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/
- * XMP%20SDK%20Release%20cc-2014-12/XMPSpecificationPart1.pdf page 22 (section 8.2.2.4)
- *
- * @param array $info Information about current property
- * @param mixed &$val Current value to validate
- * @param bool $standalone If this is a simple property or array
- */
- public function validateLangCode( $info, &$val, $standalone ) {
- if ( !$standalone ) {
- // this only validates standalone properties, not arrays, etc
- return;
- }
- if ( !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $val ) ) {
- // this is a rather naive check.
- $this->logger->info( __METHOD__ . " Expected Lang code but got $val" );
- $val = null;
- }
- }
-
- /**
- * function to validate date properties, and convert to (partial) Exif format.
- *
- * Dates can be one of the following formats:
- * YYYY
- * YYYY-MM
- * YYYY-MM-DD
- * YYYY-MM-DDThh:mmTZD
- * YYYY-MM-DDThh:mm:ssTZD
- * YYYY-MM-DDThh:mm:ss.sTZD
- *
- * @param array $info Information about current property
- * @param mixed &$val Current value to validate. Converts to TS_EXIF as a side-effect.
- * in cases where there's only a partial date, it will give things like
- * 2011:04.
- * @param bool $standalone If this is a simple property or array
- */
- public function validateDate( $info, &$val, $standalone ) {
- if ( !$standalone ) {
- // this only validates standalone properties, not arrays, etc
- return;
- }
- $res = [];
- // @codingStandardsIgnoreStart Long line that cannot be broken
- if ( !preg_match(
- /* ahh! scary regex... */
- '/^([0-3]\d{3})(?:-([01]\d)(?:-([0-3]\d)(?:T([0-2]\d):([0-6]\d)(?::([0-6]\d)(?:\.\d+)?)?([-+]\d{2}:\d{2}|Z)?)?)?)?$/D',
- $val, $res )
- ) {
- // @codingStandardsIgnoreEnd
-
- $this->logger->info( __METHOD__ . " Expected date but got $val" );
- $val = null;
- } else {
- /*
- * $res is formatted as follows:
- * 0 -> full date.
- * 1 -> year, 2-> month, 3-> day, 4-> hour, 5-> minute, 6->second
- * 7-> Timezone specifier (Z or something like +12:30 )
- * many parts are optional, some aren't. For example if you specify
- * minute, you must specify hour, day, month, and year but not second or TZ.
- */
-
- /*
- * First of all, if year = 0000, Something is wrongish,
- * so don't extract. This seems to happen when
- * some programs convert between metadata formats.
- */
- if ( $res[1] === '0000' ) {
- $this->logger->info( __METHOD__ . " Invalid date (year 0): $val" );
- $val = null;
-
- return;
- }
-
- if ( !isset( $res[4] ) ) { // hour
- // just have the year month day (if that)
- $val = $res[1];
- if ( isset( $res[2] ) ) {
- $val .= ':' . $res[2];
- }
- if ( isset( $res[3] ) ) {
- $val .= ':' . $res[3];
- }
-
- return;
- }
-
- if ( !isset( $res[7] ) || $res[7] === 'Z' ) {
- // if hour is set, then minute must also be or regex above will fail.
- $val = $res[1] . ':' . $res[2] . ':' . $res[3]
- . ' ' . $res[4] . ':' . $res[5];
- if ( isset( $res[6] ) && $res[6] !== '' ) {
- $val .= ':' . $res[6];
- }
-
- return;
- }
-
- // Extra check for empty string necessary due to TZ but no second case.
- $stripSeconds = false;
- if ( !isset( $res[6] ) || $res[6] === '' ) {
- $res[6] = '00';
- $stripSeconds = true;
- }
-
- // Do timezone processing. We've already done the case that tz = Z.
-
- // We know that if we got to this step, year, month day hour and min must be set
- // by virtue of regex not failing.
-
- $unix = wfTimestamp( TS_UNIX, $res[1] . $res[2] . $res[3] . $res[4] . $res[5] . $res[6] );
- $offset = intval( substr( $res[7], 1, 2 ) ) * 60 * 60;
- $offset += intval( substr( $res[7], 4, 2 ) ) * 60;
- if ( substr( $res[7], 0, 1 ) === '-' ) {
- $offset = -$offset;
- }
- $val = wfTimestamp( TS_EXIF, $unix + $offset );
-
- if ( $stripSeconds ) {
- // If seconds weren't specified, remove the trailing ':00'.
- $val = substr( $val, 0, -3 );
- }
- }
- }
-
- /** function to validate, and more importantly
- * translate the XMP DMS form of gps coords to
- * the decimal form we use.
- *
- * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf
- * section 1.2.7.4 on page 23
- *
- * @param array $info Unused (info about prop)
- * @param string &$val GPS string in either DDD,MM,SSk or
- * or DDD,MM.mmk form
- * @param bool $standalone If its a simple prop (should always be true)
- */
- public function validateGPS( $info, &$val, $standalone ) {
- if ( !$standalone ) {
- return;
- }
-
- $m = [];
- if ( preg_match(
- '/(\d{1,3}),(\d{1,2}),(\d{1,2})([NWSE])/D',
- $val, $m )
- ) {
- $coord = intval( $m[1] );
- $coord += intval( $m[2] ) * ( 1 / 60 );
- $coord += intval( $m[3] ) * ( 1 / 3600 );
- if ( $m[4] === 'S' || $m[4] === 'W' ) {
- $coord = -$coord;
- }
- $val = $coord;
-
- return;
- } elseif ( preg_match(
- '/(\d{1,3}),(\d{1,2}(?:.\d*)?)([NWSE])/D',
- $val, $m )
- ) {
- $coord = intval( $m[1] );
- $coord += floatval( $m[2] ) * ( 1 / 60 );
- if ( $m[3] === 'S' || $m[3] === 'W' ) {
- $coord = -$coord;
- }
- $val = $coord;
-
- return;
- } else {
- $this->logger->info( __METHOD__
- . " Expected GPSCoordinate, but got $val." );
- $val = null;
-
- return;
- }
- }
-}
diff --git a/www/wiki/includes/mime.info b/www/wiki/includes/mime.info
deleted file mode 100644
index b04d3c68..00000000
--- a/www/wiki/includes/mime.info
+++ /dev/null
@@ -1,119 +0,0 @@
-# MIME type info file.
-# the first MIME type in each line is the "main" MIME type,
-# the others are aliases for this type
-# the media type is given in upper case and square brackets,
-# like [BITMAP], and must indicate a media type as defined by
-# the MEDIATYPE_xxx constants in Defines.php
-
-
-image/gif [BITMAP]
-image/png image/x-png [BITMAP]
-image/ief [BITMAP]
-image/jpeg image/pjpeg [BITMAP]
-image/jp2 [BITMAP]
-image/xbm [BITMAP]
-image/tiff [BITMAP]
-image/x-icon image/x-ico image/vnd.microsoft.icon [BITMAP]
-image/x-rgb [BITMAP]
-image/x-portable-pixmap [BITMAP]
-image/x-portable-graymap image/x-portable-greymap [BITMAP]
-image/x-bmp image/x-ms-bmp image/bmp application/x-bmp application/bmp [BITMAP]
-image/x-photoshop image/psd image/x-psd image/photoshop image/vnd.adobe.photoshop [BITMAP]
-image/vnd.djvu image/x.djvu image/x-djvu [BITMAP]
-image/webp [BITMAP]
-
-image/svg+xml application/svg+xml application/svg image/svg [DRAWING]
-application/postscript [DRAWING]
-application/x-latex [DRAWING]
-application/x-tex [DRAWING]
-application/x-dia-diagram [DRAWING]
-
-
-audio/mpeg audio/mp3 audio/mpeg3 [AUDIO]
-audio/mp4 [AUDIO]
-audio/wav audio/x-wav audio/wave [AUDIO]
-audio/midi audio/mid [AUDIO]
-audio/basic [AUDIO]
-audio/ogg [AUDIO]
-audio/x-aiff [AUDIO]
-audio/x-pn-realaudio [AUDIO]
-audio/x-realaudio [AUDIO]
-audio/webm [AUDIO]
-audio/x-matroska [AUDIO]
-audio/x-flac [AUDIO]
-audio/flac [AUDIO]
-
-video/mpeg application/mpeg [VIDEO]
-video/ogg [VIDEO]
-video/x-sgi-video [VIDEO]
-video/x-flv [VIDEO]
-video/webm [VIDEO]
-video/x-matroska [VIDEO]
-video/mp4 [VIDEO]
-
-application/ogg application/x-ogg audio/ogg audio/x-ogg video/ogg video/x-ogg [MULTIMEDIA]
-
-application/x-shockwave-flash [MULTIMEDIA]
-audio/x-pn-realaudio-plugin [MULTIMEDIA]
-model/iges [MULTIMEDIA]
-model/mesh [MULTIMEDIA]
-model/vrml [MULTIMEDIA]
-video/quicktime [MULTIMEDIA]
-video/x-msvideo [MULTIMEDIA]
-
-text/plain [TEXT]
-text/html application/xhtml+xml [TEXT]
-application/xml text/xml [TEXT]
-text [TEXT]
-application/json [TEXT]
-text/csv [TEXT]
-text/tab-separated-values [TEXT]
-
-application/zip application/x-zip [ARCHIVE]
-application/x-gzip [ARCHIVE]
-application/x-bzip [ARCHIVE]
-application/x-bzip2 [ARCHIVE]
-application/x-tar [ARCHIVE]
-application/x-stuffit [ARCHIVE]
-application/x-opc+zip [ARCHIVE]
-application/x-7z-compressed [ARCHIVE]
-
-application/javascript text/javascript application/x-javascript application/x-ecmascript text/ecmascript [EXECUTABLE]
-application/x-bash [EXECUTABLE]
-application/x-sh [EXECUTABLE]
-application/x-csh [EXECUTABLE]
-application/x-tcsh [EXECUTABLE]
-application/x-tcl [EXECUTABLE]
-application/x-perl [EXECUTABLE]
-application/x-python [EXECUTABLE]
-
-application/pdf application/acrobat [OFFICE]
-application/msword [OFFICE]
-application/vnd.ms-excel [OFFICE]
-application/vnd.ms-powerpoint [OFFICE]
-application/x-director [OFFICE]
-text/rtf [OFFICE]
-
-application/vnd.openxmlformats-officedocument.wordprocessingml.document [OFFICE]
-application/vnd.openxmlformats-officedocument.wordprocessingml.template [OFFICE]
-application/vnd.ms-word.document.macroEnabled.12 [OFFICE]
-application/vnd.ms-word.template.macroEnabled.12 [OFFICE]
-application/vnd.openxmlformats-officedocument.presentationml.template [OFFICE]
-application/vnd.openxmlformats-officedocument.presentationml.slideshow [OFFICE]
-application/vnd.openxmlformats-officedocument.presentationml.presentation [OFFICE]
-application/vnd.ms-powerpoint.addin.macroEnabled.12 [OFFICE]
-application/vnd.ms-powerpoint.presentation.macroEnabled.12 [OFFICE]
-application/vnd.ms-powerpoint.presentation.macroEnabled.12 [OFFICE]
-application/vnd.ms-powerpoint.slideshow.macroEnabled.12 [OFFICE]
-application/vnd.openxmlformats-officedocument.spreadsheetml.sheet [OFFICE]
-application/vnd.openxmlformats-officedocument.spreadsheetml.template [OFFICE]
-application/vnd.ms-excel.sheet.macroEnabled.12 [OFFICE]
-application/vnd.ms-excel.template.macroEnabled.12 [OFFICE]
-application/vnd.ms-excel.addin.macroEnabled.12 [OFFICE]
-application/vnd.ms-excel.sheet.binary.macroEnabled.12 [OFFICE]
-application/acad application/x-acad application/autocad_dwg image/x-dwg application/dwg application/x-dwg application/x-autocad image/vnd.dwg drawing/dwg [DRAWING]
-chemical/x-mdl-molfile [DRAWING]
-chemical/x-mdl-sdfile [DRAWING]
-chemical/x-mdl-rxnfile [DRAWING]
-chemical/x-mdl-rdfile [DRAWING]
-chemical/x-mdl-rgfile [DRAWING]
diff --git a/www/wiki/includes/mime.types b/www/wiki/includes/mime.types
deleted file mode 100644
index 1ef4d26f..00000000
--- a/www/wiki/includes/mime.types
+++ /dev/null
@@ -1,186 +0,0 @@
-application/acad dwg
-application/andrew-inset ez
-application/mac-binhex40 hqx
-application/mac-compactpro cpt
-application/mathml+xml mathml
-application/msword doc dot
-application/octet-stream bin dms lha lzh exe class so dll
-application/oda oda
-application/ogg ogx ogg ogm ogv oga spx opus
-application/pdf pdf
-application/postscript ai eps ps
-application/rdf+xml rdf
-application/smil smi smil
-application/srgs gram
-application/srgs+xml grxml
-application/vnd.mif mif
-application/vnd.ms-excel xls xlt xla
-application/vnd.ms-powerpoint ppt pot pps ppa
-application/vnd.wap.wbxml wbxml
-application/vnd.wap.wmlc wmlc
-application/vnd.wap.wmlscriptc wmlsc
-application/voicexml+xml vxml
-application/x-7z-compressed 7z
-application/x-bcpio bcpio
-application/x-bzip bz
-application/x-bzip2 bz2
-application/x-cdlink vcd
-application/x-chess-pgn pgn
-application/x-cpio cpio
-application/x-csh csh
-application/x-dia-diagram dia
-application/x-director dcr dir dxr
-application/x-dvi dvi
-application/x-futuresplash spl
-application/x-gtar gtar tar
-application/x-gzip gz
-application/x-hdf hdf
-application/x-jar jar
-application/javascript js
-application/json json
-application/x-koan skp skd skt skm
-application/x-latex latex
-application/x-netcdf nc cdf
-application/x-sh sh
-application/x-shar shar
-application/x-shockwave-flash swf
-application/x-stuffit sit
-application/x-sv4cpio sv4cpio
-application/x-sv4crc sv4crc
-application/x-tar tar
-application/x-tcl tcl
-application/x-tex tex
-application/x-texinfo texinfo texi
-application/x-troff t tr roff
-application/x-troff-man man
-application/x-troff-me me
-application/x-troff-ms ms
-application/x-ustar ustar
-application/x-wais-source src
-application/x-xpinstall xpi
-application/xhtml+xml xhtml xht
-application/xslt+xml xslt
-application/xml xml xsl xsd kml
-application/xml-dtd dtd
-application/zip zip jar xpi sxc stc sxd std sxi sti sxm stm sxw stw
-application/x-rar rar
-application/font-woff woff
-application/font-woff2 woff2
-application/vnd.ms-fontobject eot
-application/x-font-ttf ttf
-audio/basic au snd
-audio/midi mid midi kar
-audio/mpeg mpga mp2 mp3
-audio/ogg oga ogg spx opus
-video/webm webm
-audio/webm webm
-audio/x-aiff aif aiff aifc
-audio/x-matroska mka mkv
-audio/x-mpegurl m3u
-audio/x-ogg oga ogg spx opus
-audio/x-pn-realaudio ram rm
-audio/x-pn-realaudio-plugin rpm
-audio/x-realaudio ra
-audio/x-wav wav
-audio/wav wav
-audio/x-flac flac
-audio/flac flac
-chemical/x-pdb pdb
-chemical/x-xyz xyz
-image/bmp bmp
-image/cgm cgm
-image/gif gif
-image/ief ief
-image/jp2 j2k jp2 jpg2
-image/jpeg jpeg jpg jpe
-image/png png apng
-image/svg+xml svg
-image/tiff tiff tif
-image/vnd.djvu djvu djv
-image/vnd.microsoft.icon ico
-image/vnd.wap.wbmp wbmp
-image/webp webp
-image/x-cmu-raster ras
-image/x-icon ico
-image/x-ms-bmp bmp
-image/x-portable-anymap pnm
-image/x-portable-bitmap pbm
-image/x-portable-graymap pgm
-image/x-portable-pixmap ppm
-image/x-rgb rgb
-image/x-photoshop psd
-image/x-xbitmap xbm
-image/x-xpixmap xpm
-image/x-xwindowdump xwd
-model/iges igs iges
-model/mesh msh mesh silo
-model/vrml wrl vrml
-text/calendar ics ifb
-text/css css
-text/csv csv
-text/html html htm
-text/plain txt
-text/richtext rtx
-text/rtf rtf
-text/sgml sgml sgm
-text/tab-separated-values tsv
-text/vnd.wap.wml wml
-text/vnd.wap.wmlscript wmls
-text/xml xml xsl xslt rss rdf
-text/x-component htc
-text/x-setext etx
-text/x-sawfish jl
-video/mpeg mpeg mpg mpe
-video/mp4 mp4 m4a m4p m4b m4r m4v
-audio/mp4 m4a
-video/ogg ogv ogm ogg
-video/quicktime qt mov
-video/vnd.mpegurl mxu
-video/x-flv flv
-video/x-matroska mkv mka
-video/x-msvideo avi
-video/x-ogg ogv ogm ogg
-video/x-sgi-movie movie
-x-conference/x-cooltalk ice
-application/vnd.oasis.opendocument.chart odc
-application/vnd.oasis.opendocument.chart-template otc
-application/vnd.oasis.opendocument.database odb
-application/vnd.oasis.opendocument.formula odf
-application/vnd.oasis.opendocument.formula-template otf
-application/vnd.oasis.opendocument.graphics odg
-application/vnd.oasis.opendocument.graphics-template otg
-application/vnd.oasis.opendocument.image odi
-application/vnd.oasis.opendocument.image-template oti
-application/vnd.oasis.opendocument.presentation odp
-application/vnd.oasis.opendocument.presentation-template otp
-application/vnd.oasis.opendocument.spreadsheet ods
-application/vnd.oasis.opendocument.spreadsheet-template ots
-application/vnd.oasis.opendocument.text odt
-application/vnd.oasis.opendocument.text-master odm
-application/vnd.oasis.opendocument.text-template ott
-application/vnd.oasis.opendocument.text-web oth
-application/vnd.openxmlformats-officedocument.wordprocessingml.document docx
-application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx
-application/vnd.ms-word.document.macroEnabled.12 docm
-application/vnd.ms-word.template.macroEnabled.12 dotm
-application/vnd.openxmlformats-officedocument.presentationml.template potx
-application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx
-application/vnd.openxmlformats-officedocument.presentationml.presentation pptx
-application/vnd.ms-powerpoint.addin.macroEnabled.12 ppam
-application/vnd.ms-powerpoint.presentation.macroEnabled.12 pptm
-application/vnd.ms-powerpoint.presentation.macroEnabled.12 potm
-application/vnd.ms-powerpoint.slideshow.macroEnabled.12 ppsm
-application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx
-application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx
-application/vnd.ms-excel.sheet.macroEnabled.12 xlsm
-application/vnd.ms-excel.template.macroEnabled.12 xltm
-application/vnd.ms-excel.addin.macroEnabled.12 xlam
-application/vnd.ms-excel.sheet.binary.macroEnabled.12 xlsb
-model/vnd.dwfx+xps dwfx
-application/vnd.ms-xpsdocument xps
-application/x-opc+zip docx dotx docm dotm potx ppsx pptx ppam pptm potm ppsm xlsx xltx xlsm xltm xlam xlsb dwfx xps
-chemical/x-mdl-molfile mol
-chemical/x-mdl-sdfile sdf
-chemical/x-mdl-rxnfile rxn
-chemical/x-mdl-rdfile rd
-chemical/x-mdl-rgfile rg
diff --git a/www/wiki/includes/objectcache/MemcachedPeclBagOStuff.php b/www/wiki/includes/objectcache/MemcachedPeclBagOStuff.php
deleted file mode 100644
index 5ca8560f..00000000
--- a/www/wiki/includes/objectcache/MemcachedPeclBagOStuff.php
+++ /dev/null
@@ -1,241 +0,0 @@
-<?php
-/**
- * Object caching using memcached.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Cache
- */
-
-/**
- * A wrapper class for the PECL memcached client
- *
- * @ingroup Cache
- */
-class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
-
- /**
- * Constructor
- *
- * Available parameters are:
- * - servers: The list of IP:port combinations holding the memcached servers.
- * - persistent: Whether to use a persistent connection
- * - compress_threshold: The minimum size an object must be before it is compressed
- * - timeout: The read timeout in microseconds
- * - connect_timeout: The connect timeout in seconds
- * - retry_timeout: Time in seconds to wait before retrying a failed connect attempt
- * - server_failure_limit: Limit for server connect failures before it is removed
- * - serializer: May be either "php" or "igbinary". Igbinary produces more compact
- * values, but serialization is much slower unless the php.ini option
- * igbinary.compact_strings is off.
- * @param array $params
- * @throws InvalidArgumentException
- */
- function __construct( $params ) {
- parent::__construct( $params );
- $params = $this->applyDefaultParams( $params );
-
- if ( $params['persistent'] ) {
- // The pool ID must be unique to the server/option combination.
- // The Memcached object is essentially shared for each pool ID.
- // We can only reuse a pool ID if we keep the config consistent.
- $this->client = new Memcached( md5( serialize( $params ) ) );
- if ( count( $this->client->getServerList() ) ) {
- $this->logger->debug( __METHOD__ . ": persistent Memcached object already loaded." );
- return; // already initialized; don't add duplicate servers
- }
- } else {
- $this->client = new Memcached;
- }
-
- if ( !isset( $params['serializer'] ) ) {
- $params['serializer'] = 'php';
- }
-
- if ( isset( $params['retry_timeout'] ) ) {
- $this->client->setOption( Memcached::OPT_RETRY_TIMEOUT, $params['retry_timeout'] );
- }
-
- if ( isset( $params['server_failure_limit'] ) ) {
- $this->client->setOption( Memcached::OPT_SERVER_FAILURE_LIMIT, $params['server_failure_limit'] );
- }
-
- // The compression threshold is an undocumented php.ini option for some
- // reason. There's probably not much harm in setting it globally, for
- // compatibility with the settings for the PHP client.
- ini_set( 'memcached.compression_threshold', $params['compress_threshold'] );
-
- // Set timeouts
- $this->client->setOption( Memcached::OPT_CONNECT_TIMEOUT, $params['connect_timeout'] * 1000 );
- $this->client->setOption( Memcached::OPT_SEND_TIMEOUT, $params['timeout'] );
- $this->client->setOption( Memcached::OPT_RECV_TIMEOUT, $params['timeout'] );
- $this->client->setOption( Memcached::OPT_POLL_TIMEOUT, $params['timeout'] / 1000 );
-
- // Set libketama mode since it's recommended by the documentation and
- // is as good as any. There's no way to configure libmemcached to use
- // hashes identical to the ones currently in use by the PHP client, and
- // even implementing one of the libmemcached hashes in pure PHP for
- // forwards compatibility would require MemcachedClient::get_sock() to be
- // rewritten.
- $this->client->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true );
-
- // Set the serializer
- switch ( $params['serializer'] ) {
- case 'php':
- $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP );
- break;
- case 'igbinary':
- if ( !Memcached::HAVE_IGBINARY ) {
- throw new InvalidArgumentException(
- __CLASS__ . ': the igbinary extension is not available ' .
- 'but igbinary serialization was requested.'
- );
- }
- $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY );
- break;
- default:
- throw new InvalidArgumentException(
- __CLASS__ . ': invalid value for serializer parameter'
- );
- }
- $servers = [];
- foreach ( $params['servers'] as $host ) {
- $servers[] = IP::splitHostAndPort( $host ); // (ip, port)
- }
- $this->client->addServers( $servers );
- }
-
- protected function getWithToken( $key, &$casToken, $flags = 0 ) {
- $this->debugLog( "get($key)" );
- if ( defined( Memcached::class . '::GET_EXTENDED' ) ) { // v3.0.0
- $flags = Memcached::GET_EXTENDED;
- $res = $this->client->get( $this->validateKeyEncoding( $key ), null, $flags );
- if ( is_array( $res ) ) {
- $result = $res['value'];
- $casToken = $res['cas'];
- } else {
- $result = false;
- $casToken = null;
- }
- } else {
- $result = $this->client->get( $this->validateKeyEncoding( $key ), null, $casToken );
- }
- $result = $this->checkResult( $key, $result );
- return $result;
- }
-
- public function set( $key, $value, $exptime = 0, $flags = 0 ) {
- $this->debugLog( "set($key)" );
- return $this->checkResult( $key, parent::set( $key, $value, $exptime ) );
- }
-
- protected function cas( $casToken, $key, $value, $exptime = 0 ) {
- $this->debugLog( "cas($key)" );
- return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime ) );
- }
-
- public function delete( $key ) {
- $this->debugLog( "delete($key)" );
- $result = parent::delete( $key );
- if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTFOUND ) {
- // "Not found" is counted as success in our interface
- return true;
- } else {
- return $this->checkResult( $key, $result );
- }
- }
-
- public function add( $key, $value, $exptime = 0 ) {
- $this->debugLog( "add($key)" );
- return $this->checkResult( $key, parent::add( $key, $value, $exptime ) );
- }
-
- public function incr( $key, $value = 1 ) {
- $this->debugLog( "incr($key)" );
- $result = $this->client->increment( $key, $value );
- return $this->checkResult( $key, $result );
- }
-
- public function decr( $key, $value = 1 ) {
- $this->debugLog( "decr($key)" );
- $result = $this->client->decrement( $key, $value );
- return $this->checkResult( $key, $result );
- }
-
- /**
- * Check the return value from a client method call and take any necessary
- * action. Returns the value that the wrapper function should return. At
- * present, the return value is always the same as the return value from
- * the client, but some day we might find a case where it should be
- * different.
- *
- * @param string $key The key used by the caller, or false if there wasn't one.
- * @param mixed $result The return value
- * @return mixed
- */
- protected function checkResult( $key, $result ) {
- if ( $result !== false ) {
- return $result;
- }
- switch ( $this->client->getResultCode() ) {
- case Memcached::RES_SUCCESS:
- break;
- case Memcached::RES_DATA_EXISTS:
- case Memcached::RES_NOTSTORED:
- case Memcached::RES_NOTFOUND:
- $this->debugLog( "result: " . $this->client->getResultMessage() );
- break;
- default:
- $msg = $this->client->getResultMessage();
- $logCtx = [];
- if ( $key !== false ) {
- $server = $this->client->getServerByKey( $key );
- $logCtx['memcached-server'] = "{$server['host']}:{$server['port']}";
- $logCtx['memcached-key'] = $key;
- $msg = "Memcached error for key \"{memcached-key}\" on server \"{memcached-server}\": $msg";
- } else {
- $msg = "Memcached error: $msg";
- }
- $this->logger->error( $msg, $logCtx );
- $this->setLastError( BagOStuff::ERR_UNEXPECTED );
- }
- return $result;
- }
-
- public function getMulti( array $keys, $flags = 0 ) {
- $this->debugLog( 'getMulti(' . implode( ', ', $keys ) . ')' );
- foreach ( $keys as $key ) {
- $this->validateKeyEncoding( $key );
- }
- $result = $this->client->getMulti( $keys ) ?: [];
- return $this->checkResult( false, $result );
- }
-
- /**
- * @param array $data
- * @param int $exptime
- * @return bool
- */
- public function setMulti( array $data, $exptime = 0 ) {
- $this->debugLog( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' );
- foreach ( array_keys( $data ) as $key ) {
- $this->validateKeyEncoding( $key );
- }
- $result = $this->client->setMulti( $data, $this->fixExpiry( $exptime ) );
- return $this->checkResult( false, $result );
- }
-}
diff --git a/www/wiki/includes/objectcache/ObjectCache.php b/www/wiki/includes/objectcache/ObjectCache.php
index 3370e5b9..67d23460 100644
--- a/www/wiki/includes/objectcache/ObjectCache.php
+++ b/www/wiki/includes/objectcache/ObjectCache.php
@@ -332,7 +332,11 @@ class ObjectCache {
* @throws UnexpectedValueException
*/
public static function newWANCacheFromParams( array $params ) {
- $erGroup = MediaWikiServices::getInstance()->getEventRelayerGroup();
+ global $wgCommandLineMode;
+
+ $services = MediaWikiServices::getInstance();
+
+ $erGroup = $services->getEventRelayerGroup();
foreach ( $params['channels'] as $action => $channel ) {
$params['relayers'][$action] = $erGroup->getRelayer( $channel );
$params['channels'][$action] = $channel;
@@ -343,6 +347,12 @@ class ObjectCache {
} else {
$params['logger'] = LoggerFactory::getInstance( 'objectcache' );
}
+ if ( !$wgCommandLineMode ) {
+ // Send the statsd data post-send on HTTP requests; avoid in CLI mode (T181385)
+ $params['stats'] = $services->getStatsdDataFactory();
+ // Let pre-emptive refreshes happen post-send on HTTP requests
+ $params['asyncHandler'] = [ DeferredUpdates::class, 'addCallableUpdate' ];
+ }
$class = $params['class'];
return new $class( $params );
diff --git a/www/wiki/includes/objectcache/RedisBagOStuff.php b/www/wiki/includes/objectcache/RedisBagOStuff.php
deleted file mode 100644
index 61e6926c..00000000
--- a/www/wiki/includes/objectcache/RedisBagOStuff.php
+++ /dev/null
@@ -1,412 +0,0 @@
-<?php
-/**
- * Object caching using Redis (http://redis.io/).
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * Redis-based caching module for redis server >= 2.6.12
- *
- * @note: avoid use of Redis::MULTI transactions for twemproxy support
- */
-class RedisBagOStuff extends BagOStuff {
- /** @var RedisConnectionPool */
- protected $redisPool;
- /** @var array List of server names */
- protected $servers;
- /** @var array Map of (tag => server name) */
- protected $serverTagMap;
- /** @var bool */
- protected $automaticFailover;
-
- /**
- * Construct a RedisBagOStuff object. Parameters are:
- *
- * - servers: An array of server names. A server name may be a hostname,
- * a hostname/port combination or the absolute path of a UNIX socket.
- * If a hostname is specified but no port, the standard port number
- * 6379 will be used. Arrays keys can be used to specify the tag to
- * hash on in place of the host/port. Required.
- *
- * - connectTimeout: The timeout for new connections, in seconds. Optional,
- * default is 1 second.
- *
- * - persistent: Set this to true to allow connections to persist across
- * multiple web requests. False by default.
- *
- * - password: The authentication password, will be sent to Redis in
- * clear text. Optional, if it is unspecified, no AUTH command will be
- * sent.
- *
- * - automaticFailover: If this is false, then each key will be mapped to
- * a single server, and if that server is down, any requests for that key
- * will fail. If this is true, a connection failure will cause the client
- * to immediately try the next server in the list (as determined by a
- * consistent hashing algorithm). True by default. This has the
- * potential to create consistency issues if a server is slow enough to
- * flap, for example if it is in swap death.
- * @param array $params
- */
- function __construct( $params ) {
- parent::__construct( $params );
- $redisConf = [ 'serializer' => 'none' ]; // manage that in this class
- foreach ( [ 'connectTimeout', 'persistent', 'password' ] as $opt ) {
- if ( isset( $params[$opt] ) ) {
- $redisConf[$opt] = $params[$opt];
- }
- }
- $this->redisPool = RedisConnectionPool::singleton( $redisConf );
-
- $this->servers = $params['servers'];
- foreach ( $this->servers as $key => $server ) {
- $this->serverTagMap[is_int( $key ) ? $server : $key] = $server;
- }
-
- if ( isset( $params['automaticFailover'] ) ) {
- $this->automaticFailover = $params['automaticFailover'];
- } else {
- $this->automaticFailover = true;
- }
- }
-
- protected function doGet( $key, $flags = 0 ) {
- list( $server, $conn ) = $this->getConnection( $key );
- if ( !$conn ) {
- return false;
- }
- try {
- $value = $conn->get( $key );
- $result = $this->unserialize( $value );
- } catch ( RedisException $e ) {
- $result = false;
- $this->handleException( $conn, $e );
- }
-
- $this->logRequest( 'get', $key, $server, $result );
- return $result;
- }
-
- public function set( $key, $value, $expiry = 0, $flags = 0 ) {
- list( $server, $conn ) = $this->getConnection( $key );
- if ( !$conn ) {
- return false;
- }
- $expiry = $this->convertToRelative( $expiry );
- try {
- if ( $expiry ) {
- $result = $conn->setex( $key, $expiry, $this->serialize( $value ) );
- } else {
- // No expiry, that is very different from zero expiry in Redis
- $result = $conn->set( $key, $this->serialize( $value ) );
- }
- } catch ( RedisException $e ) {
- $result = false;
- $this->handleException( $conn, $e );
- }
-
- $this->logRequest( 'set', $key, $server, $result );
- return $result;
- }
-
- public function delete( $key ) {
- list( $server, $conn ) = $this->getConnection( $key );
- if ( !$conn ) {
- return false;
- }
- try {
- $conn->delete( $key );
- // Return true even if the key didn't exist
- $result = true;
- } catch ( RedisException $e ) {
- $result = false;
- $this->handleException( $conn, $e );
- }
-
- $this->logRequest( 'delete', $key, $server, $result );
- return $result;
- }
-
- public function getMulti( array $keys, $flags = 0 ) {
- $batches = [];
- $conns = [];
- foreach ( $keys as $key ) {
- list( $server, $conn ) = $this->getConnection( $key );
- if ( !$conn ) {
- continue;
- }
- $conns[$server] = $conn;
- $batches[$server][] = $key;
- }
- $result = [];
- foreach ( $batches as $server => $batchKeys ) {
- $conn = $conns[$server];
- try {
- $conn->multi( Redis::PIPELINE );
- foreach ( $batchKeys as $key ) {
- $conn->get( $key );
- }
- $batchResult = $conn->exec();
- if ( $batchResult === false ) {
- $this->debug( "multi request to $server failed" );
- continue;
- }
- foreach ( $batchResult as $i => $value ) {
- if ( $value !== false ) {
- $result[$batchKeys[$i]] = $this->unserialize( $value );
- }
- }
- } catch ( RedisException $e ) {
- $this->handleException( $conn, $e );
- }
- }
-
- $this->debug( "getMulti for " . count( $keys ) . " keys " .
- "returned " . count( $result ) . " results" );
- return $result;
- }
-
- /**
- * @param array $data
- * @param int $expiry
- * @return bool
- */
- public function setMulti( array $data, $expiry = 0 ) {
- $batches = [];
- $conns = [];
- foreach ( $data as $key => $value ) {
- list( $server, $conn ) = $this->getConnection( $key );
- if ( !$conn ) {
- continue;
- }
- $conns[$server] = $conn;
- $batches[$server][] = $key;
- }
-
- $expiry = $this->convertToRelative( $expiry );
- $result = true;
- foreach ( $batches as $server => $batchKeys ) {
- $conn = $conns[$server];
- try {
- $conn->multi( Redis::PIPELINE );
- foreach ( $batchKeys as $key ) {
- if ( $expiry ) {
- $conn->setex( $key, $expiry, $this->serialize( $data[$key] ) );
- } else {
- $conn->set( $key, $this->serialize( $data[$key] ) );
- }
- }
- $batchResult = $conn->exec();
- if ( $batchResult === false ) {
- $this->debug( "setMulti request to $server failed" );
- continue;
- }
- foreach ( $batchResult as $value ) {
- if ( $value === false ) {
- $result = false;
- }
- }
- } catch ( RedisException $e ) {
- $this->handleException( $server, $conn, $e );
- $result = false;
- }
- }
-
- return $result;
- }
-
- public function add( $key, $value, $expiry = 0 ) {
- list( $server, $conn ) = $this->getConnection( $key );
- if ( !$conn ) {
- return false;
- }
- $expiry = $this->convertToRelative( $expiry );
- try {
- if ( $expiry ) {
- $result = $conn->set(
- $key,
- $this->serialize( $value ),
- [ 'nx', 'ex' => $expiry ]
- );
- } else {
- $result = $conn->setnx( $key, $this->serialize( $value ) );
- }
- } catch ( RedisException $e ) {
- $result = false;
- $this->handleException( $conn, $e );
- }
-
- $this->logRequest( 'add', $key, $server, $result );
- return $result;
- }
-
- /**
- * Non-atomic implementation of incr().
- *
- * Probably all callers actually want incr() to atomically initialise
- * values to zero if they don't exist, as provided by the Redis INCR
- * command. But we are constrained by the memcached-like interface to
- * return null in that case. Once the key exists, further increments are
- * atomic.
- * @param string $key Key to increase
- * @param int $value Value to add to $key (Default 1)
- * @return int|bool New value or false on failure
- */
- public function incr( $key, $value = 1 ) {
- list( $server, $conn ) = $this->getConnection( $key );
- if ( !$conn ) {
- return false;
- }
- if ( !$conn->exists( $key ) ) {
- return null;
- }
- try {
- // @FIXME: on races, the key may have a 0 TTL
- $result = $conn->incrBy( $key, $value );
- } catch ( RedisException $e ) {
- $result = false;
- $this->handleException( $conn, $e );
- }
-
- $this->logRequest( 'incr', $key, $server, $result );
- return $result;
- }
-
- public function modifySimpleRelayEvent( array $event ) {
- if ( array_key_exists( 'val', $event ) ) {
- $event['val'] = serialize( $event['val'] ); // this class uses PHP serialization
- }
-
- return $event;
- }
-
- /**
- * @param mixed $data
- * @return string
- */
- protected function serialize( $data ) {
- // Serialize anything but integers so INCR/DECR work
- // Do not store integer-like strings as integers to avoid type confusion (bug 60563)
- return is_int( $data ) ? $data : serialize( $data );
- }
-
- /**
- * @param string $data
- * @return mixed
- */
- protected function unserialize( $data ) {
- return ctype_digit( $data ) ? intval( $data ) : unserialize( $data );
- }
-
- /**
- * Get a Redis object with a connection suitable for fetching the specified key
- * @param string $key
- * @return array (server, RedisConnRef) or (false, false)
- */
- protected function getConnection( $key ) {
- $candidates = array_keys( $this->serverTagMap );
-
- if ( count( $this->servers ) > 1 ) {
- ArrayUtils::consistentHashSort( $candidates, $key, '/' );
- if ( !$this->automaticFailover ) {
- $candidates = array_slice( $candidates, 0, 1 );
- }
- }
-
- while ( ( $tag = array_shift( $candidates ) ) !== null ) {
- $server = $this->serverTagMap[$tag];
- $conn = $this->redisPool->getConnection( $server );
- if ( !$conn ) {
- continue;
- }
-
- // If automatic failover is enabled, check that the server's link
- // to its master (if any) is up -- but only if there are other
- // viable candidates left to consider. Also, getMasterLinkStatus()
- // does not work with twemproxy, though $candidates will be empty
- // by now in such cases.
- if ( $this->automaticFailover && $candidates ) {
- try {
- if ( $this->getMasterLinkStatus( $conn ) === 'down' ) {
- // If the master cannot be reached, fail-over to the next server.
- // If masters are in data-center A, and slaves in data-center B,
- // this helps avoid the case were fail-over happens in A but not
- // to the corresponding server in B (e.g. read/write mismatch).
- continue;
- }
- } catch ( RedisException $e ) {
- // Server is not accepting commands
- $this->handleException( $conn, $e );
- continue;
- }
- }
-
- return [ $server, $conn ];
- }
-
- $this->setLastError( BagOStuff::ERR_UNREACHABLE );
-
- return [ false, false ];
- }
-
- /**
- * Check the master link status of a Redis server that is configured as a slave.
- * @param RedisConnRef $conn
- * @return string|null Master link status (either 'up' or 'down'), or null
- * if the server is not a slave.
- */
- protected function getMasterLinkStatus( RedisConnRef $conn ) {
- $info = $conn->info();
- return isset( $info['master_link_status'] )
- ? $info['master_link_status']
- : null;
- }
-
- /**
- * Log a fatal error
- * @param string $msg
- */
- protected function logError( $msg ) {
- $this->logger->error( "Redis error: $msg" );
- }
-
- /**
- * The redis extension throws an exception in response to various read, write
- * and protocol errors. Sometimes it also closes the connection, sometimes
- * not. The safest response for us is to explicitly destroy the connection
- * object and let it be reopened during the next request.
- * @param RedisConnRef $conn
- * @param Exception $e
- */
- protected function handleException( RedisConnRef $conn, $e ) {
- $this->setLastError( BagOStuff::ERR_UNEXPECTED );
- $this->redisPool->handleError( $conn, $e );
- }
-
- /**
- * Send information about a single request to the debug log
- * @param string $method
- * @param string $key
- * @param string $server
- * @param bool $result
- */
- public function logRequest( $method, $key, $server, $result ) {
- $this->debug( "$method $key on $server: " .
- ( $result === false ? "failure" : "success" ) );
- }
-}
diff --git a/www/wiki/includes/objectcache/SqlBagOStuff.php b/www/wiki/includes/objectcache/SqlBagOStuff.php
index 2cfd2a1d..8ff14ed7 100644
--- a/www/wiki/includes/objectcache/SqlBagOStuff.php
+++ b/www/wiki/includes/objectcache/SqlBagOStuff.php
@@ -21,15 +21,15 @@
* @ingroup Cache
*/
+use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\DBError;
use Wikimedia\Rdbms\DBQueryError;
use Wikimedia\Rdbms\DBConnectionError;
-use \MediaWiki\MediaWikiServices;
-use \Wikimedia\WaitConditionLoop;
-use \Wikimedia\Rdbms\TransactionProfiler;
use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\TransactionProfiler;
+use Wikimedia\WaitConditionLoop;
/**
* Class to store objects in the database
@@ -181,7 +181,7 @@ class SqlBagOStuff extends BagOStuff {
$index = $this->replicaOnly ? DB_REPLICA : DB_MASTER;
if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
// Keep a separate connection to avoid contention and deadlocks
- $db = $lb->getConnection( $index, [], false, $lb::CONN_TRX_AUTO );
+ $db = $lb->getConnection( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT );
// @TODO: Use a blank trx profiler to ignore expections as this is a cache
} else {
// However, SQLite has the opposite behavior due to DB-level locking.
@@ -681,9 +681,9 @@ class SqlBagOStuff extends BagOStuff {
*/
protected function unserialize( $serial ) {
if ( function_exists( 'gzinflate' ) ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$decomp = gzinflate( $serial );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( false !== $decomp ) {
$serial = $decomp;
@@ -808,6 +808,9 @@ class SqlBagOStuff extends BagOStuff {
// Main LB is used; wait for any replica DBs to catch up
$masterPos = $lb->getMasterPos();
+ if ( !$masterPos ) {
+ return true; // not applicable
+ }
$loop = new WaitConditionLoop(
function () use ( $lb, $masterPos ) {
diff --git a/www/wiki/includes/page/Article.php b/www/wiki/includes/page/Article.php
index f03bcc20..8fff6147 100644
--- a/www/wiki/includes/page/Article.php
+++ b/www/wiki/includes/page/Article.php
@@ -76,6 +76,13 @@ class Article implements Page {
public $mParserOutput;
/**
+ * @var bool Whether render() was called. With the way subclasses work
+ * here, there doesn't seem to be any other way to stop calling
+ * OutputPage::enableSectionEditLinks() and still have it work as it did before.
+ */
+ private $disableSectionEditForRender = false;
+
+ /**
* Constructor and clear the article
* @param Title $title Reference to a Title object.
* @param int $oldId Revision ID, null to fetch from request, zero for current
@@ -469,12 +476,15 @@ class Article implements Page {
$parserCache = MediaWikiServices::getInstance()->getParserCache();
$parserOptions = $this->getParserOptions();
+ $poOptions = [];
# Render printable version, use printable version cache
if ( $outputPage->isPrintable() ) {
$parserOptions->setIsPrintable( true );
- $parserOptions->setEditSection( false );
- } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit', $user ) ) {
- $parserOptions->setEditSection( false );
+ $poOptions['enableSectionEditLinks'] = false;
+ } elseif ( $this->disableSectionEditForRender
+ || !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit', $user )
+ ) {
+ $poOptions['enableSectionEditLinks'] = false;
}
# Try client and file cache
@@ -533,7 +543,7 @@ class Article implements Page {
} else {
wfDebug( __METHOD__ . ": showing parser cache contents\n" );
}
- $outputPage->addParserOutput( $this->mParserOutput );
+ $outputPage->addParserOutput( $this->mParserOutput, $poOptions );
# Ensure that UI elements requiring revision ID have
# the correct version information.
$outputPage->setRevisionId( $this->mPage->getLatest() );
@@ -568,7 +578,7 @@ class Article implements Page {
$outputPage->setRevisionTimestamp( $this->mPage->getTimestamp() );
# Pages containing custom CSS or JavaScript get special treatment
- if ( $this->getTitle()->isCssOrJsPage() || $this->getTitle()->isCssJsSubpage() ) {
+ if ( $this->getTitle()->isSiteConfigPage() || $this->getTitle()->isUserConfigPage() ) {
$dir = $this->getContext()->getLanguage()->getDir();
$lang = $this->getContext()->getLanguage()->getHtmlCode();
@@ -599,14 +609,14 @@ class Article implements Page {
$outputPage->setRobotPolicy( 'noindex,nofollow' );
$errortext = $error->getWikiText( false, 'view-pool-error' );
- $outputPage->addWikiText( '<div class="errorbox">' . $errortext . '</div>' );
+ $outputPage->addWikiText( Html::errorBox( $errortext ) );
}
# Connection or timeout error
return;
}
$this->mParserOutput = $poolArticleView->getParserOutput();
- $outputPage->addParserOutput( $this->mParserOutput );
+ $outputPage->addParserOutput( $this->mParserOutput, $poOptions );
if ( $content->getRedirectTarget() ) {
$outputPage->addSubtitle( "<span id=\"redirectsub\">" .
$this->getContext()->msg( 'redirectpagesub' )->parse() . "</span>" );
@@ -1187,7 +1197,8 @@ class Article implements Page {
$cache = MediaWikiServices::getInstance()->getMainObjectStash();
$key = $cache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
$loggedIn = $this->getContext()->getUser()->isLoggedIn();
- if ( $loggedIn || $cache->get( $key ) ) {
+ $sessionExists = $this->getContext()->getRequest()->getSession()->isPersistent();
+ if ( $loggedIn || $cache->get( $key ) || $sessionExists ) {
$logTypes = [ 'delete', 'move', 'protect' ];
$dbr = wfGetDB( DB_REPLICA );
@@ -1204,7 +1215,7 @@ class Article implements Page {
'lim' => 10,
'conds' => $conds,
'showIfEmpty' => false,
- 'msgKey' => [ $loggedIn
+ 'msgKey' => [ $loggedIn || $sessionExists
? 'moveddeleted-notice'
: 'moveddeleted-notice-recent'
]
@@ -1246,7 +1257,7 @@ class Article implements Page {
}
$dir = $this->getContext()->getLanguage()->getDir();
- $lang = $this->getContext()->getLanguage()->getCode();
+ $lang = $this->getContext()->getLanguage()->getHtmlCode();
$outputPage->addWikiText( Xml::openElement( 'div', [
'class' => "noarticletext mw-content-$dir",
'dir' => $dir,
@@ -1521,7 +1532,7 @@ class Article implements Page {
public function render() {
$this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' );
$this->getContext()->getOutput()->setArticleBodyOnly( true );
- $this->getContext()->getOutput()->enableSectionEditLinks( false );
+ $this->disableSectionEditForRender = true;
$this->view();
}
@@ -1680,6 +1691,7 @@ class Article implements Page {
$outputPage->setPageTitle( wfMessage( 'delete-confirm', $title->getPrefixedText() ) );
$outputPage->addBacklinkSubtitle( $title );
$outputPage->setRobotPolicy( 'noindex,nofollow' );
+ $outputPage->addModules( 'mediawiki.action.delete' );
$backlinkCache = $title->getBacklinkCache();
if ( $backlinkCache->hasLinks( 'pagelinks' ) || $backlinkCache->hasLinks( 'templatelinks' ) ) {
@@ -1724,12 +1736,17 @@ class Article implements Page {
]
);
+ // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
+ // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
+ // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+ $conf = $this->getContext()->getConfig();
+ $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
$fields[] = new OOUI\FieldLayout(
new OOUI\TextInputWidget( [
'name' => 'wpReason',
'inputId' => 'wpReason',
'tabIndex' => 2,
- 'maxLength' => 255,
+ 'maxLength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
'infusable' => true,
'value' => $reason,
'autofocus' => true,
@@ -2126,16 +2143,6 @@ class Article implements Page {
/**
* Call to WikiPage function for backwards compatibility.
- * @see WikiPage::getLastPurgeTimestamp
- * @deprecated since 1.29
- */
- public function getLastPurgeTimestamp() {
- wfDeprecated( __METHOD__, '1.29' );
- return $this->mPage->getLastPurgeTimestamp();
- }
-
- /**
- * Call to WikiPage function for backwards compatibility.
* @see WikiPage::doViewUpdates
*/
public function doViewUpdates( User $user, $oldid = 0 ) {
@@ -2557,7 +2564,7 @@ class Article implements Page {
* @see WikiPage::updateRedirectOn
*/
public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
- return $this->mPage->updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null );
+ return $this->mPage->updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect );
}
/**
@@ -2656,45 +2663,5 @@ class Article implements Page {
return $handler->getAutoDeleteReason( $title, $hasHistory );
}
- /**
- * @return array
- *
- * @deprecated since 1.24, use WikiPage::selectFields() instead
- */
- public static function selectFields() {
- wfDeprecated( __METHOD__, '1.24' );
- return WikiPage::selectFields();
- }
-
- /**
- * @param Title $title
- *
- * @deprecated since 1.24, use WikiPage::onArticleCreate() instead
- */
- public static function onArticleCreate( $title ) {
- wfDeprecated( __METHOD__, '1.24' );
- WikiPage::onArticleCreate( $title );
- }
-
- /**
- * @param Title $title
- *
- * @deprecated since 1.24, use WikiPage::onArticleDelete() instead
- */
- public static function onArticleDelete( $title ) {
- wfDeprecated( __METHOD__, '1.24' );
- WikiPage::onArticleDelete( $title );
- }
-
- /**
- * @param Title $title
- *
- * @deprecated since 1.24, use WikiPage::onArticleEdit() instead
- */
- public static function onArticleEdit( $title ) {
- wfDeprecated( __METHOD__, '1.24' );
- WikiPage::onArticleEdit( $title );
- }
-
// ******
}
diff --git a/www/wiki/includes/page/CategoryPage.php b/www/wiki/includes/page/CategoryPage.php
index 2d7e8f24..491726be 100644
--- a/www/wiki/includes/page/CategoryPage.php
+++ b/www/wiki/includes/page/CategoryPage.php
@@ -27,7 +27,7 @@
*/
class CategoryPage extends Article {
# Subclasses can change this to override the viewer class.
- protected $mCategoryViewerClass = 'CategoryViewer';
+ protected $mCategoryViewerClass = CategoryViewer::class;
/**
* @var WikiCategoryPage
diff --git a/www/wiki/includes/page/ImagePage.php b/www/wiki/includes/page/ImagePage.php
index 0e3eaa5b..b5ff8059 100644
--- a/www/wiki/includes/page/ImagePage.php
+++ b/www/wiki/includes/page/ImagePage.php
@@ -251,13 +251,14 @@ class ImagePage extends Article {
protected function makeMetadataTable( $metadata ) {
$r = "<div class=\"mw-imagepage-section-metadata\">";
$r .= $this->getContext()->msg( 'metadata-help' )->plain();
- $r .= "<table id=\"mw_metadata\" class=\"mw_metadata\">\n";
+ // Intial state is collapsed
+ // see filepage.css and mediawiki.action.view.metadata module.
+ $r .= "<table id=\"mw_metadata\" class=\"mw_metadata collapsed\">\n";
foreach ( $metadata as $type => $stuff ) {
foreach ( $stuff as $v ) {
$class = str_replace( ' ', '_', $v['id'] );
if ( $type == 'collapsed' ) {
- // Handled by mediawiki.action.view.metadata module.
- $class .= ' collapsable';
+ $class .= ' mw-metadata-collapsible';
}
$r .= Html::rawElement( 'tr',
[ 'class' => $class ],
@@ -285,6 +286,22 @@ class ImagePage extends Article {
return parent::getContentObject();
}
+ private function getLanguageForRendering( WebRequest $request, File $file ) {
+ $handler = $this->displayImg->getHandler();
+ if ( !$handler ) {
+ return null;
+ }
+
+ $requestLanguage = $request->getVal( 'lang' );
+ if ( !is_null( $requestLanguage ) ) {
+ if ( $handler->validateParam( 'lang', $requestLanguage ) ) {
+ return $requestLanguage;
+ }
+ }
+
+ return $handler->getDefaultRenderLanguage( $this->displayImg );
+ }
+
protected function openShowImage() {
global $wgEnableUploads, $wgSend404Code, $wgSVGMaxSize;
@@ -309,14 +326,9 @@ class ImagePage extends Article {
$params = [ 'page' => $page ];
}
- $renderLang = $request->getVal( 'lang' );
+ $renderLang = $this->getLanguageForRendering( $request, $this->displayImg );
if ( !is_null( $renderLang ) ) {
- $handler = $this->displayImg->getHandler();
- if ( $handler && $handler->validateParam( 'lang', $renderLang ) ) {
- $params['lang'] = $renderLang;
- } else {
- $renderLang = null;
- }
+ $params['lang'] = $renderLang;
}
$width_orig = $this->displayImg->getWidth( $page );
@@ -527,13 +539,13 @@ class ImagePage extends Article {
// The dirmark, however, must not be immediately adjacent
// to the filename, because it can get copied with it.
// See T27277.
- // @codingStandardsIgnoreStart Ignore long line
+ // phpcs:disable Generic.Files.LineLength
$out->addWikiText( <<<EOT
<div class="fullMedia"><span class="dangerousLink">{$medialink}</span> $dirmark<span class="fileInfo">$longDesc</span></div>
<div class="mediaWarning">$warning</div>
EOT
);
- // @codingStandardsIgnoreEnd
+ // phpcs:enable
} else {
$out->addWikiText( <<<EOT
<div class="fullMedia">{$medialink} {$dirmark}<span class="fileInfo">$longDesc</span>
@@ -544,12 +556,7 @@ EOT
$renderLangOptions = $this->displayImg->getAvailableLanguages();
if ( count( $renderLangOptions ) >= 1 ) {
- $currentLanguage = $renderLang;
- $defaultLang = $this->displayImg->getDefaultRenderLanguage();
- if ( is_null( $currentLanguage ) ) {
- $currentLanguage = $defaultLang;
- }
- $out->addHTML( $this->doRenderLangOpt( $renderLangOptions, $currentLanguage, $defaultLang ) );
+ $out->addHTML( $this->doRenderLangOpt( $renderLangOptions, $renderLang ) );
}
// Add cannot animate thumbnail warning
@@ -583,7 +590,7 @@ EOT
# Show deletion log to be consistent with normal articles
LogEventsList::showLogExtract(
$out,
- [ 'delete', 'move' ],
+ [ 'delete', 'move', 'protect' ],
$this->getTitle()->getPrefixedText(),
'',
[ 'lim' => 10,
@@ -625,7 +632,7 @@ EOT
* @param string $sizeLinkBigImagePreview HTML for the current size
* @return string HTML output
*/
- private function getThumbPrevText( $params, $sizeLinkBigImagePreview ) {
+ protected function getThumbPrevText( $params, $sizeLinkBigImagePreview ) {
if ( $sizeLinkBigImagePreview ) {
// Show a different message of preview is different format from original.
$previewTypeDiffers = false;
@@ -663,7 +670,7 @@ EOT
* @param int $height
* @return string
*/
- private function makeSizeLink( $params, $width, $height ) {
+ protected function makeSizeLink( $params, $width, $height ) {
$params['width'] = $width;
$params['height'] = $height;
$thumbnail = $this->displayImg->transform( $params );
@@ -963,7 +970,7 @@ EOT
$fromSrc = $this->getContext()->msg(
'shared-repo-from',
$file->getRepo()->getDisplayName()
- )->text();
+ )->escaped();
}
$out->addHTML( "<li>{$link} {$fromSrc}</li>\n" );
}
@@ -1047,60 +1054,31 @@ EOT
* Output a drop-down box for language options for the file
*
* @param array $langChoices Array of string language codes
- * @param string $curLang Language code file is being viewed in.
- * @param string $defaultLang Language code that image is rendered in by default
+ * @param string $renderLang Language code for the language we want the file to rendered in.
* @return string HTML to insert underneath image.
*/
- protected function doRenderLangOpt( array $langChoices, $curLang, $defaultLang ) {
+ protected function doRenderLangOpt( array $langChoices, $renderLang ) {
global $wgScript;
- sort( $langChoices );
- $curLang = wfBCP47( $curLang );
- $defaultLang = wfBCP47( $defaultLang );
$opts = '';
- $haveCurrentLang = false;
- $haveDefaultLang = false;
-
- // We make a list of all the language choices in the file.
- // Additionally if the default language to render this file
- // is not included as being in this file (for example, in svgs
- // usually the fallback content is the english content) also
- // include a choice for that. Last of all, if we're viewing
- // the file in a language not on the list, add it as a choice.
+
+ $matchedRenderLang = $this->displayImg->getMatchedLanguage( $renderLang );
+
foreach ( $langChoices as $lang ) {
- $code = wfBCP47( $lang );
- $name = Language::fetchLanguageName( $code, $this->getContext()->getLanguage()->getCode() );
- if ( $name !== '' ) {
- $display = $this->getContext()->msg( 'img-lang-opt', $code, $name )->text();
- } else {
- $display = $code;
- }
- $opts .= "\n" . Xml::option( $display, $code, $curLang === $code );
- if ( $curLang === $code ) {
- $haveCurrentLang = true;
- }
- if ( $defaultLang === $code ) {
- $haveDefaultLang = true;
- }
- }
- if ( !$haveDefaultLang ) {
- // Its hard to know if the content is really in the default language, or
- // if its just unmarked content that could be in any language.
- $opts = Xml::option(
- $this->getContext()->msg( 'img-lang-default' )->text(),
- $defaultLang,
- $defaultLang === $curLang
- ) . $opts;
- }
- if ( !$haveCurrentLang && $defaultLang !== $curLang ) {
- $name = Language::fetchLanguageName( $curLang, $this->getContext()->getLanguage()->getCode() );
- if ( $name !== '' ) {
- $display = $this->getContext()->msg( 'img-lang-opt', $curLang, $name )->text();
- } else {
- $display = $curLang;
- }
- $opts = Xml::option( $display, $curLang, true ) . $opts;
+ $opts .= $this->createXmlOptionStringForLanguage(
+ $lang,
+ $matchedRenderLang === $lang
+ );
}
+ // Allow for the default case in an svg <switch> that is displayed if no
+ // systemLanguage attribute matches
+ $opts .= "\n" .
+ Xml::option(
+ $this->getContext()->msg( 'img-lang-default' )->text(),
+ 'und',
+ is_null( $matchedRenderLang )
+ );
+
$select = Html::rawElement(
'select',
[ 'id' => 'mw-imglangselector', 'name' => 'lang' ],
@@ -1120,6 +1098,27 @@ EOT
}
/**
+ * @param $lang string
+ * @param $selected bool
+ * @return string
+ */
+ private function createXmlOptionStringForLanguage( $lang, $selected ) {
+ $code = LanguageCode::bcp47( $lang );
+ $name = Language::fetchLanguageName( $code, $this->getContext()->getLanguage()->getCode() );
+ if ( $name !== '' ) {
+ $display = $this->getContext()->msg( 'img-lang-opt', $code, $name )->text();
+ } else {
+ $display = $code;
+ }
+ return "\n" .
+ Xml::option(
+ $display,
+ $lang,
+ $selected
+ );
+ }
+
+ /**
* Get the width and height to display image at.
*
* @note This method assumes that it is only called if one
diff --git a/www/wiki/includes/page/PageArchive.php b/www/wiki/includes/page/PageArchive.php
index af936cc7..8b42020a 100644
--- a/www/wiki/includes/page/PageArchive.php
+++ b/www/wiki/includes/page/PageArchive.php
@@ -176,44 +176,32 @@ class PageArchive {
* @return ResultWrapper
*/
public function listRevisions() {
- $dbr = wfGetDB( DB_REPLICA );
- $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
-
- $tables = [ 'archive' ] + $commentQuery['tables'];
-
- $fields = [
- 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
- 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
- 'ar_page_id'
- ] + $commentQuery['fields'];
-
- if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
- $fields[] = 'ar_content_format';
- $fields[] = 'ar_content_model';
- }
-
- $conds = [ 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey() ];
+ $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+ $queryInfo = $revisionStore->getArchiveQueryInfo();
+ $conds = [
+ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ ];
$options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
- $join_conds = [] + $commentQuery['joins'];
-
ChangeTags::modifyDisplayQuery(
- $tables,
- $fields,
+ $queryInfo['tables'],
+ $queryInfo['fields'],
$conds,
- $join_conds,
+ $queryInfo['joins'],
$options,
''
);
- return $dbr->select( $tables,
- $fields,
+ $dbr = wfGetDB( DB_REPLICA );
+ return $dbr->select(
+ $queryInfo['tables'],
+ $queryInfo['fields'],
$conds,
__METHOD__,
$options,
- $join_conds
+ $queryInfo['joins']
);
}
@@ -231,12 +219,14 @@ class PageArchive {
}
$dbr = wfGetDB( DB_REPLICA );
+ $fileQuery = ArchivedFile::getQueryInfo();
return $dbr->select(
- 'filearchive',
- ArchivedFile::selectFields(),
+ $fileQuery['tables'],
+ $fileQuery['fields'],
[ 'fa_name' => $this->title->getDBkey() ],
__METHOD__,
- [ 'ORDER BY' => 'fa_timestamp DESC' ]
+ [ 'ORDER BY' => 'fa_timestamp DESC' ],
+ $fileQuery['joins']
);
}
@@ -249,34 +239,11 @@ class PageArchive {
*/
public function getRevision( $timestamp ) {
$dbr = wfGetDB( DB_REPLICA );
- $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
-
- $tables = [ 'archive' ] + $commentQuery['tables'];
-
- $fields = [
- 'ar_rev_id',
- 'ar_text',
- 'ar_user',
- 'ar_user_text',
- 'ar_timestamp',
- 'ar_minor_edit',
- 'ar_flags',
- 'ar_text_id',
- 'ar_deleted',
- 'ar_len',
- 'ar_sha1',
- ] + $commentQuery['fields'];
-
- if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
- $fields[] = 'ar_content_format';
- $fields[] = 'ar_content_model';
- }
-
- $join_conds = [] + $commentQuery['joins'];
+ $arQuery = Revision::getArchiveQueryInfo();
$row = $dbr->selectRow(
- $tables,
- $fields,
+ $arQuery['tables'],
+ $arQuery['fields'],
[
'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey(),
@@ -284,7 +251,7 @@ class PageArchive {
],
__METHOD__,
[],
- $join_conds
+ $arQuery['joins']
);
if ( $row ) {
@@ -348,19 +315,13 @@ class PageArchive {
}
/**
- * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
+ * Get the text from an archive row containing ar_text_id
*
+ * @deprecated since 1.31
* @param object $row Database row
* @return string
*/
public function getTextFromRow( $row ) {
- if ( is_null( $row->ar_text_id ) ) {
- // An old row from MediaWiki 1.4 or previous.
- // Text is embedded in this row in classic compression format.
- return Revision::getRevisionText( $row, 'ar_' );
- }
-
- // New-style: keyed to the text storage backend.
$dbr = wfGetDB( DB_REPLICA );
$text = $dbr->selectRow( 'text',
[ 'old_text', 'old_flags' ],
@@ -380,15 +341,18 @@ class PageArchive {
*/
public function getLastRevisionText() {
$dbr = wfGetDB( DB_REPLICA );
- $row = $dbr->selectRow( 'archive',
- [ 'ar_text', 'ar_flags', 'ar_text_id' ],
+ $row = $dbr->selectRow(
+ [ 'archive', 'text' ],
+ [ 'old_text', 'old_flags' ],
[ 'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey() ],
__METHOD__,
- [ 'ORDER BY' => 'ar_timestamp DESC' ] );
+ [ 'ORDER BY' => 'ar_timestamp DESC, ar_id DESC' ],
+ [ 'text' => [ 'JOIN', 'old_id = ar_text_id' ] ]
+ );
if ( $row ) {
- return $this->getTextFromRow( $row );
+ return Revision::getRevisionText( $row );
}
return null;
@@ -563,47 +527,23 @@ class PageArchive {
$oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
}
- $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
-
- $tables = [ 'archive', 'revision' ] + $commentQuery['tables'];
-
- $fields = [
- 'ar_id',
- 'ar_rev_id',
- 'rev_id',
- 'ar_text',
- 'ar_user',
- 'ar_user_text',
- 'ar_timestamp',
- 'ar_minor_edit',
- 'ar_flags',
- 'ar_text_id',
- 'ar_deleted',
- 'ar_page_id',
- 'ar_len',
- 'ar_sha1'
- ] + $commentQuery['fields'];
-
- if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
- $fields[] = 'ar_content_format';
- $fields[] = 'ar_content_model';
- }
-
- $join_conds = [
- 'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ],
- ] + $commentQuery['joins'];
+ $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+ $queryInfo = $revisionStore->getArchiveQueryInfo();
+ $queryInfo['tables'][] = 'revision';
+ $queryInfo['fields'][] = 'rev_id';
+ $queryInfo['joins']['revision'] = [ 'LEFT JOIN', 'ar_rev_id=rev_id' ];
/**
* Select each archived revision...
*/
$result = $dbw->select(
- $tables,
- $fields,
+ $queryInfo['tables'],
+ $queryInfo['fields'],
$oldWhere,
__METHOD__,
/* options */
[ 'ORDER BY' => 'ar_timestamp' ],
- $join_conds
+ $queryInfo['joins']
);
$rev_count = $result->numRows();
@@ -785,7 +725,9 @@ class PageArchive {
Hooks::run( 'ArticleUndelete',
[ &$this->title, $created, $comment, $oldPageId, $restoredPages ] );
if ( $this->title->getNamespace() == NS_FILE ) {
- DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) );
+ DeferredUpdates::addUpdate(
+ new HTMLCacheUpdate( $this->title, 'imagelinks', 'file-restore' )
+ );
}
}
diff --git a/www/wiki/includes/page/WikiFilePage.php b/www/wiki/includes/page/WikiFilePage.php
index 972a397c..4c2ebdc2 100644
--- a/www/wiki/includes/page/WikiFilePage.php
+++ b/www/wiki/includes/page/WikiFilePage.php
@@ -173,7 +173,9 @@ class WikiFilePage extends WikiPage {
if ( $this->mFile->exists() ) {
wfDebug( 'ImagePage::doPurge purging ' . $this->mFile->getName() . "\n" );
- DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ) );
+ DeferredUpdates::addUpdate(
+ new HTMLCacheUpdate( $this->mTitle, 'imagelinks', 'file-purge' )
+ );
} else {
wfDebug( 'ImagePage::doPurge no image for '
. $this->mFile->getName() . "; limiting purge to cache only\n" );
diff --git a/www/wiki/includes/page/WikiPage.php b/www/wiki/includes/page/WikiPage.php
index 5eb41b5e..820df58d 100644
--- a/www/wiki/includes/page/WikiPage.php
+++ b/www/wiki/includes/page/WikiPage.php
@@ -21,11 +21,11 @@
*/
use MediaWiki\Edit\PreparedEdit;
-use \MediaWiki\Logger\LoggerFactory;
-use \MediaWiki\MediaWikiServices;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
+use Wikimedia\Assert\Assert;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\DBError;
use Wikimedia\Rdbms\DBUnexpectedError;
/**
@@ -88,12 +88,6 @@ class WikiPage implements Page, IDBAccessObject {
*/
protected $mLinksUpdated = '19700101000000';
- /** @deprecated since 1.29. Added in 1.28 for partial purging, no longer used. */
- const PURGE_CDN_CACHE = 1;
- const PURGE_CLUSTER_PCACHE = 2;
- const PURGE_GLOBAL_PCACHE = 4;
- const PURGE_ALL = 7;
-
/**
* Constructor and clear the article
* @param Title $title Reference to a Title object.
@@ -164,8 +158,11 @@ class WikiPage implements Page, IDBAccessObject {
$from = self::convertSelectType( $from );
$db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
+ $pageQuery = self::getQueryInfo();
$row = $db->selectRow(
- 'page', self::selectFields(), [ 'page_id' => $id ], __METHOD__ );
+ $pageQuery['tables'], $pageQuery['fields'], [ 'page_id' => $id ], __METHOD__,
+ [], $pageQuery['joins']
+ );
if ( !$row ) {
return null;
}
@@ -197,15 +194,15 @@ class WikiPage implements Page, IDBAccessObject {
*/
private static function convertSelectType( $type ) {
switch ( $type ) {
- case 'fromdb':
- return self::READ_NORMAL;
- case 'fromdbmaster':
- return self::READ_LATEST;
- case 'forupdate':
- return self::READ_LOCKING;
- default:
- // It may already be an integer or whatever else
- return $type;
+ case 'fromdb':
+ return self::READ_NORMAL;
+ case 'fromdbmaster':
+ return self::READ_LATEST;
+ case 'forupdate':
+ return self::READ_LOCKING;
+ default:
+ // It may already be an integer or whatever else
+ return $type;
}
}
@@ -283,11 +280,14 @@ class WikiPage implements Page, IDBAccessObject {
* Return the list of revision fields that should be selected to create
* a new page.
*
+ * @deprecated since 1.31, use self::getQueryInfo() instead.
* @return array
*/
public static function selectFields() {
global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
+ wfDeprecated( __METHOD__, '1.31' );
+
$fields = [
'page_id',
'page_namespace',
@@ -314,6 +314,47 @@ class WikiPage implements Page, IDBAccessObject {
}
/**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new page object.
+ * @since 1.31
+ * @return array With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ */
+ public static function getQueryInfo() {
+ global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
+
+ $ret = [
+ 'tables' => [ 'page' ],
+ 'fields' => [
+ 'page_id',
+ 'page_namespace',
+ 'page_title',
+ 'page_restrictions',
+ 'page_is_redirect',
+ 'page_is_new',
+ 'page_random',
+ 'page_touched',
+ 'page_links_updated',
+ 'page_latest',
+ 'page_len',
+ ],
+ 'joins' => [],
+ ];
+
+ if ( $wgContentHandlerUseDB ) {
+ $ret['fields'][] = 'page_content_model';
+ }
+
+ if ( $wgPageLanguageUseDB ) {
+ $ret['fields'][] = 'page_lang';
+ }
+
+ return $ret;
+ }
+
+ /**
* Fetch a page record with the given conditions
* @param IDatabase $dbr
* @param array $conditions
@@ -321,14 +362,23 @@ class WikiPage implements Page, IDBAccessObject {
* @return object|bool Database result resource, or false on failure
*/
protected function pageData( $dbr, $conditions, $options = [] ) {
- $fields = self::selectFields();
+ $pageQuery = self::getQueryInfo();
// Avoid PHP 7.1 warning of passing $this by reference
$wikiPage = $this;
- Hooks::run( 'ArticlePageDataBefore', [ &$wikiPage, &$fields ] );
+ Hooks::run( 'ArticlePageDataBefore', [
+ &$wikiPage, &$pageQuery['fields'], &$pageQuery['tables'], &$pageQuery['joins']
+ ] );
- $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
+ $row = $dbr->selectRow(
+ $pageQuery['tables'],
+ $pageQuery['fields'],
+ $conditions,
+ __METHOD__,
+ $options,
+ $pageQuery['joins']
+ );
Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
@@ -383,8 +433,9 @@ class WikiPage implements Page, IDBAccessObject {
if ( is_int( $from ) ) {
list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
- $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
$loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ $db = $loadBalancer->getConnection( $index );
+ $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
if ( !$data
&& $index == DB_REPLICA
@@ -393,7 +444,8 @@ class WikiPage implements Page, IDBAccessObject {
) {
$from = self::READ_LATEST;
list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
- $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
+ $db = $loadBalancer->getConnection( $index );
+ $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
}
} else {
// No idea from where the caller got this data, assume replica DB.
@@ -512,7 +564,7 @@ class WikiPage implements Page, IDBAccessObject {
$cache = ObjectCache::getMainWANInstance();
return $cache->getWithSetCallback(
- $cache->makeKey( 'page', 'content-model', $this->getLatest() ),
+ $cache->makeKey( 'page-content-model', $this->getLatest() ),
$cache::TTL_MONTH,
function () {
$rev = $this->getRevision();
@@ -621,7 +673,7 @@ class WikiPage implements Page, IDBAccessObject {
$revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
} else {
$dbr = wfGetDB( DB_REPLICA );
- $revision = Revision::newKnownCurrent( $dbr, $this->getId(), $latest );
+ $revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest );
}
if ( $revision ) { // sanity
@@ -983,18 +1035,16 @@ class WikiPage implements Page, IDBAccessObject {
$dbr = wfGetDB( DB_REPLICA );
- if ( $dbr->implicitGroupby() ) {
- $realNameField = 'user_real_name';
- } else {
- $realNameField = 'MIN(user_real_name) AS user_real_name';
- }
+ $actorMigration = ActorMigration::newMigration();
+ $actorQuery = $actorMigration->getJoin( 'rev_user' );
- $tables = [ 'revision', 'user' ];
+ $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
$fields = [
- 'user_id' => 'rev_user',
- 'user_name' => 'rev_user_text',
- $realNameField,
+ 'user_id' => $actorQuery['fields']['rev_user'],
+ 'user_name' => $actorQuery['fields']['rev_user_text'],
+ 'actor_id' => $actorQuery['fields']['rev_actor'],
+ 'user_real_name' => 'MIN(user_real_name)',
'timestamp' => 'MAX(rev_timestamp)',
];
@@ -1002,22 +1052,20 @@ class WikiPage implements Page, IDBAccessObject {
// The user who made the top revision gets credited as "this page was last edited by
// John, based on contributions by Tom, Dick and Harry", so don't include them twice.
- $user = $this->getUser();
- if ( $user ) {
- $conds[] = "rev_user != $user";
- } else {
- $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
- }
+ $user = $this->getUser()
+ ? User::newFromId( $this->getUser() )
+ : User::newFromName( $this->getUserText(), false );
+ $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
// Username hidden?
$conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
$jconds = [
- 'user' => [ 'LEFT JOIN', 'rev_user = user_id' ],
- ];
+ 'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
+ ] + $actorQuery['joins'];
$options = [
- 'GROUP BY' => [ 'rev_user', 'rev_user_text' ],
+ 'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
'ORDER BY' => 'timestamp DESC',
];
@@ -1098,14 +1146,16 @@ class WikiPage implements Page, IDBAccessObject {
return;
}
- Hooks::run( 'PageViewUpdates', [ $this, $user ] );
- // Update newtalk / watchlist notification status
- try {
- $user->clearNotification( $this->mTitle, $oldid );
- } catch ( DBError $e ) {
- // Avoid outage if the master is not reachable
- MWExceptionHandler::logException( $e );
- }
+ // Update newtalk / watchlist notification status;
+ // Avoid outage if the master is not reachable by using a deferred updated
+ DeferredUpdates::addCallableUpdate(
+ function () use ( $user, $oldid ) {
+ Hooks::run( 'PageViewUpdates', [ $this, $user ] );
+
+ $user->clearNotification( $this->mTitle, $oldid );
+ },
+ DeferredUpdates::PRESEND
+ );
}
/**
@@ -1141,18 +1191,6 @@ class WikiPage implements Page, IDBAccessObject {
}
/**
- * Get the last time a user explicitly purged the page via action=purge
- *
- * @return string|bool TS_MW timestamp or false
- * @since 1.28
- * @deprecated since 1.29. It will always return false.
- */
- public function getLastPurgeTimestamp() {
- wfDeprecated( __METHOD__, '1.29' );
- return false;
- }
-
- /**
* Insert a new empty page record for this article.
* This *must* be followed up by creating a revision
* and running $this->updateRevisionOn( ... );
@@ -1232,8 +1270,11 @@ class WikiPage implements Page, IDBAccessObject {
$conditions['page_latest'] = $lastRevision;
}
+ $revId = $revision->getId();
+ Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
+
$row = [ /* SET */
- 'page_latest' => $revision->getId(),
+ 'page_latest' => $revId,
'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
'page_is_redirect' => $rt !== null ? 1 : 0,
@@ -1273,7 +1314,7 @@ class WikiPage implements Page, IDBAccessObject {
* Add row to the redirect table if this is a redirect, remove otherwise.
*
* @param IDatabase $dbw
- * @param Title $redirectTitle Title object pointing to the redirect target,
+ * @param Title|null $redirectTitle Title object pointing to the redirect target,
* or NULL if this is not a redirect
* @param null|bool $lastRevIsRedirect If given, will optimize adding and
* removing rows in redirect table.
@@ -1389,16 +1430,17 @@ class WikiPage implements Page, IDBAccessObject {
) {
$baseRevId = null;
if ( $edittime && $sectionId !== 'new' ) {
- $dbr = wfGetDB( DB_REPLICA );
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ $dbr = $lb->getConnection( DB_REPLICA );
$rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
// Try the master if this thread may have just added it.
// This could be abstracted into a Revision method, but we don't want
// to encourage loading of revisions by timestamp.
if ( !$rev
- && wfGetLB()->getServerCount() > 1
- && wfGetLB()->hasOrMadeRecentMasterChanges()
+ && $lb->getServerCount() > 1
+ && $lb->hasOrMadeRecentMasterChanges()
) {
- $dbw = wfGetDB( DB_MASTER );
+ $dbw = $lb->getConnection( DB_MASTER );
$rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
}
if ( $rev ) {
@@ -1585,13 +1627,20 @@ class WikiPage implements Page, IDBAccessObject {
$old_revision = $this->getRevision(); // current revision
$old_content = $this->getContent( Revision::RAW ); // current revision's content
- if ( $old_content && $old_content->getModel() !== $content->getModel() ) {
- $tags[] = 'mw-contentmodelchange';
+ $handler = $content->getContentHandler();
+ $tag = $handler->getChangeTag( $old_content, $content, $flags );
+ // If there is no applicable tag, null is returned, so we need to check
+ if ( $tag ) {
+ $tags[] = $tag;
}
- // Provide autosummaries if one is not provided and autosummaries are enabled
+ // Check for undo tag
+ if ( $undidRevId !== 0 && in_array( 'mw-undo', ChangeTags::getSoftwareTags() ) ) {
+ $tags[] = 'mw-undo';
+ }
+
+ // Provide autosummaries if summary is not provided and autosummaries are enabled
if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
- $handler = $content->getContentHandler();
$summary = $handler->getAutosummary( $old_content, $content, $flags );
}
@@ -1673,27 +1722,27 @@ class WikiPage implements Page, IDBAccessObject {
throw new MWException( "Could not find text for current revision {$oldid}." );
}
- // @TODO: pass content object?!
- $revision = new Revision( [
- 'page' => $this->getId(),
- 'title' => $this->mTitle, // for determining the default content model
- 'comment' => $summary,
- 'minor_edit' => $meta['minor'],
- 'text' => $meta['serialized'],
- 'len' => $newsize,
- 'parent_id' => $oldid,
- 'user' => $user->getId(),
- 'user_text' => $user->getName(),
- 'timestamp' => $now,
- 'content_model' => $content->getModel(),
- 'content_format' => $meta['serialFormat'],
- ] );
-
$changed = !$content->equals( $oldContent );
$dbw = wfGetDB( DB_MASTER );
if ( $changed ) {
+ // @TODO: pass content object?!
+ $revision = new Revision( [
+ 'page' => $this->getId(),
+ 'title' => $this->mTitle, // for determining the default content model
+ 'comment' => $summary,
+ 'minor_edit' => $meta['minor'],
+ 'text' => $meta['serialized'],
+ 'len' => $newsize,
+ 'parent_id' => $oldid,
+ 'user' => $user->getId(),
+ 'user_text' => $user->getName(),
+ 'timestamp' => $now,
+ 'content_model' => $content->getModel(),
+ 'content_format' => $meta['serialFormat'],
+ ] );
+
$prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
$status->merge( $prepStatus );
if ( !$status->isOK() ) {
@@ -1725,13 +1774,14 @@ class WikiPage implements Page, IDBAccessObject {
throw new MWException( "Failed to update page row to use new revision." );
}
+ $tags = $meta['tags'];
Hooks::run( 'NewRevisionFromEditComplete',
- [ $this, $revision, $meta['baseRevId'], $user ] );
+ [ $this, $revision, $meta['baseRevId'], $user, &$tags ] );
// Update recentchanges
if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
// Mark as patrolled if the user can do so
- $patrolled = $wgUseRCPatrol && !count(
+ $autopatrolled = $wgUseRCPatrol && !count(
$this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
// Add RC row to the DB
RecentChange::notifyEdit(
@@ -1747,8 +1797,9 @@ class WikiPage implements Page, IDBAccessObject {
$oldContent ? $oldContent->getSize() : 0,
$newsize,
$revisionId,
- $patrolled,
- $meta['tags']
+ $autopatrolled ? RecentChange::PRC_AUTOPATROLLED :
+ RecentChange::PRC_UNPATROLLED,
+ $tags
);
}
@@ -1759,11 +1810,9 @@ class WikiPage implements Page, IDBAccessObject {
} else {
// T34948: revision ID must be set to page {{REVISIONID}} and
// related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
- $revision->setId( $this->getLatest() );
- $revision->setUserIdAndName(
- $this->getUser( Revision::RAW ),
- $this->getUserText( Revision::RAW )
- );
+ // Since we don't insert a new revision into the database, the least
+ // error-prone way is to reuse given old revision.
+ $revision = $meta['oldRevision'];
}
if ( $changed ) {
@@ -2084,15 +2133,6 @@ class WikiPage implements Page, IDBAccessObject {
$edit->newContent = $content;
$edit->oldContent = $this->getContent( Revision::RAW );
- // NOTE: B/C for hooks! don't use these fields!
- $edit->newText = $edit->newContent
- ? ContentHandler::getContentText( $edit->newContent )
- : '';
- $edit->oldText = $edit->oldContent
- ? ContentHandler::getContentText( $edit->oldContent )
- : '';
- $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
-
if ( $edit->output ) {
$edit->output->setCacheTime( wfTimestampNow() );
}
@@ -2177,6 +2217,7 @@ class WikiPage implements Page, IDBAccessObject {
$this->getTitle(), null, $recursive, $editInfo->output
);
foreach ( $updates as $update ) {
+ $update->setCause( 'edit-page', $user->getName() );
if ( $update instanceof LinksUpdate ) {
$update->setRevision( $revision );
$update->setTriggeringUser( $user );
@@ -2233,9 +2274,11 @@ class WikiPage implements Page, IDBAccessObject {
$good = 0;
}
$edits = $options['changed'] ? 1 : 0;
- $total = $options['created'] ? 1 : 0;
+ $pages = $options['created'] ? 1 : 0;
- DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) );
+ DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
+ [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
+ ) );
DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
// If this is another user's talk page, update newtalk.
@@ -2481,7 +2524,7 @@ class WikiPage implements Page, IDBAccessObject {
$cascade = false;
if ( $limit['create'] != '' ) {
- $commentFields = CommentStore::newKey( 'pt_reason' )->insert( $dbw, $reason );
+ $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
$dbw->replace( 'protected_titles',
[ [ 'pt_namespace', 'pt_title' ] ],
[
@@ -2733,7 +2776,7 @@ class WikiPage implements Page, IDBAccessObject {
* @param int $u1 Unused
* @param bool $u2 Unused
* @param array|string &$error Array of errors to append to
- * @param User $user The deleting user
+ * @param User $deleter The deleting user
* @param array $tags Tags to apply to the deletion action
* @param string $logsubtype
* @return Status Status object; if successful, $status->value is the log_id of the
@@ -2741,10 +2784,11 @@ class WikiPage implements Page, IDBAccessObject {
* found, $status is a non-fatal 'cannotdelete' error
*/
public function doDeleteArticleReal(
- $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
+ $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
$tags = [], $logsubtype = 'delete'
) {
- global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage;
+ global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage,
+ $wgActorTableSchemaMigrationStage;
wfDebug( __METHOD__ . "\n" );
@@ -2759,9 +2803,9 @@ class WikiPage implements Page, IDBAccessObject {
// Avoid PHP 7.1 warning of passing $this by reference
$wikiPage = $this;
- $user = is_null( $user ) ? $wgUser : $user;
+ $deleter = is_null( $deleter ) ? $wgUser : $deleter;
if ( !Hooks::run( 'ArticleDelete',
- [ &$wikiPage, &$user, &$reason, &$error, &$status, $suppress ]
+ [ &$wikiPage, &$deleter, &$reason, &$error, &$status, $suppress ]
) ) {
if ( $status->isOK() ) {
// Hook aborted but didn't set a fatal status
@@ -2808,16 +2852,16 @@ class WikiPage implements Page, IDBAccessObject {
$content = null;
}
- $revCommentStore = new CommentStore( 'rev_comment' );
- $arCommentStore = new CommentStore( 'ar_comment' );
+ $commentStore = CommentStore::getStore();
+ $actorMigration = ActorMigration::newMigration();
- $fields = Revision::selectFields();
+ $revQuery = Revision::getQueryInfo();
$bitfield = false;
// Bitfields to further suppress the content
if ( $suppress ) {
$bitfield = Revision::SUPPRESSED_ALL;
- $fields = array_diff( $fields, [ 'rev_deleted' ] );
+ $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
}
// For now, shunt the revision data into the archive table.
@@ -2827,15 +2871,33 @@ class WikiPage implements Page, IDBAccessObject {
// In the future, we may keep revisions and mark them with
// the rev_deleted field, which is reserved for this purpose.
- // Get all of the page revisions
- $commentQuery = $revCommentStore->getJoin();
+ // Lock rows in `revision` and its temp tables, but not any others.
+ // Note array_intersect() preserves keys from the first arg, and we're
+ // assuming $revQuery has `revision` primary and isn't using subtables
+ // for anything we care about.
$res = $dbw->select(
- [ 'revision' ] + $commentQuery['tables'],
- $fields + $commentQuery['fields'],
+ array_intersect(
+ $revQuery['tables'],
+ [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
+ ),
+ '1',
[ 'rev_page' => $id ],
__METHOD__,
'FOR UPDATE',
- $commentQuery['joins']
+ $revQuery['joins']
+ );
+ foreach ( $res as $row ) {
+ // Fetch all rows in case the DB needs that to properly lock them.
+ }
+
+ // Get all of the page revisions
+ $res = $dbw->select(
+ $revQuery['tables'],
+ $revQuery['fields'],
+ [ 'rev_page' => $id ],
+ __METHOD__,
+ [],
+ $revQuery['joins']
);
// Build their equivalent archive rows
@@ -2846,24 +2908,22 @@ class WikiPage implements Page, IDBAccessObject {
$ipRevIds = [];
foreach ( $res as $row ) {
- $comment = $revCommentStore->getComment( $row );
+ $comment = $commentStore->getComment( 'rev_comment', $row );
+ $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
$rowInsert = [
'ar_namespace' => $namespace,
'ar_title' => $dbKey,
- 'ar_user' => $row->rev_user,
- 'ar_user_text' => $row->rev_user_text,
'ar_timestamp' => $row->rev_timestamp,
'ar_minor_edit' => $row->rev_minor_edit,
'ar_rev_id' => $row->rev_id,
'ar_parent_id' => $row->rev_parent_id,
'ar_text_id' => $row->rev_text_id,
- 'ar_text' => '',
- 'ar_flags' => '',
'ar_len' => $row->rev_len,
'ar_page_id' => $id,
'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
'ar_sha1' => $row->rev_sha1,
- ] + $arCommentStore->insert( $dbw, $comment );
+ ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
+ + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
if ( $wgContentHandlerUseDB ) {
$rowInsert['ar_content_model'] = $row->rev_content_model;
$rowInsert['ar_content_format'] = $row->rev_content_format;
@@ -2893,6 +2953,9 @@ class WikiPage implements Page, IDBAccessObject {
if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
$dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
}
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+ $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
+ }
// Also delete records from ip_changes as applicable.
if ( count( $ipRevIds ) > 0 ) {
@@ -2903,7 +2966,7 @@ class WikiPage implements Page, IDBAccessObject {
$logtype = $suppress ? 'suppress' : 'delete';
$logEntry = new ManualLogEntry( $logtype, $logsubtype );
- $logEntry->setPerformer( $user );
+ $logEntry->setPerformer( $deleter );
$logEntry->setTarget( $logTitle );
$logEntry->setComment( $reason );
$logEntry->setTags( $tags );
@@ -2919,11 +2982,11 @@ class WikiPage implements Page, IDBAccessObject {
$dbw->endAtomic( __METHOD__ );
- $this->doDeleteUpdates( $id, $content, $revision );
+ $this->doDeleteUpdates( $id, $content, $revision, $deleter );
Hooks::run( 'ArticleDeleteComplete', [
&$wikiPageBeforeDelete,
- &$user,
+ &$deleter,
$reason,
$id,
$content,
@@ -2970,8 +3033,11 @@ class WikiPage implements Page, IDBAccessObject {
* the required updates. This may be needed because $this->getContent()
* may already return null when the page proper was deleted.
* @param Revision|null $revision The latest page revision
+ * @param User|null $user The user that caused the deletion
*/
- public function doDeleteUpdates( $id, Content $content = null, Revision $revision = null ) {
+ public function doDeleteUpdates(
+ $id, Content $content = null, Revision $revision = null, User $user = null
+ ) {
try {
$countable = $this->isCountable();
} catch ( Exception $ex ) {
@@ -2981,7 +3047,9 @@ class WikiPage implements Page, IDBAccessObject {
}
// Update site status
- DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$countable, -1 ) );
+ DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
+ [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
+ ) );
// Delete pagelinks, update secondary indexes, etc
$updates = $this->getDeletionUpdates( $content );
@@ -2989,12 +3057,14 @@ class WikiPage implements Page, IDBAccessObject {
DeferredUpdates::addUpdate( $update );
}
+ $causeAgent = $user ? $user->getName() : 'unknown';
// Reparse any pages transcluding this page
- LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
-
+ LinksUpdate::queueRecursiveJobsForTable(
+ $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
// Reparse any pages including this image
if ( $this->mTitle->getNamespace() == NS_FILE ) {
- LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
+ LinksUpdate::queueRecursiveJobsForTable(
+ $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
}
// Clear caches
@@ -3117,16 +3187,31 @@ class WikiPage implements Page, IDBAccessObject {
// Get the last edit not by this person...
// Note: these may not be public values
- $user = intval( $current->getUser( Revision::RAW ) );
- $user_text = $dbw->addQuotes( $current->getUserText( Revision::RAW ) );
- $s = $dbw->selectRow( 'revision',
+ $userId = intval( $current->getUser( Revision::RAW ) );
+ $userName = $current->getUserText( Revision::RAW );
+ if ( $userId ) {
+ $user = User::newFromId( $userId );
+ $user->setName( $userName );
+ } else {
+ $user = User::newFromName( $current->getUserText( Revision::RAW ), false );
+ }
+
+ $actorWhere = ActorMigration::newMigration()->getWhere( $dbw, 'rev_user', $user );
+
+ $s = $dbw->selectRow(
+ [ 'revision' ] + $actorWhere['tables'],
[ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
- [ 'rev_page' => $current->getPage(),
- "rev_user != {$user} OR rev_user_text != {$user_text}"
- ], __METHOD__,
- [ 'USE INDEX' => 'page_timestamp',
- 'ORDER BY' => 'rev_timestamp DESC' ]
- );
+ [
+ 'rev_page' => $current->getPage(),
+ 'NOT(' . $actorWhere['conds'] . ')',
+ ],
+ __METHOD__,
+ [
+ 'USE INDEX' => [ 'revision' => 'page_timestamp' ],
+ 'ORDER BY' => 'rev_timestamp DESC'
+ ],
+ $actorWhere['joins']
+ );
if ( $s === false ) {
// No one else ever edited this page
return [ [ 'cantrollback' ] ];
@@ -3176,6 +3261,10 @@ class WikiPage implements Page, IDBAccessObject {
$targetContent = $target->getContent();
$changingContentModel = $targetContent->getModel() !== $current->getContentModel();
+ if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
+ $tags[] = 'mw-rollback';
+ }
+
// Actually store the edit
$status = $this->doEditContent(
$targetContent,
@@ -3197,15 +3286,16 @@ class WikiPage implements Page, IDBAccessObject {
if ( $wgUseRCPatrol ) {
// Mark all reverted edits as patrolled
- $set['rc_patrolled'] = 1;
+ $set['rc_patrolled'] = RecentChange::PRC_PATROLLED;
}
if ( count( $set ) ) {
+ $actorWhere = ActorMigration::newMigration()->getWhere( $dbw, 'rc_user', $user, false );
$dbw->update( 'recentchanges', $set,
[ /* WHERE */
'rc_cur_id' => $current->getPage(),
- 'rc_user_text' => $current->getUserText(),
'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
+ $actorWhere['conds'], // No tables/joins are needed for rc_user
],
__METHOD__
);
@@ -3252,7 +3342,8 @@ class WikiPage implements Page, IDBAccessObject {
'summary' => $summary,
'current' => $current,
'target' => $target,
- 'newid' => $revId
+ 'newid' => $revId,
+ 'tags' => $tags
];
return [];
@@ -3282,7 +3373,9 @@ class WikiPage implements Page, IDBAccessObject {
MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
// Invalidate caches of articles which include this page
- DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) );
+ DeferredUpdates::addUpdate(
+ new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' )
+ );
if ( $title->getNamespace() == NS_CATEGORY ) {
// Load the Category object, which will schedule a job to create
@@ -3300,6 +3393,8 @@ class WikiPage implements Page, IDBAccessObject {
*/
public static function onArticleDelete( Title $title ) {
// Update existence markers on article/talk tabs...
+ // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
+ BacklinkCache::get( $title )->clear();
$other = $title->getOtherPage();
$other->purgeSquid();
@@ -3320,7 +3415,9 @@ class WikiPage implements Page, IDBAccessObject {
// Images
if ( $title->getNamespace() == NS_FILE ) {
- DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
+ DeferredUpdates::addUpdate(
+ new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' )
+ );
}
// User talk pages
@@ -3343,10 +3440,14 @@ class WikiPage implements Page, IDBAccessObject {
*/
public static function onArticleEdit( Title $title, Revision $revision = null ) {
// Invalidate caches of articles which include this page
- DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) );
+ DeferredUpdates::addUpdate(
+ new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
+ );
// Invalidate the caches of all pages which redirect here
- DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) );
+ DeferredUpdates::addUpdate(
+ new HTMLCacheUpdate( $title, 'redirect', 'page-edit' )
+ );
MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
diff --git a/www/wiki/includes/pager/IndexPager.php b/www/wiki/includes/pager/IndexPager.php
index 6620c475..6880d588 100644
--- a/www/wiki/includes/pager/IndexPager.php
+++ b/www/wiki/includes/pager/IndexPager.php
@@ -21,7 +21,7 @@
* @ingroup Pager
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -124,7 +124,7 @@ abstract class IndexPager extends ContextSource implements Pager {
/**
* Result object for the query. Warning: seek before use.
*
- * @var ResultWrapper
+ * @var IResultWrapper
*/
public $mResult;
@@ -162,8 +162,8 @@ abstract class IndexPager extends ContextSource implements Pager {
: [];
} elseif ( is_array( $index ) ) {
# First element is the default
- reset( $index );
- list( $this->mOrderType, $this->mIndexField ) = each( $index );
+ $this->mIndexField = reset( $index );
+ $this->mOrderType = key( $index );
$this->mExtraSortFields = isset( $extraSort[$this->mOrderType] )
? (array)$extraSort[$this->mOrderType]
: [];
@@ -232,7 +232,7 @@ abstract class IndexPager extends ContextSource implements Pager {
}
/**
- * @return ResultWrapper The result wrapper.
+ * @return IResultWrapper The result wrapper.
*/
function getResult() {
return $this->mResult;
@@ -292,9 +292,9 @@ abstract class IndexPager extends ContextSource implements Pager {
* @param bool $isFirst False if there are rows before those fetched (i.e.
* if a "previous" link would make sense)
* @param int $limit Exact query limit
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
- function extractResultInfo( $isFirst, $limit, ResultWrapper $res ) {
+ function extractResultInfo( $isFirst, $limit, IResultWrapper $res ) {
$numRows = $res->numRows();
if ( $numRows ) {
# Remove any table prefix from index field
@@ -359,7 +359,7 @@ abstract class IndexPager extends ContextSource implements Pager {
* @param string $offset Index offset, inclusive
* @param int $limit Exact query limit
* @param bool $descending Query direction, false for ascending, true for descending
- * @return ResultWrapper
+ * @return IResultWrapper
*/
public function reallyDoQuery( $offset, $limit, $descending ) {
list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
@@ -406,7 +406,7 @@ abstract class IndexPager extends ContextSource implements Pager {
/**
* Pre-process results; useful for performing batch existence checks, etc.
*
- * @param ResultWrapper $result
+ * @param IResultWrapper $result
*/
protected function preprocessResults( $result ) {
}
diff --git a/www/wiki/includes/parser/BlockLevelPass.php b/www/wiki/includes/parser/BlockLevelPass.php
index fab9ab7f..1173dd20 100644
--- a/www/wiki/includes/parser/BlockLevelPass.php
+++ b/www/wiki/includes/parser/BlockLevelPass.php
@@ -26,7 +26,7 @@ class BlockLevelPass {
private $DTopen = false;
private $inPre = false;
private $lastSection = '';
- private $linestart;
+ private $lineStart;
private $text;
# State constants for the definition list colon extraction
@@ -236,7 +236,8 @@ class BlockLevelPass {
$term = $t2 = '';
if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
$t = $t2;
- $output .= $term . $this->nextItem( ':' );
+ // Trim whitespace in list items
+ $output .= trim( $term ) . $this->nextItem( ':' );
}
}
} elseif ( $prefixLength || $lastPrefixLength ) {
@@ -274,7 +275,8 @@ class BlockLevelPass {
# @todo FIXME: This is dupe of code above
if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
$t = $t2;
- $output .= $term . $this->nextItem( ':' );
+ // Trim whitespace in list items
+ $output .= trim( $term ) . $this->nextItem( ':' );
}
}
++$commonPrefixLength;
@@ -304,8 +306,12 @@ class BlockLevelPass {
if ( $openMatch || $closeMatch ) {
$pendingPTag = false;
- # @todo T7718: paragraph closed
- $output .= $this->closeParagraph();
+ // Only close the paragraph if we're not inside a <pre> tag, or if
+ // that <pre> tag has just been opened
+ if ( !$this->inPre || $preOpenMatch ) {
+ // @todo T7718: paragraph closed
+ $output .= $this->closeParagraph();
+ }
if ( $preOpenMatch && !$preCloseMatch ) {
$this->inPre = true;
}
@@ -329,6 +335,14 @@ class BlockLevelPass {
$this->lastSection = 'pre';
}
$t = substr( $t, 1 );
+ } elseif ( preg_match( '/^(?:<style\\b[^>]*>.*?<\\/style>\s*|<link\\b[^>]*>\s*)+$/iS', $t ) ) {
+ # T186965: <style> or <link> by itself on a line shouldn't open or close paragraphs.
+ # But it should clear $pendingPTag.
+ if ( $pendingPTag ) {
+ $output .= $this->closeParagraph();
+ $pendingPTag = false;
+ $this->lastSection = '';
+ }
} else {
# paragraph
if ( trim( $t ) === '' ) {
@@ -363,9 +377,12 @@ class BlockLevelPass {
$this->inPre = false;
}
if ( $pendingPTag === false ) {
- $output .= $t;
if ( $prefixLength === 0 ) {
+ $output .= $t;
$output .= "\n";
+ } else {
+ // Trim whitespace in list items
+ $output .= trim( $t );
}
}
}
@@ -417,130 +434,130 @@ class BlockLevelPass {
$c = $str[$i];
switch ( $state ) {
- case self::COLON_STATE_TEXT:
- switch ( $c ) {
- case "<":
- # Could be either a <start> tag or an </end> tag
- $state = self::COLON_STATE_TAGSTART;
- break;
- case ":":
- if ( $ltLevel === 0 ) {
- # We found it!
- $before = substr( $str, 0, $i );
- $after = substr( $str, $i + 1 );
- return $i;
+ case self::COLON_STATE_TEXT:
+ switch ( $c ) {
+ case "<":
+ # Could be either a <start> tag or an </end> tag
+ $state = self::COLON_STATE_TAGSTART;
+ break;
+ case ":":
+ if ( $ltLevel === 0 ) {
+ # We found it!
+ $before = substr( $str, 0, $i );
+ $after = substr( $str, $i + 1 );
+ return $i;
+ }
+ # Embedded in a tag; don't break it.
+ break;
+ default:
+ # Skip ahead looking for something interesting
+ if ( !preg_match( '/:|<|-\{/', $str, $m, PREG_OFFSET_CAPTURE, $i ) ) {
+ # Nothing else interesting
+ return false;
+ }
+ if ( $m[0][0] === '-{' ) {
+ $state = self::COLON_STATE_LC;
+ $lcLevel++;
+ $i = $m[0][1] + 1;
+ } else {
+ # Skip ahead to next interesting character.
+ $i = $m[0][1] - 1;
+ }
+ break;
}
- # Embedded in a tag; don't break it.
break;
- default:
- # Skip ahead looking for something interesting
- if ( !preg_match( '/:|<|-\{/', $str, $m, PREG_OFFSET_CAPTURE, $i ) ) {
- # Nothing else interesting
- return false;
- }
- if ( $m[0][0] === '-{' ) {
- $state = self::COLON_STATE_LC;
+ case self::COLON_STATE_LC:
+ # In language converter markup -{ ... }-
+ if ( !preg_match( '/-\{|\}-/', $str, $m, PREG_OFFSET_CAPTURE, $i ) ) {
+ # Nothing else interesting to find; abort!
+ # We're nested in language converter markup, but there
+ # are no close tags left. Abort!
+ break 2;
+ } elseif ( $m[0][0] === '-{' ) {
+ $i = $m[0][1] + 1;
$lcLevel++;
+ } elseif ( $m[0][0] === '}-' ) {
$i = $m[0][1] + 1;
- } else {
- # Skip ahead to next interesting character.
- $i = $m[0][1] - 1;
+ $lcLevel--;
+ if ( $lcLevel === 0 ) {
+ $state = self::COLON_STATE_TEXT;
+ }
}
break;
- }
- break;
- case self::COLON_STATE_LC:
- # In language converter markup -{ ... }-
- if ( !preg_match( '/-\{|\}-/', $str, $m, PREG_OFFSET_CAPTURE, $i ) ) {
- # Nothing else interesting to find; abort!
- # We're nested in language converter markup, but there
- # are no close tags left. Abort!
- break 2;
- } elseif ( $m[0][0] === '-{' ) {
- $i = $m[0][1] + 1;
- $lcLevel++;
- } elseif ( $m[0][0] === '}-' ) {
- $i = $m[0][1] + 1;
- $lcLevel--;
- if ( $lcLevel === 0 ) {
- $state = self::COLON_STATE_TEXT;
+ case self::COLON_STATE_TAG:
+ # In a <tag>
+ switch ( $c ) {
+ case ">":
+ $ltLevel++;
+ $state = self::COLON_STATE_TEXT;
+ break;
+ case "/":
+ # Slash may be followed by >?
+ $state = self::COLON_STATE_TAGSLASH;
+ break;
+ default:
+ # ignore
}
- }
- break;
- case self::COLON_STATE_TAG:
- # In a <tag>
- switch ( $c ) {
- case ">":
- $ltLevel++;
- $state = self::COLON_STATE_TEXT;
break;
- case "/":
- # Slash may be followed by >?
- $state = self::COLON_STATE_TAGSLASH;
+ case self::COLON_STATE_TAGSTART:
+ switch ( $c ) {
+ case "/":
+ $state = self::COLON_STATE_CLOSETAG;
+ break;
+ case "!":
+ $state = self::COLON_STATE_COMMENT;
+ break;
+ case ">":
+ # Illegal early close? This shouldn't happen D:
+ $state = self::COLON_STATE_TEXT;
+ break;
+ default:
+ $state = self::COLON_STATE_TAG;
+ }
break;
- default:
- # ignore
- }
- break;
- case self::COLON_STATE_TAGSTART:
- switch ( $c ) {
- case "/":
- $state = self::COLON_STATE_CLOSETAG;
+ case self::COLON_STATE_CLOSETAG:
+ # In a </tag>
+ if ( $c === ">" ) {
+ if ( $ltLevel > 0 ) {
+ $ltLevel--;
+ } else {
+ # ignore the excess close tag, but keep looking for
+ # colons. (This matches Parsoid behavior.)
+ wfDebug( __METHOD__ . ": Invalid input; too many close tags\n" );
+ }
+ $state = self::COLON_STATE_TEXT;
+ }
break;
- case "!":
- $state = self::COLON_STATE_COMMENT;
+ case self::COLON_STATE_TAGSLASH:
+ if ( $c === ">" ) {
+ # Yes, a self-closed tag <blah/>
+ $state = self::COLON_STATE_TEXT;
+ } else {
+ # Probably we're jumping the gun, and this is an attribute
+ $state = self::COLON_STATE_TAG;
+ }
break;
- case ">":
- # Illegal early close? This shouldn't happen D:
- $state = self::COLON_STATE_TEXT;
+ case self::COLON_STATE_COMMENT:
+ if ( $c === "-" ) {
+ $state = self::COLON_STATE_COMMENTDASH;
+ }
break;
- default:
- $state = self::COLON_STATE_TAG;
- }
- break;
- case self::COLON_STATE_CLOSETAG:
- # In a </tag>
- if ( $c === ">" ) {
- if ( $ltLevel > 0 ) {
- $ltLevel--;
+ case self::COLON_STATE_COMMENTDASH:
+ if ( $c === "-" ) {
+ $state = self::COLON_STATE_COMMENTDASHDASH;
} else {
- # ignore the excess close tag, but keep looking for
- # colons. (This matches Parsoid behavior.)
- wfDebug( __METHOD__ . ": Invalid input; too many close tags\n" );
+ $state = self::COLON_STATE_COMMENT;
}
- $state = self::COLON_STATE_TEXT;
- }
- break;
- case self::COLON_STATE_TAGSLASH:
- if ( $c === ">" ) {
- # Yes, a self-closed tag <blah/>
- $state = self::COLON_STATE_TEXT;
- } else {
- # Probably we're jumping the gun, and this is an attribute
- $state = self::COLON_STATE_TAG;
- }
- break;
- case self::COLON_STATE_COMMENT:
- if ( $c === "-" ) {
- $state = self::COLON_STATE_COMMENTDASH;
- }
- break;
- case self::COLON_STATE_COMMENTDASH:
- if ( $c === "-" ) {
- $state = self::COLON_STATE_COMMENTDASHDASH;
- } else {
- $state = self::COLON_STATE_COMMENT;
- }
- break;
- case self::COLON_STATE_COMMENTDASHDASH:
- if ( $c === ">" ) {
- $state = self::COLON_STATE_TEXT;
- } else {
- $state = self::COLON_STATE_COMMENT;
- }
- break;
- default:
- throw new MWException( "State machine error in " . __METHOD__ );
+ break;
+ case self::COLON_STATE_COMMENTDASHDASH:
+ if ( $c === ">" ) {
+ $state = self::COLON_STATE_TEXT;
+ } else {
+ $state = self::COLON_STATE_COMMENT;
+ }
+ break;
+ default:
+ throw new MWException( "State machine error in " . __METHOD__ );
}
}
if ( $ltLevel > 0 || $lcLevel > 0 ) {
diff --git a/www/wiki/includes/parser/CoreParserFunctions.php b/www/wiki/includes/parser/CoreParserFunctions.php
index 45a5092c..0e30b3c8 100644
--- a/www/wiki/includes/parser/CoreParserFunctions.php
+++ b/www/wiki/includes/parser/CoreParserFunctions.php
@@ -337,8 +337,8 @@ class CoreParserFunctions {
// default
$gender = User::getDefaultOption( 'gender' );
- // allow prefix.
- $title = Title::newFromText( $username );
+ // allow prefix and normalize (e.g. "&#42;foo" -> "*foo" ).
+ $title = Title::newFromText( $username, NS_USER );
if ( $title && $title->inNamespace( NS_USER ) ) {
$username = $title->getText();
@@ -830,7 +830,7 @@ class CoreParserFunctions {
$restrictions = $titleObject->getRestrictions( strtolower( $type ) );
# Title::getRestrictions returns an array, its possible it may have
# multiple values in the future
- return implode( $restrictions, ',' );
+ return implode( ',', $restrictions );
}
return '';
}
@@ -875,7 +875,7 @@ class CoreParserFunctions {
$code = strtolower( $code );
$inLanguage = strtolower( $inLanguage );
$lang = Language::fetchLanguageName( $code, $inLanguage );
- return $lang !== '' ? $lang : wfBCP47( $code );
+ return $lang !== '' ? $lang : LanguageCode::bcp47( $code );
}
/**
@@ -935,7 +935,8 @@ class CoreParserFunctions {
*/
public static function anchorencode( $parser, $text ) {
$text = $parser->killMarkers( $text );
- return (string)substr( $parser->guessSectionNameFromWikiText( $text ), 1 );
+ $section = (string)substr( $parser->guessSectionNameFromWikiText( $text ), 1 );
+ return Sanitizer::safeEncodeAttribute( $section );
}
public static function special( $parser, $text ) {
@@ -1009,10 +1010,10 @@ class CoreParserFunctions {
if ( $argA == 'nowiki' ) {
// {{filepath: | option [| size] }}
$isNowiki = true;
- $parsedWidthParam = $parser->parseWidthParam( $argB );
+ $parsedWidthParam = Parser::parseWidthParam( $argB );
} else {
// {{filepath: [| size [|option]] }}
- $parsedWidthParam = $parser->parseWidthParam( $argA );
+ $parsedWidthParam = Parser::parseWidthParam( $argA );
$isNowiki = ( $argB == 'nowiki' );
}
@@ -1343,7 +1344,7 @@ class CoreParserFunctions {
foreach ( $sources[0] as $sourceTitle ) {
$names[] = $sourceTitle->getPrefixedText();
}
- return implode( $names, '|' );
+ return implode( '|', $names );
}
return '';
}
diff --git a/www/wiki/includes/parser/DateFormatter.php b/www/wiki/includes/parser/DateFormatter.php
index 605a873b..0a4a60e9 100644
--- a/www/wiki/includes/parser/DateFormatter.php
+++ b/www/wiki/includes/parser/DateFormatter.php
@@ -126,13 +126,16 @@ class DateFormatter {
/**
* Get a DateFormatter object
*
- * @param Language|string|null $lang In which language to format the date
+ * @param Language|null $lang In which language to format the date
* Defaults to the site content language
* @return DateFormatter
*/
public static function getInstance( $lang = null ) {
global $wgContLang, $wgMainCacheType;
+ if ( is_string( $lang ) ) {
+ wfDeprecated( __METHOD__ . ' with type string for $lang', '1.31' );
+ }
$lang = $lang ? wfGetLangObj( $lang ) : $wgContLang;
$cache = ObjectCache::getLocalServerInstance( $wgMainCacheType );
diff --git a/www/wiki/includes/parser/LinkHolderArray.php b/www/wiki/includes/parser/LinkHolderArray.php
index bc5182c1..816f7f79 100644
--- a/www/wiki/includes/parser/LinkHolderArray.php
+++ b/www/wiki/includes/parser/LinkHolderArray.php
@@ -134,7 +134,7 @@ class LinkHolderArray {
$maxId = $newKey > $maxId ? $newKey : $maxId;
}
}
- $texts = preg_replace_callback( '/(<!--LINK \d+:)(\d+)(-->)/',
+ $texts = preg_replace_callback( '/(<!--LINK\'" \d+:)(\d+)(-->)/',
[ $this, 'mergeForeignCallback' ], $texts );
# Renumber interwiki links
@@ -143,7 +143,7 @@ class LinkHolderArray {
$this->interwikis[$newKey] = $entry;
$maxId = $newKey > $maxId ? $newKey : $maxId;
}
- $texts = preg_replace_callback( '/(<!--IWLINK )(\d+)(-->)/',
+ $texts = preg_replace_callback( '/(<!--IWLINK\'" )(\d+)(-->)/',
[ $this, 'mergeForeignCallback' ], $texts );
# Set the parent link ID to be beyond the highest used ID
@@ -172,7 +172,7 @@ class LinkHolderArray {
# Internal links
$pos = 0;
while ( $pos < strlen( $text ) ) {
- if ( !preg_match( '/<!--LINK (\d+):(\d+)-->/',
+ if ( !preg_match( '/<!--LINK\'" (\d+):(\d+)-->/',
$text, $m, PREG_OFFSET_CAPTURE, $pos )
) {
break;
@@ -186,7 +186,7 @@ class LinkHolderArray {
# Interwiki links
$pos = 0;
while ( $pos < strlen( $text ) ) {
- if ( !preg_match( '/<!--IWLINK (\d+)-->/', $text, $m, PREG_OFFSET_CAPTURE, $pos ) ) {
+ if ( !preg_match( '/<!--IWLINK\'" (\d+)-->/', $text, $m, PREG_OFFSET_CAPTURE, $pos ) ) {
break;
}
$key = $m[1][0];
@@ -249,12 +249,12 @@ class LinkHolderArray {
// Use a globally unique ID to keep the objects mergable
$key = $this->parent->nextLinkID();
$this->interwikis[$key] = $entry;
- $retVal = "<!--IWLINK $key-->{$trail}";
+ $retVal = "<!--IWLINK'\" $key-->{$trail}";
} else {
$key = $this->parent->nextLinkID();
$ns = $nt->getNamespace();
$this->internals[$ns][$key] = $entry;
- $retVal = "<!--LINK $ns:$key-->{$trail}";
+ $retVal = "<!--LINK'\" $ns:$key-->{$trail}";
}
$this->size++;
}
@@ -374,7 +374,7 @@ class LinkHolderArray {
$title = $entry['title'];
$query = isset( $entry['query'] ) ? $entry['query'] : [];
$key = "$ns:$index";
- $searchkey = "<!--LINK $key-->";
+ $searchkey = "<!--LINK'\" $key-->";
$displayText = $entry['text'];
if ( isset( $entry['selflink'] ) ) {
$replacePairs[$searchkey] = Linker::makeSelfLinkObj( $title, $displayText, $query );
@@ -408,7 +408,7 @@ class LinkHolderArray {
# Do the thing
$text = preg_replace_callback(
- '/(<!--LINK .*?-->)/',
+ '/(<!--LINK\'" .*?-->)/',
$replacer->cb(),
$text
);
@@ -437,7 +437,7 @@ class LinkHolderArray {
$replacer = new HashtableReplacer( $replacePairs, 1 );
$text = preg_replace_callback(
- '/<!--IWLINK (.*?)-->/',
+ '/<!--IWLINK\'" (.*?)-->/',
$replacer->cb(),
$text );
}
@@ -612,7 +612,7 @@ class LinkHolderArray {
*/
public function replaceText( $text ) {
$text = preg_replace_callback(
- '/<!--(LINK|IWLINK) (.*?)-->/',
+ '/<!--(LINK|IWLINK)\'" (.*?)-->/',
[ $this, 'replaceTextCallback' ],
$text );
diff --git a/www/wiki/includes/parser/MWTidy.php b/www/wiki/includes/parser/MWTidy.php
index ffc884eb..19cf5731 100644
--- a/www/wiki/includes/parser/MWTidy.php
+++ b/www/wiki/includes/parser/MWTidy.php
@@ -53,27 +53,6 @@ class MWTidy {
}
/**
- * Check HTML for errors, used if $wgValidateAllHtml = true.
- *
- * @param string $text
- * @param string &$errorStr Return the error string
- * @return bool Whether the HTML is valid
- * @throws MWException
- */
- public static function checkErrors( $text, &$errorStr = null ) {
- $driver = self::singleton();
- if ( !$driver ) {
- throw new MWException( __METHOD__ .
- ': tidy is disabled, caller should have checked MWTidy::isEnabled()' );
- }
- if ( $driver->supportsValidate() ) {
- return $driver->validate( $text, $errorStr );
- } else {
- throw new MWException( __METHOD__ . ": error text return from HHVM tidy is not supported" );
- }
- }
-
- /**
* @return bool
*/
public static function isEnabled() {
diff --git a/www/wiki/includes/parser/Parser.php b/www/wiki/includes/parser/Parser.php
index e901f6f3..b66031cc 100644
--- a/www/wiki/includes/parser/Parser.php
+++ b/www/wiki/includes/parser/Parser.php
@@ -96,10 +96,9 @@ class Parser {
# at least one character of a host name (embeds EXT_LINK_URL_CLASS)
const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
# RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
- // @codingStandardsIgnoreStart Generic.Files.LineLength
+ // phpcs:ignore Generic.Files.LineLength
const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
- // @codingStandardsIgnoreEnd
# Regular expression for a non-newline space
const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
@@ -271,15 +270,15 @@ class Parser {
$this->mPreprocessorClass = $conf['preprocessorClass'];
} elseif ( defined( 'HPHP_VERSION' ) ) {
# Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
- $this->mPreprocessorClass = 'Preprocessor_Hash';
+ $this->mPreprocessorClass = Preprocessor_Hash::class;
} elseif ( extension_loaded( 'domxml' ) ) {
# PECL extension that conflicts with the core DOM extension (T15770)
wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
- $this->mPreprocessorClass = 'Preprocessor_Hash';
+ $this->mPreprocessorClass = Preprocessor_Hash::class;
} elseif ( extension_loaded( 'dom' ) ) {
- $this->mPreprocessorClass = 'Preprocessor_DOM';
+ $this->mPreprocessorClass = Preprocessor_DOM::class;
} else {
- $this->mPreprocessorClass = 'Preprocessor_Hash';
+ $this->mPreprocessorClass = Preprocessor_Hash::class;
}
wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
}
@@ -359,7 +358,7 @@ class Parser {
$this->mLangLinkLanguages = [];
$this->currentRevisionCache = null;
- $this->mStripState = new StripState;
+ $this->mStripState = new StripState( $this );
# Clear these on every parse, T6549
$this->mTplRedirCache = $this->mTplDomCache = [];
@@ -406,13 +405,6 @@ class Parser {
$text, Title $title, ParserOptions $options,
$linestart = true, $clearState = true, $revid = null
) {
- /**
- * First pass--just handle <nowiki> sections, pass the rest off
- * to internalParse() which does all the real work.
- */
-
- global $wgShowHostnames;
-
if ( $clearState ) {
// We use U+007F DELETE to construct strip markers, so we have to make
// sure that this character does not occur in the input text.
@@ -474,7 +466,7 @@ class Parser {
}
}
- # Done parsing! Compute runtime adaptive expiry if set
+ # Compute runtime adaptive expiry if set
$this->mOutput->finalizeAdaptiveCacheExpiry();
# Warn if too many heavyweight parser functions were used
@@ -485,110 +477,9 @@ class Parser {
);
}
- # Information on include size limits, for the benefit of users who try to skirt them
+ # Information on limits, for the benefit of users who try to skirt them
if ( $this->mOptions->getEnableLimitReport() ) {
- $max = $this->mOptions->getMaxIncludeSize();
-
- $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
- if ( $cpuTime !== null ) {
- $this->mOutput->setLimitReportData( 'limitreport-cputime',
- sprintf( "%.3f", $cpuTime )
- );
- }
-
- $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
- $this->mOutput->setLimitReportData( 'limitreport-walltime',
- sprintf( "%.3f", $wallTime )
- );
-
- $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
- [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
- );
- $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
- [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
- );
- $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
- [ $this->mIncludeSizes['post-expand'], $max ]
- );
- $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
- [ $this->mIncludeSizes['arg'], $max ]
- );
- $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
- [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
- );
- $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
- [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
- );
- Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
-
- $limitReport = "NewPP limit report\n";
- if ( $wgShowHostnames ) {
- $limitReport .= 'Parsed by ' . wfHostname() . "\n";
- }
- $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
- $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
- $limitReport .= 'Dynamic content: ' .
- ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
- "\n";
-
- foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
- if ( Hooks::run( 'ParserLimitReportFormat',
- [ $key, &$value, &$limitReport, false, false ]
- ) ) {
- $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
- $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
- ->inLanguage( 'en' )->useDatabase( false );
- if ( !$valueMsg->exists() ) {
- $valueMsg = new RawMessage( '$1' );
- }
- if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
- $valueMsg->params( $value );
- $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
- }
- }
- }
- // Since we're not really outputting HTML, decode the entities and
- // then re-encode the things that need hiding inside HTML comments.
- $limitReport = htmlspecialchars_decode( $limitReport );
- // Run deprecated hook
- Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ], '1.22' );
-
- // Sanitize for comment. Note '‐' in the replacement is U+2010,
- // which looks much like the problematic '-'.
- $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
- $text .= "\n<!-- \n$limitReport-->\n";
-
- // Add on template profiling data in human/machine readable way
- $dataByFunc = $this->mProfiler->getFunctionStats();
- uasort( $dataByFunc, function ( $a, $b ) {
- return $a['real'] < $b['real']; // descending order
- } );
- $profileReport = [];
- foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
- $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
- $item['%real'], $item['real'], $item['calls'],
- htmlspecialchars( $item['name'] ) );
- }
- $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
- $text .= implode( "\n", $profileReport ) . "\n-->\n";
-
- $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
-
- // Add other cache related metadata
- if ( $wgShowHostnames ) {
- $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
- }
- $this->mOutput->setLimitReportData( 'cachereport-timestamp',
- $this->mOutput->getCacheTime() );
- $this->mOutput->setLimitReportData( 'cachereport-ttl',
- $this->mOutput->getCacheExpiry() );
- $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
- $this->mOutput->hasDynamicContent() );
-
- if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
- wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
- $this->mTitle->getPrefixedDBkey() );
- }
+ $text .= $this->makeLimitReport();
}
# Wrap non-interface parser output in a <div> so it can be targeted
@@ -612,6 +503,125 @@ class Parser {
}
/**
+ * Set the limit report data in the current ParserOutput, and return the
+ * limit report HTML comment.
+ *
+ * @return string
+ */
+ protected function makeLimitReport() {
+ global $wgShowHostnames;
+
+ $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
+
+ $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
+ if ( $cpuTime !== null ) {
+ $this->mOutput->setLimitReportData( 'limitreport-cputime',
+ sprintf( "%.3f", $cpuTime )
+ );
+ }
+
+ $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
+ $this->mOutput->setLimitReportData( 'limitreport-walltime',
+ sprintf( "%.3f", $wallTime )
+ );
+
+ $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
+ [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
+ );
+ $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
+ [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
+ );
+ $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
+ [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
+ );
+ $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
+ [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
+ );
+ $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
+ [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
+ );
+ $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
+ [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
+ );
+
+ foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
+ $this->mOutput->setLimitReportData( $key, $value );
+ }
+
+ Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
+
+ $limitReport = "NewPP limit report\n";
+ if ( $wgShowHostnames ) {
+ $limitReport .= 'Parsed by ' . wfHostname() . "\n";
+ }
+ $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
+ $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
+ $limitReport .= 'Dynamic content: ' .
+ ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
+ "\n";
+
+ foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
+ if ( Hooks::run( 'ParserLimitReportFormat',
+ [ $key, &$value, &$limitReport, false, false ]
+ ) ) {
+ $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
+ $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
+ ->inLanguage( 'en' )->useDatabase( false );
+ if ( !$valueMsg->exists() ) {
+ $valueMsg = new RawMessage( '$1' );
+ }
+ if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
+ $valueMsg->params( $value );
+ $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
+ }
+ }
+ }
+ // Since we're not really outputting HTML, decode the entities and
+ // then re-encode the things that need hiding inside HTML comments.
+ $limitReport = htmlspecialchars_decode( $limitReport );
+ // Run deprecated hook
+ Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ], '1.22' );
+
+ // Sanitize for comment. Note '‐' in the replacement is U+2010,
+ // which looks much like the problematic '-'.
+ $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
+ $text = "\n<!-- \n$limitReport-->\n";
+
+ // Add on template profiling data in human/machine readable way
+ $dataByFunc = $this->mProfiler->getFunctionStats();
+ uasort( $dataByFunc, function ( $a, $b ) {
+ return $a['real'] < $b['real']; // descending order
+ } );
+ $profileReport = [];
+ foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
+ $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
+ $item['%real'], $item['real'], $item['calls'],
+ htmlspecialchars( $item['name'] ) );
+ }
+ $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
+ $text .= implode( "\n", $profileReport ) . "\n-->\n";
+
+ $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
+
+ // Add other cache related metadata
+ if ( $wgShowHostnames ) {
+ $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
+ }
+ $this->mOutput->setLimitReportData( 'cachereport-timestamp',
+ $this->mOutput->getCacheTime() );
+ $this->mOutput->setLimitReportData( 'cachereport-ttl',
+ $this->mOutput->getCacheExpiry() );
+ $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
+ $this->mOutput->hasDynamicContent() );
+
+ if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
+ wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
+ $this->mTitle->getPrefixedDBkey() );
+ }
+ return $text;
+ }
+
+ /**
* Half-parse wikitext to half-parsed HTML. This recursive parser entry point
* can be called from an extension tag hook.
*
@@ -1103,7 +1113,11 @@ class Parser {
$line = "</{$last_tag}>{$line}";
}
array_pop( $tr_attributes );
- $outLine = $line . str_repeat( '</dd></dl>', $indent_level );
+ if ( $indent_level > 0 ) {
+ $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
+ } else {
+ $outLine = $line;
+ }
} elseif ( $first_two === '|-' ) {
# Now we have a table row
$line = preg_replace( '#^\|-+#', '', $line );
@@ -1194,13 +1208,15 @@ class Parser {
# be mistaken as delimiting cell parameters
# Bug T153140: Neither should language converter markup.
if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
- $cell = "{$previous}<{$last_tag}>{$cell}";
+ $cell = "{$previous}<{$last_tag}>" . trim( $cell );
} elseif ( count( $cell_data ) == 1 ) {
- $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
+ // Whitespace in cells is trimmed
+ $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
} else {
$attributes = $this->mStripState->unstripBoth( $cell_data[0] );
$attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
- $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
+ // Whitespace in cells is trimmed
+ $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
}
$outLine .= $cell;
@@ -1455,7 +1471,7 @@ class Parser {
/**
* @throws MWException
* @param array $m
- * @return HTML|string
+ * @return string HTML
*/
public function magicLinkCallback( $m ) {
if ( isset( $m[1] ) && $m[1] !== '' ) {
@@ -1607,7 +1623,9 @@ class Parser {
public function doHeadings( $text ) {
for ( $i = 6; $i >= 1; --$i ) {
$h = str_repeat( '=', $i );
- $text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text );
+ // Trim non-newline whitespace from headings
+ // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
+ $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
}
return $text;
}
@@ -1858,8 +1876,8 @@ class Parser {
$dtrail = '';
- # Set linktype for CSS - if URL==text, link is essentially free
- $linktype = ( $text === $url ) ? 'free' : 'text';
+ # Set linktype for CSS
+ $linktype = 'text';
# No link text, e.g. [http://domain.tld/some.link]
if ( $text == '' ) {
@@ -2133,11 +2151,8 @@ class Parser {
$useSubpages = $this->areSubpagesAllowed();
- // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
# Loop for each link
for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
- // @codingStandardsIgnoreEnd
-
# Check for excessive memory usage
if ( $holders->isBig() ) {
# Too big
@@ -2213,8 +2228,14 @@ class Parser {
$link = $origLink;
}
- $unstrip = $this->mStripState->unstripNoWiki( $link );
- $nt = is_string( $unstrip ) ? Title::newFromText( $unstrip ) : null;
+ // \x7f isn't a default legal title char, so most likely strip
+ // markers will force us into the "invalid form" path above. But,
+ // just in case, let's assert that xmlish tags aren't valid in
+ // the title position.
+ $unstrip = $this->mStripState->killMarkers( $link );
+ $noMarkers = ( $unstrip === $link );
+
+ $nt = $noMarkers ? Title::newFromText( $link ) : null;
if ( $nt === null ) {
$s .= $prefix . '[[' . $line;
continue;
@@ -2296,8 +2317,10 @@ class Parser {
$this->mOutput->addLanguageLink( $nt->getFullText() );
}
- $s = rtrim( $s . $prefix );
- $s .= trim( $trail, "\n" ) == '' ? '' : $prefix . $trail;
+ /**
+ * Strip the whitespace interwiki links produce, see T10897
+ */
+ $s = rtrim( $s . $prefix ) . $trail; # T175416
continue;
}
@@ -2322,7 +2345,10 @@ class Parser {
continue;
}
} elseif ( $ns == NS_CATEGORY ) {
- $s = rtrim( $s . "\n" ); # T2087
+ /**
+ * Strip the whitespace Category links produce, see T2087
+ */
+ $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
if ( $wasblank ) {
$sortkey = $this->getDefaultSort();
@@ -2334,11 +2360,6 @@ class Parser {
$sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
$this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
- /**
- * Strip the whitespace Category links produce, see T2087
- */
- $s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
-
continue;
}
}
@@ -2504,10 +2525,10 @@ class Parser {
$value = '|';
break;
case 'currentmonth':
- $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
+ $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ), true );
break;
case 'currentmonth1':
- $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
+ $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ), true );
break;
case 'currentmonthname':
$value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
@@ -2519,16 +2540,16 @@ class Parser {
$value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
break;
case 'currentday':
- $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
+ $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ), true );
break;
case 'currentday2':
- $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
+ $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ), true );
break;
case 'localmonth':
- $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
+ $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ), true );
break;
case 'localmonth1':
- $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
+ $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ), true );
break;
case 'localmonthname':
$value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
@@ -2540,10 +2561,10 @@ class Parser {
$value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
break;
case 'localday':
- $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
+ $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ), true );
break;
case 'localday2':
- $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
+ $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ), true );
break;
case 'pagename':
$value = wfEscapeWikiText( $this->mTitle->getText() );
@@ -3108,10 +3129,32 @@ class Parser {
throw $ex;
}
- # The interface for parser functions allows for extracting
- # flags into the local scope. Extract any forwarded flags
- # here.
- extract( $result );
+ // Extract any forwarded flags
+ if ( isset( $result['title'] ) ) {
+ $title = $result['title'];
+ }
+ if ( isset( $result['found'] ) ) {
+ $found = $result['found'];
+ }
+ if ( array_key_exists( 'text', $result ) ) {
+ // a string or null
+ $text = $result['text'];
+ }
+ if ( isset( $result['nowiki'] ) ) {
+ $nowiki = $result['nowiki'];
+ }
+ if ( isset( $result['isHTML'] ) ) {
+ $isHTML = $result['isHTML'];
+ }
+ if ( isset( $result['forceRawInterwiki'] ) ) {
+ $forceRawInterwiki = $result['forceRawInterwiki'];
+ }
+ if ( isset( $result['isChildObj'] ) ) {
+ $isChildObj = $result['isChildObj'];
+ }
+ if ( isset( $result['isLocalObj'] ) ) {
+ $isLocalObj = $result['isLocalObj'];
+ }
}
}
@@ -3492,13 +3535,7 @@ class Parser {
* @return Revision|bool False if missing
*/
public static function statelessFetchRevision( Title $title, $parser = false ) {
- $pageId = $title->getArticleID();
- $revId = $title->getLatestRevID();
-
- $rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $pageId, $revId );
- if ( $rev ) {
- $rev->setTitle( $title );
- }
+ $rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title );
return $rev;
}
@@ -3555,9 +3592,8 @@ class Parser {
$deps = [];
# Loop to fetch the article, with up to 1 redirect
- // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
+ // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
- // @codingStandardsIgnoreEnd
# Give extensions a chance to select the revision instead
$id = false; # Assume current
Hooks::run( 'BeforeParserFetchTemplateAndtitle',
@@ -3862,11 +3898,12 @@ class Parser {
}
if ( is_array( $output ) ) {
- # Extract flags to local scope (to override $markerType)
+ // Extract flags
$flags = $output;
$output = $flags[0];
- unset( $flags[0] );
- extract( $flags );
+ if ( isset( $flags['markerType'] ) ) {
+ $markerType = $flags['markerType'];
+ }
}
} else {
if ( is_null( $attrText ) ) {
@@ -3944,7 +3981,7 @@ class Parser {
$this->mForceTocPosition = true;
# Set a placeholder. At the end we'll fill it in with the TOC.
- $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
+ $text = $mw->replace( '<!--MWTOC\'"-->', $text, 1 );
# Only keep the first one.
$text = $mw->replace( '', $text );
@@ -4014,20 +4051,18 @@ class Parser {
# Inhibit editsection links if requested in the page
if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
- $maybeShowEditLink = $showEditLink = false;
+ $maybeShowEditLink = false;
} else {
- $maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */
- $showEditLink = $this->mOptions->getEditSection();
- }
- if ( $showEditLink ) {
- $this->mOutput->setEditSectionTokens( true );
+ $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
}
# Get all headlines for numbering them and adding funky stuff like [edit]
# links - this is for later, but we need the number of headlines right now
+ # NOTE: white space in headings have been trimmed in doHeadings. They shouldn't
+ # be trimmed here since whitespace in HTML headings is significant.
$matches = [];
$numMatches = preg_match_all(
- '/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
+ '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
$text,
$matches
);
@@ -4206,6 +4241,9 @@ class Parser {
# Decode HTML entities
$safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
+
+ $safeHeadline = self::normalizeSectionName( $safeHeadline );
+
$fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
$linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
$safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
@@ -4227,9 +4265,8 @@ class Parser {
$anchor = $safeHeadline;
$fallbackAnchor = $fallbackHeadline;
if ( isset( $refers[$arrayKey] ) ) {
- // @codingStandardsIgnoreStart
+ // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall,Generic.Formatting.DisallowMultipleStatements
for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
- // @codingStandardsIgnoreEnd
$anchor .= "_$i";
$linkAnchor .= "_$i";
$refers["${arrayKey}_$i"] = true;
@@ -4237,9 +4274,8 @@ class Parser {
$refers[$arrayKey] = true;
}
if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
- // @codingStandardsIgnoreStart
+ // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall,Generic.Formatting.DisallowMultipleStatements
for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
- // @codingStandardsIgnoreEnd
$fallbackAnchor .= "_$i";
$refers["${fallbackArrayKey}_$i"] = true;
} else {
@@ -4371,9 +4407,9 @@ class Parser {
* $this : caller
* $section : the section number
* &$sectionContent : ref to the content of the section
- * $showEditLinks : boolean describing whether this section has an edit link
+ * $maybeShowEditLinks : boolean describing whether this section has an edit link
*/
- Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $showEditLink ] );
+ Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $maybeShowEditLink ] );
$i++;
}
@@ -4387,7 +4423,7 @@ class Parser {
$full .= implode( '', $sections );
if ( $this->mForceTocPosition ) {
- return str_replace( '<!--MWTOC-->', $toc, $full );
+ return str_replace( '<!--MWTOC\'"-->', $toc, $full );
} else {
return $full;
}
@@ -4660,7 +4696,7 @@ class Parser {
* Wrapper for preprocess()
*
* @param string $text The text to preprocess
- * @param ParserOptions $options Options
+ * @param ParserOptions $options
* @param Title|null $title Title object or null to use $wgTitle
* @return string
*/
@@ -5019,40 +5055,40 @@ class Parser {
$paramName = $paramMap[$magicName];
switch ( $paramName ) {
- case 'gallery-internal-alt':
- $alt = $this->stripAltText( $match, false );
- break;
- case 'gallery-internal-link':
- $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
- $chars = self::EXT_LINK_URL_CLASS;
- $addr = self::EXT_LINK_ADDR;
- $prots = $this->mUrlProtocols;
- // check to see if link matches an absolute url, if not then it must be a wiki link.
- if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
- // Result of LanguageConverter::markNoConversion
- // invoked on an external link.
- $linkValue = substr( $linkValue, 4, -2 );
- }
- if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
- $link = $linkValue;
- $this->mOutput->addExternalLink( $link );
- } else {
- $localLinkTitle = Title::newFromText( $linkValue );
- if ( $localLinkTitle !== null ) {
- $this->mOutput->addLink( $localLinkTitle );
- $link = $localLinkTitle->getLinkURL();
+ case 'gallery-internal-alt':
+ $alt = $this->stripAltText( $match, false );
+ break;
+ case 'gallery-internal-link':
+ $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
+ $chars = self::EXT_LINK_URL_CLASS;
+ $addr = self::EXT_LINK_ADDR;
+ $prots = $this->mUrlProtocols;
+ // check to see if link matches an absolute url, if not then it must be a wiki link.
+ if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
+ // Result of LanguageConverter::markNoConversion
+ // invoked on an external link.
+ $linkValue = substr( $linkValue, 4, -2 );
+ }
+ if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
+ $link = $linkValue;
+ $this->mOutput->addExternalLink( $link );
+ } else {
+ $localLinkTitle = Title::newFromText( $linkValue );
+ if ( $localLinkTitle !== null ) {
+ $this->mOutput->addLink( $localLinkTitle );
+ $link = $localLinkTitle->getLinkURL();
+ }
+ }
+ break;
+ default:
+ // Must be a handler specific parameter.
+ if ( $handler->validateParam( $paramName, $match ) ) {
+ $handlerOptions[$paramName] = $match;
+ } else {
+ // Guess not, consider it as caption.
+ wfDebug( "$parameterMatch failed parameter validation\n" );
+ $label = '|' . $parameterMatch;
}
- }
- break;
- default:
- // Must be a handler specific parameter.
- if ( $handler->validateParam( $paramName, $match ) ) {
- $handlerOptions[$paramName] = $match;
- } else {
- // Guess not, consider it as caption.
- wfDebug( "$parameterMatch failed parameter validation\n" );
- $label = '|' . $parameterMatch;
- }
}
} else {
@@ -5191,7 +5227,7 @@ class Parser {
# Special case; width and height come in one variable together
if ( $type === 'handler' && $paramName === 'width' ) {
- $parsedWidthParam = $this->parseWidthParam( $value );
+ $parsedWidthParam = self::parseWidthParam( $value );
if ( isset( $parsedWidthParam['width'] ) ) {
$width = $parsedWidthParam['width'];
if ( $handler->validateParam( 'width', $width ) ) {
@@ -5214,52 +5250,52 @@ class Parser {
} else {
# Validate internal parameters
switch ( $paramName ) {
- case 'manualthumb':
- case 'alt':
- case 'class':
- # @todo FIXME: Possibly check validity here for
- # manualthumb? downstream behavior seems odd with
- # missing manual thumbs.
- $validated = true;
- $value = $this->stripAltText( $value, $holders );
- break;
- case 'link':
- $chars = self::EXT_LINK_URL_CLASS;
- $addr = self::EXT_LINK_ADDR;
- $prots = $this->mUrlProtocols;
- if ( $value === '' ) {
- $paramName = 'no-link';
- $value = true;
+ case 'manualthumb':
+ case 'alt':
+ case 'class':
+ # @todo FIXME: Possibly check validity here for
+ # manualthumb? downstream behavior seems odd with
+ # missing manual thumbs.
$validated = true;
- } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
- if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
- $paramName = 'link-url';
- $this->mOutput->addExternalLink( $value );
- if ( $this->mOptions->getExternalLinkTarget() ) {
- $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
- }
- $validated = true;
- }
- } else {
- $linkTitle = Title::newFromText( $value );
- if ( $linkTitle ) {
- $paramName = 'link-title';
- $value = $linkTitle;
- $this->mOutput->addLink( $linkTitle );
+ $value = $this->stripAltText( $value, $holders );
+ break;
+ case 'link':
+ $chars = self::EXT_LINK_URL_CLASS;
+ $addr = self::EXT_LINK_ADDR;
+ $prots = $this->mUrlProtocols;
+ if ( $value === '' ) {
+ $paramName = 'no-link';
+ $value = true;
$validated = true;
+ } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
+ if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
+ $paramName = 'link-url';
+ $this->mOutput->addExternalLink( $value );
+ if ( $this->mOptions->getExternalLinkTarget() ) {
+ $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
+ }
+ $validated = true;
+ }
+ } else {
+ $linkTitle = Title::newFromText( $value );
+ if ( $linkTitle ) {
+ $paramName = 'link-title';
+ $value = $linkTitle;
+ $this->mOutput->addLink( $linkTitle );
+ $validated = true;
+ }
}
- }
- break;
- case 'frameless':
- case 'framed':
- case 'thumbnail':
- // use first appearing option, discard others.
- $validated = !$seenformat;
- $seenformat = true;
- break;
- default:
- # Most other things appear to be empty or numeric...
- $validated = ( $value === false || is_numeric( trim( $value ) ) );
+ break;
+ case 'frameless':
+ case 'framed':
+ case 'thumbnail':
+ // use first appearing option, discard others.
+ $validated = !$seenformat;
+ $seenformat = true;
+ break;
+ default:
+ # Most other things appear to be empty or numeric...
+ $validated = ( $value === false || is_numeric( trim( $value ) ) );
}
}
@@ -5753,21 +5789,42 @@ class Parser {
return $this->mDefaultSort;
}
+ private static function getSectionNameFromStrippedText( $text ) {
+ $text = Sanitizer::normalizeSectionNameWhitespace( $text );
+ $text = Sanitizer::decodeCharReferences( $text );
+ $text = self::normalizeSectionName( $text );
+ return $text;
+ }
+
+ private static function makeAnchor( $sectionName ) {
+ return '#' . Sanitizer::escapeIdForLink( $sectionName );
+ }
+
+ private static function makeLegacyAnchor( $sectionName ) {
+ global $wgFragmentMode;
+ if ( isset( $wgFragmentMode[1] ) && $wgFragmentMode[1] === 'legacy' ) {
+ // ForAttribute() and ForLink() are the same for legacy encoding
+ $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
+ } else {
+ $id = Sanitizer::escapeIdForLink( $sectionName );
+ }
+
+ return "#$id";
+ }
+
/**
* Try to guess the section anchor name based on a wikitext fragment
* presumably extracted from a heading, for example "Header" from
* "== Header ==".
*
* @param string $text
- *
- * @return string
+ * @return string Anchor (starting with '#')
*/
public function guessSectionNameFromWikiText( $text ) {
# Strip out wikitext links(they break the anchor)
$text = $this->stripSectionName( $text );
- $text = Sanitizer::normalizeSectionNameWhitespace( $text );
- $text = Sanitizer::decodeCharReferences( $text );
- return '#' . Sanitizer::escapeIdForLink( $text );
+ $sectionName = self::getSectionNameFromStrippedText( $text );
+ return self::makeAnchor( $sectionName );
}
/**
@@ -5777,24 +5834,41 @@ class Parser {
* than UTF-8, resulting in breakage.
*
* @param string $text The section name
- * @return string An anchor
+ * @return string Anchor (starting with '#')
*/
public function guessLegacySectionNameFromWikiText( $text ) {
- global $wgFragmentMode;
-
# Strip out wikitext links(they break the anchor)
$text = $this->stripSectionName( $text );
- $text = Sanitizer::normalizeSectionNameWhitespace( $text );
- $text = Sanitizer::decodeCharReferences( $text );
+ $sectionName = self::getSectionNameFromStrippedText( $text );
+ return self::makeLegacyAnchor( $sectionName );
+ }
- if ( isset( $wgFragmentMode[1] ) && $wgFragmentMode[1] === 'legacy' ) {
- // ForAttribute() and ForLink() are the same for legacy encoding
- $id = Sanitizer::escapeIdForAttribute( $text, Sanitizer::ID_FALLBACK );
- } else {
- $id = Sanitizer::escapeIdForLink( $text );
- }
+ /**
+ * Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
+ * @param string $text Section name (plain text)
+ * @return string Anchor (starting with '#')
+ */
+ public static function guessSectionNameFromStrippedText( $text ) {
+ $sectionName = self::getSectionNameFromStrippedText( $text );
+ return self::makeAnchor( $sectionName );
+ }
- return "#$id";
+ /**
+ * Apply the same normalization as code making links to this section would
+ *
+ * @param string $text
+ * @return string
+ */
+ private static function normalizeSectionName( $text ) {
+ # T90902: ensure the same normalization is applied for IDs as to links
+ $titleParser = MediaWikiServices::getInstance()->getTitleParser();
+ try {
+
+ $parts = $titleParser->splitTitleString( "#$text" );
+ } catch ( MalformedTitleException $ex ) {
+ return $text;
+ }
+ return $parts['fragment'];
}
/**
@@ -5915,7 +5989,7 @@ class Parser {
/**
* Remove any strip markers found in the given text.
*
- * @param string $text Input string
+ * @param string $text
* @return string
*/
public function killMarkers( $text ) {
@@ -5934,11 +6008,13 @@ class Parser {
* unserializeHalfParsedText(). The text can then be safely incorporated into
* the return value of a parser hook.
*
+ * @deprecated since 1.31
* @param string $text
*
* @return array
*/
public function serializeHalfParsedText( $text ) {
+ wfDeprecated( __METHOD__, '1.31' );
$data = [
'text' => $text,
'version' => self::HALF_PARSED_VERSION,
@@ -5959,11 +6035,13 @@ class Parser {
* If the $data array has been stored persistently, the caller should first
* check whether it is still valid, by calling isValidHalfParsedText().
*
+ * @deprecated since 1.31
* @param array $data Serialized data
* @throws MWException
* @return string
*/
public function unserializeHalfParsedText( $data ) {
+ wfDeprecated( __METHOD__, '1.31' );
if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
throw new MWException( __METHOD__ . ': invalid version' );
}
@@ -5984,11 +6062,13 @@ class Parser {
* serializeHalfParsedText(), is compatible with the current version of the
* parser.
*
+ * @deprecated since 1.31
* @param array $data
*
* @return bool
*/
public function isValidHalfParsedText( $data ) {
+ wfDeprecated( __METHOD__, '1.31' );
return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
}
@@ -5996,11 +6076,12 @@ class Parser {
* Parsed a width param of imagelink like 300px or 200x300px
*
* @param string $value
+ * @param bool $parseHeight
*
* @return array
* @since 1.20
*/
- public function parseWidthParam( $value ) {
+ public static function parseWidthParam( $value, $parseHeight = true ) {
$parsedWidthParam = [];
if ( $value === '' ) {
return $parsedWidthParam;
@@ -6008,7 +6089,7 @@ class Parser {
$m = [];
# (T15500) In both cases (width/height and width only),
# permit trailing "px" for backward compatibility.
- if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
+ if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
$width = intval( $m[1] );
$height = intval( $m[2] );
$parsedWidthParam['width'] = $width;
diff --git a/www/wiki/includes/parser/ParserCache.php b/www/wiki/includes/parser/ParserCache.php
index c6801299..8a7fca6c 100644
--- a/www/wiki/includes/parser/ParserCache.php
+++ b/www/wiki/includes/parser/ParserCache.php
@@ -253,11 +253,6 @@ class ParserCache {
wfDebug( "ParserOutput cache found.\n" );
- // The edit section preference may not be the appropiate one in
- // the ParserOutput, as we are not storing it in the parsercache
- // key. Force it here. See T33445.
- $value->setEditSectionTokens( $popts->getEditSection() );
-
$wikiPage = method_exists( $article, 'getPage' )
? $article->getPage()
: $article;
diff --git a/www/wiki/includes/parser/ParserOptions.php b/www/wiki/includes/parser/ParserOptions.php
index c7146a13..ff21ef02 100644
--- a/www/wiki/includes/parser/ParserOptions.php
+++ b/www/wiki/includes/parser/ParserOptions.php
@@ -65,7 +65,6 @@ class ParserOptions {
'stubthreshold' => true,
'printable' => true,
'userlang' => true,
- 'wrapclass' => true,
];
/**
@@ -82,13 +81,6 @@ class ParserOptions {
private $mTimestamp;
/**
- * The edit section flag is in ParserOptions for historical reasons, but
- * doesn't actually affect the parser output since Feb 2015.
- * @var bool
- */
- private $mEditSection = true;
-
- /**
* Stored user object
* @var User
* @todo Track this for caching somehow without fragmenting the cache insanely
@@ -780,13 +772,17 @@ class ParserOptions {
/**
* CSS class to use to wrap output from Parser::parse()
* @since 1.30
- * @param string|bool $className Set false to disable wrapping.
+ * @param string $className Class name to use for wrapping.
+ * Passing false to indicate "no wrapping" was deprecated in MediaWiki 1.31.
* @return string|bool Current value
*/
public function setWrapOutputClass( $className ) {
if ( $className === true ) { // DWIM, they probably want the default class name
$className = 'mw-parser-output';
}
+ if ( $className === false ) {
+ wfDeprecated( __METHOD__ . '( false )', '1.31' );
+ }
return $this->setOption( 'wrapclass', $className );
}
@@ -869,19 +865,23 @@ class ParserOptions {
/**
* Create "edit section" links?
+ * @deprecated since 1.31, use ParserOutput::getText() options instead.
* @return bool
*/
public function getEditSection() {
- return $this->mEditSection;
+ wfDeprecated( __METHOD__, '1.31' );
+ return true;
}
/**
* Create "edit section" links?
+ * @deprecated since 1.31, use ParserOutput::getText() options instead.
* @param bool|null $x New value (null is no change)
* @return bool Old value
*/
public function setEditSection( $x ) {
- return wfSetVar( $this->mEditSection, $x );
+ wfDeprecated( __METHOD__, '1.31' );
+ return true;
}
/**
@@ -1057,18 +1057,16 @@ class ParserOptions {
'printable' => false,
'allowUnsafeRawHtml' => true,
'wrapclass' => 'mw-parser-output',
- 'currentRevisionCallback' => [ 'Parser', 'statelessFetchRevision' ],
- 'templateCallback' => [ 'Parser', 'statelessFetchTemplate' ],
+ 'currentRevisionCallback' => [ Parser::class, 'statelessFetchRevision' ],
+ 'templateCallback' => [ Parser::class, 'statelessFetchTemplate' ],
'speculativeRevIdCallback' => null,
];
- // @codingStandardsIgnoreStart Squiz.WhiteSpace.OperatorSpacing.NoSpaceAfterAmp
Hooks::run( 'ParserOptionsRegister', [
&self::$defaults,
&self::$inCacheKey,
&self::$lazyOptions,
] );
- // @codingStandardsIgnoreEnd
ksort( self::$inCacheKey );
}
@@ -1256,7 +1254,7 @@ class ParserOptions {
} elseif ( $value instanceof Language ) {
return $value->getCode();
} elseif ( is_array( $value ) ) {
- return '[' . join( ',', array_map( [ $this, 'optionToString' ], $value ) ) . ']';
+ return '[' . implode( ',', array_map( [ $this, 'optionToString' ], $value ) ) . ']';
} else {
return (string)$value;
}
@@ -1281,18 +1279,6 @@ class ParserOptions {
$defaults = self::getCanonicalOverrides() + self::getDefaults();
$inCacheKey = self::$inCacheKey;
- // Historical hack: 'editsection' hasn't been a true parser option since
- // Feb 2015 (instead the parser outputs a constant placeholder and post-parse
- // processing handles the option). But Wikibase forces it in $forOptions
- // and expects the cache key to still vary on it for T85252.
- // @deprecated since 1.30, Wikibase should use addExtraKey() or something instead.
- if ( in_array( 'editsection', $forOptions, true ) ) {
- $options['editsection'] = $this->mEditSection;
- $defaults['editsection'] = true;
- $inCacheKey['editsection'] = true;
- ksort( $inCacheKey );
- }
-
// We only include used options with non-canonical values in the key
// so adding a new option doesn't invalidate the entire parser cache.
// The drawback to this is that changing the default value of an option
@@ -1309,7 +1295,7 @@ class ParserOptions {
}
}
- $confstr = $values ? join( '!', $values ) : 'canonical';
+ $confstr = $values ? implode( '!', $values ) : 'canonical';
// add in language specific options, if any
// @todo FIXME: This is just a way of retrieving the url/user preferred variant
diff --git a/www/wiki/includes/parser/ParserOutput.php b/www/wiki/includes/parser/ParserOutput.php
index 3480a51f..8f0a1d7c 100644
--- a/www/wiki/includes/parser/ParserOutput.php
+++ b/www/wiki/includes/parser/ParserOutput.php
@@ -21,8 +21,16 @@
* @file
* @ingroup Parser
*/
+
class ParserOutput extends CacheTime {
/**
+ * Feature flags to indicate to extensions that MediaWiki core supports and
+ * uses getText() stateless transforms.
+ */
+ const SUPPORTS_STATELESS_TRANSFORMS = 1;
+ const SUPPORTS_UNWRAP_TRANSFORM = 1;
+
+ /**
* @var string $mText The output text
*/
public $mText;
@@ -144,11 +152,6 @@ class ParserOutput extends CacheTime {
public $mSections = [];
/**
- * @var bool $mEditSectionTokens prefix/suffix markers if edit sections were output as tokens.
- */
- public $mEditSectionTokens = false;
-
- /**
* @var array $mProperties Name/value pairs to be cached in the DB.
*/
public $mProperties = [];
@@ -164,11 +167,6 @@ class ParserOutput extends CacheTime {
public $mTimestamp;
/**
- * @var bool $mTOCEnabled Whether TOC should be shown, can't override __NOTOC__.
- */
- public $mTOCEnabled = true;
-
- /**
* @var bool $mEnableOOUI Whether OOUI should be enabled.
*/
public $mEnableOOUI = false;
@@ -250,9 +248,56 @@ class ParserOutput extends CacheTime {
return $this->mText;
}
- public function getText() {
+ /**
+ * Get the output HTML
+ *
+ * @param array $options (since 1.31) Transformations to apply to the HTML
+ * - allowTOC: (bool) Show the TOC, assuming there were enough headings
+ * to generate one and `__NOTOC__` wasn't used. Default is true,
+ * but might be statefully overridden.
+ * - enableSectionEditLinks: (bool) Include section edit links, assuming
+ * section edit link tokens are present in the HTML. Default is true,
+ * but might be statefully overridden.
+ * - unwrap: (bool) Remove a wrapping mw-parser-output div. Default is false.
+ * - deduplicateStyles: (bool) When true, which is the default, `<style>`
+ * tags with the `data-mw-deduplicate` attribute set are deduplicated by
+ * value of the attribute: all but the first will be replaced by `<link
+ * rel="mw-deduplicated-inline-style" href="mw-data:..."/>` tags, where
+ * the scheme-specific-part of the href is the (percent-encoded) value
+ * of the `data-mw-deduplicate` attribute.
+ * @return string HTML
+ */
+ public function getText( $options = [] ) {
+ $options += [
+ 'allowTOC' => true,
+ 'enableSectionEditLinks' => true,
+ 'unwrap' => false,
+ 'deduplicateStyles' => true,
+ ];
$text = $this->mText;
- if ( $this->mEditSectionTokens ) {
+
+ Hooks::runWithoutAbort( 'ParserOutputPostCacheTransform', [ $this, &$text, &$options ] );
+
+ if ( $options['unwrap'] !== false ) {
+ $start = Html::openElement( 'div', [
+ 'class' => 'mw-parser-output'
+ ] );
+ $startLen = strlen( $start );
+ $end = Html::closeElement( 'div' );
+ $endPos = strrpos( $text, $end );
+ $endLen = strlen( $end );
+
+ if ( substr( $text, 0, $startLen ) === $start && $endPos !== false
+ // if the closing div is followed by real content, bail out of unwrapping
+ && preg_match( '/^(?>\s*<!--.*?-->)*\s*$/s', substr( $text, $endPos + $endLen ) )
+ ) {
+ $text = substr( $text, $startLen );
+ $text = substr( $text, 0, $endPos - $startLen )
+ . substr( $text, $endPos - $startLen + $endLen );
+ }
+ }
+
+ if ( $options['enableSectionEditLinks'] ) {
$text = preg_replace_callback(
self::EDITSECTION_REGEX,
function ( $m ) {
@@ -278,8 +323,7 @@ class ParserOutput extends CacheTime {
$text = preg_replace( self::EDITSECTION_REGEX, '', $text );
}
- // If you have an old cached version of this class - sorry, you can't disable the TOC
- if ( isset( $this->mTOCEnabled ) && $this->mTOCEnabled ) {
+ if ( $options['allowTOC'] ) {
$text = str_replace( [ Parser::TOC_START, Parser::TOC_END ], '', $text );
} else {
$text = preg_replace(
@@ -288,6 +332,36 @@ class ParserOutput extends CacheTime {
$text
);
}
+
+ if ( $options['deduplicateStyles'] ) {
+ $seen = [];
+ $text = preg_replace_callback(
+ '#<style\s+([^>]*data-mw-deduplicate\s*=[^>]*)>.*?</style>#s',
+ function ( $m ) use ( &$seen ) {
+ $attr = Sanitizer::decodeTagAttributes( $m[1] );
+ if ( !isset( $attr['data-mw-deduplicate'] ) ) {
+ return $m[0];
+ }
+
+ $key = $attr['data-mw-deduplicate'];
+ if ( !isset( $seen[$key] ) ) {
+ $seen[$key] = true;
+ return $m[0];
+ }
+
+ // We were going to use an empty <style> here, but there
+ // was concern that would be too much overhead for browsers.
+ // So let's hope a <link> with a non-standard rel and href isn't
+ // going to be misinterpreted or mangled by any subsequent processing.
+ return Html::element( 'link', [
+ 'rel' => 'mw-deduplicated-inline-style',
+ 'href' => "mw-data:" . wfUrlencode( $key ),
+ ] );
+ },
+ $text
+ );
+ }
+
return $text;
}
@@ -339,8 +413,12 @@ class ParserOutput extends CacheTime {
return $this->mSections;
}
+ /**
+ * @deprecated since 1.31 Use getText() options.
+ */
public function getEditSectionTokens() {
- return $this->mEditSectionTokens;
+ wfDeprecated( __METHOD__, '1.31' );
+ return true;
}
public function &getLinks() {
@@ -426,8 +504,12 @@ class ParserOutput extends CacheTime {
return $this->mLimitReportJSData;
}
+ /**
+ * @deprecated since 1.31 Use getText() options.
+ */
public function getTOCEnabled() {
- return $this->mTOCEnabled;
+ wfDeprecated( __METHOD__, '1.31' );
+ return true;
}
public function getEnableOOUI() {
@@ -454,8 +536,12 @@ class ParserOutput extends CacheTime {
return wfSetVar( $this->mSections, $toc );
}
+ /**
+ * @deprecated since 1.31 Use getText() options.
+ */
public function setEditSectionTokens( $t ) {
- return wfSetVar( $this->mEditSectionTokens, $t );
+ wfDeprecated( __METHOD__, '1.31' );
+ return true;
}
public function setIndexPolicy( $policy ) {
@@ -470,8 +556,12 @@ class ParserOutput extends CacheTime {
return wfSetVar( $this->mTimestamp, $timestamp );
}
+ /**
+ * @deprecated since 1.31 Use getText() options.
+ */
public function setTOCEnabled( $flag ) {
- return wfSetVar( $this->mTOCEnabled, $flag );
+ wfDeprecated( __METHOD__, '1.31' );
+ return true;
}
public function addCategory( $c, $sort ) {
@@ -547,7 +637,7 @@ class ParserOutput extends CacheTime {
# Replace unnecessary URL escape codes with the referenced character
# This prevents spammers from hiding links from the filters
- $url = parser::normalizeLinkUrl( $url );
+ $url = Parser::normalizeLinkUrl( $url );
$registerExternalLink = true;
if ( !$wgRegisterInternalExternals ) {
diff --git a/www/wiki/includes/parser/Preprocessor_DOM.php b/www/wiki/includes/parser/Preprocessor_DOM.php
index 25889626..64edbb2f 100644
--- a/www/wiki/includes/parser/Preprocessor_DOM.php
+++ b/www/wiki/includes/parser/Preprocessor_DOM.php
@@ -24,9 +24,8 @@
/**
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class Preprocessor_DOM extends Preprocessor {
- // @codingStandardsIgnoreEnd
/**
* @var Parser
@@ -87,9 +86,9 @@ class Preprocessor_DOM extends Preprocessor {
$xml .= "</list>";
$dom = new DOMDocument();
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$result = $dom->loadXML( $xml );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$result ) {
// Try running the XML through UtfNormal to get rid of invalid characters
$xml = UtfNormal\Validator::cleanUp( $xml );
@@ -164,9 +163,9 @@ class Preprocessor_DOM extends Preprocessor {
}
$dom = new DOMDocument;
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$result = $dom->loadXML( $xml );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !$result ) {
// Try running the XML through UtfNormal to get rid of invalid characters
$xml = UtfNormal\Validator::cleanUp( $xml );
@@ -275,13 +274,6 @@ class Preprocessor_DOM extends Preprocessor {
$search = $searchBase;
if ( $stack->top === false ) {
$currentClosing = '';
- } elseif (
- $stack->top->close === '}-' &&
- $stack->top->count > 2
- ) {
- # adjust closing for -{{{...{{
- $currentClosing = '}';
- $search .= $currentClosing;
} else {
$currentClosing = $stack->top->close;
$search .= $currentClosing;
@@ -559,8 +551,16 @@ class Preprocessor_DOM extends Preprocessor {
'count' => $count ];
$stack->push( $piece );
$accum =& $stack->getAccum();
- $flags = $stack->getFlags();
- extract( $flags );
+ $stackFlags = $stack->getFlags();
+ if ( isset( $stackFlags['findEquals'] ) ) {
+ $findEquals = $stackFlags['findEquals'];
+ }
+ if ( isset( $stackFlags['findPipe'] ) ) {
+ $findPipe = $stackFlags['findPipe'];
+ }
+ if ( isset( $stackFlags['inHeading'] ) ) {
+ $inHeading = $stackFlags['inHeading'];
+ }
$i += $count;
}
} elseif ( $found == 'line-end' ) {
@@ -610,8 +610,16 @@ class Preprocessor_DOM extends Preprocessor {
// Unwind the stack
$stack->pop();
$accum =& $stack->getAccum();
- $flags = $stack->getFlags();
- extract( $flags );
+ $stackFlags = $stack->getFlags();
+ if ( isset( $stackFlags['findEquals'] ) ) {
+ $findEquals = $stackFlags['findEquals'];
+ }
+ if ( isset( $stackFlags['findPipe'] ) ) {
+ $findPipe = $stackFlags['findPipe'];
+ }
+ if ( isset( $stackFlags['inHeading'] ) ) {
+ $inHeading = $stackFlags['inHeading'];
+ }
// Append the result to the enclosing accumulator
$accum .= $element;
@@ -628,23 +636,44 @@ class Preprocessor_DOM extends Preprocessor {
strspn( $text, $curChar[$curLen - 1], $i + 1 ) + 1 :
strspn( $text, $curChar, $i );
+ $savedPrefix = '';
+ $lineStart = ( $i > 0 && $text[$i - 1] == "\n" );
+
+ if ( $curChar === "-{" && $count > $curLen ) {
+ // -{ => {{ transition because rightmost wins
+ $savedPrefix = '-';
+ $i++;
+ $curChar = '{';
+ $count--;
+ $rule = $this->rules[$curChar];
+ }
+
# we need to add to stack only if opening brace count is enough for one of the rules
if ( $count >= $rule['min'] ) {
# Add it to the stack
$piece = [
'open' => $curChar,
'close' => $rule['end'],
+ 'savedPrefix' => $savedPrefix,
'count' => $count,
- 'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ),
+ 'lineStart' => $lineStart,
];
$stack->push( $piece );
$accum =& $stack->getAccum();
- $flags = $stack->getFlags();
- extract( $flags );
+ $stackFlags = $stack->getFlags();
+ if ( isset( $stackFlags['findEquals'] ) ) {
+ $findEquals = $stackFlags['findEquals'];
+ }
+ if ( isset( $stackFlags['findPipe'] ) ) {
+ $findPipe = $stackFlags['findPipe'];
+ }
+ if ( isset( $stackFlags['inHeading'] ) ) {
+ $inHeading = $stackFlags['inHeading'];
+ }
} else {
# Add literal brace(s)
- $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
+ $accum .= htmlspecialchars( $savedPrefix . str_repeat( $curChar, $count ) );
}
$i += $count;
} elseif ( $found == 'close' ) {
@@ -661,10 +690,6 @@ class Preprocessor_DOM extends Preprocessor {
# check for maximum matching characters (if there are 5 closing
# characters, we will probably need only 3 - depending on the rules)
$rule = $this->rules[$piece->open];
- if ( $piece->close === '}-' && $piece->count > 2 ) {
- # tweak for -{..{{ }}..}-
- $rule = $this->rules['{'];
- }
if ( $count > $rule['max'] ) {
# The specified maximum exists in the callback array, unless the caller
# has made an error
@@ -701,7 +726,9 @@ class Preprocessor_DOM extends Preprocessor {
# The invocation is at the start of the line if lineStart is set in
# the stack, and all opening brackets are used up.
- if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
+ if ( $maxCount == $matchingCount &&
+ !empty( $piece->lineStart ) &&
+ strlen( $piece->savedPrefix ) == 0 ) {
$attr = ' lineStart="1"';
} else {
$attr = '';
@@ -739,17 +766,35 @@ class Preprocessor_DOM extends Preprocessor {
if ( $piece->count >= $min ) {
$stack->push( $piece );
$accum =& $stack->getAccum();
+ } elseif ( $piece->count == 1 && $piece->open === '{' && $piece->savedPrefix === '-' ) {
+ $piece->savedPrefix = '';
+ $piece->open = '-{';
+ $piece->count = 2;
+ $piece->close = $this->rules[$piece->open]['end'];
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
} else {
$s = substr( $piece->open, 0, -1 );
$s .= str_repeat(
substr( $piece->open, -1 ),
$piece->count - strlen( $s )
);
- $accum .= $s;
+ $accum .= $piece->savedPrefix . $s;
}
+ } elseif ( $piece->savedPrefix !== '' ) {
+ $accum .= $piece->savedPrefix;
+ }
+
+ $stackFlags = $stack->getFlags();
+ if ( isset( $stackFlags['findEquals'] ) ) {
+ $findEquals = $stackFlags['findEquals'];
+ }
+ if ( isset( $stackFlags['findPipe'] ) ) {
+ $findPipe = $stackFlags['findPipe'];
+ }
+ if ( isset( $stackFlags['inHeading'] ) ) {
+ $inHeading = $stackFlags['inHeading'];
}
- $flags = $stack->getFlags();
- extract( $flags );
# Add XML element to the enclosing accumulator
$accum .= $element;
@@ -763,9 +808,6 @@ class Preprocessor_DOM extends Preprocessor {
$stack->getCurrentPart()->eqpos = strlen( $accum );
$accum .= '=';
++$i;
- } elseif ( $found == 'dash' ) {
- $accum .= '-';
- ++$i;
}
}
@@ -792,7 +834,7 @@ class PPDStack {
*/
public $top;
public $out;
- public $elementClass = 'PPDStackElement';
+ public $elementClass = PPDStackElement::class;
public static $false = false;
@@ -885,6 +927,12 @@ class PPDStackElement {
public $close;
/**
+ * @var string Saved prefix that may affect later processing,
+ * e.g. to differentiate `-{{{{` and `{{{{` after later seeing `}}}`.
+ */
+ public $savedPrefix = '';
+
+ /**
* @var int Number of opening characters found (number of "=" for heading)
*/
public $count;
@@ -900,7 +948,7 @@ class PPDStackElement {
*/
public $lineStart;
- public $partClass = 'PPDPart';
+ public $partClass = PPDPart::class;
public function __construct( $data = [] ) {
$class = $this->partClass;
@@ -945,7 +993,7 @@ class PPDStackElement {
*/
public function breakSyntax( $openingCount = false ) {
if ( $this->open == "\n" ) {
- $s = $this->parts[0]->out;
+ $s = $this->savedPrefix . $this->parts[0]->out;
} else {
if ( $openingCount === false ) {
$openingCount = $this->count;
@@ -955,6 +1003,7 @@ class PPDStackElement {
substr( $this->open, -1 ),
$openingCount - strlen( $s )
);
+ $s = $this->savedPrefix . $s;
$first = true;
foreach ( $this->parts as $part ) {
if ( $first ) {
@@ -992,9 +1041,8 @@ class PPDPart {
* An expansion frame, used as a context to expand the result of preprocessToObj()
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPFrame_DOM implements PPFrame {
- // @codingStandardsIgnoreEnd
/**
* @var Preprocessor
@@ -1610,9 +1658,8 @@ class PPFrame_DOM implements PPFrame {
* Expansion frame with template arguments
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPTemplateFrame_DOM extends PPFrame_DOM {
- // @codingStandardsIgnoreEnd
public $numberedArgs, $namedArgs;
@@ -1789,9 +1836,8 @@ class PPTemplateFrame_DOM extends PPFrame_DOM {
* Expansion frame with custom arguments
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPCustomFrame_DOM extends PPFrame_DOM {
- // @codingStandardsIgnoreEnd
public $args;
@@ -1842,9 +1888,8 @@ class PPCustomFrame_DOM extends PPFrame_DOM {
/**
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPNode_DOM implements PPNode {
- // @codingStandardsIgnoreEnd
/**
* @var DOMElement
diff --git a/www/wiki/includes/parser/Preprocessor_Hash.php b/www/wiki/includes/parser/Preprocessor_Hash.php
index 332f8e9f..c7f630d5 100644
--- a/www/wiki/includes/parser/Preprocessor_Hash.php
+++ b/www/wiki/includes/parser/Preprocessor_Hash.php
@@ -39,9 +39,8 @@
*
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class Preprocessor_Hash extends Preprocessor {
- // @codingStandardsIgnoreEnd
/**
* @var Parser
@@ -207,13 +206,6 @@ class Preprocessor_Hash extends Preprocessor {
$search = $searchBase;
if ( $stack->top === false ) {
$currentClosing = '';
- } elseif (
- $stack->top->close === '}-' &&
- $stack->top->count > 2
- ) {
- # adjust closing for -{{{...{{
- $currentClosing = '}';
- $search .= $currentClosing;
} else {
$currentClosing = $stack->top->close;
$search .= $currentClosing;
@@ -497,7 +489,16 @@ class Preprocessor_Hash extends Preprocessor {
'count' => $count ];
$stack->push( $piece );
$accum =& $stack->getAccum();
- extract( $stack->getFlags() );
+ $stackFlags = $stack->getFlags();
+ if ( isset( $stackFlags['findEquals'] ) ) {
+ $findEquals = $stackFlags['findEquals'];
+ }
+ if ( isset( $stackFlags['findPipe'] ) ) {
+ $findPipe = $stackFlags['findPipe'];
+ }
+ if ( isset( $stackFlags['inHeading'] ) ) {
+ $inHeading = $stackFlags['inHeading'];
+ }
$i += $count;
}
} elseif ( $found == 'line-end' ) {
@@ -554,7 +555,16 @@ class Preprocessor_Hash extends Preprocessor {
// Unwind the stack
$stack->pop();
$accum =& $stack->getAccum();
- extract( $stack->getFlags() );
+ $stackFlags = $stack->getFlags();
+ if ( isset( $stackFlags['findEquals'] ) ) {
+ $findEquals = $stackFlags['findEquals'];
+ }
+ if ( isset( $stackFlags['findPipe'] ) ) {
+ $findPipe = $stackFlags['findPipe'];
+ }
+ if ( isset( $stackFlags['inHeading'] ) ) {
+ $inHeading = $stackFlags['inHeading'];
+ }
// Append the result to the enclosing accumulator
array_splice( $accum, count( $accum ), 0, $element );
@@ -572,22 +582,44 @@ class Preprocessor_Hash extends Preprocessor {
strspn( $text, $curChar[$curLen - 1], $i + 1 ) + 1 :
strspn( $text, $curChar, $i );
+ $savedPrefix = '';
+ $lineStart = ( $i > 0 && $text[$i - 1] == "\n" );
+
+ if ( $curChar === "-{" && $count > $curLen ) {
+ // -{ => {{ transition because rightmost wins
+ $savedPrefix = '-';
+ $i++;
+ $curChar = '{';
+ $count--;
+ $rule = $this->rules[$curChar];
+ }
+
# we need to add to stack only if opening brace count is enough for one of the rules
if ( $count >= $rule['min'] ) {
# Add it to the stack
$piece = [
'open' => $curChar,
'close' => $rule['end'],
+ 'savedPrefix' => $savedPrefix,
'count' => $count,
- 'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ),
+ 'lineStart' => $lineStart,
];
$stack->push( $piece );
$accum =& $stack->getAccum();
- extract( $stack->getFlags() );
+ $stackFlags = $stack->getFlags();
+ if ( isset( $stackFlags['findEquals'] ) ) {
+ $findEquals = $stackFlags['findEquals'];
+ }
+ if ( isset( $stackFlags['findPipe'] ) ) {
+ $findPipe = $stackFlags['findPipe'];
+ }
+ if ( isset( $stackFlags['inHeading'] ) ) {
+ $inHeading = $stackFlags['inHeading'];
+ }
} else {
# Add literal brace(s)
- self::addLiteral( $accum, str_repeat( $curChar, $count ) );
+ self::addLiteral( $accum, $savedPrefix . str_repeat( $curChar, $count ) );
}
$i += $count;
} elseif ( $found == 'close' ) {
@@ -604,10 +636,6 @@ class Preprocessor_Hash extends Preprocessor {
# check for maximum matching characters (if there are 5 closing
# characters, we will probably need only 3 - depending on the rules)
$rule = $this->rules[$piece->open];
- if ( $piece->close === '}-' && $piece->count > 2 ) {
- # tweak for -{..{{ }}..}-
- $rule = $this->rules['{'];
- }
if ( $count > $rule['max'] ) {
# The specified maximum exists in the callback array, unless the caller
# has made an error
@@ -646,7 +674,9 @@ class Preprocessor_Hash extends Preprocessor {
# The invocation is at the start of the line if lineStart is set in
# the stack, and all opening brackets are used up.
- if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
+ if ( $maxCount == $matchingCount &&
+ !empty( $piece->lineStart ) &&
+ strlen( $piece->savedPrefix ) == 0 ) {
$children[] = [ '@lineStart', [ 1 ] ];
}
$titleNode = [ 'title', $titleAccum ];
@@ -685,17 +715,35 @@ class Preprocessor_Hash extends Preprocessor {
if ( $piece->count >= $min ) {
$stack->push( $piece );
$accum =& $stack->getAccum();
+ } elseif ( $piece->count == 1 && $piece->open === '{' && $piece->savedPrefix === '-' ) {
+ $piece->savedPrefix = '';
+ $piece->open = '-{';
+ $piece->count = 2;
+ $piece->close = $this->rules[$piece->open]['end'];
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
} else {
$s = substr( $piece->open, 0, -1 );
$s .= str_repeat(
substr( $piece->open, -1 ),
$piece->count - strlen( $s )
);
- self::addLiteral( $accum, $s );
+ self::addLiteral( $accum, $piece->savedPrefix . $s );
}
+ } elseif ( $piece->savedPrefix !== '' ) {
+ self::addLiteral( $accum, $piece->savedPrefix );
}
- extract( $stack->getFlags() );
+ $stackFlags = $stack->getFlags();
+ if ( isset( $stackFlags['findEquals'] ) ) {
+ $findEquals = $stackFlags['findEquals'];
+ }
+ if ( isset( $stackFlags['findPipe'] ) ) {
+ $findPipe = $stackFlags['findPipe'];
+ }
+ if ( isset( $stackFlags['inHeading'] ) ) {
+ $inHeading = $stackFlags['inHeading'];
+ }
# Add XML element to the enclosing accumulator
array_splice( $accum, count( $accum ), 0, $element );
@@ -709,9 +757,6 @@ class Preprocessor_Hash extends Preprocessor {
$accum[] = [ 'equals', [ '=' ] ];
$stack->getCurrentPart()->eqpos = count( $accum ) - 1;
++$i;
- } elseif ( $found == 'dash' ) {
- self::addLiteral( $accum, '-' );
- ++$i;
}
}
@@ -753,12 +798,11 @@ class Preprocessor_Hash extends Preprocessor {
* Stack class to help Preprocessor::preprocessToObj()
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPDStack_Hash extends PPDStack {
- // @codingStandardsIgnoreEnd
public function __construct() {
- $this->elementClass = 'PPDStackElement_Hash';
+ $this->elementClass = PPDStackElement_Hash::class;
parent::__construct();
$this->rootAccum = [];
}
@@ -767,12 +811,11 @@ class PPDStack_Hash extends PPDStack {
/**
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPDStackElement_Hash extends PPDStackElement {
- // @codingStandardsIgnoreEnd
public function __construct( $data = [] ) {
- $this->partClass = 'PPDPart_Hash';
+ $this->partClass = PPDPart_Hash::class;
parent::__construct( $data );
}
@@ -784,7 +827,7 @@ class PPDStackElement_Hash extends PPDStackElement {
*/
public function breakSyntax( $openingCount = false ) {
if ( $this->open == "\n" ) {
- $accum = $this->parts[0]->out;
+ $accum = array_merge( [ $this->savedPrefix ], $this->parts[0]->out );
} else {
if ( $openingCount === false ) {
$openingCount = $this->count;
@@ -794,7 +837,7 @@ class PPDStackElement_Hash extends PPDStackElement {
substr( $this->open, -1 ),
$openingCount - strlen( $s )
);
- $accum = [ $s ];
+ $accum = [ $this->savedPrefix . $s ];
$lastIndex = 0;
$first = true;
foreach ( $this->parts as $part ) {
@@ -821,9 +864,8 @@ class PPDStackElement_Hash extends PPDStackElement {
/**
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPDPart_Hash extends PPDPart {
- // @codingStandardsIgnoreEnd
public function __construct( $out = '' ) {
if ( $out !== '' ) {
@@ -839,9 +881,8 @@ class PPDPart_Hash extends PPDPart {
* An expansion frame, used as a context to expand the result of preprocessToObj()
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPFrame_Hash implements PPFrame {
- // @codingStandardsIgnoreEnd
/**
* @var Parser
@@ -1439,9 +1480,8 @@ class PPFrame_Hash implements PPFrame {
* Expansion frame with template arguments
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPTemplateFrame_Hash extends PPFrame_Hash {
- // @codingStandardsIgnoreEnd
public $numberedArgs, $namedArgs, $parent;
public $numberedExpansionCache, $namedExpansionCache;
@@ -1622,9 +1662,8 @@ class PPTemplateFrame_Hash extends PPFrame_Hash {
* Expansion frame with custom arguments
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPCustomFrame_Hash extends PPFrame_Hash {
- // @codingStandardsIgnoreEnd
public $args;
@@ -1675,9 +1714,8 @@ class PPCustomFrame_Hash extends PPFrame_Hash {
/**
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPNode_Hash_Tree implements PPNode {
- // @codingStandardsIgnoreEnd
public $name;
@@ -1738,12 +1776,12 @@ class PPNode_Hash_Tree implements PPNode {
$descriptor = $store[$index];
if ( is_string( $descriptor ) ) {
- $class = 'PPNode_Hash_Text';
+ $class = PPNode_Hash_Text::class;
} elseif ( is_array( $descriptor ) ) {
if ( $descriptor[self::NAME][0] === '@' ) {
- $class = 'PPNode_Hash_Attr';
+ $class = PPNode_Hash_Attr::class;
} else {
- $class = 'PPNode_Hash_Tree';
+ $class = self::class;
}
} else {
throw new MWException( __METHOD__.': invalid node descriptor' );
@@ -1922,18 +1960,18 @@ class PPNode_Hash_Tree implements PPNode {
continue;
}
switch ( $child[self::NAME] ) {
- case 'name':
- $bits['name'] = new self( $children, $i );
- break;
- case 'attr':
- $bits['attr'] = new self( $children, $i );
- break;
- case 'inner':
- $bits['inner'] = new self( $children, $i );
- break;
- case 'close':
- $bits['close'] = new self( $children, $i );
- break;
+ case 'name':
+ $bits['name'] = new self( $children, $i );
+ break;
+ case 'attr':
+ $bits['attr'] = new self( $children, $i );
+ break;
+ case 'inner':
+ $bits['inner'] = new self( $children, $i );
+ break;
+ case 'close':
+ $bits['close'] = new self( $children, $i );
+ break;
}
}
if ( !isset( $bits['name'] ) ) {
@@ -2001,15 +2039,15 @@ class PPNode_Hash_Tree implements PPNode {
continue;
}
switch ( $child[self::NAME] ) {
- case 'title':
- $bits['title'] = new self( $children, $i );
- break;
- case 'part':
- $parts[] = new self( $children, $i );
- break;
- case '@lineStart':
- $bits['lineStart'] = '1';
- break;
+ case 'title':
+ $bits['title'] = new self( $children, $i );
+ break;
+ case 'part':
+ $parts[] = new self( $children, $i );
+ break;
+ case '@lineStart':
+ $bits['lineStart'] = '1';
+ break;
}
}
if ( !isset( $bits['title'] ) ) {
@@ -2023,9 +2061,8 @@ class PPNode_Hash_Tree implements PPNode {
/**
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPNode_Hash_Text implements PPNode {
- // @codingStandardsIgnoreEnd
public $value;
private $store, $index;
@@ -2094,9 +2131,8 @@ class PPNode_Hash_Text implements PPNode {
/**
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPNode_Hash_Array implements PPNode {
- // @codingStandardsIgnoreEnd
public $value;
@@ -2152,9 +2188,8 @@ class PPNode_Hash_Array implements PPNode {
/**
* @ingroup Parser
*/
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPNode_Hash_Attr implements PPNode {
- // @codingStandardsIgnoreEnd
public $name, $value;
private $store, $index;
diff --git a/www/wiki/includes/parser/RemexStripTagHandler.php b/www/wiki/includes/parser/RemexStripTagHandler.php
new file mode 100644
index 00000000..2839147d
--- /dev/null
+++ b/www/wiki/includes/parser/RemexStripTagHandler.php
@@ -0,0 +1,40 @@
+<?php
+
+use RemexHtml\Tokenizer\Attributes;
+use RemexHtml\Tokenizer\TokenHandler;
+use RemexHtml\Tokenizer\Tokenizer;
+
+/**
+ * @internal
+ */
+class RemexStripTagHandler implements TokenHandler {
+ private $text = '';
+ public function getResult() {
+ return $this->text;
+ }
+
+ function startDocument( Tokenizer $t, $fns, $fn ) {
+ // Do nothing.
+ }
+ function endDocument( $pos ) {
+ // Do nothing.
+ }
+ function error( $text, $pos ) {
+ // Do nothing.
+ }
+ function characters( $text, $start, $length, $sourceStart, $sourceLength ) {
+ $this->text .= substr( $text, $start, $length );
+ }
+ function startTag( $name, Attributes $attrs, $selfClose, $sourceStart, $sourceLength ) {
+ // Do nothing.
+ }
+ function endTag( $name, $sourceStart, $sourceLength ) {
+ // Do nothing.
+ }
+ function doctype( $name, $public, $system, $quirks, $sourceStart, $sourceLength ) {
+ // Do nothing.
+ }
+ function comment( $text, $sourceStart, $sourceLength ) {
+ // Do nothing.
+ }
+}
diff --git a/www/wiki/includes/Sanitizer.php b/www/wiki/includes/parser/Sanitizer.php
index b904a252..0b3a07b0 100644
--- a/www/wiki/includes/Sanitizer.php
+++ b/www/wiki/includes/parser/Sanitizer.php
@@ -477,7 +477,16 @@ class Sanitizer {
public static function removeHTMLtags( $text, $processCallback = null,
$args = [], $extratags = [], $removetags = [], $warnCallback = null
) {
- extract( self::getRecognizedTagData( $extratags, $removetags ) );
+ $tagData = self::getRecognizedTagData( $extratags, $removetags );
+ $htmlpairs = $tagData['htmlpairs'];
+ $htmlsingle = $tagData['htmlsingle'];
+ $htmlsingleonly = $tagData['htmlsingleonly'];
+ $htmlnest = $tagData['htmlnest'];
+ $tabletags = $tagData['tabletags'];
+ $htmllist = $tagData['htmllist'];
+ $listtags = $tagData['listtags'];
+ $htmlsingleallowed = $tagData['htmlsingleallowed'];
+ $htmlelements = $tagData['htmlelements'];
# Remove HTML comments
$text = self::removeHTMLcomments( $text );
@@ -506,9 +515,9 @@ class Sanitizer {
$badtag = true;
} elseif ( $slash ) {
# Closing a tag... is it the one we just opened?
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ot = array_pop( $tagstack );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $ot != $t ) {
if ( isset( $htmlsingleallowed[$ot] ) ) {
@@ -516,32 +525,32 @@ class Sanitizer {
# and see if we find a match below them
$optstack = [];
array_push( $optstack, $ot );
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ot = array_pop( $tagstack );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
while ( $ot != $t && isset( $htmlsingleallowed[$ot] ) ) {
array_push( $optstack, $ot );
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ot = array_pop( $tagstack );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
if ( $t != $ot ) {
# No match. Push the optional elements back again
$badtag = true;
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ot = array_pop( $optstack );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
while ( $ot ) {
array_push( $tagstack, $ot );
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$ot = array_pop( $optstack );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
}
} else {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
array_push( $tagstack, $ot );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
# <li> can be nested in <ul> or <ol>, skip those cases:
if ( !isset( $htmllist[$ot] ) || !isset( $listtags[$t] ) ) {
@@ -824,7 +833,7 @@ class Sanitizer {
|| $attribute === 'aria-labelledby'
|| $attribute === 'aria-owns'
) {
- $value = self::escapeIdReferenceList( $value, 'noninitial' );
+ $value = self::escapeIdReferenceList( $value );
}
// RDFa and microdata properties allow URLs, URIs and/or CURIs.
@@ -1041,9 +1050,11 @@ class Sanitizer {
| -o-link\s*:
| -o-link-source\s*:
| -o-replace\s*:
+ | url\s*\(
| image\s*\(
| image-set\s*\(
| attr\s*\([^)]+[\s,]+url
+ | var\s*\(
!ix', $value ) ) {
return '/* insecure input */';
}
@@ -1149,6 +1160,7 @@ class Sanitizer {
'{' => '&#123;',
'}' => '&#125;', // prevent unpaired language conversion syntax
'[' => '&#91;',
+ ']' => '&#93;',
"''" => '&#39;&#39;',
'ISBN' => '&#73;SBN',
'RFC' => '&#82;FC',
@@ -1160,7 +1172,9 @@ class Sanitizer {
# Stupid hack
$encValue = preg_replace_callback(
'/((?i)' . wfUrlProtocols() . ')/',
- [ 'Sanitizer', 'armorLinksCallback' ],
+ function ( $matches ) {
+ return str_replace( ':', '&#58;', $matches[1] );
+ },
$encValue );
return $encValue;
}
@@ -1343,7 +1357,7 @@ class Sanitizer {
* Given a string containing a space delimited list of ids, escape each id
* to match ids escaped by the escapeId() function.
*
- * @todo wfDeprecated() uses of $options in 1.31, remove completely in 1.32
+ * @todo remove $options completely in 1.32
*
* @since 1.27
*
@@ -1352,6 +1366,9 @@ class Sanitizer {
* @return string
*/
static function escapeIdReferenceList( $referenceString, $options = [] ) {
+ if ( $options ) {
+ wfDeprecated( __METHOD__ . ' with $options', '1.31' );
+ }
# Explode the space delimited list string into an array of tokens
$references = preg_split( '/\s+/', "{$referenceString}", -1, PREG_SPLIT_NO_EMPTY );
@@ -1403,15 +1420,6 @@ class Sanitizer {
}
/**
- * Regex replace callback for armoring links against further processing.
- * @param array $matches
- * @return string
- */
- private static function armorLinksCallback( $matches ) {
- return str_replace( ':', '&#58;', $matches[1] );
- }
-
- /**
* Return an associative array of attribute names and values from
* a partial tag string. Attribute names are forced to lowercase,
* character references are decoded to UTF-8 text.
@@ -1535,7 +1543,7 @@ class Sanitizer {
static function normalizeCharReferences( $text ) {
return preg_replace_callback(
self::CHAR_REFS_REGEX,
- [ 'Sanitizer', 'normalizeCharReferencesCallback' ],
+ [ self::class, 'normalizeCharReferencesCallback' ],
$text );
}
@@ -1635,7 +1643,7 @@ class Sanitizer {
public static function decodeCharReferences( $text ) {
return preg_replace_callback(
self::CHAR_REFS_REGEX,
- [ 'Sanitizer', 'decodeCharReferencesCallback' ],
+ [ self::class, 'decodeCharReferencesCallback' ],
$text );
}
@@ -1653,7 +1661,7 @@ class Sanitizer {
global $wgContLang;
$text = preg_replace_callback(
self::CHAR_REFS_REGEX,
- [ 'Sanitizer', 'decodeCharReferencesCallback' ],
+ [ self::class, 'decodeCharReferencesCallback' ],
$text,
-1, //limit
$count
@@ -1963,17 +1971,22 @@ class Sanitizer {
* Warning: this return value must be further escaped for literal
* inclusion in HTML output as of 1.10!
*
- * @param string $text HTML fragment
+ * @param string $html HTML fragment
* @return string
*/
- static function stripAllTags( $text ) {
- # Actual <tags>
- $text = StringUtils::delimiterReplace( '<', '>', '', $text );
+ static function stripAllTags( $html ) {
+ // Use RemexHtml to tokenize $html and extract the text
+ $handler = new RemexStripTagHandler;
+ $tokenizer = new RemexHtml\Tokenizer\Tokenizer( $handler, $html, [
+ 'ignoreErrors' => true,
+ // don't ignore char refs, we want them to be decoded
+ 'ignoreNulls' => true,
+ 'skipPreprocess' => true,
+ ] );
+ $tokenizer->execute();
+ $text = $handler->getResult();
- # Normalize &entities and whitespace
- $text = self::decodeCharReferences( $text );
$text = self::normalizeWhitespace( $text );
-
return $text;
}
diff --git a/www/wiki/includes/parser/StripState.php b/www/wiki/includes/parser/StripState.php
index 4ed176ce..855ce1d5 100644
--- a/www/wiki/includes/parser/StripState.php
+++ b/www/wiki/includes/parser/StripState.php
@@ -26,32 +26,38 @@
* @ingroup Parser
*/
class StripState {
- protected $prefix;
protected $data;
protected $regex;
- protected $tempType, $tempMergePrefix;
+ protected $parser;
+
protected $circularRefGuard;
- protected $recursionLevel = 0;
+ protected $depth = 0;
+ protected $highestDepth = 0;
+ protected $expandSize = 0;
- const UNSTRIP_RECURSION_LIMIT = 20;
+ protected $depthLimit = 20;
+ protected $sizeLimit = 5000000;
/**
- * @param string|null $prefix
- * @since 1.26 The prefix argument should be omitted, as the strip marker
- * prefix string is now a constant.
+ * @param Parser|null $parser
+ * @param array $options
*/
- public function __construct( $prefix = null ) {
- if ( $prefix !== null ) {
- wfDeprecated( __METHOD__ . ' with called with $prefix argument' .
- ' (call with no arguments instead)', '1.26' );
- }
+ public function __construct( Parser $parser = null, $options = [] ) {
$this->data = [
'nowiki' => [],
'general' => []
];
$this->regex = '/' . Parser::MARKER_PREFIX . "([^\x7f<>&'\"]+)" . Parser::MARKER_SUFFIX . '/';
$this->circularRefGuard = [];
+ $this->parser = $parser;
+
+ if ( isset( $options['depthLimit'] ) ) {
+ $this->depthLimit = $options['depthLimit'];
+ }
+ if ( isset( $options['sizeLimit'] ) ) {
+ $this->sizeLimit = $options['sizeLimit'];
+ }
}
/**
@@ -122,56 +128,109 @@ class StripState {
return $text;
}
- $oldType = $this->tempType;
- $this->tempType = $type;
- $text = preg_replace_callback( $this->regex, [ $this, 'unstripCallback' ], $text );
- $this->tempType = $oldType;
+ $callback = function ( $m ) use ( $type ) {
+ $marker = $m[1];
+ if ( isset( $this->data[$type][$marker] ) ) {
+ if ( isset( $this->circularRefGuard[$marker] ) ) {
+ return $this->getWarning( 'parser-unstrip-loop-warning' );
+ }
+
+ if ( $this->depth > $this->highestDepth ) {
+ $this->highestDepth = $this->depth;
+ }
+ if ( $this->depth >= $this->depthLimit ) {
+ return $this->getLimitationWarning( 'unstrip-depth', $this->depthLimit );
+ }
+
+ $value = $this->data[$type][$marker];
+ if ( $value instanceof Closure ) {
+ $value = $value();
+ }
+
+ $this->expandSize += strlen( $value );
+ if ( $this->expandSize > $this->sizeLimit ) {
+ return $this->getLimitationWarning( 'unstrip-size', $this->sizeLimit );
+ }
+
+ $this->circularRefGuard[$marker] = true;
+ $this->depth++;
+ $ret = $this->unstripType( $type, $value );
+ $this->depth--;
+ unset( $this->circularRefGuard[$marker] );
+
+ return $ret;
+ } else {
+ return $m[0];
+ }
+ };
+
+ $text = preg_replace_callback( $this->regex, $callback, $text );
return $text;
}
/**
- * @param array $m
- * @return array
+ * Get warning HTML and register a limitation warning with the parser
+ *
+ * @param string $type
+ * @param int $max
+ * @return string
*/
- protected function unstripCallback( $m ) {
- $marker = $m[1];
- if ( isset( $this->data[$this->tempType][$marker] ) ) {
- if ( isset( $this->circularRefGuard[$marker] ) ) {
- return '<span class="error">'
- . wfMessage( 'parser-unstrip-loop-warning' )->inContentLanguage()->text()
- . '</span>';
- }
- if ( $this->recursionLevel >= self::UNSTRIP_RECURSION_LIMIT ) {
- return '<span class="error">' .
- wfMessage( 'parser-unstrip-recursion-limit' )
- ->numParams( self::UNSTRIP_RECURSION_LIMIT )->inContentLanguage()->text() .
- '</span>';
- }
- $this->circularRefGuard[$marker] = true;
- $this->recursionLevel++;
- $value = $this->data[$this->tempType][$marker];
- if ( $value instanceof Closure ) {
- $value = $value();
- }
- $ret = $this->unstripType( $this->tempType, $value );
- $this->recursionLevel--;
- unset( $this->circularRefGuard[$marker] );
- return $ret;
- } else {
- return $m[0];
+ private function getLimitationWarning( $type, $max = '' ) {
+ if ( $this->parser ) {
+ $this->parser->limitationWarn( $type, $max );
}
+ return $this->getWarning( "$type-warning", $max );
+ }
+
+ /**
+ * Get warning HTML
+ *
+ * @param string $message
+ * @param int $max
+ * @return string
+ */
+ private function getWarning( $message, $max = '' ) {
+ return '<span class="error">' .
+ wfMessage( $message )
+ ->numParams( $max )->inContentLanguage()->text() .
+ '</span>';
+ }
+
+ /**
+ * Get an array of parameters to pass to ParserOutput::setLimitReportData()
+ *
+ * @internal Should only be called by Parser
+ * @return array
+ */
+ public function getLimitReport() {
+ return [
+ [ 'limitreport-unstrip-depth',
+ [
+ $this->highestDepth,
+ $this->depthLimit
+ ],
+ ],
+ [ 'limitreport-unstrip-size',
+ [
+ $this->expandSize,
+ $this->sizeLimit
+ ],
+ ]
+ ];
}
/**
* Get a StripState object which is sufficient to unstrip the given text.
* It will contain the minimum subset of strip items necessary.
*
+ * @deprecated since 1.31
* @param string $text
- *
* @return StripState
*/
public function getSubState( $text ) {
- $subState = new StripState();
+ wfDeprecated( __METHOD__, '1.31' );
+
+ $subState = new StripState;
$pos = 0;
while ( true ) {
$startPos = strpos( $text, Parser::MARKER_PREFIX, $pos );
@@ -202,11 +261,14 @@ class StripState {
* will not be preserved. The strings in the $texts array will have their
* strip markers rewritten, the resulting array of strings will be returned.
*
+ * @deprecated since 1.31
* @param StripState $otherState
* @param array $texts
* @return array
*/
public function merge( $otherState, $texts ) {
+ wfDeprecated( __METHOD__, '1.31' );
+
$mergePrefix = wfRandomString( 16 );
foreach ( $otherState->data as $type => $items ) {
@@ -215,25 +277,18 @@ class StripState {
}
}
- $this->tempMergePrefix = $mergePrefix;
- $texts = preg_replace_callback( $otherState->regex, [ $this, 'mergeCallback' ], $texts );
- $this->tempMergePrefix = null;
+ $callback = function ( $m ) use ( $mergePrefix ) {
+ $key = $m[1];
+ return Parser::MARKER_PREFIX . $mergePrefix . '-' . $key . Parser::MARKER_SUFFIX;
+ };
+ $texts = preg_replace_callback( $otherState->regex, $callback, $texts );
return $texts;
}
/**
- * @param array $m
- * @return string
- */
- protected function mergeCallback( $m ) {
- $key = $m[1];
- return Parser::MARKER_PREFIX . $this->tempMergePrefix . '-' . $key . Parser::MARKER_SUFFIX;
- }
-
- /**
* Remove any strip markers found in the given text.
*
- * @param string $text Input string
+ * @param string $text
* @return string
*/
public function killMarkers( $text ) {
diff --git a/www/wiki/includes/password/PasswordFactory.php b/www/wiki/includes/password/PasswordFactory.php
index 3383fe38..bc37b484 100644
--- a/www/wiki/includes/password/PasswordFactory.php
+++ b/www/wiki/includes/password/PasswordFactory.php
@@ -41,7 +41,7 @@ final class PasswordFactory {
* @see Setup.php
*/
private $types = [
- '' => [ 'type' => '', 'class' => 'InvalidPassword' ],
+ '' => [ 'type' => '', 'class' => InvalidPassword::class ],
];
/**
diff --git a/www/wiki/includes/password/PasswordPolicyChecks.php b/www/wiki/includes/password/PasswordPolicyChecks.php
index b3776bd8..502f1e02 100644
--- a/www/wiki/includes/password/PasswordPolicyChecks.php
+++ b/www/wiki/includes/password/PasswordPolicyChecks.php
@@ -20,7 +20,7 @@
* @file
*/
-use \Cdb\Reader as CdbReader;
+use Cdb\Reader as CdbReader;
/**
* Functions to check passwords against a policy requirement
diff --git a/www/wiki/includes/poolcounter/PoolCounter.php b/www/wiki/includes/poolcounter/PoolCounter.php
index bd7072ab..ba0b4cb3 100644
--- a/www/wiki/includes/poolcounter/PoolCounter.php
+++ b/www/wiki/includes/poolcounter/PoolCounter.php
@@ -209,9 +209,8 @@ abstract class PoolCounter {
}
}
-// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PoolCounter_Stub extends PoolCounter {
- // @codingStandardsIgnoreEnd
public function __construct() {
/* No parameters needed */
diff --git a/www/wiki/includes/poolcounter/PoolCounterRedis.php b/www/wiki/includes/poolcounter/PoolCounterRedis.php
index 65ea8333..9515f25d 100644
--- a/www/wiki/includes/poolcounter/PoolCounterRedis.php
+++ b/www/wiki/includes/poolcounter/PoolCounterRedis.php
@@ -152,7 +152,7 @@ class PoolCounterRedis extends PoolCounter {
}
$conn = $status->value;
- // @codingStandardsIgnoreStart Generic.Files.LineLength
+ // phpcs:disable Generic.Files.LineLength
static $script =
/** @lang Lua */
<<<LUA
@@ -191,7 +191,7 @@ class PoolCounterRedis extends PoolCounter {
end
return 1
LUA;
- // @codingStandardsIgnoreEnd
+ // phpcs:enable
try {
$conn->luaEval( $script,
diff --git a/www/wiki/includes/poolcounter/PoolWorkArticleView.php b/www/wiki/includes/poolcounter/PoolWorkArticleView.php
index 17b62d77..7b888ab5 100644
--- a/www/wiki/includes/poolcounter/PoolWorkArticleView.php
+++ b/www/wiki/includes/poolcounter/PoolWorkArticleView.php
@@ -79,7 +79,7 @@ class PoolWorkArticleView extends PoolCounterWork {
/**
* Get the ParserOutput from this object, or false in case of failure
*
- * @return ParserOutput
+ * @return ParserOutput|bool
*/
public function getParserOutput() {
return $this->parserOutput;
diff --git a/www/wiki/includes/preferences/DefaultPreferencesFactory.php b/www/wiki/includes/preferences/DefaultPreferencesFactory.php
new file mode 100644
index 00000000..6eceb84b
--- /dev/null
+++ b/www/wiki/includes/preferences/DefaultPreferencesFactory.php
@@ -0,0 +1,1741 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Preferences;
+
+use CentralIdLookup;
+use Config;
+use DateTime;
+use DateTimeZone;
+use Exception;
+use Hooks;
+use Html;
+use HtmlArmor;
+use HTMLForm;
+use HTMLFormField;
+use IContextSource;
+use Language;
+use LanguageCode;
+use LanguageConverter;
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\PasswordAuthenticationRequest;
+use MediaWiki\Linker\LinkRenderer;
+use MediaWiki\MediaWikiServices;
+use MessageLocalizer;
+use MWException;
+use MWNamespace;
+use MWTimestamp;
+use Parser;
+use ParserOptions;
+use PreferencesForm;
+use Psr\Log\LoggerAwareTrait;
+use Psr\Log\NullLogger;
+use Skin;
+use SpecialPage;
+use Status;
+use Title;
+use User;
+use UserGroupMembership;
+use Xml;
+
+/**
+ * This is the default implementation of PreferencesFactory.
+ */
+class DefaultPreferencesFactory implements PreferencesFactory {
+ use LoggerAwareTrait;
+
+ /** @var Config */
+ protected $config;
+
+ /** @var Language The wiki's content language, equivalent to $wgContLang. */
+ protected $contLang;
+
+ /** @var AuthManager */
+ protected $authManager;
+
+ /** @var LinkRenderer */
+ protected $linkRenderer;
+
+ /**
+ * @param Config $config
+ * @param Language $contLang
+ * @param AuthManager $authManager
+ * @param LinkRenderer $linkRenderer
+ */
+ public function __construct(
+ Config $config,
+ Language $contLang,
+ AuthManager $authManager,
+ LinkRenderer $linkRenderer
+ ) {
+ $this->config = $config;
+ $this->contLang = $contLang;
+ $this->authManager = $authManager;
+ $this->linkRenderer = $linkRenderer;
+ $this->logger = new NullLogger();
+ }
+
+ /**
+ * @return callable[]
+ */
+ protected function getSaveFilters() {
+ // Wrap intval() so that we can pass it multiple parameters and treat all filters the same.
+ $intvalFilter = function ( $value, $alldata ) {
+ return intval( $value );
+ };
+ return [
+ 'timecorrection' => [ $this, 'filterTimezoneInput' ],
+ 'rclimit' => $intvalFilter,
+ 'wllimit' => $intvalFilter,
+ 'searchlimit' => $intvalFilter,
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getSaveBlacklist() {
+ return [
+ 'realname',
+ 'emailaddress',
+ ];
+ }
+
+ /**
+ * @throws MWException
+ * @param User $user
+ * @param IContextSource $context
+ * @return array|null
+ */
+ public function getFormDescriptor( User $user, IContextSource $context ) {
+ $preferences = [];
+
+ $canIPUseHTTPS = wfCanIPUseHTTPS( $context->getRequest()->getIP() );
+ $this->profilePreferences( $user, $context, $preferences, $canIPUseHTTPS );
+ $this->skinPreferences( $user, $context, $preferences );
+ $this->datetimePreferences( $user, $context, $preferences );
+ $this->filesPreferences( $context, $preferences );
+ $this->renderingPreferences( $context, $preferences );
+ $this->editingPreferences( $user, $context, $preferences );
+ $this->rcPreferences( $user, $context, $preferences );
+ $this->watchlistPreferences( $user, $context, $preferences );
+ $this->searchPreferences( $preferences );
+
+ Hooks::run( 'GetPreferences', [ $user, &$preferences ] );
+
+ $this->loadPreferenceValues( $user, $context, $preferences );
+ $this->logger->debug( "Created form descriptor for user '{$user->getName()}'" );
+ return $preferences;
+ }
+
+ /**
+ * Loads existing values for a given array of preferences
+ * @throws MWException
+ * @param User $user
+ * @param IContextSource $context
+ * @param array &$defaultPreferences Array to load values for
+ * @return array|null
+ */
+ private function loadPreferenceValues(
+ User $user, IContextSource $context, &$defaultPreferences
+ ) {
+ # # Remove preferences that wikis don't want to use
+ foreach ( $this->config->get( 'HiddenPrefs' ) as $pref ) {
+ if ( isset( $defaultPreferences[$pref] ) ) {
+ unset( $defaultPreferences[$pref] );
+ }
+ }
+
+ # # Make sure that form fields have their parent set. See T43337.
+ $dummyForm = new HTMLForm( [], $context );
+
+ $disable = !$user->isAllowed( 'editmyoptions' );
+
+ $defaultOptions = User::getDefaultOptions();
+ # # Prod in defaults from the user
+ foreach ( $defaultPreferences as $name => &$info ) {
+ $prefFromUser = $this->getOptionFromUser( $name, $info, $user );
+ if ( $disable && !in_array( $name, $this->getSaveBlacklist() ) ) {
+ $info['disabled'] = 'disabled';
+ }
+ $field = HTMLForm::loadInputFromParameters( $name, $info, $dummyForm ); // For validation
+ $globalDefault = isset( $defaultOptions[$name] )
+ ? $defaultOptions[$name]
+ : null;
+
+ // If it validates, set it as the default
+ if ( isset( $info['default'] ) ) {
+ // Already set, no problem
+ continue;
+ } elseif ( !is_null( $prefFromUser ) && // Make sure we're not just pulling nothing
+ $field->validate( $prefFromUser, $user->getOptions() ) === true ) {
+ $info['default'] = $prefFromUser;
+ } elseif ( $field->validate( $globalDefault, $user->getOptions() ) === true ) {
+ $info['default'] = $globalDefault;
+ } else {
+ throw new MWException( "Global default '$globalDefault' is invalid for field $name" );
+ }
+ }
+
+ return $defaultPreferences;
+ }
+
+ /**
+ * Pull option from a user account. Handles stuff like array-type preferences.
+ *
+ * @param string $name
+ * @param array $info
+ * @param User $user
+ * @return array|string
+ */
+ protected function getOptionFromUser( $name, $info, User $user ) {
+ $val = $user->getOption( $name );
+
+ // Handling for multiselect preferences
+ if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
+ ( isset( $info['class'] ) && $info['class'] == \HTMLMultiSelectField::class ) ) {
+ $options = HTMLFormField::flattenOptions( $info['options'] );
+ $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
+ $val = [];
+
+ foreach ( $options as $value ) {
+ if ( $user->getOption( "$prefix$value" ) ) {
+ $val[] = $value;
+ }
+ }
+ }
+
+ // Handling for checkmatrix preferences
+ if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
+ ( isset( $info['class'] ) && $info['class'] == \HTMLCheckMatrix::class ) ) {
+ $columns = HTMLFormField::flattenOptions( $info['columns'] );
+ $rows = HTMLFormField::flattenOptions( $info['rows'] );
+ $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
+ $val = [];
+
+ foreach ( $columns as $column ) {
+ foreach ( $rows as $row ) {
+ if ( $user->getOption( "$prefix$column-$row" ) ) {
+ $val[] = "$column-$row";
+ }
+ }
+ }
+ }
+
+ return $val;
+ }
+
+ /**
+ * @todo Inject user Language instead of using context.
+ * @param User $user
+ * @param IContextSource $context
+ * @param array &$defaultPreferences
+ * @param bool $canIPUseHTTPS Whether the user's IP is likely to be able to access the wiki
+ * via HTTPS.
+ * @return void
+ */
+ protected function profilePreferences(
+ User $user, IContextSource $context, &$defaultPreferences, $canIPUseHTTPS
+ ) {
+ // retrieving user name for GENDER and misc.
+ $userName = $user->getName();
+
+ # # User info #####################################
+ // Information panel
+ $defaultPreferences['username'] = [
+ 'type' => 'info',
+ 'label-message' => [ 'username', $userName ],
+ 'default' => $userName,
+ 'section' => 'personal/info',
+ ];
+
+ $lang = $context->getLanguage();
+
+ # Get groups to which the user belongs
+ $userEffectiveGroups = $user->getEffectiveGroups();
+ $userGroupMemberships = $user->getGroupMemberships();
+ $userGroups = $userMembers = $userTempGroups = $userTempMembers = [];
+ foreach ( $userEffectiveGroups as $ueg ) {
+ if ( $ueg == '*' ) {
+ // Skip the default * group, seems useless here
+ continue;
+ }
+
+ if ( isset( $userGroupMemberships[$ueg] ) ) {
+ $groupStringOrObject = $userGroupMemberships[$ueg];
+ } else {
+ $groupStringOrObject = $ueg;
+ }
+
+ $userG = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html' );
+ $userM = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html',
+ $userName );
+
+ // Store expiring groups separately, so we can place them before non-expiring
+ // groups in the list. This is to avoid the ambiguity of something like
+ // "administrator, bureaucrat (until X date)" -- users might wonder whether the
+ // expiry date applies to both groups, or just the last one
+ if ( $groupStringOrObject instanceof UserGroupMembership &&
+ $groupStringOrObject->getExpiry()
+ ) {
+ $userTempGroups[] = $userG;
+ $userTempMembers[] = $userM;
+ } else {
+ $userGroups[] = $userG;
+ $userMembers[] = $userM;
+ }
+ }
+ sort( $userGroups );
+ sort( $userMembers );
+ sort( $userTempGroups );
+ sort( $userTempMembers );
+ $userGroups = array_merge( $userTempGroups, $userGroups );
+ $userMembers = array_merge( $userTempMembers, $userMembers );
+
+ $defaultPreferences['usergroups'] = [
+ 'type' => 'info',
+ 'label' => $context->msg( 'prefs-memberingroups' )->numParams(
+ count( $userGroups ) )->params( $userName )->parse(),
+ 'default' => $context->msg( 'prefs-memberingroups-type' )
+ ->rawParams( $lang->commaList( $userGroups ), $lang->commaList( $userMembers ) )
+ ->escaped(),
+ 'raw' => true,
+ 'section' => 'personal/info',
+ ];
+
+ $contribTitle = SpecialPage::getTitleFor( "Contributions", $userName );
+ $formattedEditCount = $lang->formatNum( $user->getEditCount() );
+ $editCount = $this->linkRenderer->makeLink( $contribTitle, $formattedEditCount );
+
+ $defaultPreferences['editcount'] = [
+ 'type' => 'info',
+ 'raw' => true,
+ 'label-message' => 'prefs-edits',
+ 'default' => $editCount,
+ 'section' => 'personal/info',
+ ];
+
+ if ( $user->getRegistration() ) {
+ $displayUser = $context->getUser();
+ $userRegistration = $user->getRegistration();
+ $defaultPreferences['registrationdate'] = [
+ 'type' => 'info',
+ 'label-message' => 'prefs-registration',
+ 'default' => $context->msg(
+ 'prefs-registration-date-time',
+ $lang->userTimeAndDate( $userRegistration, $displayUser ),
+ $lang->userDate( $userRegistration, $displayUser ),
+ $lang->userTime( $userRegistration, $displayUser )
+ )->parse(),
+ 'section' => 'personal/info',
+ ];
+ }
+
+ $canViewPrivateInfo = $user->isAllowed( 'viewmyprivateinfo' );
+ $canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' );
+
+ // Actually changeable stuff
+ $defaultPreferences['realname'] = [
+ // (not really "private", but still shouldn't be edited without permission)
+ 'type' => $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'realname' )
+ ? 'text' : 'info',
+ 'default' => $user->getRealName(),
+ 'section' => 'personal/info',
+ 'label-message' => 'yourrealname',
+ 'help-message' => 'prefs-help-realname',
+ ];
+
+ if ( $canEditPrivateInfo && $this->authManager->allowsAuthenticationDataChange(
+ new PasswordAuthenticationRequest(), false )->isGood()
+ ) {
+ $link = $this->linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ),
+ $context->msg( 'prefs-resetpass' )->text(), [],
+ [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
+ $defaultPreferences['password'] = [
+ 'type' => 'info',
+ 'raw' => true,
+ 'default' => $link,
+ 'label-message' => 'yourpassword',
+ 'section' => 'personal/info',
+ ];
+ }
+ // Only show prefershttps if secure login is turned on
+ if ( $this->config->get( 'SecureLogin' ) && $canIPUseHTTPS ) {
+ $defaultPreferences['prefershttps'] = [
+ 'type' => 'toggle',
+ 'label-message' => 'tog-prefershttps',
+ 'help-message' => 'prefs-help-prefershttps',
+ 'section' => 'personal/info'
+ ];
+ }
+
+ // Language
+ $languages = Language::fetchLanguageNames( null, 'mw' );
+ $languageCode = $this->config->get( 'LanguageCode' );
+ if ( !array_key_exists( $languageCode, $languages ) ) {
+ $languages[$languageCode] = $languageCode;
+ }
+ ksort( $languages );
+
+ $options = [];
+ foreach ( $languages as $code => $name ) {
+ $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
+ $options[$display] = $code;
+ }
+ $defaultPreferences['language'] = [
+ 'type' => 'select',
+ 'section' => 'personal/i18n',
+ 'options' => $options,
+ 'label-message' => 'yourlanguage',
+ ];
+
+ $defaultPreferences['gender'] = [
+ 'type' => 'radio',
+ 'section' => 'personal/i18n',
+ 'options' => [
+ $context->msg( 'parentheses' )
+ ->params( $context->msg( 'gender-unknown' )->plain() )
+ ->escaped() => 'unknown',
+ $context->msg( 'gender-female' )->escaped() => 'female',
+ $context->msg( 'gender-male' )->escaped() => 'male',
+ ],
+ 'label-message' => 'yourgender',
+ 'help-message' => 'prefs-help-gender',
+ ];
+
+ // see if there are multiple language variants to choose from
+ if ( !$this->config->get( 'DisableLangConversion' ) ) {
+ foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
+ if ( $langCode == $this->contLang->getCode() ) {
+ $variants = $this->contLang->getVariants();
+
+ if ( count( $variants ) <= 1 ) {
+ continue;
+ }
+
+ $variantArray = [];
+ foreach ( $variants as $v ) {
+ $v = str_replace( '_', '-', strtolower( $v ) );
+ $variantArray[$v] = $lang->getVariantname( $v, false );
+ }
+
+ $options = [];
+ foreach ( $variantArray as $code => $name ) {
+ $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
+ $options[$display] = $code;
+ }
+
+ $defaultPreferences['variant'] = [
+ 'label-message' => 'yourvariant',
+ 'type' => 'select',
+ 'options' => $options,
+ 'section' => 'personal/i18n',
+ 'help-message' => 'prefs-help-variant',
+ ];
+ } else {
+ $defaultPreferences["variant-$langCode"] = [
+ 'type' => 'api',
+ ];
+ }
+ }
+ }
+
+ // Stuff from Language::getExtraUserToggles()
+ // FIXME is this dead code? $extraUserToggles doesn't seem to be defined for any language
+ $toggles = $this->contLang->getExtraUserToggles();
+
+ foreach ( $toggles as $toggle ) {
+ $defaultPreferences[$toggle] = [
+ 'type' => 'toggle',
+ 'section' => 'personal/i18n',
+ 'label-message' => "tog-$toggle",
+ ];
+ }
+
+ // show a preview of the old signature first
+ $oldsigWikiText = MediaWikiServices::getInstance()->getParser()->preSaveTransform(
+ '~~~',
+ $context->getTitle(),
+ $user,
+ ParserOptions::newFromContext( $context )
+ );
+ $oldsigHTML = $context->getOutput()->parseInline( $oldsigWikiText, true, true );
+ $defaultPreferences['oldsig'] = [
+ 'type' => 'info',
+ 'raw' => true,
+ 'label-message' => 'tog-oldsig',
+ 'default' => $oldsigHTML,
+ 'section' => 'personal/signature',
+ ];
+ $defaultPreferences['nickname'] = [
+ 'type' => $this->authManager->allowsPropertyChange( 'nickname' ) ? 'text' : 'info',
+ 'maxlength' => $this->config->get( 'MaxSigChars' ),
+ 'label-message' => 'yournick',
+ 'validation-callback' => function ( $signature, $alldata, HTMLForm $form ) {
+ return $this->validateSignature( $signature, $alldata, $form );
+ },
+ 'section' => 'personal/signature',
+ 'filter-callback' => function ( $signature, array $alldata, HTMLForm $form ) {
+ return $this->cleanSignature( $signature, $alldata, $form );
+ },
+ ];
+ $defaultPreferences['fancysig'] = [
+ 'type' => 'toggle',
+ 'label-message' => 'tog-fancysig',
+ // show general help about signature at the bottom of the section
+ 'help-message' => 'prefs-help-signature',
+ 'section' => 'personal/signature'
+ ];
+
+ # # Email stuff
+
+ if ( $this->config->get( 'EnableEmail' ) ) {
+ if ( $canViewPrivateInfo ) {
+ $helpMessages[] = $this->config->get( 'EmailConfirmToEdit' )
+ ? 'prefs-help-email-required'
+ : 'prefs-help-email';
+
+ if ( $this->config->get( 'EnableUserEmail' ) ) {
+ // additional messages when users can send email to each other
+ $helpMessages[] = 'prefs-help-email-others';
+ }
+
+ $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
+ if ( $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'emailaddress' ) ) {
+ $link = $this->linkRenderer->makeLink(
+ SpecialPage::getTitleFor( 'ChangeEmail' ),
+ $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
+ [],
+ [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
+
+ $emailAddress .= $emailAddress == '' ? $link : (
+ $context->msg( 'word-separator' )->escaped()
+ . $context->msg( 'parentheses' )->rawParams( $link )->escaped()
+ );
+ }
+
+ $defaultPreferences['emailaddress'] = [
+ 'type' => 'info',
+ 'raw' => true,
+ 'default' => $emailAddress,
+ 'label-message' => 'youremail',
+ 'section' => 'personal/email',
+ 'help-messages' => $helpMessages,
+ # 'cssclass' chosen below
+ ];
+ }
+
+ $disableEmailPrefs = false;
+
+ if ( $this->config->get( 'EmailAuthentication' ) ) {
+ $emailauthenticationclass = 'mw-email-not-authenticated';
+ if ( $user->getEmail() ) {
+ if ( $user->getEmailAuthenticationTimestamp() ) {
+ // date and time are separate parameters to facilitate localisation.
+ // $time is kept for backward compat reasons.
+ // 'emailauthenticated' is also used in SpecialConfirmemail.php
+ $displayUser = $context->getUser();
+ $emailTimestamp = $user->getEmailAuthenticationTimestamp();
+ $time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
+ $d = $lang->userDate( $emailTimestamp, $displayUser );
+ $t = $lang->userTime( $emailTimestamp, $displayUser );
+ $emailauthenticated = $context->msg( 'emailauthenticated',
+ $time, $d, $t )->parse() . '<br />';
+ $disableEmailPrefs = false;
+ $emailauthenticationclass = 'mw-email-authenticated';
+ } else {
+ $disableEmailPrefs = true;
+ $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
+ $this->linkRenderer->makeKnownLink(
+ SpecialPage::getTitleFor( 'Confirmemail' ),
+ $context->msg( 'emailconfirmlink' )->text()
+ ) . '<br />';
+ $emailauthenticationclass = "mw-email-not-authenticated";
+ }
+ } else {
+ $disableEmailPrefs = true;
+ $emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
+ $emailauthenticationclass = 'mw-email-none';
+ }
+
+ if ( $canViewPrivateInfo ) {
+ $defaultPreferences['emailauthentication'] = [
+ 'type' => 'info',
+ 'raw' => true,
+ 'section' => 'personal/email',
+ 'label-message' => 'prefs-emailconfirm-label',
+ 'default' => $emailauthenticated,
+ # Apply the same CSS class used on the input to the message:
+ 'cssclass' => $emailauthenticationclass,
+ ];
+ }
+ }
+
+ if ( $this->config->get( 'EnableUserEmail' ) && $user->isAllowed( 'sendemail' ) ) {
+ $defaultPreferences['disablemail'] = [
+ 'id' => 'wpAllowEmail',
+ 'type' => 'toggle',
+ 'invert' => true,
+ 'section' => 'personal/email',
+ 'label-message' => 'allowemail',
+ 'disabled' => $disableEmailPrefs,
+ ];
+
+ $defaultPreferences['email-allow-new-users'] = [
+ 'id' => 'wpAllowEmailFromNewUsers',
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label-message' => 'email-allow-new-users-label',
+ 'disabled' => $disableEmailPrefs,
+ ];
+
+ $defaultPreferences['ccmeonemails'] = [
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label-message' => 'tog-ccmeonemails',
+ 'disabled' => $disableEmailPrefs,
+ ];
+
+ if ( $this->config->get( 'EnableUserEmailBlacklist' ) ) {
+ $lookup = CentralIdLookup::factory();
+ $ids = $user->getOption( 'email-blacklist', [] );
+ $names = $ids ? $lookup->namesFromCentralIds( $ids, $user ) : [];
+
+ $defaultPreferences['email-blacklist'] = [
+ 'type' => 'usersmultiselect',
+ 'label-message' => 'email-blacklist-label',
+ 'section' => 'personal/email',
+ 'default' => implode( "\n", $names ),
+ 'disabled' => $disableEmailPrefs,
+ ];
+ }
+ }
+
+ if ( $this->config->get( 'EnotifWatchlist' ) ) {
+ $defaultPreferences['enotifwatchlistpages'] = [
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label-message' => 'tog-enotifwatchlistpages',
+ 'disabled' => $disableEmailPrefs,
+ ];
+ }
+ if ( $this->config->get( 'EnotifUserTalk' ) ) {
+ $defaultPreferences['enotifusertalkpages'] = [
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label-message' => 'tog-enotifusertalkpages',
+ 'disabled' => $disableEmailPrefs,
+ ];
+ }
+ if ( $this->config->get( 'EnotifUserTalk' ) || $this->config->get( 'EnotifWatchlist' ) ) {
+ if ( $this->config->get( 'EnotifMinorEdits' ) ) {
+ $defaultPreferences['enotifminoredits'] = [
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label-message' => 'tog-enotifminoredits',
+ 'disabled' => $disableEmailPrefs,
+ ];
+ }
+
+ if ( $this->config->get( 'EnotifRevealEditorAddress' ) ) {
+ $defaultPreferences['enotifrevealaddr'] = [
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label-message' => 'tog-enotifrevealaddr',
+ 'disabled' => $disableEmailPrefs,
+ ];
+ }
+ }
+ }
+ }
+
+ /**
+ * @param User $user
+ * @param IContextSource $context
+ * @param array &$defaultPreferences
+ * @return void
+ */
+ protected function skinPreferences( User $user, IContextSource $context, &$defaultPreferences ) {
+ # # Skin #####################################
+
+ // Skin selector, if there is at least one valid skin
+ $skinOptions = $this->generateSkinOptions( $user, $context );
+ if ( $skinOptions ) {
+ $defaultPreferences['skin'] = [
+ 'type' => 'radio',
+ 'options' => $skinOptions,
+ 'section' => 'rendering/skin',
+ ];
+ }
+
+ $allowUserCss = $this->config->get( 'AllowUserCss' );
+ $allowUserJs = $this->config->get( 'AllowUserJs' );
+ # Create links to user CSS/JS pages for all skins
+ # This code is basically copied from generateSkinOptions(). It'd
+ # be nice to somehow merge this back in there to avoid redundancy.
+ if ( $allowUserCss || $allowUserJs ) {
+ $linkTools = [];
+ $userName = $user->getName();
+
+ if ( $allowUserCss ) {
+ $cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
+ $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
+ $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
+ }
+
+ if ( $allowUserJs ) {
+ $jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
+ $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
+ $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
+ }
+
+ $defaultPreferences['commoncssjs'] = [
+ 'type' => 'info',
+ 'raw' => true,
+ 'default' => $context->getLanguage()->pipeList( $linkTools ),
+ 'label-message' => 'prefs-common-config',
+ 'section' => 'rendering/skin',
+ ];
+ }
+ }
+
+ /**
+ * @param IContextSource $context
+ * @param array &$defaultPreferences
+ */
+ protected function filesPreferences( IContextSource $context, &$defaultPreferences ) {
+ # # Files #####################################
+ $defaultPreferences['imagesize'] = [
+ 'type' => 'select',
+ 'options' => $this->getImageSizes( $context ),
+ 'label-message' => 'imagemaxsize',
+ 'section' => 'rendering/files',
+ ];
+ $defaultPreferences['thumbsize'] = [
+ 'type' => 'select',
+ 'options' => $this->getThumbSizes( $context ),
+ 'label-message' => 'thumbsize',
+ 'section' => 'rendering/files',
+ ];
+ }
+
+ /**
+ * @param User $user
+ * @param IContextSource $context
+ * @param array &$defaultPreferences
+ * @return void
+ */
+ protected function datetimePreferences( $user, IContextSource $context, &$defaultPreferences ) {
+ # # Date and time #####################################
+ $dateOptions = $this->getDateOptions( $context );
+ if ( $dateOptions ) {
+ $defaultPreferences['date'] = [
+ 'type' => 'radio',
+ 'options' => $dateOptions,
+ 'section' => 'rendering/dateformat',
+ ];
+ }
+
+ // Info
+ $now = wfTimestampNow();
+ $lang = $context->getLanguage();
+ $nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
+ $lang->userTime( $now, $user ) );
+ $nowserver = $lang->userTime( $now, $user,
+ [ 'format' => false, 'timecorrection' => false ] ) .
+ Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
+
+ $defaultPreferences['nowserver'] = [
+ 'type' => 'info',
+ 'raw' => 1,
+ 'label-message' => 'servertime',
+ 'default' => $nowserver,
+ 'section' => 'rendering/timeoffset',
+ ];
+
+ $defaultPreferences['nowlocal'] = [
+ 'type' => 'info',
+ 'raw' => 1,
+ 'label-message' => 'localtime',
+ 'default' => $nowlocal,
+ 'section' => 'rendering/timeoffset',
+ ];
+
+ // Grab existing pref.
+ $tzOffset = $user->getOption( 'timecorrection' );
+ $tz = explode( '|', $tzOffset, 3 );
+
+ $tzOptions = $this->getTimezoneOptions( $context );
+
+ $tzSetting = $tzOffset;
+ if ( count( $tz ) > 1 && $tz[0] == 'ZoneInfo' &&
+ !in_array( $tzOffset, HTMLFormField::flattenOptions( $tzOptions ) )
+ ) {
+ // Timezone offset can vary with DST
+ try {
+ $userTZ = new DateTimeZone( $tz[2] );
+ $minDiff = floor( $userTZ->getOffset( new DateTime( 'now' ) ) / 60 );
+ $tzSetting = "ZoneInfo|$minDiff|{$tz[2]}";
+ } catch ( Exception $e ) {
+ // User has an invalid time zone set. Fall back to just using the offset
+ $tz[0] = 'Offset';
+ }
+ }
+ if ( count( $tz ) > 1 && $tz[0] == 'Offset' ) {
+ $minDiff = $tz[1];
+ $tzSetting = sprintf( '%+03d:%02d', floor( $minDiff / 60 ), abs( $minDiff ) % 60 );
+ }
+
+ $defaultPreferences['timecorrection'] = [
+ 'class' => \HTMLSelectOrOtherField::class,
+ 'label-message' => 'timezonelegend',
+ 'options' => $tzOptions,
+ 'default' => $tzSetting,
+ 'size' => 20,
+ 'section' => 'rendering/timeoffset',
+ ];
+ }
+
+ /**
+ * @param MessageLocalizer $l10n
+ * @param array &$defaultPreferences
+ */
+ protected function renderingPreferences( MessageLocalizer $l10n, &$defaultPreferences ) {
+ # # Diffs ####################################
+ $defaultPreferences['diffonly'] = [
+ 'type' => 'toggle',
+ 'section' => 'rendering/diffs',
+ 'label-message' => 'tog-diffonly',
+ ];
+ $defaultPreferences['norollbackdiff'] = [
+ 'type' => 'toggle',
+ 'section' => 'rendering/diffs',
+ 'label-message' => 'tog-norollbackdiff',
+ ];
+
+ # # Page Rendering ##############################
+ if ( $this->config->get( 'AllowUserCssPrefs' ) ) {
+ $defaultPreferences['underline'] = [
+ 'type' => 'select',
+ 'options' => [
+ $l10n->msg( 'underline-never' )->text() => 0,
+ $l10n->msg( 'underline-always' )->text() => 1,
+ $l10n->msg( 'underline-default' )->text() => 2,
+ ],
+ 'label-message' => 'tog-underline',
+ 'section' => 'rendering/advancedrendering',
+ ];
+ }
+
+ $stubThresholdValues = [ 50, 100, 500, 1000, 2000, 5000, 10000 ];
+ $stubThresholdOptions = [ $l10n->msg( 'stub-threshold-disabled' )->text() => 0 ];
+ foreach ( $stubThresholdValues as $value ) {
+ $stubThresholdOptions[$l10n->msg( 'size-bytes', $value )->text()] = $value;
+ }
+
+ $defaultPreferences['stubthreshold'] = [
+ 'type' => 'select',
+ 'section' => 'rendering/advancedrendering',
+ 'options' => $stubThresholdOptions,
+ // This is not a raw HTML message; label-raw is needed for the manual <a></a>
+ 'label-raw' => $l10n->msg( 'stub-threshold' )->rawParams(
+ '<a href="#" class="stub">' .
+ $l10n->msg( 'stub-threshold-sample-link' )->parse() .
+ '</a>' )->parse(),
+ ];
+
+ $defaultPreferences['showhiddencats'] = [
+ 'type' => 'toggle',
+ 'section' => 'rendering/advancedrendering',
+ 'label-message' => 'tog-showhiddencats'
+ ];
+
+ $defaultPreferences['numberheadings'] = [
+ 'type' => 'toggle',
+ 'section' => 'rendering/advancedrendering',
+ 'label-message' => 'tog-numberheadings',
+ ];
+ }
+
+ /**
+ * @param User $user
+ * @param MessageLocalizer $l10n
+ * @param array &$defaultPreferences
+ */
+ protected function editingPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
+ # # Editing #####################################
+ $defaultPreferences['editsectiononrightclick'] = [
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-editsectiononrightclick',
+ ];
+ $defaultPreferences['editondblclick'] = [
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-editondblclick',
+ ];
+
+ if ( $this->config->get( 'AllowUserCssPrefs' ) ) {
+ $defaultPreferences['editfont'] = [
+ 'type' => 'select',
+ 'section' => 'editing/editor',
+ 'label-message' => 'editfont-style',
+ 'options' => [
+ $l10n->msg( 'editfont-monospace' )->text() => 'monospace',
+ $l10n->msg( 'editfont-sansserif' )->text() => 'sans-serif',
+ $l10n->msg( 'editfont-serif' )->text() => 'serif',
+ ]
+ ];
+ }
+
+ if ( $user->isAllowed( 'minoredit' ) ) {
+ $defaultPreferences['minordefault'] = [
+ 'type' => 'toggle',
+ 'section' => 'editing/editor',
+ 'label-message' => 'tog-minordefault',
+ ];
+ }
+
+ $defaultPreferences['forceeditsummary'] = [
+ 'type' => 'toggle',
+ 'section' => 'editing/editor',
+ 'label-message' => 'tog-forceeditsummary',
+ ];
+ $defaultPreferences['useeditwarning'] = [
+ 'type' => 'toggle',
+ 'section' => 'editing/editor',
+ 'label-message' => 'tog-useeditwarning',
+ ];
+ $defaultPreferences['showtoolbar'] = [
+ 'type' => 'toggle',
+ 'section' => 'editing/editor',
+ 'label-message' => 'tog-showtoolbar',
+ ];
+
+ $defaultPreferences['previewonfirst'] = [
+ 'type' => 'toggle',
+ 'section' => 'editing/preview',
+ 'label-message' => 'tog-previewonfirst',
+ ];
+ $defaultPreferences['previewontop'] = [
+ 'type' => 'toggle',
+ 'section' => 'editing/preview',
+ 'label-message' => 'tog-previewontop',
+ ];
+ $defaultPreferences['uselivepreview'] = [
+ 'type' => 'toggle',
+ 'section' => 'editing/preview',
+ 'label-message' => 'tog-uselivepreview',
+ ];
+ }
+
+ /**
+ * @param User $user
+ * @param MessageLocalizer $l10n
+ * @param array &$defaultPreferences
+ */
+ protected function rcPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
+ $rcMaxAge = $this->config->get( 'RCMaxAge' );
+ # # RecentChanges #####################################
+ $defaultPreferences['rcdays'] = [
+ 'type' => 'float',
+ 'label-message' => 'recentchangesdays',
+ 'section' => 'rc/displayrc',
+ 'min' => 1,
+ 'max' => ceil( $rcMaxAge / ( 3600 * 24 ) ),
+ 'help' => $l10n->msg( 'recentchangesdays-max' )->numParams(
+ ceil( $rcMaxAge / ( 3600 * 24 ) ) )->escaped()
+ ];
+ $defaultPreferences['rclimit'] = [
+ 'type' => 'int',
+ 'min' => 0,
+ 'max' => 1000,
+ 'label-message' => 'recentchangescount',
+ 'help-message' => 'prefs-help-recentchangescount',
+ 'section' => 'rc/displayrc',
+ ];
+ $defaultPreferences['usenewrc'] = [
+ 'type' => 'toggle',
+ 'label-message' => 'tog-usenewrc',
+ 'section' => 'rc/advancedrc',
+ ];
+ $defaultPreferences['hideminor'] = [
+ 'type' => 'toggle',
+ 'label-message' => 'tog-hideminor',
+ 'section' => 'rc/advancedrc',
+ ];
+ $defaultPreferences['rcfilters-saved-queries'] = [
+ 'type' => 'api',
+ ];
+ $defaultPreferences['rcfilters-wl-saved-queries'] = [
+ 'type' => 'api',
+ ];
+ // Override RCFilters preferences for RecentChanges 'limit'
+ $defaultPreferences['rcfilters-limit'] = [
+ 'type' => 'api',
+ ];
+ $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [
+ 'type' => 'api',
+ ];
+ $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [
+ 'type' => 'api',
+ ];
+
+ if ( $this->config->get( 'RCWatchCategoryMembership' ) ) {
+ $defaultPreferences['hidecategorization'] = [
+ 'type' => 'toggle',
+ 'label-message' => 'tog-hidecategorization',
+ 'section' => 'rc/advancedrc',
+ ];
+ }
+
+ if ( $user->useRCPatrol() ) {
+ $defaultPreferences['hidepatrolled'] = [
+ 'type' => 'toggle',
+ 'section' => 'rc/advancedrc',
+ 'label-message' => 'tog-hidepatrolled',
+ ];
+ }
+
+ if ( $user->useNPPatrol() ) {
+ $defaultPreferences['newpageshidepatrolled'] = [
+ 'type' => 'toggle',
+ 'section' => 'rc/advancedrc',
+ 'label-message' => 'tog-newpageshidepatrolled',
+ ];
+ }
+
+ if ( $this->config->get( 'RCShowWatchingUsers' ) ) {
+ $defaultPreferences['shownumberswatching'] = [
+ 'type' => 'toggle',
+ 'section' => 'rc/advancedrc',
+ 'label-message' => 'tog-shownumberswatching',
+ ];
+ }
+
+ if ( $this->config->get( 'StructuredChangeFiltersShowPreference' ) ) {
+ $defaultPreferences['rcenhancedfilters-disable'] = [
+ 'type' => 'toggle',
+ 'section' => 'rc/opt-out',
+ 'label-message' => 'rcfilters-preference-label',
+ 'help-message' => 'rcfilters-preference-help',
+ ];
+ }
+ }
+
+ /**
+ * @param User $user
+ * @param IContextSource $context
+ * @param array &$defaultPreferences
+ */
+ protected function watchlistPreferences(
+ User $user, IContextSource $context, &$defaultPreferences
+ ) {
+ $watchlistdaysMax = ceil( $this->config->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
+
+ # # Watchlist #####################################
+ if ( $user->isAllowed( 'editmywatchlist' ) ) {
+ $editWatchlistLinks = [];
+ $editWatchlistModes = [
+ 'edit' => [ 'EditWatchlist', false ],
+ 'raw' => [ 'EditWatchlist', 'raw' ],
+ 'clear' => [ 'EditWatchlist', 'clear' ],
+ ];
+ foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) {
+ // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
+ $editWatchlistLinks[] = $this->linkRenderer->makeKnownLink(
+ SpecialPage::getTitleFor( $mode[0], $mode[1] ),
+ new HtmlArmor( $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() )
+ );
+ }
+
+ $defaultPreferences['editwatchlist'] = [
+ 'type' => 'info',
+ 'raw' => true,
+ 'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ),
+ 'label-message' => 'prefs-editwatchlist-label',
+ 'section' => 'watchlist/editwatchlist',
+ ];
+ }
+
+ $defaultPreferences['watchlistdays'] = [
+ 'type' => 'float',
+ 'min' => 0,
+ 'max' => $watchlistdaysMax,
+ 'section' => 'watchlist/displaywatchlist',
+ 'help' => $context->msg( 'prefs-watchlist-days-max' )->numParams(
+ $watchlistdaysMax )->escaped(),
+ 'label-message' => 'prefs-watchlist-days',
+ ];
+ $defaultPreferences['wllimit'] = [
+ 'type' => 'int',
+ 'min' => 0,
+ 'max' => 1000,
+ 'label-message' => 'prefs-watchlist-edits',
+ 'help' => $context->msg( 'prefs-watchlist-edits-max' )->escaped(),
+ 'section' => 'watchlist/displaywatchlist',
+ ];
+ $defaultPreferences['extendwatchlist'] = [
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-extendwatchlist',
+ ];
+ $defaultPreferences['watchlisthideminor'] = [
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthideminor',
+ ];
+ $defaultPreferences['watchlisthidebots'] = [
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthidebots',
+ ];
+ $defaultPreferences['watchlisthideown'] = [
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthideown',
+ ];
+ $defaultPreferences['watchlisthideanons'] = [
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthideanons',
+ ];
+ $defaultPreferences['watchlisthideliu'] = [
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthideliu',
+ ];
+
+ if ( !\SpecialWatchlist::checkStructuredFilterUiEnabled(
+ $this->config,
+ $user
+ ) ) {
+ $defaultPreferences['watchlistreloadautomatically'] = [
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlistreloadautomatically',
+ ];
+ }
+
+ $defaultPreferences['watchlistunwatchlinks'] = [
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlistunwatchlinks',
+ ];
+
+ if ( $this->config->get( 'RCWatchCategoryMembership' ) ) {
+ $defaultPreferences['watchlisthidecategorization'] = [
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthidecategorization',
+ ];
+ }
+
+ if ( $user->useRCPatrol() ) {
+ $defaultPreferences['watchlisthidepatrolled'] = [
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthidepatrolled',
+ ];
+ }
+
+ $watchTypes = [
+ 'edit' => 'watchdefault',
+ 'move' => 'watchmoves',
+ 'delete' => 'watchdeletion'
+ ];
+
+ // Kinda hacky
+ if ( $user->isAllowed( 'createpage' ) || $user->isAllowed( 'createtalk' ) ) {
+ $watchTypes['read'] = 'watchcreations';
+ }
+
+ if ( $user->isAllowed( 'rollback' ) ) {
+ $watchTypes['rollback'] = 'watchrollback';
+ }
+
+ if ( $user->isAllowed( 'upload' ) ) {
+ $watchTypes['upload'] = 'watchuploads';
+ }
+
+ foreach ( $watchTypes as $action => $pref ) {
+ if ( $user->isAllowed( $action ) ) {
+ // Messages:
+ // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
+ // tog-watchrollback
+ $defaultPreferences[$pref] = [
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => "tog-$pref",
+ ];
+ }
+ }
+
+ if ( $this->config->get( 'EnableAPI' ) ) {
+ $defaultPreferences['watchlisttoken'] = [
+ 'type' => 'api',
+ ];
+ $defaultPreferences['watchlisttoken-info'] = [
+ 'type' => 'info',
+ 'section' => 'watchlist/tokenwatchlist',
+ 'label-message' => 'prefs-watchlist-token',
+ 'default' => $user->getTokenFromOption( 'watchlisttoken' ),
+ 'help-message' => 'prefs-help-watchlist-token2',
+ ];
+ }
+ }
+
+ /**
+ * @param array &$defaultPreferences
+ */
+ protected function searchPreferences( &$defaultPreferences ) {
+ foreach ( MWNamespace::getValidNamespaces() as $n ) {
+ $defaultPreferences['searchNs' . $n] = [
+ 'type' => 'api',
+ ];
+ }
+ }
+
+ /**
+ * @param User $user The User object
+ * @param IContextSource $context
+ * @return array Text/links to display as key; $skinkey as value
+ */
+ protected function generateSkinOptions( User $user, IContextSource $context ) {
+ $ret = [];
+
+ $mptitle = Title::newMainPage();
+ $previewtext = $context->msg( 'skin-preview' )->escaped();
+
+ # Only show skins that aren't disabled in $wgSkipSkins
+ $validSkinNames = Skin::getAllowedSkins();
+
+ foreach ( $validSkinNames as $skinkey => &$skinname ) {
+ $msg = $context->msg( "skinname-{$skinkey}" );
+ if ( $msg->exists() ) {
+ $skinname = htmlspecialchars( $msg->text() );
+ }
+ }
+
+ $defaultSkin = $this->config->get( 'DefaultSkin' );
+ $allowUserCss = $this->config->get( 'AllowUserCss' );
+ $allowUserJs = $this->config->get( 'AllowUserJs' );
+
+ # Sort by the internal name, so that the ordering is the same for each display language,
+ # especially if some skin names are translated to use a different alphabet and some are not.
+ uksort( $validSkinNames, function ( $a, $b ) use ( $defaultSkin ) {
+ # Display the default first in the list by comparing it as lesser than any other.
+ if ( strcasecmp( $a, $defaultSkin ) === 0 ) {
+ return -1;
+ }
+ if ( strcasecmp( $b, $defaultSkin ) === 0 ) {
+ return 1;
+ }
+ return strcasecmp( $a, $b );
+ } );
+
+ $foundDefault = false;
+ foreach ( $validSkinNames as $skinkey => $sn ) {
+ $linkTools = [];
+
+ # Mark the default skin
+ if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
+ $linkTools[] = $context->msg( 'default' )->escaped();
+ $foundDefault = true;
+ }
+
+ # Create preview link
+ $mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
+ $linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
+
+ # Create links to user CSS/JS pages
+ if ( $allowUserCss ) {
+ $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
+ $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
+ $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
+ }
+
+ if ( $allowUserJs ) {
+ $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
+ $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
+ $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
+ }
+
+ $display = $sn . ' ' . $context->msg( 'parentheses' )
+ ->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
+ ->escaped();
+ $ret[$display] = $skinkey;
+ }
+
+ if ( !$foundDefault ) {
+ // If the default skin is not available, things are going to break horribly because the
+ // default value for skin selector will not be a valid value. Let's just not show it then.
+ return [];
+ }
+
+ return $ret;
+ }
+
+ /**
+ * @param IContextSource $context
+ * @return array
+ */
+ protected function getDateOptions( IContextSource $context ) {
+ $lang = $context->getLanguage();
+ $dateopts = $lang->getDatePreferences();
+
+ $ret = [];
+
+ if ( $dateopts ) {
+ if ( !in_array( 'default', $dateopts ) ) {
+ $dateopts[] = 'default'; // Make sure default is always valid T21237
+ }
+
+ // FIXME KLUGE: site default might not be valid for user language
+ global $wgDefaultUserOptions;
+ if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
+ $wgDefaultUserOptions['date'] = 'default';
+ }
+
+ $epoch = wfTimestampNow();
+ foreach ( $dateopts as $key ) {
+ if ( $key == 'default' ) {
+ $formatted = $context->msg( 'datedefault' )->escaped();
+ } else {
+ $formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
+ }
+ $ret[$formatted] = $key;
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * @param MessageLocalizer $l10n
+ * @return array
+ */
+ protected function getImageSizes( MessageLocalizer $l10n ) {
+ $ret = [];
+ $pixels = $l10n->msg( 'unit-pixel' )->text();
+
+ foreach ( $this->config->get( 'ImageLimits' ) as $index => $limits ) {
+ // Note: A left-to-right marker (\u200e) is inserted, see T144386
+ $display = "{$limits[0]}" . json_decode( '"\u200e"' ) . "×{$limits[1]}" . $pixels;
+ $ret[$display] = $index;
+ }
+
+ return $ret;
+ }
+
+ /**
+ * @param MessageLocalizer $l10n
+ * @return array
+ */
+ protected function getThumbSizes( MessageLocalizer $l10n ) {
+ $ret = [];
+ $pixels = $l10n->msg( 'unit-pixel' )->text();
+
+ foreach ( $this->config->get( 'ThumbLimits' ) as $index => $size ) {
+ $display = $size . $pixels;
+ $ret[$display] = $index;
+ }
+
+ return $ret;
+ }
+
+ /**
+ * @param string $signature
+ * @param array $alldata
+ * @param HTMLForm $form
+ * @return bool|string
+ */
+ protected function validateSignature( $signature, $alldata, HTMLForm $form ) {
+ $maxSigChars = $this->config->get( 'MaxSigChars' );
+ if ( mb_strlen( $signature ) > $maxSigChars ) {
+ return Xml::element( 'span', [ 'class' => 'error' ],
+ $form->msg( 'badsiglength' )->numParams( $maxSigChars )->text() );
+ } elseif ( isset( $alldata['fancysig'] ) &&
+ $alldata['fancysig'] &&
+ MediaWikiServices::getInstance()->getParser()->validateSig( $signature ) === false
+ ) {
+ return Xml::element(
+ 'span',
+ [ 'class' => 'error' ],
+ $form->msg( 'badsig' )->text()
+ );
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * @param string $signature
+ * @param array $alldata
+ * @param HTMLForm $form
+ * @return string
+ */
+ protected function cleanSignature( $signature, $alldata, HTMLForm $form ) {
+ $parser = MediaWikiServices::getInstance()->getParser();
+ if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
+ $signature = $parser->cleanSig( $signature );
+ } else {
+ // When no fancy sig used, make sure ~{3,5} get removed.
+ $signature = Parser::cleanSigInSig( $signature );
+ }
+
+ return $signature;
+ }
+
+ /**
+ * @param User $user
+ * @param IContextSource $context
+ * @param string $formClass
+ * @param array $remove Array of items to remove
+ * @return PreferencesForm|HTMLForm
+ */
+ public function getForm(
+ User $user,
+ IContextSource $context,
+ $formClass = PreferencesForm::class,
+ array $remove = []
+ ) {
+ $formDescriptor = $this->getFormDescriptor( $user, $context );
+ if ( count( $remove ) ) {
+ $removeKeys = array_flip( $remove );
+ $formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
+ }
+
+ // Remove type=api preferences. They are not intended for rendering in the form.
+ foreach ( $formDescriptor as $name => $info ) {
+ if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
+ unset( $formDescriptor[$name] );
+ }
+ }
+
+ /**
+ * @var $htmlForm PreferencesForm
+ */
+ $htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
+
+ $htmlForm->setModifiedUser( $user );
+ $htmlForm->setId( 'mw-prefs-form' );
+ $htmlForm->setAutocomplete( 'off' );
+ $htmlForm->setSubmitText( $context->msg( 'saveprefs' )->text() );
+ # Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
+ $htmlForm->setSubmitTooltip( 'preferences-save' );
+ $htmlForm->setSubmitID( 'prefcontrol' );
+ $htmlForm->setSubmitCallback( function ( array $formData, PreferencesForm $form ) {
+ return $this->submitForm( $formData, $form );
+ } );
+
+ return $htmlForm;
+ }
+
+ /**
+ * @param IContextSource $context
+ * @return array
+ */
+ protected function getTimezoneOptions( IContextSource $context ) {
+ $opt = [];
+
+ $localTZoffset = $this->config->get( 'LocalTZoffset' );
+ $timeZoneList = $this->getTimeZoneList( $context->getLanguage() );
+
+ $timestamp = MWTimestamp::getLocalInstance();
+ // Check that the LocalTZoffset is the same as the local time zone offset
+ if ( $localTZoffset == $timestamp->format( 'Z' ) / 60 ) {
+ $timezoneName = $timestamp->getTimezone()->getName();
+ // Localize timezone
+ if ( isset( $timeZoneList[$timezoneName] ) ) {
+ $timezoneName = $timeZoneList[$timezoneName]['name'];
+ }
+ $server_tz_msg = $context->msg(
+ 'timezoneuseserverdefault',
+ $timezoneName
+ )->text();
+ } else {
+ $tzstring = sprintf(
+ '%+03d:%02d',
+ floor( $localTZoffset / 60 ),
+ abs( $localTZoffset ) % 60
+ );
+ $server_tz_msg = $context->msg( 'timezoneuseserverdefault', $tzstring )->text();
+ }
+ $opt[$server_tz_msg] = "System|$localTZoffset";
+ $opt[$context->msg( 'timezoneuseoffset' )->text()] = 'other';
+ $opt[$context->msg( 'guesstimezone' )->text()] = 'guess';
+
+ foreach ( $timeZoneList as $timeZoneInfo ) {
+ $region = $timeZoneInfo['region'];
+ if ( !isset( $opt[$region] ) ) {
+ $opt[$region] = [];
+ }
+ $opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
+ }
+ return $opt;
+ }
+
+ /**
+ * @param string $tz
+ * @param array $alldata
+ * @return string
+ */
+ protected function filterTimezoneInput( $tz, array $alldata ) {
+ $data = explode( '|', $tz, 3 );
+ switch ( $data[0] ) {
+ case 'ZoneInfo':
+ $valid = false;
+
+ if ( count( $data ) === 3 ) {
+ // Make sure this timezone exists
+ try {
+ new DateTimeZone( $data[2] );
+ // If the constructor didn't throw, we know it's valid
+ $valid = true;
+ } catch ( Exception $e ) {
+ // Not a valid timezone
+ }
+ }
+
+ if ( !$valid ) {
+ // If the supplied timezone doesn't exist, fall back to the encoded offset
+ return 'Offset|' . intval( $tz[1] );
+ }
+ return $tz;
+ case 'System':
+ return $tz;
+ default:
+ $data = explode( ':', $tz, 2 );
+ if ( count( $data ) == 2 ) {
+ $data[0] = intval( $data[0] );
+ $data[1] = intval( $data[1] );
+ $minDiff = abs( $data[0] ) * 60 + $data[1];
+ if ( $data[0] < 0 ) {
+ $minDiff = - $minDiff;
+ }
+ } else {
+ $minDiff = intval( $data[0] ) * 60;
+ }
+
+ # Max is +14:00 and min is -12:00, see:
+ # https://en.wikipedia.org/wiki/Timezone
+ $minDiff = min( $minDiff, 840 ); # 14:00
+ $minDiff = max( $minDiff, -720 ); # -12:00
+ return 'Offset|' . $minDiff;
+ }
+ }
+
+ /**
+ * Handle the form submission if everything validated properly
+ *
+ * @param array $formData
+ * @param PreferencesForm $form
+ * @return bool|Status|string
+ */
+ protected function saveFormData( $formData, PreferencesForm $form ) {
+ $user = $form->getModifiedUser();
+ $hiddenPrefs = $this->config->get( 'HiddenPrefs' );
+ $result = true;
+
+ if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
+ return Status::newFatal( 'mypreferencesprotected' );
+ }
+
+ // Filter input
+ foreach ( array_keys( $formData ) as $name ) {
+ $filters = $this->getSaveFilters();
+ if ( isset( $filters[$name] ) ) {
+ $formData[$name] = call_user_func( $filters[$name], $formData[$name], $formData );
+ }
+ }
+
+ // Fortunately, the realname field is MUCH simpler
+ // (not really "private", but still shouldn't be edited without permission)
+
+ if ( !in_array( 'realname', $hiddenPrefs )
+ && $user->isAllowed( 'editmyprivateinfo' )
+ && array_key_exists( 'realname', $formData )
+ ) {
+ $realName = $formData['realname'];
+ $user->setRealName( $realName );
+ }
+
+ if ( $user->isAllowed( 'editmyoptions' ) ) {
+ $oldUserOptions = $user->getOptions();
+
+ foreach ( $this->getSaveBlacklist() as $b ) {
+ unset( $formData[$b] );
+ }
+
+ # If users have saved a value for a preference which has subsequently been disabled
+ # via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
+ # is subsequently re-enabled
+ foreach ( $hiddenPrefs as $pref ) {
+ # If the user has not set a non-default value here, the default will be returned
+ # and subsequently discarded
+ $formData[$pref] = $user->getOption( $pref, null, true );
+ }
+
+ // If the user changed the rclimit preference, also change the rcfilters-rclimit preference
+ if (
+ isset( $formData['rclimit'] ) &&
+ intval( $formData[ 'rclimit' ] ) !== $user->getIntOption( 'rclimit' )
+ ) {
+ $formData['rcfilters-limit'] = $formData['rclimit'];
+ }
+
+ // Keep old preferences from interfering due to back-compat code, etc.
+ $user->resetOptions( 'unused', $form->getContext() );
+
+ foreach ( $formData as $key => $value ) {
+ $user->setOption( $key, $value );
+ }
+
+ Hooks::run(
+ 'PreferencesFormPreSave',
+ [ $formData, $form, $user, &$result, $oldUserOptions ]
+ );
+ }
+
+ AuthManager::callLegacyAuthPlugin( 'updateExternalDB', [ $user ] );
+ $user->saveSettings();
+
+ return $result;
+ }
+
+ /**
+ * DO NOT USE. Temporary function to punch hole for the Preferences class.
+ *
+ * @deprecated since 1.31, its inception
+ *
+ * @param array $formData
+ * @param PreferencesForm $form
+ * @return bool|Status|string
+ */
+ public function legacySaveFormData( $formData, PreferencesForm $form ) {
+ return $this->saveFormData( $formData, $form );
+ }
+
+ /**
+ * Save the form data and reload the page
+ *
+ * @param array $formData
+ * @param PreferencesForm $form
+ * @return Status
+ */
+ protected function submitForm( array $formData, PreferencesForm $form ) {
+ $res = $this->saveFormData( $formData, $form );
+
+ if ( $res ) {
+ $urlOptions = [];
+
+ if ( $res === 'eauth' ) {
+ $urlOptions['eauth'] = 1;
+ }
+
+ $urlOptions += $form->getExtraSuccessRedirectParameters();
+
+ $url = $form->getTitle()->getFullURL( $urlOptions );
+
+ $context = $form->getContext();
+ // Set session data for the success message
+ $context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
+
+ $context->getOutput()->redirect( $url );
+ }
+
+ return Status::newGood();
+ }
+
+ /**
+ * DO NOT USE. Temporary function to punch hole for the Preferences class.
+ *
+ * @deprecated since 1.31, its inception
+ *
+ * @param array $formData
+ * @param PreferencesForm $form
+ * @return Status
+ */
+ public function legacySubmitForm( array $formData, PreferencesForm $form ) {
+ return $this->submitForm( $formData, $form );
+ }
+
+ /**
+ * Get a list of all time zones
+ * @param Language $language Language used for the localized names
+ * @return array A list of all time zones. The system name of the time zone is used as key and
+ * the value is an array which contains localized name, the timecorrection value used for
+ * preferences and the region
+ * @since 1.26
+ */
+ protected function getTimeZoneList( Language $language ) {
+ $identifiers = DateTimeZone::listIdentifiers();
+ if ( $identifiers === false ) {
+ return [];
+ }
+ sort( $identifiers );
+
+ $tzRegions = [
+ 'Africa' => wfMessage( 'timezoneregion-africa' )->inLanguage( $language )->text(),
+ 'America' => wfMessage( 'timezoneregion-america' )->inLanguage( $language )->text(),
+ 'Antarctica' => wfMessage( 'timezoneregion-antarctica' )->inLanguage( $language )->text(),
+ 'Arctic' => wfMessage( 'timezoneregion-arctic' )->inLanguage( $language )->text(),
+ 'Asia' => wfMessage( 'timezoneregion-asia' )->inLanguage( $language )->text(),
+ 'Atlantic' => wfMessage( 'timezoneregion-atlantic' )->inLanguage( $language )->text(),
+ 'Australia' => wfMessage( 'timezoneregion-australia' )->inLanguage( $language )->text(),
+ 'Europe' => wfMessage( 'timezoneregion-europe' )->inLanguage( $language )->text(),
+ 'Indian' => wfMessage( 'timezoneregion-indian' )->inLanguage( $language )->text(),
+ 'Pacific' => wfMessage( 'timezoneregion-pacific' )->inLanguage( $language )->text(),
+ ];
+ asort( $tzRegions );
+
+ $timeZoneList = [];
+
+ $now = new DateTime();
+
+ foreach ( $identifiers as $identifier ) {
+ $parts = explode( '/', $identifier, 2 );
+
+ // DateTimeZone::listIdentifiers() returns a number of
+ // backwards-compatibility entries. This filters them out of the
+ // list presented to the user.
+ if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
+ continue;
+ }
+
+ // Localize region
+ $parts[0] = $tzRegions[$parts[0]];
+
+ $dateTimeZone = new DateTimeZone( $identifier );
+ $minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
+
+ $display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
+ $value = "ZoneInfo|$minDiff|$identifier";
+
+ $timeZoneList[$identifier] = [
+ 'name' => $display,
+ 'timecorrection' => $value,
+ 'region' => $parts[0],
+ ];
+ }
+
+ return $timeZoneList;
+ }
+}
diff --git a/www/wiki/includes/preferences/PreferencesFactory.php b/www/wiki/includes/preferences/PreferencesFactory.php
new file mode 100644
index 00000000..685f78ca
--- /dev/null
+++ b/www/wiki/includes/preferences/PreferencesFactory.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Preferences;
+
+use HTMLForm;
+use IContextSource;
+use User;
+
+/**
+ * A PreferencesFactory is a MediaWiki service that provides the definitions of preferences for a
+ * given user. These definitions are in the form of an HTMLForm descriptor.
+ *
+ * PreferencesForm (a subclass of HTMLForm) is used to generate the Preferences form, and handles
+ * generic submission, CSRF protection, layout and other logic in a reusable manner.
+ *
+ * In order to generate the form, the HTMLForm object needs an array structure detailing the
+ * form fields available, and that's what this implementations of this interface provide. Each
+ * element of the array is a basic property-list, including the type of field, the label it is to be
+ * given in the form, callbacks for validation and 'filtering', and other pertinent information.
+ * Note that the 'default' field is named for generic forms, and does not represent the preference's
+ * default (which is stored in $wgDefaultUserOptions), but the default for the form field, which
+ * should be whatever the user has set for that preference. There is no need to override it unless
+ * you have some special storage logic (for instance, those not presently stored as options, but
+ * which are best set from the user preferences view).
+ *
+ * Field types are implemented as subclasses of the generic HTMLFormField object, and typically
+ * implement at least getInputHTML, which generates the HTML for the input field to be placed in the
+ * table.
+ *
+ * Once fields have been retrieved and validated, submission logic is handed over to the
+ * submitForm() method of this interface.
+ */
+interface PreferencesFactory {
+
+ /**
+ * Get the preferences form for a given user. This method retrieves the form descriptor for the
+ * user, instantiates a new form using the descriptor and returns the instantiated form object.
+ * @param User $user
+ * @param IContextSource $contextSource
+ * @param string $formClass
+ * @param array $remove
+ * @return HTMLForm
+ */
+ public function getForm(
+ User $user,
+ IContextSource $contextSource,
+ $formClass = \PreferencesForm::class,
+ array $remove = []
+ );
+
+ /**
+ * Get the preferences form descriptor.
+ * @param User $user
+ * @param IContextSource $contextSource
+ * @return mixed[][] An HTMLForm descriptor array.
+ */
+ public function getFormDescriptor( User $user, IContextSource $contextSource );
+
+ /**
+ * Get the names of preferences that should never be saved
+ * (such as 'realname' and 'emailaddress').
+ * @return string[]
+ */
+ public function getSaveBlacklist();
+}
diff --git a/www/wiki/includes/profiler/Profiler.php b/www/wiki/includes/profiler/Profiler.php
index 4da7976d..d02011fa 100644
--- a/www/wiki/includes/profiler/Profiler.php
+++ b/www/wiki/includes/profiler/Profiler.php
@@ -64,7 +64,7 @@ abstract class Profiler {
global $wgProfiler, $wgProfileLimit;
$params = [
- 'class' => 'ProfilerStub',
+ 'class' => ProfilerStub::class,
'sampling' => 1,
'threshold' => $wgProfileLimit,
'output' => [],
@@ -74,8 +74,9 @@ abstract class Profiler {
}
$inSample = mt_rand( 0, $params['sampling'] - 1 ) === 0;
- if ( PHP_SAPI === 'cli' || !$inSample ) {
- $params['class'] = 'ProfilerStub';
+ // wfIsCLI() is not available yet
+ if ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' || !$inSample ) {
+ $params['class'] = ProfilerStub::class;
}
if ( !is_array( $params['output'] ) ) {
@@ -187,7 +188,7 @@ abstract class Profiler {
* Get all usable outputs.
*
* @throws MWException
- * @return array Array of ProfilerOutput instances.
+ * @return ProfilerOutput[]
* @since 1.25
*/
private function getOutputs() {
diff --git a/www/wiki/includes/profiler/ProfilerFunctions.php b/www/wiki/includes/profiler/ProfilerFunctions.php
deleted file mode 100644
index cc716300..00000000
--- a/www/wiki/includes/profiler/ProfilerFunctions.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-/**
- * Core profiling functions. Have to exist before basically anything.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Profiler
- */
-
-/**
- * Get system resource usage of current request context.
- * Invokes the getrusage(2) system call, requesting RUSAGE_SELF if on PHP5
- * or RUSAGE_THREAD if on HHVM. Returns false if getrusage is not available.
- *
- * @since 1.24
- * @return array|bool Resource usage data or false if no data available.
- */
-function wfGetRusage() {
- if ( !function_exists( 'getrusage' ) ) {
- return false;
- } elseif ( defined( 'HHVM_VERSION' ) && PHP_OS === 'Linux' ) {
- return getrusage( 2 /* RUSAGE_THREAD */ );
- } else {
- return getrusage( 0 /* RUSAGE_SELF */ );
- }
-}
-
-/**
- * Begin profiling of a function
- * @param string $functionname Name of the function we will profile
- * @deprecated since 1.25
- */
-function wfProfileIn( $functionname ) {
-}
-
-/**
- * Stop profiling of a function
- * @param string $functionname Name of the function we have profiled
- * @deprecated since 1.25
- */
-function wfProfileOut( $functionname = 'missing' ) {
-}
diff --git a/www/wiki/includes/profiler/ProfilerSectionOnly.php b/www/wiki/includes/profiler/ProfilerSectionOnly.php
index 41260a83..a7bc1375 100644
--- a/www/wiki/includes/profiler/ProfilerSectionOnly.php
+++ b/www/wiki/includes/profiler/ProfilerSectionOnly.php
@@ -22,7 +22,7 @@
* Profiler that only tracks explicit profiling sections
*
* @code
- * $wgProfiler['class'] = 'ProfilerSectionOnly';
+ * $wgProfiler['class'] = ProfilerSectionOnly::class;
* $wgProfiler['output'] = 'text';
* $wgProfiler['visible'] = true;
* @endcode
diff --git a/www/wiki/includes/profiler/ProfilerXhprof.php b/www/wiki/includes/profiler/ProfilerXhprof.php
index 09191ee5..ffa441ed 100644
--- a/www/wiki/includes/profiler/ProfilerXhprof.php
+++ b/www/wiki/includes/profiler/ProfilerXhprof.php
@@ -22,14 +22,14 @@
* Profiler wrapper for XHProf extension.
*
* @code
- * $wgProfiler['class'] = 'ProfilerXhprof';
+ * $wgProfiler['class'] = ProfilerXhprof::class;
* $wgProfiler['flags'] = XHPROF_FLAGS_NO_BUILTINS;
* $wgProfiler['output'] = 'text';
* $wgProfiler['visible'] = true;
* @endcode
*
* @code
- * $wgProfiler['class'] = 'ProfilerXhprof';
+ * $wgProfiler['class'] = ProfilerXhprof::class;
* $wgProfiler['flags'] = XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_NO_BUILTINS;
* $wgProfiler['output'] = 'udp';
* @endcode
diff --git a/www/wiki/includes/profiler/TransactionProfiler.php b/www/wiki/includes/profiler/TransactionProfiler.php
deleted file mode 100644
index 1aba71c3..00000000
--- a/www/wiki/includes/profiler/TransactionProfiler.php
+++ /dev/null
@@ -1,314 +0,0 @@
-<?php
-/**
- * Transaction profiling for contention
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Profiler
- * @author Aaron Schulz
- */
-
-use Psr\Log\LoggerInterface;
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\NullLogger;
-
-/**
- * Helper class that detects high-contention DB queries via profiling calls
- *
- * This class is meant to work with a DatabaseBase object, which manages queries
- *
- * @since 1.24
- */
-class TransactionProfiler implements LoggerAwareInterface {
- /** @var float Seconds */
- protected $dbLockThreshold = 3.0;
- /** @var float Seconds */
- protected $eventThreshold = .25;
-
- /** @var array transaction ID => (write start time, list of DBs involved) */
- protected $dbTrxHoldingLocks = [];
- /** @var array transaction ID => list of (query name, start time, end time) */
- protected $dbTrxMethodTimes = [];
-
- /** @var array */
- protected $hits = [
- 'writes' => 0,
- 'queries' => 0,
- 'conns' => 0,
- 'masterConns' => 0
- ];
- /** @var array */
- protected $expect = [
- 'writes' => INF,
- 'queries' => INF,
- 'conns' => INF,
- 'masterConns' => INF,
- 'maxAffected' => INF,
- 'readQueryTime' => INF,
- 'writeQueryTime' => INF
- ];
- /** @var array */
- protected $expectBy = [];
-
- /**
- * @var LoggerInterface
- */
- private $logger;
-
- public function __construct() {
- $this->setLogger( new NullLogger() );
- }
-
- public function setLogger( LoggerInterface $logger ) {
- $this->logger = $logger;
- }
-
- /**
- * Set performance expectations
- *
- * With conflicting expectations, the most narrow ones will be used
- *
- * @param string $event (writes,queries,conns,mConns)
- * @param integer $value Maximum count of the event
- * @param string $fname Caller
- * @since 1.25
- */
- public function setExpectation( $event, $value, $fname ) {
- $this->expect[$event] = isset( $this->expect[$event] )
- ? min( $this->expect[$event], $value )
- : $value;
- if ( $this->expect[$event] == $value ) {
- $this->expectBy[$event] = $fname;
- }
- }
-
- /**
- * Set multiple performance expectations
- *
- * With conflicting expectations, the most narrow ones will be used
- *
- * @param array $expects Map of (event => limit)
- * @param $fname
- * @since 1.26
- */
- public function setExpectations( array $expects, $fname ) {
- foreach ( $expects as $event => $value ) {
- $this->setExpectation( $event, $value, $fname );
- }
- }
-
- /**
- * Reset performance expectations and hit counters
- *
- * @since 1.25
- */
- public function resetExpectations() {
- foreach ( $this->hits as &$val ) {
- $val = 0;
- }
- unset( $val );
- foreach ( $this->expect as &$val ) {
- $val = INF;
- }
- unset( $val );
- $this->expectBy = [];
- }
-
- /**
- * Mark a DB as having been connected to with a new handle
- *
- * Note that there can be multiple connections to a single DB.
- *
- * @param string $server DB server
- * @param string $db DB name
- * @param bool $isMaster
- */
- public function recordConnection( $server, $db, $isMaster ) {
- // Report when too many connections happen...
- if ( $this->hits['conns']++ == $this->expect['conns'] ) {
- $this->reportExpectationViolated( 'conns', "[connect to $server ($db)]" );
- }
- if ( $isMaster && $this->hits['masterConns']++ == $this->expect['masterConns'] ) {
- $this->reportExpectationViolated( 'masterConns', "[connect to $server ($db)]" );
- }
- }
-
- /**
- * Mark a DB as in a transaction with one or more writes pending
- *
- * Note that there can be multiple connections to a single DB.
- *
- * @param string $server DB server
- * @param string $db DB name
- * @param string $id ID string of transaction
- */
- public function transactionWritingIn( $server, $db, $id ) {
- $name = "{$server} ({$db}) (TRX#$id)";
- if ( isset( $this->dbTrxHoldingLocks[$name] ) ) {
- $this->logger->info( "Nested transaction for '$name' - out of sync." );
- }
- $this->dbTrxHoldingLocks[$name] = [
- 'start' => microtime( true ),
- 'conns' => [], // all connections involved
- ];
- $this->dbTrxMethodTimes[$name] = [];
-
- foreach ( $this->dbTrxHoldingLocks as $name => &$info ) {
- // Track all DBs in transactions for this transaction
- $info['conns'][$name] = 1;
- }
- }
-
- /**
- * Register the name and time of a method for slow DB trx detection
- *
- * This assumes that all queries are synchronous (non-overlapping)
- *
- * @param string $query Function name or generalized SQL
- * @param float $sTime Starting UNIX wall time
- * @param bool $isWrite Whether this is a write query
- * @param integer $n Number of affected rows
- */
- public function recordQueryCompletion( $query, $sTime, $isWrite = false, $n = 0 ) {
- $eTime = microtime( true );
- $elapsed = ( $eTime - $sTime );
-
- if ( $isWrite && $n > $this->expect['maxAffected'] ) {
- $this->logger->info( "Query affected $n row(s):\n" . $query . "\n" .
- wfBacktrace( true ) );
- }
-
- // Report when too many writes/queries happen...
- if ( $this->hits['queries']++ == $this->expect['queries'] ) {
- $this->reportExpectationViolated( 'queries', $query );
- }
- if ( $isWrite && $this->hits['writes']++ == $this->expect['writes'] ) {
- $this->reportExpectationViolated( 'writes', $query );
- }
- // Report slow queries...
- if ( !$isWrite && $elapsed > $this->expect['readQueryTime'] ) {
- $this->reportExpectationViolated( 'readQueryTime', $query, $elapsed );
- }
- if ( $isWrite && $elapsed > $this->expect['writeQueryTime'] ) {
- $this->reportExpectationViolated( 'writeQueryTime', $query, $elapsed );
- }
-
- if ( !$this->dbTrxHoldingLocks ) {
- // Short-circuit
- return;
- } elseif ( !$isWrite && $elapsed < $this->eventThreshold ) {
- // Not an important query nor slow enough
- return;
- }
-
- foreach ( $this->dbTrxHoldingLocks as $name => $info ) {
- $lastQuery = end( $this->dbTrxMethodTimes[$name] );
- if ( $lastQuery ) {
- // Additional query in the trx...
- $lastEnd = $lastQuery[2];
- if ( $sTime >= $lastEnd ) { // sanity check
- if ( ( $sTime - $lastEnd ) > $this->eventThreshold ) {
- // Add an entry representing the time spent doing non-queries
- $this->dbTrxMethodTimes[$name][] = [ '...delay...', $lastEnd, $sTime ];
- }
- $this->dbTrxMethodTimes[$name][] = [ $query, $sTime, $eTime ];
- }
- } else {
- // First query in the trx...
- if ( $sTime >= $info['start'] ) { // sanity check
- $this->dbTrxMethodTimes[$name][] = [ $query, $sTime, $eTime ];
- }
- }
- }
- }
-
- /**
- * Mark a DB as no longer in a transaction
- *
- * This will check if locks are possibly held for longer than
- * needed and log any affected transactions to a special DB log.
- * Note that there can be multiple connections to a single DB.
- *
- * @param string $server DB server
- * @param string $db DB name
- * @param string $id ID string of transaction
- * @param float $writeTime Time spent in write queries
- */
- public function transactionWritingOut( $server, $db, $id, $writeTime = 0.0 ) {
- $name = "{$server} ({$db}) (TRX#$id)";
- if ( !isset( $this->dbTrxMethodTimes[$name] ) ) {
- $this->logger->info( "Detected no transaction for '$name' - out of sync." );
- return;
- }
-
- $slow = false;
-
- // Warn if too much time was spend writing...
- if ( $writeTime > $this->expect['writeQueryTime'] ) {
- $this->reportExpectationViolated(
- 'writeQueryTime',
- "[transaction $id writes to {$server} ({$db})]",
- $writeTime
- );
- $slow = true;
- }
- // Fill in the last non-query period...
- $lastQuery = end( $this->dbTrxMethodTimes[$name] );
- if ( $lastQuery ) {
- $now = microtime( true );
- $lastEnd = $lastQuery[2];
- if ( ( $now - $lastEnd ) > $this->eventThreshold ) {
- $this->dbTrxMethodTimes[$name][] = [ '...delay...', $lastEnd, $now ];
- }
- }
- // Check for any slow queries or non-query periods...
- foreach ( $this->dbTrxMethodTimes[$name] as $info ) {
- $elapsed = ( $info[2] - $info[1] );
- if ( $elapsed >= $this->dbLockThreshold ) {
- $slow = true;
- break;
- }
- }
- if ( $slow ) {
- $dbs = implode( ', ', array_keys( $this->dbTrxHoldingLocks[$name]['conns'] ) );
- $msg = "Sub-optimal transaction on DB(s) [{$dbs}]:\n";
- foreach ( $this->dbTrxMethodTimes[$name] as $i => $info ) {
- list( $query, $sTime, $end ) = $info;
- $msg .= sprintf( "%d\t%.6f\t%s\n", $i, ( $end - $sTime ), $query );
- }
- $this->logger->info( $msg );
- }
- unset( $this->dbTrxHoldingLocks[$name] );
- unset( $this->dbTrxMethodTimes[$name] );
- }
-
- /**
- * @param string $expect
- * @param string $query
- * @param string|float|int $actual [optional]
- */
- protected function reportExpectationViolated( $expect, $query, $actual = null ) {
- $n = $this->expect[$expect];
- $by = $this->expectBy[$expect];
- $actual = ( $actual !== null ) ? " (actual: $actual)" : "";
-
- $this->logger->info(
- "Expectation ($expect <= $n) by $by not met$actual:\n$query\n" .
- wfBacktrace( true )
- );
- }
-}
diff --git a/www/wiki/includes/profiler/output/ProfilerOutputDb.php b/www/wiki/includes/profiler/output/ProfilerOutputDb.php
index 2225e3f6..28dc2cc2 100644
--- a/www/wiki/includes/profiler/output/ProfilerOutputDb.php
+++ b/www/wiki/includes/profiler/output/ProfilerOutputDb.php
@@ -48,46 +48,59 @@ class ProfilerOutputDb extends ProfilerOutput {
}
public function log( array $stats ) {
- $pfhost = $this->perHost ? wfHostname() : '';
-
try {
$dbw = wfGetDB( DB_MASTER );
- $useTrx = ( $dbw->getType() === 'sqlite' ); // much faster
- if ( $useTrx ) {
- $dbw->startAtomic( __METHOD__ );
- }
- foreach ( $stats as $data ) {
- $name = $data['name'];
- $eventCount = $data['calls'];
- $timeSum = (float)$data['real'];
- $memorySum = (float)$data['memory'];
- $name = substr( $name, 0, 255 );
+ } catch ( DBError $e ) {
+ return; // ignore
+ }
+
+ $fname = __METHOD__;
+ $dbw->onTransactionIdle( function () use ( $stats, $dbw, $fname ) {
+ $pfhost = $this->perHost ? wfHostname() : '';
+ // Sqlite: avoid excess b-tree rebuilds (mostly for non-WAL mode)
+ // non-Sqlite: lower contention with small transactions
+ $useTrx = ( $dbw->getType() === 'sqlite' );
- // Kludge
- $timeSum = $timeSum >= 0 ? $timeSum : 0;
- $memorySum = $memorySum >= 0 ? $memorySum : 0;
+ try {
+ $useTrx ? $dbw->startAtomic( $fname ) : null;
- $dbw->upsert( 'profiling',
- [
- 'pf_name' => $name,
- 'pf_count' => $eventCount,
- 'pf_time' => $timeSum,
- 'pf_memory' => $memorySum,
- 'pf_server' => $pfhost
- ],
- [ [ 'pf_name', 'pf_server' ] ],
- [
- "pf_count=pf_count+{$eventCount}",
- "pf_time=pf_time+{$timeSum}",
- "pf_memory=pf_memory+{$memorySum}",
- ],
- __METHOD__
- );
+ foreach ( $stats as $data ) {
+ $name = $data['name'];
+ $eventCount = $data['calls'];
+ $timeSum = (float)$data['real'];
+ $memorySum = (float)$data['memory'];
+ $name = substr( $name, 0, 255 );
+
+ // Kludge
+ $timeSum = $timeSum >= 0 ? $timeSum : 0;
+ $memorySum = $memorySum >= 0 ? $memorySum : 0;
+
+ $dbw->upsert( 'profiling',
+ [
+ 'pf_name' => $name,
+ 'pf_count' => $eventCount,
+ 'pf_time' => $timeSum,
+ 'pf_memory' => $memorySum,
+ 'pf_server' => $pfhost
+ ],
+ [ [ 'pf_name', 'pf_server' ] ],
+ [
+ "pf_count=pf_count+{$eventCount}",
+ "pf_time=pf_time+{$timeSum}",
+ "pf_memory=pf_memory+{$memorySum}",
+ ],
+ $fname
+ );
+ }
+ } catch ( DBError $e ) {
+ // ignore
}
- if ( $useTrx ) {
- $dbw->endAtomic( __METHOD__ );
+
+ try {
+ $useTrx ? $dbw->endAtomic( $fname ) : null;
+ } catch ( DBError $e ) {
+ // ignore
}
- } catch ( DBError $e ) {
- }
+ } );
}
}
diff --git a/www/wiki/includes/profiler/output/ProfilerOutputText.php b/www/wiki/includes/profiler/output/ProfilerOutputText.php
index dc24f181..100304ff 100644
--- a/www/wiki/includes/profiler/output/ProfilerOutputText.php
+++ b/www/wiki/includes/profiler/output/ProfilerOutputText.php
@@ -59,7 +59,7 @@ class ProfilerOutputText extends ProfilerOutput {
);
$contentType = $this->collector->getContentType();
- if ( PHP_SAPI === 'cli' ) {
+ if ( wfIsCLI() ) {
print "<!--\n{$out}\n-->\n";
} elseif ( $contentType === 'text/html' ) {
$visible = isset( $this->params['visible'] ) ?
diff --git a/www/wiki/includes/rcfeed/IRCColourfulRCFeedFormatter.php b/www/wiki/includes/rcfeed/IRCColourfulRCFeedFormatter.php
index 531a3eb2..158ee595 100644
--- a/www/wiki/includes/rcfeed/IRCColourfulRCFeedFormatter.php
+++ b/www/wiki/includes/rcfeed/IRCColourfulRCFeedFormatter.php
@@ -94,7 +94,7 @@ class IRCColourfulRCFeedFormatter implements RCFeedFormatter {
$flag = $attribs['rc_log_action'];
} else {
$comment = self::cleanupForIRC(
- CommentStore::newKey( 'rc_comment' )->getComment( $attribs )->text
+ CommentStore::getStore()->getComment( 'rc_comment', $attribs )->text
);
$flag = '';
if ( !$attribs['rc_patrolled']
diff --git a/www/wiki/includes/registration/CoreVersionChecker.php b/www/wiki/includes/registration/CoreVersionChecker.php
deleted file mode 100644
index f64d826d..00000000
--- a/www/wiki/includes/registration/CoreVersionChecker.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-/**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- */
-
-use Composer\Semver\VersionParser;
-use Composer\Semver\Constraint\Constraint;
-
-/**
- * @since 1.26
- */
-class CoreVersionChecker {
-
- /**
- * @var Constraint|bool representing $wgVersion
- */
- private $coreVersion = false;
-
- /**
- * @var VersionParser
- */
- private $versionParser;
-
- /**
- * @param string $coreVersion Current version of core
- */
- public function __construct( $coreVersion ) {
- $this->versionParser = new VersionParser();
- try {
- $this->coreVersion = new Constraint(
- '==',
- $this->versionParser->normalize( $coreVersion )
- );
- } catch ( UnexpectedValueException $e ) {
- // Non-parsable version, don't fatal.
- }
- }
-
- /**
- * Check that the provided constraint is compatible with the current version of core
- *
- * @param string $constraint Something like ">= 1.26"
- * @return bool
- */
- public function check( $constraint ) {
- if ( $this->coreVersion === false ) {
- // Couldn't parse the core version, so we can't check anything
- return true;
- }
-
- return $this->versionParser->parseConstraints( $constraint )
- ->matches( $this->coreVersion );
- }
-}
diff --git a/www/wiki/includes/registration/ExtensionDependencyError.php b/www/wiki/includes/registration/ExtensionDependencyError.php
new file mode 100644
index 00000000..d380d077
--- /dev/null
+++ b/www/wiki/includes/registration/ExtensionDependencyError.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+/**
+ * @since 1.31
+ */
+class ExtensionDependencyError extends Exception {
+
+ /**
+ * @var string[]
+ */
+ public $missingExtensions = [];
+
+ /**
+ * @var string[]
+ */
+ public $missingSkins = [];
+
+ /**
+ * @var string[]
+ */
+ public $incompatibleExtensions = [];
+
+ /**
+ * @var string[]
+ */
+ public $incompatibleSkins = [];
+
+ /**
+ * @var bool
+ */
+ public $incompatibleCore = false;
+
+ /**
+ * @param array $errors Each error has a 'msg' and 'type' key at minimum
+ */
+ public function __construct( array $errors ) {
+ $msg = '';
+ foreach ( $errors as $info ) {
+ $msg .= $info['msg'] . "\n";
+ switch ( $info['type'] ) {
+ case 'incompatible-core':
+ $this->incompatibleCore = true;
+ break;
+ case 'missing-skins':
+ $this->missingSkins[] = $info['missing'];
+ break;
+ case 'missing-extensions':
+ $this->missingExtensions[] = $info['missing'];
+ break;
+ case 'incompatible-skins':
+ $this->incompatibleSkins[] = $info['incompatible'];
+ break;
+ case 'incompatible-extensions':
+ $this->incompatibleExtensions[] = $info['incompatible'];
+ break;
+ // default: continue
+ }
+ }
+
+ parent::__construct( $msg );
+ }
+
+}
diff --git a/www/wiki/includes/registration/ExtensionJsonValidator.php b/www/wiki/includes/registration/ExtensionJsonValidator.php
index b860a172..7e3afaa8 100644
--- a/www/wiki/includes/registration/ExtensionJsonValidator.php
+++ b/www/wiki/includes/registration/ExtensionJsonValidator.php
@@ -21,8 +21,6 @@
use Composer\Spdx\SpdxLicenses;
use JsonSchema\Validator;
-use Seld\JsonLint\JsonParser;
-use Seld\JsonLint\ParsingException;
/**
* @since 1.29
@@ -42,6 +40,7 @@ class ExtensionJsonValidator {
}
/**
+ * @codeCoverageIgnore
* @return bool
*/
public function checkDependencies() {
@@ -55,10 +54,6 @@ class ExtensionJsonValidator {
'The spdx-licenses library cannot be found, please install it through composer.'
);
return false;
- } elseif ( !class_exists( JsonParser::class ) ) {
- call_user_func( $this->missingDepCallback,
- 'The JSON lint library cannot be found, please install it through composer.'
- );
}
return true;
@@ -70,14 +65,8 @@ class ExtensionJsonValidator {
* @throws ExtensionJsonValidationError on any failure
*/
public function validate( $path ) {
- $contents = file_get_contents( $path );
- $jsonParser = new JsonParser();
- try {
- $data = $jsonParser->parse( $contents, JsonParser::DETECT_KEY_CONFLICTS );
- } catch ( ParsingException $e ) {
- if ( $e instanceof \Seld\JsonLint\DuplicateKeyException ) {
- throw new ExtensionJsonValidationError( $e->getMessage() );
- }
+ $data = json_decode( file_get_contents( $path ) );
+ if ( !is_object( $data ) ) {
throw new ExtensionJsonValidationError( "$path is not valid JSON" );
}
diff --git a/www/wiki/includes/registration/ExtensionProcessor.php b/www/wiki/includes/registration/ExtensionProcessor.php
index ce262bd2..14d4a17d 100644
--- a/www/wiki/includes/registration/ExtensionProcessor.php
+++ b/www/wiki/includes/registration/ExtensionProcessor.php
@@ -186,24 +186,39 @@ class ExtensionProcessor implements Processor {
*/
public function extractInfo( $path, array $info, $version ) {
$dir = dirname( $path );
- if ( $version === 2 ) {
- $this->extractConfig2( $info, $dir );
- } else {
- // $version === 1
- $this->extractConfig1( $info );
- }
$this->extractHooks( $info );
$this->extractExtensionMessagesFiles( $dir, $info );
$this->extractMessagesDirs( $dir, $info );
$this->extractNamespaces( $info );
$this->extractResourceLoaderModules( $dir, $info );
- $this->extractServiceWiringFiles( $dir, $info );
- $this->extractParserTestFiles( $dir, $info );
+ if ( isset( $info['ServiceWiringFiles'] ) ) {
+ $this->extractPathBasedGlobal(
+ 'wgServiceWiringFiles',
+ $dir,
+ $info['ServiceWiringFiles']
+ );
+ }
+ if ( isset( $info['ParserTestFiles'] ) ) {
+ $this->extractPathBasedGlobal(
+ 'wgParserTestFiles',
+ $dir,
+ $info['ParserTestFiles']
+ );
+ }
$name = $this->extractCredits( $path, $info );
if ( isset( $info['callback'] ) ) {
$this->callbacks[$name] = $info['callback'];
}
+ // config should be after all core globals are extracted,
+ // so duplicate setting detection will work fully
+ if ( $version === 2 ) {
+ $this->extractConfig2( $info, $dir );
+ } else {
+ // $version === 1
+ $this->extractConfig1( $info );
+ }
+
if ( $version === 2 ) {
$this->extractAttributes( $path, $info );
}
@@ -378,9 +393,10 @@ class ExtensionProcessor implements Processor {
protected function extractExtensionMessagesFiles( $dir, array $info ) {
if ( isset( $info['ExtensionMessagesFiles'] ) ) {
- $this->globals["wgExtensionMessagesFiles"] += array_map( function ( $file ) use ( $dir ) {
- return "$dir/$file";
- }, $info['ExtensionMessagesFiles'] );
+ foreach ( $info['ExtensionMessagesFiles'] as &$file ) {
+ $file = "$dir/$file";
+ }
+ $this->globals["wgExtensionMessagesFiles"] += $info['ExtensionMessagesFiles'];
}
}
@@ -450,7 +466,7 @@ class ExtensionProcessor implements Processor {
}
foreach ( $info['config'] as $key => $val ) {
if ( $key[0] !== '@' ) {
- $this->globals["$prefix$key"] = $val;
+ $this->addConfigGlobal( "$prefix$key", $val, $info['name'] );
}
}
}
@@ -478,24 +494,30 @@ class ExtensionProcessor implements Processor {
if ( isset( $data['path'] ) && $data['path'] ) {
$value = "$dir/$value";
}
- $this->globals["$prefix$key"] = $value;
+ $this->addConfigGlobal( "$prefix$key", $value, $info['name'] );
}
}
}
- protected function extractServiceWiringFiles( $dir, array $info ) {
- if ( isset( $info['ServiceWiringFiles'] ) ) {
- foreach ( $info['ServiceWiringFiles'] as $path ) {
- $this->globals['wgServiceWiringFiles'][] = "$dir/$path";
- }
+ /**
+ * Helper function to set a value to a specific global, if it isn't set already.
+ *
+ * @param string $key The config key with the prefix and anything
+ * @param mixed $value The value of the config
+ * @param string $extName Name of the extension
+ */
+ private function addConfigGlobal( $key, $value, $extName ) {
+ if ( array_key_exists( $key, $this->globals ) ) {
+ throw new RuntimeException(
+ "The configuration setting '$key' was already set by MediaWiki core or"
+ . " another extension, and cannot be set again by $extName." );
}
+ $this->globals[$key] = $value;
}
- protected function extractParserTestFiles( $dir, array $info ) {
- if ( isset( $info['ParserTestFiles'] ) ) {
- foreach ( $info['ParserTestFiles'] as $path ) {
- $this->globals['wgParserTestFiles'][] = "$dir/$path";
- }
+ protected function extractPathBasedGlobal( $global, $dir, $paths ) {
+ foreach ( $paths as $path ) {
+ $this->globals[$global][] = "$dir/$path";
}
}
@@ -520,10 +542,7 @@ class ExtensionProcessor implements Processor {
public function getExtraAutoloaderPaths( $dir, array $info ) {
$paths = [];
if ( isset( $info['load_composer_autoloader'] ) && $info['load_composer_autoloader'] === true ) {
- $path = "$dir/vendor/autoload.php";
- if ( file_exists( $path ) ) {
- $paths[] = $path;
- }
+ $paths[] = "$dir/vendor/autoload.php";
}
return $paths;
}
diff --git a/www/wiki/includes/registration/ExtensionRegistry.php b/www/wiki/includes/registration/ExtensionRegistry.php
index b2267321..aae5fc28 100644
--- a/www/wiki/includes/registration/ExtensionRegistry.php
+++ b/www/wiki/includes/registration/ExtensionRegistry.php
@@ -20,10 +20,17 @@ class ExtensionRegistry {
/**
* Version of the highest supported manifest version
+ * Note: Update MANIFEST_VERSION_MW_VERSION when changing this
*/
const MANIFEST_VERSION = 2;
/**
+ * MediaWiki version constraint representing what the current
+ * highest MANIFEST_VERSION is supported in
+ */
+ const MANIFEST_VERSION_MW_VERSION = '>= 1.29.0';
+
+ /**
* Version of the oldest supported manifest version
*/
const OLDEST_MANIFEST_VERSION = 1;
@@ -75,6 +82,7 @@ class ExtensionRegistry {
private static $instance;
/**
+ * @codeCoverageIgnore
* @return ExtensionRegistry
*/
public static function getInstance() {
@@ -98,10 +106,11 @@ class ExtensionRegistry {
} else {
throw new Exception( "$path does not exist!" );
}
-
+ // @codeCoverageIgnoreStart
if ( $mtime === false ) {
$err = error_get_last();
throw new Exception( "Couldn't stat $path: {$err['message']}" );
+ // @codeCoverageIgnoreEnd
}
}
$this->queued[$path] = $mtime;
@@ -193,10 +202,12 @@ class ExtensionRegistry {
* @param array $queue keys are filenames, values are ignored
* @return array extracted info
* @throws Exception
+ * @throws ExtensionDependencyError
*/
public function readFromQueue( array $queue ) {
global $wgVersion;
$autoloadClasses = [];
+ $autoloadNamespaces = [];
$autoloaderPaths = [];
$processor = new ExtensionProcessor();
$versionChecker = new VersionChecker( $wgVersion );
@@ -227,10 +238,16 @@ class ExtensionRegistry {
$incompatible[] = "$path: unsupported manifest_version: {$version}";
}
- $autoload = $this->processAutoLoader( dirname( $path ), $info );
- // Set up the autoloader now so custom processors will work
- $GLOBALS['wgAutoloadClasses'] += $autoload;
- $autoloadClasses += $autoload;
+ $dir = dirname( $path );
+ if ( isset( $info['AutoloadClasses'] ) ) {
+ $autoload = $this->processAutoLoader( $dir, $info['AutoloadClasses'] );
+ $GLOBALS['wgAutoloadClasses'] += $autoload;
+ $autoloadClasses += $autoload;
+ }
+ if ( isset( $info['AutoloadNamespaces'] ) ) {
+ $autoloadNamespaces += $this->processAutoLoader( $dir, $info['AutoloadNamespaces'] );
+ AutoLoader::$psr4Namespaces += $autoloadNamespaces;
+ }
// get all requirements/dependencies for this extension
$requires = $processor->getRequirements( $info );
@@ -242,7 +259,7 @@ class ExtensionRegistry {
// Get extra paths for later inclusion
$autoloaderPaths = array_merge( $autoloaderPaths,
- $processor->getExtraAutoloaderPaths( dirname( $path ), $info ) );
+ $processor->getExtraAutoloaderPaths( $dir, $info ) );
// Compatible, read and extract info
$processor->extractInfo( $path, $info, $version );
}
@@ -258,17 +275,14 @@ class ExtensionRegistry {
);
if ( $incompatible ) {
- if ( count( $incompatible ) === 1 ) {
- throw new Exception( $incompatible[0] );
- } else {
- throw new Exception( implode( "\n", $incompatible ) );
- }
+ throw new ExtensionDependencyError( $incompatible );
}
// Need to set this so we can += to it later
$data['globals']['wgAutoloadClasses'] = [];
$data['autoload'] = $autoloadClasses;
$data['autoloaderPaths'] = $autoloaderPaths;
+ $data['autoloaderNS'] = $autoloadNamespaces;
return $data;
}
@@ -285,7 +299,7 @@ class ExtensionRegistry {
// Optimistic: If the global is not set, or is an empty array, replace it entirely.
// Will be O(1) performance.
- if ( !isset( $GLOBALS[$key] ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
+ if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
$GLOBALS[$key] = $val;
continue;
}
@@ -316,11 +330,17 @@ class ExtensionRegistry {
}
}
+ if ( isset( $info['autoloaderNS'] ) ) {
+ AutoLoader::$psr4Namespaces += $info['autoloaderNS'];
+ }
+
foreach ( $info['defines'] as $name => $val ) {
define( $name, $val );
}
foreach ( $info['autoloaderPaths'] as $path ) {
- require_once $path;
+ if ( file_exists( $path ) ) {
+ require_once $path;
+ }
}
$this->loaded += $info['credits'];
@@ -388,30 +408,17 @@ class ExtensionRegistry {
}
/**
- * Mark a thing as loaded
- *
- * @param string $name
- * @param array $credits
- */
- protected function markLoaded( $name, array $credits ) {
- $this->loaded[$name] = $credits;
- }
-
- /**
- * Register classes with the autoloader
+ * Fully expand autoloader paths
*
* @param string $dir
- * @param array $info
+ * @param array $files
* @return array
*/
- protected function processAutoLoader( $dir, array $info ) {
- if ( isset( $info['AutoloadClasses'] ) ) {
- // Make paths absolute, relative to the JSON file
- return array_map( function ( $file ) use ( $dir ) {
- return "$dir/$file";
- }, $info['AutoloadClasses'] );
- } else {
- return [];
+ protected function processAutoLoader( $dir, array $files ) {
+ // Make paths absolute, relative to the JSON file
+ foreach ( $files as &$file ) {
+ $file = "$dir/$file";
}
+ return $files;
}
}
diff --git a/www/wiki/includes/registration/VersionChecker.php b/www/wiki/includes/registration/VersionChecker.php
index a31551c3..59853b42 100644
--- a/www/wiki/includes/registration/VersionChecker.php
+++ b/www/wiki/includes/registration/VersionChecker.php
@@ -110,13 +110,18 @@ class VersionChecker {
case ExtensionRegistry::MEDIAWIKI_CORE:
$mwError = $this->handleMediaWikiDependency( $values, $extension );
if ( $mwError !== false ) {
- $errors[] = $mwError;
+ $errors[] = [
+ 'msg' => $mwError,
+ 'type' => 'incompatible-core',
+ ];
}
break;
case 'extensions':
- case 'skin':
+ case 'skins':
foreach ( $values as $dependency => $constraint ) {
- $extError = $this->handleExtensionDependency( $dependency, $constraint, $extension );
+ $extError = $this->handleExtensionDependency(
+ $dependency, $constraint, $extension, $dependencyType
+ );
if ( $extError !== false ) {
$errors[] = $extError;
}
@@ -164,24 +169,36 @@ class VersionChecker {
* @param string $dependencyName The name of the dependency
* @param string $constraint The required version constraint for this dependency
* @param string $checkedExt The Extension, which depends on this dependency
- * @return bool|string false for no errors, or a string message
+ * @param string $type Either 'extensions' or 'skins'
+ * @return bool|array false for no errors, or an array of info
*/
- private function handleExtensionDependency( $dependencyName, $constraint, $checkedExt ) {
+ private function handleExtensionDependency( $dependencyName, $constraint, $checkedExt,
+ $type
+ ) {
// Check if the dependency is even installed
if ( !isset( $this->loaded[$dependencyName] ) ) {
- return "{$checkedExt} requires {$dependencyName} to be installed.";
+ return [
+ 'msg' => "{$checkedExt} requires {$dependencyName} to be installed.",
+ 'type' => "missing-$type",
+ 'missing' => $dependencyName,
+ ];
}
// Check if the dependency has specified a version
if ( !isset( $this->loaded[$dependencyName]['version'] ) ) {
// If we depend upon any version, and none is set, that's fine.
if ( $constraint === '*' ) {
- wfDebug( "{$dependencyName} does not expose it's version, but {$checkedExt}
- mentions it with constraint '*'. Assume it's ok so." );
+ wfDebug( "{$dependencyName} does not expose its version, but {$checkedExt}"
+ . " mentions it with constraint '*'. Assume it's ok so." );
return false;
} else {
// Otherwise, mark it as incompatible.
- return "{$dependencyName} does not expose it's version, but {$checkedExt}
- requires: {$constraint}.";
+ $msg = "{$dependencyName} does not expose its version, but {$checkedExt}"
+ . " requires: {$constraint}.";
+ return [
+ 'msg' => $msg,
+ 'type' => "incompatible-$type",
+ 'incompatible' => $checkedExt,
+ ];
}
} else {
// Try to get a constraint for the dependency version
@@ -193,16 +210,24 @@ class VersionChecker {
} catch ( UnexpectedValueException $e ) {
// Non-parsable version, output an error message that the version
// string is invalid
- return "$dependencyName does not have a valid version string.";
+ return [
+ 'msg' => "$dependencyName does not have a valid version string.",
+ 'type' => 'invalid-version',
+ ];
}
// Check if the constraint actually matches...
if (
!$this->versionParser->parseConstraints( $constraint )->matches( $installedVersion )
) {
- return "{$checkedExt} is not compatible with the current "
+ $msg = "{$checkedExt} is not compatible with the current "
. "installed version of {$dependencyName} "
. "({$this->loaded[$dependencyName]['version']}), "
. "it requires: " . $constraint . '.';
+ return [
+ 'msg' => $msg,
+ 'type' => "incompatible-$type",
+ 'incompatible' => $checkedExt,
+ ];
}
}
diff --git a/www/wiki/includes/resourceloader/ResourceLoader.php b/www/wiki/includes/resourceloader/ResourceLoader.php
index c58bb00b..90c31406 100644
--- a/www/wiki/includes/resourceloader/ResourceLoader.php
+++ b/www/wiki/includes/resourceloader/ResourceLoader.php
@@ -26,8 +26,8 @@ use MediaWiki\MediaWikiServices;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
-use WrappedString\WrappedString;
use Wikimedia\Rdbms\DBConnectionError;
+use Wikimedia\WrappedString;
/**
* Dynamic JavaScript and CSS resource loading system.
@@ -236,8 +236,6 @@ class ResourceLoader implements LoggerAwareInterface {
return $data;
}
- /* Methods */
-
/**
* Register core modules and runs registration hooks.
* @param Config $config [optional]
@@ -555,7 +553,7 @@ class ResourceLoader implements LoggerAwareInterface {
$object->setLogger( $this->logger );
} else {
if ( !isset( $info['class'] ) ) {
- $class = 'ResourceLoaderFileModule';
+ $class = ResourceLoaderFileModule::class;
} else {
$class = $info['class'];
}
@@ -588,8 +586,8 @@ class ResourceLoader implements LoggerAwareInterface {
}
if (
isset( $info['class'] ) &&
- $info['class'] !== 'ResourceLoaderFileModule' &&
- !is_subclass_of( $info['class'], 'ResourceLoaderFileModule' )
+ $info['class'] !== ResourceLoaderFileModule::class &&
+ !is_subclass_of( $info['class'], ResourceLoaderFileModule::class )
) {
return false;
}
@@ -692,7 +690,6 @@ class ResourceLoader implements LoggerAwareInterface {
*
* @since 1.28
* @param ResourceLoaderContext $context
- * @param string[] $modules List of module names
* @return string Hash
*/
public function makeVersionQuery( ResourceLoaderContext $context ) {
@@ -728,6 +725,8 @@ class ResourceLoader implements LoggerAwareInterface {
// See https://bugs.php.net/bug.php?id=36514
ob_start();
+ $this->measureResponseTime( RequestContext::getMain()->getTiming() );
+
// Find out which modules are missing and instantiate the others
$modules = [];
$missing = [];
@@ -828,6 +827,16 @@ class ResourceLoader implements LoggerAwareInterface {
echo $response;
}
+ protected function measureResponseTime( Timing $timing ) {
+ DeferredUpdates::addCallableUpdate( function () use ( $timing ) {
+ $measure = $timing->measure( 'responseTime', 'requestStart', 'requestShutdown' );
+ if ( $measure !== false ) {
+ $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
+ $stats->timing( 'resourceloader.responseTime', $measure['duration'] * 1000 );
+ }
+ } );
+ }
+
/**
* Send main response headers to the client.
*
@@ -1198,8 +1207,6 @@ MESSAGE;
return $moduleNames;
}
- /* Static Methods */
-
/**
* Return JS code that calls mw.loader.implement with given module properties.
*
@@ -1220,7 +1227,11 @@ MESSAGE;
$name, $scripts, $styles, $messages, $templates
) {
if ( $scripts instanceof XmlJsCode ) {
- $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
+ if ( self::inDebugMode() ) {
+ $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
+ } else {
+ $scripts = new XmlJsCode( 'function($,jQuery,require,module){'. $scripts->value . '}' );
+ }
} elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
}
@@ -1475,10 +1486,8 @@ MESSAGE;
}
/**
- * Returns JS code which runs given JS code if the client-side framework is
- * present.
+ * Wraps JavaScript code to run after startup and base modules.
*
- * @deprecated since 1.25; use makeInlineScript instead
* @param string $script JavaScript code
* @return string JavaScript code
*/
@@ -1488,10 +1497,10 @@ MESSAGE;
}
/**
- * Construct an inline script tag with given JS code.
+ * Returns an HTML script tag that runs given JS code after startup and base modules.
*
- * The code will be wrapped in a closure, and it will be executed by ResourceLoader
- * only if the client has adequate support for MediaWiki JavaScript code.
+ * The code will be wrapped in a closure, and it will be executed by ResourceLoader's
+ * startup module if the client has adequate support for MediaWiki JavaScript code.
*
* @param string $script JavaScript code
* @return WrappedString HTML
@@ -1523,27 +1532,31 @@ MESSAGE;
/**
* Convert an array of module names to a packed query string.
*
- * For example, [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ]
- * becomes 'foo.bar,baz|bar.baz,quux'
+ * For example, `[ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ]`
+ * becomes `'foo.bar,baz|bar.baz,quux'`.
+ *
+ * This process is reversed by ResourceLoaderContext::expandModuleNames().
+ * See also mw.loader#buildModulesString() which is a port of this, used
+ * on the client-side.
+ *
* @param array $modules List of module names (strings)
* @return string Packed query string
*/
public static function makePackedModulesString( $modules ) {
- $groups = []; // [ prefix => [ suffixes ] ]
+ $moduleMap = []; // [ prefix => [ suffixes ] ]
foreach ( $modules as $module ) {
$pos = strrpos( $module, '.' );
$prefix = $pos === false ? '' : substr( $module, 0, $pos );
$suffix = $pos === false ? $module : substr( $module, $pos + 1 );
- $groups[$prefix][] = $suffix;
+ $moduleMap[$prefix][] = $suffix;
}
$arr = [];
- foreach ( $groups as $prefix => $suffixes ) {
+ foreach ( $moduleMap as $prefix => $suffixes ) {
$p = $prefix === '' ? '' : $prefix . '.';
$arr[] = $p . implode( ',', $suffixes );
}
- $str = implode( '|', $arr );
- return $str;
+ return implode( '|', $arr );
}
/**
@@ -1710,10 +1723,8 @@ MESSAGE;
* @return array Map of variable names to string CSS values.
*/
public function getLessVars() {
- if ( !$this->lessVars ) {
- $lessVars = $this->config->get( 'ResourceLoaderLESSVars' );
- Hooks::run( 'ResourceLoaderGetLessVars', [ &$lessVars ] );
- $this->lessVars = $lessVars;
+ if ( $this->lessVars === null ) {
+ $this->lessVars = $this->config->get( 'ResourceLoaderLESSVars' );
}
return $this->lessVars;
}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderClientHtml.php b/www/wiki/includes/resourceloader/ResourceLoaderClientHtml.php
index 06f9841d..545fd3bd 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderClientHtml.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderClientHtml.php
@@ -18,7 +18,7 @@
* @file
*/
-use WrappedString\WrappedStringList;
+use Wikimedia\WrappedStringList;
/**
* Bootstrap a ResourceLoader client on an HTML page.
@@ -33,8 +33,8 @@ class ResourceLoaderClientHtml {
/** @var ResourceLoader */
private $resourceLoader;
- /** @var string|null */
- private $target;
+ /** @var array */
+ private $options;
/** @var array */
private $config = [];
@@ -56,12 +56,13 @@ class ResourceLoaderClientHtml {
/**
* @param ResourceLoaderContext $context
- * @param string|null $target [optional] Custom 'target' parameter for the startup module
+ * @param array $options [optional] Array of options
+ * - 'target': Custom parameter passed to StartupModule.
*/
- public function __construct( ResourceLoaderContext $context, $target = null ) {
+ public function __construct( ResourceLoaderContext $context, array $options = [] ) {
$this->context = $context;
$this->resourceLoader = $context->getResourceLoader();
- $this->target = $target;
+ $this->options = $options;
}
/**
@@ -131,9 +132,7 @@ class ResourceLoaderClientHtml {
// moduleName => state
],
'general' => [],
- 'styles' => [
- // moduleName
- ],
+ 'styles' => [],
'scripts' => [],
// Embedding for private modules
'embed' => [
@@ -149,6 +148,13 @@ class ResourceLoaderClientHtml {
continue;
}
+ $context = $this->getContext( $module->getGroup(), ResourceLoaderModule::TYPE_COMBINED );
+ if ( $module->isKnownEmpty( $context ) ) {
+ // Avoid needless request or embed for empty module
+ $data['states'][$name] = 'ready';
+ continue;
+ }
+
if ( $module->shouldEmbedModule( $this->context ) ) {
// Embed via mw.loader.implement per T36907.
$data['embed']['general'][] = $name;
@@ -175,20 +181,17 @@ class ResourceLoaderClientHtml {
}
// Stylesheet doesn't trigger mw.loader callback.
- // Set "ready" state to allow dependencies and avoid duplicate requests. (T87871)
+ // Set "ready" state to allow script modules to depend on this module (T87871).
+ // And to avoid duplicate requests at run-time from mw.loader.
$data['states'][$name] = 'ready';
$group = $module->getGroup();
$context = $this->getContext( $group, ResourceLoaderModule::TYPE_STYLES );
- if ( $module->isKnownEmpty( $context ) ) {
- // Avoid needless request for empty module
- $data['states'][$name] = 'ready';
- } else {
+ // Avoid needless request for empty module
+ if ( !$module->isKnownEmpty( $context ) ) {
if ( $module->shouldEmbedModule( $this->context ) ) {
// Embed via style element
$data['embed']['styles'][] = $name;
- // Avoid duplicate request from mw.loader
- $data['states'][$name] = 'ready';
} else {
// Load from load.php?only=styles via <link rel=stylesheet>
$data['styles'][] = $name;
@@ -307,8 +310,10 @@ class ResourceLoaderClientHtml {
}
// Async scripts. Once the startup is loaded, inline RLQ scripts will run.
- // Pass-through a custom target from OutputPage (T143066).
- $startupQuery = $this->target ? [ 'target' => $this->target ] : [];
+ // Pass-through a custom 'target' from OutputPage (T143066).
+ $startupQuery = isset( $this->options['target'] )
+ ? [ 'target' => (string)$this->options['target'] ]
+ : [];
$chunks[] = $this->getLoad(
'startup',
ResourceLoaderModule::TYPE_SCRIPTS,
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderContext.php b/www/wiki/includes/resourceloader/ResourceLoaderContext.php
index cbb0beca..c4e9884a 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderContext.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderContext.php
@@ -63,12 +63,8 @@ class ResourceLoaderContext implements MessageLocalizer {
$this->request = $request;
$this->logger = $resourceLoader->getLogger();
- // Future developers: Avoid use of getVal() in this class, which performs
- // expensive UTF normalisation by default. Use getRawVal() instead.
- // Values here are either one of a finite number of internal IDs,
- // or previously-stored user input (e.g. titles, user names) that were passed
- // to this endpoint by ResourceLoader itself from the canonical value.
- // Values do not come directly from user input and need not match.
+ // Future developers: Use WebRequest::getRawVal() instead getVal().
+ // The getVal() method performs slow Language+UTF logic. (f303bb9360)
// List of modules
$modules = $request->getRawVal( 'modules' );
@@ -98,9 +94,12 @@ class ResourceLoaderContext implements MessageLocalizer {
}
/**
- * Expand a string of the form jquery.foo,bar|jquery.ui.baz,quux to
- * an array of module names like [ 'jquery.foo', 'jquery.bar',
- * 'jquery.ui.baz', 'jquery.ui.quux' ]
+ * Expand a string of the form `jquery.foo,bar|jquery.ui.baz,quux` to
+ * an array of module names like `[ 'jquery.foo', 'jquery.bar',
+ * 'jquery.ui.baz', 'jquery.ui.quux' ]`.
+ *
+ * This process is reversed by ResourceLoader::makePackedModulesString().
+ *
* @param string $modules Packed module name list
* @return array Array of module names
*/
@@ -182,7 +181,6 @@ class ResourceLoaderContext implements MessageLocalizer {
$lang = $this->getRequest()->getRawVal( 'lang', '' );
// Stricter version of RequestContext::sanitizeLangCode()
if ( !Language::isValidBuiltInCode( $lang ) ) {
- wfDebug( "Invalid user language code\n" );
$lang = $this->getResourceLoader()->getConfig()->get( 'LanguageCode' );
}
$this->language = $lang;
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderFileModule.php b/www/wiki/includes/resourceloader/ResourceLoaderFileModule.php
index 46751918..f2f3383f 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderFileModule.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderFileModule.php
@@ -26,7 +26,6 @@
* ResourceLoader module based on local JavaScript/CSS files.
*/
class ResourceLoaderFileModule extends ResourceLoaderModule {
- /* Protected Members */
/** @var string Local base path, see __construct() */
protected $localBasePath = '';
@@ -149,8 +148,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
*/
protected $missingLocalFileRefs = [];
- /* Methods */
-
/**
* Constructs a new module from an options array.
*
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderFilePath.php b/www/wiki/includes/resourceloader/ResourceLoaderFilePath.php
index dd239d09..3cf09d82 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderFilePath.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderFilePath.php
@@ -26,7 +26,6 @@
* and local base path, for use with ResourceLoaderFileModule.
*/
class ResourceLoaderFilePath {
- /* Protected Members */
/** @var string Local base path */
protected $localBasePath;
@@ -38,8 +37,6 @@ class ResourceLoaderFilePath {
* @var string Path to the file */
protected $path;
- /* Methods */
-
/**
* @param string $path Path to the file.
* @param string $localBasePath Base path to prepend when generating a local path.
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderImage.php b/www/wiki/includes/resourceloader/ResourceLoaderImage.php
index 072ae794..d38a1750 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderImage.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderImage.php
@@ -130,13 +130,27 @@ class ResourceLoaderImage {
$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()] ) ) {
+ }
+ if ( isset( $desc['lang'] ) ) {
+ $contextLang = $context->getLanguage();
+ if ( isset( $desc['lang'][$contextLang] ) ) {
+ return $this->basePath . '/' . $desc['lang'][$contextLang];
+ }
+ $fallbacks = Language::getFallbacksFor( $contextLang );
+ foreach ( $fallbacks as $lang ) {
+ // Images will fallback to 'default' instead of 'en', except for 'en-*' variants
+ if (
+ ( $lang !== 'en' || substr( $contextLang, 0, 3 ) === 'en-' ) &&
+ isset( $desc['lang'][$lang] )
+ ) {
+ return $this->basePath . '/' . $desc['lang'][$lang];
+ }
+ }
+ }
+ if ( isset( $desc[$context->getDirection()] ) ) {
return $this->basePath . '/' . $desc[$context->getDirection()];
- } else {
- return $this->basePath . '/' . $desc['default'];
}
+ return $this->basePath . '/' . $desc['default'];
}
/**
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderImageModule.php b/www/wiki/includes/resourceloader/ResourceLoaderImageModule.php
index 8b549595..5e329e84 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderImageModule.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderImageModule.php
@@ -249,7 +249,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
$fileDescriptor = is_string( $options ) ? $options : $options['file'];
$allowedVariants = array_merge(
- is_array( $options ) && isset( $options['variants'] ) ? $options['variants'] : [],
+ ( is_array( $options ) && isset( $options['variants'] ) ) ? $options['variants'] : [],
$this->getGlobalVariants( $context )
);
if ( isset( $this->variants[$skin] ) ) {
@@ -386,8 +386,6 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
return [
"background-image: $fallbackUrl;",
"background-image: linear-gradient(transparent, transparent), $primaryUrl;",
- // Do not serve SVG to Opera 12, bad rendering with border-radius or background-size (T87504)
- "background-image: -o-linear-gradient(transparent, transparent), $fallbackUrl;",
];
}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderLanguageDataModule.php b/www/wiki/includes/resourceloader/ResourceLoaderLanguageDataModule.php
index ef942faf..e78484a2 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderLanguageDataModule.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderLanguageDataModule.php
@@ -40,6 +40,7 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
return [
'digitTransformTable' => $language->digitTransformTable(),
'separatorTransformTable' => $language->separatorTransformTable(),
+ 'minimumGroupingDigits' => $language->minimumGroupingDigits(),
'grammarForms' => $language->getGrammarForms(),
'grammarTransformations' => $language->getGrammarTransformations(),
'pluralRules' => $language->getPluralRules(),
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderModule.php b/www/wiki/includes/resourceloader/ResourceLoaderModule.php
index b3c1cd14..2abc17c2 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderModule.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderModule.php
@@ -26,6 +26,7 @@ use MediaWiki\MediaWikiServices;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
+use Wikimedia\RelPath;
use Wikimedia\ScopedCallback;
/**
@@ -65,8 +66,6 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
# pages like Special:UserLogin and Special:Preferences
protected $origin = self::ORIGIN_CORE_SITEWIDE;
- /* Protected Members */
-
protected $name = null;
protected $targets = [ 'desktop' ];
@@ -94,8 +93,6 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
*/
protected $logger;
- /* Methods */
-
/**
* Get this module's name. This is set when the module is registered
* with ResourceLoader::register()
@@ -110,7 +107,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
* Set this module's name. This is called by ResourceLoader::register()
* when registering the module. Other code should not call this.
*
- * @param string $name Name
+ * @param string $name
*/
public function setName( $name ) {
$this->name = $name;
@@ -331,16 +328,6 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
}
/**
- * From where in the page HTML should this module be loaded?
- *
- * @deprecated since 1.29 Obsolete. All modules load async from `<head>`.
- * @return string
- */
- public function getPosition() {
- return 'top';
- }
-
- /**
* 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
* appended to the bottom of the script.
@@ -498,7 +485,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
'md_skin' => $vary,
'md_deps' => $deps,
],
- [ 'md_module', 'md_skin' ],
+ [ [ 'md_module', 'md_skin' ] ],
[
'md_deps' => $deps,
]
@@ -531,7 +518,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
public static function getRelativePaths( array $filePaths ) {
global $IP;
return array_map( function ( $path ) use ( $IP ) {
- return RelPath\getRelativePath( $path, $IP );
+ return RelPath::getRelativePath( $path, $IP );
}, $filePaths );
}
@@ -545,7 +532,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
public static function expandRelativePaths( array $filePaths ) {
global $IP;
return array_map( function ( $path ) use ( $IP ) {
- return RelPath\joinPath( $IP, $path );
+ return RelPath::joinPath( $IP, $path );
}, $filePaths );
}
@@ -633,7 +620,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
* 'https://example.org/image.png' => [ 'as' => 'image' ],
* ];
* }
- * @encode
+ * @endcode
*
* @par Example using HiDPI image variants
* @code
@@ -649,7 +636,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
* ],
* ];
* }
- * @encode
+ * @endcode
*
* @see ResourceLoaderModule::getHeaders
* @since 1.30
@@ -932,7 +919,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
* Get this module's last modification timestamp for a given context.
*
* @deprecated since 1.26 Use getDefinitionSummary() instead
- * @param ResourceLoaderContext $context Context object
+ * @param ResourceLoaderContext $context
* @return int|null UNIX timestamp
*/
public function getModifiedTime( ResourceLoaderContext $context ) {
@@ -951,41 +938,6 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
}
/**
- * 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
* definitely going to be empty, it should override this method to
@@ -1072,9 +1024,9 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
* @return int UNIX timestamp
*/
protected static function safeFilemtime( $filePath ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$mtime = filemtime( $filePath ) ?: 1;
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return $mtime;
}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderOOUIImageModule.php b/www/wiki/includes/resourceloader/ResourceLoaderOOUIImageModule.php
index ee87d8d8..5c9e1d94 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderOOUIImageModule.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderOOUIImageModule.php
@@ -98,11 +98,8 @@ class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule {
// Extra selectors to allow using the same icons for old-style MediaWiki UI code
if ( substr( $module, 0, 5 ) === 'icons' ) {
$definition['selectorWithoutVariant'] = '.oo-ui-icon-{name}, .mw-ui-icon-{name}:before';
- $definition['selectorWithVariant'] = '
- .oo-ui-image-{variant}.oo-ui-icon-{name}, .mw-ui-icon-{name}-{variant}:before,
- /* Hack for Flow, see T110051 */
- .mw-ui-hovericon:hover .mw-ui-icon-{name}-{variant}-hover:before,
- .mw-ui-hovericon.mw-ui-icon-{name}-{variant}-hover:hover:before';
+ $definition['selectorWithVariant'] = '.oo-ui-image-{variant}.oo-ui-icon-{name}, ' .
+ '.mw-ui-icon-{name}-{variant}:before';
}
// Fields from module definition silently override keys from JSON files
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderSkinModule.php b/www/wiki/includes/resourceloader/ResourceLoaderSkinModule.php
index ca6e59f2..fbd0a24a 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderSkinModule.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderSkinModule.php
@@ -32,7 +32,7 @@ class ResourceLoaderSkinModule extends ResourceLoaderFileModule {
* @return array
*/
public function getStyles( ResourceLoaderContext $context ) {
- $logo = $this->getLogo( $this->getConfig() );
+ $logo = $this->getLogoData( $this->getConfig() );
$styles = parent::getStyles( $context );
$this->normalizeStyles( $styles );
@@ -42,25 +42,34 @@ class ResourceLoaderSkinModule extends ResourceLoaderFileModule {
'; }';
if ( is_array( $logo ) ) {
- if ( isset( $logo['1.5x'] ) ) {
- $styles[
- '(-webkit-min-device-pixel-ratio: 1.5), ' .
- '(min--moz-device-pixel-ratio: 1.5), ' .
+ if ( isset( $logo['svg'] ) ) {
+ $styles['all'][] = '.mw-wiki-logo { ' .
+ 'background-image: -webkit-linear-gradient(transparent, transparent), ' .
+ CSSMin::buildUrlValue( $logo['svg'] ) . '; ' .
+ 'background-image: linear-gradient(transparent, transparent), ' .
+ CSSMin::buildUrlValue( $logo['svg'] ) . ';' .
+ 'background-size: 135px auto; }';
+ } else {
+ if ( isset( $logo['1.5x'] ) ) {
+ $styles[
+ '(-webkit-min-device-pixel-ratio: 1.5), ' .
+ '(min--moz-device-pixel-ratio: 1.5), ' .
'(min-resolution: 1.5dppx), ' .
- '(min-resolution: 144dpi)'
- ][] = '.mw-wiki-logo { background-image: ' .
- CSSMin::buildUrlValue( $logo['1.5x'] ) . ';' .
- 'background-size: 135px auto; }';
- }
- if ( isset( $logo['2x'] ) ) {
- $styles[
- '(-webkit-min-device-pixel-ratio: 2), ' .
- '(min--moz-device-pixel-ratio: 2),' .
- '(min-resolution: 2dppx), ' .
- '(min-resolution: 192dpi)'
- ][] = '.mw-wiki-logo { background-image: ' .
- CSSMin::buildUrlValue( $logo['2x'] ) . ';' .
- 'background-size: 135px auto; }';
+ '(min-resolution: 144dpi)'
+ ][] = '.mw-wiki-logo { background-image: ' .
+ CSSMin::buildUrlValue( $logo['1.5x'] ) . ';' .
+ 'background-size: 135px auto; }';
+ }
+ if ( isset( $logo['2x'] ) ) {
+ $styles[
+ '(-webkit-min-device-pixel-ratio: 2), ' .
+ '(min--moz-device-pixel-ratio: 2), ' .
+ '(min-resolution: 2dppx), ' .
+ '(min-resolution: 192dpi)'
+ ][] = '.mw-wiki-logo { background-image: ' .
+ CSSMin::buildUrlValue( $logo['2x'] ) . ';' .
+ 'background-size: 135px auto; }';
+ }
}
}
@@ -84,10 +93,20 @@ class ResourceLoaderSkinModule extends ResourceLoaderFileModule {
}
/**
+ * @since 1.31
+ * @param Config $conf
+ * @return string|array
+ */
+ protected function getLogoData( Config $conf ) {
+ return static::getLogo( $conf );
+ }
+
+ /**
* @param Config $conf
- * @return string|array Single url if no variants are defined
- * or array of logo urls keyed by dppx in form "<float>x".
- * Key "1x" is always defined.
+ * @return string|array Single url if no variants are defined,
+ * or an array of logo urls keyed by dppx in form "<float>x".
+ * Key "1x" is always defined. Key "svg" may also be defined,
+ * in which case variants other than "1x" are omitted.
*/
public static function getLogo( Config $conf ) {
$logo = $conf->get( 'Logo' );
@@ -103,18 +122,25 @@ class ResourceLoaderSkinModule extends ResourceLoaderFileModule {
'1x' => $logo1Url,
];
- // Only 1.5x and 2x are supported
- if ( isset( $logoHD['1.5x'] ) ) {
- $logoUrls['1.5x'] = OutputPage::transformResourcePath(
+ if ( isset( $logoHD['svg'] ) ) {
+ $logoUrls['svg'] = OutputPage::transformResourcePath(
$conf,
- $logoHD['1.5x']
- );
- }
- if ( isset( $logoHD['2x'] ) ) {
- $logoUrls['2x'] = OutputPage::transformResourcePath(
- $conf,
- $logoHD['2x']
+ $logoHD['svg']
);
+ } else {
+ // Only 1.5x and 2x are supported
+ if ( isset( $logoHD['1.5x'] ) ) {
+ $logoUrls['1.5x'] = OutputPage::transformResourcePath(
+ $conf,
+ $logoHD['1.5x']
+ );
+ }
+ if ( isset( $logoHD['2x'] ) ) {
+ $logoUrls['2x'] = OutputPage::transformResourcePath(
+ $conf,
+ $logoHD['2x']
+ );
+ }
}
return $logoUrls;
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderStartUpModule.php b/www/wiki/includes/resourceloader/ResourceLoaderStartUpModule.php
index 8b9feeb8..56d88358 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderStartUpModule.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderStartUpModule.php
@@ -1,7 +1,5 @@
<?php
/**
- * Module for ResourceLoader initialization.
- *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
@@ -22,6 +20,19 @@
* @author Roan Kattouw
*/
+/**
+ * Module for ResourceLoader initialization.
+ *
+ * See also <https://www.mediawiki.org/wiki/ResourceLoader/Features#Startup_Module>
+ *
+ * The startup module, as being called only from ResourceLoaderClientHtml, has
+ * the ability to vary based extra query parameters, in addition to those
+ * from ResourceLoaderContext:
+ *
+ * - target: Only register modules in the client allowed within this target.
+ * Default: "desktop".
+ * See also: OutputPage::setTarget(), ResourceLoaderModule::getTargets().
+ */
class ResourceLoaderStartUpModule extends ResourceLoaderModule {
// Cache for getConfigSettings() as it's called by multiple methods
@@ -66,6 +77,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
}
$illegalFileChars = $conf->get( 'IllegalFileChars' );
+ $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
// Build list of variables
$vars = [
@@ -76,7 +88,6 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
'wgUrlProtocols' => wfUrlProtocols(),
'wgArticlePath' => $conf->get( 'ArticlePath' ),
'wgScriptPath' => $conf->get( 'ScriptPath' ),
- 'wgScriptExtension' => '.php',
'wgScript' => wfScript(),
'wgSearchType' => $conf->get( 'SearchType' ),
'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ),
@@ -113,6 +124,8 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ),
'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
'wgEnableUploads' => $conf->get( 'EnableUploads' ),
+ 'wgCommentByteLimit' => $oldCommentSchema ? 255 : null,
+ 'wgCommentCodePointLimit' => $oldCommentSchema ? null : CommentStore::COMMENT_CHARACTER_LIMIT,
];
Hooks::run( 'ResourceLoaderGetConfigVars', [ &$vars ] );
@@ -192,7 +205,9 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
*/
public function getModuleRegistrations( ResourceLoaderContext $context ) {
$resourceLoader = $context->getResourceLoader();
- $target = $context->getRequest()->getVal( 'target', 'desktop' );
+ // Future developers: Use WebRequest::getRawVal() instead getVal().
+ // The getVal() method performs slow Language+UTF logic. (f303bb9360)
+ $target = $context->getRequest()->getRawVal( 'target', 'desktop' );
// Bypass target filter if this request is Special:JavaScriptTest.
// To prevent misuse in production, this is only allowed if testing is enabled server-side.
$byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php b/www/wiki/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
deleted file mode 100644
index 9c198d14..00000000
--- a/www/wiki/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-/**
- * ResourceLoader module for user preference customizations.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author Trevor Parscal
- * @author Roan Kattouw
- */
-
-/**
- * Module for user preference customizations
- */
-class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule {
-
- protected $origin = self::ORIGIN_CORE_INDIVIDUAL;
-
- /**
- * @return bool
- */
- public function enableModuleContentVersion() {
- return true;
- }
-
- /**
- * @param ResourceLoaderContext $context
- * @return array
- */
- public function getStyles( ResourceLoaderContext $context ) {
- if ( !$this->getConfig()->get( 'AllowUserCssPrefs' ) ) {
- return [];
- }
-
- $options = $context->getUserObj()->getOptions();
-
- // Build CSS rules
- $rules = [];
-
- // Underline: 2 = skin default, 1 = always, 0 = never
- if ( $options['underline'] < 2 ) {
- $rules[] = "a { text-decoration: " .
- ( $options['underline'] ? 'underline' : 'none' ) . "; }";
- }
- if ( $options['editfont'] !== 'default' ) {
- // Double-check that $options['editfont'] consists of safe characters only
- if ( preg_match( '/^[a-zA-Z0-9_, -]+$/', $options['editfont'] ) ) {
- $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
- }
- }
- $style = implode( "\n", $rules );
- if ( $this->getFlip( $context ) ) {
- $style = CSSJanus::transform( $style, true, false );
- }
- return [ 'all' => $style ];
- }
-
- /**
- * @param ResourceLoaderContext $context
- * @return bool
- */
- public function isKnownEmpty( ResourceLoaderContext $context ) {
- $styles = $this->getStyles( $context );
- return isset( $styles['all'] ) && $styles['all'] === '';
- }
-
- /**
- * @return string
- */
- public function getGroup() {
- return 'private';
- }
-}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderUserGroupsModule.php b/www/wiki/includes/resourceloader/ResourceLoaderUserGroupsModule.php
deleted file mode 100644
index e2a8e410..00000000
--- a/www/wiki/includes/resourceloader/ResourceLoaderUserGroupsModule.php
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-/**
- * ResourceLoader module for user customizations.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * Module for user customizations
- */
-class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule {
-
- protected $origin = self::ORIGIN_USER_SITEWIDE;
- protected $targets = [ 'desktop', 'mobile' ];
-
- /**
- * @param ResourceLoaderContext $context
- * @return array
- */
- protected function getPages( ResourceLoaderContext $context ) {
- $useSiteJs = $this->getConfig()->get( 'UseSiteJs' );
- $useSiteCss = $this->getConfig()->get( 'UseSiteCss' );
- if ( !$useSiteJs && !$useSiteCss ) {
- return [];
- }
-
- $user = $context->getUserObj();
- if ( !$user || $user->isAnon() ) {
- return [];
- }
-
- $pages = [];
- foreach ( $user->getEffectiveGroups() as $group ) {
- if ( $group == '*' ) {
- continue;
- }
- if ( $useSiteJs ) {
- $pages["MediaWiki:Group-$group.js"] = [ 'type' => 'script' ];
- }
- if ( $useSiteCss ) {
- $pages["MediaWiki:Group-$group.css"] = [ 'type' => 'style' ];
- }
- }
- return $pages;
- }
-
- /**
- * Get group name
- *
- * @return string
- */
- public function getGroup() {
- return 'user';
- }
-}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/www/wiki/includes/resourceloader/ResourceLoaderUserOptionsModule.php
index 0c332cff..ffa55c08 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderUserOptionsModule.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderUserOptionsModule.php
@@ -51,7 +51,9 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
* @return string JavaScript code
*/
public function getScript( ResourceLoaderContext $context ) {
- return Xml::encodeJsCall( 'mw.user.options.set',
+ // Use FILTER_NOMIN annotation to prevent needless minification and caching (T84960).
+ return ResourceLoader::FILTER_NOMIN . Xml::encodeJsCall(
+ 'mw.user.options.set',
[ $context->getUserObj()->getOptions( User::GETOPTIONS_EXCLUDE_DEFAULTS ) ],
ResourceLoader::inDebugMode()
);
@@ -65,6 +67,14 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
}
/**
+ * @param ResourceLoaderContext $context
+ * @return bool
+ */
+ public function isKnownEmpty( ResourceLoaderContext $context ) {
+ return !$context->getUserObj()->getOptions( User::GETOPTIONS_EXCLUDE_DEFAULTS );
+ }
+
+ /**
* @return string
*/
public function getGroup() {
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderUserTokensModule.php b/www/wiki/includes/resourceloader/ResourceLoaderUserTokensModule.php
index bfa7326d..ae4fb67b 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderUserTokensModule.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderUserTokensModule.php
@@ -26,14 +26,10 @@
*/
class ResourceLoaderUserTokensModule extends ResourceLoaderModule {
- /* Protected Members */
-
protected $origin = self::ORIGIN_CORE_INDIVIDUAL;
protected $targets = [ 'desktop', 'mobile' ];
- /* Methods */
-
/**
* Fetch the tokens for the current user.
*
@@ -52,19 +48,16 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule {
}
/**
- * Generate the JavaScript content of this module.
- *
- * Add FILTER_NOMIN annotation to prevent needless minification and caching (T84960).
- *
* @param ResourceLoaderContext $context
* @return string JavaScript code
*/
public function getScript( ResourceLoaderContext $context ) {
- return Xml::encodeJsCall(
+ // Use FILTER_NOMIN annotation to prevent needless minification and caching (T84960).
+ return ResourceLoader::FILTER_NOMIN . Xml::encodeJsCall(
'mw.user.tokens.set',
[ $this->contextUserTokens( $context ) ],
ResourceLoader::inDebugMode()
- ) . ResourceLoader::FILTER_NOMIN;
+ );
}
/**
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderWikiModule.php b/www/wiki/includes/resourceloader/ResourceLoaderWikiModule.php
index bebc1887..5b512af7 100644
--- a/www/wiki/includes/resourceloader/ResourceLoaderWikiModule.php
+++ b/www/wiki/includes/resourceloader/ResourceLoaderWikiModule.php
@@ -29,8 +29,8 @@ use Wikimedia\Rdbms\IDatabase;
* Abstraction for ResourceLoader 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
- * and Title::isCssOrJsPage().
+ * because of its dependence on the functionality of Title::isUserConfigPage()
+ * and Title::isSiteConfigPage().
*
* 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
@@ -183,12 +183,10 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
* @return Content|null
*/
protected function getContentObj( Title $title ) {
- $revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title->getArticleID(),
- $title->getLatestRevID() );
+ $revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title );
if ( !$revision ) {
return null;
}
- $revision->setTitle( $title );
$content = $revision->getContent( Revision::RAW );
if ( !$content ) {
wfDebugLog( 'resourceloader', __METHOD__ . ': failed to load content of JS/CSS page!' );
@@ -452,7 +450,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
} elseif ( $new && in_array( $new->getContentFormat(), $formats ) ) {
$purge = true;
} else {
- $purge = ( $title->isCssOrJsPage() || $title->isCssJsSubpage() );
+ $purge = ( $title->isSiteConfigPage() || $title->isUserConfigPage() );
}
if ( $purge ) {
diff --git a/www/wiki/includes/revisiondelete/RevDelArchiveItem.php b/www/wiki/includes/revisiondelete/RevDelArchiveItem.php
index ab74dbd2..679acc64 100644
--- a/www/wiki/includes/revisiondelete/RevDelArchiveItem.php
+++ b/www/wiki/includes/revisiondelete/RevDelArchiveItem.php
@@ -45,6 +45,10 @@ class RevDelArchiveItem extends RevDelRevisionItem {
return 'ar_user_text';
}
+ public function getAuthorActorField() {
+ return 'ar_actor';
+ }
+
public function getId() {
# Convert DB timestamp to MW timestamp
return $this->revision->getTimestamp();
diff --git a/www/wiki/includes/revisiondelete/RevDelArchiveList.php b/www/wiki/includes/revisiondelete/RevDelArchiveList.php
index 9afaf404..4f66cdae 100644
--- a/www/wiki/includes/revisiondelete/RevDelArchiveList.php
+++ b/www/wiki/includes/revisiondelete/RevDelArchiveList.php
@@ -43,14 +43,15 @@ class RevDelArchiveList extends RevDelRevisionList {
$timestamps[] = $db->timestamp( $id );
}
- $tables = [ 'archive' ];
- $fields = Revision::selectArchiveFields();
+ $arQuery = Revision::getArchiveQueryInfo();
+ $tables = $arQuery['tables'];
+ $fields = $arQuery['fields'];
$conds = [
'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey(),
'ar_timestamp' => $timestamps,
];
- $join_conds = [];
+ $join_conds = $arQuery['joins'];
$options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
ChangeTags::modifyDisplayQuery(
diff --git a/www/wiki/includes/revisiondelete/RevDelArchivedFileItem.php b/www/wiki/includes/revisiondelete/RevDelArchivedFileItem.php
index b0984224..d36fac92 100644
--- a/www/wiki/includes/revisiondelete/RevDelArchivedFileItem.php
+++ b/www/wiki/includes/revisiondelete/RevDelArchivedFileItem.php
@@ -50,6 +50,10 @@ class RevDelArchivedFileItem extends RevDelFileItem {
return 'fa_user_text';
}
+ public function getAuthorActorField() {
+ return 'fa_actor';
+ }
+
public function getId() {
return $this->row->fa_id;
}
diff --git a/www/wiki/includes/revisiondelete/RevDelArchivedFileList.php b/www/wiki/includes/revisiondelete/RevDelArchivedFileList.php
index 1d80d869..1ed87263 100644
--- a/www/wiki/includes/revisiondelete/RevDelArchivedFileList.php
+++ b/www/wiki/includes/revisiondelete/RevDelArchivedFileList.php
@@ -40,15 +40,17 @@ class RevDelArchivedFileList extends RevDelFileList {
public function doQuery( $db ) {
$ids = array_map( 'intval', $this->ids );
+ $fileQuery = ArchivedFile::getQueryInfo();
return $db->select(
- 'filearchive',
- ArchivedFile::selectFields(),
+ $fileQuery['tables'],
+ $fileQuery['fields'],
[
'fa_name' => $this->title->getDBkey(),
'fa_id' => $ids
],
__METHOD__,
- [ 'ORDER BY' => 'fa_id DESC' ]
+ [ 'ORDER BY' => 'fa_id DESC' ],
+ $fileQuery['joins']
);
}
diff --git a/www/wiki/includes/revisiondelete/RevDelFileItem.php b/www/wiki/includes/revisiondelete/RevDelFileItem.php
index 9beafc98..0ca84d7f 100644
--- a/www/wiki/includes/revisiondelete/RevDelFileItem.php
+++ b/www/wiki/includes/revisiondelete/RevDelFileItem.php
@@ -49,6 +49,10 @@ class RevDelFileItem extends RevDelItem {
return 'oi_user_text';
}
+ public function getAuthorActorField() {
+ return 'oi_actor';
+ }
+
public function getId() {
$parts = explode( '!', $this->row->oi_archive_name );
diff --git a/www/wiki/includes/revisiondelete/RevDelFileList.php b/www/wiki/includes/revisiondelete/RevDelFileList.php
index 77cf9767..6a6b86c0 100644
--- a/www/wiki/includes/revisiondelete/RevDelFileList.php
+++ b/www/wiki/includes/revisiondelete/RevDelFileList.php
@@ -60,15 +60,17 @@ class RevDelFileList extends RevDelList {
$archiveNames[] = $timestamp . '!' . $this->title->getDBkey();
}
+ $oiQuery = OldLocalFile::getQueryInfo();
return $db->select(
- 'oldimage',
- OldLocalFile::selectFields(),
+ $oiQuery['tables'],
+ $oiQuery['fields'],
[
'oi_name' => $this->title->getDBkey(),
'oi_archive_name' => $archiveNames
],
__METHOD__,
- [ 'ORDER BY' => 'oi_timestamp DESC' ]
+ [ 'ORDER BY' => 'oi_timestamp DESC' ],
+ $oiQuery['joins']
);
}
diff --git a/www/wiki/includes/revisiondelete/RevDelItem.php b/www/wiki/includes/revisiondelete/RevDelItem.php
index bf97bd41..47620852 100644
--- a/www/wiki/includes/revisiondelete/RevDelItem.php
+++ b/www/wiki/includes/revisiondelete/RevDelItem.php
@@ -57,7 +57,7 @@ abstract class RevDelItem extends RevisionItemBase {
/**
* Get the return information about the revision for the API
* @since 1.23
- * @param ApiResult $result API result object
+ * @param ApiResult $result
* @return array Data for the API result
*/
abstract public function getApiData( ApiResult $result );
diff --git a/www/wiki/includes/revisiondelete/RevDelList.php b/www/wiki/includes/revisiondelete/RevDelList.php
index 011c7b09..89025bc6 100644
--- a/www/wiki/includes/revisiondelete/RevDelList.php
+++ b/www/wiki/includes/revisiondelete/RevDelList.php
@@ -106,6 +106,8 @@ abstract class RevDelList extends RevisionListBase {
* @since 1.23 Added 'perItemStatus' param
*/
public function setVisibility( array $params ) {
+ global $wgActorTableSchemaMigrationStage;
+
$status = Status::newGood();
$bitPars = $params['value'];
@@ -134,7 +136,7 @@ abstract class RevDelList extends RevisionListBase {
$missing = array_flip( $this->ids );
$this->clearFileOps();
$idsForLog = [];
- $authorIds = $authorIPs = [];
+ $authorIds = $authorIPs = $authorActors = [];
if ( $perItemStatus ) {
$status->itemStatuses = [];
@@ -216,10 +218,15 @@ abstract class RevDelList extends RevisionListBase {
$virtualOldBits |= $removedBits;
$status->successCount++;
- if ( $item->getAuthorId() > 0 ) {
- $authorIds[] = $item->getAuthorId();
- } elseif ( IP::isIPAddress( $item->getAuthorName() ) ) {
- $authorIPs[] = $item->getAuthorName();
+ if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+ if ( $item->getAuthorId() > 0 ) {
+ $authorIds[] = $item->getAuthorId();
+ } elseif ( IP::isIPAddress( $item->getAuthorName() ) ) {
+ $authorIPs[] = $item->getAuthorName();
+ }
+ }
+ if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+ $authorActors[] = $item->getAuthorActor();
}
// Save the old and new bits in $visibilityChangeMap for
@@ -263,6 +270,14 @@ abstract class RevDelList extends RevisionListBase {
}
// Log it
+ $authorFields = [];
+ if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+ $authorFields['authorIds'] = $authorIds;
+ $authorFields['authorIPs'] = $authorIPs;
+ }
+ if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+ $authorFields['authorActors'] = $authorActors;
+ }
$this->updateLog(
$logType,
[
@@ -272,10 +287,8 @@ abstract class RevDelList extends RevisionListBase {
'oldBits' => $virtualOldBits,
'comment' => $comment,
'ids' => $idsForLog,
- 'authorIds' => $authorIds,
- 'authorIPs' => $authorIPs,
'tags' => isset( $params['tags'] ) ? $params['tags'] : [],
- ]
+ ] + $authorFields
);
// Clear caches after commit
@@ -330,8 +343,9 @@ abstract class RevDelList extends RevisionListBase {
* title: The target title
* ids: The ID list
* comment: The log comment
- * authorsIds: The array of the user IDs of the offenders
- * authorsIPs: The array of the IP/anon user offenders
+ * authorIds: The array of the user IDs of the offenders
+ * authorIPs: The array of the IP/anon user offenders
+ * authorActors: The array of the actor IDs of the offenders
* tags: The array of change tags to apply to the log entry
* @throws MWException
*/
@@ -350,11 +364,21 @@ abstract class RevDelList extends RevisionListBase {
$logEntry->setParameters( $logParams );
$logEntry->setPerformer( $this->getUser() );
// Allow for easy searching of deletion log items for revision/log items
- $logEntry->setRelations( [
+ $relations = [
$field => $params['ids'],
- 'target_author_id' => $params['authorIds'],
- 'target_author_ip' => $params['authorIPs'],
- ] );
+ ];
+ if ( isset( $params['authorIds'] ) ) {
+ $relations += [
+ 'target_author_id' => $params['authorIds'],
+ 'target_author_ip' => $params['authorIPs'],
+ ];
+ }
+ if ( isset( $params['authorActors'] ) ) {
+ $relations += [
+ 'target_author_actor' => $params['authorActors'],
+ ];
+ }
+ $logEntry->setRelations( $relations );
// Apply change tags to the log entry
$logEntry->setTags( $params['tags'] );
$logId = $logEntry->insert();
diff --git a/www/wiki/includes/revisiondelete/RevDelLogItem.php b/www/wiki/includes/revisiondelete/RevDelLogItem.php
index 998c695f..36198cd2 100644
--- a/www/wiki/includes/revisiondelete/RevDelLogItem.php
+++ b/www/wiki/includes/revisiondelete/RevDelLogItem.php
@@ -39,6 +39,10 @@ class RevDelLogItem extends RevDelItem {
return 'log_user_text';
}
+ public function getAuthorActorField() {
+ return 'log_actor';
+ }
+
public function canView() {
return LogEventsList::userCan( $this->row, Revision::DELETED_RESTRICTED, $this->list->getUser() );
}
@@ -71,7 +75,7 @@ class RevDelLogItem extends RevDelItem {
$dbw->update( 'recentchanges',
[
'rc_deleted' => $bits,
- 'rc_patrolled' => 1
+ 'rc_patrolled' => RecentChange::PRC_PATROLLED
],
[
'rc_logid' => $this->row->log_id,
@@ -102,7 +106,7 @@ class RevDelLogItem extends RevDelItem {
// User links and action text
$action = $formatter->getActionText();
// Comment
- $comment = CommentStore::newKey( 'log_comment' )->getComment( $this->row )->text;
+ $comment = CommentStore::getStore()->getComment( 'log_comment', $this->row )->text;
$comment = $this->list->getLanguage()->getDirMark()
. Linker::commentBlock( $comment );
@@ -136,7 +140,8 @@ class RevDelLogItem extends RevDelItem {
}
if ( LogEventsList::userCan( $this->row, LogPage::DELETED_COMMENT, $user ) ) {
$ret += [
- 'comment' => CommentStore::newKey( 'log_comment' )->getComment( $this->row )->text,
+ 'comment' => CommentStore::getStore()->getComment( 'log_comment', $this->row )
+ ->text,
];
}
diff --git a/www/wiki/includes/revisiondelete/RevDelLogList.php b/www/wiki/includes/revisiondelete/RevDelLogList.php
index 728deb8e..b26fffd1 100644
--- a/www/wiki/includes/revisiondelete/RevDelLogList.php
+++ b/www/wiki/includes/revisiondelete/RevDelLogList.php
@@ -63,27 +63,26 @@ class RevDelLogList extends RevDelList {
public function doQuery( $db ) {
$ids = array_map( 'intval', $this->ids );
- $commentQuery = CommentStore::newKey( 'log_comment' )->getJoin();
+ $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
return $db->select(
- [ 'logging' ] + $commentQuery['tables'],
+ [ 'logging' ] + $commentQuery['tables'] + $actorQuery['tables'],
[
'log_id',
'log_type',
'log_action',
'log_timestamp',
- 'log_user',
- 'log_user_text',
'log_namespace',
'log_title',
'log_page',
'log_params',
'log_deleted'
- ] + $commentQuery['fields'],
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
[ 'log_id' => $ids ],
__METHOD__,
[ 'ORDER BY' => 'log_id DESC' ],
- $commentQuery['joins']
+ $commentQuery['joins'] + $actorQuery['joins']
);
}
diff --git a/www/wiki/includes/revisiondelete/RevDelRevisionItem.php b/www/wiki/includes/revisiondelete/RevDelRevisionItem.php
index a9753b44..7b5d130b 100644
--- a/www/wiki/includes/revisiondelete/RevDelRevisionItem.php
+++ b/www/wiki/includes/revisiondelete/RevDelRevisionItem.php
@@ -47,6 +47,10 @@ class RevDelRevisionItem extends RevDelItem {
return 'rev_user_text';
}
+ public function getAuthorActorField() {
+ return 'rev_actor';
+ }
+
public function canView() {
return $this->revision->userCan( Revision::DELETED_RESTRICTED, $this->list->getUser() );
}
@@ -79,7 +83,7 @@ class RevDelRevisionItem extends RevDelItem {
$dbw->update( 'recentchanges',
[
'rc_deleted' => $bits,
- 'rc_patrolled' => 1
+ 'rc_patrolled' => RecentChange::PRC_PATROLLED
],
[
'rc_this_oldid' => $this->revision->getId(), // condition
diff --git a/www/wiki/includes/revisiondelete/RevDelRevisionList.php b/www/wiki/includes/revisiondelete/RevDelRevisionList.php
index 1ea6a381..07362c42 100644
--- a/www/wiki/includes/revisiondelete/RevDelRevisionList.php
+++ b/www/wiki/includes/revisiondelete/RevDelRevisionList.php
@@ -62,9 +62,10 @@ class RevDelRevisionList extends RevDelList {
*/
public function doQuery( $db ) {
$ids = array_map( 'intval', $this->ids );
+ $revQuery = Revision::getQueryInfo( [ 'page', 'user' ] );
$queryInfo = [
- 'tables' => [ 'revision', 'page', 'user' ],
- 'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ),
+ 'tables' => $revQuery['tables'],
+ 'fields' => $revQuery['fields'],
'conds' => [
'rev_page' => $this->title->getArticleID(),
'rev_id' => $ids,
@@ -73,10 +74,7 @@ class RevDelRevisionList extends RevDelList {
'ORDER BY' => 'rev_id DESC',
'USE INDEX' => [ 'revision' => 'PRIMARY' ] // workaround for MySQL bug (T104313)
],
- 'join_conds' => [
- 'page' => Revision::pageJoinCond(),
- 'user' => Revision::userJoinCond(),
- ],
+ 'join_conds' => $revQuery['joins'],
];
ChangeTags::modifyDisplayQuery(
$queryInfo['tables'],
@@ -100,14 +98,15 @@ class RevDelRevisionList extends RevDelList {
return $live;
}
+ $arQuery = Revision::getArchiveQueryInfo();
$archiveQueryInfo = [
- 'tables' => [ 'archive' ],
- 'fields' => Revision::selectArchiveFields(),
+ 'tables' => $arQuery['tables'],
+ 'fields' => $arQuery['fields'],
'conds' => [
'ar_rev_id' => $ids,
],
'options' => [ 'ORDER BY' => 'ar_rev_id DESC' ],
- 'join_conds' => [],
+ 'join_conds' => $arQuery['joins'],
];
ChangeTags::modifyDisplayQuery(
diff --git a/www/wiki/includes/revisiondelete/RevisionDeleteUser.php b/www/wiki/includes/revisiondelete/RevisionDeleteUser.php
index 7812fb98..6291e8d9 100644
--- a/www/wiki/includes/revisiondelete/RevisionDeleteUser.php
+++ b/www/wiki/includes/revisiondelete/RevisionDeleteUser.php
@@ -42,6 +42,8 @@ class RevisionDeleteUser {
* @return bool
*/
private static function setUsernameBitfields( $name, $userId, $op, $dbw ) {
+ global $wgActorTableSchemaMigrationStage;
+
if ( !$userId || ( $op !== '|' && $op !== '&' ) ) {
return false; // sanity check
}
@@ -65,43 +67,120 @@ class RevisionDeleteUser {
$userTitle = Title::makeTitleSafe( NS_USER, $name );
$userDbKey = $userTitle->getDBkey();
- # Hide name from live edits
- $dbw->update(
- 'revision',
- [ self::buildSetBitDeletedField( 'rev_deleted', $op, $delUser, $dbw ) ],
- [ 'rev_user' => $userId ],
- __METHOD__ );
+ if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+ # Hide name from live edits
+ $dbw->update(
+ 'revision',
+ [ self::buildSetBitDeletedField( 'rev_deleted', $op, $delUser, $dbw ) ],
+ [ 'rev_user' => $userId ],
+ __METHOD__ );
- # Hide name from deleted edits
- $dbw->update(
- 'archive',
- [ self::buildSetBitDeletedField( 'ar_deleted', $op, $delUser, $dbw ) ],
- [ 'ar_user_text' => $name ],
- __METHOD__
- );
+ # Hide name from deleted edits
+ $dbw->update(
+ 'archive',
+ [ self::buildSetBitDeletedField( 'ar_deleted', $op, $delUser, $dbw ) ],
+ [ 'ar_user_text' => $name ],
+ __METHOD__
+ );
- # Hide name from logs
- $dbw->update(
- 'logging',
- [ self::buildSetBitDeletedField( 'log_deleted', $op, $delUser, $dbw ) ],
- [ 'log_user' => $userId, 'log_type != ' . $dbw->addQuotes( 'suppress' ) ],
- __METHOD__
- );
+ # Hide name from logs
+ $dbw->update(
+ 'logging',
+ [ self::buildSetBitDeletedField( 'log_deleted', $op, $delUser, $dbw ) ],
+ [ 'log_user' => $userId, 'log_type != ' . $dbw->addQuotes( 'suppress' ) ],
+ __METHOD__
+ );
+
+ # Hide name from RC
+ $dbw->update(
+ 'recentchanges',
+ [ self::buildSetBitDeletedField( 'rc_deleted', $op, $delUser, $dbw ) ],
+ [ 'rc_user_text' => $name ],
+ __METHOD__
+ );
+
+ # Hide name from live images
+ $dbw->update(
+ 'oldimage',
+ [ self::buildSetBitDeletedField( 'oi_deleted', $op, $delUser, $dbw ) ],
+ [ 'oi_user_text' => $name ],
+ __METHOD__
+ );
+
+ # Hide name from deleted images
+ $dbw->update(
+ 'filearchive',
+ [ self::buildSetBitDeletedField( 'fa_deleted', $op, $delUser, $dbw ) ],
+ [ 'fa_user_text' => $name ],
+ __METHOD__
+ );
+ }
+
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+ $actorId = $dbw->selectField( 'actor', 'actor_id', [ 'actor_name' => $name ], __METHOD__ );
+ if ( $actorId ) {
+ # Hide name from live edits
+ $subquery = $dbw->selectSQLText(
+ 'revision_actor_temp', 'revactor_rev', [ 'revactor_actor' => $actorId ], __METHOD__
+ );
+ $dbw->update(
+ 'revision',
+ [ self::buildSetBitDeletedField( 'rev_deleted', $op, $delUser, $dbw ) ],
+ [ "rev_id IN ($subquery)" ],
+ __METHOD__ );
+
+ # Hide name from deleted edits
+ $dbw->update(
+ 'archive',
+ [ self::buildSetBitDeletedField( 'ar_deleted', $op, $delUser, $dbw ) ],
+ [ 'ar_actor' => $actorId ],
+ __METHOD__
+ );
+
+ # Hide name from logs
+ $dbw->update(
+ 'logging',
+ [ self::buildSetBitDeletedField( 'log_deleted', $op, $delUser, $dbw ) ],
+ [ 'log_actor' => $actorId, 'log_type != ' . $dbw->addQuotes( 'suppress' ) ],
+ __METHOD__
+ );
+
+ # Hide name from RC
+ $dbw->update(
+ 'recentchanges',
+ [ self::buildSetBitDeletedField( 'rc_deleted', $op, $delUser, $dbw ) ],
+ [ 'rc_actor' => $actorId ],
+ __METHOD__
+ );
+
+ # Hide name from live images
+ $dbw->update(
+ 'oldimage',
+ [ self::buildSetBitDeletedField( 'oi_deleted', $op, $delUser, $dbw ) ],
+ [ 'oi_actor' => $actorId ],
+ __METHOD__
+ );
+
+ # Hide name from deleted images
+ $dbw->update(
+ 'filearchive',
+ [ self::buildSetBitDeletedField( 'fa_deleted', $op, $delUser, $dbw ) ],
+ [ 'fa_actor' => $actorId ],
+ __METHOD__
+ );
+ }
+ }
+
+ # Hide log entries pointing to the user page
$dbw->update(
'logging',
[ self::buildSetBitDeletedField( 'log_deleted', $op, $delAction, $dbw ) ],
[ 'log_namespace' => NS_USER, 'log_title' => $userDbKey,
- 'log_type != ' . $dbw->addQuotes( 'suppress' ) ],
+ 'log_type != ' . $dbw->addQuotes( 'suppress' ) ],
__METHOD__
);
- # Hide name from RC
- $dbw->update(
- 'recentchanges',
- [ self::buildSetBitDeletedField( 'rc_deleted', $op, $delUser, $dbw ) ],
- [ 'rc_user_text' => $name ],
- __METHOD__
- );
+ # Hide RC entries pointing to the user page
$dbw->update(
'recentchanges',
[ self::buildSetBitDeletedField( 'rc_deleted', $op, $delAction, $dbw ) ],
@@ -109,21 +188,6 @@ class RevisionDeleteUser {
__METHOD__
);
- # Hide name from live images
- $dbw->update(
- 'oldimage',
- [ self::buildSetBitDeletedField( 'oi_deleted', $op, $delUser, $dbw ) ],
- [ 'oi_user_text' => $name ],
- __METHOD__
- );
-
- # Hide name from deleted images
- $dbw->update(
- 'filearchive',
- [ self::buildSetBitDeletedField( 'fa_deleted', $op, $delUser, $dbw ) ],
- [ 'fa_user_text' => $name ],
- __METHOD__
- );
# Done!
return true;
}
diff --git a/www/wiki/includes/revisiondelete/RevisionDeleter.php b/www/wiki/includes/revisiondelete/RevisionDeleter.php
index 76fa590d..7b2147a5 100644
--- a/www/wiki/includes/revisiondelete/RevisionDeleter.php
+++ b/www/wiki/includes/revisiondelete/RevisionDeleter.php
@@ -29,11 +29,11 @@
class RevisionDeleter {
/** List of known revdel types, with their corresponding list classes */
private static $allowedTypes = [
- 'revision' => 'RevDelRevisionList',
- 'archive' => 'RevDelArchiveList',
- 'oldimage' => 'RevDelFileList',
- 'filearchive' => 'RevDelArchivedFileList',
- 'logging' => 'RevDelLogList',
+ 'revision' => RevDelRevisionList::class,
+ 'archive' => RevDelArchiveList::class,
+ 'oldimage' => RevDelFileList::class,
+ 'filearchive' => RevDelArchivedFileList::class,
+ 'logging' => RevDelLogList::class,
];
/** Type map to support old log entries */
diff --git a/www/wiki/includes/search/SearchEngine.php b/www/wiki/includes/search/SearchEngine.php
index 3c8fe608..bfcfb598 100644
--- a/www/wiki/includes/search/SearchEngine.php
+++ b/www/wiki/includes/search/SearchEngine.php
@@ -112,11 +112,11 @@ abstract class SearchEngine {
*/
public function supports( $feature ) {
switch ( $feature ) {
- case 'search-update':
- return true;
- case 'title-suffix-filter':
- default:
- return false;
+ case 'search-update':
+ return true;
+ case 'title-suffix-filter':
+ default:
+ return false;
}
}
@@ -407,17 +407,6 @@ abstract class SearchEngine {
}
/**
- * Get OpenSearch suggestion template
- *
- * @deprecated since 1.25
- * @return string
- */
- public static function getOpenSearchTemplate() {
- wfDeprecated( __METHOD__, '1.25' );
- return ApiOpenSearch::getOpenSearchTemplate( 'application/x-suggestions+json' );
- }
-
- /**
* Get the raw text for updating the index from a content object
* Nicer search backends could possibly do something cooler than
* just returning raw text
@@ -454,7 +443,9 @@ abstract class SearchEngine {
$ns = $this->namespaces;
if ( $title && !$title->isExternal() ) {
$ns = [ $title->getNamespace() ];
- $search = $title->getText();
+ if ( $title->getNamespace() !== NS_MAIN ) {
+ $search = substr( $search, strpos( $search, ':' ) + 1 );
+ }
if ( $ns[0] == NS_MAIN ) {
$ns = $this->namespaces; // no explicit prefix, use default namespaces
Hooks::run( 'PrefixSearchExtractNamespace', [ &$ns, &$search ] );
@@ -543,7 +534,7 @@ abstract class SearchEngine {
$this->setLimitOffset( $fallbackLimit );
$fallbackSearchResult = $this->completionSearch( $fbs );
$results->appendAll( $fallbackSearchResult );
- $fallbackLimit -= count( $fallbackSearchResult );
+ $fallbackLimit -= $fallbackSearchResult->getSize();
if ( $fallbackLimit <= 0 ) {
break;
}
diff --git a/www/wiki/includes/search/SearchEngineFactory.php b/www/wiki/includes/search/SearchEngineFactory.php
index 613d33ca..8cdca571 100644
--- a/www/wiki/includes/search/SearchEngineFactory.php
+++ b/www/wiki/includes/search/SearchEngineFactory.php
@@ -49,17 +49,17 @@ class SearchEngineFactory {
public static function getSearchEngineClass( IDatabase $db ) {
switch ( $db->getType() ) {
case 'sqlite':
- return 'SearchSqlite';
+ return SearchSqlite::class;
case 'mysql':
- return 'SearchMySQL';
+ return SearchMySQL::class;
case 'postgres':
- return 'SearchPostgres';
+ return SearchPostgres::class;
case 'mssql':
- return 'SearchMssql';
+ return SearchMssql::class;
case 'oracle':
- return 'SearchOracle';
+ return SearchOracle::class;
default:
- return 'SearchEngineDummy';
+ return SearchEngineDummy::class;
}
}
}
diff --git a/www/wiki/includes/search/SearchExactMatchRescorer.php b/www/wiki/includes/search/SearchExactMatchRescorer.php
index 0e99ba91..354b3909 100644
--- a/www/wiki/includes/search/SearchExactMatchRescorer.php
+++ b/www/wiki/includes/search/SearchExactMatchRescorer.php
@@ -34,7 +34,7 @@ class SearchExactMatchRescorer {
* may sort based on other algorithms that may cause the exact title match
* to not be in the results or be lower down the list.
* @param string $search the query
- * @param int[] $namespaces the namespaces
+ * @param int[] $namespaces
* @param string[] $srchres results
* @param int $limit the max number of results to return
* @return string[] munged results
@@ -96,11 +96,11 @@ class SearchExactMatchRescorer {
}
/**
- * @param string[] $titles as strings
+ * @param string[] $titles
* @return array redirect target prefixedText to index of title in titles
* that is a redirect to it.
*/
- private function redirectTargetsToRedirect( $titles ) {
+ private function redirectTargetsToRedirect( array $titles ) {
$result = [];
foreach ( $titles as $key => $titleText ) {
$title = Title::newFromText( $titleText );
@@ -122,7 +122,7 @@ class SearchExactMatchRescorer {
* @param int $key key to pull to the front
* @return array $array with the item at $key pulled to the front
*/
- private function pullFront( $key, $array ) {
+ private function pullFront( $key, array $array ) {
$cut = array_splice( $array, $key, 1 );
array_unshift( $array, $cut[0] );
return $array;
diff --git a/www/wiki/includes/search/SearchMySQL.php b/www/wiki/includes/search/SearchMySQL.php
index 77dcfe9c..8e705c1f 100644
--- a/www/wiki/includes/search/SearchMySQL.php
+++ b/www/wiki/includes/search/SearchMySQL.php
@@ -54,9 +54,9 @@ class SearchMySQL extends SearchDatabase {
if ( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
$filteredText, $m, PREG_SET_ORDER ) ) {
foreach ( $m as $bits ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
list( /* all */, $modifier, $term, $nonQuoted, $wildcard ) = $bits;
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $nonQuoted != '' ) {
$term = $nonQuoted;
@@ -209,10 +209,10 @@ class SearchMySQL extends SearchDatabase {
public function supports( $feature ) {
switch ( $feature ) {
- case 'title-suffix-filter':
- return true;
- default:
- return parent::supports( $feature );
+ case 'title-suffix-filter':
+ return true;
+ default:
+ return parent::supports( $feature );
}
}
diff --git a/www/wiki/includes/search/SearchSqlite.php b/www/wiki/includes/search/SearchSqlite.php
index 3d4da42c..af29212b 100644
--- a/www/wiki/includes/search/SearchSqlite.php
+++ b/www/wiki/includes/search/SearchSqlite.php
@@ -52,9 +52,9 @@ class SearchSqlite extends SearchDatabase {
if ( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
$filteredText, $m, PREG_SET_ORDER ) ) {
foreach ( $m as $bits ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
list( /* all */, $modifier, $term, $nonQuoted, $wildcard ) = $bits;
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $nonQuoted != '' ) {
$term = $nonQuoted;
diff --git a/www/wiki/includes/session/PHPSessionHandler.php b/www/wiki/includes/session/PHPSessionHandler.php
index b76f0ff6..e1415df4 100644
--- a/www/wiki/includes/session/PHPSessionHandler.php
+++ b/www/wiki/includes/session/PHPSessionHandler.php
@@ -122,22 +122,28 @@ class PHPSessionHandler implements \SessionHandlerInterface {
// Close any auto-started session, before we replace it
session_write_close();
- // Tell PHP not to mess with cookies itself
- ini_set( 'session.use_cookies', 0 );
- ini_set( 'session.use_trans_sid', 0 );
-
- // T124510: Disable automatic PHP session related cache headers.
- // MediaWiki adds it's own headers and the default PHP behavior may
- // set headers such as 'Pragma: no-cache' that cause problems with
- // some user agents.
- session_cache_limiter( '' );
-
- // Also set a sane serialization handler
- \Wikimedia\PhpSessionSerializer::setSerializeHandler();
-
- // Register this as the save handler, and register an appropriate
- // shutdown function.
- session_set_save_handler( self::$instance, true );
+ try {
+ \Wikimedia\suppressWarnings();
+
+ // Tell PHP not to mess with cookies itself
+ ini_set( 'session.use_cookies', 0 );
+ ini_set( 'session.use_trans_sid', 0 );
+
+ // T124510: Disable automatic PHP session related cache headers.
+ // MediaWiki adds it's own headers and the default PHP behavior may
+ // set headers such as 'Pragma: no-cache' that cause problems with
+ // some user agents.
+ session_cache_limiter( '' );
+
+ // Also set a sane serialization handler
+ \Wikimedia\PhpSessionSerializer::setSerializeHandler();
+
+ // Register this as the save handler, and register an appropriate
+ // shutdown function.
+ session_set_save_handler( self::$instance, true );
+ } finally {
+ \Wikimedia\restoreWarnings();
+ }
}
/**
diff --git a/www/wiki/includes/session/Session.php b/www/wiki/includes/session/Session.php
index 23d9ab38..024bf9a2 100644
--- a/www/wiki/includes/session/Session.php
+++ b/www/wiki/includes/session/Session.php
@@ -621,31 +621,37 @@ final class Session implements \Countable, \Iterator, \ArrayAccess {
* @{
*/
+ /** @inheritDoc */
public function count() {
$data = &$this->backend->getData();
return count( $data );
}
+ /** @inheritDoc */
public function current() {
$data = &$this->backend->getData();
return current( $data );
}
+ /** @inheritDoc */
public function key() {
$data = &$this->backend->getData();
return key( $data );
}
+ /** @inheritDoc */
public function next() {
$data = &$this->backend->getData();
next( $data );
}
+ /** @inheritDoc */
public function rewind() {
$data = &$this->backend->getData();
reset( $data );
}
+ /** @inheritDoc */
public function valid() {
$data = &$this->backend->getData();
return key( $data ) !== null;
@@ -678,10 +684,12 @@ final class Session implements \Countable, \Iterator, \ArrayAccess {
return $data[$offset];
}
+ /** @inheritDoc */
public function offsetSet( $offset, $value ) {
$this->set( $offset, $value );
}
+ /** @inheritDoc */
public function offsetUnset( $offset ) {
$this->remove( $offset );
}
diff --git a/www/wiki/includes/session/SessionBackend.php b/www/wiki/includes/session/SessionBackend.php
index d37b73b5..a3760378 100644
--- a/www/wiki/includes/session/SessionBackend.php
+++ b/www/wiki/includes/session/SessionBackend.php
@@ -97,7 +97,7 @@ final class SessionBackend {
private $shutdown = false;
/**
- * @param SessionId $id Session ID object
+ * @param SessionId $id
* @param SessionInfo $info Session info to populate from
* @param CachedBagOStuff $store Backend data store
* @param LoggerInterface $logger
@@ -243,7 +243,7 @@ final class SessionBackend {
if ( $restart ) {
session_id( (string)$this->id );
- \MediaWiki\quietCall( 'session_start' );
+ \Wikimedia\quietCall( 'session_start' );
}
$this->autosave();
@@ -764,7 +764,7 @@ final class SessionBackend {
'session' => $this->id,
] );
session_id( (string)$this->id );
- \MediaWiki\quietCall( 'session_start' );
+ \Wikimedia\quietCall( 'session_start' );
}
}
}
diff --git a/www/wiki/includes/session/SessionManager.php b/www/wiki/includes/session/SessionManager.php
index 40a568ff..603985f5 100644
--- a/www/wiki/includes/session/SessionManager.php
+++ b/www/wiki/includes/session/SessionManager.php
@@ -32,6 +32,7 @@ use Config;
use FauxRequest;
use User;
use WebRequest;
+use Wikimedia\ObjectFactory;
/**
* This serves as the entry point to the MediaWiki session handling system.
@@ -429,7 +430,7 @@ final class SessionManager implements SessionManagerInterface {
if ( $this->sessionProviders === null ) {
$this->sessionProviders = [];
foreach ( $this->config->get( 'SessionProviders' ) as $spec ) {
- $provider = \ObjectFactory::getObjectFromSpec( $spec );
+ $provider = ObjectFactory::getObjectFromSpec( $spec );
$provider->setLogger( $this->logger );
$provider->setConfig( $this->config );
$provider->setManager( $this );
diff --git a/www/wiki/includes/shell/Command.php b/www/wiki/includes/shell/Command.php
index 7eaf61ce..d9fa82df 100644
--- a/www/wiki/includes/shell/Command.php
+++ b/www/wiki/includes/shell/Command.php
@@ -36,7 +36,7 @@ class Command {
use LoggerAwareTrait;
/** @var string */
- private $command = '';
+ protected $command = '';
/** @var array */
private $limits = [
@@ -56,8 +56,14 @@ class Command {
/** @var string */
private $method;
+ /** @var string|null */
+ private $inputString;
+
+ /** @var bool */
+ private $doIncludeStderr = false;
+
/** @var bool */
- private $useStderr = false;
+ private $doLogStderr = false;
/** @var bool */
private $everExecuted = false;
@@ -66,6 +72,13 @@ class Command {
private $cgroup = false;
/**
+ * bitfield with restrictions
+ *
+ * @var int
+ */
+ protected $restrictions = 0;
+
+ /**
* Constructor. Don't call directly, instead use Shell::command()
*
* @throws ShellDisabledError
@@ -180,6 +193,18 @@ class Command {
}
/**
+ * Sends the provided input to the command.
+ * When set to null (default), the command will use the standard input.
+ * @param string|null $inputString
+ * @return $this
+ */
+ public function input( $inputString ) {
+ $this->inputString = is_null( $inputString ) ? null : (string)$inputString;
+
+ return $this;
+ }
+
+ /**
* Controls whether stderr should be included in stdout, including errors from limit.sh.
* Default: don't include.
*
@@ -187,7 +212,19 @@ class Command {
* @return $this
*/
public function includeStderr( $yesno = true ) {
- $this->useStderr = $yesno;
+ $this->doIncludeStderr = $yesno;
+
+ return $this;
+ }
+
+ /**
+ * When enabled, text sent to stderr will be logged with a level of 'error'.
+ *
+ * @param bool $yesno
+ * @return $this
+ */
+ public function logStderr( $yesno = true ) {
+ $this->doLogStderr = $yesno;
return $this;
}
@@ -205,19 +242,52 @@ class Command {
}
/**
- * Executes command. Afterwards, getExitCode() and getOutput() can be used to access execution
- * results.
+ * Set additional restrictions for this request
*
- * @return Result
- * @throws Exception
- * @throws ProcOpenError
- * @throws ShellDisabledError
+ * @since 1.31
+ * @param int $restrictions
+ * @return $this
*/
- public function execute() {
- $this->everExecuted = true;
+ public function restrict( $restrictions ) {
+ $this->restrictions |= $restrictions;
- $profileMethod = $this->method ?: wfGetCaller();
+ return $this;
+ }
+
+ /**
+ * Bitfield helper on whether a specific restriction is enabled
+ *
+ * @param int $restriction
+ *
+ * @return bool
+ */
+ protected function hasRestriction( $restriction ) {
+ return ( $this->restrictions & $restriction ) === $restriction;
+ }
+
+ /**
+ * If called, only the files/directories that are
+ * whitelisted will be available to the shell command.
+ *
+ * limit.sh will always be whitelisted
+ *
+ * @param string[] $paths
+ *
+ * @return $this
+ */
+ public function whitelistPaths( array $paths ) {
+ // Default implementation is a no-op
+ return $this;
+ }
+ /**
+ * String together all the options and build the final command
+ * to execute
+ *
+ * @param string $command Already-escaped command to run
+ * @return array [ command, whether to use log pipe ]
+ */
+ protected function buildFinalCommand( $command ) {
$envcmd = '';
foreach ( $this->env as $k => $v ) {
if ( wfIsWindows() ) {
@@ -236,9 +306,9 @@ class Command {
}
}
- $cmd = $envcmd . trim( $this->command );
-
$useLogPipe = false;
+ $cmd = $envcmd . trim( $command );
+
if ( is_executable( '/bin/bash' ) ) {
$time = intval( $this->limits['time'] );
$wallTime = intval( $this->limits['walltime'] );
@@ -247,24 +317,43 @@ class Command {
if ( $time > 0 || $mem > 0 || $filesize > 0 || $wallTime > 0 ) {
$cmd = '/bin/bash ' . escapeshellarg( __DIR__ . '/limit.sh' ) . ' ' .
- escapeshellarg( $cmd ) . ' ' .
- escapeshellarg(
- "MW_INCLUDE_STDERR=" . ( $this->useStderr ? '1' : '' ) . ';' .
- "MW_CPU_LIMIT=$time; " .
- 'MW_CGROUP=' . escapeshellarg( $this->cgroup ) . '; ' .
- "MW_MEM_LIMIT=$mem; " .
- "MW_FILE_SIZE_LIMIT=$filesize; " .
- "MW_WALL_CLOCK_LIMIT=$wallTime; " .
- "MW_USE_LOG_PIPE=yes"
- );
+ escapeshellarg( $cmd ) . ' ' .
+ escapeshellarg(
+ "MW_INCLUDE_STDERR=" . ( $this->doIncludeStderr ? '1' : '' ) . ';' .
+ "MW_CPU_LIMIT=$time; " .
+ 'MW_CGROUP=' . escapeshellarg( $this->cgroup ) . '; ' .
+ "MW_MEM_LIMIT=$mem; " .
+ "MW_FILE_SIZE_LIMIT=$filesize; " .
+ "MW_WALL_CLOCK_LIMIT=$wallTime; " .
+ "MW_USE_LOG_PIPE=yes"
+ );
$useLogPipe = true;
- } elseif ( $this->useStderr ) {
- $cmd .= ' 2>&1';
}
- } elseif ( $this->useStderr ) {
+ }
+ if ( !$useLogPipe && $this->doIncludeStderr ) {
$cmd .= ' 2>&1';
}
- wfDebug( __METHOD__ . ": $cmd\n" );
+
+ return [ $cmd, $useLogPipe ];
+ }
+
+ /**
+ * Executes command. Afterwards, getExitCode() and getOutput() can be used to access execution
+ * results.
+ *
+ * @return Result
+ * @throws Exception
+ * @throws ProcOpenError
+ * @throws ShellDisabledError
+ */
+ public function execute() {
+ $this->everExecuted = true;
+
+ $profileMethod = $this->method ?: wfGetCaller();
+
+ list( $cmd, $useLogPipe ) = $this->buildFinalCommand( $this->command );
+
+ $this->logger->debug( __METHOD__ . ": $cmd" );
// Don't try to execute commands that exceed Linux's MAX_ARG_STRLEN.
// Other platforms may be more accomodating, but we don't want to be
@@ -272,13 +361,13 @@ class Command {
// input. See T129506.
if ( strlen( $cmd ) > SHELL_MAX_ARG_STRLEN ) {
throw new Exception( __METHOD__ .
- '(): total length of $cmd must not exceed SHELL_MAX_ARG_STRLEN' );
+ '(): total length of $cmd must not exceed SHELL_MAX_ARG_STRLEN' );
}
$desc = [
- 0 => [ 'file', 'php://stdin', 'r' ],
+ 0 => $this->inputString === null ? [ 'file', 'php://stdin', 'r' ] : [ 'pipe', 'r' ],
1 => [ 'pipe', 'w' ],
- 2 => [ 'file', 'php://stderr', 'w' ],
+ 2 => [ 'pipe', 'w' ],
];
if ( $useLogPipe ) {
$desc[3] = [ 'pipe', 'w' ];
@@ -290,7 +379,13 @@ class Command {
$this->logger->error( "proc_open() failed: {command}", [ 'command' => $cmd ] );
throw new ProcOpenError();
}
- $outBuffer = $logBuffer = '';
+
+ $buffers = [
+ 0 => $this->inputString, // input
+ 1 => '', // stdout
+ 2 => null, // stderr
+ 3 => '', // log
+ ];
$emptyArray = [];
$status = false;
$logMsg = false;
@@ -309,11 +404,20 @@ class Command {
$eintr = defined( 'SOCKET_EINTR' ) ? SOCKET_EINTR : 4;
$eintrMessage = "stream_select(): unable to select [$eintr]";
+ /* The select(2) system call only guarantees a "sufficiently small write"
+ * can be made without blocking. And on Linux the read might block too
+ * in certain cases, although I don't know if any of them can occur here.
+ * Regardless, set all the pipes to non-blocking to avoid T184171.
+ */
+ foreach ( $pipes as $pipe ) {
+ stream_set_blocking( $pipe, false );
+ }
+
$running = true;
$timeout = null;
$numReadyPipes = 0;
- while ( $running === true || $numReadyPipes !== 0 ) {
+ while ( $pipes && ( $running === true || $numReadyPipes !== 0 ) ) {
if ( $running ) {
$status = proc_get_status( $proc );
// If the process has terminated, switch to nonblocking selects
@@ -324,15 +428,27 @@ class Command {
}
}
- $readyPipes = $pipes;
-
- \MediaWiki\suppressWarnings();
- trigger_error( '' );
- $numReadyPipes = stream_select( $readyPipes, $emptyArray, $emptyArray, $timeout );
- \MediaWiki\restoreWarnings();
-
+ // clear get_last_error without actually raising an error
+ // from http://php.net/manual/en/function.error-get-last.php#113518
+ // TODO replace with clear_last_error when requirements are bumped to PHP7
+ set_error_handler( function () {
+ }, 0 );
+ // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
+ @trigger_error( '' );
+ restore_error_handler();
+
+ $readPipes = wfArrayFilterByKey( $pipes, function ( $fd ) use ( $desc ) {
+ return $desc[$fd][0] === 'pipe' && $desc[$fd][1] === 'r';
+ } );
+ $writePipes = wfArrayFilterByKey( $pipes, function ( $fd ) use ( $desc ) {
+ return $desc[$fd][0] === 'pipe' && $desc[$fd][1] === 'w';
+ } );
+ // stream_select parameter names are from the POV of us being able to do the operation;
+ // proc_open desriptor types are from the POV of the process doing it.
+ // So $writePipes is passed as the $read parameter and $readPipes as $write.
+ // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
+ $numReadyPipes = @stream_select( $writePipes, $readPipes, $emptyArray, $timeout );
if ( $numReadyPipes === false ) {
- // @codingStandardsIgnoreEnd
$error = error_get_last();
if ( strncmp( $error['message'], $eintrMessage, strlen( $eintrMessage ) ) == 0 ) {
continue;
@@ -342,28 +458,45 @@ class Command {
break;
}
}
- foreach ( $readyPipes as $fd => $pipe ) {
- $block = fread( $pipe, 65536 );
- if ( $block === '' ) {
- // End of file
- fclose( $pipes[$fd] );
- unset( $pipes[$fd] );
- if ( !$pipes ) {
- break 2;
+ foreach ( $writePipes + $readPipes as $fd => $pipe ) {
+ // True if a pipe is unblocked for us to write into, false if for reading from
+ $isWrite = array_key_exists( $fd, $readPipes );
+
+ if ( $isWrite ) {
+ // Don't bother writing if the buffer is empty
+ if ( $buffers[$fd] === '' ) {
+ fclose( $pipes[$fd] );
+ unset( $pipes[$fd] );
+ continue;
}
- } elseif ( $block === false ) {
- // Read error
- $logMsg = "Error reading from pipe";
+ $res = fwrite( $pipe, $buffers[$fd], 65536 );
+ } else {
+ $res = fread( $pipe, 65536 );
+ }
+
+ if ( $res === false ) {
+ $logMsg = 'Error ' . ( $isWrite ? 'writing to' : 'reading from' ) . ' pipe';
break 2;
- } elseif ( $fd == 1 ) {
- // From stdout
- $outBuffer .= $block;
- } elseif ( $fd == 3 ) {
- // From log FD
- $logBuffer .= $block;
- if ( strpos( $block, "\n" ) !== false ) {
- $lines = explode( "\n", $logBuffer );
- $logBuffer = array_pop( $lines );
+ }
+
+ if ( $res === '' || $res === 0 ) {
+ // End of file?
+ if ( feof( $pipe ) ) {
+ fclose( $pipes[$fd] );
+ unset( $pipes[$fd] );
+ }
+ } elseif ( $isWrite ) {
+ $buffers[$fd] = (string)substr( $buffers[$fd], $res );
+ if ( $buffers[$fd] === '' ) {
+ fclose( $pipes[$fd] );
+ unset( $pipes[$fd] );
+ }
+ } else {
+ $buffers[$fd] .= $res;
+ if ( $fd === 3 && strpos( $res, "\n" ) !== false ) {
+ // For the log FD, every line is a separate log entry.
+ $lines = explode( "\n", $buffers[3] );
+ $buffers[3] = array_pop( $lines );
foreach ( $lines as $line ) {
$this->logger->info( $line );
}
@@ -408,6 +541,15 @@ class Command {
$this->logger->warning( "$logMsg: {command}", [ 'command' => $cmd ] );
}
- return new Result( $retval, $outBuffer );
+ if ( $buffers[2] && $this->doLogStderr ) {
+ $this->logger->error( "Error running {command}: {error}", [
+ 'command' => $cmd,
+ 'error' => $buffers[2],
+ 'exitcode' => $retval,
+ 'exception' => new Exception( 'Shell error' ),
+ ] );
+ }
+
+ return new Result( $retval, $buffers[1], $buffers[2] );
}
}
diff --git a/www/wiki/includes/shell/CommandFactory.php b/www/wiki/includes/shell/CommandFactory.php
index c0b8f899..b4b9b921 100644
--- a/www/wiki/includes/shell/CommandFactory.php
+++ b/www/wiki/includes/shell/CommandFactory.php
@@ -20,6 +20,7 @@
namespace MediaWiki\Shell;
+use ExecutableFinder;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
@@ -37,29 +38,77 @@ class CommandFactory {
/** @var string|bool */
private $cgroup;
+ /** @var bool */
+ private $doLogStderr = false;
+
+ /**
+ * @var string|bool
+ */
+ private $restrictionMethod;
+
+ /**
+ * @var string|bool
+ */
+ private $firejail;
+
/**
* Constructor
*
* @param array $limits See {@see Command::limits()}
* @param string|bool $cgroup See {@see Command::cgroup()}
+ * @param string|bool $restrictionMethod
*/
- public function __construct( array $limits, $cgroup ) {
+ public function __construct( array $limits, $cgroup, $restrictionMethod ) {
$this->limits = $limits;
$this->cgroup = $cgroup;
+ if ( $restrictionMethod === 'autodetect' ) {
+ // On Linux systems check for firejail
+ if ( PHP_OS === 'Linux' && $this->findFirejail() !== false ) {
+ $this->restrictionMethod = 'firejail';
+ } else {
+ $this->restrictionMethod = false;
+ }
+ } else {
+ $this->restrictionMethod = $restrictionMethod;
+ }
$this->setLogger( new NullLogger() );
}
+ private function findFirejail() {
+ if ( $this->firejail === null ) {
+ $this->firejail = ExecutableFinder::findInDefaultPaths( 'firejail' );
+ }
+
+ return $this->firejail;
+ }
+
+ /**
+ * When enabled, text sent to stderr will be logged with a level of 'error'.
+ *
+ * @param bool $yesno
+ * @see Command::logStderr
+ */
+ public function logStderr( $yesno = true ) {
+ $this->doLogStderr = $yesno;
+ }
+
/**
* Instantiates a new Command
*
* @return Command
*/
public function create() {
- $command = new Command();
+ if ( $this->restrictionMethod === 'firejail' ) {
+ $command = new FirejailCommand( $this->findFirejail() );
+ $command->restrict( Shell::RESTRICT_DEFAULT );
+ } else {
+ $command = new Command();
+ }
$command->setLogger( $this->logger );
return $command
->limits( $this->limits )
- ->cgroup( $this->cgroup );
+ ->cgroup( $this->cgroup )
+ ->logStderr( $this->doLogStderr );
}
}
diff --git a/www/wiki/includes/shell/FirejailCommand.php b/www/wiki/includes/shell/FirejailCommand.php
new file mode 100644
index 00000000..d8189304
--- /dev/null
+++ b/www/wiki/includes/shell/FirejailCommand.php
@@ -0,0 +1,160 @@
+<?php
+/**
+ * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+namespace MediaWiki\Shell;
+
+use RuntimeException;
+
+/**
+ * Restricts execution of shell commands using firejail
+ *
+ * @see https://firejail.wordpress.com/
+ * @since 1.31
+ */
+class FirejailCommand extends Command {
+
+ /**
+ * @var string Path to firejail
+ */
+ private $firejail;
+
+ /**
+ * @var string[]
+ */
+ private $whitelistedPaths = [];
+
+ /**
+ * @param string $firejail Path to firejail
+ */
+ public function __construct( $firejail ) {
+ parent::__construct();
+ $this->firejail = $firejail;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function whitelistPaths( array $paths ) {
+ $this->whitelistedPaths = array_merge( $this->whitelistedPaths, $paths );
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function buildFinalCommand( $command ) {
+ // If there are no restrictions, don't use firejail
+ if ( $this->restrictions === 0 ) {
+ $splitCommand = explode( ' ', $command, 2 );
+ $this->logger->debug(
+ "firejail: Command {$splitCommand[0]} {params} has no restrictions",
+ [ 'params' => isset( $splitCommand[1] ) ? $splitCommand[1] : '' ]
+ );
+ return parent::buildFinalCommand( $command );
+ }
+
+ if ( $this->firejail === false ) {
+ throw new RuntimeException( 'firejail is enabled, but cannot be found' );
+ }
+ // quiet has to come first to prevent firejail from adding
+ // any output.
+ $cmd = [ $this->firejail, '--quiet' ];
+ // Use a profile that allows people to add local overrides
+ // if their system is setup in an incompatible manner. Also it
+ // prevents any default profiles from running.
+ // FIXME: Doesn't actually override command-line switches?
+ $cmd[] = '--profile=' . __DIR__ . '/firejail.profile';
+
+ // By default firejail hides all other user directories, so if
+ // MediaWiki is inside a home directory (/home) but not the
+ // current user's home directory, pass --allusers to whitelist
+ // the home directories again.
+ static $useAllUsers = null;
+ if ( $useAllUsers === null ) {
+ global $IP;
+ // In case people are doing funny things with symlinks
+ // or relative paths, resolve them all.
+ $realIP = realpath( $IP );
+ $currentUser = posix_getpwuid( posix_geteuid() );
+ $useAllUsers = ( strpos( $realIP, '/home/' ) === 0 )
+ && ( strpos( $realIP, $currentUser['dir'] ) !== 0 );
+ if ( $useAllUsers ) {
+ $this->logger->warning( 'firejail: MediaWiki is located ' .
+ 'in a home directory that does not belong to the ' .
+ 'current user, so allowing access to all home ' .
+ 'directories (--allusers)' );
+ }
+ }
+
+ if ( $useAllUsers ) {
+ $cmd[] = '--allusers';
+ }
+
+ if ( $this->whitelistedPaths ) {
+ // Always whitelist limit.sh
+ $cmd[] = '--whitelist=' . __DIR__ . '/limit.sh';
+ foreach ( $this->whitelistedPaths as $whitelistedPath ) {
+ $cmd[] = "--whitelist={$whitelistedPath}";
+ }
+ }
+
+ if ( $this->hasRestriction( Shell::NO_LOCALSETTINGS ) ) {
+ $cmd[] = '--blacklist=' . realpath( MW_CONFIG_FILE );
+ }
+
+ if ( $this->hasRestriction( Shell::NO_ROOT ) ) {
+ $cmd[] = '--noroot';
+ }
+
+ $useSeccomp = $this->hasRestriction( Shell::SECCOMP );
+ $extraSeccomp = [];
+
+ if ( $this->hasRestriction( Shell::NO_EXECVE ) ) {
+ $extraSeccomp[] = 'execve';
+ // Normally firejail will run commands in a bash shell,
+ // but that won't work if we ban the execve syscall, so
+ // run the command without a shell.
+ $cmd[] = '--shell=none';
+ }
+
+ if ( $useSeccomp ) {
+ $seccomp = '--seccomp';
+ if ( $extraSeccomp ) {
+ // The "@default" seccomp group will always be enabled
+ $seccomp .= '=' . implode( ',', $extraSeccomp );
+ }
+ $cmd[] = $seccomp;
+ }
+
+ if ( $this->hasRestriction( Shell::PRIVATE_DEV ) ) {
+ $cmd[] = '--private-dev';
+ }
+
+ if ( $this->hasRestriction( Shell::NO_NETWORK ) ) {
+ $cmd[] = '--net=none';
+ }
+
+ $builtCmd = implode( ' ', $cmd );
+
+ // Prefix the firejail command in front of the wanted command
+ return parent::buildFinalCommand( "$builtCmd -- {$command}" );
+ }
+
+}
diff --git a/www/wiki/includes/shell/Result.php b/www/wiki/includes/shell/Result.php
index c1429dfc..a105cd12 100644
--- a/www/wiki/includes/shell/Result.php
+++ b/www/wiki/includes/shell/Result.php
@@ -32,13 +32,18 @@ class Result {
/** @var string */
private $stdout;
+ /** @var string|null */
+ private $stderr;
+
/**
* @param int $exitCode
* @param string $stdout
+ * @param string|null $stderr
*/
- public function __construct( $exitCode, $stdout ) {
+ public function __construct( $exitCode, $stdout, $stderr = null ) {
$this->exitCode = $exitCode;
$this->stdout = $stdout;
+ $this->stderr = $stderr;
}
/**
@@ -58,4 +63,14 @@ class Result {
public function getStdout() {
return $this->stdout;
}
+
+ /**
+ * Returns stderr of the process or null if the Command was configured to add stderr to stdout
+ * with includeStderr( true )
+ *
+ * @return string|null
+ */
+ public function getStderr() {
+ return $this->stderr;
+ }
}
diff --git a/www/wiki/includes/shell/Shell.php b/www/wiki/includes/shell/Shell.php
index cef9ffa3..742e1424 100644
--- a/www/wiki/includes/shell/Shell.php
+++ b/www/wiki/includes/shell/Shell.php
@@ -22,6 +22,7 @@
namespace MediaWiki\Shell;
+use Hooks;
use MediaWiki\MediaWikiServices;
/**
@@ -31,16 +32,83 @@ use MediaWiki\MediaWikiServices;
*
* Use call chaining with this class for expressiveness:
* $result = Shell::command( 'some command' )
+ * ->input( 'foo' )
* ->environment( [ 'ENVIRONMENT_VARIABLE' => 'VALUE' ] )
* ->limits( [ 'time' => 300 ] )
* ->execute();
*
* ... = $result->getExitCode();
* ... = $result->getStdout();
+ * ... = $result->getStderr();
*/
class Shell {
/**
+ * Apply a default set of restrictions for improved
+ * security out of the box.
+ *
+ * Equal to NO_ROOT | SECCOMP | PRIVATE_DEV | NO_LOCALSETTINGS
+ *
+ * @note This value will change over time to provide increased security
+ * by default, and is not guaranteed to be backwards-compatible.
+ * @since 1.31
+ */
+ const RESTRICT_DEFAULT = 39;
+
+ /**
+ * Disallow any root access. Any setuid binaries
+ * will be run without elevated access.
+ *
+ * @since 1.31
+ */
+ const NO_ROOT = 1;
+
+ /**
+ * Use seccomp to block dangerous syscalls
+ * @see <https://en.wikipedia.org/wiki/seccomp>
+ *
+ * @since 1.31
+ */
+ const SECCOMP = 2;
+
+ /**
+ * Create a private /dev
+ *
+ * @since 1.31
+ */
+ const PRIVATE_DEV = 4;
+
+ /**
+ * Restrict the request to have no
+ * network access
+ *
+ * @since 1.31
+ */
+ const NO_NETWORK = 8;
+
+ /**
+ * Deny execve syscall with seccomp
+ * @see <https://en.wikipedia.org/wiki/exec_(system_call)>
+ *
+ * @since 1.31
+ */
+ const NO_EXECVE = 16;
+
+ /**
+ * Deny access to LocalSettings.php (MW_CONFIG_FILE)
+ *
+ * @since 1.31
+ */
+ const NO_LOCALSETTINGS = 32;
+
+ /**
+ * Don't apply any restrictions
+ *
+ * @since 1.31
+ */
+ const RESTRICT_NONE = 0;
+
+ /**
* Returns a new instance of Command class
*
* @param string|string[] $command String or array of strings representing the command to
@@ -115,14 +183,12 @@ class Shell {
if ( wfIsWindows() ) {
// Escaping for an MSVC-style command line parser and CMD.EXE
- // @codingStandardsIgnoreStart For long URLs
// Refs:
// * https://web.archive.org/web/20020708081031/http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html
// * https://technet.microsoft.com/en-us/library/cc723564.aspx
// * T15518
// * CR r63214
// Double the backslashes before any double quotes. Escape the double quotes.
- // @codingStandardsIgnoreEnd
$tokens = preg_split( '/(\\\\*")/', $arg, -1, PREG_SPLIT_DELIM_CAPTURE );
$arg = '';
$iteration = 0;
@@ -154,4 +220,32 @@ class Shell {
}
return $retVal;
}
+
+ /**
+ * Generate a Command object to run a MediaWiki CLI script.
+ * Note that $parameters should be a flat array and an option with an argument
+ * should consist of two consecutive items in the array (do not use "--option value").
+ *
+ * @param string $script MediaWiki CLI script with full path
+ * @param string[] $parameters Arguments and options to the script
+ * @param array $options Associative array of options:
+ * 'php': The path to the php executable
+ * 'wrapper': Path to a PHP wrapper to handle the maintenance script
+ * @return Command
+ */
+ public static function makeScriptCommand( $script, $parameters, $options = [] ) {
+ global $wgPhpCli;
+ // Give site config file a chance to run the script in a wrapper.
+ // The caller may likely want to call wfBasename() on $script.
+ Hooks::run( 'wfShellWikiCmd', [ &$script, &$parameters, &$options ] );
+ $cmd = isset( $options['php'] ) ? [ $options['php'] ] : [ $wgPhpCli ];
+ if ( isset( $options['wrapper'] ) ) {
+ $cmd[] = $options['wrapper'];
+ }
+ $cmd[] = $script;
+
+ return self::command( $cmd )
+ ->params( $parameters )
+ ->restrict( self::RESTRICT_DEFAULT & ~self::NO_LOCALSETTINGS );
+ }
}
diff --git a/www/wiki/includes/shell/firejail.profile b/www/wiki/includes/shell/firejail.profile
new file mode 100644
index 00000000..07f059ba
--- /dev/null
+++ b/www/wiki/includes/shell/firejail.profile
@@ -0,0 +1,7 @@
+# Firejail profile used by MediaWiki when shelling out
+# See <https://firejail.wordpress.com/features-3/man-firejail-profile/> for
+# syntax documentation
+# Persistent local customizations
+include /etc/firejail/mediawiki.local
+# Persistent global definitions
+include /etc/firejail/globals.local
diff --git a/www/wiki/includes/site/MediaWikiPageNameNormalizer.php b/www/wiki/includes/site/MediaWikiPageNameNormalizer.php
index c4e490a4..8a12c4f7 100644
--- a/www/wiki/includes/site/MediaWikiPageNameNormalizer.php
+++ b/www/wiki/includes/site/MediaWikiPageNameNormalizer.php
@@ -53,7 +53,8 @@ class MediaWikiPageNameNormalizer {
/**
* Returns the normalized form of the given page title, using the
* normalization rules of the given site. If the given title is a redirect,
- * the redirect weill be resolved and the redirect target is returned.
+ * the redirect will be resolved and the redirect target is returned.
+ * Only titles of existing pages will be returned.
*
* @note This actually makes an API request to the remote site, so beware
* that this function is slow and depends on an external service.
@@ -65,7 +66,9 @@ class MediaWikiPageNameNormalizer {
* @param string $pageName
* @param string $apiUrl
*
- * @return string
+ * @return string|false The normalized form of the title,
+ * or false to indicate an invalid title, a missing page,
+ * or some other kind of error.
* @throws \MWException
*/
public function normalizePageName( $pageName, $apiUrl ) {
diff --git a/www/wiki/includes/site/MediaWikiSite.php b/www/wiki/includes/site/MediaWikiSite.php
index f31a77d3..e1e7ce69 100644
--- a/www/wiki/includes/site/MediaWikiSite.php
+++ b/www/wiki/includes/site/MediaWikiSite.php
@@ -64,7 +64,8 @@ class MediaWikiSite extends Site {
/**
* Returns the normalized form of the given page title, using the
* normalization rules of the given site. If the given title is a redirect,
- * the redirect weill be resolved and the redirect target is returned.
+ * the redirect will be resolved and the redirect target is returned.
+ * Only titles of existing pages will be returned.
*
* @note This actually makes an API request to the remote site, so beware
* that this function is slow and depends on an external service.
@@ -79,7 +80,9 @@ class MediaWikiSite extends Site {
*
* @param string $pageName
*
- * @return string
+ * @return string|false The normalized form of the title,
+ * or false to indicate an invalid title, a missing page,
+ * or some other kind of error.
* @throws MWException
*/
public function normalizePageName( $pageName ) {
diff --git a/www/wiki/includes/site/Site.php b/www/wiki/includes/site/Site.php
index a6e63391..f5e3f22e 100644
--- a/www/wiki/includes/site/Site.php
+++ b/www/wiki/includes/site/Site.php
@@ -382,8 +382,10 @@ class Site implements Serializable {
}
/**
- * Returns $pageName without changes.
- * Subclasses may override this to apply some kind of normalization.
+ * Attempt to normalize the page name in some fashion.
+ * May return false to indicate various kinds of failure.
+ *
+ * This implementation returns $pageName without changes.
*
* @see Site::normalizePageName
*
@@ -391,7 +393,7 @@ class Site implements Serializable {
*
* @param string $pageName
*
- * @return string
+ * @return string|false
*/
public function normalizePageName( $pageName ) {
return $pageName;
diff --git a/www/wiki/includes/site/SiteList.php b/www/wiki/includes/site/SiteList.php
index a94aa0b9..b942d6e4 100644
--- a/www/wiki/includes/site/SiteList.php
+++ b/www/wiki/includes/site/SiteList.php
@@ -63,7 +63,7 @@ class SiteList extends GenericArrayObject {
* @return string
*/
public function getObjectType() {
- return 'Site';
+ return Site::class;
}
/**
diff --git a/www/wiki/includes/skins/BaseTemplate.php b/www/wiki/includes/skins/BaseTemplate.php
index 8d5ce10d..e1f2969a 100644
--- a/www/wiki/includes/skins/BaseTemplate.php
+++ b/www/wiki/includes/skins/BaseTemplate.php
@@ -98,14 +98,7 @@ abstract class BaseTemplate extends QuickTemplate {
}
if ( isset( $this->data['nav_urls']['permalink'] ) && $this->data['nav_urls']['permalink'] ) {
$toolbox['permalink'] = $this->data['nav_urls']['permalink'];
- if ( $toolbox['permalink']['href'] === '' ) {
- unset( $toolbox['permalink']['href'] );
- $toolbox['ispermalink']['tooltiponly'] = true;
- $toolbox['ispermalink']['id'] = 't-ispermalink';
- $toolbox['ispermalink']['msg'] = 'permalink';
- } else {
- $toolbox['permalink']['id'] = 't-permalink';
- }
+ $toolbox['permalink']['id'] = 't-permalink';
}
if ( isset( $this->data['nav_urls']['info'] ) && $this->data['nav_urls']['info'] ) {
$toolbox['info'] = $this->data['nav_urls']['info'];
@@ -143,7 +136,7 @@ abstract class BaseTemplate extends QuickTemplate {
if ( isset( $plink['active'] ) ) {
$ptool['active'] = $plink['active'];
}
- foreach ( [ 'href', 'class', 'text', 'dir', 'data' ] as $k ) {
+ foreach ( [ 'href', 'class', 'text', 'dir', 'data', 'exists' ] as $k ) {
if ( isset( $plink[$k] ) ) {
$ptool['links'][0][$k] = $plink[$k];
}
@@ -182,44 +175,44 @@ abstract class BaseTemplate extends QuickTemplate {
continue;
}
switch ( $boxName ) {
- case 'SEARCH':
- // Search is a special case, skins should custom implement this
- $boxes[$boxName] = [
- 'id' => 'p-search',
- 'header' => $this->getMsg( 'search' )->text(),
- 'generated' => false,
- 'content' => true,
- ];
- break;
- case 'TOOLBOX':
- $msgObj = $this->getMsg( 'toolbox' );
- $boxes[$boxName] = [
- 'id' => 'p-tb',
- 'header' => $msgObj->exists() ? $msgObj->text() : 'toolbox',
- 'generated' => false,
- 'content' => $this->getToolbox(),
- ];
- break;
- case 'LANGUAGES':
- if ( $this->data['language_urls'] !== false ) {
- $msgObj = $this->getMsg( 'otherlanguages' );
+ case 'SEARCH':
+ // Search is a special case, skins should custom implement this
$boxes[$boxName] = [
- 'id' => 'p-lang',
- 'header' => $msgObj->exists() ? $msgObj->text() : 'otherlanguages',
+ 'id' => 'p-search',
+ 'header' => $this->getMsg( 'search' )->text(),
'generated' => false,
- 'content' => $this->data['language_urls'] ?: [],
+ 'content' => true,
];
- }
- break;
- default:
- $msgObj = $this->getMsg( $boxName );
- $boxes[$boxName] = [
- 'id' => "p-$boxName",
- 'header' => $msgObj->exists() ? $msgObj->text() : $boxName,
- 'generated' => true,
- 'content' => $content,
- ];
- break;
+ break;
+ case 'TOOLBOX':
+ $msgObj = $this->getMsg( 'toolbox' );
+ $boxes[$boxName] = [
+ 'id' => 'p-tb',
+ 'header' => $msgObj->exists() ? $msgObj->text() : 'toolbox',
+ 'generated' => false,
+ 'content' => $this->getToolbox(),
+ ];
+ break;
+ case 'LANGUAGES':
+ if ( $this->data['language_urls'] !== false ) {
+ $msgObj = $this->getMsg( 'otherlanguages' );
+ $boxes[$boxName] = [
+ 'id' => 'p-lang',
+ 'header' => $msgObj->exists() ? $msgObj->text() : 'otherlanguages',
+ 'generated' => false,
+ 'content' => $this->data['language_urls'] ?: [],
+ ];
+ }
+ break;
+ default:
+ $msgObj = $this->getMsg( $boxName );
+ $boxes[$boxName] = [
+ 'id' => "p-$boxName",
+ 'header' => $msgObj->exists() ? $msgObj->text() : $boxName,
+ 'generated' => true,
+ 'content' => $content,
+ ];
+ break;
}
}
@@ -370,7 +363,7 @@ abstract class BaseTemplate extends QuickTemplate {
if ( isset( $item['text'] ) ) {
$text = $item['text'];
} else {
- $text = $this->translator->translate( isset( $item['msg'] ) ? $item['msg'] : $key );
+ $text = wfMessage( isset( $item['msg'] ) ? $item['msg'] : $key )->text();
}
$html = htmlspecialchars( $text );
@@ -391,7 +384,7 @@ abstract class BaseTemplate extends QuickTemplate {
if ( isset( $item['href'] ) || isset( $options['link-fallback'] ) ) {
$attrs = $item;
foreach ( [ 'single-id', 'text', 'msg', 'tooltiponly', 'context', 'primary',
- 'tooltip-params' ] as $k ) {
+ 'tooltip-params', 'exists' ] as $k ) {
unset( $attrs[$k] );
}
@@ -412,13 +405,19 @@ abstract class BaseTemplate extends QuickTemplate {
}
if ( isset( $item['single-id'] ) ) {
+ $tooltipOption = isset( $item['exists'] ) && $item['exists'] === false ? 'nonexisting' : null;
+
if ( isset( $item['tooltiponly'] ) && $item['tooltiponly'] ) {
- $title = Linker::titleAttrib( $item['single-id'], null, $tooltipParams );
+ $title = Linker::titleAttrib( $item['single-id'], $tooltipOption, $tooltipParams );
if ( $title !== false ) {
$attrs['title'] = $title;
}
} else {
- $tip = Linker::tooltipAndAccesskeyAttribs( $item['single-id'], $tooltipParams );
+ $tip = Linker::tooltipAndAccesskeyAttribs(
+ $item['single-id'],
+ $tooltipParams,
+ $tooltipOption
+ );
if ( isset( $tip['title'] ) && $tip['title'] !== false ) {
$attrs['title'] = $tip['title'];
}
@@ -535,8 +534,7 @@ abstract class BaseTemplate extends QuickTemplate {
$realAttrs = [
'type' => 'submit',
'name' => $mode,
- 'value' => $this->translator->translate(
- $mode == 'go' ? 'searcharticle' : 'searchbutton' ),
+ 'value' => wfMessage( $mode == 'go' ? 'searcharticle' : 'searchbutton' )->text(),
];
$realAttrs = array_merge(
$realAttrs,
@@ -562,7 +560,7 @@ abstract class BaseTemplate extends QuickTemplate {
'src' => $attrs['src'],
'alt' => isset( $attrs['alt'] )
? $attrs['alt']
- : $this->translator->translate( 'searchbutton' ),
+ : wfMessage( 'searchbutton' )->text(),
'width' => isset( $attrs['width'] ) ? $attrs['width'] : null,
'height' => isset( $attrs['height'] ) ? $attrs['height'] : null,
];
diff --git a/www/wiki/includes/skins/MediaWikiI18N.php b/www/wiki/includes/skins/MediaWikiI18N.php
index 7fcdb3c9..731897e4 100644
--- a/www/wiki/includes/skins/MediaWikiI18N.php
+++ b/www/wiki/includes/skins/MediaWikiI18N.php
@@ -28,11 +28,20 @@
class MediaWikiI18N {
private $context = [];
+ /**
+ * @deprecate since 1.31 Use BaseTemplate::msg() or Skin::msg() instead for setting
+ * message parameters.
+ */
function set( $varName, $value ) {
+ wfDeprecated( __METHOD__, '1.31' );
$this->context[$varName] = $value;
}
+ /**
+ * @deprecate since 1.31 Use BaseTemplate::msg(), Skin::msg(), or wfMessage() instead.
+ */
function translate( $value ) {
+ wfDeprecated( __METHOD__, '1.31' );
// Hack for i18n:attributes in PHPTAL 1.0.0 dev version as of 2004-10-23
$value = preg_replace( '/^string:/', '', $value );
@@ -41,9 +50,9 @@ class MediaWikiI18N {
$m = [];
while ( preg_match( '/\$([0-9]*?)/sm', $value, $m ) ) {
list( $src, $var ) = $m;
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$varValue = $this->context[$var];
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
$value = str_replace( $src, $varValue, $value );
}
return $value;
diff --git a/www/wiki/includes/skins/QuickTemplate.php b/www/wiki/includes/skins/QuickTemplate.php
index d1be4bb0..18867464 100644
--- a/www/wiki/includes/skins/QuickTemplate.php
+++ b/www/wiki/includes/skins/QuickTemplate.php
@@ -91,17 +91,24 @@ abstract class QuickTemplate {
}
/**
+ * @deprecated since 1.31 This function is a now-redundant optimisation intended
+ * for very old versions of PHP. The use of references here makes the code
+ * more fragile and is incompatible with plans like T140664. Use set() instead.
* @param string $name
* @param mixed &$value
*/
public function setRef( $name, &$value ) {
+ wfDeprecated( __METHOD__, '1.31' );
$this->data[$name] =& $value;
}
/**
* @param MediaWikiI18N &$t
+ * @deprecate since 1.31 Use BaseTemplate::msg() or Skin::msg() instead for setting
+ * message parameters.
*/
public function setTranslator( &$t ) {
+ wfDeprecated( __METHOD__, '1.31' );
$this->translator = &$t;
}
@@ -129,29 +136,29 @@ abstract class QuickTemplate {
/**
* @private
- * @param string $str
+ * @param string $msgKey
*/
- function msg( $str ) {
- echo htmlspecialchars( $this->translator->translate( $str ) );
+ function msg( $msgKey ) {
+ echo htmlspecialchars( wfMessage( $msgKey )->text() );
}
/**
* @private
- * @param string $str
+ * @param string $msgKey
*/
- function msgHtml( $str ) {
- echo $this->translator->translate( $str );
+ function msgHtml( $msgKey ) {
+ echo wfMessage( $msgKey )->text();
}
/**
* An ugly, ugly hack.
* @private
- * @param string $str
+ * @param string $msgKey
*/
- function msgWiki( $str ) {
+ function msgWiki( $msgKey ) {
global $wgOut;
- $text = $this->translator->translate( $str );
+ $text = wfMessage( $msgKey )->text();
echo $wgOut->parse( $text );
}
@@ -167,12 +174,12 @@ abstract class QuickTemplate {
/**
* @private
*
- * @param string $str
+ * @param string $msgKey
* @return bool
*/
- function haveMsg( $str ) {
- $msg = $this->translator->translate( $str );
- return ( $msg != '-' ) && ( $msg != '' ); # ????
+ function haveMsg( $msgKey ) {
+ $msg = wfMessage( $msgKey );
+ return $msg->exists() && !$msg->isDisabled();
}
/**
diff --git a/www/wiki/includes/skins/Skin.php b/www/wiki/includes/skins/Skin.php
index 8fb0d1c0..e2de9ec3 100644
--- a/www/wiki/includes/skins/Skin.php
+++ b/www/wiki/includes/skins/Skin.php
@@ -34,7 +34,11 @@ use MediaWiki\MediaWikiServices;
* @ingroup Skins
*/
abstract class Skin extends ContextSource {
+ /**
+ * @var string|null
+ */
protected $skinname = null;
+
protected $mRelevantTitle = null;
protected $mRelevantUser = null;
@@ -134,7 +138,17 @@ abstract class Skin extends ContextSource {
}
/**
- * @return string Skin name
+ * @since 1.31
+ * @param string|null $skinname
+ */
+ public function __construct( $skinname = null ) {
+ if ( is_string( $skinname ) ) {
+ $this->skinname = $skinname;
+ }
+ }
+
+ /**
+ * @return string|null Skin name
*/
public function getSkinName() {
return $this->skinname;
@@ -754,15 +768,6 @@ abstract class Skin extends ContextSource {
}
/**
- * @deprecated since 1.27, feature removed
- * @return bool Always false
- */
- function showIPinHeader() {
- wfDeprecated( __METHOD__, '1.27' );
- return false;
- }
-
- /**
* @return string
*/
function getSearchLink() {
@@ -897,7 +902,7 @@ abstract class Skin extends ContextSource {
$s = '';
}
- if ( wfGetLB()->getLaggedReplicaMode() ) {
+ if ( MediaWikiServices::getInstance()->getDBLoadBalancer()->getLaggedReplicaMode() ) {
$s .= ' <strong>' . $this->msg( 'laggedslavemode' )->parse() . '</strong>';
}
@@ -1088,14 +1093,14 @@ abstract class Skin extends ContextSource {
/* these are used extensively in SkinTemplate, but also some other places */
/**
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @return string
*/
static function makeMainPageUrl( $urlaction = '' ) {
$title = Title::newMainPage();
self::checkTitle( $title, '' );
- return $title->getLocalURL( $urlaction );
+ return $title->getLinkURL( $urlaction );
}
/**
@@ -1105,7 +1110,7 @@ abstract class Skin extends ContextSource {
* URL with the protocol specified.
*
* @param string $name Name of the Special page
- * @param string $urlaction Query to append
+ * @param string|string[] $urlaction Query to append
* @param string|null $proto Protocol to use or null for a local URL
* @return string
*/
@@ -1121,7 +1126,7 @@ abstract class Skin extends ContextSource {
/**
* @param string $name
* @param string $subpage
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @return string
*/
static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) {
@@ -1131,7 +1136,7 @@ abstract class Skin extends ContextSource {
/**
* @param string $name
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @return string
*/
static function makeI18nUrl( $name, $urlaction = '' ) {
@@ -1142,7 +1147,7 @@ abstract class Skin extends ContextSource {
/**
* @param string $name
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @return string
*/
static function makeUrl( $name, $urlaction = '' ) {
@@ -1169,7 +1174,7 @@ abstract class Skin extends ContextSource {
/**
* this can be passed the NS number as defined in Language.php
* @param string $name
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @param int $namespace
* @return string
*/
@@ -1183,7 +1188,7 @@ abstract class Skin extends ContextSource {
/**
* these return an array with the 'href' and boolean 'exists'
* @param string $name
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @return array
*/
static function makeUrlDetails( $name, $urlaction = '' ) {
@@ -1199,7 +1204,7 @@ abstract class Skin extends ContextSource {
/**
* Make URL details where the article exists (or at least it's convenient to think so)
* @param string $name Article name
- * @param string $urlaction
+ * @param string|string[] $urlaction
* @return array
*/
static function makeKnownUrlDetails( $name, $urlaction = '' ) {
@@ -1248,30 +1253,38 @@ abstract class Skin extends ContextSource {
*
* @return array
*/
- function buildSidebar() {
+ public function buildSidebar() {
global $wgEnableSidebarCache, $wgSidebarCacheExpiry;
- $callback = function () {
+ $callback = function ( $old = null, &$ttl = null ) {
$bar = [];
$this->addToSidebar( $bar, 'sidebar' );
Hooks::run( 'SkinBuildSidebar', [ $this, &$bar ] );
+ if ( MessageCache::singleton()->isDisabled() ) {
+ $ttl = WANObjectCache::TTL_UNCACHEABLE; // bug T133069
+ }
return $bar;
};
- if ( $wgEnableSidebarCache ) {
- $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
- $sidebar = $cache->getWithSetCallback(
- $cache->makeKey( 'sidebar', $this->getLanguage()->getCode() ),
- MessageCache::singleton()->isDisabled()
- ? $cache::TTL_UNCACHEABLE // bug T133069
- : $wgSidebarCacheExpiry,
+ $msgCache = MessageCache::singleton();
+ $wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+
+ $sidebar = $wgEnableSidebarCache
+ ? $wanCache->getWithSetCallback(
+ $wanCache->makeKey( 'sidebar', $this->getLanguage()->getCode() ),
+ $wgSidebarCacheExpiry,
$callback,
- [ 'lockTSE' => 30 ]
- );
- } else {
- $sidebar = $callback();
- }
+ [
+ 'checkKeys' => [
+ // Unless there is both no exact $code override nor an i18n definition
+ // in the the software, the only MediaWiki page to check is for $code.
+ $msgCache->getCheckKey( $this->getLanguage()->getCode() )
+ ],
+ 'lockTSE' => 30
+ ]
+ )
+ : $callback();
// Apply post-processing to the cached value
Hooks::run( 'SidebarBeforeOutput', [ $this, &$sidebar ] );
diff --git a/www/wiki/includes/skins/SkinApi.php b/www/wiki/includes/skins/SkinApi.php
index 6679098f..6966ff71 100644
--- a/www/wiki/includes/skins/SkinApi.php
+++ b/www/wiki/includes/skins/SkinApi.php
@@ -4,8 +4,6 @@
* the usual skin elements but still using CSS, JS, and such via OutputPage and
* ResourceLoader.
*
- * Created on Sep 08, 2014
- *
* Copyright © 2014 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
@@ -32,7 +30,7 @@
*/
class SkinApi extends SkinTemplate {
public $skinname = 'apioutput';
- public $template = 'SkinApiTemplate';
+ public $template = SkinApiTemplate::class;
public function setupSkinUserCss( OutputPage $out ) {
parent::setupSkinUserCss( $out );
diff --git a/www/wiki/includes/skins/SkinApiTemplate.php b/www/wiki/includes/skins/SkinApiTemplate.php
index d3e453a6..222a7626 100644
--- a/www/wiki/includes/skins/SkinApiTemplate.php
+++ b/www/wiki/includes/skins/SkinApiTemplate.php
@@ -4,8 +4,6 @@
* the usual skin elements but still using CSS, JS, and such via OutputPage and
* ResourceLoader.
*
- * Created on Sep 08, 2014
- *
* Copyright © 2014 Wikimedia Foundation and contributors
*
* This program is free software; you can redistribute it and/or modify
diff --git a/www/wiki/includes/skins/SkinFallback.php b/www/wiki/includes/skins/SkinFallback.php
index 96ff2285..d5f764c6 100644
--- a/www/wiki/includes/skins/SkinFallback.php
+++ b/www/wiki/includes/skins/SkinFallback.php
@@ -14,7 +14,7 @@
class SkinFallback extends SkinTemplate {
public $skinname = 'fallback';
- public $template = 'SkinFallbackTemplate';
+ public $template = SkinFallbackTemplate::class;
/**
* Add CSS via ResourceLoader
diff --git a/www/wiki/includes/skins/SkinFallbackTemplate.php b/www/wiki/includes/skins/SkinFallbackTemplate.php
index ee8d8417..bd02fa32 100644
--- a/www/wiki/includes/skins/SkinFallbackTemplate.php
+++ b/www/wiki/includes/skins/SkinFallbackTemplate.php
@@ -26,8 +26,7 @@ class SkinFallbackTemplate extends BaseTemplate {
// Filter out skins that aren't installed
$possibleSkins = array_filter( $possibleSkins, function ( $skinDir ) use ( $styleDirectory ) {
- return
- is_file( "$styleDirectory/$skinDir/skin.json" )
+ return is_file( "$styleDirectory/$skinDir/skin.json" )
|| is_file( "$styleDirectory/$skinDir/$skinDir.php" );
} );
@@ -96,12 +95,9 @@ class SkinFallbackTemplate extends BaseTemplate {
* warning message and page content.
*/
public function execute() {
- $this->html( 'headelement' ) ?>
-
- <div class="warningbox">
- <?php echo $this->buildHelpfulInformationMessage() ?>
- </div>
-
+ $this->html( 'headelement' );
+ echo Html::warningBox( $this->buildHelpfulInformationMessage() );
+ ?>
<form action="<?php $this->text( 'wgScript' ) ?>">
<input type="hidden" name="title" value="<?php $this->text( 'searchtitle' ) ?>" />
<h3><label for="searchInput"><?php $this->msg( 'search' ) ?></label></h3>
diff --git a/www/wiki/includes/skins/SkinTemplate.php b/www/wiki/includes/skins/SkinTemplate.php
index 4fcc8657..203326f1 100644
--- a/www/wiki/includes/skins/SkinTemplate.php
+++ b/www/wiki/includes/skins/SkinTemplate.php
@@ -46,7 +46,7 @@ class SkinTemplate extends Skin {
* @var string For QuickTemplate, the name of the subclass which will
* actually fill the template. Child classes should override the default.
*/
- public $template = 'QuickTemplate';
+ public $template = QuickTemplate::class;
public $thispage;
public $titletxt;
@@ -174,7 +174,7 @@ class SkinTemplate extends Skin {
)->text();
}
- $ilInterwikiCodeBCP47 = wfBCP47( $ilInterwikiCode );
+ $ilInterwikiCodeBCP47 = LanguageCode::bcp47( $ilInterwikiCode );
$languageLink = [
'href' => $languageLinkTitle->getFullURL(),
'text' => $ilLangName,
@@ -524,16 +524,49 @@ class SkinTemplate extends Skin {
* @return string
*/
public function getPersonalToolsList() {
+ return $this->makePersonalToolsList();
+ }
+
+ /**
+ * Get the HTML for the personal tools list
+ *
+ * @since 1.31
+ *
+ * @param array $personalTools
+ * @param array $options
+ * @return string
+ */
+ public function makePersonalToolsList( $personalTools = null, $options = [] ) {
$tpl = $this->setupTemplateForOutput();
$tpl->set( 'personal_urls', $this->buildPersonalUrls() );
$html = '';
- foreach ( $tpl->getPersonalTools() as $key => $item ) {
- $html .= $tpl->makeListItem( $key, $item );
+
+ if ( $personalTools === null ) {
+ $personalTools = $tpl->getPersonalTools();
}
+
+ foreach ( $personalTools as $key => $item ) {
+ $html .= $tpl->makeListItem( $key, $item, $options );
+ }
+
return $html;
}
/**
+ * Get personal tools for the user
+ *
+ * @since 1.31
+ *
+ * @return array Array of personal tools
+ */
+ public function getStructuredPersonalTools() {
+ $tpl = $this->setupTemplateForOutput();
+ $tpl->set( 'personal_urls', $this->buildPersonalUrls() );
+
+ return $tpl->getPersonalTools();
+ }
+
+ /**
* Format language name for use in sidebar interlanguage links list.
* By default it is capitalized.
*
@@ -594,21 +627,24 @@ class SkinTemplate extends Skin {
$page = Title::newFromText( $request->getVal( 'title', '' ) );
}
$page = $request->getVal( 'returnto', $page );
- $a = [];
+ $returnto = [];
if ( strval( $page ) !== '' ) {
- $a['returnto'] = $page;
+ $returnto['returnto'] = $page;
$query = $request->getVal( 'returntoquery', $this->thisquery );
+ $paramsArray = wfCgiToArray( $query );
+ unset( $paramsArray['logoutToken'] );
+ $query = wfArrayToCgi( $paramsArray );
if ( $query != '' ) {
- $a['returntoquery'] = $query;
+ $returnto['returntoquery'] = $query;
}
}
- $returnto = wfArrayToCgi( $a );
if ( $this->loggedin ) {
$personal_urls['userpage'] = [
'text' => $this->username,
'href' => &$this->userpageUrlDetails['href'],
'class' => $this->userpageUrlDetails['exists'] ? false : 'new',
+ 'exists' => $this->userpageUrlDetails['exists'],
'active' => ( $this->userpageUrlDetails['href'] == $pageurl ),
'dir' => 'auto'
];
@@ -617,6 +653,7 @@ class SkinTemplate extends Skin {
'text' => $this->msg( 'mytalk' )->text(),
'href' => &$usertalkUrlDetails['href'],
'class' => $usertalkUrlDetails['exists'] ? false : 'new',
+ 'exists' => $usertalkUrlDetails['exists'],
'active' => ( $usertalkUrlDetails['href'] == $pageurl )
];
$href = self::makeSpecialUrl( 'Preferences' );
@@ -664,9 +701,10 @@ class SkinTemplate extends Skin {
$personal_urls['logout'] = [
'text' => $this->msg( 'pt-userlogout' )->text(),
'href' => self::makeSpecialUrl( 'Userlogout',
- // userlogout link must always contain an & character, otherwise we might not be able
+ // Note: userlogout link must always contain an & character, otherwise we might not be able
// to detect a buggy precaching proxy (T19790)
- $title->isSpecial( 'Preferences' ) ? 'noreturnto' : $returnto ),
+ ( $title->isSpecial( 'Preferences' ) ? [] : $returnto )
+ + [ 'logoutToken' => $this->getUser()->getEditToken( 'logoutToken', $this->getRequest() ) ] ),
'active' => false
];
}
@@ -729,7 +767,7 @@ class SkinTemplate extends Skin {
}
}
- Hooks::run( 'PersonalUrls', [ &$personal_urls, &$title, $this ] );
+ Hooks::runWithoutAbort( 'PersonalUrls', [ &$personal_urls, &$title, $this ] );
return $personal_urls;
}
@@ -749,8 +787,10 @@ class SkinTemplate extends Skin {
if ( $selected ) {
$classes[] = 'selected';
}
+ $exists = true;
if ( $checkEdit && !$title->isKnown() ) {
$classes[] = 'new';
+ $exists = false;
if ( $query !== '' ) {
$query = 'action=edit&redlink=1&' . $query;
} else {
@@ -788,6 +828,7 @@ class SkinTemplate extends Skin {
'class' => implode( ' ', $classes ),
'text' => $text,
'href' => $title->getLocalURL( $query ),
+ 'exists' => $exists,
'primary' => true ];
if ( $linkClass !== '' ) {
$result['link-class'] = $linkClass;
@@ -1095,7 +1136,10 @@ class SkinTemplate extends Skin {
// Avoid PHP 7.1 warning of passing $this by reference
$skinTemplate = $this;
- Hooks::run( 'SkinTemplateNavigation', [ &$skinTemplate, &$content_navigation ] );
+ Hooks::runWithoutAbort(
+ 'SkinTemplateNavigation',
+ [ &$skinTemplate, &$content_navigation ]
+ );
if ( $userCanRead && !$wgDisableLangConversion ) {
$pageLang = $title->getPageLanguage();
@@ -1122,8 +1166,8 @@ class SkinTemplate extends Skin {
'class' => ( $code == $preferred ) ? 'selected' : false,
'text' => $varname,
'href' => $title->getLocalURL( [ 'variant' => $code ] + $params ),
- 'lang' => wfBCP47( $code ),
- 'hreflang' => wfBCP47( $code ),
+ 'lang' => LanguageCode::bcp47( $code ),
+ 'hreflang' => LanguageCode::bcp47( $code ),
];
}
}
@@ -1139,14 +1183,15 @@ class SkinTemplate extends Skin {
// Avoid PHP 7.1 warning of passing $this by reference
$skinTemplate = $this;
- Hooks::run( 'SkinTemplateNavigation::SpecialPage',
+ Hooks::runWithoutAbort( 'SkinTemplateNavigation::SpecialPage',
[ &$skinTemplate, &$content_navigation ] );
}
// Avoid PHP 7.1 warning of passing $this by reference
$skinTemplate = $this;
// Equiv to SkinTemplateContentActions
- Hooks::run( 'SkinTemplateNavigation::Universal', [ &$skinTemplate, &$content_navigation ] );
+ Hooks::runWithoutAbort( 'SkinTemplateNavigation::Universal',
+ [ &$skinTemplate, &$content_navigation ] );
// Setup xml ids and tooltip info
foreach ( $content_navigation as $section => &$links ) {
diff --git a/www/wiki/includes/sparql/SparqlClient.php b/www/wiki/includes/sparql/SparqlClient.php
new file mode 100644
index 00000000..778a3b32
--- /dev/null
+++ b/www/wiki/includes/sparql/SparqlClient.php
@@ -0,0 +1,220 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Sparql;
+
+use Http;
+use MediaWiki\Http\HttpRequestFactory;
+
+/**
+ * Simple SPARQL client
+ *
+ * @author Stas Malyshev
+ */
+class SparqlClient {
+
+ /**
+ * Limit on how long can be the query to be sent by GET.
+ */
+ const MAX_GET_SIZE = 2048;
+
+ /**
+ * User agent for HTTP requests.
+ * @var string
+ */
+ private $userAgent;
+
+ /**
+ * Query timeout (seconds)
+ * @var int
+ */
+ private $timeout = 30;
+
+ /**
+ * SPARQL endpoint URL
+ * @var string
+ */
+ private $endpoint;
+
+ /**
+ * Client options
+ * @var array
+ */
+ private $options = [];
+
+ /**
+ * @var HttpRequestFactory
+ */
+ private $requestFactory;
+
+ /**
+ * @param string $url SPARQL Endpoint
+ * @param HttpRequestFactory $requestFactory
+ */
+ public function __construct( $url, HttpRequestFactory $requestFactory ) {
+ $this->endpoint = $url;
+ $this->requestFactory = $requestFactory;
+ $this->userAgent = Http::userAgent() . " SparqlClient";
+ }
+
+ /**
+ * Set query timeout (in seconds)
+ * @param int $timeout
+ * @return $this
+ */
+ public function setTimeout( $timeout ) {
+ if ( $timeout >= 0 ) {
+ $this->timeout = $timeout;
+ }
+ return $this;
+ }
+
+ /**
+ * Set client options
+ * @param array $options
+ * @return $this
+ */
+ public function setClientOptions( $options ) {
+ $this->options = $options;
+ return $this;
+ }
+
+ /**
+ * Get current user agent.
+ * @return string
+ */
+ public function getUserAgent() {
+ return $this->userAgent;
+ }
+
+ /**
+ * Set user agent string.
+ *
+ * Mote it is not recommended to completely override user agent for
+ * most applications.
+ * @see appendUserAgent() for recommended way of specifying user agent.
+ *
+ * @param string $agent
+ */
+ public function setUserAgent( $agent ) {
+ $this->userAgent = $agent;
+ }
+
+ /**
+ * Append specific string to user agent.
+ *
+ * This is the recommended way of specifying the user agent
+ * for specific applications of the SparqlClient inside MediaWiki
+ * and extension code.
+ *
+ * @param string $agent
+ */
+ public function appendUserAgent( $agent ) {
+ $this->userAgent .= ' ' . $agent;
+ }
+
+ /**
+ * Query SPARQL endpoint
+ *
+ * @param string $sparql query
+ * @param bool $rawData Whether to return only values or full data objects
+ *
+ * @return array List of results, one row per array element
+ * Each row will contain fields indexed by variable name.
+ * @throws SparqlException
+ */
+ public function query( $sparql, $rawData = false ) {
+ if ( empty( $this->endpoint ) ) {
+ throw new SparqlException( 'Endpoint URL can not be empty' );
+ }
+ $queryData = [ "query" => $sparql, "format" => "json" ];
+ $options = array_merge( [ 'method' => 'GET' ], $this->options );
+
+ if ( empty( $options['userAgent'] ) ) {
+ $options['userAgent'] = $this->userAgent;
+ }
+
+ if ( $this->timeout >= 0 ) {
+ // Blazegraph setting, see https://wiki.blazegraph.com/wiki/index.php/REST_API
+ $queryData['maxQueryTimeMillis'] = $this->timeout * 1000;
+ $options['timeout'] = $this->timeout;
+ }
+
+ if ( strlen( $sparql ) > self::MAX_GET_SIZE ) {
+ // big requests go to POST
+ $options['method'] = 'POST';
+ $options['postData'] = 'query=' . urlencode( $sparql );
+ unset( $queryData['query'] );
+ }
+
+ $url = wfAppendQuery( $this->endpoint, $queryData );
+ $request = $this->requestFactory->create( $url, $options, __METHOD__ );
+
+ $status = $request->execute();
+
+ if ( !$status->isOK() ) {
+ throw new SparqlException( "HTTP error: {$status->getWikiText()}" );
+ }
+ $result = $request->getContent();
+ \Wikimedia\suppressWarnings();
+ $data = json_decode( $result, true );
+ \Wikimedia\restoreWarnings();
+ if ( $data === null || $data === false ) {
+ throw new SparqlException( "HTTP request failed, response:\n" .
+ substr( $result, 1024 ) );
+ }
+
+ return $this->extractData( $data, $rawData );
+ }
+
+ /**
+ * Extract data from SPARQL response format.
+ * The response must be in format described in:
+ * https://www.w3.org/TR/sparql11-results-json/
+ *
+ * @param array $data SPARQL result
+ * @param bool $rawData Whether to return only values or full data objects
+ *
+ * @return array List of results, one row per element.
+ */
+ private function extractData( $data, $rawData = false ) {
+ $result = [];
+ if ( $data && !empty( $data['results'] ) ) {
+ $vars = $data['head']['vars'];
+ $resrow = [];
+ foreach ( $data['results']['bindings'] as $row ) {
+ foreach ( $vars as $var ) {
+ if ( !isset( $row[$var] ) ) {
+ $resrow[$var] = null;
+ continue;
+ }
+ if ( $rawData ) {
+ $resrow[$var] = $row[$var];
+ } else {
+ $resrow[$var] = $row[$var]['value'];
+ }
+ }
+ $result[] = $resrow;
+ }
+ }
+ return $result;
+ }
+
+}
diff --git a/www/wiki/includes/sparql/SparqlException.php b/www/wiki/includes/sparql/SparqlException.php
new file mode 100644
index 00000000..d65521e4
--- /dev/null
+++ b/www/wiki/includes/sparql/SparqlException.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Sparql;
+
+use Exception;
+
+/**
+ * Exception for SPARQLClient
+ * @author Stas Malyshev
+ */
+class SparqlException extends Exception {
+}
diff --git a/www/wiki/includes/specialpage/AuthManagerSpecialPage.php b/www/wiki/includes/specialpage/AuthManagerSpecialPage.php
index 95729f3c..b9745de1 100644
--- a/www/wiki/includes/specialpage/AuthManagerSpecialPage.php
+++ b/www/wiki/includes/specialpage/AuthManagerSpecialPage.php
@@ -609,7 +609,7 @@ abstract class AuthManagerSpecialPage extends SpecialPage {
} elseif ( array_key_exists( 'type', $definition ) ) {
$class = HTMLForm::$typeMappings[$definition['type']];
}
- if ( $class !== 'HTMLInfoField' ) {
+ if ( $class !== HTMLInfoField::class ) {
$definition['tabindex'] = $i;
$i++;
}
diff --git a/www/wiki/includes/specialpage/ChangesListSpecialPage.php b/www/wiki/includes/specialpage/ChangesListSpecialPage.php
index dd0dd92a..ac13f113 100644
--- a/www/wiki/includes/specialpage/ChangesListSpecialPage.php
+++ b/www/wiki/includes/specialpage/ChangesListSpecialPage.php
@@ -21,7 +21,8 @@
* @ingroup SpecialPage
*/
use MediaWiki\Logger\LoggerFactory;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\DBQueryTimeoutError;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
@@ -33,11 +34,29 @@ use Wikimedia\Rdbms\IDatabase;
*/
abstract class ChangesListSpecialPage extends SpecialPage {
/**
+ * Maximum length of a tag description in UTF-8 characters.
+ * Longer descriptions will be truncated.
+ */
+ const TAG_DESC_CHARACTER_LIMIT = 120;
+
+ /**
* Preference name for saved queries. Subclasses that use saved queries should override this.
* @var string
*/
protected static $savedQueriesPreferenceName;
+ /**
+ * Preference name for 'days'. Subclasses should override this.
+ * @var string
+ */
+ protected static $daysPreferenceName;
+
+ /**
+ * Preference name for 'limit'. Subclasses should override this.
+ * @var string
+ */
+ protected static $limitPreferenceName;
+
/** @var string */
protected $rcSubpage;
@@ -68,9 +87,12 @@ abstract class ChangesListSpecialPage extends SpecialPage {
// Same format as filterGroupDefinitions, but for a single group (reviewStatus)
// that is registered conditionally.
+ private $legacyReviewStatusFilterGroupDefinition;
+
+ // Single filter group registered conditionally
private $reviewStatusFilterGroupDefinition;
- // Single filter registered conditionally
+ // Single filter group registered conditionally
private $hideCategorizationFilterDefinition;
/**
@@ -84,6 +106,9 @@ abstract class ChangesListSpecialPage extends SpecialPage {
public function __construct( $name, $restriction ) {
parent::__construct( $name, $restriction );
+ $nonRevisionTypes = [ RC_LOG ];
+ Hooks::run( 'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] );
+
$this->filterGroupDefinitions = [
[
'name' => 'registration',
@@ -99,7 +124,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $conds[] = 'rc_user = 0';
+ $actorMigration = ActorMigration::newMigration();
+ $actorQuery = $actorMigration->getJoin( 'rc_user' );
+ $tables += $actorQuery['tables'];
+ $join_conds += $actorQuery['joins'];
+ $conds[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
},
'isReplacedInStructuredUi' => true,
@@ -113,7 +142,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $conds[] = 'rc_user != 0';
+ $actorMigration = ActorMigration::newMigration();
+ $actorQuery = $actorMigration->getJoin( 'rc_user' );
+ $tables += $actorQuery['tables'];
+ $join_conds += $actorQuery['joins'];
+ $conds[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
},
'isReplacedInStructuredUi' => true,
]
@@ -198,8 +231,10 @@ abstract class ChangesListSpecialPage extends SpecialPage {
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $user = $ctx->getUser();
- $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
+ $actorQuery = ActorMigration::newMigration()->getWhere( $dbr, 'rc_user', $ctx->getUser() );
+ $tables += $actorQuery['tables'];
+ $join_conds += $actorQuery['joins'];
+ $conds[] = 'NOT(' . $actorQuery['conds'] . ')';
},
'cssClassSuffix' => 'self',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
@@ -214,8 +249,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $user = $ctx->getUser();
- $conds[] = 'rc_user_text = ' . $dbr->addQuotes( $user->getName() );
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $dbr, 'rc_user', $ctx->getUser(), false );
+ $tables += $actorQuery['tables'];
+ $join_conds += $actorQuery['joins'];
+ $conds[] = $actorQuery['conds'];
},
'cssClassSuffix' => 'others',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
@@ -241,7 +279,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $conds[] = 'rc_bot = 0';
+ $conds['rc_bot'] = 0;
},
'cssClassSuffix' => 'bot',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
@@ -256,7 +294,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $conds[] = 'rc_bot = 1';
+ $conds['rc_bot'] = 1;
},
'cssClassSuffix' => 'human',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
@@ -266,7 +304,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
]
],
- // reviewStatus (conditional)
+ // significance (conditional)
[
'name' => 'significance',
@@ -322,8 +360,14 @@ abstract class ChangesListSpecialPage extends SpecialPage {
'description' => 'rcfilters-filter-lastrevision-description',
'default' => false,
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
- &$query_options, &$join_conds ) {
- $conds[] = 'rc_this_oldid <> page_latest';
+ &$query_options, &$join_conds ) use ( $nonRevisionTypes ) {
+ $conds[] = $dbr->makeList(
+ [
+ 'rc_this_oldid <> page_latest',
+ 'rc_type' => $nonRevisionTypes,
+ ],
+ LIST_OR
+ );
},
'cssClassSuffix' => 'last',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
@@ -336,8 +380,14 @@ abstract class ChangesListSpecialPage extends SpecialPage {
'description' => 'rcfilters-filter-previousrevision-description',
'default' => false,
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
- &$query_options, &$join_conds ) {
- $conds[] = 'rc_this_oldid = page_latest';
+ &$query_options, &$join_conds ) use ( $nonRevisionTypes ) {
+ $conds[] = $dbr->makeList(
+ [
+ 'rc_this_oldid = page_latest',
+ 'rc_type' => $nonRevisionTypes,
+ ],
+ LIST_OR
+ );
},
'cssClassSuffix' => 'previous',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
@@ -410,17 +460,14 @@ abstract class ChangesListSpecialPage extends SpecialPage {
];
- $this->reviewStatusFilterGroupDefinition = [
+ $this->legacyReviewStatusFilterGroupDefinition = [
[
- 'name' => 'reviewStatus',
+ 'name' => 'legacyReviewStatus',
'title' => 'rcfilters-filtergroup-reviewstatus',
'class' => ChangesListBooleanFilterGroup::class,
- 'priority' => -5,
'filters' => [
[
'name' => 'hidepatrolled',
- 'label' => 'rcfilters-filter-patrolled-label',
- 'description' => 'rcfilters-filter-patrolled-description',
// rcshowhidepatr-show, rcshowhidepatr-hide
// wlshowhidepatr
'showHideSuffix' => 'showhidepatr',
@@ -428,29 +475,77 @@ abstract class ChangesListSpecialPage extends SpecialPage {
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $conds[] = 'rc_patrolled = 0';
- },
- 'cssClassSuffix' => 'patrolled',
- 'isRowApplicableCallable' => function ( $ctx, $rc ) {
- return $rc->getAttribute( 'rc_patrolled' );
+ $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
},
+ 'isReplacedInStructuredUi' => true,
],
[
'name' => 'hideunpatrolled',
- 'label' => 'rcfilters-filter-unpatrolled-label',
- 'description' => 'rcfilters-filter-unpatrolled-description',
'default' => false,
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $conds[] = 'rc_patrolled = 1';
+ $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
},
- 'cssClassSuffix' => 'unpatrolled',
+ 'isReplacedInStructuredUi' => true,
+ ],
+ ],
+ ]
+ ];
+
+ $this->reviewStatusFilterGroupDefinition = [
+ [
+ 'name' => 'reviewStatus',
+ 'title' => 'rcfilters-filtergroup-reviewstatus',
+ 'class' => ChangesListStringOptionsFilterGroup::class,
+ 'isFullCoverage' => true,
+ 'priority' => -5,
+ 'filters' => [
+ [
+ 'name' => 'unpatrolled',
+ 'label' => 'rcfilters-filter-reviewstatus-unpatrolled-label',
+ 'description' => 'rcfilters-filter-reviewstatus-unpatrolled-description',
+ 'cssClassSuffix' => 'reviewstatus-unpatrolled',
+ 'isRowApplicableCallable' => function ( $ctx, $rc ) {
+ return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED;
+ },
+ ],
+ [
+ 'name' => 'manual',
+ 'label' => 'rcfilters-filter-reviewstatus-manual-label',
+ 'description' => 'rcfilters-filter-reviewstatus-manual-description',
+ 'cssClassSuffix' => 'reviewstatus-manual',
+ 'isRowApplicableCallable' => function ( $ctx, $rc ) {
+ return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_PATROLLED;
+ },
+ ],
+ [
+ 'name' => 'auto',
+ 'label' => 'rcfilters-filter-reviewstatus-auto-label',
+ 'description' => 'rcfilters-filter-reviewstatus-auto-description',
+ 'cssClassSuffix' => 'reviewstatus-auto',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
- return !$rc->getAttribute( 'rc_patrolled' );
+ return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_AUTOPATROLLED;
},
],
],
+ 'default' => ChangesListStringOptionsFilterGroup::NONE,
+ 'queryCallable' => function ( $specialPageClassName, $ctx, $dbr,
+ &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected
+ ) {
+ if ( $selected === [] ) {
+ return;
+ }
+ $rcPatrolledValues = [
+ 'unpatrolled' => RecentChange::PRC_UNPATROLLED,
+ 'manual' => RecentChange::PRC_PATROLLED,
+ 'auto' => RecentChange::PRC_AUTOPATROLLED,
+ ];
+ // e.g. rc_patrolled IN (0, 2)
+ $conds['rc_patrolled'] = array_map( function ( $s ) use ( $rcPatrolledValues ) {
+ return $rcPatrolledValues[ $s ];
+ }, $selected );
+ }
]
];
@@ -525,45 +620,66 @@ abstract class ChangesListSpecialPage extends SpecialPage {
public function execute( $subpage ) {
$this->rcSubpage = $subpage;
- $rows = $this->getRows();
+ $this->considerActionsForDefaultSavedQuery( $subpage );
+
$opts = $this->getOptions();
- if ( $rows === false ) {
- $rows = new FakeResultWrapper( [] );
- }
+ try {
+ $rows = $this->getRows();
+ if ( $rows === false ) {
+ $rows = new FakeResultWrapper( [] );
+ }
- // Used by Structured UI app to get results without MW chrome
- if ( $this->getRequest()->getVal( 'action' ) === 'render' ) {
- $this->getOutput()->setArticleBodyOnly( true );
- }
+ // Used by Structured UI app to get results without MW chrome
+ if ( $this->getRequest()->getVal( 'action' ) === 'render' ) {
+ $this->getOutput()->setArticleBodyOnly( true );
+ }
- // Used by "live update" and "view newest" to check
- // if there's new changes with minimal data transfer
- if ( $this->getRequest()->getBool( 'peek' ) ) {
- $code = $rows->numRows() > 0 ? 200 : 204;
- $this->getOutput()->setStatusCode( $code );
- return;
- }
+ // Used by "live update" and "view newest" to check
+ // if there's new changes with minimal data transfer
+ if ( $this->getRequest()->getBool( 'peek' ) ) {
+ $code = $rows->numRows() > 0 ? 200 : 204;
+ $this->getOutput()->setStatusCode( $code );
- $batch = new LinkBatch;
- foreach ( $rows as $row ) {
- $batch->add( NS_USER, $row->rc_user_text );
- $batch->add( NS_USER_TALK, $row->rc_user_text );
- $batch->add( $row->rc_namespace, $row->rc_title );
- if ( $row->rc_source === RecentChange::SRC_LOG ) {
- $formatter = LogFormatter::newFromRow( $row );
- foreach ( $formatter->getPreloadTitles() as $title ) {
- $batch->addObj( $title );
+ if ( $this->getUser()->isAnon() !==
+ $this->getRequest()->getFuzzyBool( 'isAnon' )
+ ) {
+ $this->getOutput()->setStatusCode( 205 );
}
+
+ return;
}
- }
- $batch->execute();
- $this->setHeaders();
- $this->outputHeader();
- $this->addModules();
- $this->webOutput( $rows, $opts );
+ $batch = new LinkBatch;
+ foreach ( $rows as $row ) {
+ $batch->add( NS_USER, $row->rc_user_text );
+ $batch->add( NS_USER_TALK, $row->rc_user_text );
+ $batch->add( $row->rc_namespace, $row->rc_title );
+ if ( $row->rc_source === RecentChange::SRC_LOG ) {
+ $formatter = LogFormatter::newFromRow( $row );
+ foreach ( $formatter->getPreloadTitles() as $title ) {
+ $batch->addObj( $title );
+ }
+ }
+ }
+ $batch->execute();
+
+ $this->setHeaders();
+ $this->outputHeader();
+ $this->addModules();
+ $this->webOutput( $rows, $opts );
- $rows->free();
+ $rows->free();
+ } catch ( DBQueryTimeoutError $timeoutException ) {
+ MWExceptionHandler::logException( $timeoutException );
+
+ $this->setHeaders();
+ $this->outputHeader();
+ $this->addModules();
+
+ $this->getOutput()->setStatusCode( 500 );
+ $this->webOutputHeader( 0, $opts );
+ $this->outputTimeout();
+ }
if ( $this->getConfig()->get( 'EnableWANCacheReaper' ) ) {
// Clean up any bad page entries for titles showing up in RC
@@ -577,6 +693,79 @@ abstract class ChangesListSpecialPage extends SpecialPage {
}
/**
+ * Check whether or not the page should load defaults, and if so, whether
+ * a default saved query is relevant to be redirected to. If it is relevant,
+ * redirect properly with all necessary query parameters.
+ *
+ * @param string $subpage
+ */
+ protected function considerActionsForDefaultSavedQuery( $subpage ) {
+ if ( !$this->isStructuredFilterUiEnabled() || $this->including() ) {
+ return;
+ }
+
+ $knownParams = call_user_func_array(
+ [ $this->getRequest(), 'getValues' ],
+ array_keys( $this->getOptions()->getAllValues() )
+ );
+
+ // HACK: Temporarily until we can properly define "sticky" filters and parameters,
+ // we need to exclude several parameters we know should not be counted towards preventing
+ // the loading of defaults.
+ $excludedParams = [ 'limit' => '', 'days' => '', 'enhanced' => '', 'from' => '' ];
+ $knownParams = array_diff_key( $knownParams, $excludedParams );
+
+ if (
+ // If there are NO known parameters in the URL request
+ // (that are not excluded) then we need to check into loading
+ // the default saved query
+ count( $knownParams ) === 0
+ ) {
+ // Get the saved queries data and parse it
+ $savedQueries = FormatJson::decode(
+ $this->getUser()->getOption( static::$savedQueriesPreferenceName ),
+ true
+ );
+
+ if ( $savedQueries && isset( $savedQueries[ 'default' ] ) ) {
+ // Only load queries that are 'version' 2, since those
+ // have parameter representation
+ if ( isset( $savedQueries[ 'version' ] ) && $savedQueries[ 'version' ] === '2' ) {
+ $savedQueryDefaultID = $savedQueries[ 'default' ];
+ $defaultQuery = $savedQueries[ 'queries' ][ $savedQueryDefaultID ][ 'data' ];
+
+ // Build the entire parameter list
+ $query = array_merge(
+ $defaultQuery[ 'params' ],
+ $defaultQuery[ 'highlights' ],
+ [
+ 'urlversion' => '2',
+ ]
+ );
+ // Add to the query any parameters that we may have ignored before
+ // but are still valid and requested in the URL
+ $query = array_merge( $this->getRequest()->getValues(), $query );
+ unset( $query[ 'title' ] );
+ $this->getOutput()->redirect( $this->getPageTitle( $subpage )->getCanonicalURL( $query ) );
+ } else {
+ // There's a default, but the version is not 2, and the server can't
+ // actually recognize the query itself. This happens if it is before
+ // the conversion, so we need to tell the UI to reload saved query as
+ // it does the conversion to version 2
+ $this->getOutput()->addJsConfigVars(
+ 'wgStructuredChangeFiltersDefaultSavedQueryExists',
+ true
+ );
+
+ // Add the class that tells the frontend it is still loading
+ // another query
+ $this->getOutput()->addBodyClasses( 'mw-rcfilters-ui-loading' );
+ }
+ }
+ }
+ }
+
+ /**
* Include the modules and configuration for the RCFilters app.
* Conditional on the user having the feature enabled.
*
@@ -584,7 +773,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
*/
protected function includeRcFiltersApp() {
$out = $this->getOutput();
- if ( $this->isStructuredFilterUiEnabled() ) {
+ if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) {
$jsData = $this->getStructuredFilterJsData();
$messages = [];
@@ -600,18 +789,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
)
);
- $experimentalStructuredChangeFilters =
- $this->getConfig()->get( 'StructuredChangeFiltersEnableExperimentalViews' );
-
$out->addJsConfigVars( 'wgStructuredChangeFilters', $jsData['groups'] );
- $out->addJsConfigVars(
- 'wgStructuredChangeFiltersEnableExperimentalViews',
- $experimentalStructuredChangeFilters
- );
$out->addJsConfigVars(
'wgRCFiltersChangeTags',
- $this->buildChangeTagList()
+ $this->getChangeTagList()
);
$out->addJsConfigVars(
'StructuredChangeFiltersDisplayConfig',
@@ -624,20 +806,23 @@ abstract class ChangesListSpecialPage extends SpecialPage {
]
);
- if ( static::$savedQueriesPreferenceName ) {
- $savedQueries = FormatJson::decode(
- $this->getUser()->getOption( static::$savedQueriesPreferenceName )
- );
- if ( $savedQueries && isset( $savedQueries->default ) ) {
- // If there is a default saved query, show a loading spinner,
- // since the frontend is going to reload the results
- $out->addBodyClasses( 'mw-rcfilters-ui-loading' );
- }
- $out->addJsConfigVars(
- 'wgStructuredChangeFiltersSavedQueriesPreferenceName',
- static::$savedQueriesPreferenceName
- );
- }
+ $out->addJsConfigVars(
+ 'wgStructuredChangeFiltersSavedQueriesPreferenceName',
+ static::$savedQueriesPreferenceName
+ );
+ $out->addJsConfigVars(
+ 'wgStructuredChangeFiltersLimitPreferenceName',
+ static::$limitPreferenceName
+ );
+ $out->addJsConfigVars(
+ 'wgStructuredChangeFiltersDaysPreferenceName',
+ static::$daysPreferenceName
+ );
+
+ $out->addJsConfigVars(
+ 'StructuredChangeFiltersLiveUpdatePollingRate',
+ $this->getConfig()->get( 'StructuredChangeFiltersLiveUpdatePollingRate' )
+ );
} else {
$out->addBodyClasses( 'mw-rcfilters-disabled' );
}
@@ -648,49 +833,60 @@ abstract class ChangesListSpecialPage extends SpecialPage {
*
* @return Array Tag data
*/
- protected function buildChangeTagList() {
- $explicitlyDefinedTags = array_fill_keys( ChangeTags::listExplicitlyDefinedTags(), 0 );
- $softwareActivatedTags = array_fill_keys( ChangeTags::listSoftwareActivatedTags(), 0 );
-
- // Hit counts disabled for perf reasons, see T169997
- /*
- $tagStats = ChangeTags::tagUsageStatistics();
- $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
-
- // Sort by hits
- arsort( $tagHitCounts );
- */
- $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags );
-
- // Build the list and data
- $result = [];
- foreach ( $tagHitCounts as $tagName => $hits ) {
- if (
- // Only get active tags
- isset( $explicitlyDefinedTags[ $tagName ] ) ||
- isset( $softwareActivatedTags[ $tagName ] )
- ) {
- // Parse description
- $desc = ChangeTags::tagLongDescriptionMessage( $tagName, $this->getContext() );
-
- $result[] = [
- 'name' => $tagName,
- 'label' => Sanitizer::stripAllTags(
- ChangeTags::tagDescription( $tagName, $this->getContext() )
- ),
- 'description' => $desc ? Sanitizer::stripAllTags( $desc->parse() ) : '',
- 'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
- 'hits' => $hits,
- ];
- }
- }
+ protected function getChangeTagList() {
+ $cache = ObjectCache::getMainWANInstance();
+ $context = $this->getContext();
+ return $cache->getWithSetCallback(
+ $cache->makeKey( 'changeslistspecialpage-changetags', $context->getLanguage()->getCode() ),
+ $cache::TTL_MINUTE * 10,
+ function () use ( $context ) {
+ $explicitlyDefinedTags = array_fill_keys( ChangeTags::listExplicitlyDefinedTags(), 0 );
+ $softwareActivatedTags = array_fill_keys( ChangeTags::listSoftwareActivatedTags(), 0 );
+
+ // Hit counts disabled for perf reasons, see T169997
+ /*
+ $tagStats = ChangeTags::tagUsageStatistics();
+ $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
+
+ // Sort by hits
+ arsort( $tagHitCounts );
+ */
+ $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags );
+
+ // Build the list and data
+ $result = [];
+ foreach ( $tagHitCounts as $tagName => $hits ) {
+ if (
+ // Only get active tags
+ isset( $explicitlyDefinedTags[ $tagName ] ) ||
+ isset( $softwareActivatedTags[ $tagName ] )
+ ) {
+ $result[] = [
+ 'name' => $tagName,
+ 'label' => Sanitizer::stripAllTags(
+ ChangeTags::tagDescription( $tagName, $context )
+ ),
+ 'description' =>
+ ChangeTags::truncateTagDescription(
+ $tagName, self::TAG_DESC_CHARACTER_LIMIT, $context
+ ),
+ 'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
+ 'hits' => $hits,
+ ];
+ }
+ }
- // Instead of sorting by hit count (disabled, see above), sort by display name
- usort( $result, function ( $a, $b ) {
- return strcasecmp( $a['label'], $b['label'] );
- } );
+ // Instead of sorting by hit count (disabled, see above), sort by display name
+ usort( $result, function ( $a, $b ) {
+ return strcasecmp( $a['label'], $b['label'] );
+ } );
- return $result;
+ return $result;
+ },
+ [
+ 'lockTSE' => 30
+ ]
+ );
}
/**
@@ -705,9 +901,20 @@ abstract class ChangesListSpecialPage extends SpecialPage {
}
/**
+ * Add the "timeout" message to the output
+ */
+ protected function outputTimeout() {
+ $this->getOutput()->addHTML(
+ '<div class="mw-changeslist-empty mw-changeslist-timeout">' .
+ $this->msg( 'recentchanges-timeout' )->parse() .
+ '</div>'
+ );
+ }
+
+ /**
* Get the database result for this special page instance. Used by ApiFeedRecentChanges.
*
- * @return bool|ResultWrapper Result or false
+ * @return bool|IResultWrapper Result or false
*/
public function getRows() {
$opts = $this->getOptions();
@@ -751,6 +958,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
// information to all users just because the user that saves the edit can
// patrol or is logged in)
if ( !$this->including() && $this->getUser()->useRCPatrol() ) {
+ $this->registerFiltersFromDefinitions( $this->legacyReviewStatusFilterGroupDefinition );
$this->registerFiltersFromDefinitions( $this->reviewStatusFilterGroupDefinition );
}
@@ -884,6 +1092,23 @@ abstract class ChangesListSpecialPage extends SpecialPage {
}
/**
+ * @return array The legacy show/hide toggle filters
+ */
+ protected function getLegacyShowHideFilters() {
+ $filters = [];
+ foreach ( $this->filterGroups as $group ) {
+ if ( $group instanceof ChangesListBooleanFilterGroup ) {
+ foreach ( $group->getFilters() as $key => $filter ) {
+ if ( $filter->displaysOnUnstructuredUi( $this ) ) {
+ $filters[ $key ] = $filter;
+ }
+ }
+ }
+ }
+ return $filters;
+ }
+
+ /**
* Register all the filters, including legacy hook-driven ones.
* Then create a FormOptions object with options as specified by the user
*
@@ -923,19 +1148,9 @@ abstract class ChangesListSpecialPage extends SpecialPage {
// If urlversion=2 is set, ignore the filter defaults and set them all to false/empty
$useDefaults = $this->getRequest()->getInt( 'urlversion' ) !== 2;
- // Add all filters
/** @var ChangesListFilterGroup $filterGroup */
foreach ( $this->filterGroups as $filterGroup ) {
- // URL parameters can be per-group, like 'userExpLevel',
- // or per-filter, like 'hideminor'.
- if ( $filterGroup->isPerGroupRequestParameter() ) {
- $opts->add( $filterGroup->getName(), $useDefaults ? $filterGroup->getDefault() : '' );
- } else {
- /** @var ChangesListBooleanFilter $filter */
- foreach ( $filterGroup->getFilters() as $filter ) {
- $opts->add( $filter->getName(), $useDefaults ? $filter->getDefault( $structuredUI ) : false );
- }
- }
+ $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
}
$opts->add( 'namespace', '', FormOptions::STRING );
@@ -944,6 +1159,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
$opts->add( 'urlversion', 1 );
$opts->add( 'tagfilter', '' );
+ $opts->add( 'days', $this->getDefaultDays(), FormOptions::FLOAT );
+ $opts->add( 'limit', $this->getDefaultLimit(), FormOptions::INT );
+
+ $opts->add( 'from', '' );
+
return $opts;
}
@@ -1061,9 +1281,9 @@ abstract class ChangesListSpecialPage extends SpecialPage {
// or per-filter, like 'hideminor'.
foreach ( $this->filterGroups as $filterGroup ) {
- if ( $filterGroup->isPerGroupRequestParameter() ) {
+ if ( $filterGroup instanceof ChangesListStringOptionsFilterGroup ) {
$stringParameterNameSet[$filterGroup->getName()] = true;
- } elseif ( $filterGroup->getType() === ChangesListBooleanFilterGroup::TYPE ) {
+ } elseif ( $filterGroup instanceof ChangesListBooleanFilterGroup ) {
foreach ( $filterGroup->getFilters() as $filter ) {
$hideParameterNameSet[$filter->getName()] = true;
}
@@ -1093,10 +1313,16 @@ abstract class ChangesListSpecialPage extends SpecialPage {
* @param FormOptions $opts
*/
public function validateOptions( FormOptions $opts ) {
- if ( $this->fixContradictoryOptions( $opts ) ) {
+ $isContradictory = $this->fixContradictoryOptions( $opts );
+ $isReplaced = $this->replaceOldOptions( $opts );
+
+ if ( $isContradictory || $isReplaced ) {
$query = wfArrayToCgi( $this->convertParamsForLink( $opts->getChangedValues() ) );
$this->getOutput()->redirect( $this->getPageTitle()->getCanonicalURL( $query ) );
}
+
+ $opts->validateIntBounds( 'limit', 0, 5000 );
+ $opts->validateBounds( 'days', 0, $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
}
/**
@@ -1162,6 +1388,53 @@ abstract class ChangesListSpecialPage extends SpecialPage {
}
/**
+ * Replace old options with their structured UI equivalents
+ *
+ * @param FormOptions $opts
+ * @return bool True if the change was made
+ */
+ public function replaceOldOptions( FormOptions $opts ) {
+ if ( !$this->isStructuredFilterUiEnabled() ) {
+ return false;
+ }
+
+ $changed = false;
+
+ // At this point 'hideanons' and 'hideliu' cannot be both true,
+ // because fixBackwardsCompatibilityOptions resets (at least) 'hideanons' in such case
+ if ( $opts[ 'hideanons' ] ) {
+ $opts->reset( 'hideanons' );
+ $opts[ 'userExpLevel' ] = 'registered';
+ $changed = true;
+ }
+
+ if ( $opts[ 'hideliu' ] ) {
+ $opts->reset( 'hideliu' );
+ $opts[ 'userExpLevel' ] = 'unregistered';
+ $changed = true;
+ }
+
+ if ( $this->getFilterGroup( 'legacyReviewStatus' ) ) {
+ if ( $opts[ 'hidepatrolled' ] ) {
+ $opts->reset( 'hidepatrolled' );
+ $opts[ 'reviewStatus' ] = 'unpatrolled';
+ $changed = true;
+ }
+
+ if ( $opts[ 'hideunpatrolled' ] ) {
+ $opts->reset( 'hideunpatrolled' );
+ $opts[ 'reviewStatus' ] = implode(
+ ChangesListStringOptionsFilterGroup::SEPARATOR,
+ [ 'manual', 'auto' ]
+ );
+ $changed = true;
+ }
+ }
+
+ return $changed;
+ }
+
+ /**
* Convert parameters values from true/false to 1/0
* so they are not omitted by wfArrayToCgi()
* Bug 36524
@@ -1196,20 +1469,10 @@ abstract class ChangesListSpecialPage extends SpecialPage {
$dbr = $this->getDB();
$isStructuredUI = $this->isStructuredFilterUiEnabled();
+ /** @var ChangesListFilterGroup $filterGroup */
foreach ( $this->filterGroups as $filterGroup ) {
- // URL parameters can be per-group, like 'userExpLevel',
- // or per-filter, like 'hideminor'.
- if ( $filterGroup->isPerGroupRequestParameter() ) {
- $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds,
- $query_options, $join_conds, $opts[$filterGroup->getName()] );
- } else {
- foreach ( $filterGroup->getFilters() as $filter ) {
- if ( $filter->isActive( $opts, $isStructuredUI ) ) {
- $filter->modifyQuery( $dbr, $this, $tables, $fields, $conds,
- $query_options, $join_conds );
- }
- }
- }
+ $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds,
+ $query_options, $join_conds, $opts, $isStructuredUI );
}
// Namespace filtering
@@ -1236,6 +1499,19 @@ abstract class ChangesListSpecialPage extends SpecialPage {
}
$conds[] = "rc_namespace $operator $value";
}
+
+ // Calculate cutoff
+ $cutoff_unixtime = time() - $opts['days'] * 3600 * 24;
+ $cutoff = $dbr->timestamp( $cutoff_unixtime );
+
+ $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
+ if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) {
+ $cutoff = $dbr->timestamp( $opts['from'] );
+ } else {
+ $opts->reset( 'from' );
+ }
+
+ $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
}
/**
@@ -1247,13 +1523,15 @@ abstract class ChangesListSpecialPage extends SpecialPage {
* @param array $query_options Array of query options; see IDatabase::select $options
* @param array $join_conds Array of join conditions; see IDatabase::select $join_conds
* @param FormOptions $opts
- * @return bool|ResultWrapper Result or false
+ * @return bool|IResultWrapper Result or false
*/
protected function doMainQuery( $tables, $fields, $conds,
$query_options, $join_conds, FormOptions $opts
) {
- $tables[] = 'recentchanges';
- $fields = array_merge( RecentChange::selectFields(), $fields );
+ $rcQuery = RecentChange::getQueryInfo();
+ $tables = array_merge( $tables, $rcQuery['tables'] );
+ $fields = array_merge( $rcQuery['fields'], $fields );
+ $join_conds = array_merge( $join_conds, $rcQuery['joins'] );
ChangeTags::modifyDisplayQuery(
$tables,
@@ -1301,16 +1579,26 @@ abstract class ChangesListSpecialPage extends SpecialPage {
}
/**
- * Send output to the OutputPage object, only called if not used feeds
+ * Send header output to the OutputPage object, only called if not using feeds
*
- * @param ResultWrapper $rows Database rows
+ * @param int $rowCount Number of database rows
* @param FormOptions $opts
*/
- public function webOutput( $rows, $opts ) {
+ private function webOutputHeader( $rowCount, $opts ) {
if ( !$this->including() ) {
$this->outputFeedLinks();
- $this->doHeader( $opts, $rows->numRows() );
+ $this->doHeader( $opts, $rowCount );
}
+ }
+
+ /**
+ * Send output to the OutputPage object, only called if not used feeds
+ *
+ * @param IResultWrapper $rows Database rows
+ * @param FormOptions $opts
+ */
+ public function webOutput( $rows, $opts ) {
+ $this->webOutputHeader( $rows->numRows(), $opts );
$this->outputChangesList( $rows, $opts );
}
@@ -1325,7 +1613,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
/**
* Build and output the actual changes list.
*
- * @param ResultWrapper $rows Database rows
+ * @param IResultWrapper $rows Database rows
* @param FormOptions $opts
*/
abstract public function outputChangesList( $rows, $opts );
@@ -1422,8 +1710,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
$context->msg( 'recentchanges-legend-heading' )->parse();
# Collapsible
+ $collapsedState = $this->getRequest()->getCookie( 'changeslist-state' );
+ $collapsedClass = $collapsedState === 'collapsed' ? ' mw-collapsed' : '';
+
$legend =
- '<div class="mw-changeslist-legend">' .
+ '<div class="mw-changeslist-legend mw-collapsible' . $collapsedClass . '">' .
$legendHeading .
'<div class="mw-collapsible-content">' . $legend . '</div>' .
'</div>';
@@ -1443,7 +1734,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
] );
$out->addModules( 'mediawiki.special.changeslist.legend.js' );
- if ( $this->isStructuredFilterUiEnabled() ) {
+ if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) {
$out->addModules( 'mediawiki.rcfilters.filters.ui' );
$out->addModuleStyles( 'mediawiki.rcfilters.filters.base.styles' );
}
@@ -1492,22 +1783,27 @@ abstract class ChangesListSpecialPage extends SpecialPage {
return;
}
+ $actorMigration = ActorMigration::newMigration();
+ $actorQuery = $actorMigration->getJoin( 'rc_user' );
+ $tables += $actorQuery['tables'];
+ $join_conds += $actorQuery['joins'];
+
// 'registered' but not 'unregistered', experience levels, if any, are included in 'registered'
if (
in_array( 'registered', $selectedExpLevels ) &&
!in_array( 'unregistered', $selectedExpLevels )
) {
- $conds[] = 'rc_user != 0';
+ $conds[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
return;
}
if ( $selectedExpLevels === [ 'unregistered' ] ) {
- $conds[] = 'rc_user = 0';
+ $conds[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
return;
}
$tables[] = 'user';
- $join_conds['user'] = [ 'LEFT JOIN', 'rc_user = user_id' ];
+ $join_conds['user'] = [ 'LEFT JOIN', $actorQuery['fields']['rc_user'] . ' = user_id' ];
if ( $now === 0 ) {
$now = time();
@@ -1537,7 +1833,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
if ( in_array( 'unregistered', $selectedExpLevels ) ) {
$selectedExpLevels = array_diff( $selectedExpLevels, [ 'unregistered' ] );
- $conditions[] = 'rc_user = 0';
+ $conditions[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
}
if ( $selectedExpLevels === [ 'newcomer' ] ) {
@@ -1559,7 +1855,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
} elseif ( $selectedExpLevels === [ 'experienced', 'learner' ] ) {
$conditions[] = $aboveNewcomer;
} elseif ( $selectedExpLevels === [ 'experienced', 'learner', 'newcomer' ] ) {
- $conditions[] = 'rc_user != 0';
+ $conditions[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
}
if ( count( $conditions ) > 1 ) {
@@ -1579,11 +1875,10 @@ abstract class ChangesListSpecialPage extends SpecialPage {
return true;
}
- if ( $this->getConfig()->get( 'StructuredChangeFiltersShowPreference' ) ) {
- return !$this->getUser()->getOption( 'rcenhancedfilters-disable' );
- } else {
- return $this->getUser()->getOption( 'rcenhancedfilters' );
- }
+ return static::checkStructuredFilterUiEnabled(
+ $this->getConfig(),
+ $this->getUser()
+ );
}
/**
@@ -1600,14 +1895,42 @@ abstract class ChangesListSpecialPage extends SpecialPage {
}
}
- abstract function getDefaultLimit();
+ /**
+ * Static method to check whether StructuredFilter UI is enabled for the given user
+ *
+ * @since 1.31
+ * @param Config $config
+ * @param User $user
+ * @return bool
+ */
+ public static function checkStructuredFilterUiEnabled( Config $config, User $user ) {
+ if ( $config->get( 'StructuredChangeFiltersShowPreference' ) ) {
+ return !$user->getOption( 'rcenhancedfilters-disable' );
+ } else {
+ return $user->getOption( 'rcenhancedfilters' );
+ }
+ }
+
+ /**
+ * Get the default value of the number of changes to display when loading
+ * the result set.
+ *
+ * @since 1.30
+ * @return int
+ */
+ public function getDefaultLimit() {
+ return $this->getUser()->getIntOption( static::$limitPreferenceName );
+ }
/**
* Get the default value of the number of days to display when loading
* the result set.
* Supports fractional values, and should be cast to a float.
*
+ * @since 1.30
* @return float
*/
- abstract function getDefaultDays();
+ public function getDefaultDays() {
+ return floatval( $this->getUser()->getOption( static::$daysPreferenceName ) );
+ }
}
diff --git a/www/wiki/includes/specialpage/FormSpecialPage.php b/www/wiki/includes/specialpage/FormSpecialPage.php
index 66c7d47e..81a0036e 100644
--- a/www/wiki/includes/specialpage/FormSpecialPage.php
+++ b/www/wiki/includes/specialpage/FormSpecialPage.php
@@ -36,6 +36,12 @@ abstract class FormSpecialPage extends SpecialPage {
protected $par = null;
/**
+ * @var array|null POST data preserved across re-authentication
+ * @since 1.32
+ */
+ protected $reauthPostData = null;
+
+ /**
* Get an HTMLForm descriptor array
* @return array
*/
@@ -89,13 +95,31 @@ abstract class FormSpecialPage extends SpecialPage {
* @return HTMLForm|null
*/
protected function getForm() {
+ $context = $this->getContext();
+ $onSubmit = [ $this, 'onSubmit' ];
+
+ if ( $this->reauthPostData ) {
+ // Restore POST data
+ $context = new DerivativeContext( $context );
+ $oldRequest = $this->getRequest();
+ $context->setRequest( new DerivativeRequest(
+ $oldRequest, $this->reauthPostData + $oldRequest->getQueryValues(), true
+ ) );
+
+ // But don't treat it as a "real" submission just in case of some
+ // crazy kind of CSRF.
+ $onSubmit = function () {
+ return false;
+ };
+ }
+
$form = HTMLForm::factory(
$this->getDisplayFormat(),
$this->getFormFields(),
- $this->getContext(),
+ $context,
$this->getMessagePrefix()
);
- $form->setSubmitCallback( [ $this, 'onSubmit' ] );
+ $form->setSubmitCallback( $onSubmit );
if ( $this->getDisplayFormat() !== 'ooui' ) {
// No legend and wrapper by default in OOUI forms, but can be set manually
// from alterForm()
@@ -151,6 +175,11 @@ abstract class FormSpecialPage extends SpecialPage {
// This will throw exceptions if there's a problem
$this->checkExecutePermissions( $this->getUser() );
+ $securityLevel = $this->getLoginSecurityLevel();
+ if ( $securityLevel !== false && !$this->checkLoginSecurityLevel( $securityLevel ) ) {
+ return;
+ }
+
$form = $this->getForm();
if ( $form->show() ) {
$this->onSuccess();
@@ -199,4 +228,14 @@ abstract class FormSpecialPage extends SpecialPage {
public function requiresUnblock() {
return true;
}
+
+ /**
+ * Preserve POST data across reauthentication
+ *
+ * @since 1.32
+ * @param array $data
+ */
+ protected function setReauthPostData( array $data ) {
+ $this->reauthPostData = $data;
+ }
}
diff --git a/www/wiki/includes/specialpage/ImageQueryPage.php b/www/wiki/includes/specialpage/ImageQueryPage.php
index 59abefd8..49aaffd0 100644
--- a/www/wiki/includes/specialpage/ImageQueryPage.php
+++ b/www/wiki/includes/specialpage/ImageQueryPage.php
@@ -21,7 +21,7 @@
* @ingroup SpecialPage
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -39,7 +39,7 @@ abstract class ImageQueryPage extends QueryPage {
* @param OutputPage $out OutputPage to print to
* @param Skin $skin User skin to use [unused]
* @param IDatabase $dbr (read) connection to use
- * @param ResultWrapper $res Result pointer
+ * @param IResultWrapper $res Result pointer
* @param int $num Number of available result rows
* @param int $offset Paging offset
*/
diff --git a/www/wiki/includes/specialpage/LoginSignupSpecialPage.php b/www/wiki/includes/specialpage/LoginSignupSpecialPage.php
index 04d391b3..1c54d13d 100644
--- a/www/wiki/includes/specialpage/LoginSignupSpecialPage.php
+++ b/www/wiki/includes/specialpage/LoginSignupSpecialPage.php
@@ -478,7 +478,6 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
/**
* Replace some globals to make sure the fact that the user has just been logged in is
* reflected in the current request.
- * @param User $user
*/
protected function setSessionUserForCurrentRequest() {
global $wgUser, $wgLang;
@@ -516,7 +515,6 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
* @private
*/
protected function mainLoginForm( array $requests, $msg = '', $msgtype = 'error' ) {
- $titleObj = $this->getPageTitle();
$user = $this->getUser();
$out = $this->getOutput();
@@ -1182,7 +1180,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
],
$this->msg(
$loggedIn ? 'userlogin-createanother' : 'userlogin-joinproject'
- )->escaped()
+ )->text()
)
);
},
diff --git a/www/wiki/includes/specialpage/PageQueryPage.php b/www/wiki/includes/specialpage/PageQueryPage.php
index f7f04993..7d6db054 100644
--- a/www/wiki/includes/specialpage/PageQueryPage.php
+++ b/www/wiki/includes/specialpage/PageQueryPage.php
@@ -21,7 +21,7 @@
* @ingroup SpecialPage
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -36,7 +36,7 @@ abstract class PageQueryPage extends QueryPage {
* This should be done for live data and cached data.
*
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
public function preprocessResults( $db, $res ) {
$this->executeLBFromResultWrapper( $res );
diff --git a/www/wiki/includes/specialpage/QueryPage.php b/www/wiki/includes/specialpage/QueryPage.php
index 73b81289..f642106a 100644
--- a/www/wiki/includes/specialpage/QueryPage.php
+++ b/www/wiki/includes/specialpage/QueryPage.php
@@ -21,7 +21,7 @@
* @ingroup SpecialPage
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\DBError;
@@ -69,40 +69,40 @@ abstract class QueryPage extends SpecialPage {
if ( $qp === null ) {
// QueryPage subclass, Special page name
$qp = [
- [ 'AncientPagesPage', 'Ancientpages' ],
- [ 'BrokenRedirectsPage', 'BrokenRedirects' ],
- [ 'DeadendPagesPage', 'Deadendpages' ],
- [ 'DoubleRedirectsPage', 'DoubleRedirects' ],
- [ 'FileDuplicateSearchPage', 'FileDuplicateSearch' ],
- [ 'ListDuplicatedFilesPage', 'ListDuplicatedFiles' ],
- [ 'LinkSearchPage', 'LinkSearch' ],
- [ 'ListredirectsPage', 'Listredirects' ],
- [ 'LonelyPagesPage', 'Lonelypages' ],
- [ 'LongPagesPage', 'Longpages' ],
- [ 'MediaStatisticsPage', 'MediaStatistics' ],
- [ 'MIMEsearchPage', 'MIMEsearch' ],
- [ 'MostcategoriesPage', 'Mostcategories' ],
- [ 'MostimagesPage', 'Mostimages' ],
- [ 'MostinterwikisPage', 'Mostinterwikis' ],
- [ 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ],
- [ 'MostlinkedTemplatesPage', 'Mostlinkedtemplates' ],
- [ 'MostlinkedPage', 'Mostlinked' ],
- [ 'MostrevisionsPage', 'Mostrevisions' ],
- [ 'FewestrevisionsPage', 'Fewestrevisions' ],
- [ 'ShortPagesPage', 'Shortpages' ],
- [ 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ],
- [ 'UncategorizedPagesPage', 'Uncategorizedpages' ],
- [ 'UncategorizedImagesPage', 'Uncategorizedimages' ],
- [ 'UncategorizedTemplatesPage', 'Uncategorizedtemplates' ],
- [ 'UnusedCategoriesPage', 'Unusedcategories' ],
- [ 'UnusedimagesPage', 'Unusedimages' ],
- [ 'WantedCategoriesPage', 'Wantedcategories' ],
- [ 'WantedFilesPage', 'Wantedfiles' ],
- [ 'WantedPagesPage', 'Wantedpages' ],
- [ 'WantedTemplatesPage', 'Wantedtemplates' ],
- [ 'UnwatchedpagesPage', 'Unwatchedpages' ],
- [ 'UnusedtemplatesPage', 'Unusedtemplates' ],
- [ 'WithoutInterwikiPage', 'Withoutinterwiki' ],
+ [ AncientPagesPage::class, 'Ancientpages' ],
+ [ BrokenRedirectsPage::class, 'BrokenRedirects' ],
+ [ DeadendPagesPage::class, 'Deadendpages' ],
+ [ DoubleRedirectsPage::class, 'DoubleRedirects' ],
+ [ FileDuplicateSearchPage::class, 'FileDuplicateSearch' ],
+ [ ListDuplicatedFilesPage::class, 'ListDuplicatedFiles' ],
+ [ LinkSearchPage::class, 'LinkSearch' ],
+ [ ListredirectsPage::class, 'Listredirects' ],
+ [ LonelyPagesPage::class, 'Lonelypages' ],
+ [ LongPagesPage::class, 'Longpages' ],
+ [ MediaStatisticsPage::class, 'MediaStatistics' ],
+ [ MIMEsearchPage::class, 'MIMEsearch' ],
+ [ MostcategoriesPage::class, 'Mostcategories' ],
+ [ MostimagesPage::class, 'Mostimages' ],
+ [ MostinterwikisPage::class, 'Mostinterwikis' ],
+ [ MostlinkedCategoriesPage::class, 'Mostlinkedcategories' ],
+ [ MostlinkedTemplatesPage::class, 'Mostlinkedtemplates' ],
+ [ MostlinkedPage::class, 'Mostlinked' ],
+ [ MostrevisionsPage::class, 'Mostrevisions' ],
+ [ FewestrevisionsPage::class, 'Fewestrevisions' ],
+ [ ShortPagesPage::class, 'Shortpages' ],
+ [ UncategorizedCategoriesPage::class, 'Uncategorizedcategories' ],
+ [ UncategorizedPagesPage::class, 'Uncategorizedpages' ],
+ [ UncategorizedImagesPage::class, 'Uncategorizedimages' ],
+ [ UncategorizedTemplatesPage::class, 'Uncategorizedtemplates' ],
+ [ UnusedCategoriesPage::class, 'Unusedcategories' ],
+ [ UnusedimagesPage::class, 'Unusedimages' ],
+ [ WantedCategoriesPage::class, 'Wantedcategories' ],
+ [ WantedFilesPage::class, 'Wantedfiles' ],
+ [ WantedPagesPage::class, 'Wantedpages' ],
+ [ WantedTemplatesPage::class, 'Wantedtemplates' ],
+ [ UnwatchedpagesPage::class, 'Unwatchedpages' ],
+ [ UnusedtemplatesPage::class, 'Unusedtemplates' ],
+ [ WithoutInterwikiPage::class, 'Withoutinterwiki' ],
];
Hooks::run( 'wgQueryPages', [ &$qp ] );
}
@@ -387,7 +387,7 @@ abstract class QueryPage extends SpecialPage {
* Run the query and return the result
* @param int|bool $limit Numerical limit or false for no limit
* @param int|bool $offset Numerical offset or false for no offset
- * @return ResultWrapper
+ * @return IResultWrapper
* @since 1.18
*/
public function reallyDoQuery( $limit, $offset = false ) {
@@ -439,7 +439,7 @@ abstract class QueryPage extends SpecialPage {
* Somewhat deprecated, you probably want to be using execute()
* @param int|bool $offset
* @param int|bool $limit
- * @return ResultWrapper
+ * @return IResultWrapper
*/
public function doQuery( $offset = false, $limit = false ) {
if ( $this->isCached() && $this->isCacheable() ) {
@@ -453,7 +453,7 @@ abstract class QueryPage extends SpecialPage {
* Fetch the query results from the query cache
* @param int|bool $limit Numerical limit or false for no limit
* @param int|bool $offset Numerical offset or false for no offset
- * @return ResultWrapper
+ * @return IResultWrapper
* @since 1.18
*/
public function fetchFromCache( $limit, $offset = false ) {
@@ -685,7 +685,7 @@ abstract class QueryPage extends SpecialPage {
* @param OutputPage $out OutputPage to print to
* @param Skin $skin User skin to use
* @param IDatabase $dbr Database (read) connection to use
- * @param ResultWrapper $res Result pointer
+ * @param IResultWrapper $res Result pointer
* @param int $num Number of available result rows
* @param int $offset Paging offset
*/
@@ -700,9 +700,8 @@ abstract class QueryPage extends SpecialPage {
# $res might contain the whole 1,000 rows, so we read up to
# $num [should update this to use a Pager]
- // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
+ // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
- // @codingStandardsIgnoreEnd
$line = $this->formatResult( $skin, $row );
if ( $line ) {
$html[] = $this->listoutput
@@ -752,7 +751,7 @@ abstract class QueryPage extends SpecialPage {
/**
* Do any necessary preprocessing of the result object.
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function preprocessResults( $db, $res ) {
}
@@ -854,12 +853,12 @@ abstract class QueryPage extends SpecialPage {
* title and optional the namespace field) and executes the batch. This operation will pre-cache
* LinkCache information like page existence and information for stub color and redirect hints.
*
- * @param ResultWrapper $res The ResultWrapper object to process. Needs to include the title
+ * @param IResultWrapper $res The ResultWrapper object to process. Needs to include the title
* field and namespace field, if the $ns parameter isn't set.
* @param null $ns Use this namespace for the given titles in the ResultWrapper object,
* instead of the namespace value of $res.
*/
- protected function executeLBFromResultWrapper( ResultWrapper $res, $ns = null ) {
+ protected function executeLBFromResultWrapper( IResultWrapper $res, $ns = null ) {
if ( !$res->numRows() ) {
return;
}
diff --git a/www/wiki/includes/specialpage/SpecialPage.php b/www/wiki/includes/specialpage/SpecialPage.php
index 4c3ca54b..317aa0d7 100644
--- a/www/wiki/includes/specialpage/SpecialPage.php
+++ b/www/wiki/includes/specialpage/SpecialPage.php
@@ -353,6 +353,23 @@ class SpecialPage implements MessageLocalizer {
}
/**
+ * Record preserved POST data after a reauthentication.
+ *
+ * This is called from checkLoginSecurityLevel() when returning from the
+ * redirect for reauthentication, if the redirect had been served in
+ * response to a POST request.
+ *
+ * The base SpecialPage implementation does nothing. If your subclass uses
+ * getLoginSecurityLevel() or checkLoginSecurityLevel(), it should probably
+ * implement this to do something with the data.
+ *
+ * @since 1.32
+ * @param array $data
+ */
+ protected function setReauthPostData( array $data ) {
+ }
+
+ /**
* Verifies that the user meets the security level, possibly reauthenticating them in the process.
*
* This should be used when the page does something security-sensitive and needs extra defense
@@ -378,16 +395,42 @@ class SpecialPage implements MessageLocalizer {
*/
protected function checkLoginSecurityLevel( $level = null ) {
$level = $level ?: $this->getName();
+ $key = 'SpecialPage:reauth:' . $this->getName();
+ $request = $this->getRequest();
+
$securityStatus = AuthManager::singleton()->securitySensitiveOperationStatus( $level );
if ( $securityStatus === AuthManager::SEC_OK ) {
+ $uniqueId = $request->getVal( 'postUniqueId' );
+ if ( $uniqueId ) {
+ $key = $key . ':' . $uniqueId;
+ $session = $request->getSession();
+ $data = $session->getSecret( $key );
+ if ( $data ) {
+ $session->remove( $key );
+ $this->setReauthPostData( $data );
+ }
+ }
return true;
} elseif ( $securityStatus === AuthManager::SEC_REAUTH ) {
- $request = $this->getRequest();
$title = self::getTitleFor( 'Userlogin' );
+ $queryParams = $request->getQueryValues();
+
+ if ( $request->wasPosted() ) {
+ $data = array_diff_assoc( $request->getValues(), $request->getQueryValues() );
+ if ( $data ) {
+ // unique ID in case the same special page is open in multiple browser tabs
+ $uniqueId = MWCryptRand::generateHex( 6 );
+ $key = $key . ':' . $uniqueId;
+ $queryParams['postUniqueId'] = $uniqueId;
+ $session = $request->getSession();
+ $session->persist(); // Just in case
+ $session->setSecret( $key, $data );
+ }
+ }
+
$query = [
'returnto' => $this->getFullTitle()->getPrefixedDBkey(),
- 'returntoquery' => wfArrayToCgi( array_diff_key( $request->getQueryValues(),
- [ 'title' => true ] ) ),
+ 'returntoquery' => wfArrayToCgi( array_diff_key( $queryParams, [ 'title' => true ] ) ),
'force' => $level,
];
$url = $title->getFullURL( $query, false, PROTO_HTTPS );
@@ -568,7 +611,10 @@ class SpecialPage implements MessageLocalizer {
public function execute( $subPage ) {
$this->setHeaders();
$this->checkPermissions();
- $this->checkLoginSecurityLevel( $this->getLoginSecurityLevel() );
+ $securityLevel = $this->getLoginSecurityLevel();
+ if ( $securityLevel !== false && !$this->checkLoginSecurityLevel( $securityLevel ) ) {
+ return;
+ }
$this->outputHeader();
}
@@ -615,6 +661,7 @@ class SpecialPage implements MessageLocalizer {
* @deprecated since 1.23, use SpecialPage::getPageTitle
*/
function getTitle( $subpage = false ) {
+ wfDeprecated( __METHOD__, '1.23' );
return $this->getPageTitle( $subpage );
}
@@ -809,7 +856,7 @@ class SpecialPage implements MessageLocalizer {
public function getFinalGroupName() {
$name = $this->getName();
- // Allow overbidding the group from the wiki side
+ // Allow overriding the group from the wiki side
$msg = $this->msg( 'specialpages-specialpagegroup-' . strtolower( $name ) )->inContentLanguage();
if ( !$msg->isBlank() ) {
$group = $msg->text();
diff --git a/www/wiki/includes/specialpage/SpecialPageFactory.php b/www/wiki/includes/specialpage/SpecialPageFactory.php
index 4433ddb7..fdf4d52d 100644
--- a/www/wiki/includes/specialpage/SpecialPageFactory.php
+++ b/www/wiki/includes/specialpage/SpecialPageFactory.php
@@ -22,6 +22,7 @@
* @defgroup SpecialPage SpecialPage
*/
use MediaWiki\Linker\LinkRenderer;
+use Wikimedia\ObjectFactory;
/**
* Factory for handling the special page list and generating SpecialPage objects.
@@ -50,143 +51,143 @@ class SpecialPageFactory {
*/
private static $coreList = [
// Maintenance Reports
- 'BrokenRedirects' => 'BrokenRedirectsPage',
- 'Deadendpages' => 'DeadendPagesPage',
- 'DoubleRedirects' => 'DoubleRedirectsPage',
- 'Longpages' => 'LongPagesPage',
- 'Ancientpages' => 'AncientPagesPage',
- 'Lonelypages' => 'LonelyPagesPage',
- 'Fewestrevisions' => 'FewestrevisionsPage',
- 'Withoutinterwiki' => 'WithoutInterwikiPage',
- 'Protectedpages' => 'SpecialProtectedpages',
- 'Protectedtitles' => 'SpecialProtectedtitles',
- 'Shortpages' => 'ShortPagesPage',
- 'Uncategorizedcategories' => 'UncategorizedCategoriesPage',
- 'Uncategorizedimages' => 'UncategorizedImagesPage',
- 'Uncategorizedpages' => 'UncategorizedPagesPage',
- 'Uncategorizedtemplates' => 'UncategorizedTemplatesPage',
- 'Unusedcategories' => 'UnusedCategoriesPage',
- 'Unusedimages' => 'UnusedimagesPage',
- 'Unusedtemplates' => 'UnusedtemplatesPage',
- 'Unwatchedpages' => 'UnwatchedpagesPage',
- 'Wantedcategories' => 'WantedCategoriesPage',
- 'Wantedfiles' => 'WantedFilesPage',
- 'Wantedpages' => 'WantedPagesPage',
- 'Wantedtemplates' => 'WantedTemplatesPage',
+ 'BrokenRedirects' => BrokenRedirectsPage::class,
+ 'Deadendpages' => DeadendPagesPage::class,
+ 'DoubleRedirects' => DoubleRedirectsPage::class,
+ 'Longpages' => LongPagesPage::class,
+ 'Ancientpages' => AncientPagesPage::class,
+ 'Lonelypages' => LonelyPagesPage::class,
+ 'Fewestrevisions' => FewestrevisionsPage::class,
+ 'Withoutinterwiki' => WithoutInterwikiPage::class,
+ 'Protectedpages' => SpecialProtectedpages::class,
+ 'Protectedtitles' => SpecialProtectedtitles::class,
+ 'Shortpages' => ShortPagesPage::class,
+ 'Uncategorizedcategories' => UncategorizedCategoriesPage::class,
+ 'Uncategorizedimages' => UncategorizedImagesPage::class,
+ 'Uncategorizedpages' => UncategorizedPagesPage::class,
+ 'Uncategorizedtemplates' => UncategorizedTemplatesPage::class,
+ 'Unusedcategories' => UnusedCategoriesPage::class,
+ 'Unusedimages' => UnusedimagesPage::class,
+ 'Unusedtemplates' => UnusedtemplatesPage::class,
+ 'Unwatchedpages' => UnwatchedpagesPage::class,
+ 'Wantedcategories' => WantedCategoriesPage::class,
+ 'Wantedfiles' => WantedFilesPage::class,
+ 'Wantedpages' => WantedPagesPage::class,
+ 'Wantedtemplates' => WantedTemplatesPage::class,
// List of pages
- 'Allpages' => 'SpecialAllPages',
- 'Prefixindex' => 'SpecialPrefixindex',
- 'Categories' => 'SpecialCategories',
- 'Listredirects' => 'ListredirectsPage',
- 'PagesWithProp' => 'SpecialPagesWithProp',
- 'TrackingCategories' => 'SpecialTrackingCategories',
+ 'Allpages' => SpecialAllPages::class,
+ 'Prefixindex' => SpecialPrefixindex::class,
+ 'Categories' => SpecialCategories::class,
+ 'Listredirects' => ListredirectsPage::class,
+ 'PagesWithProp' => SpecialPagesWithProp::class,
+ 'TrackingCategories' => SpecialTrackingCategories::class,
// Authentication
- 'Userlogin' => 'SpecialUserLogin',
- 'Userlogout' => 'SpecialUserLogout',
- 'CreateAccount' => 'SpecialCreateAccount',
- 'LinkAccounts' => 'SpecialLinkAccounts',
- 'UnlinkAccounts' => 'SpecialUnlinkAccounts',
- 'ChangeCredentials' => 'SpecialChangeCredentials',
- 'RemoveCredentials' => 'SpecialRemoveCredentials',
+ 'Userlogin' => SpecialUserLogin::class,
+ 'Userlogout' => SpecialUserLogout::class,
+ 'CreateAccount' => SpecialCreateAccount::class,
+ 'LinkAccounts' => SpecialLinkAccounts::class,
+ 'UnlinkAccounts' => SpecialUnlinkAccounts::class,
+ 'ChangeCredentials' => SpecialChangeCredentials::class,
+ 'RemoveCredentials' => SpecialRemoveCredentials::class,
// Users and rights
- 'Activeusers' => 'SpecialActiveUsers',
- 'Block' => 'SpecialBlock',
- 'Unblock' => 'SpecialUnblock',
- 'BlockList' => 'SpecialBlockList',
- 'AutoblockList' => 'SpecialAutoblockList',
- 'ChangePassword' => 'SpecialChangePassword',
- 'BotPasswords' => 'SpecialBotPasswords',
- 'PasswordReset' => 'SpecialPasswordReset',
- 'DeletedContributions' => 'DeletedContributionsPage',
- 'Preferences' => 'SpecialPreferences',
- 'ResetTokens' => 'SpecialResetTokens',
- 'Contributions' => 'SpecialContributions',
- 'Listgrouprights' => 'SpecialListGroupRights',
- 'Listgrants' => 'SpecialListGrants',
- 'Listusers' => 'SpecialListUsers',
- 'Listadmins' => 'SpecialListAdmins',
- 'Listbots' => 'SpecialListBots',
- 'Userrights' => 'UserrightsPage',
- 'EditWatchlist' => 'SpecialEditWatchlist',
+ 'Activeusers' => SpecialActiveUsers::class,
+ 'Block' => SpecialBlock::class,
+ 'Unblock' => SpecialUnblock::class,
+ 'BlockList' => SpecialBlockList::class,
+ 'AutoblockList' => SpecialAutoblockList::class,
+ 'ChangePassword' => SpecialChangePassword::class,
+ 'BotPasswords' => SpecialBotPasswords::class,
+ 'PasswordReset' => SpecialPasswordReset::class,
+ 'DeletedContributions' => DeletedContributionsPage::class,
+ 'Preferences' => SpecialPreferences::class,
+ 'ResetTokens' => SpecialResetTokens::class,
+ 'Contributions' => SpecialContributions::class,
+ 'Listgrouprights' => SpecialListGroupRights::class,
+ 'Listgrants' => SpecialListGrants::class,
+ 'Listusers' => SpecialListUsers::class,
+ 'Listadmins' => SpecialListAdmins::class,
+ 'Listbots' => SpecialListBots::class,
+ 'Userrights' => UserrightsPage::class,
+ 'EditWatchlist' => SpecialEditWatchlist::class,
// Recent changes and logs
- 'Newimages' => 'SpecialNewFiles',
- 'Log' => 'SpecialLog',
- 'Watchlist' => 'SpecialWatchlist',
- 'Newpages' => 'SpecialNewpages',
- 'Recentchanges' => 'SpecialRecentChanges',
- 'Recentchangeslinked' => 'SpecialRecentChangesLinked',
- 'Tags' => 'SpecialTags',
+ 'Newimages' => SpecialNewFiles::class,
+ 'Log' => SpecialLog::class,
+ 'Watchlist' => SpecialWatchlist::class,
+ 'Newpages' => SpecialNewpages::class,
+ 'Recentchanges' => SpecialRecentChanges::class,
+ 'Recentchangeslinked' => SpecialRecentChangesLinked::class,
+ 'Tags' => SpecialTags::class,
// Media reports and uploads
- 'Listfiles' => 'SpecialListFiles',
- 'Filepath' => 'SpecialFilepath',
- 'MediaStatistics' => 'MediaStatisticsPage',
- 'MIMEsearch' => 'MIMEsearchPage',
- 'FileDuplicateSearch' => 'FileDuplicateSearchPage',
- 'Upload' => 'SpecialUpload',
- 'UploadStash' => 'SpecialUploadStash',
- 'ListDuplicatedFiles' => 'ListDuplicatedFilesPage',
+ 'Listfiles' => SpecialListFiles::class,
+ 'Filepath' => SpecialFilepath::class,
+ 'MediaStatistics' => MediaStatisticsPage::class,
+ 'MIMEsearch' => MIMEsearchPage::class,
+ 'FileDuplicateSearch' => FileDuplicateSearchPage::class,
+ 'Upload' => SpecialUpload::class,
+ 'UploadStash' => SpecialUploadStash::class,
+ 'ListDuplicatedFiles' => ListDuplicatedFilesPage::class,
// Data and tools
- 'ApiSandbox' => 'SpecialApiSandbox',
- 'Statistics' => 'SpecialStatistics',
- 'Allmessages' => 'SpecialAllMessages',
- 'Version' => 'SpecialVersion',
- 'Lockdb' => 'SpecialLockdb',
- 'Unlockdb' => 'SpecialUnlockdb',
+ 'ApiSandbox' => SpecialApiSandbox::class,
+ 'Statistics' => SpecialStatistics::class,
+ 'Allmessages' => SpecialAllMessages::class,
+ 'Version' => SpecialVersion::class,
+ 'Lockdb' => SpecialLockdb::class,
+ 'Unlockdb' => SpecialUnlockdb::class,
// Redirecting special pages
- 'LinkSearch' => 'LinkSearchPage',
- 'Randompage' => 'RandomPage',
- 'RandomInCategory' => 'SpecialRandomInCategory',
- 'Randomredirect' => 'SpecialRandomredirect',
- 'Randomrootpage' => 'SpecialRandomrootpage',
- 'GoToInterwiki' => 'SpecialGoToInterwiki',
+ 'LinkSearch' => LinkSearchPage::class,
+ 'Randompage' => RandomPage::class,
+ 'RandomInCategory' => SpecialRandomInCategory::class,
+ 'Randomredirect' => SpecialRandomredirect::class,
+ 'Randomrootpage' => SpecialRandomrootpage::class,
+ 'GoToInterwiki' => SpecialGoToInterwiki::class,
// High use pages
- 'Mostlinkedcategories' => 'MostlinkedCategoriesPage',
- 'Mostimages' => 'MostimagesPage',
- 'Mostinterwikis' => 'MostinterwikisPage',
- 'Mostlinked' => 'MostlinkedPage',
- 'Mostlinkedtemplates' => 'MostlinkedTemplatesPage',
- 'Mostcategories' => 'MostcategoriesPage',
- 'Mostrevisions' => 'MostrevisionsPage',
+ 'Mostlinkedcategories' => MostlinkedCategoriesPage::class,
+ 'Mostimages' => MostimagesPage::class,
+ 'Mostinterwikis' => MostinterwikisPage::class,
+ 'Mostlinked' => MostlinkedPage::class,
+ 'Mostlinkedtemplates' => MostlinkedTemplatesPage::class,
+ 'Mostcategories' => MostcategoriesPage::class,
+ 'Mostrevisions' => MostrevisionsPage::class,
// Page tools
- 'ComparePages' => 'SpecialComparePages',
- 'Export' => 'SpecialExport',
- 'Import' => 'SpecialImport',
- 'Undelete' => 'SpecialUndelete',
- 'Whatlinkshere' => 'SpecialWhatLinksHere',
- 'MergeHistory' => 'SpecialMergeHistory',
- 'ExpandTemplates' => 'SpecialExpandTemplates',
+ 'ComparePages' => SpecialComparePages::class,
+ 'Export' => SpecialExport::class,
+ 'Import' => SpecialImport::class,
+ 'Undelete' => SpecialUndelete::class,
+ 'Whatlinkshere' => SpecialWhatLinksHere::class,
+ 'MergeHistory' => SpecialMergeHistory::class,
+ 'ExpandTemplates' => SpecialExpandTemplates::class,
// Other
- 'Booksources' => 'SpecialBookSources',
+ 'Booksources' => SpecialBookSources::class,
// Unlisted / redirects
- 'ApiHelp' => 'SpecialApiHelp',
- 'Blankpage' => 'SpecialBlankpage',
- 'Diff' => 'SpecialDiff',
- 'EditTags' => 'SpecialEditTags',
- 'Emailuser' => 'SpecialEmailUser',
- 'Movepage' => 'MovePageForm',
- 'Mycontributions' => 'SpecialMycontributions',
- 'MyLanguage' => 'SpecialMyLanguage',
- 'Mypage' => 'SpecialMypage',
- 'Mytalk' => 'SpecialMytalk',
- 'Myuploads' => 'SpecialMyuploads',
- 'AllMyUploads' => 'SpecialAllMyUploads',
- 'PermanentLink' => 'SpecialPermanentLink',
- 'Redirect' => 'SpecialRedirect',
- 'Revisiondelete' => 'SpecialRevisionDelete',
- 'RunJobs' => 'SpecialRunJobs',
- 'Specialpages' => 'SpecialSpecialpages',
- 'PageData' => 'SpecialPageData'
+ 'ApiHelp' => SpecialApiHelp::class,
+ 'Blankpage' => SpecialBlankpage::class,
+ 'Diff' => SpecialDiff::class,
+ 'EditTags' => SpecialEditTags::class,
+ 'Emailuser' => SpecialEmailUser::class,
+ 'Movepage' => MovePageForm::class,
+ 'Mycontributions' => SpecialMycontributions::class,
+ 'MyLanguage' => SpecialMyLanguage::class,
+ 'Mypage' => SpecialMypage::class,
+ 'Mytalk' => SpecialMytalk::class,
+ 'Myuploads' => SpecialMyuploads::class,
+ 'AllMyUploads' => SpecialAllMyUploads::class,
+ 'PermanentLink' => SpecialPermanentLink::class,
+ 'Redirect' => SpecialRedirect::class,
+ 'Revisiondelete' => SpecialRevisionDelete::class,
+ 'RunJobs' => SpecialRunJobs::class,
+ 'Specialpages' => SpecialSpecialpages::class,
+ 'PageData' => SpecialPageData::class,
];
private static $list;
@@ -214,17 +215,6 @@ class SpecialPageFactory {
/**
* Get the special page list as an array
*
- * @deprecated since 1.24, use getNames() instead.
- * @return array
- */
- public static function getList() {
- wfDeprecated( __FUNCTION__, '1.24' );
- return self::getPageList();
- }
-
- /**
- * Get the special page list as an array
- *
* @return array
*/
private static function getPageList() {
@@ -237,27 +227,27 @@ class SpecialPageFactory {
self::$list = self::$coreList;
if ( !$wgDisableInternalSearch ) {
- self::$list['Search'] = 'SpecialSearch';
+ self::$list['Search'] = SpecialSearch::class;
}
if ( $wgEmailAuthentication ) {
- self::$list['Confirmemail'] = 'EmailConfirmation';
- self::$list['Invalidateemail'] = 'EmailInvalidation';
+ self::$list['Confirmemail'] = EmailConfirmation::class;
+ self::$list['Invalidateemail'] = EmailInvalidation::class;
}
if ( $wgEnableEmail ) {
- self::$list['ChangeEmail'] = 'SpecialChangeEmail';
+ self::$list['ChangeEmail'] = SpecialChangeEmail::class;
}
if ( $wgEnableJavaScriptTest ) {
- self::$list['JavaScriptTest'] = 'SpecialJavaScriptTest';
+ self::$list['JavaScriptTest'] = SpecialJavaScriptTest::class;
}
if ( $wgPageLanguageUseDB ) {
- self::$list['PageLanguage'] = 'SpecialPageLanguage';
+ self::$list['PageLanguage'] = SpecialPageLanguage::class;
}
if ( $wgContentHandlerUseDB ) {
- self::$list['ChangeContentModel'] = 'SpecialChangeContentModel';
+ self::$list['ChangeContentModel'] = SpecialChangeContentModel::class;
}
// Add extension special pages
diff --git a/www/wiki/includes/specialpage/WantedQueryPage.php b/www/wiki/includes/specialpage/WantedQueryPage.php
index 8b60387e..83ffe40a 100644
--- a/www/wiki/includes/specialpage/WantedQueryPage.php
+++ b/www/wiki/includes/specialpage/WantedQueryPage.php
@@ -21,7 +21,7 @@
* @ingroup SpecialPage
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -41,7 +41,7 @@ abstract class WantedQueryPage extends QueryPage {
/**
* Cache page existence for performance
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function preprocessResults( $db, $res ) {
$this->executeLBFromResultWrapper( $res );
diff --git a/www/wiki/includes/specials/SpecialApiSandbox.php b/www/wiki/includes/specials/SpecialApiSandbox.php
index e9943477..2733e757 100644
--- a/www/wiki/includes/specials/SpecialApiSandbox.php
+++ b/www/wiki/includes/specials/SpecialApiSandbox.php
@@ -33,6 +33,7 @@ class SpecialApiSandbox extends SpecialPage {
public function execute( $par ) {
$this->setHeaders();
$out = $this->getOutput();
+ $this->addHelpLink( 'Help:ApiSandbox' );
if ( !$this->getConfig()->get( 'EnableAPI' ) ) {
$out->showErrorPage( 'error', 'apisandbox-api-disabled' );
diff --git a/www/wiki/includes/specials/SpecialBlock.php b/www/wiki/includes/specials/SpecialBlock.php
index cd3c0289..23691b25 100644
--- a/www/wiki/includes/specials/SpecialBlock.php
+++ b/www/wiki/includes/specials/SpecialBlock.php
@@ -99,7 +99,6 @@ class SpecialBlock extends FormSpecialPage {
* @param HTMLForm $form
*/
protected function alterForm( HTMLForm $form ) {
- $form->setWrapperLegendMsg( 'blockip-legend' );
$form->setHeaderText( '' );
$form->setSubmitDestructive();
@@ -121,6 +120,10 @@ class SpecialBlock extends FormSpecialPage {
}
}
+ protected function getDisplayFormat() {
+ return 'ooui';
+ }
+
/**
* Get the HTMLForm descriptor array for the block form
* @return array
@@ -132,16 +135,20 @@ class SpecialBlock extends FormSpecialPage {
$suggestedDurations = self::getSuggestedDurations();
+ $conf = $this->getConfig();
+ $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
+
$a = [
'Target' => [
- 'type' => 'text',
+ 'type' => 'user',
+ 'ipallowed' => true,
+ 'iprange' => true,
'label-message' => 'ipaddressorusername',
'id' => 'mw-bi-target',
'size' => '45',
'autofocus' => true,
'required' => true,
'validation-callback' => [ __CLASS__, 'validateTargetField' ],
- 'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
],
'Expiry' => [
'type' => !count( $suggestedDurations ) ? 'text' : 'selectorother',
@@ -153,7 +160,11 @@ class SpecialBlock extends FormSpecialPage {
],
'Reason' => [
'type' => 'selectandother',
- 'maxlength' => 255,
+ // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
+ // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
+ // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+ 'maxlength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
+ 'maxlength-unit' => 'codepoints',
'label-message' => 'ipbreason',
'options-message' => 'ipbreason-dropdown',
],
@@ -220,6 +231,7 @@ class SpecialBlock extends FormSpecialPage {
'type' => 'hidden',
'default' => '',
'label-message' => 'ipb-confirm',
+ 'cssclass' => 'mw-block-confirm',
];
$this->maybeAlterFormDefaults( $a );
@@ -323,7 +335,7 @@ class SpecialBlock extends FormSpecialPage {
* @return string
*/
protected function preText() {
- $this->getOutput()->addModules( [ 'mediawiki.special.block', 'mediawiki.userSuggest' ] );
+ $this->getOutput()->addModules( [ 'mediawiki.special.block' ] );
$blockCIDRLimit = $this->getConfig()->get( 'BlockCIDRLimit' );
$text = $this->msg( 'blockiptext', $blockCIDRLimit['IPv4'], $blockCIDRLimit['IPv6'] )->parse();
diff --git a/www/wiki/includes/specials/SpecialBotPasswords.php b/www/wiki/includes/specials/SpecialBotPasswords.php
index 056ce657..961ee1c5 100644
--- a/www/wiki/includes/specials/SpecialBotPasswords.php
+++ b/www/wiki/includes/specials/SpecialBotPasswords.php
@@ -153,7 +153,7 @@ class SpecialBotPasswords extends FormSpecialPage {
];
$fields['restrictions'] = [
- 'class' => 'HTMLRestrictionsField',
+ 'class' => HTMLRestrictionsField::class,
'required' => true,
'default' => $this->botPassword->getRestrictions(),
];
diff --git a/www/wiki/includes/specials/SpecialBrokenRedirects.php b/www/wiki/includes/specials/SpecialBrokenRedirects.php
index cf9ae071..3e1909b8 100644
--- a/www/wiki/includes/specials/SpecialBrokenRedirects.php
+++ b/www/wiki/includes/specials/SpecialBrokenRedirects.php
@@ -21,7 +21,7 @@
* @ingroup SpecialPage
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -167,7 +167,7 @@ class BrokenRedirectsPage extends QueryPage {
* Cache page content model for performance
*
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function preprocessResults( $db, $res ) {
$this->executeLBFromResultWrapper( $res );
diff --git a/www/wiki/includes/specials/SpecialChangeEmail.php b/www/wiki/includes/specials/SpecialChangeEmail.php
index 3d24832b..05f8022f 100644
--- a/www/wiki/includes/specials/SpecialChangeEmail.php
+++ b/www/wiki/includes/specials/SpecialChangeEmail.php
@@ -55,14 +55,16 @@ class SpecialChangeEmail extends FormSpecialPage {
* @param string $par
*/
function execute( $par ) {
- $this->checkLoginSecurityLevel();
-
$out = $this->getOutput();
$out->disallowUserJs();
parent::execute( $par );
}
+ protected function getLoginSecurityLevel() {
+ return $this->getName();
+ }
+
protected function checkExecutePermissions( User $user ) {
if ( !AuthManager::singleton()->allowsPropertyChange( 'emailaddress' ) ) {
throw new ErrorPageError( 'changeemail', 'cannotchangeemail' );
@@ -76,6 +78,10 @@ class SpecialChangeEmail extends FormSpecialPage {
throw new PermissionsError( 'viewmyprivateinfo' );
}
+ if ( $user->isBlockedFromEmailuser() ) {
+ throw new UserBlockedError( $user->getBlock() );
+ }
+
parent::checkExecutePermissions( $user );
}
@@ -162,6 +168,12 @@ class SpecialChangeEmail extends FormSpecialPage {
return Status::newFatal( 'changeemail-nochange' );
}
+ // To prevent spam, rate limit adding a new address, but do
+ // not rate limit removing an address.
+ if ( $newaddr !== '' && $user->pingLimiter( 'changeemail' ) ) {
+ return Status::newFatal( 'actionthrottledtext' );
+ }
+
$oldaddr = $user->getEmail();
$status = $user->setEmailWithConfirmation( $newaddr );
if ( !$status->isGood() ) {
diff --git a/www/wiki/includes/specials/SpecialContributions.php b/www/wiki/includes/specials/SpecialContributions.php
index 5a5f005b..6fc8306a 100644
--- a/www/wiki/includes/specials/SpecialContributions.php
+++ b/www/wiki/includes/specials/SpecialContributions.php
@@ -21,6 +21,7 @@
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
use MediaWiki\Widget\DateInputWidget;
/**
@@ -39,14 +40,12 @@ class SpecialContributions extends IncludableSpecialPage {
$this->setHeaders();
$this->outputHeader();
$out = $this->getOutput();
+ // Modules required for viewing the list of contributions (also when included on other pages)
$out->addModuleStyles( [
'mediawiki.special',
'mediawiki.special.changeslist',
- 'mediawiki.widgets.DateInputWidget.styles',
] );
- $out->addModules( 'mediawiki.special.contributions' );
$this->addHelpLink( 'Help:User contributions' );
- $out->enableOOUI();
$this->opts = [];
$request = $this->getRequest();
@@ -82,21 +81,39 @@ class SpecialContributions extends IncludableSpecialPage {
$this->opts['newOnly'] = $request->getBool( 'newOnly' );
$this->opts['hideMinor'] = $request->getBool( 'hideMinor' );
- $nt = Title::makeTitleSafe( NS_USER, $target );
- if ( !$nt ) {
- $out->addHTML( $this->getForm() );
-
- return;
- }
- $userObj = User::newFromName( $nt->getText(), false );
- if ( !$userObj ) {
- $out->addHTML( $this->getForm() );
+ $id = 0;
+ if ( $this->opts['contribs'] === 'newbie' ) {
+ $userObj = User::newFromName( $target ); // hysterical raisins
+ $out->addSubtitle( $this->msg( 'sp-contributions-newbies-sub' ) );
+ $out->setHTMLTitle( $this->msg(
+ 'pagetitle',
+ $this->msg( 'sp-contributions-newbies-title' )->plain()
+ )->inContentLanguage() );
+ } elseif ( ExternalUserNames::isExternal( $target ) ) {
+ $userObj = User::newFromName( $target, false );
+ if ( !$userObj ) {
+ $out->addHTML( $this->getForm() );
+ return;
+ }
- return;
- }
- $id = $userObj->getId();
+ $out->addSubtitle( $this->contributionsSub( $userObj ) );
+ $out->setHTMLTitle( $this->msg(
+ 'pagetitle',
+ $this->msg( 'contributions-title', $target )->plain()
+ )->inContentLanguage() );
+ } else {
+ $nt = Title::makeTitleSafe( NS_USER, $target );
+ if ( !$nt ) {
+ $out->addHTML( $this->getForm() );
+ return;
+ }
+ $userObj = User::newFromName( $nt->getText(), false );
+ if ( !$userObj ) {
+ $out->addHTML( $this->getForm() );
+ return;
+ }
+ $id = $userObj->getId();
- if ( $this->opts['contribs'] != 'newbie' ) {
$target = $nt->getText();
$out->addSubtitle( $this->contributionsSub( $userObj ) );
$out->setHTMLTitle( $this->msg(
@@ -109,12 +126,6 @@ class SpecialContributions extends IncludableSpecialPage {
if ( !IP::isValidRange( $target ) ) {
$this->getSkin()->setRelevantUser( $userObj );
}
- } else {
- $out->addSubtitle( $this->msg( 'sp-contributions-newbies-sub' ) );
- $out->setHTMLTitle( $this->msg(
- 'pagetitle',
- $this->msg( 'sp-contributions-newbies-title' )->plain()
- )->inContentLanguage() );
}
$ns = $request->getVal( 'namespace', null );
@@ -220,7 +231,8 @@ class SpecialContributions extends IncludableSpecialPage {
$out->addWikiMsg( 'nocontribs', $target );
} else {
# Show a message about replica DB lag, if applicable
- $lag = wfGetLB()->safeGetLag( $pager->getDatabase() );
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ $lag = $lb->safeGetLag( $pager->getDatabase() );
if ( $lag > 0 ) {
$out->showLagWarning( $lag );
}
@@ -495,6 +507,14 @@ class SpecialContributions extends IncludableSpecialPage {
$this->opts['hideMinor'] = false;
}
+ // Modules required only for the form
+ $this->getOutput()->addModules( [
+ 'mediawiki.userSuggest',
+ 'mediawiki.special.contributions',
+ ] );
+ $this->getOutput()->addModuleStyles( 'mediawiki.widgets.DateInputWidget.styles' );
+ $this->getOutput()->enableOOUI();
+
$form = Html::openElement(
'form',
[
@@ -542,8 +562,6 @@ class SpecialContributions extends IncludableSpecialPage {
$filterSelection = Html::rawElement( 'div', [], '' );
}
- $this->getOutput()->addModules( 'mediawiki.userSuggest' );
-
$labelNewbies = Xml::radioLabel(
$this->msg( 'sp-contributions-newbies' )->text(),
'contribs',
diff --git a/www/wiki/includes/specials/SpecialDeletedContributions.php b/www/wiki/includes/specials/SpecialDeletedContributions.php
index 5c8b3a62..975d64e3 100644
--- a/www/wiki/includes/specials/SpecialDeletedContributions.php
+++ b/www/wiki/includes/specials/SpecialDeletedContributions.php
@@ -21,6 +21,8 @@
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Implements Special:DeletedContributions to display archived revisions
* @ingroup SpecialPage
@@ -97,7 +99,8 @@ class DeletedContributionsPage extends SpecialPage {
}
# Show a message about replica DB lag, if applicable
- $lag = wfGetLB()->safeGetLag( $pager->getDatabase() );
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ $lag = $lb->safeGetLag( $pager->getDatabase() );
if ( $lag > 0 ) {
$out->showLagWarning( $lag );
}
diff --git a/www/wiki/includes/specials/SpecialDiff.php b/www/wiki/includes/specials/SpecialDiff.php
index 28cd0d19..b27a8b4d 100644
--- a/www/wiki/includes/specials/SpecialDiff.php
+++ b/www/wiki/includes/specials/SpecialDiff.php
@@ -78,7 +78,7 @@ class SpecialDiff extends RedirectSpecialPage {
],
'diff' => [
'name' => 'diff',
- 'class' => 'HTMLTextField',
+ 'class' => HTMLTextField::class,
'label-message' => 'diff-form-revid',
],
], $this->getContext(), 'diff-form' );
diff --git a/www/wiki/includes/specials/SpecialDoubleRedirects.php b/www/wiki/includes/specials/SpecialDoubleRedirects.php
index d73ac198..77c59f03 100644
--- a/www/wiki/includes/specials/SpecialDoubleRedirects.php
+++ b/www/wiki/includes/specials/SpecialDoubleRedirects.php
@@ -21,7 +21,7 @@
* @ingroup SpecialPage
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -202,7 +202,7 @@ class DoubleRedirectsPage extends QueryPage {
* Cache page content model and gender distinction for performance
*
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function preprocessResults( $db, $res ) {
if ( !$res->numRows() ) {
diff --git a/www/wiki/includes/specials/SpecialEditTags.php b/www/wiki/includes/specials/SpecialEditTags.php
index 476c452a..d11cf64c 100644
--- a/www/wiki/includes/specials/SpecialEditTags.php
+++ b/www/wiki/includes/specials/SpecialEditTags.php
@@ -222,10 +222,12 @@ class SpecialEditTags extends UnlistedSpecialPage {
$numRevisions = 0;
// Live revisions...
$list = $this->getList();
- // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
+ // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
for ( $list->reset(); $list->current(); $list->next() ) {
- // @codingStandardsIgnoreEnd
$item = $list->current();
+ if ( !$item->canView() ) {
+ throw new ErrorPageError( 'permissionserrors', 'tags-update-no-permission' );
+ }
$numRevisions++;
$out->addHTML( $item->getHTML() );
}
@@ -240,6 +242,9 @@ class SpecialEditTags extends UnlistedSpecialPage {
// Show form if the user can submit
if ( $this->isAllowed ) {
+ $conf = $this->getConfig();
+ $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
+
$form = Xml::openElement( 'form', [ 'method' => 'post',
'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ),
'id' => 'mw-revdel-form-revisions' ] ) .
@@ -252,12 +257,14 @@ class SpecialEditTags extends UnlistedSpecialPage {
Xml::label( $this->msg( 'tags-edit-reason' )->text(), 'wpReason' ) .
'</td>' .
'<td class="mw-input">' .
- Xml::input(
- 'wpReason',
- 60,
- $this->reason,
- [ 'id' => 'wpReason', 'maxlength' => 100 ]
- ) .
+ Xml::input( 'wpReason', 60, $this->reason, [
+ 'id' => 'wpReason',
+ // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
+ // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
+ // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+ // "- 155" is to leave room for the auto-generated part of the log entry.
+ 'maxlength' => $oldCommentSchema ? 100 : CommentStore::COMMENT_CHARACTER_LIMIT - 155,
+ ] ) .
'</td>' .
"</tr><tr>\n" .
'<td></td>' .
@@ -310,9 +317,8 @@ class SpecialEditTags extends UnlistedSpecialPage {
// Otherwise, use a multi-select field for adding tags, and a list of
// checkboxes for removing them
- // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
+ // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
for ( $list->reset(); $list->current(); $list->next() ) {
- // @codingStandardsIgnoreEnd
$currentTags = $list->current()->getTags();
if ( $currentTags ) {
$tags = array_merge( $tags, explode( ',', $currentTags ) );
@@ -451,9 +457,8 @@ class SpecialEditTags extends UnlistedSpecialPage {
*/
protected function failure( $status ) {
$this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
- $this->getOutput()->addWikiText( '<div class="errorbox">' .
- $status->getWikiText( 'tags-edit-failure' ) .
- '</div>'
+ $this->getOutput()->addWikiText(
+ Html::errorBox( $status->getWikiText( 'tags-edit-failure' ) )
);
$this->showForm();
}
diff --git a/www/wiki/includes/specials/SpecialEditWatchlist.php b/www/wiki/includes/specials/SpecialEditWatchlist.php
index d2940e4a..f702bc0b 100644
--- a/www/wiki/includes/specials/SpecialEditWatchlist.php
+++ b/www/wiki/includes/specials/SpecialEditWatchlist.php
@@ -243,17 +243,12 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
$this->showTitles( $toUnwatch, $this->successMessage );
}
} else {
- $this->clearWatchlist();
- $this->getUser()->invalidateCache();
- if ( count( $current ) > 0 ) {
- $this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
- } else {
+ if ( count( $current ) === 0 ) {
return false;
}
- $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' )
- ->numParams( count( $current ) )->parse();
+ $this->clearUserWatchedItems( $current, 'raw' );
$this->showTitles( $current, $this->successMessage );
}
@@ -262,17 +257,29 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
public function submitClear( $data ) {
$current = $this->getWatchlist();
- $this->clearWatchlist();
- $this->getUser()->invalidateCache();
- $this->successMessage = $this->msg( 'watchlistedit-clear-done' )->parse();
- $this->successMessage .= ' ' . $this->msg( 'watchlistedit-clear-removed' )
- ->numParams( count( $current ) )->parse();
+ $this->clearUserWatchedItems( $current, 'clear' );
$this->showTitles( $current, $this->successMessage );
-
return true;
}
/**
+ * @param array $current
+ * @param string $messageFor 'raw' or 'clear'
+ */
+ private function clearUserWatchedItems( $current, $messageFor ) {
+ $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
+ if ( $watchedItemStore->clearUserWatchedItems( $this->getUser() ) ) {
+ $this->successMessage = $this->msg( 'watchlistedit-' . $messageFor . '-done' )->parse();
+ $this->successMessage .= ' ' . $this->msg( 'watchlistedit-' . $messageFor . '-removed' )
+ ->numParams( count( $current ) )->parse();
+ $this->getUser()->invalidateCache();
+ } else {
+ $watchedItemStore->clearUserWatchedItemsUsingJobQueue( $this->getUser() );
+ $this->successMessage = $this->msg( 'watchlistedit-clear-jobqueue' )->parse();
+ }
+ }
+
+ /**
* Print out a list of linked titles
*
* $titles can be an array of strings or Title objects; the former
@@ -448,18 +455,6 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
}
/**
- * Remove all titles from a user's watchlist
- */
- private function clearWatchlist() {
- $dbw = wfGetDB( DB_MASTER );
- $dbw->delete(
- 'watchlist',
- [ 'wl_user' => $this->getUser()->getId() ],
- __METHOD__
- );
- }
-
- /**
* Add a list of targets to a user's watchlist
*
* @param string[]|LinkTarget[] $targets
@@ -568,7 +563,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
// checkTitle can filter some options out, avoid empty sections
if ( count( $options ) > 0 ) {
$fields['TitlesNs' . $namespace] = [
- 'class' => 'EditWatchlistCheckboxSeriesField',
+ 'class' => EditWatchlistCheckboxSeriesField::class,
'options' => $options,
'section' => "ns$namespace",
];
@@ -662,7 +657,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
* @return HTMLForm
*/
protected function getRawForm() {
- $titles = implode( $this->getWatchlist(), "\n" );
+ $titles = implode( "\n", $this->getWatchlist() );
$fields = [
'Titles' => [
'type' => 'textarea',
diff --git a/www/wiki/includes/specials/SpecialEmailuser.php b/www/wiki/includes/specials/SpecialEmailuser.php
index 249be7f1..f322ac40 100644
--- a/www/wiki/includes/specials/SpecialEmailuser.php
+++ b/www/wiki/includes/specials/SpecialEmailuser.php
@@ -224,15 +224,29 @@ class SpecialEmailUser extends UnlistedSpecialPage {
wfDebug( "Target is invalid user.\n" );
return 'notarget';
- } elseif ( !$target->isEmailConfirmed() ) {
+ }
+
+ if ( !$target->isEmailConfirmed() ) {
wfDebug( "User has no valid email.\n" );
return 'noemail';
- } elseif ( !$target->canReceiveEmail() ) {
+ }
+
+ if ( !$target->canReceiveEmail() ) {
wfDebug( "User does not allow user emails.\n" );
return 'nowikiemail';
- } elseif ( $sender !== null ) {
+ }
+
+ if ( $sender !== null && !$target->getOption( 'email-allow-new-users' ) &&
+ $sender->isNewbie()
+ ) {
+ wfDebug( "User does not allow user emails from new users.\n" );
+
+ return 'nowikiemail';
+ }
+
+ if ( $sender !== null ) {
$blacklist = $target->getOption( 'email-blacklist', [] );
if ( $blacklist ) {
$lookup = CentralIdLookup::factory();
@@ -280,7 +294,9 @@ class SpecialEmailUser extends UnlistedSpecialPage {
return "blockedemailuser";
}
- if ( $user->pingLimiter( 'emailuser' ) ) {
+ // Check the ping limiter without incrementing it - we'll check it
+ // again later and increment it on a successful send
+ if ( $user->pingLimiter( 'emailuser', 0 ) ) {
wfDebug( "Ping limiter triggered.\n" );
return 'actionthrottledtext';
@@ -376,6 +392,11 @@ class SpecialEmailUser extends UnlistedSpecialPage {
$text .= $context->msg( 'emailuserfooter',
$from->name, $to->name )->inContentLanguage()->text();
+ // Check and increment the rate limits
+ if ( $context->getUser()->pingLimiter( 'emailuser' ) ) {
+ throw new ThrottledError();
+ }
+
$error = false;
if ( !Hooks::run( 'EmailUser', [ &$to, &$from, &$subject, &$text, &$error ] ) ) {
if ( $error instanceof Status ) {
diff --git a/www/wiki/includes/specials/SpecialExpandTemplates.php b/www/wiki/includes/specials/SpecialExpandTemplates.php
index 560d75a6..73ca76bb 100644
--- a/www/wiki/includes/specials/SpecialExpandTemplates.php
+++ b/www/wiki/includes/specials/SpecialExpandTemplates.php
@@ -56,6 +56,7 @@ class SpecialExpandTemplates extends SpecialPage {
global $wgParser;
$this->setHeaders();
+ $this->addHelpLink( 'Help:ExpandTemplates' );
$request = $this->getRequest();
$titleStr = $request->getText( 'wpContextTitle' );
@@ -163,7 +164,6 @@ class SpecialExpandTemplates extends SpecialPage {
'size' => 60,
'default' => $title,
'autofocus' => true,
- 'cssclass' => 'mw-ui-input-inline',
],
'input' => [
'type' => 'textarea',
@@ -172,6 +172,7 @@ class SpecialExpandTemplates extends SpecialPage {
'rows' => 10,
'default' => $input,
'id' => 'input',
+ 'useeditfont' => true,
],
'removecomments' => [
'type' => 'check',
@@ -226,7 +227,11 @@ class SpecialExpandTemplates extends SpecialPage {
$output,
10,
10,
- [ 'id' => 'output', 'readonly' => 'readonly' ]
+ [
+ 'id' => 'output',
+ 'readonly' => 'readonly',
+ 'class' => 'mw-editfont-' . $this->getUser()->getOption( 'editfont' )
+ ]
);
return $out;
diff --git a/www/wiki/includes/specials/SpecialExport.php b/www/wiki/includes/specials/SpecialExport.php
index 8e6c4462..5a98bb90 100644
--- a/www/wiki/includes/specials/SpecialExport.php
+++ b/www/wiki/includes/specials/SpecialExport.php
@@ -23,7 +23,7 @@
* @ingroup SpecialPage
*/
-use Mediawiki\MediaWikiServices;
+use MediaWiki\MediaWikiServices;
/**
* A special page that allows users to export pages in a XML file
@@ -238,7 +238,7 @@ class SpecialExport extends SpecialPage {
$formDescriptor += [
'textarea' => [
- 'class' => 'HTMLTextAreaField',
+ 'class' => HTMLTextAreaField::class,
'name' => 'pages',
'label-message' => 'export-manual',
'nodata' => true,
@@ -380,9 +380,9 @@ class SpecialExport extends SpecialPage {
$buffer = WikiExporter::STREAM;
// This might take a while... :D
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
set_time_limit( 0 );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
}
$exporter = new WikiExporter( $db, $history, $buffer );
@@ -533,9 +533,7 @@ class SpecialExport extends SpecialPage {
* @return array
*/
private function getPageLinks( $inputPages, $pageSet, $depth ) {
- // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
for ( ; $depth > 0; --$depth ) {
- // @codingStandardsIgnoreEnd
$pageSet = $this->getLinks(
$inputPages, $pageSet, 'pagelinks',
[ 'namespace' => 'pl_namespace', 'title' => 'pl_title' ],
diff --git a/www/wiki/includes/specials/SpecialFileDuplicateSearch.php b/www/wiki/includes/specials/SpecialFileDuplicateSearch.php
index 8021bc2c..7694a610 100644
--- a/www/wiki/includes/specials/SpecialFileDuplicateSearch.php
+++ b/www/wiki/includes/specials/SpecialFileDuplicateSearch.php
@@ -85,15 +85,17 @@ class FileDuplicateSearchPage extends QueryPage {
}
public function getQueryInfo() {
+ $imgQuery = LocalFile::getQueryInfo();
return [
- 'tables' => [ 'image' ],
+ 'tables' => $imgQuery['tables'],
'fields' => [
'title' => 'img_name',
'value' => 'img_sha1',
- 'img_user_text',
+ 'img_user_text' => $imgQuery['fields']['img_user_text'],
'img_timestamp'
],
- 'conds' => [ 'img_sha1' => $this->hash ]
+ 'conds' => [ 'img_sha1' => $this->hash ],
+ 'join_conds' => $imgQuery['joins'],
];
}
diff --git a/www/wiki/includes/specials/SpecialImport.php b/www/wiki/includes/specials/SpecialImport.php
index 9ce52ef0..ab5d4d72 100644
--- a/www/wiki/includes/specials/SpecialImport.php
+++ b/www/wiki/includes/specials/SpecialImport.php
@@ -43,6 +43,8 @@ class SpecialImport extends SpecialPage {
private $includeTemplates = false;
private $pageLinkDepth;
private $importSources;
+ private $assignKnownUsers;
+ private $usernamePrefix;
public function __construct() {
parent::__construct( 'Import', 'import' );
@@ -110,6 +112,7 @@ class SpecialImport extends SpecialPage {
$isUpload = false;
$request = $this->getRequest();
$this->sourceName = $request->getVal( "source" );
+ $this->assignKnownUsers = $request->getCheck( 'assignKnownUsers' );
$this->logcomment = $request->getText( 'log-comment' );
$this->pageLinkDepth = $this->getConfig()->get( 'ExportMaxLinkDepth' ) == 0
@@ -130,6 +133,7 @@ class SpecialImport extends SpecialPage {
$source = Status::newFatal( 'import-token-mismatch' );
} elseif ( $this->sourceName === 'upload' ) {
$isUpload = true;
+ $this->usernamePrefix = $this->fullInterwikiPrefix = $request->getVal( 'usernamePrefix' );
if ( $user->isAllowed( 'importupload' ) ) {
$source = ImportStreamSource::newFromUpload( "xmlimport" );
} else {
@@ -169,6 +173,10 @@ class SpecialImport extends SpecialPage {
$source = Status::newFatal( "importunknownsource" );
}
+ if ( (string)$this->fullInterwikiPrefix === '' ) {
+ $source->fatal( 'importnoprefix' );
+ }
+
$out = $this->getOutput();
if ( !$source->isGood() ) {
$out->addWikiText( "<p class=\"error\">\n" .
@@ -192,6 +200,7 @@ class SpecialImport extends SpecialPage {
return;
}
}
+ $importer->setUsernamePrefix( $this->fullInterwikiPrefix, $this->assignKnownUsers );
$out->addWikiMsg( "importstart" );
@@ -338,6 +347,28 @@ class SpecialImport extends SpecialPage {
</tr>
<tr>
<td class='mw-label'>" .
+ Xml::label( $this->msg( 'import-upload-username-prefix' )->text(),
+ 'mw-import-usernamePrefix' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'usernamePrefix', 50,
+ $this->usernamePrefix,
+ [ 'id' => 'usernamePrefix', 'type' => 'text' ] ) . ' ' .
+ "</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel(
+ $this->msg( 'import-assign-known-users' )->text(),
+ 'assignKnownUsers',
+ 'assignKnownUsers',
+ $this->assignKnownUsers
+ ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
Xml::label( $this->msg( 'import-comment' )->text(), 'mw-import-comment' ) .
"</td>
<td class='mw-input'>" .
@@ -489,6 +520,17 @@ class SpecialImport extends SpecialPage {
) .
"</td>
</tr>
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel(
+ $this->msg( 'import-assign-known-users' )->text(),
+ 'assignKnownUsers',
+ 'assignKnownUsers',
+ $this->assignKnownUsers
+ ) .
+ "</td>
+ </tr>
$importDepth
<tr>
<td class='mw-label'>" .
diff --git a/www/wiki/includes/specials/SpecialJavaScriptTest.php b/www/wiki/includes/specials/SpecialJavaScriptTest.php
index 17c64c8e..b786c869 100644
--- a/www/wiki/includes/specials/SpecialJavaScriptTest.php
+++ b/www/wiki/includes/specials/SpecialJavaScriptTest.php
@@ -199,15 +199,6 @@ HTML;
echo $html;
}
- /**
- * Return an array of subpages that this special page will accept.
- *
- * @return string[] subpages
- */
- public function getSubpagesForPrefixSearch() {
- return self::$frameworks;
- }
-
protected function getGroupName() {
return 'other';
}
diff --git a/www/wiki/includes/specials/SpecialLinkSearch.php b/www/wiki/includes/specials/SpecialLinkSearch.php
index cda0854d..ef952543 100644
--- a/www/wiki/includes/specials/SpecialLinkSearch.php
+++ b/www/wiki/includes/specials/SpecialLinkSearch.php
@@ -22,7 +22,7 @@
* @author Brion Vibber
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -225,7 +225,7 @@ class LinkSearchPage extends QueryPage {
* Pre-fill the link cache
*
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function preprocessResults( $db, $res ) {
$this->executeLBFromResultWrapper( $res );
diff --git a/www/wiki/includes/specials/SpecialListDuplicatedFiles.php b/www/wiki/includes/specials/SpecialListDuplicatedFiles.php
index d5fb0018..4c847e9e 100644
--- a/www/wiki/includes/specials/SpecialListDuplicatedFiles.php
+++ b/www/wiki/includes/specials/SpecialListDuplicatedFiles.php
@@ -24,7 +24,7 @@
* @author Brian Wolff
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -75,7 +75,7 @@ class ListDuplicatedFilesPage extends QueryPage {
* Pre-fill the link cache
*
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function preprocessResults( $db, $res ) {
$this->executeLBFromResultWrapper( $res );
diff --git a/www/wiki/includes/specials/SpecialListgrouprights.php b/www/wiki/includes/specials/SpecialListgrouprights.php
index 2315887a..cc62d614 100644
--- a/www/wiki/includes/specials/SpecialListgrouprights.php
+++ b/www/wiki/includes/specials/SpecialListgrouprights.php
@@ -29,7 +29,7 @@
* @author Petr Kadlec <mormegil@centrum.cz>
*/
class SpecialListGroupRights extends SpecialPage {
- function __construct() {
+ public function __construct() {
parent::__construct( 'Listgrouprights' );
}
@@ -81,14 +81,10 @@ class SpecialListGroupRights extends SpecialPage {
? 'all'
: $group;
- $msg = $this->msg( 'group-' . $groupname );
- $groupnameLocalized = !$msg->isBlank() ? $msg->text() : $groupname;
+ $groupnameLocalized = UserGroupMembership::getGroupName( $groupname );
- $msg = $this->msg( 'grouppage-' . $groupname )->inContentLanguage();
- $grouppageLocalized = !$msg->isBlank() ?
- $msg->text() :
- MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname;
- $grouppageLocalizedTitle = Title::newFromText( $grouppageLocalized );
+ $grouppageLocalizedTitle = UserGroupMembership::getGroupPage( $groupname )
+ ?: Title::newFromText( MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname );
if ( $group == '*' || !$grouppageLocalizedTitle ) {
// Do not make a link for the generic * group or group with invalid group page
@@ -141,7 +137,7 @@ class SpecialListGroupRights extends SpecialPage {
}
private function outputNamespaceProtectionInfo() {
- global $wgParser, $wgContLang;
+ global $wgContLang;
$out = $this->getOutput();
$namespaceProtection = $this->getConfig()->get( 'NamespaceProtection' );
@@ -149,11 +145,11 @@ class SpecialListGroupRights extends SpecialPage {
return;
}
- $header = $this->msg( 'listgrouprights-namespaceprotection-header' )->parse();
+ $header = $this->msg( 'listgrouprights-namespaceprotection-header' )->text();
$out->addHTML(
Html::rawElement( 'h2', [], Html::element( 'span', [
'class' => 'mw-headline',
- 'id' => $wgParser->guessSectionNameFromWikiText( $header )
+ 'id' => substr( Parser::guessSectionNameFromStrippedText( $header ), 1 )
], $header ) ) .
Xml::openElement( 'table', [ 'class' => 'wikitable' ] ) .
Html::element(
@@ -231,7 +227,7 @@ class SpecialListGroupRights extends SpecialPage {
* @param array $remove Array of groups this group is allowed to remove or true
* @param array $addSelf Array of groups this group is allowed to add to self or true
* @param array $removeSelf Array of group this group is allowed to remove from self or true
- * @return string List of all granted permissions, separated by comma separator
+ * @return string HTML list of all granted permissions
*/
private function formatPermissions( $permissions, $revoke, $add, $remove, $addSelf, $removeSelf ) {
$r = [];
diff --git a/www/wiki/includes/specials/SpecialListredirects.php b/www/wiki/includes/specials/SpecialListredirects.php
index f81c03c7..48f36402 100644
--- a/www/wiki/includes/specials/SpecialListredirects.php
+++ b/www/wiki/includes/specials/SpecialListredirects.php
@@ -24,7 +24,7 @@
* @author Rob Church <robchur@gmail.com>
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -76,7 +76,7 @@ class ListredirectsPage extends QueryPage {
* Cache page existence for performance
*
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function preprocessResults( $db, $res ) {
if ( !$res->numRows() ) {
diff --git a/www/wiki/includes/specials/SpecialLockdb.php b/www/wiki/includes/specials/SpecialLockdb.php
index 2d087ca4..fb04b90b 100644
--- a/www/wiki/includes/specials/SpecialLockdb.php
+++ b/www/wiki/includes/specials/SpecialLockdb.php
@@ -80,9 +80,9 @@ class SpecialLockdb extends FormSpecialPage {
return Status::newFatal( 'locknoconfirm' );
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$fp = fopen( $this->getConfig()->get( 'ReadOnlyFile' ), 'w' );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( false === $fp ) {
# This used to show a file not found error, but the likeliest reason for fopen()
diff --git a/www/wiki/includes/specials/SpecialLog.php b/www/wiki/includes/specials/SpecialLog.php
index c82d001d..bad17466 100644
--- a/www/wiki/includes/specials/SpecialLog.php
+++ b/www/wiki/includes/specials/SpecialLog.php
@@ -32,6 +32,8 @@ class SpecialLog extends SpecialPage {
}
public function execute( $par ) {
+ global $wgActorTableSchemaMigrationStage;
+
$this->setHeaders();
$this->outputHeader();
$this->getOutput()->addModules( 'mediawiki.userSuggest' );
@@ -79,11 +81,34 @@ class SpecialLog extends SpecialPage {
# Handle type-specific inputs
$qc = [];
if ( $opts->getValue( 'type' ) == 'suppress' ) {
- $offender = User::newFromName( $opts->getValue( 'offender' ), false );
- if ( $offender && $offender->getId() > 0 ) {
- $qc = [ 'ls_field' => 'target_author_id', 'ls_value' => $offender->getId() ];
- } elseif ( $offender && IP::isIPAddress( $offender->getName() ) ) {
- $qc = [ 'ls_field' => 'target_author_ip', 'ls_value' => $offender->getName() ];
+ $offenderName = $opts->getValue( 'offender' );
+ $offender = empty( $offenderName ) ? null : User::newFromName( $offenderName, false );
+ if ( $offender ) {
+ if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+ $qc = [ 'ls_field' => 'target_author_actor', 'ls_value' => $offender->getActorId() ];
+ } else {
+ if ( $offender->getId() > 0 ) {
+ $field = 'target_author_id';
+ $value = $offender->getId();
+ } else {
+ $field = 'target_author_ip';
+ $value = $offender->getName();
+ }
+ if ( !$offender->getActorId() ) {
+ $qc = [ 'ls_field' => $field, 'ls_value' => $value ];
+ } else {
+ $db = wfGetDB( DB_REPLICA );
+ $qc = [
+ 'ls_field' => [ 'target_author_actor', $field ], // So LogPager::getQueryInfo() works right
+ $db->makeList( [
+ $db->makeList(
+ [ 'ls_field' => 'target_author_actor', 'ls_value' => $offender->getActorId() ], LIST_AND
+ ),
+ $db->makeList( [ 'ls_field' => $field, 'ls_value' => $value ], LIST_AND ),
+ ], LIST_OR ),
+ ];
+ }
+ }
}
} else {
// Allow extensions to add relations to their search types
diff --git a/www/wiki/includes/specials/SpecialMIMEsearch.php b/www/wiki/includes/specials/SpecialMIMEsearch.php
index 3290abd5..a54d72de 100644
--- a/www/wiki/includes/specials/SpecialMIMEsearch.php
+++ b/www/wiki/includes/specials/SpecialMIMEsearch.php
@@ -56,8 +56,9 @@ class MIMEsearchPage extends QueryPage {
// Allow wildcard searching
$minorType['img_minor_mime'] = $this->minor;
}
+ $imgQuery = LocalFile::getQueryInfo();
$qi = [
- 'tables' => [ 'image' ],
+ 'tables' => $imgQuery['tables'],
'fields' => [
'namespace' => NS_FILE,
'title' => 'img_name',
@@ -67,7 +68,7 @@ class MIMEsearchPage extends QueryPage {
'img_size',
'img_width',
'img_height',
- 'img_user_text',
+ 'img_user_text' => $imgQuery['fields']['img_user_text'],
'img_timestamp'
],
'conds' => [
@@ -89,6 +90,7 @@ class MIMEsearchPage extends QueryPage {
MEDIATYPE_3D,
],
] + $minorType,
+ 'join_conds' => $imgQuery['joins'],
];
return $qi;
diff --git a/www/wiki/includes/specials/SpecialMediaStatistics.php b/www/wiki/includes/specials/SpecialMediaStatistics.php
index a6d4a3e9..943fa570 100644
--- a/www/wiki/includes/specials/SpecialMediaStatistics.php
+++ b/www/wiki/includes/specials/SpecialMediaStatistics.php
@@ -22,7 +22,7 @@
* @author Brian Wolff
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -114,7 +114,7 @@ class MediaStatisticsPage extends QueryPage {
* @param OutputPage $out
* @param Skin $skin (deprecated presumably)
* @param IDatabase $dbr
- * @param ResultWrapper $res Results from query
+ * @param IResultWrapper $res Results from query
* @param int $num Number of results
* @param int $offset Paging offset (Should always be 0 in our case)
*/
@@ -237,7 +237,8 @@ class MediaStatisticsPage extends QueryPage {
* @return string Comma separated list of allowed extensions (e.g. ".ogg, .oga")
*/
private function getExtensionList( $mime ) {
- $exts = MimeMagic::singleton()->getExtensionsForType( $mime );
+ $exts = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer()
+ ->getExtensionsForType( $mime );
if ( $exts === null ) {
return '';
}
@@ -355,7 +356,7 @@ class MediaStatisticsPage extends QueryPage {
* Initialize total values so we can figure out percentages later.
*
* @param IDatabase $dbr
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
public function preprocessResults( $dbr, $res ) {
$this->executeLBFromResultWrapper( $res );
diff --git a/www/wiki/includes/specials/SpecialMostcategories.php b/www/wiki/includes/specials/SpecialMostcategories.php
index bebed12e..123c1740 100644
--- a/www/wiki/includes/specials/SpecialMostcategories.php
+++ b/www/wiki/includes/specials/SpecialMostcategories.php
@@ -24,7 +24,7 @@
* @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -69,7 +69,7 @@ class MostcategoriesPage extends QueryPage {
/**
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function preprocessResults( $db, $res ) {
$this->executeLBFromResultWrapper( $res );
diff --git a/www/wiki/includes/specials/SpecialMostinterwikis.php b/www/wiki/includes/specials/SpecialMostinterwikis.php
index 5e56694f..c9638384 100644
--- a/www/wiki/includes/specials/SpecialMostinterwikis.php
+++ b/www/wiki/includes/specials/SpecialMostinterwikis.php
@@ -21,7 +21,7 @@
* @ingroup SpecialPage
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -72,7 +72,7 @@ class MostinterwikisPage extends QueryPage {
* Pre-fill the link cache
*
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function preprocessResults( $db, $res ) {
$this->executeLBFromResultWrapper( $res );
diff --git a/www/wiki/includes/specials/SpecialMostlinked.php b/www/wiki/includes/specials/SpecialMostlinked.php
index fbfaa738..c4553a4f 100644
--- a/www/wiki/includes/specials/SpecialMostlinked.php
+++ b/www/wiki/includes/specials/SpecialMostlinked.php
@@ -25,7 +25,7 @@
* @author Rob Church <robchur@gmail.com>
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -78,7 +78,7 @@ class MostlinkedPage extends QueryPage {
* Pre-fill the link cache
*
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function preprocessResults( $db, $res ) {
$this->executeLBFromResultWrapper( $res );
diff --git a/www/wiki/includes/specials/SpecialMostlinkedcategories.php b/www/wiki/includes/specials/SpecialMostlinkedcategories.php
index 956207f8..f238f6c0 100644
--- a/www/wiki/includes/specials/SpecialMostlinkedcategories.php
+++ b/www/wiki/includes/specials/SpecialMostlinkedcategories.php
@@ -24,7 +24,7 @@
* @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -59,7 +59,7 @@ class MostlinkedCategoriesPage extends QueryPage {
* Fetch user page links and cache their existence
*
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function preprocessResults( $db, $res ) {
$this->executeLBFromResultWrapper( $res );
diff --git a/www/wiki/includes/specials/SpecialMostlinkedtemplates.php b/www/wiki/includes/specials/SpecialMostlinkedtemplates.php
index dee1c8ec..4544468d 100644
--- a/www/wiki/includes/specials/SpecialMostlinkedtemplates.php
+++ b/www/wiki/includes/specials/SpecialMostlinkedtemplates.php
@@ -22,7 +22,7 @@
* @author Rob Church <robchur@gmail.com>
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -79,7 +79,7 @@ class MostlinkedTemplatesPage extends QueryPage {
* Pre-cache page existence to speed up link generation
*
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
public function preprocessResults( $db, $res ) {
$this->executeLBFromResultWrapper( $res );
diff --git a/www/wiki/includes/specials/SpecialMovepage.php b/www/wiki/includes/specials/SpecialMovepage.php
index 46d7cf7a..d30ff432 100644
--- a/www/wiki/includes/specials/SpecialMovepage.php
+++ b/www/wiki/includes/specials/SpecialMovepage.php
@@ -235,18 +235,18 @@ class MovePageForm extends UnlistedSpecialPage {
}
if ( count( $err ) ) {
- $out->addHTML( "<div class='errorbox'>\n" );
$action_desc = $this->msg( 'action-move' )->plain();
- $out->addWikiMsg( 'permissionserrorstext-withaction', count( $err ), $action_desc );
+ $errMsgHtml = $this->msg( 'permissionserrorstext-withaction',
+ count( $err ), $action_desc )->parseAsBlock();
if ( count( $err ) == 1 ) {
$errMsg = $err[0];
$errMsgName = array_shift( $errMsg );
if ( $errMsgName == 'hookaborted' ) {
- $out->addHTML( "<p>{$errMsg[0]}</p>\n" );
+ $errMsgHtml .= "<p>{$errMsg[0]}</p>\n";
} else {
- $out->addWikiMsgArray( $errMsgName, $errMsg );
+ $errMsgHtml .= $this->msg( $errMsgName, $errMsg )->parseAsBlock();
}
} else {
$errStr = [];
@@ -260,9 +260,9 @@ class MovePageForm extends UnlistedSpecialPage {
}
}
- $out->addHTML( '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n" );
+ $errMsgHtml .= '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n";
}
- $out->addHTML( "</div>\n" );
+ $out->addHTML( Html::errorBox( $errMsgHtml ) );
}
if ( $this->oldTitle->isProtected( 'move' ) ) {
@@ -287,8 +287,8 @@ class MovePageForm extends UnlistedSpecialPage {
$out->addHTML( "</div>\n" );
}
- // Byte limit (not string length limit) for wpReason and wpNewTitleMain
- // is enforced in the mediawiki.special.movePage module
+ // Length limit for wpReason and wpNewTitleMain is enforced in the
+ // mediawiki.special.movePage module
$immovableNamespaces = [];
foreach ( array_keys( $this->getLanguage()->getNamespaces() ) as $nsId ) {
@@ -326,11 +326,16 @@ class MovePageForm extends UnlistedSpecialPage {
]
);
+ // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
+ // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
+ // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+ $conf = $this->getConfig();
+ $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
$fields[] = new OOUI\FieldLayout(
new OOUI\TextInputWidget( [
'name' => 'wpReason',
'id' => 'wpReason',
- 'maxLength' => 200,
+ 'maxLength' => $oldCommentSchema ? 200 : CommentStore::COMMENT_CHARACTER_LIMIT,
'infusable' => true,
'value' => $this->reason,
] ),
diff --git a/www/wiki/includes/specials/SpecialMyLanguage.php b/www/wiki/includes/specials/SpecialMyLanguage.php
index 9cb6d4b5..37d96f47 100644
--- a/www/wiki/includes/specials/SpecialMyLanguage.php
+++ b/www/wiki/includes/specials/SpecialMyLanguage.php
@@ -81,6 +81,7 @@ class SpecialMyLanguage extends RedirectSpecialArticle {
}
if ( !$base ) {
+ // No subpage provided or base page does not exist
return null;
}
@@ -90,14 +91,38 @@ class SpecialMyLanguage extends RedirectSpecialArticle {
}
$uiCode = $this->getLanguage()->getCode();
+ $wikiLangCode = $this->getConfig()->get( 'LanguageCode' );
+
+ if ( $uiCode === $wikiLangCode ) {
+ // Short circuit when the current UI language is the
+ // wiki's default language to avoid unnecessary page lookups.
+ return $base;
+ }
+
+ // Check for a subpage in current UI language
$proposed = $base->getSubpage( $uiCode );
- if ( $proposed && $proposed->exists() && $uiCode !== $base->getPageLanguage()->getCode() ) {
+ if ( $proposed && $proposed->exists() ) {
return $proposed;
- } elseif ( $provided && $provided->exists() ) {
+ }
+
+ if ( $provided !== $base && $provided->exists() ) {
+ // Explicit language code given and the page exists
return $provided;
- } else {
- return $base;
}
+
+ // Check for fallback languages specified by the UI language
+ $possibilities = Language::getFallbacksFor( $uiCode );
+ foreach ( $possibilities as $lang ) {
+ if ( $lang !== $wikiLangCode ) {
+ $proposed = $base->getSubpage( $lang );
+ if ( $proposed && $proposed->exists() ) {
+ return $proposed;
+ }
+ }
+ }
+
+ // When all else has failed, return the base page
+ return $base;
}
/**
diff --git a/www/wiki/includes/specials/SpecialNewpages.php b/www/wiki/includes/specials/SpecialNewpages.php
index 671ab6fb..46d5276c 100644
--- a/www/wiki/includes/specials/SpecialNewpages.php
+++ b/www/wiki/includes/specials/SpecialNewpages.php
@@ -290,15 +290,17 @@ class SpecialNewpages extends IncludableSpecialPage {
/**
* @param stdClass $result Result row from recent changes
- * @return Revision|bool
+ * @param Title $title
+ * @return bool|Revision
*/
- protected function revisionFromRcResult( stdClass $result ) {
+ protected function revisionFromRcResult( stdClass $result, Title $title ) {
return new Revision( [
- 'comment' => CommentStore::newKey( 'rc_comment' )->getComment( $result )->text,
+ 'comment' => CommentStore::getStore()->getComment( 'rc_comment', $result )->text,
'deleted' => $result->rc_deleted,
'user_text' => $result->rc_user_text,
'user' => $result->rc_user,
- ] );
+ 'actor' => $result->rc_actor,
+ ], 0, $title );
}
/**
@@ -313,8 +315,7 @@ class SpecialNewpages extends IncludableSpecialPage {
// Revision deletion works on revisions,
// so cast our recent change row to a revision row.
- $rev = $this->revisionFromRcResult( $result );
- $rev->setTitle( $title );
+ $rev = $this->revisionFromRcResult( $result, $title );
$classes = [];
$attribs = [ 'data-mw-revid' => $result->rev_id ];
diff --git a/www/wiki/includes/specials/SpecialPageData.php b/www/wiki/includes/specials/SpecialPageData.php
index c52c426e..978efa7f 100644
--- a/www/wiki/includes/specials/SpecialPageData.php
+++ b/www/wiki/includes/specials/SpecialPageData.php
@@ -1,11 +1,32 @@
<?php
+/**
+ * Special page to act as an endpoint for accessing raw page data.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
/**
* Special page to act as an endpoint for accessing raw page data.
* The web server should generally be configured to make this accessible via a canonical URL/URI,
* such as <http://my.domain.org/data/main/Foo>.
*
- * @license GPL-2.0+
+ * @class
+ * @ingroup SpecialPage
*/
class SpecialPageData extends SpecialPage {
diff --git a/www/wiki/includes/specials/SpecialPasswordReset.php b/www/wiki/includes/specials/SpecialPasswordReset.php
index a4f16bd7..84292f3e 100644
--- a/www/wiki/includes/specials/SpecialPasswordReset.php
+++ b/www/wiki/includes/specials/SpecialPasswordReset.php
@@ -37,11 +37,6 @@ class SpecialPasswordReset extends FormSpecialPage {
private $passwordReset = null;
/**
- * @var string[] Temporary storage for the passwords which have been sent out, keyed by username.
- */
- private $passwords = [];
-
- /**
* @var Status
*/
private $result;
diff --git a/www/wiki/includes/specials/SpecialPreferences.php b/www/wiki/includes/specials/SpecialPreferences.php
index ba5a57ea..a5c24e7b 100644
--- a/www/wiki/includes/specials/SpecialPreferences.php
+++ b/www/wiki/includes/specials/SpecialPreferences.php
@@ -21,6 +21,8 @@
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
+
/**
* A special page that allows users to change their preferences
*
@@ -82,7 +84,6 @@ class SpecialPreferences extends SpecialPage {
}
$htmlForm = $this->getFormObject( $user, $this->getContext() );
- $htmlForm->setSubmitCallback( [ 'Preferences', 'tryUISubmit' ] );
$sectionTitles = $htmlForm->getPreferenceSections();
$prefTabs = '';
@@ -121,13 +122,15 @@ class SpecialPreferences extends SpecialPage {
* Get the preferences form to use.
* @param User $user The user.
* @param IContextSource $context The context.
- * @return PreferencesForm|HtmlForm
+ * @return PreferencesForm|HTMLForm
*/
protected function getFormObject( $user, IContextSource $context ) {
- return Preferences::getFormObject( $user, $context );
+ $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
+ $form = $preferencesFactory->getForm( $user, $context );
+ return $form;
}
- private function showResetForm() {
+ protected function showResetForm() {
if ( !$this->getUser()->isAllowed( 'editmyoptions' ) ) {
throw new PermissionsError( 'editmyoptions' );
}
diff --git a/www/wiki/includes/specials/SpecialProtectedpages.php b/www/wiki/includes/specials/SpecialProtectedpages.php
index 8e20d883..d693b990 100644
--- a/www/wiki/includes/specials/SpecialProtectedpages.php
+++ b/www/wiki/includes/specials/SpecialProtectedpages.php
@@ -42,7 +42,7 @@ class SpecialProtectedpages extends SpecialPage {
$request = $this->getRequest();
$type = $request->getVal( $this->IdType );
$level = $request->getVal( $this->IdLevel );
- $sizetype = $request->getVal( 'sizetype' );
+ $sizetype = $request->getVal( 'size-mode' );
$size = $request->getIntOrNull( 'size' );
$ns = $request->getIntOrNull( 'namespace' );
$indefOnly = $request->getBool( 'indefonly' ) ? 1 : 0;
@@ -95,121 +95,53 @@ class SpecialProtectedpages extends SpecialPage {
protected function showOptions( $namespace, $type = 'edit', $level, $sizetype,
$size, $indefOnly, $cascadeOnly, $noRedirect
) {
- $title = $this->getPageTitle();
-
- return Xml::openElement( 'form', [ 'method' => 'get', 'action' => wfScript() ] ) .
- Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', [], $this->msg( 'protectedpages' )->text() ) .
- Html::hidden( 'title', $title->getPrefixedDBkey() ) . "\n" .
- $this->getNamespaceMenu( $namespace ) . "\n" .
- $this->getTypeMenu( $type ) . "\n" .
- $this->getLevelMenu( $level ) . "\n" .
- "<br />\n" .
- $this->getExpiryCheck( $indefOnly ) . "\n" .
- $this->getCascadeCheck( $cascadeOnly ) . "\n" .
- $this->getRedirectCheck( $noRedirect ) . "\n" .
- "<br />\n" .
- $this->getSizeLimit( $sizetype, $size ) . "\n" .
- Xml::submitButton( $this->msg( 'protectedpages-submit' )->text() ) . "\n" .
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' );
- }
-
- /**
- * Prepare the namespace filter drop-down; standard namespace
- * selector, sans the MediaWiki namespace
- *
- * @param string|null $namespace Pre-select namespace
- * @return string
- */
- protected function getNamespaceMenu( $namespace = null ) {
- return Html::rawElement( 'span', [ 'class' => 'mw-input-with-label' ],
- Html::namespaceSelector(
- [
- 'selected' => $namespace,
- 'all' => '',
- 'label' => $this->msg( 'namespace' )->text()
- ], [
- 'name' => 'namespace',
- 'id' => 'namespace',
- 'class' => 'namespaceselector',
- ]
- )
- );
- }
-
- /**
- * @param bool $indefOnly
- * @return string Formatted HTML
- */
- protected function getExpiryCheck( $indefOnly ) {
- return '<span class="mw-input-with-label">' . Xml::checkLabel(
- $this->msg( 'protectedpages-indef' )->text(),
- 'indefonly',
- 'indefonly',
- $indefOnly
- ) . "</span>\n";
- }
-
- /**
- * @param bool $cascadeOnly
- * @return string Formatted HTML
- */
- protected function getCascadeCheck( $cascadeOnly ) {
- return '<span class="mw-input-with-label">' . Xml::checkLabel(
- $this->msg( 'protectedpages-cascade' )->text(),
- 'cascadeonly',
- 'cascadeonly',
- $cascadeOnly
- ) . "</span>\n";
- }
-
- /**
- * @param bool $noRedirect
- * @return string Formatted HTML
- */
- protected function getRedirectCheck( $noRedirect ) {
- return '<span class="mw-input-with-label">' . Xml::checkLabel(
- $this->msg( 'protectedpages-noredirect' )->text(),
- 'noredirect',
- 'noredirect',
- $noRedirect
- ) . "</span>\n";
- }
-
- /**
- * @param string $sizetype "min" or "max"
- * @param mixed $size
- * @return string Formatted HTML
- */
- protected function getSizeLimit( $sizetype, $size ) {
- $max = $sizetype === 'max';
-
- return '<span class="mw-input-with-label">' . Xml::radioLabel(
- $this->msg( 'minimum-size' )->text(),
- 'sizetype',
- 'min',
- 'wpmin',
- !$max
- ) .
- ' ' .
- Xml::radioLabel(
- $this->msg( 'maximum-size' )->text(),
- 'sizetype',
- 'max',
- 'wpmax',
- $max
- ) .
- ' ' .
- Xml::input( 'size', 9, $size, [ 'id' => 'wpsize' ] ) .
- ' ' .
- Xml::label( $this->msg( 'pagesize' )->text(), 'wpsize' ) . "</span>\n";
+ $formDescriptor = [
+ 'namespace' => [
+ 'class' => HTMLSelectNamespace::class,
+ 'name' => 'namespace',
+ 'id' => 'namespace',
+ 'cssclass' => 'namespaceselector',
+ 'all' => '',
+ 'label' => $this->msg( 'namespace' )->text(),
+ ],
+ 'typemenu' => $this->getTypeMenu( $type ),
+ 'levelmenu' => $this->getLevelMenu( $level ),
+ 'expirycheck' => [
+ 'type' => 'check',
+ 'label' => $this->msg( 'protectedpages-indef' )->text(),
+ 'name' => 'indefonly',
+ 'id' => 'indefonly',
+ ],
+ 'cascadecheck' => [
+ 'type' => 'check',
+ 'label' => $this->msg( 'protectedpages-cascade' )->text(),
+ 'name' => 'cascadeonly',
+ 'id' => 'cascadeonly',
+ ],
+ 'redirectcheck' => [
+ 'type' => 'check',
+ 'label' => $this->msg( 'protectedpages-noredirect' )->text(),
+ 'name' => 'noredirect',
+ 'id' => 'noredirect',
+ ],
+ 'sizelimit' => [
+ 'class' => HTMLSizeFilterField::class,
+ 'name' => 'size',
+ ]
+ ];
+ $htmlForm = new HTMLForm( $formDescriptor, $this->getContext() );
+ $htmlForm
+ ->setMethod( 'get' )
+ ->setWrapperLegendMsg( 'protectedpages' )
+ ->setSubmitText( $this->msg( 'protectedpages-submit' )->text() );
+
+ return $htmlForm->prepareForm()->getHTML( false );
}
/**
* Creates the input label of the restriction type
* @param string $pr_type Protection type
- * @return string Formatted HTML
+ * @return array
*/
protected function getTypeMenu( $pr_type ) {
$m = []; // Temporary array
@@ -224,21 +156,22 @@ class SpecialProtectedpages extends SpecialPage {
// Third pass generates sorted XHTML content
foreach ( $m as $text => $type ) {
- $selected = ( $type == $pr_type );
- $options[] = Xml::option( $text, $type, $selected ) . "\n";
+ $options[$text] = $type;
}
- return '<span class="mw-input-with-label">' .
- Xml::label( $this->msg( 'restriction-type' )->text(), $this->IdType ) . ' ' .
- Xml::tags( 'select',
- [ 'id' => $this->IdType, 'name' => $this->IdType ],
- implode( "\n", $options ) ) . "</span>";
+ return [
+ 'type' => 'select',
+ 'options' => $options,
+ 'label' => $this->msg( 'restriction-type' )->text(),
+ 'name' => $this->IdType,
+ 'id' => $this->IdType,
+ ];
}
/**
* Creates the input label of the restriction level
* @param string $pr_level Protection level
- * @return string Formatted HTML
+ * @return array
*/
protected function getLevelMenu( $pr_level ) {
// Temporary array
@@ -256,15 +189,16 @@ class SpecialProtectedpages extends SpecialPage {
// Third pass generates sorted XHTML content
foreach ( $m as $text => $type ) {
- $selected = ( $type == $pr_level );
- $options[] = Xml::option( $text, $type, $selected );
+ $options[$text] = $type;
}
- return '<span class="mw-input-with-label">' .
- Xml::label( $this->msg( 'restriction-level' )->text(), $this->IdLevel ) . ' ' .
- Xml::tags( 'select',
- [ 'id' => $this->IdLevel, 'name' => $this->IdLevel ],
- implode( "\n", $options ) ) . "</span>";
+ return [
+ 'type' => 'select',
+ 'options' => $options,
+ 'label' => $this->msg( 'restriction-level' )->text(),
+ 'name' => $this->IdLevel,
+ 'id' => $this->IdLevel
+ ];
}
protected function getGroupName() {
diff --git a/www/wiki/includes/specials/SpecialProtectedtitles.php b/www/wiki/includes/specials/SpecialProtectedtitles.php
index fa9033cb..fa12f507 100644
--- a/www/wiki/includes/specials/SpecialProtectedtitles.php
+++ b/www/wiki/includes/specials/SpecialProtectedtitles.php
@@ -85,10 +85,8 @@ class SpecialProtectedtitles extends SpecialPage {
}
$link = $this->getLinkRenderer()->makeLink( $title );
- $description_items = [];
// Messages: restriction-level-sysop, restriction-level-autoconfirmed
- $protType = $this->msg( 'restriction-level-' . $row->pt_create_perm )->escaped();
- $description_items[] = $protType;
+ $description = $this->msg( 'restriction-level-' . $row->pt_create_perm )->escaped();
$lang = $this->getLanguage();
$expiry = strlen( $row->pt_expiry ) ?
$lang->formatExpiry( $row->pt_expiry, TS_MW ) :
@@ -96,7 +94,7 @@ class SpecialProtectedtitles extends SpecialPage {
if ( $expiry !== 'infinity' ) {
$user = $this->getUser();
- $description_items[] = $this->msg(
+ $description .= $this->msg( 'comma-separator' )->escaped() . $this->msg(
'protect-expiring-local',
$lang->userTimeAndDate( $expiry, $user ),
$lang->userDate( $expiry, $user ),
@@ -104,8 +102,7 @@ class SpecialProtectedtitles extends SpecialPage {
)->escaped();
}
- // @todo i18n: This should use a comma separator instead of a hard coded comma, right?
- return '<li>' . $lang->specialList( $link, implode( $description_items, ', ' ) ) . "</li>\n";
+ return '<li>' . $lang->specialList( $link, $description ) . "</li>\n";
}
/**
@@ -116,39 +113,25 @@ class SpecialProtectedtitles extends SpecialPage {
* @private
*/
function showOptions( $namespace, $type = 'edit', $level ) {
- $action = htmlspecialchars( wfScript() );
- $title = $this->getPageTitle();
- $special = htmlspecialchars( $title->getPrefixedDBkey() );
-
- return "<form action=\"$action\" method=\"get\">\n" .
- '<fieldset>' .
- Xml::element( 'legend', [], $this->msg( 'protectedtitles' )->text() ) .
- Html::hidden( 'title', $special ) . "&#160;\n" .
- $this->getNamespaceMenu( $namespace ) . "&#160;\n" .
- $this->getLevelMenu( $level ) . "&#160;\n" .
- "&#160;" . Xml::submitButton( $this->msg( 'protectedtitles-submit' )->text() ) . "\n" .
- "</fieldset></form>";
- }
-
- /**
- * Prepare the namespace filter drop-down; standard namespace
- * selector, sans the MediaWiki namespace
- *
- * @param string|null $namespace Pre-select namespace
- * @return string
- */
- function getNamespaceMenu( $namespace = null ) {
- return Html::namespaceSelector(
- [
- 'selected' => $namespace,
- 'all' => '',
- 'label' => $this->msg( 'namespace' )->text()
- ], [
+ $formDescriptor = [
+ 'namespace' => [
+ 'class' => 'HTMLSelectNamespace',
'name' => 'namespace',
'id' => 'namespace',
- 'class' => 'namespaceselector',
- ]
- );
+ 'cssclass' => 'namespaceselector',
+ 'all' => '',
+ 'label' => $this->msg( 'namespace' )->text()
+ ],
+ 'levelmenu' => $this->getLevelMenu( $level )
+ ];
+
+ $htmlForm = new HTMLForm( $formDescriptor, $this->getContext() );
+ $htmlForm
+ ->setMethod( 'get' )
+ ->setWrapperLegendMsg( 'protectedtitles' )
+ ->setSubmitText( $this->msg( 'protectedtitles-submit' )->text() );
+
+ return $htmlForm->prepareForm()->getHTML( false );
}
/**
@@ -176,14 +159,16 @@ class SpecialProtectedtitles extends SpecialPage {
}
// Third pass generates sorted XHTML content
foreach ( $m as $text => $type ) {
- $selected = ( $type == $pr_level );
- $options[] = Xml::option( $text, $type, $selected );
+ $options[ $text ] = $type;
}
- return Xml::label( $this->msg( 'restriction-level' )->text(), $this->IdLevel ) . '&#160;' .
- Xml::tags( 'select',
- [ 'id' => $this->IdLevel, 'name' => $this->IdLevel ],
- implode( "\n", $options ) );
+ return [
+ 'type' => 'select',
+ 'options' => $options,
+ 'label' => $this->msg( 'restriction-level' )->text(),
+ 'name' => $this->IdLevel,
+ 'id' => $this->IdLevel
+ ];
}
protected function getGroupName() {
diff --git a/www/wiki/includes/specials/SpecialRecentchanges.php b/www/wiki/includes/specials/SpecialRecentchanges.php
index 40834cb5..bfef5e03 100644
--- a/www/wiki/includes/specials/SpecialRecentchanges.php
+++ b/www/wiki/includes/specials/SpecialRecentchanges.php
@@ -22,7 +22,7 @@
*/
use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\FakeResultWrapper;
/**
@@ -33,10 +33,11 @@ use Wikimedia\Rdbms\FakeResultWrapper;
class SpecialRecentChanges extends ChangesListSpecialPage {
protected static $savedQueriesPreferenceName = 'rcfilters-saved-queries';
+ protected static $daysPreferenceName = 'rcdays'; // Use general RecentChanges preference
+ protected static $limitPreferenceName = 'rcfilters-limit'; // Use RCFilters-specific preference
private $watchlistFilterGroupDefinition;
- // @codingStandardsIgnoreStart Needed "useless" override to change parameters.
public function __construct( $name = 'Recentchanges', $restriction = '' ) {
parent::__construct( $name, $restriction );
@@ -132,7 +133,6 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
}
];
}
- // @codingStandardsIgnoreEnd
/**
* Main execution point
@@ -164,10 +164,6 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
true
);
parent::execute( $subpage );
-
- if ( $this->isStructuredFilterUiEnabled() ) {
- $out->addJsConfigVars( 'wgStructuredChangeFiltersLiveUpdateSupported', true );
- }
}
/**
@@ -212,8 +208,12 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
$reviewStatus = $this->getFilterGroup( 'reviewStatus' );
if ( $reviewStatus !== null ) {
// Conditional on feature being available and rights
- $hidePatrolled = $reviewStatus->getFilter( 'hidepatrolled' );
- $hidePatrolled->setDefault( $user->getBoolOption( 'hidepatrolled' ) );
+ if ( $user->getBoolOption( 'hidepatrolled' ) ) {
+ $reviewStatus->setDefault( 'unpatrolled' );
+ $legacyReviewStatus = $this->getFilterGroup( 'legacyReviewStatus' );
+ $legacyHidePatrolled = $legacyReviewStatus->getFilter( 'hidepatrolled' );
+ $legacyHidePatrolled->setDefault( true );
+ }
}
$changeType = $this->getFilterGroup( 'changeType' );
@@ -225,24 +225,6 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
}
/**
- * Get a FormOptions object containing the default options
- *
- * @return FormOptions
- */
- public function getDefaultOptions() {
- $opts = parent::getDefaultOptions();
-
- $opts->add( 'days', $this->getDefaultDays(), FormOptions::FLOAT );
- $opts->add( 'limit', $this->getDefaultLimit() );
- $opts->add( 'from', '' );
-
- $opts->add( 'categories', '' );
- $opts->add( 'categories_any', false );
-
- return $opts;
- }
-
- /**
* Get all custom filters
*
* @return array Map of filter URL param names to properties (msg/default)
@@ -287,36 +269,6 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
}
}
- public function validateOptions( FormOptions $opts ) {
- $opts->validateIntBounds( 'limit', 0, 5000 );
- $opts->validateBounds( 'days', 0, $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
- parent::validateOptions( $opts );
- }
-
- /**
- * @inheritDoc
- */
- protected function buildQuery( &$tables, &$fields, &$conds,
- &$query_options, &$join_conds, FormOptions $opts
- ) {
- $dbr = $this->getDB();
- parent::buildQuery( $tables, $fields, $conds,
- $query_options, $join_conds, $opts );
-
- // Calculate cutoff
- $cutoff_unixtime = time() - $opts['days'] * 3600 * 24;
- $cutoff = $dbr->timestamp( $cutoff_unixtime );
-
- $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
- if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) {
- $cutoff = $dbr->timestamp( $opts['from'] );
- } else {
- $opts->reset( 'from' );
- }
-
- $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
- }
-
/**
* @inheritDoc
*/
@@ -326,8 +278,10 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
$dbr = $this->getDB();
$user = $this->getUser();
- $tables[] = 'recentchanges';
- $fields = array_merge( RecentChange::selectFields(), $fields );
+ $rcQuery = RecentChange::getQueryInfo();
+ $tables = array_merge( $tables, $rcQuery['tables'] );
+ $fields = array_merge( $rcQuery['fields'], $fields );
+ $join_conds = array_merge( $join_conds, $rcQuery['joins'] );
// JOIN on watchlist for users
if ( $user->isLoggedIn() && $user->isAllowed( 'viewmywatchlist' ) ) {
@@ -395,11 +349,6 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
$join_conds
);
- // Build the final data
- if ( $this->getConfig()->get( 'AllowCategorizedRecentChanges' ) ) {
- $this->filterByCategories( $rows, $opts );
- }
-
return $rows;
}
@@ -444,7 +393,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
/**
* Build and output the actual changes list.
*
- * @param ResultWrapper $rows Database rows
+ * @param IResultWrapper $rows Database rows
* @param FormOptions $opts
*/
public function outputChangesList( $rows, $opts ) {
@@ -644,7 +593,9 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
/*interface*/false,
$wgContLang
);
- $content = $parserOutput->getText();
+ $content = $parserOutput->getText( [
+ 'enableSectionEditLinks' => false,
+ ] );
// Add only metadata here (including the language links), text is added below
$this->getOutput()->addParserOutputMetadata( $parserOutput );
@@ -656,12 +607,26 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
$topLinksAttributes = [ 'class' => 'mw-recentchanges-toplinks' ];
if ( $this->isStructuredFilterUiEnabled() ) {
- $contentTitle = Html::rawElement( 'div',
- [ 'class' => 'mw-recentchanges-toplinks-title' ],
- $this->msg( 'rcfilters-other-review-tools' )->parse()
- );
+ // Check whether the widget is already collapsed or expanded
+ $collapsedState = $this->getRequest()->getCookie( 'rcfilters-toplinks-collapsed-state' );
+ // Note that an empty/unset cookie means collapsed, so check for !== 'expanded'
+ $topLinksAttributes[ 'class' ] .= $collapsedState !== 'expanded' ?
+ ' mw-recentchanges-toplinks-collapsed' : '';
+
+ $this->getOutput()->enableOOUI();
+ $contentTitle = new OOUI\ButtonWidget( [
+ 'classes' => [ 'mw-recentchanges-toplinks-title' ],
+ 'label' => new OOUI\HtmlSnippet( $this->msg( 'rcfilters-other-review-tools' )->parse() ),
+ 'framed' => false,
+ 'indicator' => $collapsedState !== 'expanded' ? 'down' : 'up',
+ 'flags' => [ 'progressive' ],
+ ] );
+
$contentWrapper = Html::rawElement( 'div',
- array_merge( [ 'class' => 'mw-collapsible-content' ], $langAttributes ),
+ array_merge(
+ [ 'class' => 'mw-recentchanges-toplinks-content mw-collapsible-content' ],
+ $langAttributes
+ ),
$content
);
$content = $contentTitle . $contentWrapper;
@@ -687,16 +652,12 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
*/
function getExtraOptions( $opts ) {
$opts->consumeValues( [
- 'namespace', 'invert', 'associated', 'tagfilter', 'categories', 'categories_any'
+ 'namespace', 'invert', 'associated', 'tagfilter'
] );
$extraOpts = [];
$extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
- if ( $this->getConfig()->get( 'AllowCategorizedRecentChanges' ) ) {
- $extraOpts['category'] = $this->categoryFilterForm( $opts );
- }
-
$tagFilter = ChangeTags::buildTagFilterSelector(
$opts['tagfilter'], false, $this->getContext() );
if ( count( $tagFilter ) ) {
@@ -729,7 +690,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
*/
public function checkLastModified() {
$dbr = $this->getDB();
- $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ );
+ $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', '', __METHOD__ );
return $lastmod;
}
@@ -761,28 +722,16 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
}
/**
- * Create an input to filter changes by categories
- *
- * @param FormOptions $opts
- * @return array
- */
- protected function categoryFilterForm( FormOptions $opts ) {
- list( $label, $input ) = Xml::inputLabelSep( $this->msg( 'rc_categories' )->text(),
- 'categories', 'mw-categories', false, $opts['categories'] );
-
- $input .= ' ' . Xml::checkLabel( $this->msg( 'rc_categories_any' )->text(),
- 'categories_any', 'mw-categories_any', $opts['categories_any'] );
-
- return [ $label, $input ];
- }
-
- /**
* Filter $rows by categories set in $opts
*
- * @param ResultWrapper &$rows Database rows
+ * @deprecated since 1.31
+ *
+ * @param IResultWrapper &$rows Database rows
* @param FormOptions $opts
*/
function filterByCategories( &$rows, FormOptions $opts ) {
+ wfDeprecated( __METHOD__, '1.31' );
+
$categories = array_map( 'trim', explode( '|', $opts['categories'] ) );
if ( !count( $categories ) ) {
@@ -843,7 +792,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
/**
* Makes change an option link which carries all the other options
*
- * @param string $title Title
+ * @param string $title
* @param array $override Options to override
* @param array $options Current options
* @param bool $active Whether to show the link in bold
@@ -939,40 +888,32 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
$links = [];
- $filterGroups = $this->getFilterGroups();
-
- foreach ( $filterGroups as $groupName => $group ) {
- if ( !$group->isPerGroupRequestParameter() ) {
- foreach ( $group->getFilters() as $key => $filter ) {
- if ( $filter->displaysOnUnstructuredUi( $this ) ) {
- $msg = $filter->getShowHide();
- $linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] );
- // Extensions can define additional filters, but don't need to define the corresponding
- // messages. If they don't exist, just fall back to 'show' and 'hide'.
- if ( !$linkMessage->exists() ) {
- $linkMessage = $this->msg( $showhide[1 - $options[$key]] );
- }
-
- $link = $this->makeOptionsLink( $linkMessage->text(),
- [ $key => 1 - $options[$key] ], $nondefaults );
-
- $attribs = [
- 'class' => "$msg rcshowhideoption clshowhideoption",
- 'data-filter-name' => $filter->getName(),
- ];
-
- if ( $filter->isFeatureAvailableOnStructuredUi( $this ) ) {
- $attribs['data-feature-in-structured-ui'] = true;
- }
-
- $links[] = Html::rawElement(
- 'span',
- $attribs,
- $this->msg( $msg )->rawParams( $link )->escaped()
- );
- }
- }
+ foreach ( $this->getLegacyShowHideFilters() as $key => $filter ) {
+ $msg = $filter->getShowHide();
+ $linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] );
+ // Extensions can define additional filters, but don't need to define the corresponding
+ // messages. If they don't exist, just fall back to 'show' and 'hide'.
+ if ( !$linkMessage->exists() ) {
+ $linkMessage = $this->msg( $showhide[1 - $options[$key]] );
+ }
+
+ $link = $this->makeOptionsLink( $linkMessage->text(),
+ [ $key => 1 - $options[$key] ], $nondefaults );
+
+ $attribs = [
+ 'class' => "$msg rcshowhideoption clshowhideoption",
+ 'data-filter-name' => $filter->getName(),
+ ];
+
+ if ( $filter->isFeatureAvailableOnStructuredUi( $this ) ) {
+ $attribs['data-feature-in-structured-ui'] = true;
}
+
+ $links[] = Html::rawElement(
+ 'span',
+ $attribs,
+ $this->msg( $msg )->rawParams( $link )->parse()
+ );
}
// show from this onward link
@@ -1002,11 +943,14 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
return 60 * 5;
}
- function getDefaultLimit() {
- return $this->getUser()->getIntOption( 'rclimit' );
- }
+ public function getDefaultLimit() {
+ $systemPrefValue = $this->getUser()->getIntOption( 'rclimit' );
+ // Prefer the RCFilters-specific preference if RCFilters is enabled
+ if ( $this->isStructuredFilterUiEnabled() ) {
+ return $this->getUser()->getIntOption( static::$limitPreferenceName, $systemPrefValue );
+ }
- function getDefaultDays() {
- return floatval( $this->getUser()->getOption( 'rcdays' ) );
+ // Otherwise, use the system rclimit preference value
+ return $systemPrefValue;
}
}
diff --git a/www/wiki/includes/specials/SpecialRecentchangeslinked.php b/www/wiki/includes/specials/SpecialRecentchangeslinked.php
index a13af55d..181b4db4 100644
--- a/www/wiki/includes/specials/SpecialRecentchangeslinked.php
+++ b/www/wiki/includes/specials/SpecialRecentchangeslinked.php
@@ -62,9 +62,9 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges {
$outputPage = $this->getOutput();
$title = Title::newFromText( $target );
if ( !$title || $title->isExternal() ) {
- $outputPage->addHTML( '<div class="errorbox">' . $this->msg( 'allpagesbadtitle' )
- ->parse() . '</div>' );
-
+ $outputPage->addHTML(
+ Html::errorBox( $this->msg( 'allpagesbadtitle' )->parse() )
+ );
return false;
}
@@ -84,8 +84,10 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges {
$ns = $title->getNamespace();
$dbkey = $title->getDBkey();
- $tables[] = 'recentchanges';
- $select = array_merge( RecentChange::selectFields(), $select );
+ $rcQuery = RecentChange::getQueryInfo();
+ $tables = array_merge( $tables, $rcQuery['tables'] );
+ $select = array_merge( $rcQuery['fields'], $select );
+ $join_conds = array_merge( $join_conds, $rcQuery['joins'] );
// left join with watchlist table to highlight watched rows
$uid = $this->getUser()->getId();
@@ -290,4 +292,23 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges {
public function prefixSearchSubpages( $search, $limit, $offset ) {
return $this->prefixSearchString( $search, $limit, $offset );
}
+
+ protected function outputNoResults() {
+ $targetTitle = $this->getTargetTitle();
+ if ( $targetTitle === false ) {
+ $this->getOutput()->addHTML(
+ '<div class="mw-changeslist-empty mw-changeslist-notargetpage">' .
+ $this->msg( 'recentchanges-notargetpage' )->parse() .
+ '</div>'
+ );
+ } elseif ( !$targetTitle || $targetTitle->isExternal() ) {
+ $this->getOutput()->addHTML(
+ '<div class="mw-changeslist-empty mw-changeslist-invalidtargetpage">' .
+ $this->msg( 'allpagesbadtitle' )->parse() .
+ '</div>'
+ );
+ } else {
+ parent::outputNoResults();
+ }
+ }
}
diff --git a/www/wiki/includes/specials/SpecialRedirect.php b/www/wiki/includes/specials/SpecialRedirect.php
index a3635ebe..e8279113 100644
--- a/www/wiki/includes/specials/SpecialRedirect.php
+++ b/www/wiki/includes/specials/SpecialRedirect.php
@@ -176,7 +176,6 @@ class SpecialRedirect extends FormSpecialPage {
if ( $logid === 0 ) {
return null;
}
-
$query = [ 'title' => 'Special:Log', 'logid' => $logid ];
return wfAppendQuery( wfScript( 'index' ), $query );
}
diff --git a/www/wiki/includes/specials/SpecialResetTokens.php b/www/wiki/includes/specials/SpecialResetTokens.php
index 3e896863..964a261a 100644
--- a/www/wiki/includes/specials/SpecialResetTokens.php
+++ b/www/wiki/includes/specials/SpecialResetTokens.php
@@ -74,7 +74,7 @@ class SpecialResetTokens extends FormSpecialPage {
public function onSuccess() {
$this->getOutput()->wrapWikiMsg(
- "<div class='successbox'>\n$1\n</div>",
+ Html::successBox( '$1' ),
'resettokens-done'
);
}
diff --git a/www/wiki/includes/specials/SpecialRevisiondelete.php b/www/wiki/includes/specials/SpecialRevisiondelete.php
index e1d4dd1b..e7db9f5e 100644
--- a/www/wiki/includes/specials/SpecialRevisiondelete.php
+++ b/www/wiki/includes/specials/SpecialRevisiondelete.php
@@ -388,9 +388,8 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$numRevisions = 0;
// Live revisions...
$list = $this->getList();
- // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
+ // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
for ( $list->reset(); $list->current(); $list->next() ) {
- // @codingStandardsIgnoreEnd
$item = $list->current();
if ( !$item->canView() ) {
@@ -419,8 +418,12 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
// Show form if the user can submit
if ( $this->mIsAllowed ) {
+ $out->addModules( [ 'mediawiki.special.revisionDelete' ] );
$out->addModuleStyles( 'mediawiki.special' );
+ $conf = $this->getConfig();
+ $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
+
$form = Xml::openElement( 'form', [ 'method' => 'post',
'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ),
'id' => 'mw-revdel-form-revisions' ] ) .
@@ -443,12 +446,14 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
Xml::label( $this->msg( 'revdelete-otherreason' )->text(), 'wpReason' ) .
'</td>' .
'<td class="mw-input">' .
- Xml::input(
- 'wpReason',
- 60,
- $this->otherReason,
- [ 'id' => 'wpReason', 'maxlength' => 100 ]
- ) .
+ Xml::input( 'wpReason', 60, $this->otherReason, [
+ 'id' => 'wpReason',
+ // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
+ // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
+ // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+ // "- 155" is to leave room for the 'wpRevDeleteReasonList' value.
+ 'maxlength' => $oldCommentSchema ? 100 : CommentStore::COMMENT_CHARACTER_LIMIT - 155,
+ ] ) .
'</td>' .
"</tr><tr>\n" .
'<td></td>' .
@@ -636,9 +641,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
protected function failure( $status ) {
// Messages: revdelete-failure, logdelete-failure
$this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
- $this->getOutput()->addWikiText( '<div class="errorbox">' .
- $status->getWikiText( $this->typeLabels['failure'] ) .
- '</div>'
+ $this->getOutput()->addWikiText(
+ Html::errorBox(
+ $status->getWikiText( $this->typeLabels['failure'] )
+ )
);
$this->showForm();
}
diff --git a/www/wiki/includes/specials/SpecialRunJobs.php b/www/wiki/includes/specials/SpecialRunJobs.php
index cb1e892e..375694be 100644
--- a/www/wiki/includes/specials/SpecialRunJobs.php
+++ b/www/wiki/includes/specials/SpecialRunJobs.php
@@ -39,17 +39,21 @@ class SpecialRunJobs extends UnlistedSpecialPage {
public function execute( $par = '' ) {
$this->getOutput()->disable();
+
if ( wfReadOnly() ) {
wfHttpError( 423, 'Locked', 'Wiki is in read-only mode.' );
return;
- } elseif ( !$this->getRequest()->wasPosted() ) {
+ }
+
+ // Validate request method
+ if ( !$this->getRequest()->wasPosted() ) {
wfHttpError( 400, 'Bad Request', 'Request must be POSTed.' );
return;
}
+ // Validate request parameters
$optional = [ 'maxjobs' => 0, 'maxtime' => 30, 'type' => false, 'async' => true ];
$required = array_flip( [ 'title', 'tasks', 'signature', 'sigexpiry' ] );
-
$params = array_intersect_key( $this->getRequest()->getValues(), $required + $optional );
$missing = array_diff_key( $required, $params );
if ( count( $missing ) ) {
@@ -59,11 +63,11 @@ class SpecialRunJobs extends UnlistedSpecialPage {
return;
}
+ // Validate request signature
$squery = $params;
unset( $squery['signature'] );
$correctSignature = self::getQuerySignature( $squery, $this->getConfig()->get( 'SecretKey' ) );
$providedSignature = $params['signature'];
-
$verified = is_string( $providedSignature )
&& hash_equals( $correctSignature, $providedSignature );
if ( !$verified || $params['sigexpiry'] < time() ) {
@@ -75,39 +79,34 @@ class SpecialRunJobs extends UnlistedSpecialPage {
$params += $optional;
if ( $params['async'] ) {
- // Client will usually disconnect before checking the response,
- // but it needs to know when it is safe to disconnect. Until this
- // reaches ignore_user_abort(), it is not safe as the jobs won't run.
- ignore_user_abort( true ); // jobs may take a bit of time
// HTTP 202 Accepted
HttpStatus::header( 202 );
- ob_flush();
- flush();
- // Once the client receives this response, it can disconnect
- set_error_handler( function ( $errno, $errstr ) {
- if ( strpos( $errstr, 'Cannot modify header information' ) !== false ) {
- return true; // bug T115413
- }
- // Delegate unhandled errors to the default MediaWiki handler
- // so that fatal errors get proper logging (T89169)
- return call_user_func_array(
- 'MWExceptionHandler::handleError', func_get_args()
- );
- } );
+ // Clients are meant to disconnect without waiting for the full response.
+ // Let the page output happen before the jobs start, so that clients know it's
+ // safe to disconnect. MediaWiki::preOutputCommit() calls ignore_user_abort()
+ // or similar to make sure we stay alive to run the deferred update.
+ DeferredUpdates::addUpdate(
+ new TransactionRoundDefiningUpdate(
+ function () use ( $params ) {
+ $this->doRun( $params );
+ },
+ __METHOD__
+ ),
+ DeferredUpdates::POSTSEND
+ );
+ } else {
+ $this->doRun( $params );
+ print "Done\n";
}
+ }
- // Do all of the specified tasks...
- if ( in_array( 'jobs', explode( '|', $params['tasks'] ) ) ) {
- $runner = new JobRunner( LoggerFactory::getInstance( 'runJobs' ) );
- $response = $runner->run( [
- 'type' => $params['type'],
- 'maxJobs' => $params['maxjobs'] ? $params['maxjobs'] : 1,
- 'maxTime' => $params['maxtime'] ? $params['maxjobs'] : 30
- ] );
- if ( !$params['async'] ) {
- print FormatJson::encode( $response, true );
- }
- }
+ protected function doRun( array $params ) {
+ $runner = new JobRunner( LoggerFactory::getInstance( 'runJobs' ) );
+ $runner->run( [
+ 'type' => $params['type'],
+ 'maxJobs' => $params['maxjobs'] ?: 1,
+ 'maxTime' => $params['maxtime'] ?: 30
+ ] );
}
/**
diff --git a/www/wiki/includes/specials/SpecialSearch.php b/www/wiki/includes/specials/SpecialSearch.php
index 85b4572b..f8268445 100644
--- a/www/wiki/includes/specials/SpecialSearch.php
+++ b/www/wiki/includes/specials/SpecialSearch.php
@@ -365,16 +365,12 @@ class SpecialSearch extends SpecialPage {
if ( $hasErrors ) {
list( $error, $warning ) = $textStatus->splitByErrorType();
if ( $error->getErrors() ) {
- $out->addHTML( Html::rawElement(
- 'div',
- [ 'class' => 'errorbox' ],
+ $out->addHTML( Html::errorBox(
$error->getHTML( 'search-error' )
) );
}
if ( $warning->getErrors() ) {
- $out->addHTML( Html::rawElement(
- 'div',
- [ 'class' => 'warningbox' ],
+ $out->addHTML( Html::warningBox(
$warning->getHTML( 'search-warning' )
) );
}
@@ -398,7 +394,8 @@ class SpecialSearch extends SpecialPage {
$linkRenderer = $this->getLinkRenderer();
$mainResultWidget = new FullSearchResultWidget( $this, $linkRenderer );
- if ( $search->getFeatureData( 'enable-new-crossproject-page' ) ) {
+ // Default (null) on. Can be explicitly disabled.
+ if ( $search->getFeatureData( 'enable-new-crossproject-page' ) !== false ) {
$sidebarResultWidget = new InterwikiSearchResultWidget( $this, $linkRenderer );
$sidebarResultsWidget = new InterwikiSearchResultSetWidget(
$this,
@@ -529,7 +526,7 @@ class SpecialSearch extends SpecialPage {
if ( strval( $term ) !== '' ) {
$out->setPageTitle( $this->msg( 'searchresults' ) );
$out->setHTMLTitle( $this->msg( 'pagetitle' )
- ->rawParams( $this->msg( 'searchresults-title' )->rawParams( $term )->text() )
+ ->plaintextParams( $this->msg( 'searchresults-title' )->plaintextParams( $term )->text() )
->inContentLanguage()->text()
);
}
diff --git a/www/wiki/includes/specials/SpecialShortpages.php b/www/wiki/includes/specials/SpecialShortpages.php
index e9c15e7b..d90f72c2 100644
--- a/www/wiki/includes/specials/SpecialShortpages.php
+++ b/www/wiki/includes/specials/SpecialShortpages.php
@@ -21,7 +21,7 @@
* @ingroup SpecialPage
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -124,7 +124,7 @@ class ShortPagesPage extends QueryPage {
/**
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function preprocessResults( $db, $res ) {
$this->executeLBFromResultWrapper( $res );
diff --git a/www/wiki/includes/specials/SpecialTags.php b/www/wiki/includes/specials/SpecialTags.php
index 605ee008..6b0598ce 100644
--- a/www/wiki/includes/specials/SpecialTags.php
+++ b/www/wiki/includes/specials/SpecialTags.php
@@ -441,7 +441,7 @@ class SpecialTags extends SpecialPage {
$out = $context->getOutput();
$tag = $data['HiddenTag'];
- $status = call_user_func( [ 'ChangeTags', "{$form->tagAction}TagWithChecks" ],
+ $status = call_user_func( [ ChangeTags::class, "{$form->tagAction}TagWithChecks" ],
$tag, $data['Reason'], $context->getUser(), true );
if ( $status->isGood() ) {
diff --git a/www/wiki/includes/specials/SpecialUnblock.php b/www/wiki/includes/specials/SpecialUnblock.php
index 01125fcf..b2d5a163 100644
--- a/www/wiki/includes/specials/SpecialUnblock.php
+++ b/www/wiki/includes/specials/SpecialUnblock.php
@@ -57,9 +57,9 @@ class SpecialUnblock extends SpecialPage {
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'unblockip' ) );
- $out->addModules( [ 'mediawiki.special', 'mediawiki.userSuggest' ] );
+ $out->addModules( [ 'mediawiki.userSuggest' ] );
- $form = new HTMLForm( $this->getFields(), $this->getContext() );
+ $form = HTMLForm::factory( 'ooui', $this->getFields(), $this->getContext() );
$form->setWrapperLegendMsg( 'unblockip' );
$form->setSubmitCallback( [ __CLASS__, 'processUIUnblock' ] );
$form->setSubmitTextMsg( 'ipusubmit' );
diff --git a/www/wiki/includes/specials/SpecialUncategorizedcategories.php b/www/wiki/includes/specials/SpecialUncategorizedcategories.php
index 5ff9e04e..2dcb77f8 100644
--- a/www/wiki/includes/specials/SpecialUncategorizedcategories.php
+++ b/www/wiki/includes/specials/SpecialUncategorizedcategories.php
@@ -60,7 +60,7 @@ class UncategorizedCategoriesPage extends UncategorizedPagesPage {
$title = Title::makeTitleSafe( NS_CATEGORY, $titleStr );
}
if ( $title ) {
- $this->exceptionList[] = $title->getDBKey();
+ $this->exceptionList[] = $title->getDBkey();
}
}
}
diff --git a/www/wiki/includes/specials/SpecialUndelete.php b/www/wiki/includes/specials/SpecialUndelete.php
index 740207d6..540dbc6b 100644
--- a/www/wiki/includes/specials/SpecialUndelete.php
+++ b/www/wiki/includes/specials/SpecialUndelete.php
@@ -21,7 +21,7 @@
* @ingroup SpecialPage
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
/**
* Special page allowing users with the appropriate permissions to view
@@ -306,7 +306,7 @@ class SpecialUndelete extends SpecialPage {
/**
* Generic list of deleted pages
*
- * @param ResultWrapper $result
+ * @param IResultWrapper $result
* @return bool
*/
private function showList( $result ) {
@@ -450,40 +450,40 @@ class SpecialUndelete extends SpecialPage {
if ( ( $this->mPreview || !$isText ) && $content ) {
// NOTE: non-text content has no source view, so always use rendered preview
- // Hide [edit]s
$popts = $out->parserOptions();
- $popts->setEditSection( false );
$pout = $content->getParserOutput( $this->mTargetObj, $rev->getId(), $popts, true );
- $out->addParserOutput( $pout );
+ $out->addParserOutput( $pout, [
+ 'enableSectionEditLinks' => false,
+ ] );
}
+ $out->enableOOUI();
+ $buttonFields = [];
+
if ( $isText ) {
// source view for textual content
- $sourceView = Xml::element(
- 'textarea',
- [
- 'readonly' => 'readonly',
- 'cols' => 80,
- 'rows' => 25
- ],
- $content->getNativeData() . "\n"
- );
+ $sourceView = Xml::element( 'textarea', [
+ 'readonly' => 'readonly',
+ 'cols' => 80,
+ 'rows' => 25
+ ], $content->getNativeData() . "\n" );
- $previewButton = Xml::element( 'input', [
+ $buttonFields[] = new OOUI\ButtonInputWidget( [
'type' => 'submit',
'name' => 'preview',
- 'value' => $this->msg( 'showpreview' )->text()
+ 'label' => $this->msg( 'showpreview' )->text()
] );
} else {
$sourceView = '';
$previewButton = '';
}
- $diffButton = Xml::element( 'input', [
+ $buttonFields[] = new OOUI\ButtonInputWidget( [
'name' => 'diff',
'type' => 'submit',
- 'value' => $this->msg( 'showdiff' )->text() ] );
+ 'label' => $this->msg( 'showdiff' )->text()
+ ] );
$out->addHTML(
$sourceView .
@@ -504,8 +504,13 @@ class SpecialUndelete extends SpecialPage {
'type' => 'hidden',
'name' => 'wpEditToken',
'value' => $user->getEditToken() ] ) .
- $previewButton .
- $diffButton .
+ new OOUI\FieldLayout(
+ new OOUI\Widget( [
+ 'content' => new OOUI\HorizontalLayout( [
+ 'items' => $buttonFields
+ ] )
+ ] )
+ ) .
Xml::closeElement( 'form' ) .
Xml::closeElement( 'div' )
);
@@ -734,6 +739,9 @@ class SpecialUndelete extends SpecialPage {
'content' => new OOUI\HtmlSnippet( $this->msg( 'undeleteextrahelp' )->parseAsBlock() )
] );
+ $conf = $this->getConfig();
+ $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
+
$fields[] = new OOUI\FieldLayout(
new OOUI\TextInputWidget( [
'name' => 'wpComment',
@@ -741,6 +749,10 @@ class SpecialUndelete extends SpecialPage {
'infusable' => true,
'value' => $this->mComment,
'autofocus' => true,
+ // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
+ // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
+ // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+ 'maxLength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
] ),
[
'label' => $this->msg( 'undeletecomment' )->text(),
@@ -969,12 +981,12 @@ class SpecialUndelete extends SpecialPage {
$key = urlencode( $row->fa_storage_key );
$pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key );
} else {
- $pageLink = $this->getLanguage()->userTimeAndDate( $ts, $user );
+ $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
}
$userLink = $this->getFileUser( $file );
$data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
$bytes = $this->msg( 'parentheses' )
- ->rawParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() )
+ ->plaintextParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() )
->plain();
$data = htmlspecialchars( $data . ' ' . $bytes );
$comment = $this->getFileComment( $file );
@@ -1049,7 +1061,7 @@ class SpecialUndelete extends SpecialPage {
$time = $this->getLanguage()->userTimeAndDate( $ts, $user );
if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
- return '<span class="history-deleted">' . $time . '</span>';
+ return '<span class="history-deleted">' . htmlspecialchars( $time ) . '</span>';
}
$link = $this->getLinkRenderer()->makeKnownLink(
diff --git a/www/wiki/includes/specials/SpecialUnlockdb.php b/www/wiki/includes/specials/SpecialUnlockdb.php
index 8cd86ce6..3135653c 100644
--- a/www/wiki/includes/specials/SpecialUnlockdb.php
+++ b/www/wiki/includes/specials/SpecialUnlockdb.php
@@ -69,9 +69,9 @@ class SpecialUnlockdb extends FormSpecialPage {
}
$readOnlyFile = $this->getConfig()->get( 'ReadOnlyFile' );
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$res = unlink( $readOnlyFile );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $res ) {
return Status::newGood();
diff --git a/www/wiki/includes/specials/SpecialUnwatchedpages.php b/www/wiki/includes/specials/SpecialUnwatchedpages.php
index fea7e216..0ea7dfae 100644
--- a/www/wiki/includes/specials/SpecialUnwatchedpages.php
+++ b/www/wiki/includes/specials/SpecialUnwatchedpages.php
@@ -24,7 +24,7 @@
* @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -50,7 +50,7 @@ class UnwatchedpagesPage extends QueryPage {
* Pre-cache page existence to speed up link generation
*
* @param IDatabase $db
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
public function preprocessResults( $db, $res ) {
if ( !$res->numRows() ) {
diff --git a/www/wiki/includes/specials/SpecialUpload.php b/www/wiki/includes/specials/SpecialUpload.php
index 024034a6..f7cb6545 100644
--- a/www/wiki/includes/specials/SpecialUpload.php
+++ b/www/wiki/includes/specials/SpecialUpload.php
@@ -612,25 +612,29 @@ class SpecialUpload extends SpecialPage {
}
}
+ $licenseText = '';
+ if ( $license !== '' ) {
+ $licenseText = '== ' . $msg['license-header'] . " ==\n{{" . $license . "}}\n";
+ }
+
+ $pageText = $comment . "\n";
+ $headerText = '== ' . $msg['filedesc'] . ' ==';
+ if ( $comment !== '' && strpos( $comment, $headerText ) === false ) {
+ // prepend header to page text unless it's already there (or there is no content)
+ $pageText = $headerText . "\n" . $pageText;
+ }
+
if ( $config->get( 'UseCopyrightUpload' ) ) {
- $licensetxt = '';
- if ( $license != '' ) {
- $licensetxt = '== ' . $msg['license-header'] . " ==\n" . '{{' . $license . '}}' . "\n";
- }
- $pageText = '== ' . $msg['filedesc'] . " ==\n" . $comment . "\n" .
- '== ' . $msg['filestatus'] . " ==\n" . $copyStatus . "\n" .
- "$licensetxt" .
- '== ' . $msg['filesource'] . " ==\n" . $source;
+ $pageText .= '== ' . $msg['filestatus'] . " ==\n" . $copyStatus . "\n";
+ $pageText .= $licenseText;
+ $pageText .= '== ' . $msg['filesource'] . " ==\n" . $source;
} else {
- if ( $license != '' ) {
- $filedesc = $comment == '' ? '' : '== ' . $msg['filedesc'] . " ==\n" . $comment . "\n";
- $pageText = $filedesc .
- '== ' . $msg['license-header'] . " ==\n" . '{{' . $license . '}}' . "\n";
- } else {
- $pageText = $comment;
- }
+ $pageText .= $licenseText;
}
+ // allow extensions to modify the content
+ Hooks::run( 'UploadForm:getInitialPageText', [ &$pageText, $msg, $config ] );
+
return $pageText;
}
diff --git a/www/wiki/includes/specials/SpecialUploadStash.php b/www/wiki/includes/specials/SpecialUploadStash.php
index b0bb595e..c8b1578f 100644
--- a/www/wiki/includes/specials/SpecialUploadStash.php
+++ b/www/wiki/includes/specials/SpecialUploadStash.php
@@ -18,8 +18,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup SpecialPage
- * @ingroup Upload
*/
/**
@@ -31,6 +29,9 @@
*
* Since this is based on the user's session, in effect this creates a private temporary file area.
* However, the URLs for the files cannot be shared.
+ *
+ * @ingroup SpecialPage
+ * @ingroup Upload
*/
class SpecialUploadStash extends UnlistedSpecialPage {
// UploadStash
@@ -106,8 +107,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
$message = $e->getMessage();
} catch ( SpecialUploadStashTooLargeException $e ) {
$code = 500;
- $message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES .
- ' bytes. ' . $e->getMessage();
+ $message = $e->getMessage();
} catch ( Exception $e ) {
$code = 500;
$message = $e->getMessage();
@@ -129,7 +129,9 @@ class SpecialUploadStash extends UnlistedSpecialPage {
$type = strtok( $key, '/' );
if ( $type !== 'file' && $type !== 'thumb' ) {
- throw new UploadStashBadPathException( "Unknown type '$type'" );
+ throw new UploadStashBadPathException(
+ wfMessage( 'uploadstash-bad-path-unknown-type', $type )
+ );
}
$fileName = strtok( '/' );
$thumbPart = strtok( '/' );
@@ -137,7 +139,9 @@ class SpecialUploadStash extends UnlistedSpecialPage {
if ( $type === 'thumb' ) {
$srcNamePos = strrpos( $thumbPart, $fileName );
if ( $srcNamePos === false || $srcNamePos < 1 ) {
- throw new UploadStashBadPathException( 'Unrecognized thumb name' );
+ throw new UploadStashBadPathException(
+ wfMessage( 'uploadstash-bad-path-unrecognized-thumb-name' )
+ );
}
$paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
@@ -147,8 +151,9 @@ class SpecialUploadStash extends UnlistedSpecialPage {
return [ 'file' => $file, 'type' => $type, 'params' => $params ];
} else {
- throw new UploadStashBadPathException( 'No handler found for ' .
- "mime {$file->getMimeType()} of file {$file->getPath()}" );
+ throw new UploadStashBadPathException(
+ wfMessage( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
+ );
}
}
@@ -194,12 +199,16 @@ class SpecialUploadStash extends UnlistedSpecialPage {
$thumbnailImage = $file->transform( $params, $flags );
if ( !$thumbnailImage ) {
- throw new MWException( 'Could not obtain thumbnail' );
+ throw new UploadStashFileNotFoundException(
+ wfMessage( 'uploadstash-file-not-found-no-thumb' )
+ );
}
// we should have just generated it locally
if ( !$thumbnailImage->getStoragePath() ) {
- throw new UploadStashFileNotFoundException( "no local path for scaled item" );
+ throw new UploadStashFileNotFoundException(
+ wfMessage( 'uploadstash-file-not-found-no-local-path' )
+ );
}
// now we should construct a File, so we can get MIME and other such info in a standard way
@@ -207,7 +216,9 @@ class SpecialUploadStash extends UnlistedSpecialPage {
$thumbFile = new UnregisteredLocalFile( false,
$this->stash->repo, $thumbnailImage->getStoragePath(), false );
if ( !$thumbFile ) {
- throw new UploadStashFileNotFoundException( "couldn't create local file object for thumbnail" );
+ throw new UploadStashFileNotFoundException(
+ wfMessage( 'uploadstash-file-not-found-no-object' )
+ );
}
return $this->outputLocalFile( $thumbFile );
@@ -261,13 +272,19 @@ class SpecialUploadStash extends UnlistedSpecialPage {
$status = $req->execute();
if ( !$status->isOK() ) {
$errors = $status->getErrorsArray();
- $errorStr = "Fetching thumbnail failed: " . print_r( $errors, 1 );
- $errorStr .= "\nurl = $scalerThumbUrl\n";
- throw new MWException( $errorStr );
+ throw new UploadStashFileNotFoundException(
+ wfMessage(
+ 'uploadstash-file-not-found-no-remote-thumb',
+ print_r( $errors, 1 ),
+ $scalerThumbUrl
+ )
+ );
}
$contentType = $req->getResponseHeader( "content-type" );
if ( !$contentType ) {
- throw new MWException( "Missing content-type header" );
+ throw new UploadStashFileNotFoundException(
+ wfMessage( 'uploadstash-file-not-found-missing-content-type' )
+ );
}
return $this->outputContents( $req->getContent(), $contentType );
@@ -284,7 +301,9 @@ class SpecialUploadStash extends UnlistedSpecialPage {
*/
private function outputLocalFile( File $file ) {
if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
- throw new SpecialUploadStashTooLargeException();
+ throw new SpecialUploadStashTooLargeException(
+ wfMessage( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
+ );
}
return $file->getRepo()->streamFile( $file->getPath(),
@@ -296,7 +315,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
/**
* Output HTTP response of raw content
* Side effect: writes HTTP response to STDOUT.
- * @param string $content Content
+ * @param string $content
* @param string $contentType MIME type
* @throws SpecialUploadStashTooLargeException
* @return bool
@@ -304,7 +323,9 @@ class SpecialUploadStash extends UnlistedSpecialPage {
private function outputContents( $content, $contentType ) {
$size = strlen( $content );
if ( $size > self::MAX_SERVE_BYTES ) {
- throw new SpecialUploadStashTooLargeException();
+ throw new SpecialUploadStashTooLargeException(
+ wfMessage( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
+ );
}
// Cancel output buffering and gzipping if set
wfResetOutputBuffers();
@@ -427,5 +448,9 @@ class SpecialUploadStash extends UnlistedSpecialPage {
}
}
-class SpecialUploadStashTooLargeException extends MWException {
+/**
+ * @ingroup SpecialPage
+ * @ingroup Upload
+ */
+class SpecialUploadStashTooLargeException extends UploadStashException {
}
diff --git a/www/wiki/includes/specials/SpecialUserLogout.php b/www/wiki/includes/specials/SpecialUserLogout.php
index a9b732ef..568327d2 100644
--- a/www/wiki/includes/specials/SpecialUserLogout.php
+++ b/www/wiki/includes/specials/SpecialUserLogout.php
@@ -48,6 +48,28 @@ class SpecialUserLogout extends UnlistedSpecialPage {
$this->setHeaders();
$this->outputHeader();
+ $out = $this->getOutput();
+ $user = $this->getUser();
+ $request = $this->getRequest();
+
+ $logoutToken = $request->getVal( 'logoutToken' );
+ $urlParams = [
+ 'logoutToken' => $user->getEditToken( 'logoutToken', $request )
+ ] + $request->getValues();
+ unset( $urlParams['title'] );
+ $continueLink = $this->getFullTitle()->getFullUrl( $urlParams );
+
+ if ( $logoutToken === null ) {
+ $this->getOutput()->addWikiMsg( 'userlogout-continue', $continueLink );
+ return;
+ }
+ if ( !$this->getUser()->matchEditToken(
+ $logoutToken, 'logoutToken', $this->getRequest(), 24 * 60 * 60
+ ) ) {
+ $this->getOutput()->addWikiMsg( 'userlogout-sessionerror', $continueLink );
+ return;
+ }
+
// Make sure it's possible to log out
$session = MediaWiki\Session\SessionManager::getGlobalSession();
if ( !$session->canSetUser() ) {
diff --git a/www/wiki/includes/specials/SpecialUserrights.php b/www/wiki/includes/specials/SpecialUserrights.php
index 0a712eff..40f02a5f 100644
--- a/www/wiki/includes/specials/SpecialUserrights.php
+++ b/www/wiki/includes/specials/SpecialUserrights.php
@@ -716,6 +716,8 @@ class UserrightsPage extends SpecialPage {
->rawParams( $userToolLinks )->parse()
);
if ( $canChangeAny ) {
+ $conf = $this->getConfig();
+ $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
$this->getOutput()->addHTML(
$this->msg( 'userrights-groups-help', $user->getName() )->parse() .
$grouplist .
@@ -726,8 +728,13 @@ class UserrightsPage extends SpecialPage {
Xml::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) .
"</td>
<td class='mw-input'>" .
- Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason', false ),
- [ 'id' => 'wpReason', 'maxlength' => 255 ] ) .
+ Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason', false ), [
+ 'id' => 'wpReason',
+ // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
+ // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
+ // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+ 'maxlength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
+ ] ) .
"</td>
</tr>
<tr>
@@ -761,7 +768,7 @@ class UserrightsPage extends SpecialPage {
/**
* Adds a table with checkboxes where you can select what groups to add/remove
*
- * @param array $usergroups Associative array of (group name as string =>
+ * @param UserGroupMembership[] $usergroups Associative array of (group name as string =>
* UserGroupMembership object) for groups the user belongs to
* @param User $user
* @return Array with 2 elements: the XHTML table element with checkxboes, and
@@ -835,7 +842,10 @@ class UserrightsPage extends SpecialPage {
}
$ret .= "\t<td style='vertical-align:top;'>\n";
foreach ( $column as $group => $checkbox ) {
- $attr = $checkbox['disabled'] ? [ 'disabled' => 'disabled' ] : [];
+ $attr = [ 'class' => 'mw-userrights-groupcheckbox' ];
+ if ( $checkbox['disabled'] ) {
+ $attr['disabled'] = 'disabled';
+ }
$member = UserGroupMembership::getGroupMemberName( $group, $user->getName() );
if ( $checkbox['irreversible'] ) {
@@ -847,10 +857,6 @@ class UserrightsPage extends SpecialPage {
}
$checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
"wpGroup-" . $group, $checkbox['set'], $attr );
- $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] )
- ? Xml::tags( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
- : Xml::tags( 'div', [], $checkboxHtml )
- ) . "\n";
if ( $this->canProcessExpiries() ) {
$uiUser = $this->getUser();
@@ -874,6 +880,10 @@ class UserrightsPage extends SpecialPage {
} else {
$expiryHtml = $this->msg( 'userrights-expiry-none' )->text();
}
+ // T171345: Add a hidden form element so that other groups can still be manipulated,
+ // otherwise saving errors out with an invalid expiry time for this group.
+ $expiryHtml .= Html::Hidden( "wpExpiry-$group",
+ $currentExpiry ? 'existing' : 'infinite' );
$expiryHtml .= "<br />\n";
} else {
$expiryHtml = Xml::element( 'span', null,
@@ -920,7 +930,10 @@ class UserrightsPage extends SpecialPage {
$expiryHtml .= $expiryFormOptions->getHTML() . '<br />';
// Add custom expiry field
- $attribs = [ 'id' => "mw-input-wpExpiry-$group-other" ];
+ $attribs = [
+ 'id' => "mw-input-wpExpiry-$group-other",
+ 'class' => 'mw-userrights-expiryfield',
+ ];
if ( $checkbox['disabled-expiry'] ) {
$attribs['disabled'] = 'disabled';
}
@@ -939,8 +952,12 @@ class UserrightsPage extends SpecialPage {
'id' => "mw-userrights-nested-wpGroup-$group",
'class' => 'mw-userrights-nested',
];
- $ret .= "\t\t\t" . Xml::tags( 'div', $divAttribs, $expiryHtml ) . "\n";
+ $checkboxHtml .= "\t\t\t" . Xml::tags( 'div', $divAttribs, $expiryHtml ) . "\n";
}
+ $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] )
+ ? Xml::tags( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
+ : Xml::tags( 'div', [], $checkboxHtml )
+ ) . "\n";
}
$ret .= "\t</td>\n";
}
diff --git a/www/wiki/includes/specials/SpecialVersion.php b/www/wiki/includes/specials/SpecialVersion.php
index f176b407..6590756f 100644
--- a/www/wiki/includes/specials/SpecialVersion.php
+++ b/www/wiki/includes/specials/SpecialVersion.php
@@ -169,7 +169,9 @@ class SpecialVersion extends SpecialPage {
$ret .= '<div class="plainlinks">';
$ret .= "__NOTOC__
" . self::getCopyrightAndAuthorList() . "\n
- " . wfMessage( 'version-license-info' )->text();
+ " . '<div class="mw-version-license-info">' .
+ wfMessage( 'version-license-info' )->text() .
+ '</div>';
$ret .= '</div>';
return str_replace( "\t\t", '', $ret ) . "\n";
@@ -366,6 +368,7 @@ class SpecialVersion extends SpecialPage {
if ( self::$extensionTypes === false ) {
self::$extensionTypes = [
'specialpage' => wfMessage( 'version-specialpages' )->text(),
+ 'editor' => wfMessage( 'version-editors' )->text(),
'parserhook' => wfMessage( 'version-parserhooks' )->text(),
'variable' => wfMessage( 'version-variables' )->text(),
'media' => wfMessage( 'version-mediahandlers' )->text(),
diff --git a/www/wiki/includes/specials/SpecialWatchlist.php b/www/wiki/includes/specials/SpecialWatchlist.php
index 4f4570e3..c266a80e 100644
--- a/www/wiki/includes/specials/SpecialWatchlist.php
+++ b/www/wiki/includes/specials/SpecialWatchlist.php
@@ -22,7 +22,7 @@
*/
use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
@@ -33,6 +33,8 @@ use Wikimedia\Rdbms\IDatabase;
*/
class SpecialWatchlist extends ChangesListSpecialPage {
protected static $savedQueriesPreferenceName = 'rcfilters-wl-saved-queries';
+ protected static $daysPreferenceName = 'watchlistdays';
+ protected static $limitPreferenceName = 'wllimit';
private $maxDays;
@@ -101,7 +103,6 @@ class SpecialWatchlist extends ChangesListSpecialPage {
if ( $this->isStructuredFilterUiEnabled() ) {
$output->addModuleStyles( [ 'mediawiki.rcfilters.highlightCircles.seenunseen.styles' ] );
- $output->addJsConfigVars( 'wgStructuredChangeFiltersLiveUpdateSupported', false );
$output->addJsConfigVars(
'wgStructuredChangeFiltersEditWatchlistUrl',
SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL()
@@ -109,18 +110,13 @@ class SpecialWatchlist extends ChangesListSpecialPage {
}
}
- public function isStructuredFilterUiEnabled() {
- return $this->getRequest()->getBool( 'rcfilters' ) || (
- $this->getConfig()->get( 'StructuredChangeFiltersOnWatchlist' ) &&
- $this->getUser()->getOption( 'rcenhancedfilters' )
+ public static function checkStructuredFilterUiEnabled( Config $config, User $user ) {
+ return (
+ $config->get( 'StructuredChangeFiltersOnWatchlist' ) &&
+ $user->getOption( 'rcenhancedfilters' )
);
}
- public function isStructuredFilterUiEnabledByDefault() {
- return $this->getConfig()->get( 'StructuredChangeFiltersOnWatchlist' ) &&
- $this->getUser()->getDefaultOption( 'rcenhancedfilters' );
- }
-
/**
* Return an array of subpages that this special page will accept.
*
@@ -249,11 +245,31 @@ class SpecialWatchlist extends ChangesListSpecialPage {
$hideLiu = $registration->getFilter( 'hideliu' );
$hideLiu->setDefault( $user->getBoolOption( 'watchlisthideliu' ) );
+ // Selecting both hideanons and hideliu on watchlist preferances
+ // gives mutually exclusive filters, so those are ignored
+ if ( $user->getBoolOption( 'watchlisthideanons' ) &&
+ !$user->getBoolOption( 'watchlisthideliu' )
+ ) {
+ $this->getFilterGroup( 'userExpLevel' )
+ ->setDefault( 'registered' );
+ }
+
+ if ( $user->getBoolOption( 'watchlisthideliu' ) &&
+ !$user->getBoolOption( 'watchlisthideanons' )
+ ) {
+ $this->getFilterGroup( 'userExpLevel' )
+ ->setDefault( 'unregistered' );
+ }
+
$reviewStatus = $this->getFilterGroup( 'reviewStatus' );
if ( $reviewStatus !== null ) {
// Conditional on feature being available and rights
- $hidePatrolled = $reviewStatus->getFilter( 'hidepatrolled' );
- $hidePatrolled->setDefault( $user->getBoolOption( 'watchlisthidepatrolled' ) );
+ if ( $user->getBoolOption( 'watchlisthidepatrolled' ) ) {
+ $reviewStatus->setDefault( 'unpatrolled' );
+ $legacyReviewStatus = $this->getFilterGroup( 'legacyReviewStatus' );
+ $legacyHidePatrolled = $legacyReviewStatus->getFilter( 'hidepatrolled' );
+ $legacyHidePatrolled->setDefault( true );
+ }
}
$authorship = $this->getFilterGroup( 'authorship' );
@@ -269,26 +285,6 @@ class SpecialWatchlist extends ChangesListSpecialPage {
}
/**
- * Get a FormOptions object containing the default options
- *
- * @return FormOptions
- */
- public function getDefaultOptions() {
- $opts = parent::getDefaultOptions();
-
- $opts->add( 'days', $this->getDefaultDays(), FormOptions::FLOAT );
- $opts->add( 'limit', $this->getDefaultLimit(), FormOptions::INT );
-
- return $opts;
- }
-
- public function validateOptions( FormOptions $opts ) {
- $opts->validateBounds( 'days', 0, $this->maxDays );
- $opts->validateIntBounds( 'limit', 0, 5000 );
- parent::validateOptions( $opts );
- }
-
- /**
* Get all custom filters
*
* @return array Map of filter URL param names to properties (msg/default)
@@ -338,15 +334,8 @@ class SpecialWatchlist extends ChangesListSpecialPage {
// This is how we handle the fact that HTML forms don't submit
// unchecked boxes.
- foreach ( $this->filterGroups as $filterGroup ) {
- if ( $filterGroup instanceof ChangesListBooleanFilterGroup ) {
- /** @var ChangesListBooleanFilter $filter */
- foreach ( $filterGroup->getFilters() as $filter ) {
- if ( $filter->displaysOnUnstructuredUi() ) {
- $allBooleansFalse[$filter->getName()] = false;
- }
- }
- }
+ foreach ( $this->getLegacyShowHideFilters() as $filter ) {
+ $allBooleansFalse[ $filter->getName() ] = false;
}
$params = $params + $allBooleansFalse;
@@ -363,31 +352,15 @@ class SpecialWatchlist extends ChangesListSpecialPage {
/**
* @inheritDoc
*/
- protected function buildQuery( &$tables, &$fields, &$conds, &$query_options,
- &$join_conds, FormOptions $opts
- ) {
- $dbr = $this->getDB();
- parent::buildQuery( $tables, $fields, $conds, $query_options, $join_conds,
- $opts );
-
- // Calculate cutoff
- if ( $opts['days'] > 0 ) {
- $conds[] = 'rc_timestamp > ' .
- $dbr->addQuotes( $dbr->timestamp( time() - $opts['days'] * 3600 * 24 ) );
- }
- }
-
- /**
- * @inheritDoc
- */
protected function doMainQuery( $tables, $fields, $conds, $query_options,
$join_conds, FormOptions $opts
) {
$dbr = $this->getDB();
$user = $this->getUser();
- $tables = array_merge( [ 'recentchanges', 'watchlist' ], $tables );
- $fields = array_merge( RecentChange::selectFields(), $fields );
+ $rcQuery = RecentChange::getQueryInfo();
+ $tables = array_merge( $tables, $rcQuery['tables'], [ 'watchlist' ] );
+ $fields = array_merge( $rcQuery['fields'], $fields );
$join_conds = array_merge(
[
@@ -400,6 +373,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
],
],
],
+ $rcQuery['joins'],
$join_conds
);
@@ -507,7 +481,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
/**
* Build and output the actual changes list.
*
- * @param ResultWrapper $rows Database rows
+ * @param IResultWrapper $rows Database rows
* @param FormOptions $opts
*/
public function outputChangesList( $rows, $opts ) {
@@ -516,7 +490,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
$output = $this->getOutput();
# Show a message about replica DB lag, if applicable
- $lag = wfGetLB()->safeGetLag( $dbr );
+ $lag = MediaWikiServices::getInstance()->getDBLoadBalancer()->safeGetLag( $dbr );
if ( $lag > 0 ) {
$output->showLagWarning( $lag );
}
@@ -646,16 +620,14 @@ class SpecialWatchlist extends ChangesListSpecialPage {
}
$lang = $this->getLanguage();
- if ( $opts['days'] > 0 ) {
- $days = $opts['days'];
- } else {
- $days = $this->maxDays;
- }
$timestamp = wfTimestampNow();
$wlInfo = Html::rawElement(
'span',
- [ 'class' => 'wlinfo' ],
- $this->msg( 'wlnote' )->numParams( $numRows, round( $days * 24 ) )->params(
+ [
+ 'class' => 'wlinfo',
+ 'data-params' => json_encode( [ 'from' => $timestamp ] ),
+ ],
+ $this->msg( 'wlnote' )->numParams( $numRows, round( $opts['days'] * 24 ) )->params(
$lang->userDate( $timestamp, $user ), $lang->userTime( $timestamp, $user )
)->parse()
) . "<br />\n";
@@ -670,21 +642,15 @@ class SpecialWatchlist extends ChangesListSpecialPage {
# Spit out some control panel links
$links = [];
$namesOfDisplayedFilters = [];
- foreach ( $this->getFilterGroups() as $groupName => $group ) {
- if ( !$group->isPerGroupRequestParameter() ) {
- foreach ( $group->getFilters() as $filterName => $filter ) {
- if ( $filter->displaysOnUnstructuredUi( $this ) ) {
- $namesOfDisplayedFilters[] = $filterName;
- $links[] = $this->showHideCheck(
- $nondefaults,
- $filter->getShowHide(),
- $filterName,
- $opts[$filterName],
- $filter->isFeatureAvailableOnStructuredUi( $this )
- );
- }
- }
- }
+ foreach ( $this->getLegacyShowHideFilters() as $filterName => $filter ) {
+ $namesOfDisplayedFilters[] = $filterName;
+ $links[] = $this->showHideCheck(
+ $nondefaults,
+ $filter->getShowHide(),
+ $filterName,
+ $opts[ $filterName ],
+ $filter->isFeatureAvailableOnStructuredUi( $this )
+ );
}
$hiddenFields = $nondefaults;
@@ -785,45 +751,36 @@ class SpecialWatchlist extends ChangesListSpecialPage {
}
function cutoffselector( $options ) {
- // Cast everything to strings immediately, so that we know all of the values have the same
- // precision, and can be compared with '==='. 2/24 has a few more decimal places than its
- // default string representation, for example, and would confuse comparisons.
-
- // Misleadingly, the 'days' option supports hours too.
- $days = array_map( 'strval', [ 1 / 24, 2 / 24, 6 / 24, 12 / 24, 1, 3, 7 ] );
-
- $userWatchlistOption = (string)$this->getUser()->getOption( 'watchlistdays' );
- // add the user preference, if it isn't available already
- if ( !in_array( $userWatchlistOption, $days ) && $userWatchlistOption !== '0' ) {
- $days[] = $userWatchlistOption;
- }
-
- $maxDays = (string)$this->maxDays;
- // add the maximum possible value, if it isn't available already
- if ( !in_array( $maxDays, $days ) ) {
- $days[] = $maxDays;
- }
-
- $selected = (string)$options['days'];
+ $selected = (float)$options['days'];
if ( $selected <= 0 ) {
- $selected = $maxDays;
- }
-
- // add the currently selected value, if it isn't available already
- if ( !in_array( $selected, $days ) ) {
- $days[] = $selected;
- }
+ $selected = $this->maxDays;
+ }
+
+ $selectedHours = round( $selected * 24 );
+
+ $hours = array_unique( array_filter( [
+ 1,
+ 2,
+ 6,
+ 12,
+ 24,
+ 72,
+ 168,
+ 24 * (float)$this->getUser()->getOption( 'watchlistdays', 0 ),
+ 24 * $this->maxDays,
+ $selectedHours
+ ] ) );
+ asort( $hours );
- $select = new XmlSelect( 'days', 'days', $selected );
+ $select = new XmlSelect( 'days', 'days', (float)( $selectedHours / 24 ) );
- asort( $days );
- foreach ( $days as $value ) {
- if ( $value < 1 ) {
- $name = $this->msg( 'hours' )->numParams( $value * 24 )->text();
+ foreach ( $hours as $value ) {
+ if ( $value < 24 ) {
+ $name = $this->msg( 'hours' )->numParams( $value )->text();
} else {
- $name = $this->msg( 'days' )->numParams( $value )->text();
+ $name = $this->msg( 'days' )->numParams( $value / 24 )->text();
}
- $select->addOption( $name, $value );
+ $select->addOption( $name, (float)( $value / 24 ) );
}
return $select->getHTML() . "\n<br />\n";
@@ -890,11 +847,12 @@ class SpecialWatchlist extends ChangesListSpecialPage {
return Html::rawElement(
'span',
$attribs,
- Xml::checkLabel(
- $this->msg( $message, '' )->text(),
- $name,
- $name,
- (int)$value
+ // not using Html::checkLabel because that would escape the contents
+ Html::check( $name, (int)$value, [ 'id' => $name ] ) . Html::rawElement(
+ 'label',
+ $attribs + [ 'for' => $name ],
+ // <nowiki/> at beginning to avoid messages with "$1 ..." being parsed as pre tags
+ $this->msg( $message, '<nowiki/>' )->parse()
)
);
}
@@ -911,12 +869,4 @@ class SpecialWatchlist extends ChangesListSpecialPage {
$count = $store->countWatchedItems( $this->getUser() );
return floor( $count / 2 );
}
-
- function getDefaultLimit() {
- return $this->getUser()->getIntOption( 'wllimit' );
- }
-
- function getDefaultDays() {
- return floatval( $this->getUser()->getOption( 'watchlistdays' ) );
- }
}
diff --git a/www/wiki/includes/specials/SpecialWhatlinkshere.php b/www/wiki/includes/specials/SpecialWhatlinkshere.php
index 6f91c46f..3080fbfe 100644
--- a/www/wiki/includes/specials/SpecialWhatlinkshere.php
+++ b/www/wiki/includes/specials/SpecialWhatlinkshere.php
@@ -162,7 +162,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage {
];
$on['rd_namespace'] = $target->getNamespace();
// Inner LIMIT is 2X in case of stale backlinks with wrong namespaces
- $subQuery = $dbr->selectSQLText(
+ $subQuery = $dbr->buildSelectSubquery(
[ $table, 'redirect', 'page' ],
[ $fromCol, 'rd_from' ],
$conds[$table],
@@ -175,7 +175,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage {
]
);
return $dbr->select(
- [ 'page', 'temp_backlink_range' => "($subQuery)" ],
+ [ 'page', 'temp_backlink_range' => $subQuery ],
[ 'page_id', 'page_namespace', 'page_title', 'rd_from', 'page_is_redirect' ],
[],
__CLASS__ . '::showIndirectLinks',
diff --git a/www/wiki/includes/specials/formfields/Licenses.php b/www/wiki/includes/specials/formfields/Licenses.php
index f499cc16..931cd240 100644
--- a/www/wiki/includes/specials/formfields/Licenses.php
+++ b/www/wiki/includes/specials/formfields/Licenses.php
@@ -21,7 +21,6 @@
* @ingroup SpecialPage
* @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
* @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
*/
/**
@@ -32,10 +31,13 @@ class Licenses extends HTMLFormField {
protected $msg;
/** @var array */
- protected $licenses = [];
+ protected $lines = [];
/** @var string */
protected $html;
+
+ /** @var string|null */
+ protected $selected;
/**#@-*/
/**
@@ -44,18 +46,34 @@ class Licenses extends HTMLFormField {
public function __construct( $params ) {
parent::__construct( $params );
- $this->msg = empty( $params['licenses'] )
+ $this->msg = static::getMessageFromParams( $params );
+ $this->selected = null;
+
+ $this->makeLines();
+ }
+
+ /**
+ * @param array $params
+ * @return string
+ */
+ protected static function getMessageFromParams( $params ) {
+ return empty( $params['licenses'] )
? wfMessage( 'licenses' )->inContentLanguage()->plain()
: $params['licenses'];
- $this->selected = null;
+ }
- $this->makeLicenses();
+ /**
+ * @param string $line
+ * @return License
+ */
+ protected function buildLine( $line ) {
+ return new License( $line );
}
/**
* @private
*/
- protected function makeLicenses() {
+ protected function makeLines() {
$levels = [];
$lines = explode( "\n", $this->msg );
@@ -66,8 +84,8 @@ class Licenses extends HTMLFormField {
list( $level, $line ) = $this->trimStars( $line );
if ( strpos( $line, '|' ) !== false ) {
- $obj = new License( $line );
- $this->stackItem( $this->licenses, $levels, $obj );
+ $obj = $this->buildLine( $line );
+ $this->stackItem( $this->lines, $levels, $obj );
} else {
if ( $level < count( $levels ) ) {
$levels = array_slice( $levels, 0, $level );
@@ -109,11 +127,14 @@ class Licenses extends HTMLFormField {
/**
* @param array $tagset
* @param int $depth
+ * @return string
*/
protected function makeHtml( $tagset, $depth = 0 ) {
+ $html = '';
+
foreach ( $tagset as $key => $val ) {
if ( is_array( $val ) ) {
- $this->html .= $this->outputOption(
+ $html .= $this->outputOption(
$key, '',
[
'disabled' => 'disabled',
@@ -121,15 +142,17 @@ class Licenses extends HTMLFormField {
],
$depth
);
- $this->makeHtml( $val, $depth + 1 );
+ $html .= $this->makeHtml( $val, $depth + 1 );
} else {
- $this->html .= $this->outputOption(
+ $html .= $this->outputOption(
$val->text, $val->template,
[ 'title' => '{{' . $val->template . '}}' ],
$depth
);
}
}
+
+ return $html;
}
/**
@@ -154,36 +177,50 @@ class Licenses extends HTMLFormField {
/**#@-*/
/**
- * Accessor for $this->licenses
+ * Accessor for $this->lines
*
* @return array
*/
- public function getLicenses() {
- return $this->licenses;
+ public function getLines() {
+ return $this->lines;
}
/**
- * Accessor for $this->html
+ * Accessor for $this->lines
*
- * @param bool $value
+ * @return array
*
- * @return string
+ * @deprecated since 1.31 Use getLines() instead
+ */
+ public function getLicenses() {
+ return $this->getLines();
+ }
+
+ /**
+ * {@inheritdoc}
*/
public function getInputHTML( $value ) {
$this->selected = $value;
- $this->html = $this->outputOption( wfMessage( 'nolicense' )->text(), '',
- (bool)$this->selected ? null : [ 'selected' => 'selected' ] );
- $this->makeHtml( $this->getLicenses() );
+ // add a default "no license selected" option
+ $default = $this->buildLine( '|nolicense' );
+ array_unshift( $this->lines, $default );
+
+ $html = $this->makeHtml( $this->getLines() );
$attribs = [
'name' => $this->mName,
'id' => $this->mID
];
if ( !empty( $this->mParams['disabled'] ) ) {
- $attibs['disabled'] = 'disabled';
+ $attribs['disabled'] = 'disabled';
}
- return Html::rawElement( 'select', $attribs, $this->html );
+ $html = Html::rawElement( 'select', $attribs, $html );
+
+ // remove default "no license selected" from lines again
+ array_shift( $this->lines );
+
+ return $html;
}
}
diff --git a/www/wiki/includes/specials/forms/UploadForm.php b/www/wiki/includes/specials/forms/UploadForm.php
index 44d91a8a..eacdace1 100644
--- a/www/wiki/includes/specials/forms/UploadForm.php
+++ b/www/wiki/includes/specials/forms/UploadForm.php
@@ -159,7 +159,7 @@ class UploadForm extends HTMLForm {
}
$descriptor['UploadFile'] = [
- 'class' => 'UploadSourceField',
+ 'class' => UploadSourceField::class,
'section' => 'source',
'type' => 'file',
'id' => 'wpUploadFile',
@@ -174,7 +174,7 @@ class UploadForm extends HTMLForm {
if ( $canUploadByUrl ) {
$this->mMaxUploadSize['url'] = UploadBase::getMaxUploadSize( 'url' );
$descriptor['UploadFileURL'] = [
- 'class' => 'UploadSourceField',
+ 'class' => UploadSourceField::class,
'section' => 'source',
'id' => 'wpUploadFileURL',
'radio-id' => 'wpSourceTypeurl',
@@ -322,7 +322,7 @@ class UploadForm extends HTMLForm {
} else {
$descriptor['License'] = [
'type' => 'select',
- 'class' => 'Licenses',
+ 'class' => Licenses::class,
'section' => 'description',
'id' => 'wpLicense',
'label-message' => 'license',
diff --git a/www/wiki/includes/specials/helpers/License.php b/www/wiki/includes/specials/helpers/License.php
index 4f94b4d2..940f69c7 100644
--- a/www/wiki/includes/specials/helpers/License.php
+++ b/www/wiki/includes/specials/helpers/License.php
@@ -35,12 +35,27 @@ class License {
public $text;
/**
- * @param string $str License name??
+ * @param string $str
*/
- function __construct( $str ) {
- list( $text, $template ) = explode( '|', strrev( $str ), 2 );
+ public function __construct( $str ) {
+ $str = $this->parse( $str );
+ list( $this->template, $this->text ) = $this->split( $str );
+ }
- $this->template = strrev( $template );
- $this->text = strrev( $text );
+ /**
+ * @param string $str
+ * @return string
+ */
+ protected function parse( $str ) {
+ return $str;
+ }
+
+ /**
+ * @param string $str
+ * @return string[] Array with [template, text]
+ */
+ protected function split( $str ) {
+ list( $text, $template ) = explode( '|', strrev( $str ), 2 );
+ return [ strrev( $template ), strrev( $text ) ];
}
}
diff --git a/www/wiki/includes/specials/pagers/ActiveUsersPager.php b/www/wiki/includes/specials/pagers/ActiveUsersPager.php
index 64af71a1..83fb8493 100644
--- a/www/wiki/includes/specials/pagers/ActiveUsersPager.php
+++ b/www/wiki/includes/specials/pagers/ActiveUsersPager.php
@@ -79,14 +79,18 @@ class ActiveUsersPager extends UsersPager {
function getQueryInfo() {
$dbr = $this->getDatabase();
+ $rcQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
+
$activeUserSeconds = $this->getConfig()->get( 'ActiveUserDays' ) * 86400;
$timestamp = $dbr->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds );
- $tables = [ 'querycachetwo', 'user', 'recentchanges' ];
+ $tables = [ 'querycachetwo', 'user', 'rc' => [ 'recentchanges' ] + $rcQuery['tables'] ];
+ $jconds = [
+ 'user' => [ 'JOIN', 'user_name = qcc_title' ],
+ 'rc' => [ 'JOIN', $rcQuery['fields']['rc_user_text'] . ' = qcc_title' ],
+ ] + $rcQuery['joins'];
$conds = [
'qcc_type' => 'activeusers',
'qcc_namespace' => NS_USER,
- 'user_name = qcc_title',
- 'rc_user_text = qcc_title',
'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Don't count wikidata.
'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ), // Don't count categorization changes.
'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ),
@@ -97,7 +101,7 @@ class ActiveUsersPager extends UsersPager {
}
if ( $this->groups !== [] ) {
$tables[] = 'user_groups';
- $conds[] = 'ug_user = user_id';
+ $jconds['user_groups'] = [ 'JOIN', [ 'ug_user = user_id' ] ];
$conds['ug_group'] = $this->groups;
$conds[] = 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() );
}
@@ -127,7 +131,8 @@ class ActiveUsersPager extends UsersPager {
'recentedits' => 'COUNT(*)'
],
'options' => [ 'GROUP BY' => [ 'qcc_title' ] ],
- 'conds' => $conds
+ 'conds' => $conds,
+ 'join_conds' => $jconds,
];
}
diff --git a/www/wiki/includes/specials/pagers/BlockListPager.php b/www/wiki/includes/specials/pagers/BlockListPager.php
index 924fd06c..5789c283 100644
--- a/www/wiki/includes/specials/pagers/BlockListPager.php
+++ b/www/wiki/includes/specials/pagers/BlockListPager.php
@@ -23,7 +23,7 @@
* @ingroup Pager
*/
use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
class BlockListPager extends TablePager {
@@ -173,7 +173,7 @@ class BlockListPager extends TablePager {
break;
case 'ipb_reason':
- $value = CommentStore::newKey( 'ipb_reason' )->getComment( $row )->text;
+ $value = CommentStore::getStore()->getComment( 'ipb_reason', $row )->text;
$formatted = Linker::formatComment( $value );
break;
@@ -209,16 +209,17 @@ class BlockListPager extends TablePager {
}
function getQueryInfo() {
- $commentQuery = CommentStore::newKey( 'ipb_reason' )->getJoin();
+ $commentQuery = CommentStore::getStore()->getJoin( 'ipb_reason' );
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' );
$info = [
- 'tables' => [ 'ipblocks', 'user' ] + $commentQuery['tables'],
+ 'tables' => array_merge(
+ [ 'ipblocks' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ]
+ ),
'fields' => [
'ipb_id',
'ipb_address',
'ipb_user',
- 'ipb_by',
- 'ipb_by_text',
'by_user_name' => 'user_name',
'ipb_timestamp',
'ipb_auto',
@@ -231,9 +232,11 @@ class BlockListPager extends TablePager {
'ipb_deleted',
'ipb_block_email',
'ipb_allow_usertalk',
- ] + $commentQuery['fields'],
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
'conds' => $this->conds,
- 'join_conds' => [ 'user' => [ 'LEFT JOIN', 'user_id = ipb_by' ] ] + $commentQuery['joins']
+ 'join_conds' => [
+ 'user' => [ 'LEFT JOIN', 'user_id = ' . $actorQuery['fields']['ipb_by'] ]
+ ] + $commentQuery['joins'] + $actorQuery['joins']
];
# Filter out any expired blocks
@@ -286,7 +289,7 @@ class BlockListPager extends TablePager {
/**
* Do a LinkBatch query to minimise database load when generating all these links
- * @param ResultWrapper $result
+ * @param IResultWrapper $result
*/
function preprocessResults( $result ) {
# Do a link batch query
diff --git a/www/wiki/includes/specials/pagers/ContribsPager.php b/www/wiki/includes/specials/pagers/ContribsPager.php
index 979460cf..e31498ac 100644
--- a/www/wiki/includes/specials/pagers/ContribsPager.php
+++ b/www/wiki/includes/specials/pagers/ContribsPager.php
@@ -24,7 +24,7 @@
* @ingroup Pager
*/
use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
@@ -113,7 +113,7 @@ class ContribsPager extends RangeChronologicalPager {
* @param string $offset Index offset, inclusive
* @param int $limit Exact query limit
* @param bool $descending Query direction, false for ascending, true for descending
- * @return ResultWrapper
+ * @return IResultWrapper
*/
function reallyDoQuery( $offset, $limit, $descending ) {
list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo(
@@ -175,81 +175,27 @@ class ContribsPager extends RangeChronologicalPager {
}
function getQueryInfo() {
- list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond();
-
- $user = $this->getUser();
- $conds = array_merge( $userCond, $this->getNamespaceCond() );
-
- // Paranoia: avoid brute force searches (T19342)
- if ( !$user->isAllowed( 'deletedhistory' ) ) {
- $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0';
- } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
- $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::SUPPRESSED_USER ) .
- ' != ' . Revision::SUPPRESSED_USER;
- }
-
- # Don't include orphaned revisions
- $join_cond['page'] = Revision::pageJoinCond();
- # Get the current user name for accounts
- $join_cond['user'] = Revision::userJoinCond();
-
- $options = [];
- if ( $index ) {
- $options['USE INDEX'] = [ 'revision' => $index ];
- }
-
+ $revQuery = Revision::getQueryInfo( [ 'page', 'user' ] );
$queryInfo = [
- 'tables' => $tables,
- 'fields' => array_merge(
- Revision::selectFields(),
- Revision::selectUserFields(),
- [ 'page_namespace', 'page_title', 'page_is_new',
- 'page_latest', 'page_is_redirect', 'page_len' ]
- ),
- 'conds' => $conds,
- 'options' => $options,
- 'join_conds' => $join_cond
+ 'tables' => $revQuery['tables'],
+ 'fields' => array_merge( $revQuery['fields'], [ 'page_is_new' ] ),
+ 'conds' => [],
+ 'options' => [],
+ 'join_conds' => $revQuery['joins'],
];
- // For IPv6, we use ipc_rev_timestamp on ip_changes as the index field,
- // which will be referenced when parsing the results of a query.
- if ( self::isQueryableRange( $this->target ) ) {
- $queryInfo['fields'][] = 'ipc_rev_timestamp';
- }
-
- ChangeTags::modifyDisplayQuery(
- $queryInfo['tables'],
- $queryInfo['fields'],
- $queryInfo['conds'],
- $queryInfo['join_conds'],
- $queryInfo['options'],
- $this->tagFilter
- );
-
- // Avoid PHP 7.1 warning from passing $this by reference
- $pager = $this;
- Hooks::run( 'ContribsPager::getQueryInfo', [ &$pager, &$queryInfo ] );
-
- return $queryInfo;
- }
-
- function getUserCond() {
- $condition = [];
- $join_conds = [];
- $tables = [ 'revision', 'page', 'user' ];
- $index = false;
if ( $this->contribs == 'newbie' ) {
- $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
- $condition[] = 'rev_user >' . (int)( $max - $max / 100 );
+ $max = $this->mDb->selectField( 'user', 'max(user_id)', '', __METHOD__ );
+ $queryInfo['conds'][] = $revQuery['fields']['rev_user'] . ' >' . (int)( $max - $max / 100 );
# ignore local groups with the bot right
# @todo FIXME: Global groups may have 'bot' rights
$groupsWithBotPermission = User::getGroupsWithPermission( 'bot' );
if ( count( $groupsWithBotPermission ) ) {
- $tables[] = 'user_groups';
- $condition[] = 'ug_group IS NULL';
- $join_conds['user_groups'] = [
+ $queryInfo['tables'][] = 'user_groups';
+ $queryInfo['conds'][] = 'ug_group IS NULL';
+ $queryInfo['join_conds']['user_groups'] = [
'LEFT JOIN', [
- 'ug_user = rev_user',
+ 'ug_user = ' . $revQuery['fields']['rev_user'],
'ug_group' => $groupsWithBotPermission,
'ug_expiry IS NULL OR ug_expiry >= ' .
$this->mDb->addQuotes( $this->mDb->timestamp() )
@@ -259,46 +205,81 @@ class ContribsPager extends RangeChronologicalPager {
// (T140537) Disallow looking too far in the past for 'newbies' queries. If the user requested
// a timestamp offset far in the past such that there are no edits by users with user_ids in
// the range, we would end up scanning all revisions from that offset until start of time.
- $condition[] = 'rev_timestamp > ' .
+ $queryInfo['conds'][] = 'rev_timestamp > ' .
$this->mDb->addQuotes( $this->mDb->timestamp( wfTimestamp() - 30 * 24 * 60 * 60 ) );
} else {
- $uid = User::idFromName( $this->target );
- if ( $uid ) {
- $condition['rev_user'] = $uid;
- $index = 'user_timestamp';
+ $user = User::newFromName( $this->target, false );
+ $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) : null;
+ if ( $ipRangeConds ) {
+ $queryInfo['tables'][] = 'ip_changes';
+ $queryInfo['join_conds']['ip_changes'] = [
+ 'LEFT JOIN', [ 'ipc_rev_id = rev_id' ]
+ ];
+ $queryInfo['conds'][] = $ipRangeConds;
} else {
- $ipRangeConds = $this->getIpRangeConds( $this->mDb, $this->target );
-
- if ( $ipRangeConds ) {
- $tables[] = 'ip_changes';
- $join_conds['ip_changes'] = [
- 'LEFT JOIN', [ 'ipc_rev_id = rev_id' ]
- ];
- $condition[] = $ipRangeConds;
- } else {
- $condition['rev_user_text'] = $this->target;
- $index = 'usertext_timestamp';
+ // tables and joins are already handled by Revision::getQueryInfo()
+ $conds = ActorMigration::newMigration()->getWhere( $this->mDb, 'rev_user', $user );
+ $queryInfo['conds'][] = $conds['conds'];
+ // Force the appropriate index to avoid bad query plans (T189026)
+ if ( count( $conds['orconds'] ) === 1 ) {
+ if ( isset( $conds['orconds']['actor'] ) ) {
+ // @todo: This will need changing when revision_comment_temp goes away
+ $queryInfo['options']['USE INDEX']['temp_rev_user'] = 'actor_timestamp';
+ } else {
+ $queryInfo['options']['USE INDEX']['revision'] =
+ isset( $conds['orconds']['userid'] ) ? 'user_timestamp' : 'usertext_timestamp';
+ }
}
}
}
if ( $this->deletedOnly ) {
- $condition[] = 'rev_deleted != 0';
+ $queryInfo['conds'][] = 'rev_deleted != 0';
}
if ( $this->topOnly ) {
- $condition[] = 'rev_id = page_latest';
+ $queryInfo['conds'][] = 'rev_id = page_latest';
}
if ( $this->newOnly ) {
- $condition[] = 'rev_parent_id = 0';
+ $queryInfo['conds'][] = 'rev_parent_id = 0';
}
if ( $this->hideMinor ) {
- $condition[] = 'rev_minor_edit = 0';
+ $queryInfo['conds'][] = 'rev_minor_edit = 0';
+ }
+
+ $user = $this->getUser();
+ $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() );
+
+ // Paranoia: avoid brute force searches (T19342)
+ if ( !$user->isAllowed( 'deletedhistory' ) ) {
+ $queryInfo['conds'][] = $this->mDb->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0';
+ } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+ $queryInfo['conds'][] = $this->mDb->bitAnd( 'rev_deleted', Revision::SUPPRESSED_USER ) .
+ ' != ' . Revision::SUPPRESSED_USER;
}
- return [ $tables, $index, $condition, $join_conds ];
+ // For IPv6, we use ipc_rev_timestamp on ip_changes as the index field,
+ // which will be referenced when parsing the results of a query.
+ if ( self::isQueryableRange( $this->target ) ) {
+ $queryInfo['fields'][] = 'ipc_rev_timestamp';
+ }
+
+ ChangeTags::modifyDisplayQuery(
+ $queryInfo['tables'],
+ $queryInfo['fields'],
+ $queryInfo['conds'],
+ $queryInfo['join_conds'],
+ $queryInfo['options'],
+ $this->tagFilter
+ );
+
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $pager = $this;
+ Hooks::run( 'ContribsPager::getQueryInfo', [ &$pager, &$queryInfo ] );
+
+ return $queryInfo;
}
function getNamespaceCond() {
@@ -451,14 +432,14 @@ class ContribsPager extends RangeChronologicalPager {
* we're definitely dealing with revision data and we may proceed, if not, we'll leave it
* to extensions to subscribe to the hook to parse the row.
*/
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
try {
$rev = new Revision( $row );
$validRevision = (bool)$rev->getId();
} catch ( Exception $e ) {
$validRevision = false;
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $validRevision ) {
$attribs['data-mw-revid'] = $rev->getId();
@@ -658,7 +639,7 @@ class ContribsPager extends RangeChronologicalPager {
* @param array $opts Options array
* @return array Options array with processed start and end date filter options
*/
- public static function processDateFilter( $opts ) {
+ public static function processDateFilter( array $opts ) {
$start = isset( $opts['start'] ) ? $opts['start'] : '';
$end = isset( $opts['end'] ) ? $opts['end'] : '';
$year = isset( $opts['year'] ) ? $opts['year'] : '';
diff --git a/www/wiki/includes/specials/pagers/DeletedContribsPager.php b/www/wiki/includes/specials/pagers/DeletedContribsPager.php
index 38a332e6..f3de64d6 100644
--- a/www/wiki/includes/specials/pagers/DeletedContribsPager.php
+++ b/www/wiki/includes/specials/pagers/DeletedContribsPager.php
@@ -23,7 +23,7 @@
* @ingroup Pager
*/
use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\FakeResultWrapper;
class DeletedContribsPager extends IndexPager {
@@ -58,7 +58,12 @@ class DeletedContribsPager extends IndexPager {
}
function getQueryInfo() {
- list( $index, $userCond ) = $this->getUserCond();
+ $userCond = [
+ // ->getJoin() below takes care of any joins needed
+ ActorMigration::newMigration()->getWhere(
+ wfGetDB( DB_REPLICA ), 'ar_user', User::newFromName( $this->target, false ), false
+ )['conds']
+ ];
$conds = array_merge( $userCond, $this->getNamespaceCond() );
$user = $this->getUser();
// Paranoia: avoid brute force searches (T19792)
@@ -69,17 +74,18 @@ class DeletedContribsPager extends IndexPager {
' != ' . Revision::SUPPRESSED_USER;
}
- $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
+ $commentQuery = CommentStore::getStore()->getJoin( 'ar_comment' );
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'ar_user' );
return [
- 'tables' => [ 'archive' ] + $commentQuery['tables'],
+ 'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
'fields' => [
'ar_rev_id', 'ar_namespace', 'ar_title', 'ar_timestamp',
- 'ar_minor_edit', 'ar_user', 'ar_user_text', 'ar_deleted'
- ] + $commentQuery['fields'],
+ 'ar_minor_edit', 'ar_deleted'
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
'conds' => $conds,
- 'options' => [ 'USE INDEX' => [ 'archive' => $index ] ],
- 'join_conds' => $commentQuery['joins'],
+ 'options' => [],
+ 'join_conds' => $commentQuery['joins'] + $actorQuery['joins'],
];
}
@@ -90,7 +96,7 @@ class DeletedContribsPager extends IndexPager {
* @param string $offset Index offset, inclusive
* @param int $limit Exact query limit
* @param bool $descending Query direction, false for ascending, true for descending
- * @return ResultWrapper
+ * @return IResultWrapper
*/
function reallyDoQuery( $offset, $limit, $descending ) {
$data = [ parent::reallyDoQuery( $offset, $limit, $descending ) ];
@@ -128,15 +134,6 @@ class DeletedContribsPager extends IndexPager {
return new FakeResultWrapper( $result );
}
- function getUserCond() {
- $condition = [];
-
- $condition['ar_user_text'] = $this->target;
- $index = 'ar_usertext_timestamp';
-
- return [ $index, $condition ];
- }
-
function getIndexField() {
return 'ar_timestamp';
}
@@ -207,14 +204,14 @@ class DeletedContribsPager extends IndexPager {
* we're definitely dealing with revision data and we may proceed, if not, we'll leave it
* to extensions to subscribe to the hook to parse the row.
*/
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
try {
$rev = Revision::newFromArchiveRow( $row );
$validRevision = (bool)$rev->getId();
} catch ( Exception $e ) {
$validRevision = false;
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $validRevision ) {
$attribs['data-mw-revid'] = $rev->getId();
@@ -256,9 +253,10 @@ class DeletedContribsPager extends IndexPager {
$rev = new Revision( [
'title' => $page,
'id' => $row->ar_rev_id,
- 'comment' => CommentStore::newKey( 'ar_comment' )->getComment( $row )->text,
+ 'comment' => CommentStore::getStore()->getComment( 'ar_comment', $row )->text,
'user' => $row->ar_user,
'user_text' => $row->ar_user_text,
+ 'actor' => isset( $row->ar_actor ) ? $row->ar_actor : null,
'timestamp' => $row->ar_timestamp,
'minor_edit' => $row->ar_minor_edit,
'deleted' => $row->ar_deleted,
diff --git a/www/wiki/includes/specials/pagers/ImageListPager.php b/www/wiki/includes/specials/pagers/ImageListPager.php
index 008573be..bb4f0b34 100644
--- a/www/wiki/includes/specials/pagers/ImageListPager.php
+++ b/www/wiki/includes/specials/pagers/ImageListPager.php
@@ -23,7 +23,7 @@
* @ingroup Pager
*/
use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\FakeResultWrapper;
class ImageListPager extends TablePager {
@@ -134,7 +134,14 @@ class ImageListPager extends TablePager {
$conds = [];
if ( !is_null( $this->mUserName ) ) {
- $conds[$prefix . '_user_text'] = $this->mUserName;
+ // getQueryInfoReal() should have handled the tables and joins.
+ $dbr = wfGetDB( DB_REPLICA );
+ $actorWhere = ActorMigration::newMigration()->getWhere(
+ $dbr,
+ $prefix . '_user',
+ User::newFromName( $this->mUserName, false )
+ );
+ $conds[] = $actorWhere['conds'];
}
if ( $this->mSearch !== '' ) {
@@ -193,9 +200,9 @@ class ImageListPager extends TablePager {
}
$sortable = [ 'img_timestamp', 'img_name', 'img_size' ];
/* For reference, the indicies we can use for sorting are:
- * On the image table: img_user_timestamp, img_usertext_timestamp,
+ * On the image table: img_user_timestamp/img_usertext_timestamp/img_actor_timestamp,
* img_size, img_timestamp
- * On oldimage: oi_usertext_timestamp, oi_name_timestamp
+ * On oldimage: oi_usertext_timestamp/oi_actor_timestamp, oi_name_timestamp
*
* In particular that means we cannot sort by timestamp when not filtering
* by user and including old images in the results. Which is sad.
@@ -246,6 +253,7 @@ class ImageListPager extends TablePager {
$tables = [ $table ];
$fields = $this->getFieldNames();
unset( $fields['img_description'] );
+ unset( $fields['img_user_text'] );
$fields = array_keys( $fields );
if ( $table === 'oldimage' ) {
@@ -261,18 +269,25 @@ class ImageListPager extends TablePager {
$fields[array_search( 'top', $fields )] = "'yes' AS top";
}
}
- $fields[] = $prefix . '_user AS img_user';
$fields[array_search( 'thumb', $fields )] = $prefix . '_name AS thumb';
$options = $join_conds = [];
# Description field
- $commentQuery = CommentStore::newKey( $prefix . '_description' )->getJoin();
+ $commentQuery = CommentStore::getStore()->getJoin( $prefix . '_description' );
$tables += $commentQuery['tables'];
$fields += $commentQuery['fields'];
$join_conds += $commentQuery['joins'];
$fields['description_field'] = "'{$prefix}_description'";
+ # User fields
+ $actorQuery = ActorMigration::newMigration()->getJoin( $prefix . '_user' );
+ $tables += $actorQuery['tables'];
+ $join_conds += $actorQuery['joins'];
+ $fields['img_user'] = $actorQuery['fields'][$prefix . '_user'];
+ $fields['img_user_text'] = $actorQuery['fields'][$prefix . '_user_text'];
+ $fields['img_actor'] = $actorQuery['fields'][$prefix . '_actor'];
+
# Depends on $wgMiserMode
# Will also not happen if mShowAll is true.
if ( isset( $this->mFieldNames['count'] ) ) {
@@ -287,7 +302,7 @@ class ImageListPager extends TablePager {
unset( $field );
$columnlist = preg_grep( '/^img/', array_keys( $this->getFieldNames() ) );
- $options = [ 'GROUP BY' => array_merge( [ 'img_user' ], $columnlist ) ];
+ $options = [ 'GROUP BY' => array_merge( [ $fields['img_user'] ], $columnlist ) ];
$join_conds['oldimage'] = [ 'LEFT JOIN', 'oi_name = img_name' ];
}
@@ -348,8 +363,8 @@ class ImageListPager extends TablePager {
*
* Note: This will throw away some results
*
- * @param ResultWrapper $res1
- * @param ResultWrapper $res2
+ * @param IResultWrapper $res1
+ * @param IResultWrapper $res2
* @param int $limit
* @param bool $ascending See note about $asc in $this->reallyDoQuery
* @return FakeResultWrapper $res1 and $res2 combined
@@ -380,16 +395,12 @@ class ImageListPager extends TablePager {
}
}
- // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
for ( ; $i < $limit && $topRes1; $i++ ) {
- // @codingStandardsIgnoreEnd
$resultArray[] = $topRes1;
$topRes1 = $res1->next();
}
- // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
for ( ; $i < $limit && $topRes2; $i++ ) {
- // @codingStandardsIgnoreEnd
$resultArray[] = $topRes2;
$topRes2 = $res2->next();
}
@@ -502,7 +513,7 @@ class ImageListPager extends TablePager {
return htmlspecialchars( $this->getLanguage()->formatSize( $value ) );
case 'img_description':
$field = $this->mCurrentRow->description_field;
- $value = CommentStore::newKey( $field )->getComment( $this->mCurrentRow )->text;
+ $value = CommentStore::getStore()->getComment( $field, $this->mCurrentRow )->text;
return Linker::formatComment( $value );
case 'count':
return $this->getLanguage()->formatNum( intval( $value ) + 1 );
diff --git a/www/wiki/includes/specials/pagers/MergeHistoryPager.php b/www/wiki/includes/specials/pagers/MergeHistoryPager.php
index bbf97e13..6a8f7da7 100644
--- a/www/wiki/includes/specials/pagers/MergeHistoryPager.php
+++ b/www/wiki/includes/specials/pagers/MergeHistoryPager.php
@@ -85,13 +85,12 @@ class MergeHistoryPager extends ReverseChronologicalPager {
$conds['rev_page'] = $this->articleID;
$conds[] = "rev_timestamp < " . $this->mDb->addQuotes( $this->maxTimestamp );
+ $revQuery = Revision::getQueryInfo( [ 'page', 'user' ] );
return [
- 'tables' => [ 'revision', 'page', 'user' ],
- 'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ),
+ 'tables' => $revQuery['tables'],
+ 'fields' => $revQuery['fields'],
'conds' => $conds,
- 'join_conds' => [
- 'page' => Revision::pageJoinCond(),
- 'user' => Revision::userJoinCond() ]
+ 'join_conds' => $revQuery['joins']
];
}
diff --git a/www/wiki/includes/specials/pagers/NewFilesPager.php b/www/wiki/includes/specials/pagers/NewFilesPager.php
index 001c296d..c214f1f7 100644
--- a/www/wiki/includes/specials/pagers/NewFilesPager.php
+++ b/www/wiki/includes/specials/pagers/NewFilesPager.php
@@ -59,26 +59,24 @@ class NewFilesPager extends RangeChronologicalPager {
function getQueryInfo() {
$opts = $this->opts;
- $conds = $jconds = [];
- $tables = [ 'image' ];
- $fields = [ 'img_name', 'img_user', 'img_timestamp' ];
+ $conds = [];
+ $imgQuery = LocalFile::getQueryInfo();
+ $tables = $imgQuery['tables'];
+ $fields = [ 'img_name', 'img_timestamp' ] + $imgQuery['fields'];
$options = [];
+ $jconds = $imgQuery['joins'];
$user = $opts->getValue( 'user' );
if ( $user !== '' ) {
- $userId = User::idFromName( $user );
- if ( $userId ) {
- $conds['img_user'] = $userId;
- } else {
- $conds['img_user_text'] = $user;
- }
+ $conds[] = ActorMigration::newMigration()
+ ->getWhere( wfGetDB( DB_REPLICA ), 'img_user', User::newFromName( $user, false ) )['conds'];
}
if ( $opts->getValue( 'newbies' ) ) {
// newbie = most recent 1% of users
$dbr = wfGetDB( DB_REPLICA );
- $max = $dbr->selectField( 'user', 'max(user_id)', false, __METHOD__ );
- $conds[] = 'img_user >' . (int)( $max - $max / 100 );
+ $max = $dbr->selectField( 'user', 'max(user_id)', '', __METHOD__ );
+ $conds[] = $imgQuery['fields']['img_user'] . ' >' . (int)( $max - $max / 100 );
// there's no point in looking for new user activity in a far past;
// beyond a certain point, we'd just end up scanning the rest of the
@@ -99,7 +97,7 @@ class NewFilesPager extends RangeChronologicalPager {
'LEFT JOIN',
[
'ug_group' => $groupsWithBotPermission,
- 'ug_user = img_user',
+ 'ug_user = ' . $imgQuery['fields']['img_user'],
'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
]
];
@@ -107,16 +105,27 @@ class NewFilesPager extends RangeChronologicalPager {
}
if ( $opts->getValue( 'hidepatrolled' ) ) {
+ global $wgActorTableSchemaMigrationStage;
+
$tables[] = 'recentchanges';
$conds['rc_type'] = RC_LOG;
$conds['rc_log_type'] = 'upload';
- $conds['rc_patrolled'] = 0;
+ $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
$conds['rc_namespace'] = NS_FILE;
+
+ if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+ $jcond = 'rc_actor = ' . $imgQuery['fields']['img_actor'];
+ } else {
+ $rcQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
+ $tables += $rcQuery['tables'];
+ $jconds += $rcQuery['joins'];
+ $jcond = $rcQuery['fields']['rc_user'] . ' = ' . $imgQuery['fields']['img_user'];
+ }
$jconds['recentchanges'] = [
'INNER JOIN',
[
'rc_title = img_name',
- 'rc_user = img_user',
+ $jcond,
'rc_timestamp = img_timestamp'
]
];
diff --git a/www/wiki/includes/specials/pagers/NewPagesPager.php b/www/wiki/includes/specials/pagers/NewPagesPager.php
index 53362d9c..f16a5cb6 100644
--- a/www/wiki/includes/specials/pagers/NewPagesPager.php
+++ b/www/wiki/includes/specials/pagers/NewPagesPager.php
@@ -28,7 +28,7 @@ class NewPagesPager extends ReverseChronologicalPager {
protected $opts;
/**
- * @var HtmlForm
+ * @var HTMLForm
*/
protected $mForm;
@@ -39,6 +39,8 @@ class NewPagesPager extends ReverseChronologicalPager {
}
function getQueryInfo() {
+ $rcQuery = RecentChange::getQueryInfo();
+
$conds = [];
$conds['rc_new'] = 1;
@@ -68,18 +70,19 @@ class NewPagesPager extends ReverseChronologicalPager {
}
if ( $user ) {
- $conds['rc_user_text'] = $user->getText();
- $rcIndexes = 'rc_user_text';
+ $conds[] = ActorMigration::newMigration()->getWhere(
+ $this->mDb, 'rc_user', User::newFromName( $user->getText(), false ), false
+ )['conds'];
} elseif ( User::groupHasPermission( '*', 'createpage' ) &&
$this->opts->getValue( 'hideliu' )
) {
# If anons cannot make new pages, don't "exclude logged in users"!
- $conds['rc_user'] = 0;
+ $conds[] = ActorMigration::newMigration()->isAnon( $rcQuery['fields']['rc_user'] );
}
# If this user cannot see patrolled edits or they are off, don't do dumb queries!
if ( $this->opts->getValue( 'hidepatrolled' ) && $this->getUser()->useNPPatrol() ) {
- $conds['rc_patrolled'] = 0;
+ $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
}
if ( $this->opts->getValue( 'hidebots' ) ) {
@@ -90,17 +93,12 @@ class NewPagesPager extends ReverseChronologicalPager {
$conds['page_is_redirect'] = 0;
}
- $commentQuery = CommentStore::newKey( 'rc_comment' )->getJoin();
-
// Allow changes to the New Pages query
- $tables = [ 'recentchanges', 'page' ] + $commentQuery['tables'];
- $fields = [
- 'rc_namespace', 'rc_title', 'rc_cur_id', 'rc_user', 'rc_user_text',
- 'rc_timestamp', 'rc_patrolled', 'rc_id', 'rc_deleted',
- 'length' => 'page_len', 'rev_id' => 'page_latest', 'rc_this_oldid',
- 'page_namespace', 'page_title'
- ] + $commentQuery['fields'];
- $join_conds = [ 'page' => [ 'INNER JOIN', 'page_id=rc_cur_id' ] ] + $commentQuery['joins'];
+ $tables = array_merge( $rcQuery['tables'], [ 'page' ] );
+ $fields = array_merge( $rcQuery['fields'], [
+ 'length' => 'page_len', 'rev_id' => 'page_latest', 'page_namespace', 'page_title'
+ ] );
+ $join_conds = [ 'page' => [ 'INNER JOIN', 'page_id=rc_cur_id' ] ] + $rcQuery['joins'];
// Avoid PHP 7.1 warning from passing $this by reference
$pager = $this;
diff --git a/www/wiki/includes/specials/pagers/ProtectedPagesPager.php b/www/wiki/includes/specials/pagers/ProtectedPagesPager.php
index 1587abc0..3b69698f 100644
--- a/www/wiki/includes/specials/pagers/ProtectedPagesPager.php
+++ b/www/wiki/includes/specials/pagers/ProtectedPagesPager.php
@@ -19,12 +19,10 @@
* @ingroup Pager
*/
-use \MediaWiki\Linker\LinkRenderer;
+use MediaWiki\Linker\LinkRenderer;
-/**
- * @todo document
- */
class ProtectedPagesPager extends TablePager {
+
public $mForm, $mConds;
private $type, $level, $namespace, $sizetype, $size, $indefonly, $cascadeonly, $noredirect;
@@ -236,7 +234,7 @@ class ProtectedPagesPager extends TablePager {
LogPage::DELETED_COMMENT,
$this->getUser()
) ) {
- $value = CommentStore::newKey( 'log_comment' )->getComment( $row )->text;
+ $value = CommentStore::getStore()->getComment( 'log_comment', $row )->text;
$formatted = Linker::formatComment( $value !== null ? $value : '' );
} else {
$formatted = $this->msg( 'rev-deleted-comment' )->escaped();
@@ -285,10 +283,14 @@ class ProtectedPagesPager extends TablePager {
$conds[] = 'page_namespace=' . $this->mDb->addQuotes( $this->namespace );
}
- $commentQuery = CommentStore::newKey( 'log_comment' )->getJoin();
+ $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
return [
- 'tables' => [ 'page', 'page_restrictions', 'log_search', 'logging' ] + $commentQuery['tables'],
+ 'tables' => [
+ 'page', 'page_restrictions', 'log_search',
+ 'logparen' => [ 'logging' ] + $commentQuery['tables'] + $actorQuery['tables'],
+ ],
'fields' => [
'pr_id',
'page_namespace',
@@ -299,9 +301,8 @@ class ProtectedPagesPager extends TablePager {
'pr_expiry',
'pr_cascade',
'log_timestamp',
- 'log_user',
'log_deleted',
- ] + $commentQuery['fields'],
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
'conds' => $conds,
'join_conds' => [
'log_search' => [
@@ -309,12 +310,12 @@ class ProtectedPagesPager extends TablePager {
'ls_field' => 'pr_id', 'ls_value = ' . $this->mDb->buildStringCast( 'pr_id' )
]
],
- 'logging' => [
+ 'logparen' => [
'LEFT JOIN', [
'ls_log_id = log_id'
]
]
- ] + $commentQuery['joins']
+ ] + $commentQuery['joins'] + $actorQuery['joins']
];
}
diff --git a/www/wiki/includes/specials/pagers/UsersPager.php b/www/wiki/includes/specials/pagers/UsersPager.php
index fdd1b353..3b9f9a17 100644
--- a/www/wiki/includes/specials/pagers/UsersPager.php
+++ b/www/wiki/includes/specials/pagers/UsersPager.php
@@ -33,7 +33,7 @@
class UsersPager extends AlphabeticPager {
/**
- * @var array A array with user ids as key and a array of groups as value
+ * @var array[] A array with user ids as key and a array of groups as value
*/
protected $userGroupCache;
@@ -277,7 +277,7 @@ class UsersPager extends AlphabeticPager {
$formDescriptor = [
'user' => [
- 'class' => 'HTMLUserTextField',
+ 'class' => HTMLUserTextField::class,
'label' => $this->msg( 'listusersfrom' )->text(),
'name' => 'username',
'default' => $this->requestedUser,
@@ -286,7 +286,7 @@ class UsersPager extends AlphabeticPager {
'label' => $this->msg( 'group' )->text(),
'name' => 'group',
'default' => $this->requestedGroup,
- 'class' => 'HTMLSelectField',
+ 'class' => HTMLSelectField::class,
'options' => $groupOptions,
],
'editsOnly' => [
@@ -311,7 +311,7 @@ class UsersPager extends AlphabeticPager {
'default' => $this->mDefaultDirection
],
'limithiddenfield' => [
- 'class' => 'HTMLHiddenField',
+ 'class' => HTMLHiddenField::class,
'name' => 'limit',
'default' => $this->mLimit
]
@@ -322,14 +322,14 @@ class UsersPager extends AlphabeticPager {
if ( $beforeSubmitButtonHookOut !== '' ) {
$formDescriptor[ 'beforeSubmitButtonHookOut' ] = [
- 'class' => 'HTMLInfoField',
+ 'class' => HTMLInfoField::class,
'raw' => true,
'default' => $beforeSubmitButtonHookOut
];
}
$formDescriptor[ 'submit' ] = [
- 'class' => 'HTMLSubmitField',
+ 'class' => HTMLSubmitField::class,
'buttonlabel-message' => 'listusers-submit',
];
@@ -338,7 +338,7 @@ class UsersPager extends AlphabeticPager {
if ( $beforeClosingFieldsetHookOut !== '' ) {
$formDescriptor[ 'beforeClosingFieldsetHookOut' ] = [
- 'class' => 'HTMLInfoField',
+ 'class' => HTMLInfoField::class,
'raw' => true,
'default' => $beforeClosingFieldsetHookOut
];
@@ -391,8 +391,8 @@ class UsersPager extends AlphabeticPager {
* and the relevant UserGroupMembership objects
*
* @param int $uid User id
- * @param array|null $cache
- * @return array (group name => UserGroupMembership object)
+ * @param array[]|null $cache
+ * @return UserGroupMembership[] (group name => UserGroupMembership object)
*/
protected static function getGroupMemberships( $uid, $cache = null ) {
if ( $cache === null ) {
@@ -407,7 +407,7 @@ class UsersPager extends AlphabeticPager {
* Format a link to a group description page
*
* @param string|UserGroupMembership $group Group name or UserGroupMembership object
- * @param string $username Username
+ * @param string $username
* @return string
*/
protected function buildGroupLink( $group, $username ) {
diff --git a/www/wiki/includes/templates/AtomHeader.mustache b/www/wiki/includes/templates/AtomHeader.mustache
new file mode 100644
index 00000000..60ab75e3
--- /dev/null
+++ b/www/wiki/includes/templates/AtomHeader.mustache
@@ -0,0 +1,8 @@
+<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{{language}}}">
+ <id>{{{feedID}}}</id>
+ <title>{{{title}}}</title>
+ <link rel="self" type="application/atom+xml" href="{{{selfUrl}}}"/>
+ <link rel="alternate" type="text/html" href="{{{url}}}"/>
+ <updated>{{{timestamp}}}Z</updated>
+ <subtitle>{{{description}}}</subtitle>
+ <generator>MediaWiki {{{version}}}</generator>
diff --git a/www/wiki/includes/templates/AtomItem.mustache b/www/wiki/includes/templates/AtomItem.mustache
new file mode 100644
index 00000000..32d2f01d
--- /dev/null
+++ b/www/wiki/includes/templates/AtomItem.mustache
@@ -0,0 +1,10 @@
+ <entry>
+ <id>{{{uniqueID}}}</id>
+ <title>{{{title}}}</title>
+ <link rel="alternate" type="{{{mimeType}}}" href="{{{url}}}"/>
+ {{#date}}<updated>{{{.}}}Z</updated>{{/date}}
+
+ <summary type="html">{{{description}}}</summary>
+ {{#author}}<author><name>{{{.}}}</name></author>{{/author}}
+ {{! FIXME: Need to add comments }}
+ </entry>
diff --git a/www/wiki/includes/templates/NoLocalSettings.mustache b/www/wiki/includes/templates/NoLocalSettings.mustache
index 54579491..36391f5b 100644
--- a/www/wiki/includes/templates/NoLocalSettings.mustache
+++ b/www/wiki/includes/templates/NoLocalSettings.mustache
@@ -28,12 +28,12 @@
{{^localSettingsExists}}
<p>LocalSettings.php not found.</p>
{{#installerStarted}}
- <p>Please <a href="{{path}}mw-config/index.{{ext}}">complete the installation</a> and download LocalSettings.php.</p>
+ <p>Please <a href="{{path}}mw-config/index.php">complete the installation</a> and download LocalSettings.php.</p>
{{/installerStarted}}
{{^installerStarted}}
- <p>Please <a href="{{path}}mw-config/index.{{ext}}">set up the wiki</a> first.</p>
+ <p>Please <a href="{{path}}mw-config/index.php">set up the wiki</a> first.</p>
{{/installerStarted}}
{{/localSettingsExists}}
</div>
</body>
-</html> \ No newline at end of file
+</html>
diff --git a/www/wiki/includes/templates/RSSHeader.mustache b/www/wiki/includes/templates/RSSHeader.mustache
new file mode 100644
index 00000000..385369df
--- /dev/null
+++ b/www/wiki/includes/templates/RSSHeader.mustache
@@ -0,0 +1,8 @@
+<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <channel>
+ <title>{{{title}}}</title>
+ <link>{{{url}}}</link>
+ <description>{{{description}}}</description>
+ <language>{{{language}}}</language>
+ <generator>MediaWiki {{{version}}}</generator>
+ <lastBuildDate>{{{timestamp}}}</lastBuildDate>
diff --git a/www/wiki/includes/templates/RSSItem.mustache b/www/wiki/includes/templates/RSSItem.mustache
new file mode 100644
index 00000000..d00c1006
--- /dev/null
+++ b/www/wiki/includes/templates/RSSItem.mustache
@@ -0,0 +1,9 @@
+ <item>
+ <title>{{{title}}}</title>
+ <link>{{{url}}}</link>
+ <guid{{^permalink}} isPermaLink="false"{{/permalink}}>{{{uniqueID}}}</guid>
+ <description>{{{description}}}</description>
+ {{#date}}<pubDate>{{{.}}}</pubDate>{{/date}}
+ {{#author}}<dc:creator>{{{.}}}</dc:creator>{{/author}}
+ {{#comments}}<comments>{{{.}}}</comments>{{/comments}}
+ </item>
diff --git a/www/wiki/includes/tidy/Balancer.php b/www/wiki/includes/tidy/Balancer.php
index fbe92702..6671f49b 100644
--- a/www/wiki/includes/tidy/Balancer.php
+++ b/www/wiki/includes/tidy/Balancer.php
@@ -23,14 +23,15 @@
* @since 1.27
* @author C. Scott Ananian, 2016
*/
+
namespace MediaWiki\Tidy;
+use ExplodeIterator;
+use IteratorAggregate;
+use ReverseArrayIterator;
+use Sanitizer;
use Wikimedia\Assert\Assert;
use Wikimedia\Assert\ParameterAssertionException;
-use \ExplodeIterator;
-use \IteratorAggregate;
-use \ReverseArrayIterator;
-use \Sanitizer;
// A note for future librarization[1] -- this file is a good candidate
// for splitting into an independent library, except that it is currently
@@ -293,6 +294,9 @@ class BalanceSets {
'span' => true, 'strike' => true, 'strong' => true, 'sub' => true,
'sup' => true, 'textarea' => true, 'tt' => true, 'u' => true,
'var' => true,
+ // Those defined in tidy.conf
+ 'video' => true, 'audio' => true, 'bdi' => true, 'data' => true,
+ 'time' => true, 'mark' => true,
],
];
}
@@ -1365,7 +1369,7 @@ class BalanceStack implements IteratorAggregate {
foreach ( $this->elements as $elt ) {
array_push( $r, $elt->localName );
}
- return implode( $r, ' ' );
+ return implode( ' ', $r );
}
}
@@ -1669,13 +1673,11 @@ class BalanceActiveFormattingElements {
$this->addToNoahList( $b );
}
- // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong
/**
* Reconstruct the active formatting elements.
* @param BalanceStack $stack The open elements stack
* @see https://html.spec.whatwg.org/multipage/syntax.html#reconstruct-the-active-formatting-elements
*/
- // @codingStandardsIgnoreEnd
public function reconstruct( $stack ) {
$entry = $this->tail;
// If there are no entries in the list of active formatting elements,
@@ -1905,7 +1907,7 @@ class Balancer {
}
);
if ( count( $bad ) > 0 ) {
- $badstr = implode( array_keys( $bad ), ',' );
+ $badstr = implode( ',', array_keys( $bad ) );
throw new ParameterAssertionException(
'$config',
'Balance attempted with sanitization including ' .
@@ -2054,73 +2056,73 @@ class Balancer {
return true;
} elseif ( $token === 'tag' ) {
switch ( $value ) {
- case 'font':
- if ( isset( $attribs['color'] )
- || isset( $attribs['face'] )
- || isset( $attribs['size'] )
- ) {
- break;
- }
- // otherwise, fall through
- case 'b':
- case 'big':
- case 'blockquote':
- case 'body':
- case 'br':
- case 'center':
- case 'code':
- case 'dd':
- case 'div':
- case 'dl':
- case 'dt':
- case 'em':
- case 'embed':
- case 'h1':
- case 'h2':
- case 'h3':
- case 'h4':
- case 'h5':
- case 'h6':
- case 'head':
- case 'hr':
- case 'i':
- case 'img':
- case 'li':
- case 'listing':
- case 'menu':
- case 'meta':
- case 'nobr':
- case 'ol':
- case 'p':
- case 'pre':
- case 'ruby':
- case 's':
- case 'small':
- case 'span':
- case 'strong':
- case 'strike':
- case 'sub':
- case 'sup':
- case 'table':
- case 'tt':
- case 'u':
- case 'ul':
- case 'var':
- if ( $this->fragmentContext ) {
- break;
- }
- while ( true ) {
- $this->stack->pop();
- $node = $this->stack->currentNode;
- if (
- $node->isMathmlTextIntegrationPoint() ||
- $node->isHtmlIntegrationPoint() ||
- $node->isHtml()
+ case 'font':
+ if ( isset( $attribs['color'] )
+ || isset( $attribs['face'] )
+ || isset( $attribs['size'] )
) {
break;
}
- }
- return $this->insertToken( $token, $value, $attribs, $selfClose );
+ // otherwise, fall through
+ case 'b':
+ case 'big':
+ case 'blockquote':
+ case 'body':
+ case 'br':
+ case 'center':
+ case 'code':
+ case 'dd':
+ case 'div':
+ case 'dl':
+ case 'dt':
+ case 'em':
+ case 'embed':
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ case 'head':
+ case 'hr':
+ case 'i':
+ case 'img':
+ case 'li':
+ case 'listing':
+ case 'menu':
+ case 'meta':
+ case 'nobr':
+ case 'ol':
+ case 'p':
+ case 'pre':
+ case 'ruby':
+ case 's':
+ case 'small':
+ case 'span':
+ case 'strong':
+ case 'strike':
+ case 'sub':
+ case 'sup':
+ case 'table':
+ case 'tt':
+ case 'u':
+ case 'ul':
+ case 'var':
+ if ( $this->fragmentContext ) {
+ break;
+ }
+ while ( true ) {
+ $this->stack->pop();
+ $node = $this->stack->currentNode;
+ if (
+ $node->isMathmlTextIntegrationPoint() ||
+ $node->isHtmlIntegrationPoint() ||
+ $node->isHtml()
+ ) {
+ break;
+ }
+ }
+ return $this->insertToken( $token, $value, $attribs, $selfClose );
}
// "Any other start tag"
$adjusted = ( $this->fragmentContext && $this->stack->length() === 1 ) ?
@@ -2272,56 +2274,56 @@ class Balancer {
}
if ( $node->isHtml() ) {
switch ( $node->localName ) {
- case 'select':
- $stackLength = $this->stack->length();
- for ( $j = $i + 1; $j < $stackLength - 1; $j++ ) {
- $ancestor = $this->stack->node( $stackLength - $j - 1 );
- if ( $ancestor->isHtmlNamed( 'template' ) ) {
- break;
+ case 'select':
+ $stackLength = $this->stack->length();
+ for ( $j = $i + 1; $j < $stackLength - 1; $j++ ) {
+ $ancestor = $this->stack->node( $stackLength - $j - 1 );
+ if ( $ancestor->isHtmlNamed( 'template' ) ) {
+ break;
+ }
+ if ( $ancestor->isHtmlNamed( 'table' ) ) {
+ $this->switchMode( 'inSelectInTableMode' );
+ return;
+ }
}
- if ( $ancestor->isHtmlNamed( 'table' ) ) {
- $this->switchMode( 'inSelectInTableMode' );
- return;
- }
- }
- $this->switchMode( 'inSelectMode' );
- return;
- case 'tr':
- $this->switchMode( 'inRowMode' );
- return;
- case 'tbody':
- case 'tfoot':
- case 'thead':
- $this->switchMode( 'inTableBodyMode' );
- return;
- case 'caption':
- $this->switchMode( 'inCaptionMode' );
- return;
- case 'colgroup':
- $this->switchMode( 'inColumnGroupMode' );
- return;
- case 'table':
- $this->switchMode( 'inTableMode' );
- return;
- case 'template':
- $this->switchMode(
- array_slice( $this->templateInsertionModes, -1 )[0]
- );
- return;
- case 'body':
- $this->switchMode( 'inBodyMode' );
- return;
- // OMITTED: <frameset>
- // OMITTED: <html>
- // OMITTED: <head>
- default:
- if ( !$last ) {
- // OMITTED: <head>
- if ( $node->isA( BalanceSets::$tableCellSet ) ) {
- $this->switchMode( 'inCellMode' );
- return;
+ $this->switchMode( 'inSelectMode' );
+ return;
+ case 'tr':
+ $this->switchMode( 'inRowMode' );
+ return;
+ case 'tbody':
+ case 'tfoot':
+ case 'thead':
+ $this->switchMode( 'inTableBodyMode' );
+ return;
+ case 'caption':
+ $this->switchMode( 'inCaptionMode' );
+ return;
+ case 'colgroup':
+ $this->switchMode( 'inColumnGroupMode' );
+ return;
+ case 'table':
+ $this->switchMode( 'inTableMode' );
+ return;
+ case 'template':
+ $this->switchMode(
+ array_slice( $this->templateInsertionModes, -1 )[0]
+ );
+ return;
+ case 'body':
+ $this->switchMode( 'inBodyMode' );
+ return;
+ // OMITTED: <frameset>
+ // OMITTED: <html>
+ // OMITTED: <head>
+ default:
+ if ( !$last ) {
+ // OMITTED: <head>
+ if ( $node->isA( BalanceSets::$tableCellSet ) ) {
+ $this->switchMode( 'inCellMode' );
+ return;
+ }
}
- }
}
}
if ( $last ) {
@@ -2380,52 +2382,52 @@ class Balancer {
// Fall through to handle non-whitespace below.
} elseif ( $token === 'tag' ) {
switch ( $value ) {
- case 'meta':
- // OMITTED: in a full HTML parser, this might change the encoding.
- // falls through
- // OMITTED: <html>
- case 'base':
- case 'basefont':
- case 'bgsound':
- case 'link':
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->stack->pop();
- return true;
- // OMITTED: <title>
- // OMITTED: <noscript>
- case 'noframes':
- case 'style':
- return $this->parseRawText( $value, $attribs );
- // OMITTED: <script>
- case 'template':
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->afe->insertMarker();
- // OMITTED: frameset_ok
- $this->switchMode( 'inTemplateMode' );
- $this->templateInsertionModes[] = $this->parseMode;
- return true;
- // OMITTED: <head>
+ case 'meta':
+ // OMITTED: in a full HTML parser, this might change the encoding.
+ // falls through
+ // OMITTED: <html>
+ case 'base':
+ case 'basefont':
+ case 'bgsound':
+ case 'link':
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->stack->pop();
+ return true;
+ // OMITTED: <title>
+ // OMITTED: <noscript>
+ case 'noframes':
+ case 'style':
+ return $this->parseRawText( $value, $attribs );
+ // OMITTED: <script>
+ case 'template':
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->afe->insertMarker();
+ // OMITTED: frameset_ok
+ $this->switchMode( 'inTemplateMode' );
+ $this->templateInsertionModes[] = $this->parseMode;
+ return true;
+ // OMITTED: <head>
}
} elseif ( $token === 'endtag' ) {
switch ( $value ) {
- // OMITTED: <head>
- // OMITTED: <body>
- // OMITTED: <html>
- case 'br':
- break; // handle at the bottom of the function
- case 'template':
- if ( $this->stack->indexOf( $value ) < 0 ) {
- return true; // Ignore the token.
- }
- $this->stack->generateImpliedEndTags( null, true /* thorough */ );
- $this->stack->popTag( $value );
- $this->afe->clearToMarker();
- array_pop( $this->templateInsertionModes );
- $this->resetInsertionMode();
- return true;
- default:
- // ignore any other end tag
- return true;
+ // OMITTED: <head>
+ // OMITTED: <body>
+ // OMITTED: <html>
+ case 'br':
+ break; // handle at the bottom of the function
+ case 'template':
+ if ( $this->stack->indexOf( $value ) < 0 ) {
+ return true; // Ignore the token.
+ }
+ $this->stack->generateImpliedEndTags( null, true /* thorough */ );
+ $this->stack->popTag( $value );
+ $this->afe->clearToMarker();
+ array_pop( $this->templateInsertionModes );
+ $this->resetInsertionMode();
+ return true;
+ default:
+ // ignore any other end tag
+ return true;
}
} elseif ( $token === 'comment' ) {
$this->stack->insertComment( $value );
@@ -2451,361 +2453,361 @@ class Balancer {
return true;
} elseif ( $token === 'tag' ) {
switch ( $value ) {
- // OMITTED: <html>
- case 'base':
- case 'basefont':
- case 'bgsound':
- case 'link':
- case 'meta':
- case 'noframes':
- // OMITTED: <script>
- case 'style':
- case 'template':
- // OMITTED: <title>
- return $this->inHeadMode( $token, $value, $attribs, $selfClose );
- // OMITTED: <body>
- // OMITTED: <frameset>
-
- case 'address':
- case 'article':
- case 'aside':
- case 'blockquote':
- case 'center':
- case 'details':
- case 'dialog':
- case 'dir':
- case 'div':
- case 'dl':
- case 'fieldset':
- case 'figcaption':
- case 'figure':
- case 'footer':
- case 'header':
- case 'hgroup':
- case 'main':
- case 'nav':
- case 'ol':
- case 'p':
- case 'section':
- case 'summary':
- case 'ul':
- if ( $this->stack->inButtonScope( 'p' ) ) {
- $this->inBodyMode( 'endtag', 'p' );
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- return true;
-
- case 'menu':
- if ( $this->stack->inButtonScope( "p" ) ) {
- $this->inBodyMode( 'endtag', 'p' );
- }
- if ( $this->stack->currentNode->isHtmlNamed( 'menuitem' ) ) {
- $this->stack->pop();
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- return true;
+ // OMITTED: <html>
+ case 'base':
+ case 'basefont':
+ case 'bgsound':
+ case 'link':
+ case 'meta':
+ case 'noframes':
+ // OMITTED: <script>
+ case 'style':
+ case 'template':
+ // OMITTED: <title>
+ return $this->inHeadMode( $token, $value, $attribs, $selfClose );
+ // OMITTED: <body>
+ // OMITTED: <frameset>
- case 'h1':
- case 'h2':
- case 'h3':
- case 'h4':
- case 'h5':
- case 'h6':
- if ( $this->stack->inButtonScope( 'p' ) ) {
- $this->inBodyMode( 'endtag', 'p' );
- }
- if ( $this->stack->currentNode->isA( BalanceSets::$headingSet ) ) {
- $this->stack->pop();
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- return true;
+ case 'address':
+ case 'article':
+ case 'aside':
+ case 'blockquote':
+ case 'center':
+ case 'details':
+ case 'dialog':
+ case 'dir':
+ case 'div':
+ case 'dl':
+ case 'fieldset':
+ case 'figcaption':
+ case 'figure':
+ case 'footer':
+ case 'header':
+ case 'hgroup':
+ case 'main':
+ case 'nav':
+ case 'ol':
+ case 'p':
+ case 'section':
+ case 'summary':
+ case 'ul':
+ if ( $this->stack->inButtonScope( 'p' ) ) {
+ $this->inBodyMode( 'endtag', 'p' );
+ }
+ $this->stack->insertHTMLElement( $value, $attribs );
+ return true;
- case 'pre':
- case 'listing':
- if ( $this->stack->inButtonScope( 'p' ) ) {
- $this->inBodyMode( 'endtag', 'p' );
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->ignoreLinefeed = true;
- // OMITTED: frameset_ok
- return true;
+ case 'menu':
+ if ( $this->stack->inButtonScope( "p" ) ) {
+ $this->inBodyMode( 'endtag', 'p' );
+ }
+ if ( $this->stack->currentNode->isHtmlNamed( 'menuitem' ) ) {
+ $this->stack->pop();
+ }
+ $this->stack->insertHTMLElement( $value, $attribs );
+ return true;
- case 'form':
- if (
- $this->formElementPointer &&
- $this->stack->indexOf( 'template' ) < 0
- ) {
- return true; // in a form, not in a template.
- }
- if ( $this->stack->inButtonScope( "p" ) ) {
- $this->inBodyMode( 'endtag', 'p' );
- }
- $elt = $this->stack->insertHTMLElement( $value, $attribs );
- if ( $this->stack->indexOf( 'template' ) < 0 ) {
- $this->formElementPointer = $elt;
- }
- return true;
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ if ( $this->stack->inButtonScope( 'p' ) ) {
+ $this->inBodyMode( 'endtag', 'p' );
+ }
+ if ( $this->stack->currentNode->isA( BalanceSets::$headingSet ) ) {
+ $this->stack->pop();
+ }
+ $this->stack->insertHTMLElement( $value, $attribs );
+ return true;
- case 'li':
- // OMITTED: frameset_ok
- foreach ( $this->stack as $node ) {
- if ( $node->isHtmlNamed( 'li' ) ) {
- $this->inBodyMode( 'endtag', 'li' );
- break;
+ case 'pre':
+ case 'listing':
+ if ( $this->stack->inButtonScope( 'p' ) ) {
+ $this->inBodyMode( 'endtag', 'p' );
}
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->ignoreLinefeed = true;
+ // OMITTED: frameset_ok
+ return true;
+
+ case 'form':
if (
- $node->isA( BalanceSets::$specialSet ) &&
- !$node->isA( BalanceSets::$addressDivPSet )
+ $this->formElementPointer &&
+ $this->stack->indexOf( 'template' ) < 0
) {
- break;
+ return true; // in a form, not in a template.
}
- }
- if ( $this->stack->inButtonScope( 'p' ) ) {
- $this->inBodyMode( 'endtag', 'p' );
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- return true;
-
- case 'dd':
- case 'dt':
- // OMITTED: frameset_ok
- foreach ( $this->stack as $node ) {
- if ( $node->isHtmlNamed( 'dd' ) ) {
- $this->inBodyMode( 'endtag', 'dd' );
- break;
+ if ( $this->stack->inButtonScope( "p" ) ) {
+ $this->inBodyMode( 'endtag', 'p' );
}
- if ( $node->isHtmlNamed( 'dt' ) ) {
- $this->inBodyMode( 'endtag', 'dt' );
- break;
+ $elt = $this->stack->insertHTMLElement( $value, $attribs );
+ if ( $this->stack->indexOf( 'template' ) < 0 ) {
+ $this->formElementPointer = $elt;
}
- if (
- $node->isA( BalanceSets::$specialSet ) &&
- !$node->isA( BalanceSets::$addressDivPSet )
- ) {
- break;
+ return true;
+
+ case 'li':
+ // OMITTED: frameset_ok
+ foreach ( $this->stack as $node ) {
+ if ( $node->isHtmlNamed( 'li' ) ) {
+ $this->inBodyMode( 'endtag', 'li' );
+ break;
+ }
+ if (
+ $node->isA( BalanceSets::$specialSet ) &&
+ !$node->isA( BalanceSets::$addressDivPSet )
+ ) {
+ break;
+ }
}
- }
- if ( $this->stack->inButtonScope( 'p' ) ) {
- $this->inBodyMode( 'endtag', 'p' );
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- return true;
+ if ( $this->stack->inButtonScope( 'p' ) ) {
+ $this->inBodyMode( 'endtag', 'p' );
+ }
+ $this->stack->insertHTMLElement( $value, $attribs );
+ return true;
- // OMITTED: <plaintext>
+ case 'dd':
+ case 'dt':
+ // OMITTED: frameset_ok
+ foreach ( $this->stack as $node ) {
+ if ( $node->isHtmlNamed( 'dd' ) ) {
+ $this->inBodyMode( 'endtag', 'dd' );
+ break;
+ }
+ if ( $node->isHtmlNamed( 'dt' ) ) {
+ $this->inBodyMode( 'endtag', 'dt' );
+ break;
+ }
+ if (
+ $node->isA( BalanceSets::$specialSet ) &&
+ !$node->isA( BalanceSets::$addressDivPSet )
+ ) {
+ break;
+ }
+ }
+ if ( $this->stack->inButtonScope( 'p' ) ) {
+ $this->inBodyMode( 'endtag', 'p' );
+ }
+ $this->stack->insertHTMLElement( $value, $attribs );
+ return true;
- case 'button':
- if ( $this->stack->inScope( 'button' ) ) {
- $this->inBodyMode( 'endtag', 'button' );
- return $this->insertToken( $token, $value, $attribs, $selfClose );
- }
- $this->afe->reconstruct( $this->stack );
- $this->stack->insertHTMLElement( $value, $attribs );
- return true;
+ // OMITTED: <plaintext>
- case 'a':
- $activeElement = $this->afe->findElementByTag( 'a' );
- if ( $activeElement ) {
- $this->inBodyMode( 'endtag', 'a' );
- if ( $this->afe->isInList( $activeElement ) ) {
- $this->afe->remove( $activeElement );
- // Don't flatten here, since when we fall
- // through below we might foster parent
- // the new <a> tag inside this one.
- $this->stack->removeElement( $activeElement, false );
+ case 'button':
+ if ( $this->stack->inScope( 'button' ) ) {
+ $this->inBodyMode( 'endtag', 'button' );
+ return $this->insertToken( $token, $value, $attribs, $selfClose );
}
- }
- // Falls through
- case 'b':
- case 'big':
- case 'code':
- case 'em':
- case 'font':
- case 'i':
- case 's':
- case 'small':
- case 'strike':
- case 'strong':
- case 'tt':
- case 'u':
- $this->afe->reconstruct( $this->stack );
- $this->afe->push( $this->stack->insertHTMLElement( $value, $attribs ) );
- return true;
+ $this->afe->reconstruct( $this->stack );
+ $this->stack->insertHTMLElement( $value, $attribs );
+ return true;
- case 'nobr':
- $this->afe->reconstruct( $this->stack );
- if ( $this->stack->inScope( 'nobr' ) ) {
- $this->inBodyMode( 'endtag', 'nobr' );
+ case 'a':
+ $activeElement = $this->afe->findElementByTag( 'a' );
+ if ( $activeElement ) {
+ $this->inBodyMode( 'endtag', 'a' );
+ if ( $this->afe->isInList( $activeElement ) ) {
+ $this->afe->remove( $activeElement );
+ // Don't flatten here, since when we fall
+ // through below we might foster parent
+ // the new <a> tag inside this one.
+ $this->stack->removeElement( $activeElement, false );
+ }
+ }
+ // Falls through
+ case 'b':
+ case 'big':
+ case 'code':
+ case 'em':
+ case 'font':
+ case 'i':
+ case 's':
+ case 'small':
+ case 'strike':
+ case 'strong':
+ case 'tt':
+ case 'u':
$this->afe->reconstruct( $this->stack );
- }
- $this->afe->push( $this->stack->insertHTMLElement( $value, $attribs ) );
- return true;
+ $this->afe->push( $this->stack->insertHTMLElement( $value, $attribs ) );
+ return true;
- case 'applet':
- case 'marquee':
- case 'object':
- $this->afe->reconstruct( $this->stack );
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->afe->insertMarker();
- // OMITTED: frameset_ok
- return true;
+ case 'nobr':
+ $this->afe->reconstruct( $this->stack );
+ if ( $this->stack->inScope( 'nobr' ) ) {
+ $this->inBodyMode( 'endtag', 'nobr' );
+ $this->afe->reconstruct( $this->stack );
+ }
+ $this->afe->push( $this->stack->insertHTMLElement( $value, $attribs ) );
+ return true;
- case 'table':
- // The document is never in "quirks mode"; see simplifications
- // above.
- if ( $this->stack->inButtonScope( 'p' ) ) {
- $this->inBodyMode( 'endtag', 'p' );
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- // OMITTED: frameset_ok
- $this->switchMode( 'inTableMode' );
- return true;
+ case 'applet':
+ case 'marquee':
+ case 'object':
+ $this->afe->reconstruct( $this->stack );
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->afe->insertMarker();
+ // OMITTED: frameset_ok
+ return true;
- case 'area':
- case 'br':
- case 'embed':
- case 'img':
- case 'keygen':
- case 'wbr':
- $this->afe->reconstruct( $this->stack );
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->stack->pop();
- // OMITTED: frameset_ok
- return true;
+ case 'table':
+ // The document is never in "quirks mode"; see simplifications
+ // above.
+ if ( $this->stack->inButtonScope( 'p' ) ) {
+ $this->inBodyMode( 'endtag', 'p' );
+ }
+ $this->stack->insertHTMLElement( $value, $attribs );
+ // OMITTED: frameset_ok
+ $this->switchMode( 'inTableMode' );
+ return true;
- case 'input':
- $this->afe->reconstruct( $this->stack );
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->stack->pop();
- // OMITTED: frameset_ok
- // (hence we don't need to examine the tag's "type" attribute)
- return true;
+ case 'area':
+ case 'br':
+ case 'embed':
+ case 'img':
+ case 'keygen':
+ case 'wbr':
+ $this->afe->reconstruct( $this->stack );
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->stack->pop();
+ // OMITTED: frameset_ok
+ return true;
- case 'param':
- case 'source':
- case 'track':
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->stack->pop();
- return true;
+ case 'input':
+ $this->afe->reconstruct( $this->stack );
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->stack->pop();
+ // OMITTED: frameset_ok
+ // (hence we don't need to examine the tag's "type" attribute)
+ return true;
- case 'hr':
- if ( $this->stack->inButtonScope( 'p' ) ) {
- $this->inBodyMode( 'endtag', 'p' );
- }
- if ( $this->stack->currentNode->isHtmlNamed( 'menuitem' ) ) {
+ case 'param':
+ case 'source':
+ case 'track':
+ $this->stack->insertHTMLElement( $value, $attribs );
$this->stack->pop();
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->stack->pop();
- return true;
+ return true;
- case 'image':
- // warts!
- return $this->inBodyMode( $token, 'img', $attribs, $selfClose );
+ case 'hr':
+ if ( $this->stack->inButtonScope( 'p' ) ) {
+ $this->inBodyMode( 'endtag', 'p' );
+ }
+ if ( $this->stack->currentNode->isHtmlNamed( 'menuitem' ) ) {
+ $this->stack->pop();
+ }
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->stack->pop();
+ return true;
- case 'textarea':
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->ignoreLinefeed = true;
- $this->inRCDATA = $value; // emulate rcdata tokenizer mode
- // OMITTED: frameset_ok
- return true;
+ case 'image':
+ // warts!
+ return $this->inBodyMode( $token, 'img', $attribs, $selfClose );
- // OMITTED: <xmp>
- // OMITTED: <iframe>
- // OMITTED: <noembed>
- // OMITTED: <noscript>
-
- case 'select':
- $this->afe->reconstruct( $this->stack );
- $this->stack->insertHTMLElement( $value, $attribs );
- switch ( $this->parseMode ) {
- case 'inTableMode':
- case 'inCaptionMode':
- case 'inTableBodyMode':
- case 'inRowMode':
- case 'inCellMode':
- $this->switchMode( 'inSelectInTableMode' );
- return true;
- default:
- $this->switchMode( 'inSelectMode' );
+ case 'textarea':
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->ignoreLinefeed = true;
+ $this->inRCDATA = $value; // emulate rcdata tokenizer mode
+ // OMITTED: frameset_ok
return true;
- }
- case 'optgroup':
- case 'option':
- if ( $this->stack->currentNode->isHtmlNamed( 'option' ) ) {
- $this->inBodyMode( 'endtag', 'option' );
- }
- $this->afe->reconstruct( $this->stack );
- $this->stack->insertHTMLElement( $value, $attribs );
- return true;
+ // OMITTED: <xmp>
+ // OMITTED: <iframe>
+ // OMITTED: <noembed>
+ // OMITTED: <noscript>
- case 'menuitem':
- if ( $this->stack->currentNode->isHtmlNamed( 'menuitem' ) ) {
- $this->stack->pop();
- }
- $this->afe->reconstruct( $this->stack );
- $this->stack->insertHTMLElement( $value, $attribs );
- return true;
+ case 'select':
+ $this->afe->reconstruct( $this->stack );
+ $this->stack->insertHTMLElement( $value, $attribs );
+ switch ( $this->parseMode ) {
+ case 'inTableMode':
+ case 'inCaptionMode':
+ case 'inTableBodyMode':
+ case 'inRowMode':
+ case 'inCellMode':
+ $this->switchMode( 'inSelectInTableMode' );
+ return true;
+ default:
+ $this->switchMode( 'inSelectMode' );
+ return true;
+ }
- case 'rb':
- case 'rtc':
- if ( $this->stack->inScope( 'ruby' ) ) {
- $this->stack->generateImpliedEndTags();
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- return true;
+ case 'optgroup':
+ case 'option':
+ if ( $this->stack->currentNode->isHtmlNamed( 'option' ) ) {
+ $this->inBodyMode( 'endtag', 'option' );
+ }
+ $this->afe->reconstruct( $this->stack );
+ $this->stack->insertHTMLElement( $value, $attribs );
+ return true;
- case 'rp':
- case 'rt':
- if ( $this->stack->inScope( 'ruby' ) ) {
- $this->stack->generateImpliedEndTags( 'rtc' );
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- return true;
+ case 'menuitem':
+ if ( $this->stack->currentNode->isHtmlNamed( 'menuitem' ) ) {
+ $this->stack->pop();
+ }
+ $this->afe->reconstruct( $this->stack );
+ $this->stack->insertHTMLElement( $value, $attribs );
+ return true;
- case 'math':
- $this->afe->reconstruct( $this->stack );
- // We skip the spec's "adjust MathML attributes" and
- // "adjust foreign attributes" steps, since the browser will
- // do this later when it parses the output and it doesn't affect
- // balancing.
- $this->stack->insertForeignElement(
- BalanceSets::MATHML_NAMESPACE, $value, $attribs
- );
- if ( $selfClose ) {
- // emit explicit </math> tag.
- $this->stack->pop();
- }
- return true;
+ case 'rb':
+ case 'rtc':
+ if ( $this->stack->inScope( 'ruby' ) ) {
+ $this->stack->generateImpliedEndTags();
+ }
+ $this->stack->insertHTMLElement( $value, $attribs );
+ return true;
- case 'svg':
- $this->afe->reconstruct( $this->stack );
- // We skip the spec's "adjust SVG attributes" and
- // "adjust foreign attributes" steps, since the browser will
- // do this later when it parses the output and it doesn't affect
- // balancing.
- $this->stack->insertForeignElement(
- BalanceSets::SVG_NAMESPACE, $value, $attribs
- );
- if ( $selfClose ) {
- // emit explicit </svg> tag.
- $this->stack->pop();
- }
- return true;
+ case 'rp':
+ case 'rt':
+ if ( $this->stack->inScope( 'ruby' ) ) {
+ $this->stack->generateImpliedEndTags( 'rtc' );
+ }
+ $this->stack->insertHTMLElement( $value, $attribs );
+ return true;
- case 'caption':
- case 'col':
- case 'colgroup':
- // OMITTED: <frame>
- case 'head':
- case 'tbody':
- case 'td':
- case 'tfoot':
- case 'th':
- case 'thead':
- case 'tr':
- // Ignore table tags if we're not inTableMode
- return true;
+ case 'math':
+ $this->afe->reconstruct( $this->stack );
+ // We skip the spec's "adjust MathML attributes" and
+ // "adjust foreign attributes" steps, since the browser will
+ // do this later when it parses the output and it doesn't affect
+ // balancing.
+ $this->stack->insertForeignElement(
+ BalanceSets::MATHML_NAMESPACE, $value, $attribs
+ );
+ if ( $selfClose ) {
+ // emit explicit </math> tag.
+ $this->stack->pop();
+ }
+ return true;
+
+ case 'svg':
+ $this->afe->reconstruct( $this->stack );
+ // We skip the spec's "adjust SVG attributes" and
+ // "adjust foreign attributes" steps, since the browser will
+ // do this later when it parses the output and it doesn't affect
+ // balancing.
+ $this->stack->insertForeignElement(
+ BalanceSets::SVG_NAMESPACE, $value, $attribs
+ );
+ if ( $selfClose ) {
+ // emit explicit </svg> tag.
+ $this->stack->pop();
+ }
+ return true;
+
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ // OMITTED: <frame>
+ case 'head':
+ case 'tbody':
+ case 'td':
+ case 'tfoot':
+ case 'th':
+ case 'thead':
+ case 'tr':
+ // Ignore table tags if we're not inTableMode
+ return true;
}
// Handle any other start tag here
@@ -2814,142 +2816,142 @@ class Balancer {
return true;
} elseif ( $token === 'endtag' ) {
switch ( $value ) {
- // </body>,</html> are unsupported.
-
- case 'template':
- return $this->inHeadMode( $token, $value, $attribs, $selfClose );
-
- case 'address':
- case 'article':
- case 'aside':
- case 'blockquote':
- case 'button':
- case 'center':
- case 'details':
- case 'dialog':
- case 'dir':
- case 'div':
- case 'dl':
- case 'fieldset':
- case 'figcaption':
- case 'figure':
- case 'footer':
- case 'header':
- case 'hgroup':
- case 'listing':
- case 'main':
- case 'menu':
- case 'nav':
- case 'ol':
- case 'pre':
- case 'section':
- case 'summary':
- case 'ul':
- // Ignore if there is not a matching open tag
- if ( !$this->stack->inScope( $value ) ) {
- return true;
- }
- $this->stack->generateImpliedEndTags();
- $this->stack->popTag( $value );
- return true;
+ // </body>,</html> are unsupported.
- case 'form':
- if ( $this->stack->indexOf( 'template' ) < 0 ) {
- $openform = $this->formElementPointer;
- $this->formElementPointer = null;
- if ( !$openform || !$this->stack->inScope( $openform ) ) {
+ case 'template':
+ return $this->inHeadMode( $token, $value, $attribs, $selfClose );
+
+ case 'address':
+ case 'article':
+ case 'aside':
+ case 'blockquote':
+ case 'button':
+ case 'center':
+ case 'details':
+ case 'dialog':
+ case 'dir':
+ case 'div':
+ case 'dl':
+ case 'fieldset':
+ case 'figcaption':
+ case 'figure':
+ case 'footer':
+ case 'header':
+ case 'hgroup':
+ case 'listing':
+ case 'main':
+ case 'menu':
+ case 'nav':
+ case 'ol':
+ case 'pre':
+ case 'section':
+ case 'summary':
+ case 'ul':
+ // Ignore if there is not a matching open tag
+ if ( !$this->stack->inScope( $value ) ) {
return true;
}
$this->stack->generateImpliedEndTags();
- // Don't flatten yet if we're removing a <form> element
- // out-of-order. (eg. `<form><div></form>`)
- $flatten = ( $this->stack->currentNode === $openform );
- $this->stack->removeElement( $openform, $flatten );
- } else {
- if ( !$this->stack->inScope( 'form' ) ) {
- return true;
+ $this->stack->popTag( $value );
+ return true;
+
+ case 'form':
+ if ( $this->stack->indexOf( 'template' ) < 0 ) {
+ $openform = $this->formElementPointer;
+ $this->formElementPointer = null;
+ if ( !$openform || !$this->stack->inScope( $openform ) ) {
+ return true;
+ }
+ $this->stack->generateImpliedEndTags();
+ // Don't flatten yet if we're removing a <form> element
+ // out-of-order. (eg. `<form><div></form>`)
+ $flatten = ( $this->stack->currentNode === $openform );
+ $this->stack->removeElement( $openform, $flatten );
+ } else {
+ if ( !$this->stack->inScope( 'form' ) ) {
+ return true;
+ }
+ $this->stack->generateImpliedEndTags();
+ $this->stack->popTag( 'form' );
}
- $this->stack->generateImpliedEndTags();
- $this->stack->popTag( 'form' );
- }
- return true;
+ return true;
- case 'p':
- if ( !$this->stack->inButtonScope( 'p' ) ) {
- $this->inBodyMode( 'tag', 'p', [] );
- return $this->insertToken( $token, $value, $attribs, $selfClose );
- }
- $this->stack->generateImpliedEndTags( $value );
- $this->stack->popTag( $value );
- return true;
+ case 'p':
+ if ( !$this->stack->inButtonScope( 'p' ) ) {
+ $this->inBodyMode( 'tag', 'p', [] );
+ return $this->insertToken( $token, $value, $attribs, $selfClose );
+ }
+ $this->stack->generateImpliedEndTags( $value );
+ $this->stack->popTag( $value );
+ return true;
- case 'li':
- if ( !$this->stack->inListItemScope( $value ) ) {
- return true; // ignore
- }
- $this->stack->generateImpliedEndTags( $value );
- $this->stack->popTag( $value );
- return true;
+ case 'li':
+ if ( !$this->stack->inListItemScope( $value ) ) {
+ return true; // ignore
+ }
+ $this->stack->generateImpliedEndTags( $value );
+ $this->stack->popTag( $value );
+ return true;
- case 'dd':
- case 'dt':
- if ( !$this->stack->inScope( $value ) ) {
- return true; // ignore
- }
- $this->stack->generateImpliedEndTags( $value );
- $this->stack->popTag( $value );
- return true;
+ case 'dd':
+ case 'dt':
+ if ( !$this->stack->inScope( $value ) ) {
+ return true; // ignore
+ }
+ $this->stack->generateImpliedEndTags( $value );
+ $this->stack->popTag( $value );
+ return true;
- case 'h1':
- case 'h2':
- case 'h3':
- case 'h4':
- case 'h5':
- case 'h6':
- if ( !$this->stack->inScope( BalanceSets::$headingSet ) ) {
- return true; // ignore
- }
- $this->stack->generateImpliedEndTags();
- $this->stack->popTag( BalanceSets::$headingSet );
- return true;
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ if ( !$this->stack->inScope( BalanceSets::$headingSet ) ) {
+ return true; // ignore
+ }
+ $this->stack->generateImpliedEndTags();
+ $this->stack->popTag( BalanceSets::$headingSet );
+ return true;
- case 'sarcasm':
- // Take a deep breath, then:
- break;
+ case 'sarcasm':
+ // Take a deep breath, then:
+ break;
- case 'a':
- case 'b':
- case 'big':
- case 'code':
- case 'em':
- case 'font':
- case 'i':
- case 'nobr':
- case 's':
- case 'small':
- case 'strike':
- case 'strong':
- case 'tt':
- case 'u':
- if ( $this->stack->adoptionAgency( $value, $this->afe ) ) {
- return true; // If we did something, we're done.
- }
- break; // Go to the "any other end tag" case.
+ case 'a':
+ case 'b':
+ case 'big':
+ case 'code':
+ case 'em':
+ case 'font':
+ case 'i':
+ case 'nobr':
+ case 's':
+ case 'small':
+ case 'strike':
+ case 'strong':
+ case 'tt':
+ case 'u':
+ if ( $this->stack->adoptionAgency( $value, $this->afe ) ) {
+ return true; // If we did something, we're done.
+ }
+ break; // Go to the "any other end tag" case.
- case 'applet':
- case 'marquee':
- case 'object':
- if ( !$this->stack->inScope( $value ) ) {
- return true; // ignore
- }
- $this->stack->generateImpliedEndTags();
- $this->stack->popTag( $value );
- $this->afe->clearToMarker();
- return true;
+ case 'applet':
+ case 'marquee':
+ case 'object':
+ if ( !$this->stack->inScope( $value ) ) {
+ return true; // ignore
+ }
+ $this->stack->generateImpliedEndTags();
+ $this->stack->popTag( $value );
+ $this->afe->clearToMarker();
+ return true;
- case 'br':
- // Turn </br> into <br>
- return $this->inBodyMode( 'tag', $value, [] );
+ case 'br':
+ // Turn </br> into <br>
+ return $this->inBodyMode( 'tag', $value, [] );
}
// Any other end tag goes here
@@ -2987,87 +2989,87 @@ class Balancer {
return true;
} elseif ( $token === 'tag' ) {
switch ( $value ) {
- case 'caption':
- $this->afe->insertMarker();
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->switchMode( 'inCaptionMode' );
- return true;
- case 'colgroup':
- $this->stack->clearToContext( BalanceSets::$tableContextSet );
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->switchMode( 'inColumnGroupMode' );
- return true;
- case 'col':
- $this->inTableMode( 'tag', 'colgroup', [] );
- return $this->insertToken( $token, $value, $attribs, $selfClose );
- case 'tbody':
- case 'tfoot':
- case 'thead':
- $this->stack->clearToContext( BalanceSets::$tableContextSet );
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->switchMode( 'inTableBodyMode' );
- return true;
- case 'td':
- case 'th':
- case 'tr':
- $this->inTableMode( 'tag', 'tbody', [] );
- return $this->insertToken( $token, $value, $attribs, $selfClose );
- case 'table':
- if ( !$this->stack->inTableScope( $value ) ) {
- return true; // Ignore this tag.
- }
- $this->inTableMode( 'endtag', $value );
- return $this->insertToken( $token, $value, $attribs, $selfClose );
-
- case 'style':
- // OMITTED: <script>
- case 'template':
- return $this->inHeadMode( $token, $value, $attribs, $selfClose );
+ case 'caption':
+ $this->afe->insertMarker();
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->switchMode( 'inCaptionMode' );
+ return true;
+ case 'colgroup':
+ $this->stack->clearToContext( BalanceSets::$tableContextSet );
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->switchMode( 'inColumnGroupMode' );
+ return true;
+ case 'col':
+ $this->inTableMode( 'tag', 'colgroup', [] );
+ return $this->insertToken( $token, $value, $attribs, $selfClose );
+ case 'tbody':
+ case 'tfoot':
+ case 'thead':
+ $this->stack->clearToContext( BalanceSets::$tableContextSet );
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->switchMode( 'inTableBodyMode' );
+ return true;
+ case 'td':
+ case 'th':
+ case 'tr':
+ $this->inTableMode( 'tag', 'tbody', [] );
+ return $this->insertToken( $token, $value, $attribs, $selfClose );
+ case 'table':
+ if ( !$this->stack->inTableScope( $value ) ) {
+ return true; // Ignore this tag.
+ }
+ $this->inTableMode( 'endtag', $value );
+ return $this->insertToken( $token, $value, $attribs, $selfClose );
- case 'input':
- if ( !isset( $attribs['type'] ) || strcasecmp( $attribs['type'], 'hidden' ) !== 0 ) {
- break; // Handle this as "everything else"
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->stack->pop();
- return true;
+ case 'style':
+ // OMITTED: <script>
+ case 'template':
+ return $this->inHeadMode( $token, $value, $attribs, $selfClose );
- case 'form':
- if (
- $this->formElementPointer ||
- $this->stack->indexOf( 'template' ) >= 0
- ) {
- return true; // ignore this token
- }
- $this->formElementPointer =
+ case 'input':
+ if ( !isset( $attribs['type'] ) || strcasecmp( $attribs['type'], 'hidden' ) !== 0 ) {
+ break; // Handle this as "everything else"
+ }
$this->stack->insertHTMLElement( $value, $attribs );
- $this->stack->popTag( $this->formElementPointer );
- return true;
+ $this->stack->pop();
+ return true;
+
+ case 'form':
+ if (
+ $this->formElementPointer ||
+ $this->stack->indexOf( 'template' ) >= 0
+ ) {
+ return true; // ignore this token
+ }
+ $this->formElementPointer =
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->stack->popTag( $this->formElementPointer );
+ return true;
}
// Fall through for "anything else" clause.
} elseif ( $token === 'endtag' ) {
switch ( $value ) {
- case 'table':
- if ( !$this->stack->inTableScope( $value ) ) {
- return true; // Ignore.
- }
- $this->stack->popTag( $value );
- $this->resetInsertionMode();
- return true;
- // OMITTED: <body>
- case 'caption':
- case 'col':
- case 'colgroup':
- // OMITTED: <html>
- case 'tbody':
- case 'td':
- case 'tfoot':
- case 'th':
- case 'thead':
- case 'tr':
- return true; // Ignore the token.
- case 'template':
- return $this->inHeadMode( $token, $value, $attribs, $selfClose );
+ case 'table':
+ if ( !$this->stack->inTableScope( $value ) ) {
+ return true; // Ignore.
+ }
+ $this->stack->popTag( $value );
+ $this->resetInsertionMode();
+ return true;
+ // OMITTED: <body>
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ // OMITTED: <html>
+ case 'tbody':
+ case 'td':
+ case 'tfoot':
+ case 'th':
+ case 'thead':
+ case 'tr':
+ return true; // Ignore the token.
+ case 'template':
+ return $this->inHeadMode( $token, $value, $attribs, $selfClose );
}
// Fall through for "anything else" clause.
} elseif ( $token === 'comment' ) {
@@ -3118,43 +3120,43 @@ class Balancer {
private function inCaptionMode( $token, $value, $attribs = null, $selfClose = false ) {
if ( $token === 'tag' ) {
switch ( $value ) {
- case 'caption':
- case 'col':
- case 'colgroup':
- case 'tbody':
- case 'td':
- case 'tfoot':
- case 'th':
- case 'thead':
- case 'tr':
- if ( $this->endCaption() ) {
- $this->insertToken( $token, $value, $attribs, $selfClose );
- }
- return true;
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ case 'tbody':
+ case 'td':
+ case 'tfoot':
+ case 'th':
+ case 'thead':
+ case 'tr':
+ if ( $this->endCaption() ) {
+ $this->insertToken( $token, $value, $attribs, $selfClose );
+ }
+ return true;
}
// Fall through to "anything else" case.
} elseif ( $token === 'endtag' ) {
switch ( $value ) {
- case 'caption':
- $this->endCaption();
- return true;
- case 'table':
- if ( $this->endCaption() ) {
- $this->insertToken( $token, $value, $attribs, $selfClose );
- }
- return true;
- case 'body':
- case 'col':
- case 'colgroup':
- // OMITTED: <html>
- case 'tbody':
- case 'td':
- case 'tfoot':
- case 'th':
- case 'thead':
- case 'tr':
- // Ignore the token
- return true;
+ case 'caption':
+ $this->endCaption();
+ return true;
+ case 'table':
+ if ( $this->endCaption() ) {
+ $this->insertToken( $token, $value, $attribs, $selfClose );
+ }
+ return true;
+ case 'body':
+ case 'col':
+ case 'colgroup':
+ // OMITTED: <html>
+ case 'tbody':
+ case 'td':
+ case 'tfoot':
+ case 'th':
+ case 'thead':
+ case 'tr':
+ // Ignore the token
+ return true;
}
// Fall through to "anything else" case.
}
@@ -3174,28 +3176,28 @@ class Balancer {
// Fall through to handle non-whitespace below.
} elseif ( $token === 'tag' ) {
switch ( $value ) {
- // OMITTED: <html>
- case 'col':
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->stack->pop();
- return true;
- case 'template':
- return $this->inHeadMode( $token, $value, $attribs, $selfClose );
+ // OMITTED: <html>
+ case 'col':
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->stack->pop();
+ return true;
+ case 'template':
+ return $this->inHeadMode( $token, $value, $attribs, $selfClose );
}
// Fall through for "anything else".
} elseif ( $token === 'endtag' ) {
switch ( $value ) {
- case 'colgroup':
- if ( !$this->stack->currentNode->isHtmlNamed( 'colgroup' ) ) {
+ case 'colgroup':
+ if ( !$this->stack->currentNode->isHtmlNamed( 'colgroup' ) ) {
+ return true; // Ignore the token.
+ }
+ $this->stack->pop();
+ $this->switchMode( 'inTableMode' );
+ return true;
+ case 'col':
return true; // Ignore the token.
- }
- $this->stack->pop();
- $this->switchMode( 'inTableMode' );
- return true;
- case 'col':
- return true; // Ignore the token.
- case 'template':
- return $this->inHeadMode( $token, $value, $attribs, $selfClose );
+ case 'template':
+ return $this->inHeadMode( $token, $value, $attribs, $selfClose );
}
// Fall through for "anything else".
} elseif ( $token === 'eof' ) {
@@ -3230,50 +3232,50 @@ class Balancer {
private function inTableBodyMode( $token, $value, $attribs = null, $selfClose = false ) {
if ( $token === 'tag' ) {
switch ( $value ) {
- case 'tr':
- $this->stack->clearToContext( BalanceSets::$tableBodyContextSet );
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->switchMode( 'inRowMode' );
- return true;
- case 'th':
- case 'td':
- $this->inTableBodyMode( 'tag', 'tr', [] );
- $this->insertToken( $token, $value, $attribs, $selfClose );
- return true;
- case 'caption':
- case 'col':
- case 'colgroup':
- case 'tbody':
- case 'tfoot':
- case 'thead':
- if ( $this->endSection() ) {
+ case 'tr':
+ $this->stack->clearToContext( BalanceSets::$tableBodyContextSet );
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->switchMode( 'inRowMode' );
+ return true;
+ case 'th':
+ case 'td':
+ $this->inTableBodyMode( 'tag', 'tr', [] );
$this->insertToken( $token, $value, $attribs, $selfClose );
- }
- return true;
+ return true;
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ case 'tbody':
+ case 'tfoot':
+ case 'thead':
+ if ( $this->endSection() ) {
+ $this->insertToken( $token, $value, $attribs, $selfClose );
+ }
+ return true;
}
} elseif ( $token === 'endtag' ) {
switch ( $value ) {
- case 'table':
- if ( $this->endSection() ) {
- $this->insertToken( $token, $value, $attribs, $selfClose );
- }
- return true;
- case 'tbody':
- case 'tfoot':
- case 'thead':
- if ( $this->stack->inTableScope( $value ) ) {
- $this->endSection();
- }
- return true;
- // OMITTED: <body>
- case 'caption':
- case 'col':
- case 'colgroup':
- // OMITTED: <html>
- case 'td':
- case 'th':
- case 'tr':
- return true; // Ignore the token.
+ case 'table':
+ if ( $this->endSection() ) {
+ $this->insertToken( $token, $value, $attribs, $selfClose );
+ }
+ return true;
+ case 'tbody':
+ case 'tfoot':
+ case 'thead':
+ if ( $this->stack->inTableScope( $value ) ) {
+ $this->endSection();
+ }
+ return true;
+ // OMITTED: <body>
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ // OMITTED: <html>
+ case 'td':
+ case 'th':
+ case 'tr':
+ return true; // Ignore the token.
}
}
// Anything else:
@@ -3293,53 +3295,53 @@ class Balancer {
private function inRowMode( $token, $value, $attribs = null, $selfClose = false ) {
if ( $token === 'tag' ) {
switch ( $value ) {
- case 'th':
- case 'td':
- $this->stack->clearToContext( BalanceSets::$tableRowContextSet );
- $this->stack->insertHTMLElement( $value, $attribs );
- $this->switchMode( 'inCellMode' );
- $this->afe->insertMarker();
- return true;
- case 'caption':
- case 'col':
- case 'colgroup':
- case 'tbody':
- case 'tfoot':
- case 'thead':
- case 'tr':
- if ( $this->endRow() ) {
- $this->insertToken( $token, $value, $attribs, $selfClose );
- }
- return true;
+ case 'th':
+ case 'td':
+ $this->stack->clearToContext( BalanceSets::$tableRowContextSet );
+ $this->stack->insertHTMLElement( $value, $attribs );
+ $this->switchMode( 'inCellMode' );
+ $this->afe->insertMarker();
+ return true;
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ case 'tbody':
+ case 'tfoot':
+ case 'thead':
+ case 'tr':
+ if ( $this->endRow() ) {
+ $this->insertToken( $token, $value, $attribs, $selfClose );
+ }
+ return true;
}
} elseif ( $token === 'endtag' ) {
switch ( $value ) {
- case 'tr':
- $this->endRow();
- return true;
- case 'table':
- if ( $this->endRow() ) {
- $this->insertToken( $token, $value, $attribs, $selfClose );
- }
- return true;
- case 'tbody':
- case 'tfoot':
- case 'thead':
- if (
- $this->stack->inTableScope( $value ) &&
- $this->endRow()
- ) {
- $this->insertToken( $token, $value, $attribs, $selfClose );
- }
- return true;
- // OMITTED: <body>
- case 'caption':
- case 'col':
- case 'colgroup':
- // OMITTED: <html>
- case 'td':
- case 'th':
- return true; // Ignore the token.
+ case 'tr':
+ $this->endRow();
+ return true;
+ case 'table':
+ if ( $this->endRow() ) {
+ $this->insertToken( $token, $value, $attribs, $selfClose );
+ }
+ return true;
+ case 'tbody':
+ case 'tfoot':
+ case 'thead':
+ if (
+ $this->stack->inTableScope( $value ) &&
+ $this->endRow()
+ ) {
+ $this->insertToken( $token, $value, $attribs, $selfClose );
+ }
+ return true;
+ // OMITTED: <body>
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ // OMITTED: <html>
+ case 'td':
+ case 'th':
+ return true; // Ignore the token.
}
}
// Anything else:
@@ -3361,51 +3363,51 @@ class Balancer {
private function inCellMode( $token, $value, $attribs = null, $selfClose = false ) {
if ( $token === 'tag' ) {
switch ( $value ) {
- case 'caption':
- case 'col':
- case 'colgroup':
- case 'tbody':
- case 'td':
- case 'tfoot':
- case 'th':
- case 'thead':
- case 'tr':
- if ( $this->endCell() ) {
- $this->insertToken( $token, $value, $attribs, $selfClose );
- }
- return true;
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ case 'tbody':
+ case 'td':
+ case 'tfoot':
+ case 'th':
+ case 'thead':
+ case 'tr':
+ if ( $this->endCell() ) {
+ $this->insertToken( $token, $value, $attribs, $selfClose );
+ }
+ return true;
}
} elseif ( $token === 'endtag' ) {
switch ( $value ) {
- case 'td':
- case 'th':
- if ( $this->stack->inTableScope( $value ) ) {
- $this->stack->generateImpliedEndTags();
- $this->stack->popTag( $value );
- $this->afe->clearToMarker();
- $this->switchMode( 'inRowMode' );
- }
- return true;
- // OMITTED: <body>
- case 'caption':
- case 'col':
- case 'colgroup':
- // OMITTED: <html>
- return true;
+ case 'td':
+ case 'th':
+ if ( $this->stack->inTableScope( $value ) ) {
+ $this->stack->generateImpliedEndTags();
+ $this->stack->popTag( $value );
+ $this->afe->clearToMarker();
+ $this->switchMode( 'inRowMode' );
+ }
+ return true;
+ // OMITTED: <body>
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ // OMITTED: <html>
+ return true;
- case 'table':
- case 'tbody':
- case 'tfoot':
- case 'thead':
- case 'tr':
- if ( $this->stack->inTableScope( $value ) ) {
- $this->stack->generateImpliedEndTags();
- $this->stack->popTag( BalanceSets::$tableCellSet );
- $this->afe->clearToMarker();
- $this->switchMode( 'inRowMode' );
- $this->insertToken( $token, $value, $attribs, $selfClose );
- }
- return true;
+ case 'table':
+ case 'tbody':
+ case 'tfoot':
+ case 'thead':
+ case 'tr':
+ if ( $this->stack->inTableScope( $value ) ) {
+ $this->stack->generateImpliedEndTags();
+ $this->stack->popTag( BalanceSets::$tableCellSet );
+ $this->afe->clearToMarker();
+ $this->switchMode( 'inRowMode' );
+ $this->insertToken( $token, $value, $attribs, $selfClose );
+ }
+ return true;
}
}
// Anything else:
@@ -3420,65 +3422,65 @@ class Balancer {
return $this->inBodyMode( $token, $value, $attribs, $selfClose );
} elseif ( $token === 'tag' ) {
switch ( $value ) {
- // OMITTED: <html>
- case 'option':
- if ( $this->stack->currentNode->isHtmlNamed( 'option' ) ) {
- $this->stack->pop();
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- return true;
- case 'optgroup':
- if ( $this->stack->currentNode->isHtmlNamed( 'option' ) ) {
- $this->stack->pop();
- }
- if ( $this->stack->currentNode->isHtmlNamed( 'optgroup' ) ) {
- $this->stack->pop();
- }
- $this->stack->insertHTMLElement( $value, $attribs );
- return true;
- case 'select':
- $this->inSelectMode( 'endtag', $value ); // treat it like endtag
- return true;
- case 'input':
- case 'keygen':
- case 'textarea':
- if ( !$this->stack->inSelectScope( 'select' ) ) {
- return true; // ignore token (fragment case)
- }
- $this->inSelectMode( 'endtag', 'select' );
- return $this->insertToken( $token, $value, $attribs, $selfClose );
- case 'script':
- case 'template':
- return $this->inHeadMode( $token, $value, $attribs, $selfClose );
+ // OMITTED: <html>
+ case 'option':
+ if ( $this->stack->currentNode->isHtmlNamed( 'option' ) ) {
+ $this->stack->pop();
+ }
+ $this->stack->insertHTMLElement( $value, $attribs );
+ return true;
+ case 'optgroup':
+ if ( $this->stack->currentNode->isHtmlNamed( 'option' ) ) {
+ $this->stack->pop();
+ }
+ if ( $this->stack->currentNode->isHtmlNamed( 'optgroup' ) ) {
+ $this->stack->pop();
+ }
+ $this->stack->insertHTMLElement( $value, $attribs );
+ return true;
+ case 'select':
+ $this->inSelectMode( 'endtag', $value ); // treat it like endtag
+ return true;
+ case 'input':
+ case 'keygen':
+ case 'textarea':
+ if ( !$this->stack->inSelectScope( 'select' ) ) {
+ return true; // ignore token (fragment case)
+ }
+ $this->inSelectMode( 'endtag', 'select' );
+ return $this->insertToken( $token, $value, $attribs, $selfClose );
+ case 'script':
+ case 'template':
+ return $this->inHeadMode( $token, $value, $attribs, $selfClose );
}
} elseif ( $token === 'endtag' ) {
switch ( $value ) {
- case 'optgroup':
- if (
- $this->stack->currentNode->isHtmlNamed( 'option' ) &&
- $this->stack->length() >= 2 &&
- $this->stack->node( $this->stack->length() - 2 )->isHtmlNamed( 'optgroup' )
- ) {
- $this->stack->pop();
- }
- if ( $this->stack->currentNode->isHtmlNamed( 'optgroup' ) ) {
- $this->stack->pop();
- }
- return true;
- case 'option':
- if ( $this->stack->currentNode->isHtmlNamed( 'option' ) ) {
- $this->stack->pop();
- }
- return true;
- case 'select':
- if ( !$this->stack->inSelectScope( $value ) ) {
- return true; // fragment case
- }
- $this->stack->popTag( $value );
- $this->resetInsertionMode();
- return true;
- case 'template':
- return $this->inHeadMode( $token, $value, $attribs, $selfClose );
+ case 'optgroup':
+ if (
+ $this->stack->currentNode->isHtmlNamed( 'option' ) &&
+ $this->stack->length() >= 2 &&
+ $this->stack->node( $this->stack->length() - 2 )->isHtmlNamed( 'optgroup' )
+ ) {
+ $this->stack->pop();
+ }
+ if ( $this->stack->currentNode->isHtmlNamed( 'optgroup' ) ) {
+ $this->stack->pop();
+ }
+ return true;
+ case 'option':
+ if ( $this->stack->currentNode->isHtmlNamed( 'option' ) ) {
+ $this->stack->pop();
+ }
+ return true;
+ case 'select':
+ if ( !$this->stack->inSelectScope( $value ) ) {
+ return true; // fragment case
+ }
+ $this->stack->popTag( $value );
+ $this->resetInsertionMode();
+ return true;
+ case 'template':
+ return $this->inHeadMode( $token, $value, $attribs, $selfClose );
}
} elseif ( $token === 'comment' ) {
$this->stack->insertComment( $value );
@@ -3490,24 +3492,24 @@ class Balancer {
private function inSelectInTableMode( $token, $value, $attribs = null, $selfClose = false ) {
switch ( $value ) {
- case 'caption':
- case 'table':
- case 'tbody':
- case 'tfoot':
- case 'thead':
- case 'tr':
- case 'td':
- case 'th':
- if ( $token === 'tag' ) {
- $this->inSelectInTableMode( 'endtag', 'select' );
- return $this->insertToken( $token, $value, $attribs, $selfClose );
- } elseif ( $token === 'endtag' ) {
- if ( $this->stack->inTableScope( $value ) ) {
+ case 'caption':
+ case 'table':
+ case 'tbody':
+ case 'tfoot':
+ case 'thead':
+ case 'tr':
+ case 'td':
+ case 'th':
+ if ( $token === 'tag' ) {
$this->inSelectInTableMode( 'endtag', 'select' );
return $this->insertToken( $token, $value, $attribs, $selfClose );
+ } elseif ( $token === 'endtag' ) {
+ if ( $this->stack->inTableScope( $value ) ) {
+ $this->inSelectInTableMode( 'endtag', 'select' );
+ return $this->insertToken( $token, $value, $attribs, $selfClose );
+ }
+ return true;
}
- return true;
- }
}
// anything else
return $this->inSelectMode( $token, $value, $attribs, $selfClose );
@@ -3529,50 +3531,50 @@ class Balancer {
return true;
} elseif ( $token === 'tag' ) {
switch ( $value ) {
- case 'base':
- case 'basefont':
- case 'bgsound':
- case 'link':
- case 'meta':
- case 'noframes':
- // OMITTED: <script>
- case 'style':
- case 'template':
- // OMITTED: <title>
- return $this->inHeadMode( $token, $value, $attribs, $selfClose );
+ case 'base':
+ case 'basefont':
+ case 'bgsound':
+ case 'link':
+ case 'meta':
+ case 'noframes':
+ // OMITTED: <script>
+ case 'style':
+ case 'template':
+ // OMITTED: <title>
+ return $this->inHeadMode( $token, $value, $attribs, $selfClose );
- case 'caption':
- case 'colgroup':
- case 'tbody':
- case 'tfoot':
- case 'thead':
- return $this->switchModeAndReprocess(
- 'inTableMode', $token, $value, $attribs, $selfClose
- );
+ case 'caption':
+ case 'colgroup':
+ case 'tbody':
+ case 'tfoot':
+ case 'thead':
+ return $this->switchModeAndReprocess(
+ 'inTableMode', $token, $value, $attribs, $selfClose
+ );
- case 'col':
- return $this->switchModeAndReprocess(
- 'inColumnGroupMode', $token, $value, $attribs, $selfClose
- );
+ case 'col':
+ return $this->switchModeAndReprocess(
+ 'inColumnGroupMode', $token, $value, $attribs, $selfClose
+ );
- case 'tr':
- return $this->switchModeAndReprocess(
- 'inTableBodyMode', $token, $value, $attribs, $selfClose
- );
+ case 'tr':
+ return $this->switchModeAndReprocess(
+ 'inTableBodyMode', $token, $value, $attribs, $selfClose
+ );
- case 'td':
- case 'th':
- return $this->switchModeAndReprocess(
- 'inRowMode', $token, $value, $attribs, $selfClose
- );
+ case 'td':
+ case 'th':
+ return $this->switchModeAndReprocess(
+ 'inRowMode', $token, $value, $attribs, $selfClose
+ );
}
return $this->switchModeAndReprocess(
'inBodyMode', $token, $value, $attribs, $selfClose
);
} elseif ( $token === 'endtag' ) {
switch ( $value ) {
- case 'template':
- return $this->inHeadMode( $token, $value, $attribs, $selfClose );
+ case 'template':
+ return $this->inHeadMode( $token, $value, $attribs, $selfClose );
}
return true;
} else {
diff --git a/www/wiki/includes/tidy/RemexCompatMunger.php b/www/wiki/includes/tidy/RemexCompatMunger.php
index 73bc5f84..e6351e2e 100644
--- a/www/wiki/includes/tidy/RemexCompatMunger.php
+++ b/www/wiki/includes/tidy/RemexCompatMunger.php
@@ -28,6 +28,7 @@ class RemexCompatMunger implements TreeHandler {
"button" => true,
"cite" => true,
"code" => true,
+ "del" => true,
"dfn" => true,
"em" => true,
"font" => true,
@@ -35,6 +36,7 @@ class RemexCompatMunger implements TreeHandler {
"iframe" => true,
"img" => true,
"input" => true,
+ "ins" => true,
"kbd" => true,
"label" => true,
"legend" => true,
@@ -61,6 +63,13 @@ class RemexCompatMunger implements TreeHandler {
"tt" => true,
"u" => true,
"var" => true,
+ // Those defined in tidy.conf
+ "video" => true,
+ "audio" => true,
+ "bdi" => true,
+ "data" => true,
+ "time" => true,
+ "mark" => true,
];
private static $formattingElements = [
@@ -174,6 +183,10 @@ class RemexCompatMunger implements TreeHandler {
$length, $sourceStart, $sourceLength );
}
+ private function trace( $msg ) {
+ // echo "[RCM] $msg\n";
+ }
+
/**
* Insert or reparent an element. Create p-wrappers or split the tag stack
* as necessary.
@@ -242,25 +255,27 @@ class RemexCompatMunger implements TreeHandler {
if ( $under && $parentData->isPWrapper && !$inline ) {
// [B/b] The element is non-inline and the parent is a p-wrapper,
// close the parent and insert into its parent instead
+ $this->trace( 'insert B/b' );
$newParent = $this->serializer->getParentNode( $parent );
$parent = $newParent;
$parentData = $parent->snData;
$pElement = $parentData->childPElement;
$parentData->childPElement = null;
$newRef = $refElement->userData;
- $this->endTag( $pElement, $sourceStart, 0 );
} elseif ( $under && $parentData->isSplittable
&& (bool)$parentData->ancestorPNode !== $inline
) {
// [CS/b, DS/i] The parent is splittable and the current element is
// inline in block context, or if the current element is a block
// under a p-wrapper, split the tag stack.
+ $this->trace( $inline ? 'insert DS/i' : 'insert CS/b' );
$newRef = $this->splitTagStack( $newRef, $inline, $sourceStart );
$parent = $newRef;
$parentData = $parent->snData;
} elseif ( $under && $parentData->needsPWrapping && $inline ) {
// [A/i] If the element is inline and we are in body/blockquote,
// we need to create a p-wrapper
+ $this->trace( 'insert A/i' );
$newRef = $this->insertPWrapper( $newRef, $sourceStart );
$parent = $newRef;
$parentData = $parent->snData;
@@ -268,9 +283,12 @@ class RemexCompatMunger implements TreeHandler {
// [CU/b] If the element is non-inline and (despite attempting to
// split above) there is still an ancestor p-wrap, disable that
// p-wrap
+ $this->trace( 'insert CU/b' );
$this->disablePWrapper( $parent, $sourceStart );
+ } else {
+ // [A/b, B/i, C/i, D/b, DU/i] insert as normal
+ $this->trace( 'insert normal' );
}
- // else [A/b, B/i, C/i, D/b, DU/i] insert as normal
// An element with element children is a non-blank element
$parentData->nonblankNodeCount++;
@@ -457,6 +475,20 @@ class RemexCompatMunger implements TreeHandler {
public function reparentChildren( Element $element, Element $newParent, $sourceStart ) {
$self = $element->userData;
+ if ( $self->snData->childPElement ) {
+ // Reparent under the p-wrapper instead, so that e.g.
+ // <blockquote><mw:p-wrap>...</mw:p-wrap></blockquote>
+ // becomes
+ // <blockquote><mw:p-wrap><i>...</i></mw:p-wrap></blockquote>
+
+ // The formatting element should not be the parent of the p-wrap.
+ // Without this special case, the insertElement() of the <i> below
+ // would be diverted into the p-wrapper, causing infinite recursion
+ // (T178632)
+ $this->reparentChildren( $self->snData->childPElement, $newParent, $sourceStart );
+ return;
+ }
+
$children = $self->children;
$self->children = [];
$this->insertElement( TreeBuilder::UNDER, $element, $newParent, false, $sourceStart, 0 );
@@ -464,6 +496,7 @@ class RemexCompatMunger implements TreeHandler {
$newParentId = $newParentNode->id;
foreach ( $children as $child ) {
if ( is_object( $child ) ) {
+ $this->trace( "reparent <{$child->name}>" );
$child->parentId = $newParentId;
}
}
diff --git a/www/wiki/includes/tidy/RemexMungerData.php b/www/wiki/includes/tidy/RemexMungerData.php
index d614a381..08d148f6 100644
--- a/www/wiki/includes/tidy/RemexMungerData.php
+++ b/www/wiki/includes/tidy/RemexMungerData.php
@@ -75,4 +75,43 @@ class RemexMungerData {
public function __set( $name, $value ) {
throw new \Exception( "Cannot set property \"$name\"" );
}
+
+ /**
+ * Get a text representation of the current state of the serializer, for
+ * debugging.
+ *
+ * @return string
+ */
+ public function dump() {
+ if ( $this->childPElement ) {
+ $parts[] = 'childPElement=' . $this->childPElement->getDebugTag();
+ }
+ if ( $this->ancestorPNode ) {
+ $parts[] = "ancestorPNode=<{$this->ancestorPNode->name}>";
+ }
+ if ( $this->wrapBaseNode ) {
+ $parts[] = "wrapBaseNode=<{$this->wrapBaseNode->name}>";
+ }
+ if ( $this->currentCloneElement ) {
+ $parts[] = "currentCloneElement=" . $this->currentCloneElement->getDebugTag();
+ }
+ if ( $this->isPWrapper ) {
+ $parts[] = 'isPWrapper';
+ }
+ if ( $this->isSplittable ) {
+ $parts[] = 'isSplittable';
+ }
+ if ( $this->needsPWrapping ) {
+ $parts[] = 'needsPWrapping';
+ }
+ if ( $this->nonblankNodeCount ) {
+ $parts[] = "nonblankNodeCount={$this->nonblankNodeCount}";
+ }
+ $s = "RemexMungerData {\n";
+ foreach ( $parts as $part ) {
+ $s .= " $part\n";
+ }
+ $s .= "}\n";
+ return $s;
+ }
}
diff --git a/www/wiki/includes/tidy/TidyDriverBase.php b/www/wiki/includes/tidy/TidyDriverBase.php
index f88b6734..a53360c9 100644
--- a/www/wiki/includes/tidy/TidyDriverBase.php
+++ b/www/wiki/includes/tidy/TidyDriverBase.php
@@ -21,18 +21,6 @@ abstract class TidyDriverBase {
}
/**
- * Check HTML for errors, used if $wgValidateAllHtml = true.
- *
- * @param string $text
- * @param string &$errorStr Return the error string
- * @throws \MWException
- * @return bool Whether the HTML is valid
- */
- public function validate( $text, &$errorStr ) {
- throw new \MWException( static::class . ' does not support validate()' );
- }
-
- /**
* Clean up HTML
*
* @param string $text HTML document fragment to clean up
diff --git a/www/wiki/includes/title/ForeignTitleFactory.php b/www/wiki/includes/title/ForeignTitleFactory.php
index 427afdf3..07dd346d 100644
--- a/www/wiki/includes/title/ForeignTitleFactory.php
+++ b/www/wiki/includes/title/ForeignTitleFactory.php
@@ -16,7 +16,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
diff --git a/www/wiki/includes/title/ImportTitleFactory.php b/www/wiki/includes/title/ImportTitleFactory.php
index 629616d8..4baab22a 100644
--- a/www/wiki/includes/title/ImportTitleFactory.php
+++ b/www/wiki/includes/title/ImportTitleFactory.php
@@ -16,7 +16,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
diff --git a/www/wiki/includes/title/MediaWikiPageLinkRenderer.php b/www/wiki/includes/title/MediaWikiPageLinkRenderer.php
deleted file mode 100644
index a5652710..00000000
--- a/www/wiki/includes/title/MediaWikiPageLinkRenderer.php
+++ /dev/null
@@ -1,134 +0,0 @@
-<?php
-/**
- * A service for generating links from page titles
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @license GPL 2+
- * @author Daniel Kinzler
- */
-use MediaWiki\Linker\LinkTarget;
-
-/**
- * A service for generating links from page titles.
- *
- * @see https://www.mediawiki.org/wiki/Requests_for_comment/TitleValue
- * @since 1.23
- */
-class MediaWikiPageLinkRenderer implements PageLinkRenderer {
- /**
- * @var TitleFormatter
- */
- protected $formatter;
-
- /**
- * @var string
- */
- protected $baseUrl;
-
- /**
- * @note $formatter and $baseUrl are currently not used for generating links,
- * since we still rely on the Linker class to generate the actual HTML.
- * Once this is reversed so that Linker becomes a legacy interface to
- * HtmlPageLinkRenderer, we will be using them, so it seems prudent to
- * already declare the dependency and inject them.
- *
- * @param TitleFormatter $formatter Formatter for generating the target title string
- * @param string $baseUrl (currently unused, pending refactoring of Linker).
- * Defaults to $wgArticlePath.
- */
- public function __construct( TitleFormatter $formatter, $baseUrl = null ) {
- if ( $baseUrl === null ) {
- $baseUrl = $GLOBALS['wgArticlePath'];
- }
-
- $this->formatter = $formatter;
- $this->baseUrl = $baseUrl;
- }
-
- /**
- * Returns the (partial) URL for the given page (including any section identifier).
- *
- * @param LinkTarget $page The link's target
- * @param array $params Any additional URL parameters.
- *
- * @return string
- */
- public function getPageUrl( LinkTarget $page, $params = [] ) {
- // TODO: move the code from Linker::linkUrl here!
- // The below is just a rough estimation!
-
- $name = $this->formatter->getPrefixedText( $page );
- $name = str_replace( ' ', '_', $name );
- $name = wfUrlencode( $name );
-
- $url = $this->baseUrl . $name;
-
- if ( $params ) {
- $separator = ( strpos( $url, '?' ) ) ? '&' : '?';
- $url .= $separator . wfArrayToCgi( $params );
- }
-
- $fragment = $page->getFragment();
- if ( $fragment !== '' ) {
- $url = $url . '#' . wfUrlencode( $fragment );
- }
-
- return $url;
- }
-
- /**
- * Returns an HTML link to the given page, using the given surface text.
- *
- * @param LinkTarget $linkTarget The link's target
- * @param string $text The link's surface text (will be derived from $page if not given).
- *
- * @return string
- */
- public function renderHtmlLink( LinkTarget $linkTarget, $text = null ) {
- if ( $text === null ) {
- $text = $this->formatter->getFullText( $linkTarget );
- }
-
- // TODO: move the logic implemented by Linker here,
- // using $this->formatter and $this->baseUrl, and
- // re-implement Linker to use a HtmlPageLinkRenderer.
-
- $title = Title::newFromLinkTarget( $linkTarget );
- $link = Linker::link( $title, htmlspecialchars( $text ) );
-
- return $link;
- }
-
- /**
- * Returns a wikitext link to the given page, using the given surface text.
- *
- * @param LinkTarget $page The link's target
- * @param string $text The link's surface text (will be derived from $page if not given).
- *
- * @return string
- */
- public function renderWikitextLink( LinkTarget $page, $text = null ) {
- if ( $text === null ) {
- $text = $this->formatter->getFullText( $page );
- }
-
- $name = $this->formatter->getFullText( $page );
-
- return '[[:' . $name . '|' . wfEscapeWikiText( $text ) . ']]';
- }
-}
diff --git a/www/wiki/includes/title/MediaWikiTitleCodec.php b/www/wiki/includes/title/MediaWikiTitleCodec.php
index efc0fd4a..890a870a 100644
--- a/www/wiki/includes/title/MediaWikiTitleCodec.php
+++ b/www/wiki/includes/title/MediaWikiTitleCodec.php
@@ -1,6 +1,6 @@
<?php
/**
- * A codec for %MediaWiki page titles.
+ * A codec for MediaWiki page titles.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,7 +18,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Daniel Kinzler
*/
use MediaWiki\Interwiki\InterwikiLookup;
@@ -26,7 +25,7 @@ use MediaWiki\MediaWikiServices;
use MediaWiki\Linker\LinkTarget;
/**
- * A codec for %MediaWiki page titles.
+ * A codec for MediaWiki page titles.
*
* @note Normalization and validation is applied while parsing, not when formatting.
* It's possible to construct a TitleValue with an invalid title, and use MediaWikiTitleCodec
diff --git a/www/wiki/includes/title/NaiveForeignTitleFactory.php b/www/wiki/includes/title/NaiveForeignTitleFactory.php
index 2c2f94b2..44cf90d8 100644
--- a/www/wiki/includes/title/NaiveForeignTitleFactory.php
+++ b/www/wiki/includes/title/NaiveForeignTitleFactory.php
@@ -16,7 +16,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
diff --git a/www/wiki/includes/title/NaiveImportTitleFactory.php b/www/wiki/includes/title/NaiveImportTitleFactory.php
index 43c662e7..5cb61120 100644
--- a/www/wiki/includes/title/NaiveImportTitleFactory.php
+++ b/www/wiki/includes/title/NaiveImportTitleFactory.php
@@ -16,7 +16,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
diff --git a/www/wiki/includes/title/NamespaceAwareForeignTitleFactory.php b/www/wiki/includes/title/NamespaceAwareForeignTitleFactory.php
index 4d24cb85..c271f264 100644
--- a/www/wiki/includes/title/NamespaceAwareForeignTitleFactory.php
+++ b/www/wiki/includes/title/NamespaceAwareForeignTitleFactory.php
@@ -16,7 +16,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
diff --git a/www/wiki/includes/title/NamespaceImportTitleFactory.php b/www/wiki/includes/title/NamespaceImportTitleFactory.php
index 0c1d0c40..7c756aa1 100644
--- a/www/wiki/includes/title/NamespaceImportTitleFactory.php
+++ b/www/wiki/includes/title/NamespaceImportTitleFactory.php
@@ -16,7 +16,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
diff --git a/www/wiki/includes/title/PageLinkRenderer.php b/www/wiki/includes/title/PageLinkRenderer.php
deleted file mode 100644
index e26fe1a2..00000000
--- a/www/wiki/includes/title/PageLinkRenderer.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-/**
- * Represents a link rendering service for %MediaWiki.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @license GPL 2+
- * @author Daniel Kinzler
- */
-use MediaWiki\Linker\LinkTarget;
-
-/**
- * Represents a link rendering service for %MediaWiki.
- *
- * This is designed to encapsulate the knowledge about how page titles map to
- * URLs, and how links are encoded in a given output format.
- *
- * @see https://www.mediawiki.org/wiki/Requests_for_comment/TitleValue
- * @since 1.23
- */
-interface PageLinkRenderer {
- /**
- * Returns the URL for the given page.
- *
- * @todo expand this to cover the functionality of Linker::linkUrl
- *
- * @param LinkTarget $page The link's target
- * @param array $params Any additional URL parameters.
- *
- * @return string
- */
- public function getPageUrl( LinkTarget $page, $params = [] );
-
- /**
- * Returns an HTML link to the given page, using the given surface text.
- *
- * @todo expand this to cover the functionality of Linker::link
- *
- * @param LinkTarget $page The link's target
- * @param string $text The link's surface text (will be derived from $page if not given).
- *
- * @return string
- */
- public function renderHtmlLink( LinkTarget $page, $text = null );
-
- /**
- * Returns a wikitext link to the given page, using the given surface text.
- *
- * @param LinkTarget $page The link's target
- * @param string $text The link's surface text (will be derived from $page if not given).
- *
- * @return string
- */
- public function renderWikitextLink( LinkTarget $page, $text = null );
-}
diff --git a/www/wiki/includes/title/SubpageImportTitleFactory.php b/www/wiki/includes/title/SubpageImportTitleFactory.php
index b0be7afa..42443503 100644
--- a/www/wiki/includes/title/SubpageImportTitleFactory.php
+++ b/www/wiki/includes/title/SubpageImportTitleFactory.php
@@ -16,7 +16,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
*/
/**
diff --git a/www/wiki/includes/title/TitleFormatter.php b/www/wiki/includes/title/TitleFormatter.php
index 5177606f..4551d75a 100644
--- a/www/wiki/includes/title/TitleFormatter.php
+++ b/www/wiki/includes/title/TitleFormatter.php
@@ -1,6 +1,6 @@
<?php
/**
- * A title formatter service for %MediaWiki.
+ * A title formatter service for MediaWiki.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,7 +18,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Daniel Kinzler
*/
use MediaWiki\Linker\LinkTarget;
diff --git a/www/wiki/includes/title/TitleParser.php b/www/wiki/includes/title/TitleParser.php
index 381b1d09..de65be85 100644
--- a/www/wiki/includes/title/TitleParser.php
+++ b/www/wiki/includes/title/TitleParser.php
@@ -18,7 +18,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Daniel Kinzler
*/
diff --git a/www/wiki/includes/title/TitleValue.php b/www/wiki/includes/title/TitleValue.php
index 7c370f1a..3e133006 100644
--- a/www/wiki/includes/title/TitleValue.php
+++ b/www/wiki/includes/title/TitleValue.php
@@ -1,6 +1,6 @@
<?php
/**
- * Representation of a page title within %MediaWiki.
+ * Representation of a page title within MediaWiki.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,14 +18,13 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @license GPL 2+
* @author Daniel Kinzler
*/
use MediaWiki\Linker\LinkTarget;
use Wikimedia\Assert\Assert;
/**
- * Represents a page (or page fragment) title within %MediaWiki.
+ * Represents a page (or page fragment) title within MediaWiki.
*
* @note In contrast to Title, this is designed to be a plain value object. That is,
* it is immutable, does not use global state, and causes no side effects.
@@ -34,22 +33,27 @@ use Wikimedia\Assert\Assert;
* @since 1.23
*/
class TitleValue implements LinkTarget {
+
/**
+ * @deprecated in 1.31. This class is immutable. Use the getter for access.
* @var int
*/
protected $namespace;
/**
+ * @deprecated in 1.31. This class is immutable. Use the getter for access.
* @var string
*/
protected $dbkey;
/**
+ * @deprecated in 1.31. This class is immutable. Use the getter for access.
* @var string
*/
protected $fragment;
/**
+ * @deprecated in 1.31. This class is immutable. Use the getter for access.
* @var string
*/
protected $interwiki;
@@ -89,6 +93,7 @@ class TitleValue implements LinkTarget {
}
/**
+ * @since 1.23
* @return int
*/
public function getNamespace() {
@@ -105,6 +110,7 @@ class TitleValue implements LinkTarget {
}
/**
+ * @since 1.23
* @return string
*/
public function getFragment() {
@@ -122,6 +128,7 @@ class TitleValue implements LinkTarget {
/**
* Returns the title's DB key, as supplied to the constructor,
* without namespace prefix or fragment.
+ * @since 1.23
*
* @return string
*/
@@ -132,6 +139,7 @@ class TitleValue implements LinkTarget {
/**
* Returns the title in text form,
* without namespace prefix or fragment.
+ * @since 1.23
*
* This is computed from the DB key by replacing any underscores with spaces.
*
@@ -185,6 +193,7 @@ class TitleValue implements LinkTarget {
* Returns a string representation of the title, for logging. This is purely informative
* and must not be used programmatically. Use the appropriate TitleFormatter to generate
* the correct string representation for a given use.
+ * @since 1.23
*
* @return string
*/
diff --git a/www/wiki/includes/upload/UploadBase.php b/www/wiki/includes/upload/UploadBase.php
index da3f9f82..ae591645 100644
--- a/www/wiki/includes/upload/UploadBase.php
+++ b/www/wiki/includes/upload/UploadBase.php
@@ -420,7 +420,7 @@ abstract class UploadBase {
$chunk = fread( $fp, 256 );
fclose( $fp );
- $magic = MimeMagic::singleton();
+ $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
$extMime = $magic->guessTypesForExtension( $this->mFinalExtension );
$ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
foreach ( $ieTypes as $ieType ) {
@@ -446,7 +446,7 @@ abstract class UploadBase {
return $status;
}
- $mwProps = new MWFileProps( MimeMagic::singleton() );
+ $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
$this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
$mime = $this->mFileProps['mime'];
@@ -505,7 +505,7 @@ abstract class UploadBase {
# getTitle() sets some internal parameters like $this->mFinalExtension
$this->getTitle();
- $mwProps = new MWFileProps( MimeMagic::singleton() );
+ $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
$this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
# check MIME type, if desired
@@ -674,7 +674,10 @@ abstract class UploadBase {
$warnings['was-deleted'] = $filename;
}
- $dupes = $this->checkAgainstExistingDupes( $hash );
+ // If a file with the same name exists locally then the local file has already been tested
+ // for duplication of content
+ $ignoreLocalDupes = isset( $warnings[ 'exists '] );
+ $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
if ( $dupes ) {
$warnings['duplicate'] = $dupes;
}
@@ -789,15 +792,19 @@ abstract class UploadBase {
/**
* @param string $hash sha1 hash of the file to check
+ * @param bool $ignoreLocalDupes True to ignore local duplicates
*
* @return File[] Duplicate files, if found.
*/
- private function checkAgainstExistingDupes( $hash ) {
+ private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
$dupes = RepoGroup::singleton()->findBySha1( $hash );
$title = $this->getTitle();
- // Remove all matches against self
foreach ( $dupes as $key => $dupe ) {
- if ( $title->equals( $dupe->getTitle() ) ) {
+ if (
+ ( $dupe instanceof LocalFile ) &&
+ $ignoreLocalDupes &&
+ $title->equals( $dupe->getTitle() )
+ ) {
unset( $dupes[$key] );
}
}
@@ -950,7 +957,7 @@ abstract class UploadBase {
$this->mFinalExtension = '';
# No extension, try guessing one
- $magic = MimeMagic::singleton();
+ $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
$mime = $magic->guessMimeType( $this->mTempPath );
if ( $mime !== 'unknown/unknown' ) {
# Get a space separated list of extensions
@@ -1207,7 +1214,7 @@ abstract class UploadBase {
* @return bool
*/
public static function verifyExtension( $mime, $extension ) {
- $magic = MimeMagic::singleton();
+ $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
if ( !$mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) {
if ( !$magic->isRecognizableExtension( $extension ) ) {
@@ -1390,7 +1397,7 @@ abstract class UploadBase {
*/
public static function checkXMLEncodingMissmatch( $file ) {
global $wgSVGMetadataCutoff;
- $contents = file_get_contents( $file, false, null, -1, $wgSVGMetadataCutoff );
+ $contents = file_get_contents( $file, false, null, 0, $wgSVGMetadataCutoff );
$encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
@@ -1418,9 +1425,9 @@ abstract class UploadBase {
// detect the encoding in case is specifies an encoding not whitelisted in self::$safeXmlEncodings
$attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ];
foreach ( $attemptEncodings as $encoding ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$str = iconv( $encoding, 'UTF-8', $contents );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
if ( preg_match( $encodingRegex, $matches[1], $encMatch )
&& !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
@@ -1656,9 +1663,8 @@ abstract class UploadBase {
# image/svg, text/xml, application/xml, and text/html, which can contain scripts
if ( $stripped == 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
// rfc2397 parameters. This is only slightly slower than (;[\w;]+)*.
- // @codingStandardsIgnoreStart Generic.Files.LineLength
+ // phpcs:ignore Generic.Files.LineLength
$parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
- // @codingStandardsIgnoreEnd
if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
wfDebug( __METHOD__ . ": Found href to unwhitelisted data: uri "
diff --git a/www/wiki/includes/upload/UploadStash.php b/www/wiki/includes/upload/UploadStash.php
index 755f9fdf..e55ab1fb 100644
--- a/www/wiki/includes/upload/UploadStash.php
+++ b/www/wiki/includes/upload/UploadStash.php
@@ -18,7 +18,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup Upload
*/
/**
@@ -53,7 +52,7 @@
*/
class UploadStash {
// Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg)
- const KEY_FORMAT_REGEX = '/^[\w-\.]+\.\w*$/';
+ const KEY_FORMAT_REGEX = '/^[\w\-\.]+\.\w*$/';
const MAX_US_PROPS_SIZE = 65535;
/**
@@ -118,12 +117,15 @@ class UploadStash {
*/
public function getFile( $key, $noAuth = false ) {
if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
- throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
+ throw new UploadStashBadPathException(
+ wfMessage( 'uploadstash-bad-path-bad-format', $key )
+ );
}
if ( !$noAuth && !$this->isLoggedIn ) {
- throw new UploadStashNotLoggedInException( __METHOD__ .
- ' No user is logged in, files must belong to users' );
+ throw new UploadStashNotLoggedInException(
+ wfMessage( 'uploadstash-not-logged-in' )
+ );
}
if ( !isset( $this->fileMetadata[$key] ) ) {
@@ -134,7 +136,9 @@ class UploadStash {
}
if ( !isset( $this->fileMetadata[$key] ) ) {
- throw new UploadStashFileNotFoundException( "key '$key' not found in stash" );
+ throw new UploadStashFileNotFoundException(
+ wfMessage( 'uploadstash-file-not-found', $key )
+ );
}
// create $this->files[$key]
@@ -153,13 +157,16 @@ class UploadStash {
if ( !$this->files[$key]->exists() ) {
wfDebug( __METHOD__ . " tried to get file at $key, but it doesn't exist\n" );
// @todo Is this not an UploadStashFileNotFoundException case?
- throw new UploadStashBadPathException( "path doesn't exist" );
+ throw new UploadStashBadPathException(
+ wfMessage( 'uploadstash-bad-path' )
+ );
}
if ( !$noAuth ) {
if ( $this->fileMetadata[$key]['us_user'] != $this->userId ) {
- throw new UploadStashWrongOwnerException( "This file ($key) doesn't "
- . "belong to the current user." );
+ throw new UploadStashWrongOwnerException(
+ wfMessage( 'uploadstash-wrong-owner', $key )
+ );
}
}
@@ -205,10 +212,12 @@ class UploadStash {
public function stashFile( $path, $sourceType = null ) {
if ( !is_file( $path ) ) {
wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist\n" );
- throw new UploadStashBadPathException( "path doesn't exist" );
+ throw new UploadStashBadPathException(
+ wfMessage( 'uploadstash-bad-path' )
+ );
}
- $mwProps = new MWFileProps( MimeMagic::singleton() );
+ $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
$fileProps = $mwProps->getPropsFromPath( $path, true );
wfDebug( __METHOD__ . " stashing file at '$path'\n" );
@@ -236,7 +245,9 @@ class UploadStash {
$this->fileProps[$key] = $fileProps;
if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
- throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
+ throw new UploadStashBadPathException(
+ wfMessage( 'uploadstash-bad-path-bad-format', $key )
+ );
}
wfDebug( __METHOD__ . " key for '$path': $key\n" );
@@ -265,15 +276,15 @@ class UploadStash {
// At this point, $error should contain the single "most important"
// error, plus any parameters.
$errorMsg = array_shift( $error );
- throw new UploadStashFileException( "Error storing file in '$path': "
- . wfMessage( $errorMsg, $error )->text() );
+ throw new UploadStashFileException( wfMessage( $errorMsg, $error ) );
}
$stashPath = $storeStatus->value;
// fetch the current user ID
if ( !$this->isLoggedIn ) {
- throw new UploadStashNotLoggedInException( __METHOD__
- . ' No user is logged in, files must belong to users' );
+ throw new UploadStashNotLoggedInException(
+ wfMessage( 'uploadstash-not-logged-in' )
+ );
}
// insert the file metadata into the db.
@@ -332,8 +343,9 @@ class UploadStash {
*/
public function clear() {
if ( !$this->isLoggedIn ) {
- throw new UploadStashNotLoggedInException( __METHOD__
- . ' No user is logged in, files must belong to users' );
+ throw new UploadStashNotLoggedInException(
+ wfMessage( 'uploadstash-not-logged-in' )
+ );
}
wfDebug( __METHOD__ . ' clearing all rows for user ' . $this->userId . "\n" );
@@ -361,8 +373,9 @@ class UploadStash {
*/
public function removeFile( $key ) {
if ( !$this->isLoggedIn ) {
- throw new UploadStashNotLoggedInException( __METHOD__
- . ' No user is logged in, files must belong to users' );
+ throw new UploadStashNotLoggedInException(
+ wfMessage( 'uploadstash-not-logged-in' )
+ );
}
$dbw = $this->repo->getMasterDB();
@@ -377,12 +390,15 @@ class UploadStash {
);
if ( !$row ) {
- throw new UploadStashNoSuchKeyException( "No such key ($key), cannot remove" );
+ throw new UploadStashNoSuchKeyException(
+ wfMessage( 'uploadstash-no-such-key', $key )
+ );
}
if ( $row->us_user != $this->userId ) {
- throw new UploadStashWrongOwnerException( "Can't delete: "
- . "the file ($key) doesn't belong to this user." );
+ throw new UploadStashWrongOwnerException(
+ wfMessage( 'uploadstash-wrong-owner', $key )
+ );
}
return $this->removeFileNoAuth( $key );
@@ -427,8 +443,9 @@ class UploadStash {
*/
public function listFiles() {
if ( !$this->isLoggedIn ) {
- throw new UploadStashNotLoggedInException( __METHOD__
- . ' No user is logged in, files must belong to users' );
+ throw new UploadStashNotLoggedInException(
+ wfMessage( 'uploadstash-not-logged-in' )
+ );
}
$dbr = $this->repo->getReplicaDB();
@@ -472,16 +489,18 @@ class UploadStash {
$extension = $n ? substr( $path, $n + 1 ) : '';
} else {
// If not, assume that it should be related to the MIME type of the original file.
- $magic = MimeMagic::singleton();
+ $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
$mimeType = $magic->guessMimeType( $path );
- $extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) );
+ $extensions = explode( ' ', $magic->getExtensionsForType( $mimeType ) );
if ( count( $extensions ) ) {
$extension = $extensions[0];
}
}
if ( is_null( $extension ) ) {
- throw new UploadStashFileException( "extension is null" );
+ throw new UploadStashFileException(
+ wfMessage( 'uploadstash-no-extension' )
+ );
}
$extension = File::normalizeExtension( $extension );
@@ -515,7 +534,12 @@ class UploadStash {
$row = $dbr->selectRow(
'uploadstash',
- '*',
+ [
+ 'us_user', 'us_key', 'us_orig_path', 'us_path', 'us_props',
+ 'us_size', 'us_sha1', 'us_mime', 'us_media_type',
+ 'us_image_width', 'us_image_height', 'us_image_bits',
+ 'us_source_type', 'us_timestamp', 'us_status',
+ ],
[ 'us_key' => $key ],
__METHOD__
);
@@ -541,7 +565,9 @@ class UploadStash {
protected function initFile( $key ) {
$file = new UploadStashFile( $this->repo, $this->fileMetadata[$key]['us_path'], $key );
if ( $file->getSize() === 0 ) {
- throw new UploadStashZeroLengthFileException( "File is zero length" );
+ throw new UploadStashZeroLengthFileException(
+ wfMessage( 'uploadstash-zero-length' )
+ );
}
$this->files[$key] = $file;
@@ -549,6 +575,9 @@ class UploadStash {
}
}
+/**
+ * @ingroup Upload
+ */
class UploadStashFile extends UnregisteredLocalFile {
private $fileKey;
private $urlName;
@@ -581,14 +610,18 @@ class UploadStashFile extends UnregisteredLocalFile {
) {
wfDebug( "UploadStash: tried to construct an UploadStashFile "
. "from a file that should already exist at '$path', but path is not valid\n" );
- throw new UploadStashBadPathException( 'path is not valid' );
+ throw new UploadStashBadPathException(
+ wfMessage( 'uploadstash-bad-path-invalid' )
+ );
}
// check if path exists! and is a plain file.
if ( !$repo->fileExists( $path ) ) {
wfDebug( "UploadStash: tried to construct an UploadStashFile from "
. "a file that should already exist at '$path', but path is not found\n" );
- throw new UploadStashFileNotFoundException( 'cannot find path, or not a plain file' );
+ throw new UploadStashFileNotFoundException(
+ wfMessage( 'uploadstash-file-not-found-not-exists' )
+ );
}
}
@@ -734,26 +767,70 @@ class UploadStashFile extends UnregisteredLocalFile {
}
}
-class UploadStashException extends MWException {
+/**
+ * @ingroup Upload
+ */
+class UploadStashException extends MWException implements ILocalizedException {
+ /** @var string|array|MessageSpecifier */
+ protected $messageSpec;
+
+ /**
+ * @param string|array|MessageSpecifier $messageSpec See Message::newFromSpecifier
+ * @param int $code Exception code
+ * @param Exception|Throwable $previous The previous exception used for the exception chaining.
+ */
+ public function __construct( $messageSpec, $code = 0, $previous = null ) {
+ $this->messageSpec = $messageSpec;
+
+ $msg = $this->getMessageObject()->text();
+ $msg = preg_replace( '!</?(var|kbd|samp|code)>!', '"', $msg );
+ $msg = Sanitizer::stripAllTags( $msg );
+ parent::__construct( $msg, $code, $previous );
+ }
+
+ public function getMessageObject() {
+ return Message::newFromSpecifier( $this->messageSpec );
+ }
}
+/**
+ * @ingroup Upload
+ */
class UploadStashFileNotFoundException extends UploadStashException {
}
+/**
+ * @ingroup Upload
+ */
class UploadStashBadPathException extends UploadStashException {
}
+/**
+ * @ingroup Upload
+ */
class UploadStashFileException extends UploadStashException {
}
+/**
+ * @ingroup Upload
+ */
class UploadStashZeroLengthFileException extends UploadStashException {
}
+/**
+ * @ingroup Upload
+ */
class UploadStashNotLoggedInException extends UploadStashException {
}
+/**
+ * @ingroup Upload
+ */
class UploadStashWrongOwnerException extends UploadStashException {
}
+/**
+ * @ingroup Upload
+ */
class UploadStashNoSuchKeyException extends UploadStashException {
}
diff --git a/www/wiki/includes/user/BotPassword.php b/www/wiki/includes/user/BotPassword.php
index 0a8b3609..8074c328 100644
--- a/www/wiki/includes/user/BotPassword.php
+++ b/www/wiki/includes/user/BotPassword.php
@@ -18,6 +18,7 @@
* http://www.gnu.org/copyleft/gpl.html
*/
+use MediaWiki\MediaWikiServices;
use MediaWiki\Session\BotPasswordSessionProvider;
use Wikimedia\Rdbms\IMaintainableDatabase;
@@ -74,9 +75,10 @@ class BotPassword implements IDBAccessObject {
public static function getDB( $db ) {
global $wgBotPasswordsCluster, $wgBotPasswordsDatabase;
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$lb = $wgBotPasswordsCluster
- ? wfGetLBFactory()->getExternalLB( $wgBotPasswordsCluster )
- : wfGetLB( $wgBotPasswordsDatabase );
+ ? $lbFactory->getExternalLB( $wgBotPasswordsCluster )
+ : $lbFactory->getMainLB( $wgBotPasswordsDatabase );
return $lb->getConnectionRef( $db, [], $wgBotPasswordsDatabase );
}
diff --git a/www/wiki/includes/user/CentralIdLookup.php b/www/wiki/includes/user/CentralIdLookup.php
index 618b7f07..2a86f4a8 100644
--- a/www/wiki/includes/user/CentralIdLookup.php
+++ b/www/wiki/includes/user/CentralIdLookup.php
@@ -19,6 +19,7 @@
*
* @file
*/
+use Wikimedia\ObjectFactory;
/**
* The CentralIdLookup service allows for connecting local users with
diff --git a/www/wiki/includes/user/ExternalUserNames.php b/www/wiki/includes/user/ExternalUserNames.php
new file mode 100644
index 00000000..f953b144
--- /dev/null
+++ b/www/wiki/includes/user/ExternalUserNames.php
@@ -0,0 +1,133 @@
+<?php
+/**
+ * Class to parse and build external user names
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * Class to parse and build external user names
+ * @since 1.31
+ */
+class ExternalUserNames {
+ private $usernamePrefix = 'imported';
+ private $assignKnownUsers = false;
+ private $triedCreations = [];
+
+ /**
+ * @param string $usernamePrefix Prefix to apply to unknown (and possibly also known) usernames
+ * @param bool $assignKnownUsers Whether to apply the prefix to usernames that exist locally
+ */
+ public function __construct( $usernamePrefix, $assignKnownUsers ) {
+ $this->usernamePrefix = rtrim( (string)$usernamePrefix, ':>' );
+ $this->assignKnownUsers = (bool)$assignKnownUsers;
+ }
+
+ /**
+ * Get a target Title to link a username.
+ *
+ * @param string $userName Username to link
+ *
+ * @return null|Title
+ */
+ public static function getUserLinkTitle( $userName ) {
+ $pos = strpos( $userName, '>' );
+ if ( $pos !== false ) {
+ $iw = explode( ':', substr( $userName, 0, $pos ) );
+ $firstIw = array_shift( $iw );
+ $interwikiLookup = MediaWikiServices::getInstance()->getInterwikiLookup();
+ if ( $interwikiLookup->isValidInterwiki( $firstIw ) ) {
+ $title = MWNamespace::getCanonicalName( NS_USER ) . ':' . substr( $userName, $pos + 1 );
+ if ( $iw ) {
+ $title = implode( ':', $iw ) . ':' . $title;
+ }
+ return Title::makeTitle( NS_MAIN, $title, '', $firstIw );
+ }
+ return null;
+ } else {
+ return SpecialPage::getTitleFor( 'Contributions', $userName );
+ }
+ }
+
+ /**
+ * Add an interwiki prefix to the username, if appropriate
+ *
+ * @param string $name Name being imported
+ * @return string Name, possibly with the prefix prepended.
+ */
+ public function applyPrefix( $name ) {
+ if ( !User::isUsableName( $name ) ) {
+ return $name;
+ }
+
+ if ( $this->assignKnownUsers ) {
+ if ( User::idFromName( $name ) ) {
+ return $name;
+ }
+
+ // See if any extension wants to create it.
+ if ( !isset( $this->triedCreations[$name] ) ) {
+ $this->triedCreations[$name] = true;
+ if ( !Hooks::run( 'ImportHandleUnknownUser', [ $name ] ) &&
+ User::idFromName( $name, User::READ_LATEST )
+ ) {
+ return $name;
+ }
+ }
+ }
+
+ return $this->addPrefix( $name );
+ }
+
+ /**
+ * Add an interwiki prefix to the username regardless of circumstances
+ *
+ * @param string $name Name being imported
+ * @return string Name
+ */
+ public function addPrefix( $name ) {
+ return substr( $this->usernamePrefix . '>' . $name, 0, 255 );
+ }
+
+ /**
+ * Tells whether the username is external or not
+ *
+ * @param string $username Username to check
+ * @return bool true if it's external, false otherwise.
+ */
+ public static function isExternal( $username ) {
+ return strpos( $username, '>' ) !== false;
+ }
+
+ /**
+ * Get local part of the user name
+ *
+ * @param string $username Username to get
+ * @return string
+ */
+ public static function getLocal( $username ) {
+ if ( !self::isExternal( $username ) ) {
+ return $username;
+ }
+
+ return substr( $username, strpos( $username, '>' ) + 1 );
+ }
+
+}
diff --git a/www/wiki/includes/user/PasswordReset.php b/www/wiki/includes/user/PasswordReset.php
index dd16fb78..faf09eef 100644
--- a/www/wiki/includes/user/PasswordReset.php
+++ b/www/wiki/includes/user/PasswordReset.php
@@ -288,11 +288,14 @@ class PasswordReset implements LoggerAwareInterface {
* @throws MWException On unexpected database errors
*/
protected function getUsersByEmail( $email ) {
+ $userQuery = User::getQueryInfo();
$res = wfGetDB( DB_REPLICA )->select(
- 'user',
- User::selectFields(),
+ $userQuery['tables'],
+ $userQuery['fields'],
[ 'user_email' => $email ],
- __METHOD__
+ __METHOD__,
+ [],
+ $userQuery['joins']
);
if ( !$res ) {
diff --git a/www/wiki/includes/user/User.php b/www/wiki/includes/user/User.php
index 9de30979..464629a4 100644
--- a/www/wiki/includes/user/User.php
+++ b/www/wiki/includes/user/User.php
@@ -20,16 +20,18 @@
* @file
*/
-use IPSet\IPSet;
use MediaWiki\MediaWikiServices;
use MediaWiki\Session\SessionManager;
use MediaWiki\Session\Token;
use MediaWiki\Auth\AuthManager;
use MediaWiki\Auth\AuthenticationResponse;
use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\User\UserIdentity;
+use Wikimedia\IPSet;
use Wikimedia\ScopedCallback;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\DBExpectedError;
+use Wikimedia\Rdbms\IDatabase;
/**
* String Some punctuation to prevent editing from broken text-mangling proxies.
@@ -48,7 +50,7 @@ define( 'EDIT_TOKEN_SUFFIX', Token::SUFFIX );
* for rendering normal pages are set in the cookie to minimize use
* of the database.
*/
-class User implements IDBAccessObject {
+class User implements IDBAccessObject, UserIdentity {
/**
* @const int Number of characters in user_token field.
*/
@@ -69,7 +71,7 @@ class User implements IDBAccessObject {
/**
* @const int Serialized record version.
*/
- const VERSION = 11;
+ const VERSION = 12;
/**
* Exclude user options that are set to their default value.
@@ -110,6 +112,8 @@ class User implements IDBAccessObject {
'mGroupMemberships',
// user_properties table
'mOptionOverrides',
+ // actor table
+ 'mActorId',
];
/**
@@ -146,10 +150,12 @@ class User implements IDBAccessObject {
'editmyoptions',
'editmyprivateinfo',
'editmyusercss',
+ 'editmyuserjson',
'editmyuserjs',
'editmywatchlist',
'editsemiprotected',
'editusercss',
+ 'edituserjson',
'edituserjs',
'hideuser',
'import',
@@ -206,6 +212,8 @@ class User implements IDBAccessObject {
public $mId;
/** @var string */
public $mName;
+ /** @var int|null */
+ protected $mActorId;
/** @var string */
public $mRealName;
@@ -227,12 +235,7 @@ class User implements IDBAccessObject {
protected $mRegistration;
/** @var int */
protected $mEditCount;
- /**
- * @var array No longer used since 1.29; use User::getGroups() instead
- * @deprecated since 1.29
- */
- private $mGroups;
- /** @var array Associative array of (group name => UserGroupMembership object) */
+ /** @var UserGroupMembership[] Associative array of (group name => UserGroupMembership object) */
protected $mGroupMemberships;
/** @var array */
protected $mOptionOverrides;
@@ -255,6 +258,7 @@ class User implements IDBAccessObject {
* - 'defaults' anonymous user initialised from class defaults
* - 'name' initialise from mName
* - 'id' initialise from mId
+ * - 'actor' initialise from mActorId
* - 'session' log in from session if possible
*
* Use the User::newFrom*() family of functions to set this.
@@ -313,6 +317,7 @@ class User implements IDBAccessObject {
*
* @see newFromName()
* @see newFromId()
+ * @see newFromActorId()
* @see newFromConfirmationCode()
* @see newFromSession()
* @see newFromRow()
@@ -388,7 +393,8 @@ class User implements IDBAccessObject {
break;
case 'name':
// Make sure this thread sees its own changes
- if ( wfGetLB()->hasOrMadeRecentMasterChanges() ) {
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ if ( $lb->hasOrMadeRecentMasterChanges() ) {
$flags |= self::READ_LATEST;
$this->queryFlagsUsed = $flags;
}
@@ -402,8 +408,43 @@ class User implements IDBAccessObject {
}
break;
case 'id':
+ // Make sure this thread sees its own changes, if the ID isn't 0
+ if ( $this->mId != 0 ) {
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ if ( $lb->hasOrMadeRecentMasterChanges() ) {
+ $flags |= self::READ_LATEST;
+ $this->queryFlagsUsed = $flags;
+ }
+ }
+
$this->loadFromId( $flags );
break;
+ case 'actor':
+ // Make sure this thread sees its own changes
+ if ( wfGetLB()->hasOrMadeRecentMasterChanges() ) {
+ $flags |= self::READ_LATEST;
+ $this->queryFlagsUsed = $flags;
+ }
+
+ list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
+ $row = wfGetDB( $index )->selectRow(
+ 'actor',
+ [ 'actor_user', 'actor_name' ],
+ [ 'actor_id' => $this->mActorId ],
+ __METHOD__,
+ $options
+ );
+
+ if ( !$row ) {
+ // Ugh.
+ $this->loadDefaults();
+ } elseif ( $row->actor_user ) {
+ $this->mId = $row->actor_user;
+ $this->loadFromId( $flags );
+ } else {
+ $this->loadDefaults( $row->actor_name );
+ }
+ break;
case 'session':
if ( !$this->loadFromSession() ) {
// Loading from session failed. Load defaults.
@@ -579,6 +620,78 @@ class User implements IDBAccessObject {
}
/**
+ * Static factory method for creation from a given actor ID.
+ *
+ * @since 1.31
+ * @param int $id Valid actor ID
+ * @return User The corresponding User object
+ */
+ public static function newFromActorId( $id ) {
+ global $wgActorTableSchemaMigrationStage;
+
+ if ( $wgActorTableSchemaMigrationStage <= MIGRATION_OLD ) {
+ throw new BadMethodCallException(
+ 'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage is MIGRATION_OLD'
+ );
+ }
+
+ $u = new User;
+ $u->mActorId = $id;
+ $u->mFrom = 'actor';
+ $u->setItemLoaded( 'actor' );
+ return $u;
+ }
+
+ /**
+ * Static factory method for creation from an ID, name, and/or actor ID
+ *
+ * This does not check that the ID, name, and actor ID all correspond to
+ * the same user.
+ *
+ * @since 1.31
+ * @param int|null $userId User ID, if known
+ * @param string|null $userName User name, if known
+ * @param int|null $actorId Actor ID, if known
+ * @return User
+ */
+ public static function newFromAnyId( $userId, $userName, $actorId ) {
+ global $wgActorTableSchemaMigrationStage;
+
+ $user = new User;
+ $user->mFrom = 'defaults';
+
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD && $actorId !== null ) {
+ $user->mActorId = (int)$actorId;
+ if ( $user->mActorId !== 0 ) {
+ $user->mFrom = 'actor';
+ }
+ $user->setItemLoaded( 'actor' );
+ }
+
+ if ( $userName !== null && $userName !== '' ) {
+ $user->mName = $userName;
+ $user->mFrom = 'name';
+ $user->setItemLoaded( 'name' );
+ }
+
+ if ( $userId !== null ) {
+ $user->mId = (int)$userId;
+ if ( $user->mId !== 0 ) {
+ $user->mFrom = 'id';
+ }
+ $user->setItemLoaded( 'id' );
+ }
+
+ if ( $user->mFrom === 'defaults' ) {
+ throw new InvalidArgumentException(
+ 'Cannot create a user with no name, no ID, and no actor ID'
+ );
+ }
+
+ return $user;
+ }
+
+ /**
* Factory method to fetch whichever user has a given email confirmation code.
* This code is generated when an account is created or its e-mail address
* has changed.
@@ -688,20 +801,25 @@ class User implements IDBAccessObject {
}
$dbr = wfGetDB( DB_REPLICA );
+ $userQuery = self::getQueryInfo();
$row = $dbr->selectRow(
- 'user',
- self::selectFields(),
+ $userQuery['tables'],
+ $userQuery['fields'],
[ 'user_name' => $name ],
- __METHOD__
+ __METHOD__,
+ [],
+ $userQuery['joins']
);
if ( !$row ) {
// Try the master database...
$dbw = wfGetDB( DB_MASTER );
$row = $dbw->selectRow(
- 'user',
- self::selectFields(),
+ $userQuery['tables'],
+ $userQuery['fields'],
[ 'user_name' => $name ],
- __METHOD__
+ __METHOD__,
+ [],
+ $userQuery['joins']
);
}
@@ -769,7 +887,7 @@ class User implements IDBAccessObject {
return null;
}
- if ( !( $flags & self::READ_LATEST ) && isset( self::$idCacheByName[$name] ) ) {
+ if ( !( $flags & self::READ_LATEST ) && array_key_exists( $name, self::$idCacheByName ) ) {
return self::$idCacheByName[$name];
}
@@ -1162,6 +1280,7 @@ class User implements IDBAccessObject {
public function loadDefaults( $name = false ) {
$this->mId = 0;
$this->mName = $name;
+ $this->mActorId = null;
$this->mRealName = '';
$this->mEmail = '';
$this->mOptionOverrides = null;
@@ -1278,12 +1397,14 @@ class User implements IDBAccessObject {
list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
$db = wfGetDB( $index );
+ $userQuery = self::getQueryInfo();
$s = $db->selectRow(
- 'user',
- self::selectFields(),
+ $userQuery['tables'],
+ $userQuery['fields'],
[ 'user_id' => $this->mId ],
__METHOD__,
- $options
+ $options,
+ $userQuery['joins']
);
$this->queryFlagsUsed = $flags;
@@ -1316,11 +1437,29 @@ class User implements IDBAccessObject {
* user_properties Array with properties out of the user_properties table
*/
protected function loadFromRow( $row, $data = null ) {
+ global $wgActorTableSchemaMigrationStage;
+
+ if ( !is_object( $row ) ) {
+ throw new InvalidArgumentException( '$row must be an object' );
+ }
+
$all = true;
$this->mGroupMemberships = null; // deferred
- if ( isset( $row->user_name ) ) {
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+ if ( isset( $row->actor_id ) ) {
+ $this->mActorId = (int)$row->actor_id;
+ if ( $this->mActorId !== 0 ) {
+ $this->mFrom = 'actor';
+ }
+ $this->setItemLoaded( 'actor' );
+ } else {
+ $all = false;
+ }
+ }
+
+ if ( isset( $row->user_name ) && $row->user_name !== '' ) {
$this->mName = $row->user_name;
$this->mFrom = 'name';
$this->setItemLoaded( 'name' );
@@ -1337,13 +1476,15 @@ class User implements IDBAccessObject {
if ( isset( $row->user_id ) ) {
$this->mId = intval( $row->user_id );
- $this->mFrom = 'id';
+ if ( $this->mId !== 0 ) {
+ $this->mFrom = 'id';
+ }
$this->setItemLoaded( 'id' );
} else {
$all = false;
}
- if ( isset( $row->user_id ) && isset( $row->user_name ) ) {
+ if ( isset( $row->user_id ) && isset( $row->user_name ) && $row->user_name !== '' ) {
self::$idCacheByName[$row->user_name] = $row->user_id;
}
@@ -1462,15 +1603,17 @@ class User implements IDBAccessObject {
}
$oldGroups = $this->getGroups(); // previous groups
+ $oldUGMs = $this->getGroupMemberships();
foreach ( $toPromote as $group ) {
$this->addGroup( $group );
}
+ $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
+ $newUGMs = $this->getGroupMemberships();
+
// update groups in external authentication database
- Hooks::run( 'UserGroupsChanged', [ $this, $toPromote, [], false, false ] );
+ Hooks::run( 'UserGroupsChanged', [ $this, $toPromote, [], false, false, $oldUGMs, $newUGMs ] );
AuthManager::callLegacyAuthPlugin( 'updateExternalDBGroups', [ $this, $toPromote ] );
- $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
-
$logEntry = new ManualLogEntry( 'rights', 'autopromote' );
$logEntry->setPerformer( $this );
$logEntry->setTarget( $this->getUserPage() );
@@ -1549,7 +1692,7 @@ class User implements IDBAccessObject {
* data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
*
* @param bool|string $reloadFrom Reload user and user_groups table data from a
- * given source. May be "name", "id", "defaults", "session", or false for no reload.
+ * given source. May be "name", "id", "actor", "defaults", "session", or false for no reload.
*/
public function clearInstanceCache( $reloadFrom = false ) {
$this->mNewtalk = -1;
@@ -1632,7 +1775,7 @@ class User implements IDBAccessObject {
* Check when actually saving should be done against master.
*/
private function getBlockedStatus( $bFromSlave = true ) {
- global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff, $wgSoftBlockRanges;
+ global $wgProxyWhitelist, $wgApplyIpBlocksToXff, $wgSoftBlockRanges;
if ( -1 != $this->mBlockedby ) {
return;
@@ -1651,15 +1794,14 @@ class User implements IDBAccessObject {
# user is not immune to autoblocks/hardblocks, and they are the current user so we
# know which IP address they're actually coming from
$ip = null;
- if ( !$this->isAllowed( 'ipblock-exempt' ) ) {
- // $wgUser->getName() only works after the end of Setup.php. Until
- // then, assume it's a logged-out user.
- $globalUserName = $wgUser->isSafeToLoad()
- ? $wgUser->getName()
- : IP::sanitizeIP( $wgUser->getRequest()->getIP() );
- if ( $this->getName() === $globalUserName ) {
- $ip = $this->getRequest()->getIP();
- }
+ $sessionUser = RequestContext::getMain()->getUser();
+ // the session user is set up towards the end of Setup.php. Until then,
+ // assume it's a logged-out user.
+ $globalUserName = $sessionUser->isSafeToLoad()
+ ? $sessionUser->getName()
+ : IP::sanitizeIP( $sessionUser->getRequest()->getIP() );
+ if ( $this->getName() === $globalUserName && !$this->isAllowed( 'ipblock-exempt' ) ) {
+ $ip = $this->getRequest()->getIP();
}
// User/IP blocking
@@ -1730,15 +1872,17 @@ class User implements IDBAccessObject {
$this->mHideName = $block->mHideName;
$this->mAllowUsertalk = !$block->prevents( 'editownusertalk' );
} else {
+ $this->mBlock = null;
$this->mBlockedby = '';
+ $this->mBlockreason = '';
$this->mHideName = 0;
$this->mAllowUsertalk = false;
}
// Avoid PHP 7.1 warning of passing $this by reference
- $user = $this;
+ $thisUser = $this;
// Extensions
- Hooks::run( 'GetBlockedStatus', [ &$user ] );
+ Hooks::run( 'GetBlockedStatus', [ &$thisUser ] );
}
/**
@@ -2280,6 +2424,73 @@ class User implements IDBAccessObject {
}
/**
+ * Get the user's actor ID.
+ * @since 1.31
+ * @param IDatabase|null $dbw Assign a new actor ID, using this DB handle, if none exists
+ * @return int The actor's ID, or 0 if no actor ID exists and $dbw was null
+ */
+ public function getActorId( IDatabase $dbw = null ) {
+ global $wgActorTableSchemaMigrationStage;
+
+ if ( $wgActorTableSchemaMigrationStage <= MIGRATION_OLD ) {
+ return 0;
+ }
+
+ if ( !$this->isItemLoaded( 'actor' ) ) {
+ $this->load();
+ }
+
+ // Currently $this->mActorId might be null if $this was loaded from a
+ // cache entry that was written when $wgActorTableSchemaMigrationStage
+ // was MIGRATION_OLD. Once that is no longer a possibility (i.e. when
+ // User::VERSION is incremented after $wgActorTableSchemaMigrationStage
+ // has been removed), that condition may be removed.
+ if ( $this->mActorId === null || !$this->mActorId && $dbw ) {
+ $q = [
+ 'actor_user' => $this->getId() ?: null,
+ 'actor_name' => (string)$this->getName(),
+ ];
+ if ( $dbw ) {
+ if ( $q['actor_user'] === null && self::isUsableName( $q['actor_name'] ) ) {
+ throw new CannotCreateActorException(
+ 'Cannot create an actor for a usable name that is not an existing user'
+ );
+ }
+ if ( $q['actor_name'] === '' ) {
+ throw new CannotCreateActorException( 'Cannot create an actor for a user with no name' );
+ }
+ $dbw->insert( 'actor', $q, __METHOD__, [ 'IGNORE' ] );
+ if ( $dbw->affectedRows() ) {
+ $this->mActorId = (int)$dbw->insertId();
+ } else {
+ // Outdated cache?
+ // Use LOCK IN SHARE MODE to bypass any MySQL REPEATABLE-READ snapshot.
+ $this->mActorId = (int)$dbw->selectField(
+ 'actor',
+ 'actor_id',
+ $q,
+ __METHOD__,
+ [ 'LOCK IN SHARE MODE' ]
+ );
+ if ( !$this->mActorId ) {
+ throw new CannotCreateActorException(
+ "Cannot create actor ID for user_id={$this->getId()} user_name={$this->getName()}"
+ );
+ }
+ }
+ $this->invalidateCache();
+ } else {
+ list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $this->queryFlagsUsed );
+ $db = wfGetDB( $index );
+ $this->mActorId = (int)$db->selectField( 'actor', 'actor_id', $q, __METHOD__, $options );
+ }
+ $this->setItemLoaded( 'actor' );
+ }
+
+ return (int)$this->mActorId;
+ }
+
+ /**
* Get the user's name escaped by underscores.
* @return string Username escaped by underscores.
*/
@@ -2504,12 +2715,17 @@ class User implements IDBAccessObject {
if ( $mode === 'refresh' ) {
$cache->delete( $key, 1 );
} else {
- wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle(
- function () use ( $cache, $key ) {
- $cache->delete( $key );
- },
- __METHOD__
- );
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ if ( $lb->hasOrMadeRecentMasterChanges() ) {
+ $lb->getConnection( DB_MASTER )->onTransactionPreCommitOrIdle(
+ function () use ( $cache, $key ) {
+ $cache->delete( $key );
+ },
+ __METHOD__
+ );
+ } else {
+ $cache->delete( $key );
+ }
}
}
@@ -2874,9 +3090,9 @@ class User implements IDBAccessObject {
* Get the user's current setting for a given option.
*
* @param string $oname The option to check
- * @param string $defaultOverride A default value returned if the option does not exist
+ * @param string|array $defaultOverride A default value returned if the option does not exist
* @param bool $ignoreHidden Whether to ignore the effects of $wgHiddenPrefs
- * @return string|null User's current value for the option
+ * @return string|array|int|null User's current value for the option
* @see getBoolOption()
* @see getIntOption()
*/
@@ -3079,12 +3295,13 @@ class User implements IDBAccessObject {
$options = $this->mOptions;
}
- $prefs = Preferences::getPreferences( $this, $context );
+ $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
+ $prefs = $preferencesFactory->getFormDescriptor( $this, $context );
$mapping = [];
// Pull out the "special" options, so they don't get converted as
// multiselect or checkmatrix.
- $specialOptions = array_fill_keys( Preferences::getSaveBlacklist(), true );
+ $specialOptions = array_fill_keys( $preferencesFactory->getSaveBlacklist(), true );
foreach ( $specialOptions as $name => $value ) {
unset( $prefs[$name] );
}
@@ -3094,7 +3311,7 @@ class User implements IDBAccessObject {
$multiselectOptions = [];
foreach ( $prefs as $name => $info ) {
if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
- ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
+ ( isset( $info['class'] ) && $info['class'] == HTMLMultiSelectField::class ) ) {
$opts = HTMLFormField::flattenOptions( $info['options'] );
$prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
@@ -3108,7 +3325,7 @@ class User implements IDBAccessObject {
$checkmatrixOptions = [];
foreach ( $prefs as $name => $info ) {
if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
- ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
+ ( isset( $info['class'] ) && $info['class'] == HTMLCheckMatrix::class ) ) {
$columns = HTMLFormField::flattenOptions( $info['columns'] );
$rows = HTMLFormField::flattenOptions( $info['rows'] );
$prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
@@ -3307,7 +3524,7 @@ class User implements IDBAccessObject {
* Get the list of explicit group memberships this user has, stored as
* UserGroupMembership objects. Implicit groups are not included.
*
- * @return array Associative array of (group name as string => UserGroupMembership object)
+ * @return UserGroupMembership[] Associative array of (group name => UserGroupMembership object)
* @since 1.29
*/
public function getGroupMemberships() {
@@ -3760,51 +3977,9 @@ class User implements IDBAccessObject {
return;
}
- $dbw = wfGetDB( DB_MASTER );
- $asOfTimes = array_unique( $dbw->selectFieldValues(
- 'watchlist',
- 'wl_notificationtimestamp',
- [ 'wl_user' => $id, 'wl_notificationtimestamp IS NOT NULL' ],
- __METHOD__,
- [ 'ORDER BY' => 'wl_notificationtimestamp DESC', 'LIMIT' => 500 ]
- ) );
- if ( !$asOfTimes ) {
- return;
- }
- // Immediately update the most recent touched rows, which hopefully covers what
- // the user sees on the watchlist page before pressing "mark all pages visited"....
- $dbw->update(
- 'watchlist',
- [ 'wl_notificationtimestamp' => null ],
- [ 'wl_user' => $id, 'wl_notificationtimestamp' => $asOfTimes ],
- __METHOD__
- );
- // ...and finish the older ones in a post-send update with lag checks...
- DeferredUpdates::addUpdate( new AutoCommitUpdate(
- $dbw,
- __METHOD__,
- function () use ( $dbw, $id ) {
- global $wgUpdateRowsPerQuery;
-
- $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
- $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
- $asOfTimes = array_unique( $dbw->selectFieldValues(
- 'watchlist',
- 'wl_notificationtimestamp',
- [ 'wl_user' => $id, 'wl_notificationtimestamp IS NOT NULL' ],
- __METHOD__
- ) );
- foreach ( array_chunk( $asOfTimes, $wgUpdateRowsPerQuery ) as $asOfTimeBatch ) {
- $dbw->update(
- 'watchlist',
- [ 'wl_notificationtimestamp' => null ],
- [ 'wl_user' => $id, 'wl_notificationtimestamp' => $asOfTimeBatch ],
- __METHOD__
- );
- $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
- }
- }
- ) );
+ $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
+ $watchedItemStore->resetAllNotificationTimestampsForUser( $this );
+
// We also need to clear here the "you have new message" notification for the own
// user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
}
@@ -3846,76 +4021,6 @@ class User implements IDBAccessObject {
}
/**
- * Set a cookie on the user's client. Wrapper for
- * WebResponse::setCookie
- * @deprecated since 1.27
- * @param string $name Name of the cookie to set
- * @param string $value Value to set
- * @param int $exp Expiration time, as a UNIX time value;
- * if 0 or not specified, use the default $wgCookieExpiration
- * @param bool $secure
- * true: Force setting the secure attribute when setting the cookie
- * false: Force NOT setting the secure attribute when setting the cookie
- * null (default): Use the default ($wgCookieSecure) to set the secure attribute
- * @param array $params Array of options sent passed to WebResponse::setcookie()
- * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
- * is passed.
- */
- protected function setCookie(
- $name, $value, $exp = 0, $secure = null, $params = [], $request = null
- ) {
- wfDeprecated( __METHOD__, '1.27' );
- if ( $request === null ) {
- $request = $this->getRequest();
- }
- $params['secure'] = $secure;
- $request->response()->setCookie( $name, $value, $exp, $params );
- }
-
- /**
- * Clear a cookie on the user's client
- * @deprecated since 1.27
- * @param string $name Name of the cookie to clear
- * @param bool $secure
- * true: Force setting the secure attribute when setting the cookie
- * false: Force NOT setting the secure attribute when setting the cookie
- * null (default): Use the default ($wgCookieSecure) to set the secure attribute
- * @param array $params Array of options sent passed to WebResponse::setcookie()
- */
- protected function clearCookie( $name, $secure = null, $params = [] ) {
- wfDeprecated( __METHOD__, '1.27' );
- $this->setCookie( $name, '', time() - 86400, $secure, $params );
- }
-
- /**
- * Set an extended login cookie on the user's client. The expiry of the cookie
- * is controlled by the $wgExtendedLoginCookieExpiration configuration
- * variable.
- *
- * @see User::setCookie
- *
- * @deprecated since 1.27
- * @param string $name Name of the cookie to set
- * @param string $value Value to set
- * @param bool $secure
- * true: Force setting the secure attribute when setting the cookie
- * false: Force NOT setting the secure attribute when setting the cookie
- * null (default): Use the default ($wgCookieSecure) to set the secure attribute
- */
- protected function setExtendedLoginCookie( $name, $value, $secure ) {
- global $wgExtendedLoginCookieExpiration, $wgCookieExpiration;
-
- wfDeprecated( __METHOD__, '1.27' );
-
- $exp = time();
- $exp += $wgExtendedLoginCookieExpiration !== null
- ? $wgExtendedLoginCookieExpiration
- : $wgCookieExpiration;
-
- $this->setCookie( $name, $value, $exp, $secure );
- }
-
- /**
* Persist this user's session (e.g. set cookies)
*
* @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
@@ -4029,31 +4134,44 @@ class User implements IDBAccessObject {
$newTouched = $this->newTouchedTimestamp();
$dbw = wfGetDB( DB_MASTER );
- $dbw->update( 'user',
- [ /* SET */
- 'user_name' => $this->mName,
- 'user_real_name' => $this->mRealName,
- 'user_email' => $this->mEmail,
- 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
- 'user_touched' => $dbw->timestamp( $newTouched ),
- 'user_token' => strval( $this->mToken ),
- 'user_email_token' => $this->mEmailToken,
- 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
- ], $this->makeUpdateConditions( $dbw, [ /* WHERE */
- 'user_id' => $this->mId,
- ] ), __METHOD__
- );
-
- if ( !$dbw->affectedRows() ) {
- // Maybe the problem was a missed cache update; clear it to be safe
- $this->clearSharedCache( 'refresh' );
- // User was changed in the meantime or loaded with stale data
- $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'replica';
- throw new MWException(
- "CAS update failed on user_touched for user ID '{$this->mId}' (read from $from);" .
- " the version of the user to be saved is older than the current version."
+ $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $newTouched ) {
+ global $wgActorTableSchemaMigrationStage;
+
+ $dbw->update( 'user',
+ [ /* SET */
+ 'user_name' => $this->mName,
+ 'user_real_name' => $this->mRealName,
+ 'user_email' => $this->mEmail,
+ 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
+ 'user_touched' => $dbw->timestamp( $newTouched ),
+ 'user_token' => strval( $this->mToken ),
+ 'user_email_token' => $this->mEmailToken,
+ 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
+ ], $this->makeUpdateConditions( $dbw, [ /* WHERE */
+ 'user_id' => $this->mId,
+ ] ), $fname
);
- }
+
+ if ( !$dbw->affectedRows() ) {
+ // Maybe the problem was a missed cache update; clear it to be safe
+ $this->clearSharedCache( 'refresh' );
+ // User was changed in the meantime or loaded with stale data
+ $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'replica';
+ throw new MWException(
+ "CAS update failed on user_touched for user ID '{$this->mId}' (read from $from);" .
+ " the version of the user to be saved is older than the current version."
+ );
+ }
+
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+ $dbw->update(
+ 'actor',
+ [ 'actor_name' => $this->mName ],
+ [ 'actor_user' => $this->mId ],
+ $fname
+ );
+ }
+ } );
$this->mTouched = $newTouched;
$this->saveOptions();
@@ -4138,13 +4256,19 @@ class User implements IDBAccessObject {
foreach ( $params as $name => $value ) {
$fields["user_$name"] = $value;
}
- $dbw->insert( 'user', $fields, __METHOD__, [ 'IGNORE' ] );
- if ( $dbw->affectedRows() ) {
- $newUser = self::newFromId( $dbw->insertId() );
- } else {
- $newUser = null;
- }
- return $newUser;
+
+ return $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $fields ) {
+ $dbw->insert( 'user', $fields, $fname, [ 'IGNORE' ] );
+ if ( $dbw->affectedRows() ) {
+ $newUser = self::newFromId( $dbw->insertId() );
+ // Load the user from master to avoid replica lag
+ $newUser->load( self::READ_LATEST );
+ $newUser->updateActorId( $dbw );
+ } else {
+ $newUser = null;
+ }
+ return $newUser;
+ } );
}
/**
@@ -4185,49 +4309,56 @@ class User implements IDBAccessObject {
$this->mTouched = $this->newTouchedTimestamp();
- $noPass = PasswordFactory::newInvalidPassword()->toString();
-
$dbw = wfGetDB( DB_MASTER );
- $dbw->insert( 'user',
- [
- 'user_name' => $this->mName,
- 'user_password' => $noPass,
- 'user_newpassword' => $noPass,
- 'user_email' => $this->mEmail,
- 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
- 'user_real_name' => $this->mRealName,
- 'user_token' => strval( $this->mToken ),
- 'user_registration' => $dbw->timestamp( $this->mRegistration ),
- 'user_editcount' => 0,
- 'user_touched' => $dbw->timestamp( $this->mTouched ),
- ], __METHOD__,
- [ 'IGNORE' ]
- );
- if ( !$dbw->affectedRows() ) {
- // Use locking reads to bypass any REPEATABLE-READ snapshot.
- $this->mId = $dbw->selectField(
- 'user',
- 'user_id',
- [ 'user_name' => $this->mName ],
- __METHOD__,
- [ 'LOCK IN SHARE MODE' ]
+ $status = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) {
+ $noPass = PasswordFactory::newInvalidPassword()->toString();
+ $dbw->insert( 'user',
+ [
+ 'user_name' => $this->mName,
+ 'user_password' => $noPass,
+ 'user_newpassword' => $noPass,
+ 'user_email' => $this->mEmail,
+ 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
+ 'user_real_name' => $this->mRealName,
+ 'user_token' => strval( $this->mToken ),
+ 'user_registration' => $dbw->timestamp( $this->mRegistration ),
+ 'user_editcount' => 0,
+ 'user_touched' => $dbw->timestamp( $this->mTouched ),
+ ], $fname,
+ [ 'IGNORE' ]
);
- $loaded = false;
- if ( $this->mId ) {
- if ( $this->loadFromDatabase( self::READ_LOCKING ) ) {
- $loaded = true;
+ if ( !$dbw->affectedRows() ) {
+ // Use locking reads to bypass any REPEATABLE-READ snapshot.
+ $this->mId = $dbw->selectField(
+ 'user',
+ 'user_id',
+ [ 'user_name' => $this->mName ],
+ __METHOD__,
+ [ 'LOCK IN SHARE MODE' ]
+ );
+ $loaded = false;
+ if ( $this->mId ) {
+ if ( $this->loadFromDatabase( self::READ_LOCKING ) ) {
+ $loaded = true;
+ }
}
+ if ( !$loaded ) {
+ throw new MWException( __METHOD__ . ": hit a key conflict attempting " .
+ "to insert user '{$this->mName}' row, but it was not present in select!" );
+ }
+ return Status::newFatal( 'userexists' );
}
- if ( !$loaded ) {
- throw new MWException( __METHOD__ . ": hit a key conflict attempting " .
- "to insert user '{$this->mName}' row, but it was not present in select!" );
- }
- return Status::newFatal( 'userexists' );
+ $this->mId = $dbw->insertId();
+ self::$idCacheByName[$this->mName] = $this->mId;
+ $this->updateActorId( $dbw );
+
+ return Status::newGood();
+ } );
+ if ( !$status->isGood() ) {
+ return $status;
}
- $this->mId = $dbw->insertId();
- self::$idCacheByName[$this->mName] = $this->mId;
- // Clear instance cache other than user table data, which is already accurate
+ // Clear instance cache other than user table data and actor, which is already accurate
$this->clearInstanceCache();
$this->saveOptions();
@@ -4235,6 +4366,23 @@ class User implements IDBAccessObject {
}
/**
+ * Update the actor ID after an insert
+ * @param IDatabase $dbw Writable database handle
+ */
+ private function updateActorId( IDatabase $dbw ) {
+ global $wgActorTableSchemaMigrationStage;
+
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+ $dbw->insert(
+ 'actor',
+ [ 'actor_user' => $this->mId, 'actor_name' => $this->mName ],
+ __METHOD__
+ );
+ $this->mActorId = (int)$dbw->insertId();
+ }
+ }
+
+ /**
* If this user is logged-in and blocked,
* block any IP address they've successfully logged in from.
* @return bool A block was spread
@@ -4418,17 +4566,6 @@ class User implements IDBAccessObject {
}
/**
- * Get the embedded timestamp from a token.
- * @deprecated since 1.27, use \MediaWiki\Session\Token::getTimestamp instead.
- * @param string $val Input token
- * @return int|null
- */
- public static function getEditTokenTimestamp( $val ) {
- wfDeprecated( __METHOD__, '1.27' );
- return MediaWiki\Session\Token::getTimestamp( $val );
- }
-
- /**
* Check given value against the token value stored in the session.
* A match should confirm that the form was submitted from the
* user's own login session, not a form submission from a third-party
@@ -4573,7 +4710,7 @@ class User implements IDBAccessObject {
* (T8957 with Gmail and Internet Explorer).
*
* @param string $page Special page
- * @param string $token Token
+ * @param string $token
* @return string Formatted URL
*/
protected function getTokenUrl( $page, $token ) {
@@ -4722,10 +4859,14 @@ class User implements IDBAccessObject {
return false; // anons
}
$dbr = wfGetDB( DB_REPLICA );
- $time = $dbr->selectField( 'revision', 'rev_timestamp',
- [ 'rev_user' => $this->getId() ],
+ $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
+ $time = $dbr->selectField(
+ [ 'revision' ] + $actorWhere['tables'],
+ 'rev_timestamp',
+ [ $actorWhere['conds'] ],
__METHOD__,
- [ 'ORDER BY' => 'rev_timestamp ASC' ]
+ [ 'ORDER BY' => 'rev_timestamp ASC' ],
+ $actorWhere['joins']
);
if ( !$time ) {
return false; // no edits
@@ -5173,11 +5314,14 @@ class User implements IDBAccessObject {
// Pull from a replica DB to be less cruel to servers
// Accuracy isn't the point anyway here
$dbr = wfGetDB( DB_REPLICA );
+ $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
$count = (int)$dbr->selectField(
- 'revision',
- 'COUNT(rev_user)',
- [ 'rev_user' => $this->getId() ],
- __METHOD__
+ [ 'revision' ] + $actorWhere['tables'],
+ 'COUNT(*)',
+ [ $actorWhere['conds'] ],
+ __METHOD__,
+ [],
+ $actorWhere['joins']
);
$count = $count + $add;
@@ -5422,83 +5566,13 @@ class User implements IDBAccessObject {
}
/**
- * Lazily instantiate and return a factory object for making passwords
- *
- * @deprecated since 1.27, create a PasswordFactory directly instead
- * @return PasswordFactory
- */
- public static function getPasswordFactory() {
- wfDeprecated( __METHOD__, '1.27' );
- $ret = new PasswordFactory();
- $ret->init( RequestContext::getMain()->getConfig() );
- return $ret;
- }
-
- /**
- * Provide an array of HTML5 attributes to put on an input element
- * intended for the user to enter a new password. This may include
- * required, title, and/or pattern, depending on $wgMinimalPasswordLength.
- *
- * Do *not* use this when asking the user to enter his current password!
- * Regardless of configuration, users may have invalid passwords for whatever
- * reason (e.g., they were set before requirements were tightened up).
- * Only use it when asking for a new password, like on account creation or
- * ResetPass.
- *
- * Obviously, you still need to do server-side checking.
- *
- * NOTE: A combination of bugs in various browsers means that this function
- * actually just returns array() unconditionally at the moment. May as
- * well keep it around for when the browser bugs get fixed, though.
- *
- * @todo FIXME: This does not belong here; put it in Html or Linker or somewhere
- *
- * @deprecated since 1.27
- * @return array Array of HTML attributes suitable for feeding to
- * Html::element(), directly or indirectly. (Don't feed to Xml::*()!
- * That will get confused by the boolean attribute syntax used.)
- */
- public static function passwordChangeInputAttribs() {
- global $wgMinimalPasswordLength;
-
- if ( $wgMinimalPasswordLength == 0 ) {
- return [];
- }
-
- # Note that the pattern requirement will always be satisfied if the
- # input is empty, so we need required in all cases.
-
- # @todo FIXME: T25769: This needs to not claim the password is required
- # if e-mail confirmation is being used. Since HTML5 input validation
- # is b0rked anyway in some browsers, just return nothing. When it's
- # re-enabled, fix this code to not output required for e-mail
- # registration.
- # $ret = array( 'required' );
- $ret = [];
-
- # We can't actually do this right now, because Opera 9.6 will print out
- # the entered password visibly in its error message! When other
- # browsers add support for this attribute, or Opera fixes its support,
- # we can add support with a version check to avoid doing this on Opera
- # versions where it will be a problem. Reported to Opera as
- # DSK-262266, but they don't have a public bug tracker for us to follow.
- /*
- if ( $wgMinimalPasswordLength > 1 ) {
- $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
- $ret['title'] = wfMessage( 'passwordtooshort' )
- ->numParams( $wgMinimalPasswordLength )->text();
- }
- */
-
- return $ret;
- }
-
- /**
* Return the list of user fields that should be selected to create
* a new user object.
+ * @deprecated since 1.31, use self::getQueryInfo() instead.
* @return array
*/
public static function selectFields() {
+ wfDeprecated( __METHOD__, '1.31' );
return [
'user_id',
'user_name',
@@ -5515,6 +5589,46 @@ class User implements IDBAccessObject {
}
/**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new user object.
+ * @since 1.31
+ * @return array With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ */
+ public static function getQueryInfo() {
+ global $wgActorTableSchemaMigrationStage;
+
+ $ret = [
+ 'tables' => [ 'user' ],
+ 'fields' => [
+ 'user_id',
+ 'user_name',
+ 'user_real_name',
+ 'user_email',
+ 'user_touched',
+ 'user_token',
+ 'user_email_authenticated',
+ 'user_email_token',
+ 'user_email_token_expires',
+ 'user_registration',
+ 'user_editcount',
+ ],
+ 'joins' => [],
+ ];
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+ $ret['tables']['user_actor'] = 'actor';
+ $ret['fields'][] = 'user_actor.actor_id';
+ $ret['joins']['user_actor'] = [
+ $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
+ [ 'user_actor.actor_user = user_id' ]
+ ];
+ }
+ return $ret;
+ }
+
+ /**
* Factory function for fatal permission-denied errors
*
* @since 1.22
diff --git a/www/wiki/includes/user/UserArray.php b/www/wiki/includes/user/UserArray.php
index ab6683b2..66d9c7a1 100644
--- a/www/wiki/includes/user/UserArray.php
+++ b/www/wiki/includes/user/UserArray.php
@@ -20,11 +20,11 @@
* @file
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
abstract class UserArray implements Iterator {
/**
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
* @return UserArrayFromResult
*/
static function newFromResult( $res ) {
@@ -49,11 +49,14 @@ abstract class UserArray implements Iterator {
return new ArrayIterator( [] );
}
$dbr = wfGetDB( DB_REPLICA );
+ $userQuery = User::getQueryInfo();
$res = $dbr->select(
- 'user',
- User::selectFields(),
+ $userQuery['tables'],
+ $userQuery['fields'],
[ 'user_id' => array_unique( $ids ) ],
- __METHOD__
+ __METHOD__,
+ [],
+ $userQuery['joins']
);
return self::newFromResult( $res );
}
@@ -70,17 +73,20 @@ abstract class UserArray implements Iterator {
return new ArrayIterator( [] );
}
$dbr = wfGetDB( DB_REPLICA );
+ $userQuery = User::getQueryInfo();
$res = $dbr->select(
- 'user',
- User::selectFields(),
+ $userQuery['tables'],
+ $userQuery['fields'],
[ 'user_name' => array_unique( $names ) ],
- __METHOD__
+ __METHOD__,
+ [],
+ $userQuery['joins']
);
return self::newFromResult( $res );
}
/**
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
* @return UserArrayFromResult
*/
protected static function newFromResult_internal( $res ) {
diff --git a/www/wiki/includes/user/UserArrayFromResult.php b/www/wiki/includes/user/UserArrayFromResult.php
index 527df7fa..0830e422 100644
--- a/www/wiki/includes/user/UserArrayFromResult.php
+++ b/www/wiki/includes/user/UserArrayFromResult.php
@@ -20,10 +20,10 @@
* @file
*/
-use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IResultWrapper;
class UserArrayFromResult extends UserArray implements Countable {
- /** @var ResultWrapper */
+ /** @var IResultWrapper */
public $res;
/** @var int */
@@ -33,7 +33,7 @@ class UserArrayFromResult extends UserArray implements Countable {
public $current;
/**
- * @param ResultWrapper $res
+ * @param IResultWrapper $res
*/
function __construct( $res ) {
$this->res = $res;
diff --git a/www/wiki/includes/user/UserGroupMembership.php b/www/wiki/includes/user/UserGroupMembership.php
index a06be834..9da0370e 100644
--- a/www/wiki/includes/user/UserGroupMembership.php
+++ b/www/wiki/includes/user/UserGroupMembership.php
@@ -21,6 +21,7 @@
*/
use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\MediaWikiServices;
/**
* Represents a "user group membership" -- a specific instance of a user belonging
@@ -158,7 +159,7 @@ class UserGroupMembership {
}
// Purge old, expired memberships from the DB
- self::purgeExpired( $dbw );
+ JobQueueGroup::singleton()->push( new UserGroupExpiryJob() );
// Check that the values make sense
if ( $this->group === null ) {
@@ -237,37 +238,65 @@ class UserGroupMembership {
/**
* Purge expired memberships from the user_groups table
*
- * @param IDatabase|null $dbw
+ * @return int|bool false if purging wasn't attempted (e.g. because of
+ * readonly), the number of rows purged (might be 0) otherwise
*/
- public static function purgeExpired( IDatabase $dbw = null ) {
- if ( wfReadOnly() ) {
- return;
+ public static function purgeExpired() {
+ $services = MediaWikiServices::getInstance();
+ if ( $services->getReadOnlyMode()->isReadOnly() ) {
+ return false;
}
- if ( $dbw === null ) {
- $dbw = wfGetDB( DB_MASTER );
- }
+ $lbFactory = $services->getDBLoadBalancerFactory();
+ $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
+ $dbw = $services->getDBLoadBalancer()->getConnection( DB_MASTER );
- DeferredUpdates::addUpdate( new AtomicSectionUpdate(
- $dbw,
- __METHOD__,
- function ( IDatabase $dbw, $fname ) {
- $expiryCond = [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ];
- $res = $dbw->select( 'user_groups', self::selectFields(), $expiryCond, $fname );
+ $lockKey = $dbw->getDomainID() . ':usergroups-prune'; // specific to this wiki
+ $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 );
+ if ( !$scopedLock ) {
+ return false; // already running
+ }
- // save an array of users/groups to insert to user_former_groups
- $usersAndGroups = [];
+ $now = time();
+ $purgedRows = 0;
+ do {
+ $dbw->startAtomic( __METHOD__ );
+
+ $res = $dbw->select(
+ 'user_groups',
+ self::selectFields(),
+ [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp( $now ) ) ],
+ __METHOD__,
+ [ 'FOR UPDATE', 'LIMIT' => 100 ]
+ );
+
+ if ( $res->numRows() > 0 ) {
+ $insertData = []; // array of users/groups to insert to user_former_groups
+ $deleteCond = []; // array for deleting the rows that are to be moved around
foreach ( $res as $row ) {
- $usersAndGroups[] = [ 'ufg_user' => $row->ug_user, 'ufg_group' => $row->ug_group ];
+ $insertData[] = [ 'ufg_user' => $row->ug_user, 'ufg_group' => $row->ug_group ];
+ $deleteCond[] = $dbw->makeList(
+ [ 'ug_user' => $row->ug_user, 'ug_group' => $row->ug_group ],
+ $dbw::LIST_AND
+ );
}
+ // Delete the rows we're about to move
+ $dbw->delete(
+ 'user_groups',
+ $dbw->makeList( $deleteCond, $dbw::LIST_OR ),
+ __METHOD__
+ );
+ // Push the groups to user_former_groups
+ $dbw->insert( 'user_former_groups', $insertData, __METHOD__, [ 'IGNORE' ] );
+ // Count how many rows were purged
+ $purgedRows += $res->numRows();
+ }
- // delete 'em all
- $dbw->delete( 'user_groups', $expiryCond, $fname );
+ $dbw->endAtomic( __METHOD__ );
- // and push the groups to user_former_groups
- $dbw->insert( 'user_former_groups', $usersAndGroups, __METHOD__, [ 'IGNORE' ] );
- }
- ) );
+ $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+ } while ( $res->numRows() > 0 );
+ return $purgedRows;
}
/**
@@ -276,7 +305,7 @@ class UserGroupMembership {
*
* @param int $userId ID of the user to search for
* @param IDatabase|null $db Optional database connection
- * @return array Associative array of (group name => UserGroupMembership object)
+ * @return UserGroupMembership[] Associative array of (group name => UserGroupMembership object)
*/
public static function getMembershipsForUser( $userId, IDatabase $db = null ) {
if ( !$db ) {
diff --git a/www/wiki/includes/profiler/ProfileSection.php b/www/wiki/includes/user/UserIdentity.php
index d48f7442..d02a6788 100644
--- a/www/wiki/includes/profiler/ProfileSection.php
+++ b/www/wiki/includes/user/UserIdentity.php
@@ -1,6 +1,6 @@
<?php
/**
- * Function scope profiling assistant
+ * Interface for objects representing user identity.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,28 +18,40 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
- * @ingroup Profiler
*/
+namespace MediaWiki\User;
+
/**
- * Class for handling function-scope profiling
+ * Interface for objects representing user identity.
+ *
+ * This represents the identity of a user in the context of page revisions and log entries.
*
- * @since 1.22
- * @deprecated since 1.25 No-op now
+ * @since 1.31
*/
-class ProfileSection {
+interface UserIdentity {
+
+ /**
+ * @since 1.31
+ *
+ * @return int The user ID. May be 0 for anonymous users or for users with no local account.
+ */
+ public function getId();
+
/**
- * Begin profiling of a function and return an object that ends profiling
- * of the function when that object leaves scope. As long as the object is
- * not specifically linked to other objects, it will fall out of scope at
- * the same moment that the function to be profiled terminates.
+ * @since 1.31
*
- * This is typically called like:
- * @code$section = new ProfileSection( __METHOD__ );@endcode
+ * @return string The user's logical name. May be an IPv4 or IPv6 address for anonymous users.
+ */
+ public function getName();
+
+ /**
+ * @since 1.31
*
- * @param string $name Name of the function to profile
+ * @return int The user's actor ID. May be 0 if no actor ID is set.
*/
- public function __construct( $name ) {
- wfDeprecated( __CLASS__, '1.25' );
- }
+ public function getActorId();
+
+ // TODO: we may want to (optionally?) provide a global ID, see CentralIdLookup.
+
}
diff --git a/www/wiki/includes/user/UserIdentityValue.php b/www/wiki/includes/user/UserIdentityValue.php
new file mode 100644
index 00000000..120f31f2
--- /dev/null
+++ b/www/wiki/includes/user/UserIdentityValue.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Value object representing a user's identity.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\User;
+
+use Wikimedia\Assert\Assert;
+
+/**
+ * Value object representing a user's identity.
+ *
+ * @since 1.31
+ */
+class UserIdentityValue implements UserIdentity {
+
+ /**
+ * @var int
+ */
+ private $id;
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var int
+ */
+ private $actor;
+
+ /**
+ * @param int $id
+ * @param string $name
+ * @param int $actor
+ */
+ public function __construct( $id, $name, $actor ) {
+ Assert::parameterType( 'integer', $id, '$id' );
+ Assert::parameterType( 'string', $name, '$name' );
+ Assert::parameterType( 'integer', $actor, '$actor' );
+
+ $this->id = $id;
+ $this->name = $name;
+ $this->actor = $actor;
+ }
+
+ /**
+ * @return int The user ID. May be 0 for anonymous users or for users with no local account.
+ */
+ public function getId() {
+ return $this->id;
+ }
+
+ /**
+ * @return string The user's logical name. May be an IPv4 or IPv6 address for anonymous users.
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * @return int The user's actor ID. May be 0 if no actor ID has been assigned.
+ */
+ public function getActorId() {
+ return $this->actor;
+ }
+
+}
diff --git a/www/wiki/includes/utils/AutoloadGenerator.php b/www/wiki/includes/utils/AutoloadGenerator.php
index 4f639c13..0e2ef85d 100644
--- a/www/wiki/includes/utils/AutoloadGenerator.php
+++ b/www/wiki/includes/utils/AutoloadGenerator.php
@@ -43,6 +43,13 @@ class AutoloadGenerator {
protected $overrides = [];
/**
+ * Directories that should be excluded
+ *
+ * @var string[]
+ */
+ protected $excludePaths = [];
+
+ /**
* @param string $basepath Root path of the project being scanned for classes
* @param array|string $flags
*
@@ -61,6 +68,34 @@ class AutoloadGenerator {
}
/**
+ * Directories that should be excluded
+ *
+ * @since 1.31
+ * @param string[] $paths
+ */
+ public function setExcludePaths( array $paths ) {
+ foreach ( $paths as $path ) {
+ $this->excludePaths[] = self::normalizePathSeparator( $path );
+ }
+ }
+
+ /**
+ * Whether the file should be excluded
+ *
+ * @param string $path File path
+ * @return bool
+ */
+ private function shouldExclude( $path ) {
+ foreach ( $this->excludePaths as $dir ) {
+ if ( strpos( $path, $dir ) === 0 ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
* Force a class to be autoloaded from a specific path, regardless of where
* or if it was detected.
*
@@ -94,6 +129,9 @@ class AutoloadGenerator {
if ( substr( $inputPath, 0, $len ) !== $this->basepath ) {
throw new \Exception( "Path is not within basepath: $inputPath" );
}
+ if ( $this->shouldExclude( $inputPath ) ) {
+ return;
+ }
$result = $this->collector->getClasses(
file_get_contents( $inputPath )
);
@@ -156,8 +194,8 @@ class AutoloadGenerator {
/**
* Generates a PHP file setting up autoload information.
*
- * @param {string} $commandName Command name to include in comment
- * @param {string} $filename of PHP file to put autoload information in.
+ * @param string $commandName Command name to include in comment
+ * @param string $filename of PHP file to put autoload information in.
* @return string
*/
protected function generatePHPAutoload( $commandName, $filename ) {
@@ -198,11 +236,10 @@ class AutoloadGenerator {
}
$output = implode( "\n\t", $content );
- return
- <<<EOD
+ return <<<EOD
<?php
// This file is generated by $commandName, do not adjust manually
-// @codingStandardsIgnoreFile
+// phpcs:disable Generic.Files.LineLength
global \${$this->variableName};
\${$this->variableName} {$op} [
@@ -354,18 +391,18 @@ class ClassCollector {
// Note: When changing class name discovery logic,
// AutoLoaderTest.php may also need to be updated.
switch ( $token[0] ) {
- case T_NAMESPACE:
- case T_CLASS:
- case T_INTERFACE:
- case T_TRAIT:
- case T_DOUBLE_COLON:
- $this->startToken = $token;
- break;
- case T_STRING:
- if ( $token[1] === 'class_alias' ) {
+ case T_NAMESPACE:
+ case T_CLASS:
+ case T_INTERFACE:
+ case T_TRAIT:
+ case T_DOUBLE_COLON:
$this->startToken = $token;
- $this->alias = [];
- }
+ break;
+ case T_STRING:
+ if ( $token[1] === 'class_alias' ) {
+ $this->startToken = $token;
+ $this->alias = [];
+ }
}
}
@@ -376,78 +413,78 @@ class ClassCollector {
*/
protected function tryEndExpect( $token ) {
switch ( $this->startToken[0] ) {
- case T_DOUBLE_COLON:
- // Skip over T_CLASS after T_DOUBLE_COLON because this is something like
- // "self::static" which accesses the class name. It doens't define a new class.
- $this->startToken = null;
- break;
- case T_NAMESPACE:
- if ( $token === ';' || $token === '{' ) {
- $this->namespace = $this->implodeTokens() . '\\';
- } else {
- $this->tokens[] = $token;
- }
- break;
-
- case T_STRING:
- if ( $this->alias !== null ) {
- // Flow 1 - Two string literals:
- // - T_STRING class_alias
- // - '('
- // - T_CONSTANT_ENCAPSED_STRING 'TargetClass'
- // - ','
- // - T_WHITESPACE
- // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
- // - ')'
- // Flow 2 - Use of ::class syntax for first parameter
- // - T_STRING class_alias
- // - '('
- // - T_STRING TargetClass
- // - T_DOUBLE_COLON ::
- // - T_CLASS class
- // - ','
- // - T_WHITESPACE
- // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
- // - ')'
- if ( $token === '(' ) {
- // Start of a function call to class_alias()
- $this->alias = [ 'target' => false, 'name' => false ];
- } elseif ( $token === ',' ) {
- // Record that we're past the first parameter
- if ( $this->alias['target'] === false ) {
- $this->alias['target'] = true;
- }
- } elseif ( is_array( $token ) && $token[0] === T_CONSTANT_ENCAPSED_STRING ) {
- if ( $this->alias['target'] === true ) {
- // We already saw a first argument, this must be the second.
- // Strip quotes from the string literal.
- $this->alias['name'] = substr( $token[1], 1, -1 );
+ case T_DOUBLE_COLON:
+ // Skip over T_CLASS after T_DOUBLE_COLON because this is something like
+ // "self::static" which accesses the class name. It doens't define a new class.
+ $this->startToken = null;
+ break;
+ case T_NAMESPACE:
+ if ( $token === ';' || $token === '{' ) {
+ $this->namespace = $this->implodeTokens() . '\\';
+ } else {
+ $this->tokens[] = $token;
+ }
+ break;
+
+ case T_STRING:
+ if ( $this->alias !== null ) {
+ // Flow 1 - Two string literals:
+ // - T_STRING class_alias
+ // - '('
+ // - T_CONSTANT_ENCAPSED_STRING 'TargetClass'
+ // - ','
+ // - T_WHITESPACE
+ // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
+ // - ')'
+ // Flow 2 - Use of ::class syntax for first parameter
+ // - T_STRING class_alias
+ // - '('
+ // - T_STRING TargetClass
+ // - T_DOUBLE_COLON ::
+ // - T_CLASS class
+ // - ','
+ // - T_WHITESPACE
+ // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
+ // - ')'
+ if ( $token === '(' ) {
+ // Start of a function call to class_alias()
+ $this->alias = [ 'target' => false, 'name' => false ];
+ } elseif ( $token === ',' ) {
+ // Record that we're past the first parameter
+ if ( $this->alias['target'] === false ) {
+ $this->alias['target'] = true;
+ }
+ } elseif ( is_array( $token ) && $token[0] === T_CONSTANT_ENCAPSED_STRING ) {
+ if ( $this->alias['target'] === true ) {
+ // We already saw a first argument, this must be the second.
+ // Strip quotes from the string literal.
+ $this->alias['name'] = substr( $token[1], 1, -1 );
+ }
+ } elseif ( $token === ')' ) {
+ // End of function call
+ $this->classes[] = $this->alias['name'];
+ $this->alias = null;
+ $this->startToken = null;
+ } elseif ( !is_array( $token ) || (
+ $token[0] !== T_STRING &&
+ $token[0] !== T_DOUBLE_COLON &&
+ $token[0] !== T_CLASS &&
+ $token[0] !== T_WHITESPACE
+ ) ) {
+ // Ignore this call to class_alias() - compat/Timestamp.php
+ $this->alias = null;
+ $this->startToken = null;
}
- } elseif ( $token === ')' ) {
- // End of function call
- $this->classes[] = $this->alias['name'];
- $this->alias = null;
- $this->startToken = null;
- } elseif ( !is_array( $token ) || (
- $token[0] !== T_STRING &&
- $token[0] !== T_DOUBLE_COLON &&
- $token[0] !== T_CLASS &&
- $token[0] !== T_WHITESPACE
- ) ) {
- // Ignore this call to class_alias() - compat/Timestamp.php
- $this->alias = null;
- $this->startToken = null;
}
- }
- break;
-
- case T_CLASS:
- case T_INTERFACE:
- case T_TRAIT:
- $this->tokens[] = $token;
- if ( is_array( $token ) && $token[0] === T_STRING ) {
- $this->classes[] = $this->namespace . $this->implodeTokens();
- }
+ break;
+
+ case T_CLASS:
+ case T_INTERFACE:
+ case T_TRAIT:
+ $this->tokens[] = $token;
+ if ( is_array( $token ) && $token[0] === T_STRING ) {
+ $this->classes[] = $this->namespace . $this->implodeTokens();
+ }
}
}
diff --git a/www/wiki/includes/utils/AvroValidator.php b/www/wiki/includes/utils/AvroValidator.php
index 554dda9d..153b3135 100644
--- a/www/wiki/includes/utils/AvroValidator.php
+++ b/www/wiki/includes/utils/AvroValidator.php
@@ -37,133 +37,133 @@ class AvroValidator {
*/
public static function getErrors( AvroSchema $schema, $datum ) {
switch ( $schema->type ) {
- case AvroSchema::NULL_TYPE:
- if ( !is_null( $datum ) ) {
- return self::wrongType( 'null', $datum );
- }
- return [];
- case AvroSchema::BOOLEAN_TYPE:
- if ( !is_bool( $datum ) ) {
- return self::wrongType( 'boolean', $datum );
- }
- return [];
- case AvroSchema::STRING_TYPE:
- case AvroSchema::BYTES_TYPE:
- if ( !is_string( $datum ) ) {
- return self::wrongType( 'string', $datum );
- }
- return [];
- case AvroSchema::INT_TYPE:
- if ( !is_int( $datum ) ) {
- return self::wrongType( 'integer', $datum );
- }
- if ( AvroSchema::INT_MIN_VALUE > $datum
- || $datum > AvroSchema::INT_MAX_VALUE
- ) {
- return self::outOfRange(
- AvroSchema::INT_MIN_VALUE,
- AvroSchema::INT_MAX_VALUE,
- $datum
- );
- }
- return [];
- case AvroSchema::LONG_TYPE:
- if ( !is_int( $datum ) ) {
- return self::wrongType( 'integer', $datum );
- }
- if ( AvroSchema::LONG_MIN_VALUE > $datum
- || $datum > AvroSchema::LONG_MAX_VALUE
- ) {
- return self::outOfRange(
- AvroSchema::LONG_MIN_VALUE,
- AvroSchema::LONG_MAX_VALUE,
- $datum
- );
- }
- return [];
- case AvroSchema::FLOAT_TYPE:
- case AvroSchema::DOUBLE_TYPE:
- if ( !is_float( $datum ) && !is_int( $datum ) ) {
- return self::wrongType( 'float or integer', $datum );
- }
- return [];
- case AvroSchema::ARRAY_SCHEMA:
- if ( !is_array( $datum ) ) {
- return self::wrongType( 'array', $datum );
- }
- $errors = [];
- foreach ( $datum as $d ) {
- $result = self::getErrors( $schema->items(), $d );
- if ( $result ) {
+ case AvroSchema::NULL_TYPE:
+ if ( !is_null( $datum ) ) {
+ return self::wrongType( 'null', $datum );
+ }
+ return [];
+ case AvroSchema::BOOLEAN_TYPE:
+ if ( !is_bool( $datum ) ) {
+ return self::wrongType( 'boolean', $datum );
+ }
+ return [];
+ case AvroSchema::STRING_TYPE:
+ case AvroSchema::BYTES_TYPE:
+ if ( !is_string( $datum ) ) {
+ return self::wrongType( 'string', $datum );
+ }
+ return [];
+ case AvroSchema::INT_TYPE:
+ if ( !is_int( $datum ) ) {
+ return self::wrongType( 'integer', $datum );
+ }
+ if ( AvroSchema::INT_MIN_VALUE > $datum
+ || $datum > AvroSchema::INT_MAX_VALUE
+ ) {
+ return self::outOfRange(
+ AvroSchema::INT_MIN_VALUE,
+ AvroSchema::INT_MAX_VALUE,
+ $datum
+ );
+ }
+ return [];
+ case AvroSchema::LONG_TYPE:
+ if ( !is_int( $datum ) ) {
+ return self::wrongType( 'integer', $datum );
+ }
+ if ( AvroSchema::LONG_MIN_VALUE > $datum
+ || $datum > AvroSchema::LONG_MAX_VALUE
+ ) {
+ return self::outOfRange(
+ AvroSchema::LONG_MIN_VALUE,
+ AvroSchema::LONG_MAX_VALUE,
+ $datum
+ );
+ }
+ return [];
+ case AvroSchema::FLOAT_TYPE:
+ case AvroSchema::DOUBLE_TYPE:
+ if ( !is_float( $datum ) && !is_int( $datum ) ) {
+ return self::wrongType( 'float or integer', $datum );
+ }
+ return [];
+ case AvroSchema::ARRAY_SCHEMA:
+ if ( !is_array( $datum ) ) {
+ return self::wrongType( 'array', $datum );
+ }
+ $errors = [];
+ foreach ( $datum as $d ) {
+ $result = self::getErrors( $schema->items(), $d );
+ if ( $result ) {
+ $errors[] = $result;
+ }
+ }
+ return $errors;
+ case AvroSchema::MAP_SCHEMA:
+ if ( !is_array( $datum ) ) {
+ return self::wrongType( 'array', $datum );
+ }
+ $errors = [];
+ foreach ( $datum as $k => $v ) {
+ if ( !is_string( $k ) ) {
+ $errors[] = self::wrongType( 'string key', $k );
+ }
+ $result = self::getErrors( $schema->values(), $v );
+ if ( $result ) {
+ $errors[$k] = $result;
+ }
+ }
+ return $errors;
+ case AvroSchema::UNION_SCHEMA:
+ $errors = [];
+ foreach ( $schema->schemas() as $schema ) {
+ $result = self::getErrors( $schema, $datum );
+ if ( !$result ) {
+ return [];
+ }
$errors[] = $result;
}
- }
- return $errors;
- case AvroSchema::MAP_SCHEMA:
- if ( !is_array( $datum ) ) {
- return self::wrongType( 'array', $datum );
- }
- $errors = [];
- foreach ( $datum as $k => $v ) {
- if ( !is_string( $k ) ) {
- $errors[] = self::wrongType( 'string key', $k );
- }
- $result = self::getErrors( $schema->values(), $v );
- if ( $result ) {
- $errors[$k] = $result;
- }
- }
- return $errors;
- case AvroSchema::UNION_SCHEMA:
- $errors = [];
- foreach ( $schema->schemas() as $schema ) {
- $result = self::getErrors( $schema, $datum );
- if ( !$result ) {
- return [];
- }
- $errors[] = $result;
- }
- if ( $errors ) {
- return [ "Expected any one of these to be true", $errors ];
- }
- return "No schemas provided to union";
- case AvroSchema::ENUM_SCHEMA:
- if ( !in_array( $datum, $schema->symbols() ) ) {
- $symbols = implode( ', ', $schema->symbols );
- return "Expected one of $symbols but recieved $datum";
- }
- return [];
- case AvroSchema::FIXED_SCHEMA:
- if ( !is_string( $datum ) ) {
- return self::wrongType( 'string', $datum );
- }
- $len = strlen( $datum );
- if ( $len !== $schema->size() ) {
- return "Expected string of length {$schema->size()}, "
- . "but recieved one of length $len";
- }
- return [];
- case AvroSchema::RECORD_SCHEMA:
- case AvroSchema::ERROR_SCHEMA:
- case AvroSchema::REQUEST_SCHEMA:
- if ( !is_array( $datum ) ) {
- return self::wrongType( 'array', $datum );
- }
- $errors = [];
- foreach ( $schema->fields() as $field ) {
- $name = $field->name();
- if ( !array_key_exists( $name, $datum ) ) {
- $errors[$name] = 'Missing expected field';
- continue;
- }
- $result = self::getErrors( $field->type(), $datum[$name] );
- if ( $result ) {
- $errors[$name] = $result;
- }
- }
- return $errors;
- default:
- return "Unknown avro schema type: {$schema->type}";
+ if ( $errors ) {
+ return [ "Expected any one of these to be true", $errors ];
+ }
+ return "No schemas provided to union";
+ case AvroSchema::ENUM_SCHEMA:
+ if ( !in_array( $datum, $schema->symbols() ) ) {
+ $symbols = implode( ', ', $schema->symbols );
+ return "Expected one of $symbols but recieved $datum";
+ }
+ return [];
+ case AvroSchema::FIXED_SCHEMA:
+ if ( !is_string( $datum ) ) {
+ return self::wrongType( 'string', $datum );
+ }
+ $len = strlen( $datum );
+ if ( $len !== $schema->size() ) {
+ return "Expected string of length {$schema->size()}, "
+ . "but recieved one of length $len";
+ }
+ return [];
+ case AvroSchema::RECORD_SCHEMA:
+ case AvroSchema::ERROR_SCHEMA:
+ case AvroSchema::REQUEST_SCHEMA:
+ if ( !is_array( $datum ) ) {
+ return self::wrongType( 'array', $datum );
+ }
+ $errors = [];
+ foreach ( $schema->fields() as $field ) {
+ $name = $field->name();
+ if ( !array_key_exists( $name, $datum ) ) {
+ $errors[$name] = 'Missing expected field';
+ continue;
+ }
+ $result = self::getErrors( $field->type(), $datum[$name] );
+ if ( $result ) {
+ $errors[$name] = $result;
+ }
+ }
+ return $errors;
+ default:
+ return "Unknown avro schema type: {$schema->type}";
}
}
diff --git a/www/wiki/includes/utils/BatchRowWriter.php b/www/wiki/includes/utils/BatchRowWriter.php
index 59dcbd63..c146e964 100644
--- a/www/wiki/includes/utils/BatchRowWriter.php
+++ b/www/wiki/includes/utils/BatchRowWriter.php
@@ -20,8 +20,9 @@
* @file
* @ingroup Maintenance
*/
+
+use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\IDatabase;
-use \MediaWiki\MediaWikiServices;
class BatchRowWriter {
/**
diff --git a/www/wiki/includes/utils/ExecutableFinder.php b/www/wiki/includes/utils/ExecutableFinder.php
new file mode 100644
index 00000000..78b3f8e2
--- /dev/null
+++ b/www/wiki/includes/utils/ExecutableFinder.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+use MediaWiki\Shell\Shell;
+
+/**
+ * Utility class to find executables in likely places
+ *
+ * @since 1.31
+ */
+class ExecutableFinder {
+
+ /**
+ * Get an array of likely places we can find executables. Check a bunch
+ * of known Unix-like defaults, as well as the PATH environment variable
+ * (which should maybe make it work for Windows?)
+ *
+ * @return array
+ */
+ protected static function getPossibleBinPaths() {
+ return array_unique( array_merge(
+ [ '/usr/bin', '/bin', '/usr/local/bin', '/opt/csw/bin',
+ '/usr/gnu/bin', '/usr/sfw/bin', '/sw/bin', '/opt/local/bin' ],
+ explode( PATH_SEPARATOR, getenv( 'PATH' ) )
+ ) );
+ }
+
+ /**
+ * Search a path for any of the given executable names. Returns the
+ * executable name if found. Also checks the version string returned
+ * by each executable.
+ *
+ * Used only by environment checks.
+ *
+ * @param string $path Path to search
+ * @param string $name Executable name to look for
+ * @param array|bool $versionInfo False or array with two members:
+ * 0 => Parameter to pass to binary for version check (e.g. --version)
+ * 1 => String to compare the output with
+ *
+ * If $versionInfo is not false, only executables with a version
+ * matching $versionInfo[1] will be returned.
+ * @return bool|string
+ */
+ protected static function findExecutable( $path, $name, $versionInfo = false ) {
+ $command = $path . DIRECTORY_SEPARATOR . $name;
+
+ Wikimedia\suppressWarnings();
+ $file_exists = is_executable( $command );
+ Wikimedia\restoreWarnings();
+
+ if ( $file_exists ) {
+ if ( !$versionInfo ) {
+ return $command;
+ }
+
+ $output = Shell::command( $command, $versionInfo[0] )
+ ->includeStderr()->execute()->getStdout();
+ if ( strstr( $output, $versionInfo[1] ) !== false ) {
+ return $command;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Same as locateExecutable(), but checks in getPossibleBinPaths() by default
+ * @see locateExecutable()
+ * @param string|string[] $names Array of possible names.
+ * @param array|bool $versionInfo Default: false or array with two members:
+ * 0 => Parameter to run for version check, e.g. '--version'
+ * 1 => String to compare the output with
+ *
+ * If $versionInfo is not false, only executables with a version
+ * matching $versionInfo[1] will be returned.
+ * @return bool|string
+ */
+ public static function findInDefaultPaths( $names, $versionInfo = false ) {
+ if ( Shell::isDisabled() ) {
+ // If we can't shell out, there's no point looking for executables
+ return false;
+ }
+
+ $paths = self::getPossibleBinPaths();
+ foreach ( (array)$names as $name ) {
+ foreach ( $paths as $path ) {
+ $exe = self::findExecutable( $path, $name, $versionInfo );
+ if ( $exe !== false ) {
+ return $exe;
+ }
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/includes/utils/FileContentsHasher.php b/www/wiki/includes/utils/FileContentsHasher.php
index afe9c0a0..e390f217 100644
--- a/www/wiki/includes/utils/FileContentsHasher.php
+++ b/www/wiki/includes/utils/FileContentsHasher.php
@@ -93,11 +93,11 @@ class FileContentsHasher {
$filePaths = (array)$filePaths;
}
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
if ( count( $filePaths ) === 1 ) {
$hash = $instance->getFileContentsHashInternal( $filePaths[0], $algo );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return $hash;
}
@@ -106,7 +106,7 @@ class FileContentsHasher {
return $instance->getFileContentsHashInternal( $filePath, $algo ) ?: '';
}, $filePaths );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
$hashes = implode( '', $hashes );
return $hashes ? hash( $algo, $hashes ) : false;
diff --git a/www/wiki/includes/utils/IP.php b/www/wiki/includes/utils/IP.php
deleted file mode 100644
index 4a2205ee..00000000
--- a/www/wiki/includes/utils/IP.php
+++ /dev/null
@@ -1,791 +0,0 @@
-<?php
-/**
- * Functions and constants to play with IP addresses and ranges
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author Antoine Musso "<hashar at free dot fr>", Aaron Schulz
- */
-
-use IPSet\IPSet;
-
-// Some regex definition to "play" with IP address and IP address blocks
-
-// An IPv4 address is made of 4 bytes from x00 to xFF which is d0 to d255
-define( 'RE_IP_BYTE', '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])' );
-define( 'RE_IP_ADD', RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE );
-// An IPv4 block is an IP address and a prefix (d1 to d32)
-define( 'RE_IP_PREFIX', '(3[0-2]|[12]?\d)' );
-define( 'RE_IP_BLOCK', RE_IP_ADD . '\/' . RE_IP_PREFIX );
-
-// An IPv6 address is made up of 8 words (each x0000 to xFFFF).
-// However, the "::" abbreviation can be used on consecutive x0000 words.
-define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' );
-define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)' );
-define( 'RE_IPV6_ADD',
- '(?:' . // starts with "::" (including "::")
- ':(?::|(?::' . RE_IPV6_WORD . '){1,7})' .
- '|' . // ends with "::" (except "::")
- RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){0,6}::' .
- '|' . // contains one "::" in the middle (the ^ makes the test fail if none found)
- RE_IPV6_WORD . '(?::((?(-1)|:))?' . RE_IPV6_WORD . '){1,6}(?(-2)|^)' .
- '|' . // contains no "::"
- RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){7}' .
- ')'
-);
-// An IPv6 block is an IP address and a prefix (d1 to d128)
-define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX );
-// For IPv6 canonicalization (NOT for strict validation; these are quite lax!)
-define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' );
-define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP . '(?:ffff:)?' );
-
-// This might be useful for regexps used elsewhere, matches any IPv6 or IPv6 address or network
-define( 'IP_ADDRESS_STRING',
- '(?:' .
- RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?' . // IPv4
- '|' .
- RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?' . // IPv6
- ')'
-);
-
-/**
- * A collection of public static functions to play with IP address
- * and IP blocks.
- */
-class IP {
- /** @var IPSet */
- private static $proxyIpSet = null;
-
- /**
- * Determine if a string is as valid IP address or network (CIDR prefix).
- * SIIT IPv4-translated addresses are rejected.
- * @note canonicalize() tries to convert translated addresses to IPv4.
- *
- * @param string $ip Possible IP address
- * @return bool
- */
- public static function isIPAddress( $ip ) {
- return (bool)preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip );
- }
-
- /**
- * Given a string, determine if it as valid IP in IPv6 only.
- * @note Unlike isValid(), this looks for networks too.
- *
- * @param string $ip Possible IP address
- * @return bool
- */
- public static function isIPv6( $ip ) {
- return (bool)preg_match( '/^' . RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?$/', $ip );
- }
-
- /**
- * Given a string, determine if it as valid IP in IPv4 only.
- * @note Unlike isValid(), this looks for networks too.
- *
- * @param string $ip Possible IP address
- * @return bool
- */
- public static function isIPv4( $ip ) {
- return (bool)preg_match( '/^' . RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?$/', $ip );
- }
-
- /**
- * Validate an IP address. Ranges are NOT considered valid.
- * SIIT IPv4-translated addresses are rejected.
- * @note canonicalize() tries to convert translated addresses to IPv4.
- *
- * @param string $ip
- * @return bool True if it is valid
- */
- public static function isValid( $ip ) {
- return ( preg_match( '/^' . RE_IP_ADD . '$/', $ip )
- || preg_match( '/^' . RE_IPV6_ADD . '$/', $ip ) );
- }
-
- /**
- * Validate an IP Block (valid address WITH a valid prefix).
- * SIIT IPv4-translated addresses are rejected.
- * @note canonicalize() tries to convert translated addresses to IPv4.
- *
- * @param string $ipblock
- * @return bool True if it is valid
- */
- public static function isValidBlock( $ipblock ) {
- return ( preg_match( '/^' . RE_IPV6_BLOCK . '$/', $ipblock )
- || preg_match( '/^' . RE_IP_BLOCK . '$/', $ipblock ) );
- }
-
- /**
- * Convert an IP into a verbose, uppercase, normalized form.
- * Both IPv4 and IPv6 addresses are trimmed. Additionally,
- * IPv6 addresses in octet notation are expanded to 8 words;
- * IPv4 addresses have leading zeros, in each octet, removed.
- *
- * @param string $ip IP address in quad or octet form (CIDR or not).
- * @return string
- */
- public static function sanitizeIP( $ip ) {
- $ip = trim( $ip );
- if ( $ip === '' ) {
- return null;
- }
- /* If not an IP, just return trimmed value, since sanitizeIP() is called
- * in a number of contexts where usernames are supplied as input.
- */
- if ( !self::isIPAddress( $ip ) ) {
- return $ip;
- }
- if ( self::isIPv4( $ip ) ) {
- // Remove leading 0's from octet representation of IPv4 address
- $ip = preg_replace( '/(?:^|(?<=\.))0+(?=[1-9]|0\.|0$)/', '', $ip );
- return $ip;
- }
- // Remove any whitespaces, convert to upper case
- $ip = strtoupper( $ip );
- // Expand zero abbreviations
- $abbrevPos = strpos( $ip, '::' );
- if ( $abbrevPos !== false ) {
- // We know this is valid IPv6. Find the last index of the
- // address before any CIDR number (e.g. "a:b:c::/24").
- $CIDRStart = strpos( $ip, "/" );
- $addressEnd = ( $CIDRStart !== false )
- ? $CIDRStart - 1
- : strlen( $ip ) - 1;
- // If the '::' is at the beginning...
- if ( $abbrevPos == 0 ) {
- $repeat = '0:';
- $extra = ( $ip == '::' ) ? '0' : ''; // for the address '::'
- $pad = 9; // 7+2 (due to '::')
- // If the '::' is at the end...
- } elseif ( $abbrevPos == ( $addressEnd - 1 ) ) {
- $repeat = ':0';
- $extra = '';
- $pad = 9; // 7+2 (due to '::')
- // If the '::' is in the middle...
- } else {
- $repeat = ':0';
- $extra = ':';
- $pad = 8; // 6+2 (due to '::')
- }
- $ip = str_replace( '::',
- str_repeat( $repeat, $pad - substr_count( $ip, ':' ) ) . $extra,
- $ip
- );
- }
- // Remove leading zeros from each bloc as needed
- $ip = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip );
-
- return $ip;
- }
-
- /**
- * Prettify an IP for display to end users.
- * This will make it more compact and lower-case.
- *
- * @param string $ip
- * @return string
- */
- public static function prettifyIP( $ip ) {
- $ip = self::sanitizeIP( $ip ); // normalize (removes '::')
- if ( self::isIPv6( $ip ) ) {
- // Split IP into an address and a CIDR
- if ( strpos( $ip, '/' ) !== false ) {
- list( $ip, $cidr ) = explode( '/', $ip, 2 );
- } else {
- list( $ip, $cidr ) = [ $ip, '' ];
- }
- // Get the largest slice of words with multiple zeros
- $offset = 0;
- $longest = $longestPos = false;
- while ( preg_match(
- '!(?:^|:)0(?::0)+(?:$|:)!', $ip, $m, PREG_OFFSET_CAPTURE, $offset
- ) ) {
- list( $match, $pos ) = $m[0]; // full match
- if ( strlen( $match ) > strlen( $longest ) ) {
- $longest = $match;
- $longestPos = $pos;
- }
- $offset = ( $pos + strlen( $match ) ); // advance
- }
- if ( $longest !== false ) {
- // Replace this portion of the string with the '::' abbreviation
- $ip = substr_replace( $ip, '::', $longestPos, strlen( $longest ) );
- }
- // Add any CIDR back on
- if ( $cidr !== '' ) {
- $ip = "{$ip}/{$cidr}";
- }
- // Convert to lower case to make it more readable
- $ip = strtolower( $ip );
- }
-
- return $ip;
- }
-
- /**
- * Given a host/port string, like one might find in the host part of a URL
- * per RFC 2732, split the hostname part and the port part and return an
- * array with an element for each. If there is no port part, the array will
- * have false in place of the port. If the string was invalid in some way,
- * false is returned.
- *
- * This was easy with IPv4 and was generally done in an ad-hoc way, but
- * with IPv6 it's somewhat more complicated due to the need to parse the
- * square brackets and colons.
- *
- * A bare IPv6 address is accepted despite the lack of square brackets.
- *
- * @param string $both The string with the host and port
- * @return array|false Array normally, false on certain failures
- */
- public static function splitHostAndPort( $both ) {
- if ( substr( $both, 0, 1 ) === '[' ) {
- if ( preg_match( '/^\[(' . RE_IPV6_ADD . ')\](?::(?P<port>\d+))?$/', $both, $m ) ) {
- if ( isset( $m['port'] ) ) {
- return [ $m[1], intval( $m['port'] ) ];
- } else {
- return [ $m[1], false ];
- }
- } else {
- // Square bracket found but no IPv6
- return false;
- }
- }
- $numColons = substr_count( $both, ':' );
- if ( $numColons >= 2 ) {
- // Is it a bare IPv6 address?
- if ( preg_match( '/^' . RE_IPV6_ADD . '$/', $both ) ) {
- return [ $both, false ];
- } else {
- // Not valid IPv6, but too many colons for anything else
- return false;
- }
- }
- if ( $numColons >= 1 ) {
- // Host:port?
- $bits = explode( ':', $both );
- if ( preg_match( '/^\d+/', $bits[1] ) ) {
- return [ $bits[0], intval( $bits[1] ) ];
- } else {
- // Not a valid port
- return false;
- }
- }
-
- // Plain hostname
- return [ $both, false ];
- }
-
- /**
- * Given a host name and a port, combine them into host/port string like
- * you might find in a URL. If the host contains a colon, wrap it in square
- * brackets like in RFC 2732. If the port matches the default port, omit
- * the port specification
- *
- * @param string $host
- * @param int $port
- * @param bool|int $defaultPort
- * @return string
- */
- public static function combineHostAndPort( $host, $port, $defaultPort = false ) {
- if ( strpos( $host, ':' ) !== false ) {
- $host = "[$host]";
- }
- if ( $defaultPort !== false && $port == $defaultPort ) {
- return $host;
- } else {
- return "$host:$port";
- }
- }
-
- /**
- * Convert an IPv4 or IPv6 hexadecimal representation back to readable format
- *
- * @param string $hex Number, with "v6-" prefix if it is IPv6
- * @return string Quad-dotted (IPv4) or octet notation (IPv6)
- */
- public static function formatHex( $hex ) {
- if ( substr( $hex, 0, 3 ) == 'v6-' ) { // IPv6
- return self::hexToOctet( substr( $hex, 3 ) );
- } else { // IPv4
- return self::hexToQuad( $hex );
- }
- }
-
- /**
- * Converts a hexadecimal number to an IPv6 address in octet notation
- *
- * @param string $ip_hex Pure hex (no v6- prefix)
- * @return string (of format a:b:c:d:e:f:g:h)
- */
- public static function hexToOctet( $ip_hex ) {
- // Pad hex to 32 chars (128 bits)
- $ip_hex = str_pad( strtoupper( $ip_hex ), 32, '0', STR_PAD_LEFT );
- // Separate into 8 words
- $ip_oct = substr( $ip_hex, 0, 4 );
- for ( $n = 1; $n < 8; $n++ ) {
- $ip_oct .= ':' . substr( $ip_hex, 4 * $n, 4 );
- }
- // NO leading zeroes
- $ip_oct = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip_oct );
-
- return $ip_oct;
- }
-
- /**
- * Converts a hexadecimal number to an IPv4 address in quad-dotted notation
- *
- * @param string $ip_hex Pure hex
- * @return string (of format a.b.c.d)
- */
- public static function hexToQuad( $ip_hex ) {
- // Pad hex to 8 chars (32 bits)
- $ip_hex = str_pad( strtoupper( $ip_hex ), 8, '0', STR_PAD_LEFT );
- // Separate into four quads
- $s = '';
- for ( $i = 0; $i < 4; $i++ ) {
- if ( $s !== '' ) {
- $s .= '.';
- }
- $s .= base_convert( substr( $ip_hex, $i * 2, 2 ), 16, 10 );
- }
-
- return $s;
- }
-
- /**
- * Determine if an IP address really is an IP address, and if it is public,
- * i.e. not RFC 1918 or similar
- *
- * @param string $ip
- * @return bool
- */
- public static function isPublic( $ip ) {
- static $privateSet = null;
- if ( !$privateSet ) {
- $privateSet = new IPSet( [
- '10.0.0.0/8', # RFC 1918 (private)
- '172.16.0.0/12', # RFC 1918 (private)
- '192.168.0.0/16', # RFC 1918 (private)
- '0.0.0.0/8', # this network
- '127.0.0.0/8', # loopback
- 'fc00::/7', # RFC 4193 (local)
- '0:0:0:0:0:0:0:1', # loopback
- '169.254.0.0/16', # link-local
- 'fe80::/10', # link-local
- ] );
- }
- return !$privateSet->match( $ip );
- }
-
- /**
- * Return a zero-padded upper case hexadecimal representation of an IP address.
- *
- * Hexadecimal addresses are used because they can easily be extended to
- * IPv6 support. To separate the ranges, the return value from this
- * function for an IPv6 address will be prefixed with "v6-", a non-
- * hexadecimal string which sorts after the IPv4 addresses.
- *
- * @param string $ip Quad dotted/octet IP address.
- * @return string|bool False on failure
- */
- public static function toHex( $ip ) {
- if ( self::isIPv6( $ip ) ) {
- $n = 'v6-' . self::IPv6ToRawHex( $ip );
- } elseif ( self::isIPv4( $ip ) ) {
- // T62035/T97897: An IP with leading 0's fails in ip2long sometimes (e.g. *.08),
- // also double/triple 0 needs to be changed to just a single 0 for ip2long.
- $ip = self::sanitizeIP( $ip );
- $n = ip2long( $ip );
- if ( $n < 0 ) {
- $n += pow( 2, 32 );
- # On 32-bit platforms (and on Windows), 2^32 does not fit into an int,
- # so $n becomes a float. We convert it to string instead.
- if ( is_float( $n ) ) {
- $n = (string)$n;
- }
- }
- if ( $n !== false ) {
- # Floating points can handle the conversion; faster than Wikimedia\base_convert()
- $n = strtoupper( str_pad( base_convert( $n, 10, 16 ), 8, '0', STR_PAD_LEFT ) );
- }
- } else {
- $n = false;
- }
-
- return $n;
- }
-
- /**
- * Given an IPv6 address in octet notation, returns a pure hex string.
- *
- * @param string $ip Octet ipv6 IP address.
- * @return string|bool Pure hex (uppercase); false on failure
- */
- private static function IPv6ToRawHex( $ip ) {
- $ip = self::sanitizeIP( $ip );
- if ( !$ip ) {
- return false;
- }
- $r_ip = '';
- foreach ( explode( ':', $ip ) as $v ) {
- $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT );
- }
-
- return $r_ip;
- }
-
- /**
- * Convert a network specification in CIDR notation
- * to an integer network and a number of bits
- *
- * @param string $range IP with CIDR prefix
- * @return array(int or string, int)
- */
- public static function parseCIDR( $range ) {
- if ( self::isIPv6( $range ) ) {
- return self::parseCIDR6( $range );
- }
- $parts = explode( '/', $range, 2 );
- if ( count( $parts ) != 2 ) {
- return [ false, false ];
- }
- list( $network, $bits ) = $parts;
- $network = ip2long( $network );
- if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 32 ) {
- if ( $bits == 0 ) {
- $network = 0;
- } else {
- $network &= ~( ( 1 << ( 32 - $bits ) ) - 1 );
- }
- # Convert to unsigned
- if ( $network < 0 ) {
- $network += pow( 2, 32 );
- }
- } else {
- $network = false;
- $bits = false;
- }
-
- return [ $network, $bits ];
- }
-
- /**
- * Given a string range in a number of formats,
- * return the start and end of the range in hexadecimal.
- *
- * Formats are:
- * 1.2.3.4/24 CIDR
- * 1.2.3.4 - 1.2.3.5 Explicit range
- * 1.2.3.4 Single IP
- *
- * 2001:0db8:85a3::7344/96 CIDR
- * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range
- * 2001:0db8:85a3::7344 Single IP
- * @param string $range IP range
- * @return array(string, string)
- */
- public static function parseRange( $range ) {
- // CIDR notation
- if ( strpos( $range, '/' ) !== false ) {
- if ( self::isIPv6( $range ) ) {
- return self::parseRange6( $range );
- }
- list( $network, $bits ) = self::parseCIDR( $range );
- if ( $network === false ) {
- $start = $end = false;
- } else {
- $start = sprintf( '%08X', $network );
- $end = sprintf( '%08X', $network + pow( 2, ( 32 - $bits ) ) - 1 );
- }
- // Explicit range
- } elseif ( strpos( $range, '-' ) !== false ) {
- list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
- if ( self::isIPv6( $start ) && self::isIPv6( $end ) ) {
- return self::parseRange6( $range );
- }
- if ( self::isIPv4( $start ) && self::isIPv4( $end ) ) {
- $start = self::toHex( $start );
- $end = self::toHex( $end );
- if ( $start > $end ) {
- $start = $end = false;
- }
- } else {
- $start = $end = false;
- }
- } else {
- # Single IP
- $start = $end = self::toHex( $range );
- }
- if ( $start === false || $end === false ) {
- return [ false, false ];
- } else {
- return [ $start, $end ];
- }
- }
-
- /**
- * Convert a network specification in IPv6 CIDR notation to an
- * integer network and a number of bits
- *
- * @param string $range
- *
- * @return array(string, int)
- */
- private static function parseCIDR6( $range ) {
- # Explode into <expanded IP,range>
- $parts = explode( '/', IP::sanitizeIP( $range ), 2 );
- if ( count( $parts ) != 2 ) {
- return [ false, false ];
- }
- list( $network, $bits ) = $parts;
- $network = self::IPv6ToRawHex( $network );
- if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 128 ) {
- if ( $bits == 0 ) {
- $network = "0";
- } else {
- # Native 32 bit functions WONT work here!!!
- # Convert to a padded binary number
- $network = Wikimedia\base_convert( $network, 16, 2, 128 );
- # Truncate the last (128-$bits) bits and replace them with zeros
- $network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT );
- # Convert back to an integer
- $network = Wikimedia\base_convert( $network, 2, 10 );
- }
- } else {
- $network = false;
- $bits = false;
- }
-
- return [ $network, (int)$bits ];
- }
-
- /**
- * Given a string range in a number of formats, return the
- * start and end of the range in hexadecimal. For IPv6.
- *
- * Formats are:
- * 2001:0db8:85a3::7344/96 CIDR
- * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range
- * 2001:0db8:85a3::7344/96 Single IP
- *
- * @param string $range
- *
- * @return array(string, string)
- */
- private static function parseRange6( $range ) {
- # Expand any IPv6 IP
- $range = IP::sanitizeIP( $range );
- // CIDR notation...
- if ( strpos( $range, '/' ) !== false ) {
- list( $network, $bits ) = self::parseCIDR6( $range );
- if ( $network === false ) {
- $start = $end = false;
- } else {
- $start = Wikimedia\base_convert( $network, 10, 16, 32, false );
- # Turn network to binary (again)
- $end = Wikimedia\base_convert( $network, 10, 2, 128 );
- # Truncate the last (128-$bits) bits and replace them with ones
- $end = str_pad( substr( $end, 0, $bits ), 128, 1, STR_PAD_RIGHT );
- # Convert to hex
- $end = Wikimedia\base_convert( $end, 2, 16, 32, false );
- # see toHex() comment
- $start = "v6-$start";
- $end = "v6-$end";
- }
- // Explicit range notation...
- } elseif ( strpos( $range, '-' ) !== false ) {
- list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
- $start = self::toHex( $start );
- $end = self::toHex( $end );
- if ( $start > $end ) {
- $start = $end = false;
- }
- } else {
- # Single IP
- $start = $end = self::toHex( $range );
- }
- if ( $start === false || $end === false ) {
- return [ false, false ];
- } else {
- return [ $start, $end ];
- }
- }
-
- /**
- * Determine if a given IPv4/IPv6 address is in a given CIDR network
- *
- * @param string $addr The address to check against the given range.
- * @param string $range The range to check the given address against.
- * @return bool Whether or not the given address is in the given range.
- *
- * @note This can return unexpected results for invalid arguments!
- * Make sure you pass a valid IP address and IP range.
- */
- public static function isInRange( $addr, $range ) {
- $hexIP = self::toHex( $addr );
- list( $start, $end ) = self::parseRange( $range );
-
- return ( strcmp( $hexIP, $start ) >= 0 &&
- strcmp( $hexIP, $end ) <= 0 );
- }
-
- /**
- * Determines if an IP address is a list of CIDR a.b.c.d/n ranges.
- *
- * @since 1.25
- *
- * @param string $ip the IP to check
- * @param array $ranges the IP ranges, each element a range
- *
- * @return bool true if the specified adress belongs to the specified range; otherwise, false.
- */
- public static function isInRanges( $ip, $ranges ) {
- foreach ( $ranges as $range ) {
- if ( self::isInRange( $ip, $range ) ) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Convert some unusual representations of IPv4 addresses to their
- * canonical dotted quad representation.
- *
- * This currently only checks a few IPV4-to-IPv6 related cases. More
- * unusual representations may be added later.
- *
- * @param string $addr Something that might be an IP address
- * @return string|null Valid dotted quad IPv4 address or null
- */
- public static function canonicalize( $addr ) {
- // remove zone info (bug 35738)
- $addr = preg_replace( '/\%.*/', '', $addr );
-
- if ( self::isValid( $addr ) ) {
- return $addr;
- }
- // Turn mapped addresses from ::ce:ffff:1.2.3.4 to 1.2.3.4
- if ( strpos( $addr, ':' ) !== false && strpos( $addr, '.' ) !== false ) {
- $addr = substr( $addr, strrpos( $addr, ':' ) + 1 );
- if ( self::isIPv4( $addr ) ) {
- return $addr;
- }
- }
- // IPv6 loopback address
- $m = [];
- if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) {
- return '127.0.0.1';
- }
- // IPv4-mapped and IPv4-compatible IPv6 addresses
- if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) {
- return $m[1];
- }
- if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD .
- ':' . RE_IPV6_WORD . '$/i', $addr, $m )
- ) {
- return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) );
- }
-
- return null; // give up
- }
-
- /**
- * Gets rid of unneeded numbers in quad-dotted/octet IP strings
- * For example, 127.111.113.151/24 -> 127.111.113.0/24
- * @param string $range IP address to normalize
- * @return string
- */
- public static function sanitizeRange( $range ) {
- list( /*...*/, $bits ) = self::parseCIDR( $range );
- list( $start, /*...*/ ) = self::parseRange( $range );
- $start = self::formatHex( $start );
- if ( $bits === false ) {
- return $start; // wasn't actually a range
- }
-
- return "$start/$bits";
- }
-
- /**
- * Checks if an IP is a trusted proxy provider.
- * Useful to tell if X-Forwarded-For data is possibly bogus.
- * CDN cache servers for the site are whitelisted.
- * @since 1.24
- *
- * @param string $ip
- * @return bool
- */
- public static function isTrustedProxy( $ip ) {
- $trusted = self::isConfiguredProxy( $ip );
- Hooks::run( 'IsTrustedProxy', [ &$ip, &$trusted ] );
- return $trusted;
- }
-
- /**
- * Checks if an IP matches a proxy we've configured
- * @since 1.24
- *
- * @param string $ip
- * @return bool
- */
- public static function isConfiguredProxy( $ip ) {
- global $wgSquidServers, $wgSquidServersNoPurge;
-
- // Quick check of known singular proxy servers
- $trusted = in_array( $ip, $wgSquidServers );
-
- // Check against addresses and CIDR nets in the NoPurge list
- if ( !$trusted ) {
- if ( !self::$proxyIpSet ) {
- self::$proxyIpSet = new IPSet( $wgSquidServersNoPurge );
- }
- $trusted = self::$proxyIpSet->match( $ip );
- }
-
- return $trusted;
- }
-
- /**
- * Clears precomputed data used for proxy support.
- * Use this only for unit tests.
- */
- public static function clearCaches() {
- self::$proxyIpSet = null;
- }
-
- /**
- * Returns the subnet of a given IP
- *
- * @param string $ip
- * @return string|false
- */
- public static function getSubnet( $ip ) {
- $matches = [];
- $subnet = false;
- if ( IP::isIPv6( $ip ) ) {
- $parts = IP::parseRange( "$ip/64" );
- $subnet = $parts[0];
- } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
- // IPv4
- $subnet = $matches[1];
- }
- return $subnet;
- }
-}
diff --git a/www/wiki/includes/utils/MWCryptHash.php b/www/wiki/includes/utils/MWCryptHash.php
deleted file mode 100644
index 11173573..00000000
--- a/www/wiki/includes/utils/MWCryptHash.php
+++ /dev/null
@@ -1,115 +0,0 @@
-<?php
-/**
- * Utility functions for generating hashes
- *
- * This is based in part on Drupal code as well as what we used in our own code
- * prior to introduction of this class, by way of MWCryptRand.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-class MWCryptHash {
- /**
- * The hash algorithm being used
- */
- protected static $algo = null;
-
- /**
- * The number of bytes outputted by the hash algorithm
- */
- protected static $hashLength = [
- true => null,
- false => null,
- ];
-
- /**
- * Decide on the best acceptable hash algorithm we have available for hash()
- * @return string A hash algorithm
- */
- public static function hashAlgo() {
- if ( !is_null( self::$algo ) ) {
- return self::$algo;
- }
-
- $algos = hash_algos();
- $preference = [ 'whirlpool', 'sha256', 'sha1', 'md5' ];
-
- foreach ( $preference as $algorithm ) {
- if ( in_array( $algorithm, $algos ) ) {
- self::$algo = $algorithm;
- wfDebug( __METHOD__ . ': Using the ' . self::$algo . " hash algorithm.\n" );
-
- return self::$algo;
- }
- }
-
- // We only reach here if no acceptable hash is found in the list, this should
- // be a technical impossibility since most of php's hash list is fixed and
- // some of the ones we list are available as their own native functions
- // But since we already require at least 5.2 and hash() was default in
- // 5.1.2 we don't bother falling back to methods like sha1 and md5.
- throw new DomainException( "Could not find an acceptable hashing function in hash_algos()" );
- }
-
- /**
- * Return the byte-length output of the hash algorithm we are
- * using in self::hash and self::hmac.
- *
- * @param bool $raw True to return the length for binary data, false to
- * return for hex-encoded
- * @return int Number of bytes the hash outputs
- */
- public static function hashLength( $raw = true ) {
- $raw = (bool)$raw;
- if ( is_null( self::$hashLength[$raw] ) ) {
- self::$hashLength[$raw] = strlen( self::hash( '', $raw ) );
- }
-
- return self::$hashLength[$raw];
- }
-
- /**
- * Generate an acceptably unstable one-way-hash of some text
- * making use of the best hash algorithm that we have available.
- *
- * @param string $data
- * @param bool $raw True to return binary data, false to return it hex-encoded
- * @return string A hash of the data
- */
- public static function hash( $data, $raw = true ) {
- return hash( self::hashAlgo(), $data, $raw );
- }
-
- /**
- * Generate an acceptably unstable one-way-hmac of some text
- * making use of the best hash algorithm that we have available.
- *
- * @param string $data
- * @param string $key
- * @param bool $raw True to return binary data, false to return it hex-encoded
- * @return string An hmac hash of the data + key
- */
- public static function hmac( $data, $key, $raw = true ) {
- if ( !is_string( $key ) ) {
- // a fatal error in HHVM; an exception will at least give us a stack trace
- throw new InvalidArgumentException( 'Invalid key type: ' . gettype( $key ) );
- }
- return hash_hmac( self::hashAlgo(), $data, $key, $raw );
- }
-
-}
diff --git a/www/wiki/includes/utils/MWFileProps.php b/www/wiki/includes/utils/MWFileProps.php
index e60b9ab7..9d05c6ab 100644
--- a/www/wiki/includes/utils/MWFileProps.php
+++ b/www/wiki/includes/utils/MWFileProps.php
@@ -30,9 +30,9 @@ class MWFileProps {
private $magic;
/**
- * @param MimeMagic $magic
+ * @param MimeAnalyzer $magic
*/
- public function __construct( MimeMagic $magic ) {
+ public function __construct( MimeAnalyzer $magic ) {
$this->magic = $magic;
}
diff --git a/www/wiki/includes/utils/MWGrants.php b/www/wiki/includes/utils/MWGrants.php
deleted file mode 100644
index 58efdc72..00000000
--- a/www/wiki/includes/utils/MWGrants.php
+++ /dev/null
@@ -1,214 +0,0 @@
-<?php
-/**
- * Functions and constants to deal with grants
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- */
-
-/**
- * A collection of public static functions to deal with grants.
- */
-class MWGrants {
-
- /**
- * List all known grants.
- * @return array
- */
- public static function getValidGrants() {
- global $wgGrantPermissions;
-
- return array_keys( $wgGrantPermissions );
- }
-
- /**
- * Map all grants to corresponding user rights.
- * @return array grant => array of rights
- */
- public static function getRightsByGrant() {
- global $wgGrantPermissions;
-
- $res = [];
- foreach ( $wgGrantPermissions as $grant => $rights ) {
- $res[$grant] = array_keys( array_filter( $rights ) );
- }
- return $res;
- }
-
- /**
- * Fetch the display name of the grant
- * @param string $grant
- * @param Language|string|null $lang
- * @return string Grant description
- */
- public static function grantName( $grant, $lang = null ) {
- // Give grep a chance to find the usages:
- // grant-blockusers, grant-createeditmovepage, grant-delete,
- // grant-editinterface, grant-editmycssjs, grant-editmywatchlist,
- // grant-editpage, grant-editprotected, grant-highvolume,
- // grant-oversight, grant-patrol, grant-protect, grant-rollback,
- // grant-sendemail, grant-uploadeditmovefile, grant-uploadfile,
- // grant-basic, grant-viewdeleted, grant-viewmywatchlist,
- // grant-createaccount
- $msg = wfMessage( "grant-$grant" );
- if ( $lang !== null ) {
- if ( is_string( $lang ) ) {
- $lang = Language::factory( $lang );
- }
- $msg->inLanguage( $lang );
- }
- if ( !$msg->exists() ) {
- $msg = wfMessage( 'grant-generic', $grant );
- if ( $lang ) {
- $msg->inLanguage( $lang );
- }
- }
- return $msg->text();
- }
-
- /**
- * Fetch the display names for the grants.
- * @param string[] $grants
- * @param Language|string|null $lang
- * @return string[] Corresponding grant descriptions
- */
- public static function grantNames( array $grants, $lang = null ) {
- if ( $lang !== null ) {
- if ( is_string( $lang ) ) {
- $lang = Language::factory( $lang );
- }
- }
-
- $ret = [];
- foreach ( $grants as $grant ) {
- $ret[] = self::grantName( $grant, $lang );
- }
- return $ret;
- }
-
- /**
- * Fetch the rights allowed by a set of grants.
- * @param string[]|string $grants
- * @return string[]
- */
- public static function getGrantRights( $grants ) {
- global $wgGrantPermissions;
-
- $rights = [];
- foreach ( (array)$grants as $grant ) {
- if ( isset( $wgGrantPermissions[$grant] ) ) {
- $rights = array_merge( $rights, array_keys( array_filter( $wgGrantPermissions[$grant] ) ) );
- }
- }
- return array_unique( $rights );
- }
-
- /**
- * Test that all grants in the list are known.
- * @param string[] $grants
- * @return bool
- */
- public static function grantsAreValid( array $grants ) {
- return array_diff( $grants, self::getValidGrants() ) === [];
- }
-
- /**
- * Divide the grants into groups.
- * @param string[]|null $grantsFilter
- * @return array Map of (group => (grant list))
- */
- public static function getGrantGroups( $grantsFilter = null ) {
- global $wgGrantPermissions, $wgGrantPermissionGroups;
-
- if ( is_array( $grantsFilter ) ) {
- $grantsFilter = array_flip( $grantsFilter );
- }
-
- $groups = [];
- foreach ( $wgGrantPermissions as $grant => $rights ) {
- if ( $grantsFilter !== null && !isset( $grantsFilter[$grant] ) ) {
- continue;
- }
- if ( isset( $wgGrantPermissionGroups[$grant] ) ) {
- $groups[$wgGrantPermissionGroups[$grant]][] = $grant;
- } else {
- $groups['other'][] = $grant;
- }
- }
-
- return $groups;
- }
-
- /**
- * Get the list of grants that are hidden and should always be granted
- * @return string[]
- */
- public static function getHiddenGrants() {
- global $wgGrantPermissionGroups;
-
- $grants = [];
- foreach ( $wgGrantPermissionGroups as $grant => $group ) {
- if ( $group === 'hidden' ) {
- $grants[] = $grant;
- }
- }
- return $grants;
- }
-
- /**
- * Generate a link to Special:ListGrants for a particular grant name.
- *
- * This should be used to link end users to a full description of what
- * rights they are giving when they authorize a grant.
- *
- * @param string $grant the grant name
- * @param Language|string|null $lang
- * @return string (proto-relative) HTML link
- */
- public static function getGrantsLink( $grant, $lang = null ) {
- return \Linker::linkKnown(
- \SpecialPage::getTitleFor( 'Listgrants', false, $grant ),
- htmlspecialchars( self::grantName( $grant, $lang ) )
- );
- }
-
- /**
- * Generate wikitext to display a list of grants
- * @param string[]|null $grantsFilter If non-null, only display these grants.
- * @param Language|string|null $lang
- * @return string Wikitext
- */
- public static function getGrantsWikiText( $grantsFilter, $lang = null ) {
- global $wgContLang;
-
- if ( is_string( $lang ) ) {
- $lang = Language::factory( $lang );
- } elseif ( $lang === null ) {
- $lang = $wgContLang;
- }
-
- $s = '';
- foreach ( self::getGrantGroups( $grantsFilter ) as $group => $grants ) {
- if ( $group === 'hidden' ) {
- continue; // implicitly granted
- }
- $s .= "*<span class=\"mw-grantgroup\">" .
- wfMessage( "grant-group-$group" )->inLanguage( $lang )->text() . "</span>\n";
- $s .= ":" . $lang->semicolonList( self::grantNames( $grants, $lang ) ) . "\n";
- }
- return "$s\n";
- }
-
-}
diff --git a/www/wiki/includes/utils/UIDGenerator.php b/www/wiki/includes/utils/UIDGenerator.php
index 736109b4..4d5c3af8 100644
--- a/www/wiki/includes/utils/UIDGenerator.php
+++ b/www/wiki/includes/utils/UIDGenerator.php
@@ -53,7 +53,7 @@ class UIDGenerator {
}
// Try to get some ID that uniquely identifies this machine (RFC 4122)...
if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
if ( wfIsWindows() ) {
// https://technet.microsoft.com/en-us/library/bb490913.aspx
$csv = trim( wfShellExec( 'getmac /NH /FO CSV' ) );
@@ -67,7 +67,7 @@ class UIDGenerator {
wfShellExec( '/sbin/ifconfig -a' ), $m );
$nodeId = isset( $m[1] ) ? str_replace( ':', '', $m[1] ) : '';
}
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) {
$nodeId = MWCryptRand::generateHex( 12, true );
$nodeId[1] = dechex( hexdec( $nodeId[1] ) | 0x1 ); // set multicast bit
@@ -364,10 +364,10 @@ class UIDGenerator {
$counter = null; // post-increment persistent counter value
- // Use APC/eAccelerator/xcache if requested, available, and not in CLI mode;
+ // Use APC/etc if requested, available, and not in CLI mode;
// Counter values would not survive accross script instances in CLI mode.
$cache = null;
- if ( ( $flags & self::QUICK_VOLATILE ) && PHP_SAPI !== 'cli' ) {
+ if ( ( $flags & self::QUICK_VOLATILE ) && !wfIsCLI() ) {
$cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
}
if ( $cache ) {
diff --git a/www/wiki/includes/utils/iterators/IteratorDecorator.php b/www/wiki/includes/utils/iterators/IteratorDecorator.php
deleted file mode 100644
index c1b50207..00000000
--- a/www/wiki/includes/utils/iterators/IteratorDecorator.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-/**
- * Allows extending classes to decorate an Iterator with
- * reduced boilerplate.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Maintenance
- */
-abstract class IteratorDecorator implements Iterator {
- protected $iterator;
-
- public function __construct( Iterator $iterator ) {
- $this->iterator = $iterator;
- }
-
- public function current() {
- return $this->iterator->current();
- }
-
- public function key() {
- return $this->iterator->key();
- }
-
- public function next() {
- $this->iterator->next();
- }
-
- public function rewind() {
- $this->iterator->rewind();
- }
-
- public function valid() {
- return $this->iterator->valid();
- }
-}
diff --git a/www/wiki/includes/watcheditem/NoWriteWatchedItemStore.php b/www/wiki/includes/watcheditem/NoWriteWatchedItemStore.php
new file mode 100644
index 00000000..86e7be85
--- /dev/null
+++ b/www/wiki/includes/watcheditem/NoWriteWatchedItemStore.php
@@ -0,0 +1,145 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Watchlist
+ */
+use MediaWiki\Linker\LinkTarget;
+use Wikimedia\Rdbms\DBReadOnlyError;
+
+/**
+ * @internal
+ * @since 1.31
+ */
+class NoWriteWatchedItemStore implements WatchedItemStoreInterface {
+
+ /**
+ * @var WatchedItemStoreInterface
+ */
+ private $actualStore;
+
+ /**
+ * Initialy set WatchedItemStore that will be used in cases where writing is not needed.
+ * @param WatchedItemStoreInterface $actualStore
+ */
+ public function __construct( WatchedItemStoreInterface $actualStore ) {
+ $this->actualStore = $actualStore;
+ }
+
+ public function countWatchedItems( User $user ) {
+ return $this->actualStore->countWatchedItems( $user );
+ }
+
+ public function countWatchers( LinkTarget $target ) {
+ return $this->actualStore->countWatchers( $target );
+ }
+
+ public function countVisitingWatchers( LinkTarget $target, $threshold ) {
+ return $this->actualStore->countVisitingWatchers( $target, $threshold );
+ }
+
+ public function countWatchersMultiple( array $targets, array $options = [] ) {
+ return $this->actualStore->countVisitingWatchersMultiple( $targets, $options );
+ }
+
+ public function countVisitingWatchersMultiple(
+ array $targetsWithVisitThresholds,
+ $minimumWatchers = null
+ ) {
+ return $this->actualStore->countVisitingWatchersMultiple(
+ $targetsWithVisitThresholds,
+ $minimumWatchers
+ );
+ }
+
+ public function getWatchedItem( User $user, LinkTarget $target ) {
+ return $this->actualStore->getWatchedItem( $user, $target );
+ }
+
+ public function loadWatchedItem( User $user, LinkTarget $target ) {
+ return $this->actualStore->loadWatchedItem( $user, $target );
+ }
+
+ public function getWatchedItemsForUser( User $user, array $options = [] ) {
+ return $this->actualStore->getWatchedItemsForUser( $user, $options );
+ }
+
+ public function isWatched( User $user, LinkTarget $target ) {
+ return $this->actualStore->isWatched( $user, $target );
+ }
+
+ public function getNotificationTimestampsBatch( User $user, array $targets ) {
+ return $this->actualStore->getNotificationTimestampsBatch( $user, $targets );
+ }
+
+ public function countUnreadNotifications( User $user, $unreadLimit = null ) {
+ return $this->actualStore->countUnreadNotifications( $user, $unreadLimit );
+ }
+
+ public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget ) {
+ throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+ }
+
+ public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget ) {
+ throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+ }
+
+ public function addWatch( User $user, LinkTarget $target ) {
+ throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+ }
+
+ public function addWatchBatchForUser( User $user, array $targets ) {
+ throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+ }
+
+ public function removeWatch( User $user, LinkTarget $target ) {
+ throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+ }
+
+ public function setNotificationTimestampsForUser(
+ User $user,
+ $timestamp,
+ array $targets = []
+ ) {
+ throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+ }
+
+ public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
+ throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+ }
+
+ public function resetAllNotificationTimestampsForUser( User $user ) {
+ throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+ }
+
+ public function resetNotificationTimestamp(
+ User $user,
+ Title $title,
+ $force = '',
+ $oldid = 0
+ ) {
+ throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+ }
+
+ public function clearUserWatchedItems( User $user ) {
+ throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+ }
+
+ public function clearUserWatchedItemsUsingJobQueue( User $user ) {
+ throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+ }
+}
diff --git a/www/wiki/includes/watcheditem/WatchedItem.php b/www/wiki/includes/watcheditem/WatchedItem.php
new file mode 100644
index 00000000..43a9c4e5
--- /dev/null
+++ b/www/wiki/includes/watcheditem/WatchedItem.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Watchlist
+ */
+
+use MediaWiki\Linker\LinkTarget;
+
+/**
+ * Representation of a pair of user and title for watchlist entries.
+ *
+ * @author Tim Starling
+ * @author Addshore
+ *
+ * @ingroup Watchlist
+ */
+class WatchedItem {
+ /**
+ * @var LinkTarget
+ */
+ private $linkTarget;
+
+ /**
+ * @var User
+ */
+ private $user;
+
+ /**
+ * @var null|string the value of the wl_notificationtimestamp field
+ */
+ private $notificationTimestamp;
+
+ /**
+ * @param User $user
+ * @param LinkTarget $linkTarget
+ * @param null|string $notificationTimestamp the value of the wl_notificationtimestamp field
+ */
+ public function __construct(
+ User $user,
+ LinkTarget $linkTarget,
+ $notificationTimestamp
+ ) {
+ $this->user = $user;
+ $this->linkTarget = $linkTarget;
+ $this->notificationTimestamp = $notificationTimestamp;
+ }
+
+ /**
+ * @return User
+ */
+ public function getUser() {
+ return $this->user;
+ }
+
+ /**
+ * @return LinkTarget
+ */
+ public function getLinkTarget() {
+ return $this->linkTarget;
+ }
+
+ /**
+ * Get the notification timestamp of this entry.
+ *
+ * @return bool|null|string
+ */
+ public function getNotificationTimestamp() {
+ return $this->notificationTimestamp;
+ }
+}
diff --git a/www/wiki/includes/WatchedItemQueryService.php b/www/wiki/includes/watcheditem/WatchedItemQueryService.php
index d0f45bec..b91e36e5 100644
--- a/www/wiki/includes/WatchedItemQueryService.php
+++ b/www/wiki/includes/watcheditem/WatchedItemQueryService.php
@@ -25,8 +25,10 @@ class WatchedItemQueryService {
const INCLUDE_USER_ID = 'userid';
const INCLUDE_COMMENT = 'comment';
const INCLUDE_PATROL_INFO = 'patrol';
+ const INCLUDE_AUTOPATROL_INFO = 'autopatrol';
const INCLUDE_SIZES = 'sizes';
const INCLUDE_LOG_INFO = 'loginfo';
+ const INCLUDE_TAGS = 'tags';
// FILTER_* constants are part of public API (are used in ApiQueryWatchlist and
// ApiQueryWatchlistRaw classes) and should not be changed.
@@ -39,6 +41,8 @@ class WatchedItemQueryService {
const FILTER_NOT_ANON = '!anon';
const FILTER_PATROLLED = 'patrolled';
const FILTER_NOT_PATROLLED = '!patrolled';
+ const FILTER_AUTOPATROLLED = 'autopatrolled';
+ const FILTER_NOT_AUTOPATROLLED = '!autopatrolled';
const FILTER_UNREAD = 'unread';
const FILTER_NOT_UNREAD = '!unread';
const FILTER_CHANGED = 'changed';
@@ -55,12 +59,20 @@ class WatchedItemQueryService {
/** @var WatchedItemQueryServiceExtension[]|null */
private $extensions = null;
- /**
- * @var CommentStore|null */
- private $commentStore = null;
+ /** @var CommentStore */
+ private $commentStore;
+
+ /** @var ActorMigration */
+ private $actorMigration;
- public function __construct( LoadBalancer $loadBalancer ) {
+ public function __construct(
+ LoadBalancer $loadBalancer,
+ CommentStore $commentStore,
+ ActorMigration $actorMigration
+ ) {
$this->loadBalancer = $loadBalancer;
+ $this->commentStore = $commentStore;
+ $this->actorMigration = $actorMigration;
}
/**
@@ -82,13 +94,6 @@ class WatchedItemQueryService {
return $this->loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
}
- private function getCommentStore() {
- if ( !$this->commentStore ) {
- $this->commentStore = new CommentStore( 'rc_comment' );
- }
- return $this->commentStore;
- }
-
/**
* @param User $user
* @param array $options Allowed keys:
@@ -315,8 +320,8 @@ class WatchedItemQueryService {
}
private function getRecentChangeFieldsFromRow( stdClass $row ) {
- // This can be simplified to single array_filter call filtering by key value,
- // once we stop supporting PHP 5.5
+ // FIXME: This can be simplified to single array_filter call filtering by key value,
+ // now we have stopped supporting PHP 5.5
$allFields = get_object_vars( $row );
$rcKeys = array_filter(
array_keys( $allFields ),
@@ -333,7 +338,18 @@ class WatchedItemQueryService {
$tables[] = 'page';
}
if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
- $tables += $this->getCommentStore()->getJoin()['tables'];
+ $tables += $this->commentStore->getJoin( 'rc_comment' )['tables'];
+ }
+ if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
+ $tables[] = 'tag_summary';
+ }
+ if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
+ in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
+ in_array( self::FILTER_ANON, $options['filters'] ) ||
+ in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
+ array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
+ ) {
+ $tables += $this->actorMigration->getJoin( 'rc_user' )['tables'];
}
return $tables;
}
@@ -367,13 +383,13 @@ class WatchedItemQueryService {
$fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
}
if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
- $fields[] = 'rc_user_text';
+ $fields['rc_user_text'] = $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user_text'];
}
if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
- $fields[] = 'rc_user';
+ $fields['rc_user'] = $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user'];
}
if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
- $fields += $this->getCommentStore()->getJoin()['fields'];
+ $fields += $this->commentStore->getJoin( 'rc_comment' )['fields'];
}
if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
$fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
@@ -384,6 +400,10 @@ class WatchedItemQueryService {
if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
$fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
}
+ if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
+ // prefixed with rc_ to include the field in getRecentChangeFieldsFromRow
+ $fields['rc_tags'] = 'ts_tags';
+ }
return $fields;
}
@@ -465,18 +485,28 @@ class WatchedItemQueryService {
}
if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
- $conds[] = 'rc_user = 0';
+ $conds[] = $this->actorMigration->isAnon(
+ $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user']
+ );
} elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
- $conds[] = 'rc_user != 0';
+ $conds[] = $this->actorMigration->isNotAnon(
+ $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user']
+ );
}
if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
// TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
// right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
- $conds[] = 'rc_patrolled != 0';
+ $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
} elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
- $conds[] = 'rc_patrolled = 0';
+ $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
+ }
+
+ if ( in_array( self::FILTER_AUTOPATROLLED, $options['filters'] ) ) {
+ $conds['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
+ } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options['filters'] ) ) {
+ $conds[] = 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED;
}
}
@@ -519,9 +549,11 @@ class WatchedItemQueryService {
$conds = [];
if ( array_key_exists( 'onlyByUser', $options ) ) {
- $conds['rc_user_text'] = $options['onlyByUser'];
+ $byUser = User::newFromName( $options['onlyByUser'], false );
+ $conds[] = $this->actorMigration->getWhere( $db, 'rc_user', $byUser )['conds'];
} elseif ( array_key_exists( 'notByUser', $options ) ) {
- $conds[] = 'rc_user_text != ' . $db->addQuotes( $options['notByUser'] );
+ $byUser = User::newFromName( $options['notByUser'], false );
+ $conds[] = 'NOT(' . $this->actorMigration->getWhere( $db, 'rc_user', $byUser )['conds'] . ')';
}
// Avoid brute force searches (T19342)
@@ -676,7 +708,18 @@ class WatchedItemQueryService {
$joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
}
if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
- $joinConds += $this->getCommentStore()->getJoin()['joins'];
+ $joinConds += $this->commentStore->getJoin( 'rc_comment' )['joins'];
+ }
+ if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
+ $joinConds['tag_summary'] = [ 'LEFT JOIN', [ 'rc_id=ts_rc_id' ] ];
+ }
+ if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
+ in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
+ in_array( self::FILTER_ANON, $options['filters'] ) ||
+ in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
+ array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
+ ) {
+ $joinConds += $this->actorMigration->getJoin( 'rc_user' )['joins'];
}
return $joinConds;
}
diff --git a/www/wiki/includes/WatchedItemQueryServiceExtension.php b/www/wiki/includes/watcheditem/WatchedItemQueryServiceExtension.php
index 93d50330..93d50330 100644
--- a/www/wiki/includes/WatchedItemQueryServiceExtension.php
+++ b/www/wiki/includes/watcheditem/WatchedItemQueryServiceExtension.php
diff --git a/www/wiki/includes/WatchedItemStore.php b/www/wiki/includes/watcheditem/WatchedItemStore.php
index 60d8b769..6b0c2aab 100644
--- a/www/wiki/includes/WatchedItemStore.php
+++ b/www/wiki/includes/watcheditem/WatchedItemStore.php
@@ -7,23 +7,16 @@ use MediaWiki\MediaWikiServices;
use Wikimedia\Assert\Assert;
use Wikimedia\ScopedCallback;
use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\Rdbms\DBUnexpectedError;
/**
* Storage layer class for WatchedItems.
- * Database interaction.
- *
- * Uses database because this uses User::isAnon
- *
- * @group Database
+ * Database interaction & caching
+ * TODO caching should be factored out into a CachingWatchedItemStore class
*
* @author Addshore
* @since 1.27
*/
-class WatchedItemStore implements StatsdAwareInterface {
-
- const SORT_DESC = 'DESC';
- const SORT_ASC = 'ASC';
+class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterface {
/**
* @var LoadBalancer
@@ -59,6 +52,11 @@ class WatchedItemStore implements StatsdAwareInterface {
private $revisionGetTimestampFromIdCallback;
/**
+ * @var int
+ */
+ private $updateRowsPerQuery;
+
+ /**
* @var StatsdDataFactoryInterface
*/
private $stats;
@@ -67,20 +65,28 @@ class WatchedItemStore implements StatsdAwareInterface {
* @param LoadBalancer $loadBalancer
* @param HashBagOStuff $cache
* @param ReadOnlyMode $readOnlyMode
+ * @param int $updateRowsPerQuery
*/
public function __construct(
LoadBalancer $loadBalancer,
HashBagOStuff $cache,
- ReadOnlyMode $readOnlyMode
+ ReadOnlyMode $readOnlyMode,
+ $updateRowsPerQuery
) {
$this->loadBalancer = $loadBalancer;
$this->cache = $cache;
$this->readOnlyMode = $readOnlyMode;
$this->stats = new NullStatsdDataFactory();
- $this->deferredUpdatesAddCallableUpdateCallback = [ 'DeferredUpdates', 'addCallableUpdate' ];
- $this->revisionGetTimestampFromIdCallback = [ 'Revision', 'getTimestampFromId' ];
+ $this->deferredUpdatesAddCallableUpdateCallback =
+ [ DeferredUpdates::class, 'addCallableUpdate' ];
+ $this->revisionGetTimestampFromIdCallback =
+ [ Revision::class, 'getTimestampFromId' ];
+ $this->updateRowsPerQuery = $updateRowsPerQuery;
}
+ /**
+ * @param StatsdDataFactoryInterface $stats
+ */
public function setStatsdDataFactory( StatsdDataFactoryInterface $stats ) {
$this->stats = $stats;
}
@@ -216,11 +222,85 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
- * Count the number of individual items that are watched by the user.
- * If a subject and corresponding talk page are watched this will return 2.
+ * Deletes ALL watched items for the given user when under
+ * $updateRowsPerQuery entries exist.
+ *
+ * @since 1.30
*
* @param User $user
*
+ * @return bool true on success, false when too many items are watched
+ */
+ public function clearUserWatchedItems( User $user ) {
+ if ( $this->countWatchedItems( $user ) > $this->updateRowsPerQuery ) {
+ return false;
+ }
+
+ $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
+ $dbw->delete(
+ 'watchlist',
+ [ 'wl_user' => $user->getId() ],
+ __METHOD__
+ );
+ $this->uncacheAllItemsForUser( $user );
+
+ return true;
+ }
+
+ private function uncacheAllItemsForUser( User $user ) {
+ $userId = $user->getId();
+ foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
+ foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
+ if ( array_key_exists( $userId, $userIndex ) ) {
+ $this->cache->delete( $userIndex[$userId] );
+ unset( $this->cacheIndex[$ns][$dbKey][$userId] );
+ }
+ }
+ }
+
+ // Cleanup empty cache keys
+ foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
+ foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
+ if ( empty( $this->cacheIndex[$ns][$dbKey] ) ) {
+ unset( $this->cacheIndex[$ns][$dbKey] );
+ }
+ }
+ if ( empty( $this->cacheIndex[$ns] ) ) {
+ unset( $this->cacheIndex[$ns] );
+ }
+ }
+ }
+
+ /**
+ * Queues a job that will clear the users watchlist using the Job Queue.
+ *
+ * @since 1.31
+ *
+ * @param User $user
+ */
+ public function clearUserWatchedItemsUsingJobQueue( User $user ) {
+ $job = ClearUserWatchlistJob::newForUser( $user, $this->getMaxId() );
+ // TODO inject me.
+ JobQueueGroup::singleton()->push( $job );
+ }
+
+ /**
+ * @since 1.31
+ * @return int The maximum current wl_id
+ */
+ public function getMaxId() {
+ $dbr = $this->getConnectionRef( DB_REPLICA );
+ return (int)$dbr->selectField(
+ 'watchlist',
+ 'MAX(wl_id)',
+ '',
+ __METHOD__
+ );
+ }
+
+ /**
+ * @since 1.31
+ * @param User $user
* @return int
*/
public function countWatchedItems( User $user ) {
@@ -238,8 +318,8 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
+ * @since 1.27
* @param LinkTarget $target
- *
* @return int
*/
public function countWatchers( LinkTarget $target ) {
@@ -258,14 +338,10 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
- * Number of page watchers who also visited a "recent" edit
- *
+ * @since 1.27
* @param LinkTarget $target
- * @param mixed $threshold timestamp accepted by wfTimestamp
- *
+ * @param string|int $threshold
* @return int
- * @throws DBUnexpectedError
- * @throws MWException
*/
public function countVisitingWatchers( LinkTarget $target, $threshold ) {
$dbr = $this->getConnectionRef( DB_REPLICA );
@@ -286,13 +362,10 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
+ * @since 1.27
* @param LinkTarget[] $targets
- * @param array $options Allowed keys:
- * 'minimumWatchers' => int
- *
- * @return array multi dimensional like $return[$namespaceId][$titleString] = int $watchers
- * All targets will be present in the result. 0 either means no watchers or the number
- * of watchers was below the minimumWatchers option if passed.
+ * @param array $options
+ * @return array
*/
public function countWatchersMultiple( array $targets, array $options = [] ) {
$dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
@@ -325,24 +398,20 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
- * Number of watchers of each page who have visited recent edits to that page
- *
- * @param array $targetsWithVisitThresholds array of pairs (LinkTarget $target, mixed $threshold),
- * $threshold is:
- * - a timestamp of the recent edit if $target exists (format accepted by wfTimestamp)
- * - null if $target doesn't exist
+ * @since 1.27
+ * @param array $targetsWithVisitThresholds
* @param int|null $minimumWatchers
- * @return array multi-dimensional like $return[$namespaceId][$titleString] = $watchers,
- * where $watchers is an int:
- * - if the page exists, number of users watching who have visited the page recently
- * - if the page doesn't exist, number of users that have the page on their watchlist
- * - 0 means there are no visiting watchers or their number is below the minimumWatchers
- * option (if passed).
+ * @return array
*/
public function countVisitingWatchersMultiple(
array $targetsWithVisitThresholds,
$minimumWatchers = null
) {
+ if ( $targetsWithVisitThresholds === [] ) {
+ // No titles requested => no results returned
+ return [];
+ }
+
$dbr = $this->getConnectionRef( DB_REPLICA );
$conds = $this->getVisitingWatchersCondition( $dbr, $targetsWithVisitThresholds );
@@ -417,12 +486,10 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
- * Get an item (may be cached)
- *
+ * @since 1.27
* @param User $user
* @param LinkTarget $target
- *
- * @return WatchedItem|false
+ * @return bool
*/
public function getWatchedItem( User $user, LinkTarget $target ) {
if ( $user->isAnon() ) {
@@ -439,12 +506,10 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
- * Loads an item from the db
- *
+ * @since 1.27
* @param User $user
* @param LinkTarget $target
- *
- * @return WatchedItem|false
+ * @return WatchedItem|bool
*/
public function loadWatchedItem( User $user, LinkTarget $target ) {
// Only loggedin user can have a watchlist
@@ -475,12 +540,9 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
+ * @since 1.27
* @param User $user
- * @param array $options Allowed keys:
- * 'forWrite' => bool defaults to false
- * 'sort' => string optional sorting by namespace ID and title
- * one of the self::SORT_* constants
- *
+ * @param array $options
* @return WatchedItem[]
*/
public function getWatchedItemsForUser( User $user, array $options = [] ) {
@@ -522,11 +584,9 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
- * Must be called separately for Subject & Talk namespaces
- *
+ * @since 1.27
* @param User $user
* @param LinkTarget $target
- *
* @return bool
*/
public function isWatched( User $user, LinkTarget $target ) {
@@ -534,13 +594,10 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
+ * @since 1.27
* @param User $user
* @param LinkTarget[] $targets
- *
- * @return array multi-dimensional like $return[$namespaceId][$titleString] = $timestamp,
- * where $timestamp is:
- * - string|null value of wl_notificationtimestamp,
- * - false if $target is not watched by $user.
+ * @return array
*/
public function getNotificationTimestampsBatch( User $user, array $targets ) {
$timestamps = [];
@@ -589,8 +646,7 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
- * Must be called separately for Subject & Talk namespaces
- *
+ * @since 1.27
* @param User $user
* @param LinkTarget $target
*/
@@ -599,10 +655,10 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
+ * @since 1.27
* @param User $user
* @param LinkTarget[] $targets
- *
- * @return bool success
+ * @return bool
*/
public function addWatchBatchForUser( User $user, array $targets ) {
if ( $this->readOnlyMode->isReadOnly() ) {
@@ -642,7 +698,7 @@ class WatchedItemStore implements StatsdAwareInterface {
}
// Update process cache to ensure skin doesn't claim that the current
// page is unwatched in the response of action=watch itself (T28292).
- // This would otherwise be re-queried from a slave by isWatched().
+ // This would otherwise be re-queried from a replica by isWatched().
foreach ( $items as $item ) {
$this->cache( $item );
}
@@ -651,15 +707,10 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
- * Removes the an entry for the User watching the LinkTarget
- * Must be called separately for Subject & Talk namespaces
- *
+ * @since 1.27
* @param User $user
* @param LinkTarget $target
- *
- * @return bool success
- * @throws DBUnexpectedError
- * @throws MWException
+ * @return bool
*/
public function removeWatch( User $user, LinkTarget $target ) {
// Only logged in user can have a watchlist
@@ -683,11 +734,11 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
- * @param User $user The user to set the timestamp for
- * @param string|null $timestamp Set the update timestamp to this value
- * @param LinkTarget[] $targets List of targets to update. Default to all targets
- *
- * @return bool success
+ * @since 1.27
+ * @param User $user
+ * @param string|int $timestamp
+ * @param LinkTarget[] $targets
+ * @return bool
*/
public function setNotificationTimestampsForUser( User $user, $timestamp, array $targets = [] ) {
// Only loggedin user can have a watchlist
@@ -719,13 +770,34 @@ class WatchedItemStore implements StatsdAwareInterface {
return $success;
}
+ public function resetAllNotificationTimestampsForUser( User $user ) {
+ // Only loggedin user can have a watchlist
+ if ( $user->isAnon() ) {
+ return;
+ }
+
+ // If the page is watched by the user (or may be watched), update the timestamp
+ $job = new ClearWatchlistNotificationsJob(
+ $user->getUserPage(),
+ [ 'userId' => $user->getId(), 'casTime' => time() ]
+ );
+
+ // Try to run this post-send
+ // Calls DeferredUpdates::addCallableUpdate in normal operation
+ call_user_func(
+ $this->deferredUpdatesAddCallableUpdateCallback,
+ function () use ( $job ) {
+ $job->run();
+ }
+ );
+ }
+
/**
- * @param User $editor The editor that triggered the update. Their notification
- * timestamp will not be updated(they have already seen it)
- * @param LinkTarget $target The target to update timestamps for
- * @param string $timestamp Set the update timestamp to this value
- *
- * @return int[] Array of user IDs the timestamp has been updated for
+ * @since 1.27
+ * @param User $editor
+ * @param LinkTarget $target
+ * @param string|int $timestamp
+ * @return int[]
*/
public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
$dbw = $this->getConnectionRef( DB_MASTER );
@@ -781,16 +853,12 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
- * Reset the notification timestamp of this entry
- *
+ * @since 1.27
* @param User $user
* @param Title $title
- * @param string $force Whether to force the write query to be executed even if the
- * page is not watched or the notification timestamp is already NULL.
- * 'force' in order to force
- * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
- *
- * @return bool success
+ * @param string $force
+ * @param int $oldid
+ * @return bool
*/
public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 ) {
// Only loggedin user can have a watchlist
@@ -879,11 +947,10 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
+ * @since 1.27
* @param User $user
- * @param int $unreadLimit
- *
- * @return int|bool The number of unread notifications
- * true if greater than or equal to $unreadLimit
+ * @param int|null $unreadLimit
+ * @return int|bool
*/
public function countUnreadNotifications( User $user, $unreadLimit = null ) {
$queryOptions = [];
@@ -916,11 +983,7 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
- * Check if the given title already is watched by the user, and if so
- * add a watch for the new title.
- *
- * To be used for page renames and such.
- *
+ * @since 1.27
* @param LinkTarget $oldTarget
* @param LinkTarget $newTarget
*/
@@ -933,12 +996,7 @@ class WatchedItemStore implements StatsdAwareInterface {
}
/**
- * Check if the given title already is watched by the user, and if so
- * add a watch for the new title.
- *
- * To be used for page renames and such.
- * This must be called separately for Subject and Talk pages
- *
+ * @since 1.27
* @param LinkTarget $oldTarget
* @param LinkTarget $newTarget
*/
diff --git a/www/wiki/includes/watcheditem/WatchedItemStoreInterface.php b/www/wiki/includes/watcheditem/WatchedItemStoreInterface.php
new file mode 100644
index 00000000..a450ae53
--- /dev/null
+++ b/www/wiki/includes/watcheditem/WatchedItemStoreInterface.php
@@ -0,0 +1,318 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Watchlist
+ */
+use MediaWiki\Linker\LinkTarget;
+
+/**
+ * @author Addshore
+ * @since 1.31 interface created. WatchedItemStore implementation available since 1.27
+ */
+interface WatchedItemStoreInterface {
+
+ /**
+ * @since 1.31
+ */
+ const SORT_ASC = 'ASC';
+
+ /**
+ * @since 1.31
+ */
+ const SORT_DESC = 'DESC';
+
+ /**
+ * Count the number of individual items that are watched by the user.
+ * If a subject and corresponding talk page are watched this will return 2.
+ *
+ * @since 1.31
+ *
+ * @param User $user
+ *
+ * @return int
+ */
+ public function countWatchedItems( User $user );
+
+ /**
+ * @since 1.31
+ *
+ * @param LinkTarget $target
+ *
+ * @return int
+ */
+ public function countWatchers( LinkTarget $target );
+
+ /**
+ * Number of page watchers who also visited a "recent" edit
+ *
+ * @since 1.31
+ *
+ * @param LinkTarget $target
+ * @param mixed $threshold timestamp accepted by wfTimestamp
+ *
+ * @return int
+ * @throws DBUnexpectedError
+ * @throws MWException
+ */
+ public function countVisitingWatchers( LinkTarget $target, $threshold );
+
+ /**
+ * @since 1.31
+ *
+ * @param LinkTarget[] $targets
+ * @param array $options Allowed keys:
+ * 'minimumWatchers' => int
+ *
+ * @return array multi dimensional like $return[$namespaceId][$titleString] = int $watchers
+ * All targets will be present in the result. 0 either means no watchers or the number
+ * of watchers was below the minimumWatchers option if passed.
+ */
+ public function countWatchersMultiple( array $targets, array $options = [] );
+
+ /**
+ * Number of watchers of each page who have visited recent edits to that page
+ *
+ * @since 1.31
+ *
+ * @param array $targetsWithVisitThresholds array of pairs (LinkTarget $target, mixed
+ * $threshold),
+ * $threshold is:
+ * - a timestamp of the recent edit if $target exists (format accepted by wfTimestamp)
+ * - null if $target doesn't exist
+ * @param int|null $minimumWatchers
+ *
+ * @return array multi-dimensional like $return[$namespaceId][$titleString] = $watchers,
+ * where $watchers is an int:
+ * - if the page exists, number of users watching who have visited the page recently
+ * - if the page doesn't exist, number of users that have the page on their watchlist
+ * - 0 means there are no visiting watchers or their number is below the
+ * minimumWatchers
+ * option (if passed).
+ */
+ public function countVisitingWatchersMultiple(
+ array $targetsWithVisitThresholds,
+ $minimumWatchers = null
+ );
+
+ /**
+ * Get an item (may be cached)
+ *
+ * @since 1.31
+ *
+ * @param User $user
+ * @param LinkTarget $target
+ *
+ * @return WatchedItem|false
+ */
+ public function getWatchedItem( User $user, LinkTarget $target );
+
+ /**
+ * Loads an item from the db
+ *
+ * @since 1.31
+ *
+ * @param User $user
+ * @param LinkTarget $target
+ *
+ * @return WatchedItem|false
+ */
+ public function loadWatchedItem( User $user, LinkTarget $target );
+
+ /**
+ * @since 1.31
+ *
+ * @param User $user
+ * @param array $options Allowed keys:
+ * 'forWrite' => bool defaults to false
+ * 'sort' => string optional sorting by namespace ID and title
+ * one of the self::SORT_* constants
+ *
+ * @return WatchedItem[]
+ */
+ public function getWatchedItemsForUser( User $user, array $options = [] );
+
+ /**
+ * Must be called separately for Subject & Talk namespaces
+ *
+ * @since 1.31
+ *
+ * @param User $user
+ * @param LinkTarget $target
+ *
+ * @return bool
+ */
+ public function isWatched( User $user, LinkTarget $target );
+
+ /**
+ * @since 1.31
+ *
+ * @param User $user
+ * @param LinkTarget[] $targets
+ *
+ * @return array multi-dimensional like $return[$namespaceId][$titleString] = $timestamp,
+ * where $timestamp is:
+ * - string|null value of wl_notificationtimestamp,
+ * - false if $target is not watched by $user.
+ */
+ public function getNotificationTimestampsBatch( User $user, array $targets );
+
+ /**
+ * Must be called separately for Subject & Talk namespaces
+ *
+ * @since 1.31
+ *
+ * @param User $user
+ * @param LinkTarget $target
+ */
+ public function addWatch( User $user, LinkTarget $target );
+
+ /**
+ * @since 1.31
+ *
+ * @param User $user
+ * @param LinkTarget[] $targets
+ *
+ * @return bool success
+ */
+ public function addWatchBatchForUser( User $user, array $targets );
+
+ /**
+ * Removes the an entry for the User watching the LinkTarget
+ * Must be called separately for Subject & Talk namespaces
+ *
+ * @since 1.31
+ *
+ * @param User $user
+ * @param LinkTarget $target
+ *
+ * @return bool success
+ * @throws DBUnexpectedError
+ * @throws MWException
+ */
+ public function removeWatch( User $user, LinkTarget $target );
+
+ /**
+ * @since 1.31
+ *
+ * @param User $user The user to set the timestamps for
+ * @param string|null $timestamp Set the update timestamp to this value
+ * @param LinkTarget[] $targets List of targets to update. Default to all targets
+ *
+ * @return bool success
+ */
+ public function setNotificationTimestampsForUser(
+ User $user,
+ $timestamp,
+ array $targets = []
+ );
+
+ /**
+ * Reset all watchlist notificaton timestamps for a user using the job queue
+ *
+ * @since 1.31
+ *
+ * @param User $user The user to reset the timestamps for
+ */
+ public function resetAllNotificationTimestampsForUser( User $user );
+
+ /**
+ * @since 1.31
+ *
+ * @param User $editor The editor that triggered the update. Their notification
+ * timestamp will not be updated(they have already seen it)
+ * @param LinkTarget $target The target to update timestamps for
+ * @param string $timestamp Set the update timestamp to this value
+ *
+ * @return int[] Array of user IDs the timestamp has been updated for
+ */
+ public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp );
+
+ /**
+ * Reset the notification timestamp of this entry
+ *
+ * @since 1.31
+ *
+ * @param User $user
+ * @param Title $title
+ * @param string $force Whether to force the write query to be executed even if the
+ * page is not watched or the notification timestamp is already NULL.
+ * 'force' in order to force
+ * @param int $oldid The revision id being viewed. If not given or 0, latest revision is
+ * assumed.
+ *
+ * @return bool success Whether a job was enqueued
+ */
+ public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 );
+
+ /**
+ * @since 1.31
+ *
+ * @param User $user
+ * @param int $unreadLimit
+ *
+ * @return int|bool The number of unread notifications
+ * true if greater than or equal to $unreadLimit
+ */
+ public function countUnreadNotifications( User $user, $unreadLimit = null );
+
+ /**
+ * Check if the given title already is watched by the user, and if so
+ * add a watch for the new title.
+ *
+ * To be used for page renames and such.
+ *
+ * @since 1.31
+ *
+ * @param LinkTarget $oldTarget
+ * @param LinkTarget $newTarget
+ */
+ public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget );
+
+ /**
+ * Check if the given title already is watched by the user, and if so
+ * add a watch for the new title.
+ *
+ * To be used for page renames and such.
+ * This must be called separately for Subject and Talk pages
+ *
+ * @since 1.31
+ *
+ * @param LinkTarget $oldTarget
+ * @param LinkTarget $newTarget
+ */
+ public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget );
+
+ /**
+ * Queues a job that will clear the users watchlist using the Job Queue.
+ *
+ * @since 1.31
+ *
+ * @param User $user
+ */
+ public function clearUserWatchedItems( User $user );
+
+ /**
+ * Queues a job that will clear the users watchlist using the Job Queue.
+ *
+ * @since 1.31
+ *
+ * @param User $user
+ */
+ public function clearUserWatchedItemsUsingJobQueue( User $user );
+
+}
diff --git a/www/wiki/includes/widget/ComplexNamespaceInputWidget.php b/www/wiki/includes/widget/ComplexNamespaceInputWidget.php
index d3ada03b..5f5d1cd1 100644
--- a/www/wiki/includes/widget/ComplexNamespaceInputWidget.php
+++ b/www/wiki/includes/widget/ComplexNamespaceInputWidget.php
@@ -1,15 +1,13 @@
<?php
-/**
- * MediaWiki Widgets – ComplexNamespaceInputWidget class.
- *
- * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
- * @license The MIT License (MIT); see LICENSE.txt
- */
+
namespace MediaWiki\Widget;
/**
* Namespace input widget. Displays a dropdown box with the choice of available namespaces, plus two
* checkboxes to include associated namespace or to invert selection.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
*/
class ComplexNamespaceInputWidget extends \OOUI\Widget {
@@ -22,20 +20,20 @@ class ComplexNamespaceInputWidget extends \OOUI\Widget {
/**
* @param array $config Configuration options
- * @param array $config['namespace'] Configuration for the NamespaceInputWidget
- * dropdown with list of namespaces
- * @param string $config['namespace']['includeAllValue'] If specified,
- * add an "all namespaces" option to the dropdown, and use this as the input value for it
- * @param array|null $config['invert'] Configuration for the "invert selection"
- * CheckboxInputWidget. If null, the checkbox will not be generated.
- * @param array|null $config['associated'] Configuration for the "include associated namespace"
- * CheckboxInputWidget. If null, the checkbox will not be generated.
- * @param array $config['invertLabel'] Configuration for the FieldLayout with label
- * wrapping the "invert selection" checkbox
- * @param string $config['invertLabel']['label'] Label text for the label
- * @param array $config['associatedLabel'] Configuration for the FieldLayout with label
- * wrapping the "include associated namespace" checkbox
- * @param string $config['associatedLabel']['label'] Label text for the label
+ * - array $config['namespace'] Configuration for the NamespaceInputWidget
+ * dropdown with list of namespaces
+ * - string $config['namespace']['includeAllValue'] If specified,
+ * add an "all namespaces" option to the dropdown, and use this as the input value for it
+ * - array|null $config['invert'] Configuration for the "invert selection"
+ * CheckboxInputWidget. If null, the checkbox will not be generated.
+ * - array|null $config['associated'] Configuration for the "include associated namespace"
+ * CheckboxInputWidget. If null, the checkbox will not be generated.
+ * - array $config['invertLabel'] Configuration for the FieldLayout with label
+ * wrapping the "invert selection" checkbox
+ * - string $config['invertLabel']['label'] Label text for the label
+ * - array $config['associatedLabel'] Configuration for the FieldLayout with label
+ * wrapping the "include associated namespace" checkbox
+ * - string $config['associatedLabel']['label'] Label text for the label
*/
public function __construct( array $config = [] ) {
// Configuration initialization
@@ -51,7 +49,6 @@ class ComplexNamespaceInputWidget extends \OOUI\Widget {
$config
);
- // Parent constructor
parent::__construct( $config );
// Properties
@@ -114,6 +111,7 @@ class ComplexNamespaceInputWidget extends \OOUI\Widget {
)
)
);
+ $config['namespace']['dropdown']['$overlay'] = true;
return parent::getConfig( $config );
}
}
diff --git a/www/wiki/includes/widget/ComplexTitleInputWidget.php b/www/wiki/includes/widget/ComplexTitleInputWidget.php
index a9e80425..ca6c8484 100644
--- a/www/wiki/includes/widget/ComplexTitleInputWidget.php
+++ b/www/wiki/includes/widget/ComplexTitleInputWidget.php
@@ -1,14 +1,12 @@
<?php
-/**
- * MediaWiki Widgets – ComplexTitleInputWidget class.
- *
- * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
- * @license The MIT License (MIT); see LICENSE.txt
- */
+
namespace MediaWiki\Widget;
/**
* Complex title input widget.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
*/
class ComplexTitleInputWidget extends \OOUI\Widget {
@@ -19,9 +17,9 @@ class ComplexTitleInputWidget extends \OOUI\Widget {
* Like TitleInputWidget, but the namespace has to be input through a separate dropdown field.
*
* @param array $config Configuration options
- * @param array $config['namespace'] Configuration for the NamespaceInputWidget dropdown
- * with list of namespaces
- * @param array $config['title'] Configuration for the TitleInputWidget text field
+ * - array $config['namespace'] Configuration for the NamespaceInputWidget dropdown
+ * with list of namespaces
+ * - array $config['title'] Configuration for the TitleInputWidget text field
*/
public function __construct( array $config = [] ) {
// Configuration initialization
@@ -33,7 +31,6 @@ class ComplexTitleInputWidget extends \OOUI\Widget {
$config
);
- // Parent constructor
parent::__construct( $config );
// Properties
@@ -61,7 +58,9 @@ class ComplexTitleInputWidget extends \OOUI\Widget {
public function getConfig( &$config ) {
$config['namespace'] = $this->config['namespace'];
+ $config['namespace']['dropdown']['$overlay'] = true;
$config['title'] = $this->config['title'];
+ $config['title']['$overlay'] = true;
return parent::getConfig( $config );
}
}
diff --git a/www/wiki/includes/widget/DateInputWidget.php b/www/wiki/includes/widget/DateInputWidget.php
index 507dab6f..975f8e9c 100644
--- a/www/wiki/includes/widget/DateInputWidget.php
+++ b/www/wiki/includes/widget/DateInputWidget.php
@@ -1,10 +1,4 @@
<?php
-/**
- * MediaWiki Widgets – DateInputWidget class.
- *
- * @copyright 2016 MediaWiki Widgets Team and others; see AUTHORS.txt
- * @license The MIT License (MIT); see LICENSE.txt
- */
namespace MediaWiki\Widget;
@@ -14,6 +8,8 @@ use DateTime;
* Date input widget.
*
* @since 1.29
+ * @copyright 2016 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
*/
class DateInputWidget extends \OOUI\TextInputWidget {
@@ -25,38 +21,32 @@ class DateInputWidget extends \OOUI\TextInputWidget {
protected $precision = null;
protected $mustBeAfter = null;
protected $mustBeBefore = null;
- protected $overlay = null;
/**
* @param array $config Configuration options
- * @param string $config['inputFormat'] Date format string to use for the textual input field.
- * Displayed while the widget is active, and the user can type in a date in this format.
- * Should be short and easy to type. (default: 'YYYY-MM-DD' or 'YYYY-MM', depending on
- * `precision`)
- * @param string $config['displayFormat'] Date format string to use for the clickable label.
- * while the widget is inactive. Should be as unambiguous as possible (for example, prefer
- * to spell out the month, rather than rely on the order), even if that makes it longer.
- * Applicable only if the widget is infused. (default: language-specific)
- * @param string $config['longDisplayFormat'] If a custom displayFormat is not specified, use
- * unabbreviated day of the week and month names in the default language-specific
- * displayFormat. (default: false)
- * @param string $config['placeholderLabel'] Placeholder text shown when the widget is not
- * selected. Applicable only if the widget is infused. (default: taken from message
- * `mw-widgets-dateinput-no-date`)
- * @param string $config['placeholderDateFormat'] User-visible date format string displayed
- * in the textual input field when it's empty. Should be the same as `inputFormat`, but
- * translated to the user's language. (default: 'YYYY-MM-DD' or 'YYYY-MM', depending on
- * `precision`)
- * @param string $config['precision'] Date precision to use, 'day' or 'month' (default: 'day')
- * @param string $config['mustBeAfter'] Validates the date to be after this.
- * In the 'YYYY-MM-DD' or 'YYYY-MM' format, depending on `precision`.
- * @param string $config['mustBeBefore'] Validates the date to be before this.
- * In the 'YYYY-MM-DD' or 'YYYY-MM' format, depending on `precision`.
- * @param string $config['overlay'] The jQuery selector for the overlay layer on which to render
- * the calendar. This configuration is useful in cases where the expanded calendar is larger
- * than its container. The specified overlay layer is usually on top of the container and has
- * a larger area. Applicable only if the widget is infused. By default, the calendar uses
- * relative positioning.
+ * - string $config['inputFormat'] Date format string to use for the textual input field.
+ * Displayed while the widget is active, and the user can type in a date in this format.
+ * Should be short and easy to type. (default: 'YYYY-MM-DD' or 'YYYY-MM', depending on
+ * `precision`)
+ * - string $config['displayFormat'] Date format string to use for the clickable label.
+ * while the widget is inactive. Should be as unambiguous as possible (for example, prefer
+ * to spell out the month, rather than rely on the order), even if that makes it longer.
+ * Applicable only if the widget is infused. (default: language-specific)
+ * - string $config['longDisplayFormat'] If a custom displayFormat is not specified, use
+ * unabbreviated day of the week and month names in the default language-specific
+ * displayFormat. (default: false)
+ * - string $config['placeholderLabel'] Placeholder text shown when the widget is not
+ * selected. Applicable only if the widget is infused. (default: taken from message
+ * `mw-widgets-dateinput-no-date`)
+ * - string $config['placeholderDateFormat'] User-visible date format string displayed
+ * in the textual input field when it's empty. Should be the same as `inputFormat`, but
+ * translated to the user's language. (default: 'YYYY-MM-DD' or 'YYYY-MM', depending on
+ * `precision`)
+ * - string $config['precision'] Date precision to use, 'day' or 'month' (default: 'day')
+ * - string $config['mustBeAfter'] Validates the date to be after this.
+ * In the 'YYYY-MM-DD' or 'YYYY-MM' format, depending on `precision`.
+ * - string $config['mustBeBefore'] Validates the date to be before this.
+ * In the 'YYYY-MM-DD' or 'YYYY-MM' format, depending on `precision`.
*/
public function __construct( array $config = [] ) {
$config = array_merge( [
@@ -90,9 +80,6 @@ class DateInputWidget extends \OOUI\TextInputWidget {
if ( isset( $config['placeholderLabel'] ) ) {
$this->placeholderLabel = $config['placeholderLabel'];
}
- if ( isset( $config['overlay'] ) ) {
- $this->overlay = $config['overlay'];
- }
// Set up placeholder text visible if the browser doesn't override it (logic taken from JS)
if ( $this->placeholderDateFormat !== null ) {
@@ -109,7 +96,6 @@ class DateInputWidget extends \OOUI\TextInputWidget {
'placeholder' => $placeholder,
], $config );
- // Parent constructor
parent::__construct( $config );
// Calculate min/max attributes (which are skipped by TextInputWidget) and add to <input>
@@ -160,9 +146,7 @@ class DateInputWidget extends \OOUI\TextInputWidget {
if ( $this->mustBeBefore !== null ) {
$config['mustBeBefore'] = $this->mustBeBefore;
}
- if ( $this->overlay !== null ) {
- $config['overlay'] = $this->overlay;
- }
+ $config['$overlay'] = true;
return parent::getConfig( $config );
}
diff --git a/www/wiki/includes/widget/DateTimeInputWidget.php b/www/wiki/includes/widget/DateTimeInputWidget.php
index f0d5cdb6..21e3d793 100644
--- a/www/wiki/includes/widget/DateTimeInputWidget.php
+++ b/www/wiki/includes/widget/DateTimeInputWidget.php
@@ -1,16 +1,14 @@
<?php
-/**
- * MediaWiki Widgets – DateTimeInputWidget class.
- *
- * @copyright 2016 MediaWiki Widgets Team and others; see AUTHORS.txt
- * @license The MIT License (MIT); see LICENSE.txt
- */
+
namespace MediaWiki\Widget;
use OOUI\Tag;
/**
* Date-time input widget.
+ *
+ * @copyright 2016 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
*/
class DateTimeInputWidget extends \OOUI\InputWidget {
@@ -21,10 +19,10 @@ class DateTimeInputWidget extends \OOUI\InputWidget {
/**
* @param array $config Configuration options
- * @param string $config['type'] 'date', 'time', or 'datetime'
- * @param string $config['min'] Minimum date, time, or datetime
- * @param string $config['max'] Maximum date, time, or datetime
- * @param bool $config['clearable'] Whether to provide for blanking the value.
+ * - string $config['type'] 'date', 'time', or 'datetime'
+ * - string $config['min'] Minimum date, time, or datetime
+ * - string $config['max'] Maximum date, time, or datetime
+ * - bool $config['clearable'] Whether to provide for blanking the value.
*/
public function __construct( array $config = [] ) {
// We need $this->type set before calling the parent constructor
@@ -34,7 +32,6 @@ class DateTimeInputWidget extends \OOUI\InputWidget {
throw new \InvalidArgumentException( '$config[\'type\'] must be specified' );
}
- // Parent constructor
parent::__construct( $config );
// Properties, which are ignored in PHP and just shipped back to JS
diff --git a/www/wiki/includes/widget/NamespaceInputWidget.php b/www/wiki/includes/widget/NamespaceInputWidget.php
index 3e86738b..0840886a 100644
--- a/www/wiki/includes/widget/NamespaceInputWidget.php
+++ b/www/wiki/includes/widget/NamespaceInputWidget.php
@@ -1,14 +1,12 @@
<?php
-/**
- * MediaWiki Widgets – NamespaceInputWidget class.
- *
- * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
- * @license The MIT License (MIT); see LICENSE.txt
- */
+
namespace MediaWiki\Widget;
/**
* Namespace input widget. Displays a dropdown box with the choice of available namespaces.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
*/
class NamespaceInputWidget extends \OOUI\DropdownInputWidget {
@@ -16,15 +14,14 @@ class NamespaceInputWidget extends \OOUI\DropdownInputWidget {
/**
* @param array $config Configuration options
- * @param string $config['includeAllValue'] If specified, add a "all namespaces" option to the
+ * - string $config['includeAllValue'] If specified, add a "all namespaces" option to the
* namespace dropdown, and use this as the input value for it
- * @param number[] $config['exclude'] List of namespace numbers to exclude from the selector
+ * - int[] $config['exclude'] List of namespace numbers to exclude from the selector
*/
public function __construct( array $config = [] ) {
// Configuration initialization
$config['options'] = $this->getNamespaceDropdownOptions( $config );
- // Parent constructor
parent::__construct( $config );
// Properties
@@ -61,6 +58,7 @@ class NamespaceInputWidget extends \OOUI\DropdownInputWidget {
$config['includeAllValue'] = $this->includeAllValue;
$config['exclude'] = $this->exclude;
// Skip DropdownInputWidget's getConfig(), we don't need 'options' config
+ $config['dropdown']['$overlay'] = true;
return \OOUI\InputWidget::getConfig( $config );
}
}
diff --git a/www/wiki/includes/widget/SearchInputWidget.php b/www/wiki/includes/widget/SearchInputWidget.php
index 773c291d..6fed7942 100644
--- a/www/wiki/includes/widget/SearchInputWidget.php
+++ b/www/wiki/includes/widget/SearchInputWidget.php
@@ -1,14 +1,12 @@
<?php
-/**
- * MediaWiki Widgets – SearchInputWidget class.
- *
- * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
- * @license The MIT License (MIT); see LICENSE.txt
- */
+
namespace MediaWiki\Widget;
/**
* Search input widget.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
*/
class SearchInputWidget extends TitleInputWidget {
@@ -20,13 +18,13 @@ class SearchInputWidget extends TitleInputWidget {
/**
* @param array $config Configuration options
- * @param int|null $config['pushPending'] Whether the input should be visually marked as
- * "pending", while requesting suggestions (default: false)
- * @param bool|null $config['performSearchOnClick'] If true, the script will start a search
- * whenever a user hits a suggestion. If false, the text of the suggestion is inserted into the
- * text field only (default: true)
- * @param string $config['dataLocation'] Where the search input field will be
- * used (header or content, default: header)
+ * - int|null $config['pushPending'] Whether the input should be visually marked as
+ * "pending", while requesting suggestions (default: false)
+ * - bool|null $config['performSearchOnClick'] If true, the script will start a search
+ * whenever a user hits a suggestion. If false, the text of the suggestion is inserted into
+ * the text field only (default: true)
+ * - string $config['dataLocation'] Where the search input field will be
+ * used (header or content, default: header)
*/
public function __construct( array $config = [] ) {
$config = array_merge( [
@@ -34,7 +32,6 @@ class SearchInputWidget extends TitleInputWidget {
'icon' => 'search',
], $config );
- // Parent constructor
parent::__construct( $config );
// Properties, which are ignored in PHP and just shipped back to JS
@@ -69,6 +66,7 @@ class SearchInputWidget extends TitleInputWidget {
if ( $this->dataLocation ) {
$config['dataLocation'] = $this->dataLocation;
}
+ $config['$overlay'] = true;
return parent::getConfig( $config );
}
}
diff --git a/www/wiki/includes/widget/SelectWithInputWidget.php b/www/wiki/includes/widget/SelectWithInputWidget.php
index d2dda75e..5ceed4c4 100644
--- a/www/wiki/includes/widget/SelectWithInputWidget.php
+++ b/www/wiki/includes/widget/SelectWithInputWidget.php
@@ -1,17 +1,15 @@
<?php
-/**
- * MediaWiki Widgets – SelectWithInputWidget class.
- *
- * @copyright 2011-2017 MediaWiki Widgets Team and others; see AUTHORS.txt
- * @license The MIT License (MIT); see LICENSE.txt
- */
+
namespace MediaWiki\Widget;
-use \OOUI\TextInputWidget;
-use \OOUI\DropdownInputWidget;
+use OOUI\DropdownInputWidget;
+use OOUI\TextInputWidget;
/**
* Select and input widget.
+ *
+ * @copyright 2011-2017 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
*/
class SelectWithInputWidget extends \OOUI\Widget {
@@ -22,9 +20,9 @@ class SelectWithInputWidget extends \OOUI\Widget {
* A version of the SelectWithInputWidget, with `or` set to true.
*
* @param array $config Configuration options
- * @param array $config['textinput'] Configuration for the TextInputWidget
- * @param array $config['dropdowninput'] Configuration for the DropdownInputWidget
- * @param bool $config['or'] Configuration for whether the widget is dropdown AND input
+ * - array $config['textinput'] Configuration for the TextInputWidget
+ * - array $config['dropdowninput'] Configuration for the DropdownInputWidget
+ * - bool $config['or'] Configuration for whether the widget is dropdown AND input
* or dropdown OR input
*/
public function __construct( array $config = [] ) {
@@ -38,7 +36,6 @@ class SelectWithInputWidget extends \OOUI\Widget {
$config
);
- // Parent constructor
parent::__construct( $config );
// Properties
@@ -59,6 +56,7 @@ class SelectWithInputWidget extends \OOUI\Widget {
public function getConfig( &$config ) {
$config['textinput'] = $this->config['textinput'];
$config['dropdowninput'] = $this->config['dropdowninput'];
+ $config['dropdowninput']['dropdown']['$overlay'] = true;
$config['or'] = $this->config['or'];
return parent::getConfig( $config );
}
diff --git a/www/wiki/includes/widget/SizeFilterWidget.php b/www/wiki/includes/widget/SizeFilterWidget.php
new file mode 100644
index 00000000..c4d1dfc8
--- /dev/null
+++ b/www/wiki/includes/widget/SizeFilterWidget.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+use \OOUI\RadioSelectInputWidget;
+use \OOUI\TextInputWidget;
+use \OOUI\LabelWidget;
+
+/**
+ * Select and input widget.
+ *
+ * @copyright 2011-2018 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+class SizeFilterWidget extends \OOUI\Widget {
+
+ protected $radioselectinput = null;
+ protected $textinput = null;
+
+ /**
+ * RadioSelectInputWidget and a TextInputWidget to set minimum or maximum byte size
+ *
+ * @param array $config Configuration options
+ * - array $config['textinput'] Configuration for the TextInputWidget
+ * - array $config['radioselectinput'] Configuration for the RadioSelectWidget
+ * - bool $congif['selectMin'] Whether to select 'min', false would select 'max'
+ */
+ public function __construct( array $config = [] ) {
+ // Configuration initialization
+ $config = array_merge( [
+ 'selectMin' => true,
+ 'textinput' => [],
+ 'radioselectinput' => []
+ ], $config );
+ $config['textinput'] = array_merge( [
+ 'type' => 'number'
+ ], $config['textinput'] );
+ $config['radioselectinput'] = array_merge( [ 'options' => [
+ [
+ 'data' => 'min',
+ 'label' => wfMessage( 'minimum-size' )->text()
+ ],
+ [
+ 'data' => 'max',
+ 'label' => wfMessage( 'maximum-size' )->text()
+ ]
+ ] ], $config['radioselectinput'] );
+
+ // Parent constructor
+ parent::__construct( $config );
+
+ // Properties
+ $this->config = $config;
+ $this->radioselectinput = new RadioSelectInputWidget( $config[ 'radioselectinput'] );
+ $this->textinput = new TextInputWidget( $config[ 'textinput' ] );
+ $this->label = new LabelWidget( [ 'label' => wfMessage( 'pagesize' )->text() ] );
+
+ // Initialization
+ $this->radioselectinput->setValue( $config[ 'selectMin' ] ? 'min' : 'max' );
+ $this
+ ->addClasses( [ 'mw-widget-sizeFilterWidget' ] )
+ ->appendContent( $this->radioselectinput, $this->textinput, $this->label );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.SizeFilterWidget';
+ }
+
+ public function getConfig( &$config ) {
+ $config['textinput'] = $this->config['textinput'];
+ $config['radioselectinput'] = $this->config['radioselectinput'];
+ $config['selectMin'] = $this->config['selectMin'];
+ return parent::getConfig( $config );
+ }
+}
diff --git a/www/wiki/includes/widget/TitleInputWidget.php b/www/wiki/includes/widget/TitleInputWidget.php
index da2e94bb..db1ea0b2 100644
--- a/www/wiki/includes/widget/TitleInputWidget.php
+++ b/www/wiki/includes/widget/TitleInputWidget.php
@@ -1,14 +1,12 @@
<?php
-/**
- * MediaWiki Widgets – TitleInputWidget class.
- *
- * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
- * @license The MIT License (MIT); see LICENSE.txt
- */
+
namespace MediaWiki\Widget;
/**
* Title input widget.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
*/
class TitleInputWidget extends \OOUI\TextInputWidget {
@@ -20,17 +18,16 @@ class TitleInputWidget extends \OOUI\TextInputWidget {
/**
* @param array $config Configuration options
- * @param int|null $config['namespace'] Namespace to prepend to queries
- * @param bool|null $config['relative'] If a namespace is set,
- * return a title relative to it (default: true)
- * @param bool|null $config['suggestions'] Display search suggestions (default: true)
- * @param bool|null $config['highlightFirst'] Automatically highlight
- * the first result (default: true)
- * @param bool|null $config['validateTitle'] Whether the input must
- * be a valid title (default: true)
+ * - int|null $config['namespace'] Namespace to prepend to queries
+ * - bool|null $config['relative'] If a namespace is set,
+ * return a title relative to it (default: true)
+ * - bool|null $config['suggestions'] Display search suggestions (default: true)
+ * - bool|null $config['highlightFirst'] Automatically highlight
+ * the first result (default: true)
+ * - bool|null $config['validateTitle'] Whether the input must
+ * be a valid title (default: true)
*/
public function __construct( array $config = [] ) {
- // Parent constructor
parent::__construct(
array_merge( [ 'maxLength' => 255 ], $config )
);
@@ -76,6 +73,7 @@ class TitleInputWidget extends \OOUI\TextInputWidget {
if ( $this->validateTitle !== null ) {
$config['validateTitle'] = $this->validateTitle;
}
+ $config['$overlay'] = true;
return parent::getConfig( $config );
}
}
diff --git a/www/wiki/includes/widget/UserInputWidget.php b/www/wiki/includes/widget/UserInputWidget.php
index d591ad13..36f63c15 100644
--- a/www/wiki/includes/widget/UserInputWidget.php
+++ b/www/wiki/includes/widget/UserInputWidget.php
@@ -1,14 +1,12 @@
<?php
-/**
- * MediaWiki Widgets – UserInputWidget class.
- *
- * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
- * @license The MIT License (MIT); see LICENSE.txt
- */
+
namespace MediaWiki\Widget;
/**
* User input widget.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
*/
class UserInputWidget extends \OOUI\TextInputWidget {
@@ -16,7 +14,6 @@ class UserInputWidget extends \OOUI\TextInputWidget {
* @param array $config Configuration options
*/
public function __construct( array $config = [] ) {
- // Parent constructor
parent::__construct( $config );
// Initialization
@@ -26,4 +23,9 @@ class UserInputWidget extends \OOUI\TextInputWidget {
protected function getJavaScriptClassName() {
return 'mw.widgets.UserInputWidget';
}
+
+ public function getConfig( &$config ) {
+ $config['$overlay'] = true;
+ return parent::getConfig( $config );
+ }
}
diff --git a/www/wiki/includes/widget/UsersMultiselectWidget.php b/www/wiki/includes/widget/UsersMultiselectWidget.php
index 999cb6ab..68cdad66 100644
--- a/www/wiki/includes/widget/UsersMultiselectWidget.php
+++ b/www/wiki/includes/widget/UsersMultiselectWidget.php
@@ -1,16 +1,14 @@
<?php
-/**
- * MediaWiki Widgets – UsersMultiselectWidget class.
- *
- * @copyright 2017 MediaWiki Widgets Team and others; see AUTHORS.txt
- * @license The MIT License (MIT); see LICENSE.txt
- */
+
namespace MediaWiki\Widget;
-use \OOUI\TextInputWidget;
+use OOUI\MultilineTextInputWidget;
/**
* Widget to select multiple users.
+ *
+ * @copyright 2017 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
*/
class UsersMultiselectWidget extends \OOUI\Widget {
@@ -20,9 +18,9 @@ class UsersMultiselectWidget extends \OOUI\Widget {
/**
* @param array $config Configuration options
- * @param array $config['users'] Array of usernames to use as preset data
- * @param array $config['placeholder'] Placeholder message for input
- * @param array $config['name'] Name attribute (used in forms)
+ * - array $config['users'] Array of usernames to use as preset data
+ * - array $config['placeholder'] Placeholder message for input
+ * - array $config['name'] Name attribute (used in forms)
*/
public function __construct( array $config = [] ) {
parent::__construct( $config );
@@ -38,9 +36,8 @@ class UsersMultiselectWidget extends \OOUI\Widget {
$this->inputPlaceholder = $config['placeholder'];
}
- $textarea = new TextInputWidget( [
+ $textarea = new MultilineTextInputWidget( [
'name' => $this->inputName,
- 'multiline' => true,
'value' => implode( "\n", $this->usersArray ),
'rows' => 25,
] );
@@ -62,6 +59,7 @@ class UsersMultiselectWidget extends \OOUI\Widget {
$config['placeholder'] = $this->inputPlaceholder;
}
+ $config['$overlay'] = true;
return parent::getConfig( $config );
}
diff --git a/www/wiki/includes/widget/search/BasicSearchResultSetWidget.php b/www/wiki/includes/widget/search/BasicSearchResultSetWidget.php
index bf59fe9e..e2366405 100644
--- a/www/wiki/includes/widget/search/BasicSearchResultSetWidget.php
+++ b/www/wiki/includes/widget/search/BasicSearchResultSetWidget.php
@@ -107,10 +107,9 @@ class BasicSearchResultSetWidget {
* @return string HTML
*/
protected function header( Message $msg ) {
- return
- "<h2>" .
- "<span class='mw-headline'>" . $msg->escaped() . "</span>" .
- "</h2>";
+ return "<h2>" .
+ "<span class='mw-headline'>" . $msg->escaped() . "</span>" .
+ "</h2>";
}
/**
diff --git a/www/wiki/includes/widget/search/FullSearchResultWidget.php b/www/wiki/includes/widget/search/FullSearchResultWidget.php
index 0d0fa124..af1e0275 100644
--- a/www/wiki/includes/widget/search/FullSearchResultWidget.php
+++ b/www/wiki/includes/widget/search/FullSearchResultWidget.php
@@ -79,7 +79,7 @@ class FullSearchResultWidget implements SearchResultWidget {
if ( !Hooks::run( 'ShowSearchHit', [
$this->specialPage, $result, $terms,
&$link, &$redirect, &$section, &$extract,
- &$score, &$size, &$date, &$related, &$html
+ &$score, &$desc, &$date, &$related, &$html
] ) ) {
return $html;
}
@@ -133,13 +133,14 @@ class FullSearchResultWidget implements SearchResultWidget {
$title = clone $result->getTitle();
$query = [];
+ $attributes = [ 'data-serp-pos' => $position ];
Hooks::run( 'ShowSearchHitTitle',
- [ &$title, &$snippet, $result, $terms, $this->specialPage, &$query ] );
+ [ &$title, &$snippet, $result, $terms, $this->specialPage, &$query, &$attributes ] );
$link = $this->linkRenderer->makeLink(
$title,
$snippet,
- [ 'data-serp-pos' => $position ],
+ $attributes,
$query
);
@@ -162,7 +163,7 @@ class FullSearchResultWidget implements SearchResultWidget {
: $this->linkRenderer->makeLink( $title, $text ? new HtmlArmor( $text ) : null );
return "<span class='searchalttitle'>" .
- $this->specialPage->msg( $msgKey )->rawParams( $inner )->text()
+ $this->specialPage->msg( $msgKey )->rawParams( $inner )->parse()
. "</span>";
}
diff --git a/www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php b/www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php
index 81a1a431..b4e34148 100644
--- a/www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php
+++ b/www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php
@@ -168,7 +168,7 @@ class InterwikiSearchResultSetWidget implements SearchResultSetWidget {
*
* @param string $iwPrefix Interwiki prefix
* @return OOUI\IconWidget
- **/
+ */
protected function iwIcon( $iwPrefix ) {
$interwiki = $this->iwLookup->fetch( $iwPrefix );
$parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) );
diff --git a/www/wiki/includes/widget/search/SearchFormWidget.php b/www/wiki/includes/widget/search/SearchFormWidget.php
index 008ed19b..2c885630 100644
--- a/www/wiki/includes/widget/search/SearchFormWidget.php
+++ b/www/wiki/includes/widget/search/SearchFormWidget.php
@@ -153,10 +153,9 @@ class SearchFormWidget {
);
}
- return
- "<div class='search-types'>" .
- "<ul>" . implode( '', $items ) . "</ul>" .
- "</div>";
+ return "<div class='search-types'>" .
+ "<ul>" . implode( '', $items ) . "</ul>" .
+ "</div>";
}
/**
@@ -298,19 +297,18 @@ class SearchFormWidget {
);
}
- return
- "<fieldset id='mw-searchoptions'>" .
- "<legend>" . $this->specialSearch->msg( 'powersearch-legend' )->escaped() . '</legend>' .
- "<h4>" . $this->specialSearch->msg( 'powersearch-ns' )->parse() . '</h4>' .
- // populated by js if available
- "<div id='mw-search-togglebox'></div>" .
- $divider .
- implode(
- $divider,
- $showSections
- ) .
- $hidden .
- $remember .
- "</fieldset>";
+ return "<fieldset id='mw-searchoptions'>" .
+ "<legend>" . $this->specialSearch->msg( 'powersearch-legend' )->escaped() . '</legend>' .
+ "<h4>" . $this->specialSearch->msg( 'powersearch-ns' )->parse() . '</h4>' .
+ // populated by js if available
+ "<div id='mw-search-togglebox'></div>" .
+ $divider .
+ implode(
+ $divider,
+ $showSections
+ ) .
+ $hidden .
+ $remember .
+ "</fieldset>";
}
}
diff --git a/www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php b/www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php
index 4df2eb54..d0c259fe 100644
--- a/www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php
+++ b/www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php
@@ -13,6 +13,8 @@ use Html;
* Renders one or more SearchResultSets into a sidebar grouped by
* interwiki prefix. Includes a per-wiki header indicating where
* the results are from.
+ *
+ * @deprecated since 1.31. Use InterwikiSearchResultSetWidget
*/
class SimpleSearchResultSetWidget implements SearchResultSetWidget {
/** @var SpecialSearch */
@@ -32,6 +34,7 @@ class SimpleSearchResultSetWidget implements SearchResultSetWidget {
LinkRenderer $linkRenderer,
InterwikiLookup $iwLookup
) {
+ wfDeprecated( __METHOD__, '1.31' );
$this->specialSearch = $specialSearch;
$this->resultWidget = $resultWidget;
$this->linkRenderer = $linkRenderer;
@@ -74,13 +77,12 @@ class SimpleSearchResultSetWidget implements SearchResultSetWidget {
$out .= "</ul>";
}
- return
- "<div id='mw-search-interwiki'>" .
- "<div id='mw-search-interwiki-caption'>" .
- $this->specialSearch->msg( 'search-interwiki-caption' )->parse() .
- '</div>' .
- $out .
- "</div>";
+ return "<div id='mw-search-interwiki'>" .
+ "<div id='mw-search-interwiki-caption'>" .
+ $this->specialSearch->msg( 'search-interwiki-caption' )->parse() .
+ '</div>' .
+ $out .
+ "</div>";
}
/**
@@ -108,10 +110,9 @@ class SimpleSearchResultSetWidget implements SearchResultSetWidget {
$this->specialSearch->msg( 'search-interwiki-more' )->escaped()
);
- return
- "<div class='mw-search-interwiki-project'>" .
- "<span class='mw-search-interwiki-more'>{$searchLink}</span>" .
- $caption .
+ return "<div class='mw-search-interwiki-project'>" .
+ "<span class='mw-search-interwiki-more'>{$searchLink}</span>" .
+ $caption .
"</div>";
}
diff --git a/www/wiki/includes/widget/search/SimpleSearchResultWidget.php b/www/wiki/includes/widget/search/SimpleSearchResultWidget.php
index 8190442a..552cbaf8 100644
--- a/www/wiki/includes/widget/search/SimpleSearchResultWidget.php
+++ b/www/wiki/includes/widget/search/SimpleSearchResultWidget.php
@@ -9,6 +9,8 @@ use SpecialSearch;
/**
* Renders a simple one-line result
+ *
+ * @deprecated since 1.31. Use other result widgets.
*/
class SimpleSearchResultWidget implements SearchResultWidget {
/** @var SpecialSearch */
@@ -17,6 +19,7 @@ class SimpleSearchResultWidget implements SearchResultWidget {
protected $linkRenderer;
public function __construct( SpecialSearch $specialSearch, LinkRenderer $linkRenderer ) {
+ wfDeprecated( __METHOD__, '1.31' );
$this->specialSearch = $specialSearch;
$this->linkRenderer = $linkRenderer;
}
@@ -51,7 +54,7 @@ class SimpleSearchResultWidget implements SearchResultWidget {
"<span class='searchalttitle'>" .
$this->specialSearch->msg( 'search-redirect' )->rawParams(
$this->linkRenderer->makeLink( $redirectTitle, $redirectText )
- )->text() .
+ )->parse() .
"</span>";
}