From: jenkins-bot Date: Sat, 29 Dec 2018 01:10:13 +0000 (+0000) Subject: Merge "auth: Follow up on e907d4328dc3e" X-Git-Tag: 1.34.0-rc.0~3181 X-Git-Url: http://git.cyclocoop.org/%7B%7B%20url_for%28%27admin_vote_del%27%2C%20idvote=vote.voteid%29%20%7D%7D?a=commitdiff_plain;h=1d47891cc3d43bc6b47e30d0b605436c3dac1fc9;hp=f4cc388eb9dfcfa69f279c6ced1a8b4734df05c9;p=lhc%2Fweb%2Fwiklou.git Merge "auth: Follow up on e907d4328dc3e" --- diff --git a/.eslintrc.json b/.eslintrc.json index c0767517ea..97f7c31248 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,60 +1,15 @@ { - "extends": "wikimedia", - "env": { - "browser": true - }, + "extends": [ + "wikimedia/client", + "wikimedia/jquery" + ], "globals": { "require": false, "module": false, "mw": false, - "$": false, "OO": false }, "rules": { - "no-restricted-properties": [ - 2, - { - "object": "$", - "property": "map", - "message": "Please use Array.prototype.map instead" - }, - { - "object": "$", - "property": "inArray", - "message": "Please use Array.prototype.indexOf instead" - }, - { - "object": "$", - "property": "each", - "message": "Please consider different approaches to $.each, especially when using Array's. You can override this warning if necessary with eslint-disable-next-line." - }, - { - "object": "$", - "property": "isArray", - "message": "Please use Array.isArray instead" - }, - { - "object": "$", - "property": "isFunction", - "message": "Please use typeof (e.g. typeof e === 'function') instead" - }, - { - "object": "$", - "property": "grep", - "message": "Please use Array.prototype.filter instead" - }, - { - "object": "$", - "property": "trim", - "message": "Please use String.prototype.trim instead" - }, - { - "object": "$", - "property": "proxy", - "message": "Please use Function.prototype.bind instead" - } - ], - "dot-notation": 0, "max-len": 0 } } diff --git a/.gitignore b/.gitignore index 44f739e731..2f17bc6d5f 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ Thumbs.db .buildpath .classpath .idea +*.iml .metadata* .settings /favicon.ico diff --git a/.phpcs.xml b/.phpcs.xml index 2bce5b2a59..c0154c7543 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -323,7 +323,6 @@ includes/export/DumpPipeOutput\.php includes/resourceloader/ResourceLoaderImage\.php includes/shell/Command\.php - includes/tidy/RaggettExternal\.php maintenance/dumpTextPass\.php maintenance/mysql\.php maintenance/storage/recompressTracked\.php diff --git a/.travis.yml b/.travis.yml index b28861d4e2..3be6531dc6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,11 +17,25 @@ sudo: false # - Required for non-buggy xml library for XmlTypeCheck/UploadBaseTest (T75176). dist: trusty +git: + depth: 3 + quiet: true + +# Cache NPM and Composer directories +# +cache: + npm: true + directories: + # Composer doesn't have a dedicated cache setting in Travis CI config, so set the directory path instead. + - vendor + matrix: fast_finish: true include: # On Trusty, mysql user 'travis' doesn't have create database rights # Postgres has no user called 'root'. + - env: dbtype=mysql dbuser=root + php: 7.3 - env: dbtype=mysql dbuser=root php: 7.2 - env: dbtype=mysql dbuser=root @@ -38,6 +52,7 @@ matrix: - env: dbtype=mysql dbuser=root php: hhvm-3.18 allow_failures: + - php: 7.3 - php: hhvm-3.18 - php: hhvm-3.21 - php: hhvm-3.24 diff --git a/Gruntfile.js b/Gruntfile.js index 6be908e087..707a1fb095 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -21,6 +21,9 @@ module.exports = function ( grunt ) { grunt.initConfig( { eslint: { + options: { + reportUnusedDisableDirectives: true + }, all: [ '**/*.js', '!docs/**', diff --git a/RELEASE-NOTES-1.32 b/RELEASE-NOTES-1.32 index 438fac3ae4..cac65ab592 100644 --- a/RELEASE-NOTES-1.32 +++ b/RELEASE-NOTES-1.32 @@ -133,6 +133,8 @@ production. * Added a new hook, 'UserGetRightsRemove', which can be used to remove rights from user. Unlike the 'UserGetRights' it will ensure that removed rights will not be reinserted. +* (T197535) Extensions can now specify PHP versions and PHP extensions they + depend on. === External library changes in 1.32 === @@ -154,6 +156,7 @@ production. ** ScopedCallback objects can no longer be serialized. * Updated wikimedia/timestamp from v1.0.0 to v2.2.0. * Updated wikimedia/wrappedstring from v2.3.0 to v3.0.1. +* oyejorge/less.php replaced with our fork wikimedia/less.php * Updated composer/spdx-licenses from v1.3.0 to v1.4.0 (dev-only). * Updated mediawiki/mediawiki-codesniffer from v18.0.0 to v22.0.0 (dev-only). @@ -657,10 +660,12 @@ because of Phabricator reports. yet for creating or managing content in slots beides the main slot. See for more information. -* The image_comment_temp database table is merged into the image table and - deprecated. Since access should be mediated by the CommentStore class, this - change shouldn't affect external code. +* The image_comment_temp database table has been removed. Since all access + should be mediated by the CommentStore class, this change shouldn't affect + external code. * (T206147) Database::close() will no longer commit any open transactions. +* (T64103) Dropped columns category.cat_hidden, site_stats.ss_admins, and + recentchanges.rc_cur_time from the PostgreSQL schema. == Compatibility == MediaWiki 1.32 requires PHP 7.0.0 or later. Although HHVM 3.18.5 or later is diff --git a/RELEASE-NOTES-1.33 b/RELEASE-NOTES-1.33 index f6c819d3a6..759d91273a 100644 --- a/RELEASE-NOTES-1.33 +++ b/RELEASE-NOTES-1.33 @@ -15,23 +15,49 @@ production. the current parse language where available. ==== Changed configuration ==== +* Some external link searches will not work correctly until update.php (or + refreshExternallinksIndex.php) is run. These include searches for links using + IP addresses, internationalized domain names, and possibly mailto links. +* (T193868) $wgChangeTagsSchemaMigrationStage — This temporary setting, added in + MediaWiki 1.32, now defaults to MIGRATION_NEW instead of MIGRATION_WRITE_BOTH. * … ==== Removed configuration ==== +* (T199334) $wgTagStatisticsNewTable — This temporary setting, added in + MediaWiki 1.32, has now been removed. When loading Special:Tags, MediaWiki + will now always use the `change_tag_def` instead of the `change_tag` table. +* MediaWiki now always tidies user output, and most related + configuration has been removed. Thus $wgUseTidy, $wgTidyBin, + $wgTidyConf, $wgTidyOpts, $wgTidyInternal, and $wgDebugTidy, all + deprecated since 1.26, have now all been removed. The $wgTidyConfig + setting remains only for Remex experimental features or debugging. * … === New features in 1.33 === -* The 'GetPreferences' hook now receives an additional $context parameter. +* (T96041) __EXPECTUNUSEDCATEGORY__ on a category page causes the category + to be hidden on Special:UnusedCategories. +* Add PasswordPolicy to check the password isn't in the large blacklist. +* The AuthManagerLoginAuthenticateAudit hook has a new parameter for + additional information about the authentication event. * … === External library changes in 1.33 === ==== New external libraries ==== +* Added wikimedia/password-blacklist 0.1.4. * … +* Added guzzlehttp/guzzle 6.3.3 and dependents: + * guzzlehttp/promises 1.3.1 + * guzzlehttp/psr7 1.5.0 + * psr/http-message 1.0.1 + * ralouphie/getallheaders 2.0.5 ==== Changed external libraries ==== * Updated wikimedia/xmp-reader from 0.6.0 to 0.6.1. * Updated wikimedia/scoped-callback from 2.0.0 to 3.0.0. +* Updated wikimedia/ip-set from 1.2.0 to 2.0.0. + * The deprecated IPSet\IPSet alias was removed, Wikimedia\IPSet must be + used instead. * … ==== Removed external libraries ==== @@ -42,7 +68,16 @@ production. === Action API changes in 1.33 === * (T198913) Added 'ApiOptions' hook. -* … +* The JSON formatversion=2 is no longer experimental. +* Internal API errors (those with code beginning "internal_api_error") will + include the exception class name in a data field named "errorclass". + * Class names are not guaranteed to remain stable, and in particular database + exceptions will now include the "Wikimedia\Rdbms\" prefix in the class name. + * The code including an exception class name is deprecated. In the future, + all internal errors will use code "internal_api_error". +* (T212356) When using action=delete on pages with many revisions, the module + may return a boolean-true 'scheduled' and no 'logid'. This signifies that the + deletion will be processed via the job queue. === Action API internal changes in 1.33 === * A number of deprecated methods for API documentation, intended for overriding @@ -55,6 +90,10 @@ production. Additionally, the 'APIGetDescription' and 'APIGetParamDescription' hooks have been removed, as their only use was to let extensions override values returned by getDescription() and getParamDescription(), respectively. +* API error codes may only contain ASCII letters, numbers, underscore, and + hyphen. Methods such as ApiBase::dieWithError() and + ApiMessageTrait::setApiCode() will throw an InvalidArgumentException if + passed a bad code. * … === Languages updated in 1.33 === @@ -83,6 +122,9 @@ because of Phabricator reports. * ParserOptions defaults 'tidy' to true now, since the untidy modes of the parser are being deprecated and ParserOptions::getCanonicalOverrides() has always been true at any rate. +* Support for disabling tidy and external tidy implementations has been removed. + This was deprecated in 1.32. The pure PHP Remex tidy implementation is now + used and no configuration is necessary. * A number of deprecated methods for API documentation, intended for overriding by extensions, are no longer called by MediaWiki, and will emit deprecation notices if your extension attempts to use them: @@ -104,7 +146,33 @@ because of Phabricator reports. * The hooks LanguageGetSpecialPageAliases and LanguageGetMagic, deprecated since 1.16, have now been removed. Instead, use $specialPageAliases or $magicWords respectively in a $wgExtensionMessagesFiles file. -* … +* The following methods of the Preferences class, deprecated in 1.31, have been + removed: + * getSaveBlacklist() + * loadPreferenceValues() + * getOptionFromUser() + * profilePreferences() + * skinPreferences() + * filesPreferences() + * datetimePreferences() + * renderingPreferences() + * editingPreferences() + * rcPreferences() + * watchlistPreferences() + * searchPreferences() + * miscPreferences() + * generateSkinOptions() + * getDateOptions() + * getImageSizes() + * getThumbSizes() + * validateSignature() + * cleanSignature() + * getTimezoneOptions() + * filterIntval() + * filterTimezoneInput() + * getTimeZoneList() +* mw.util.jsMessage(), deprecated in 1.20, was removed. Use mw.notify instead. +* (T61113) User::EDIT_TOKEN_SUFFIX was removed. It was deprecated since 1.27. === Deprecations in 1.33 === * The configuration option $wgUseESI has been deprecated, and is expected @@ -121,9 +189,22 @@ because of Phabricator reports. This will help identify the issue if you added it to $wgAuthManagerConfig. * wfSplitWikiId() is now deprecated. Cache key generation should have the wiki domain ID as a key component and use makeGlobalKey(). +* (T202094) Title::getUserCaseDBKey() is deprecated; instead, please use + Title::getDBKey(), which doesn't vary case. +* User::getPasswordValidity() is now deprecated. User::checkPasswordValidity() + returns the same information in a more useful format. +* For Linker::generateTOC() and Linker::tocList(), passing strings or booleans + as the $lang parameter was deprecated. The same applies to DummyLinker. +* The PasswordPolicy 'PasswordCannotBePopular' has been deprecated. To + follow best practices, it is reccommended to use 'PasswordNotInLargeBlacklist' + instead which blacklists 100,000 commonly used passwords. * … === Other changes in 1.33 === +* (T208871) The hard-coded Google search form on the database error page was + removed. +* (T201747) Html::openElement() warns if given an element name wiht a space + in it. * … == Compatibility == diff --git a/autoload.php b/autoload.php index 2518a04ebb..6a5a9dfc45 100644 --- a/autoload.php +++ b/autoload.php @@ -542,6 +542,7 @@ $wgAutoloadLocalClasses = [ 'ForeignDBFile' => __DIR__ . '/includes/filerepo/file/ForeignDBFile.php', 'ForeignDBRepo' => __DIR__ . '/includes/filerepo/ForeignDBRepo.php', 'ForeignDBViaLBRepo' => __DIR__ . '/includes/filerepo/ForeignDBViaLBRepo.php', + 'ForeignResourceManager' => __DIR__ . '/includes/ForeignResourceManager.php', 'ForeignTitle' => __DIR__ . '/includes/title/ForeignTitle.php', 'ForeignTitleFactory' => __DIR__ . '/includes/title/ForeignTitleFactory.php', 'ForkController' => __DIR__ . '/includes/ForkController.php', @@ -570,6 +571,7 @@ $wgAutoloadLocalClasses = [ 'GitInfo' => __DIR__ . '/includes/GitInfo.php', 'GlobalDependency' => __DIR__ . '/includes/cache/CacheDependency.php', 'GlobalVarConfig' => __DIR__ . '/includes/config/GlobalVarConfig.php', + 'GuzzleHttpRequest' => __DIR__ . '/includes/http/GuzzleHttpRequest.php', 'HHVMMakeRepo' => __DIR__ . '/maintenance/hhvm/makeRepo.php', 'HTMLApiField' => __DIR__ . '/includes/htmlform/fields/HTMLApiField.php', 'HTMLAutoCompleteSelectField' => __DIR__ . '/includes/htmlform/fields/HTMLAutoCompleteSelectField.php', @@ -885,6 +887,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Logger\\MonologSpi' => __DIR__ . '/includes/debug/logger/MonologSpi.php', 'MediaWiki\\Logger\\Monolog\\AvroFormatter' => __DIR__ . '/includes/debug/logger/monolog/AvroFormatter.php', 'MediaWiki\\Logger\\Monolog\\BufferHandler' => __DIR__ . '/includes/debug/logger/monolog/BufferHandler.php', + 'MediaWiki\\Logger\\Monolog\\CeeFormatter' => __DIR__ . '/includes/debug/logger/monolog/CeeFormatter.php', 'MediaWiki\\Logger\\Monolog\\KafkaHandler' => __DIR__ . '/includes/debug/logger/monolog/KafkaHandler.php', 'MediaWiki\\Logger\\Monolog\\LegacyFormatter' => __DIR__ . '/includes/debug/logger/monolog/LegacyFormatter.php', 'MediaWiki\\Logger\\Monolog\\LegacyHandler' => __DIR__ . '/includes/debug/logger/monolog/LegacyHandler.php', @@ -923,6 +926,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Widget\\DateTimeInputWidget' => __DIR__ . '/includes/widget/DateTimeInputWidget.php', 'MediaWiki\\Widget\\ExpiryInputWidget' => __DIR__ . '/includes/widget/ExpiryInputWidget.php', 'MediaWiki\\Widget\\NamespaceInputWidget' => __DIR__ . '/includes/widget/NamespaceInputWidget.php', + 'MediaWiki\\Widget\\PendingTextInputWidget' => __DIR__ . '/includes/widget/PendingTextInputWidget.php', 'MediaWiki\\Widget\\SearchInputWidget' => __DIR__ . '/includes/widget/SearchInputWidget.php', 'MediaWiki\\Widget\\Search\\BasicSearchResultSetWidget' => __DIR__ . '/includes/widget/search/BasicSearchResultSetWidget.php', 'MediaWiki\\Widget\\Search\\DidYouMeanWidget' => __DIR__ . '/includes/widget/search/DidYouMeanWidget.php', @@ -936,6 +940,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Widget\\Search\\SimpleSearchResultWidget' => __DIR__ . '/includes/widget/search/SimpleSearchResultWidget.php', 'MediaWiki\\Widget\\SelectWithInputWidget' => __DIR__ . '/includes/widget/SelectWithInputWidget.php', 'MediaWiki\\Widget\\SizeFilterWidget' => __DIR__ . '/includes/widget/SizeFilterWidget.php', + 'MediaWiki\\Widget\\TagMultiselectWidget' => __DIR__ . '/includes/widget/TagMultiselectWidget.php', 'MediaWiki\\Widget\\TitleInputWidget' => __DIR__ . '/includes/widget/TitleInputWidget.php', 'MediaWiki\\Widget\\TitlesMultiselectWidget' => __DIR__ . '/includes/widget/TitlesMultiselectWidget.php', 'MediaWiki\\Widget\\UserInputWidget' => __DIR__ . '/includes/widget/UserInputWidget.php', @@ -1126,6 +1131,7 @@ $wgAutoloadLocalClasses = [ 'ProcessCacheLRU' => __DIR__ . '/includes/libs/ProcessCacheLRU.php', 'Processor' => __DIR__ . '/includes/registration/Processor.php', 'Profiler' => __DIR__ . '/includes/profiler/Profiler.php', + 'ProfilerExcimer' => __DIR__ . '/includes/profiler/ProfilerExcimer.php', 'ProfilerOutput' => __DIR__ . '/includes/profiler/output/ProfilerOutput.php', 'ProfilerOutputDb' => __DIR__ . '/includes/profiler/output/ProfilerOutputDb.php', 'ProfilerOutputDump' => __DIR__ . '/includes/profiler/output/ProfilerOutputDump.php', @@ -1190,6 +1196,7 @@ $wgAutoloadLocalClasses = [ 'RedisConnectionPool' => __DIR__ . '/includes/libs/redis/RedisConnectionPool.php', 'RedisLockManager' => __DIR__ . '/includes/libs/lockmanager/RedisLockManager.php', 'RedisPubSubFeedEngine' => __DIR__ . '/includes/rcfeed/RedisPubSubFeedEngine.php', + 'RefreshExternallinksIndex' => __DIR__ . '/maintenance/refreshExternallinksIndex.php', 'RefreshFileHeaders' => __DIR__ . '/maintenance/refreshFileHeaders.php', 'RefreshImageMetadata' => __DIR__ . '/maintenance/refreshImageMetadata.php', 'RefreshLinks' => __DIR__ . '/maintenance/refreshLinks.php', @@ -1206,6 +1213,7 @@ $wgAutoloadLocalClasses = [ 'RepoGroup' => __DIR__ . '/includes/filerepo/RepoGroup.php', 'RequestContext' => __DIR__ . '/includes/context/RequestContext.php', 'ResetAuthenticationThrottle' => __DIR__ . '/maintenance/resetAuthenticationThrottle.php', + 'ResetPageRandom' => __DIR__ . '/maintenance/resetPageRandom.php', 'ResetUserEmail' => __DIR__ . '/maintenance/resetUserEmail.php', 'ResetUserTokens' => __DIR__ . '/maintenance/resetUserTokens.php', 'ResourceFileCache' => __DIR__ . '/includes/cache/ResourceFileCache.php', diff --git a/composer.json b/composer.json index 181f62e49e..88f5daa590 100644 --- a/composer.json +++ b/composer.json @@ -25,14 +25,18 @@ "ext-json": "*", "ext-mbstring": "*", "ext-xml": "*", + "guzzlehttp/guzzle": "6.3.3", + "guzzlehttp/promises": "1.3.1", + "guzzlehttp/psr7": "1.5.0", "liuggio/statsd-php-client": "1.0.18", - "oojs/oojs-ui": "0.29.3", - "oyejorge/less.php": "1.7.0.14", + "oojs/oojs-ui": "0.30.0", "pear/mail": "1.4.1", "pear/mail_mime": "1.10.2", "pear/net_smtp": "1.8.0", "php": ">=5.6.99", + "psr/http-message": "1.0.1", "psr/log": "1.0.2", + "ralouphie/getallheaders": "2.0.5", "wikimedia/assert": "0.2.2", "wikimedia/at-ease": "1.2.0", "wikimedia/base-convert": "2.0.0", @@ -40,8 +44,10 @@ "wikimedia/cldr-plural-rule-parser": "1.0.0", "wikimedia/composer-merge-plugin": "1.4.1", "wikimedia/html-formatter": "1.0.2", - "wikimedia/ip-set": "1.2.0", + "wikimedia/ip-set": "2.0.0", + "wikimedia/less.php": "1.8.0", "wikimedia/object-factory": "1.0.0", + "wikimedia/password-blacklist": "0.1.4", "wikimedia/php-session-serializer": "1.0.6", "wikimedia/purtle": "1.0.7", "wikimedia/relpath": "2.1.1", @@ -63,7 +69,7 @@ "jakub-onderka/php-parallel-lint": "0.9.2", "jetbrains/phpstorm-stubs": "dev-master#38ff1a581b297f7901e961b8c923862ea80c3b96", "justinrainbow/json-schema": "~5.2", - "mediawiki/mediawiki-codesniffer": "22.0.0", + "mediawiki/mediawiki-codesniffer": "23.0.0", "monolog/monolog": "~1.22.1", "nikic/php-parser": "3.1.3", "seld/jsonlint": "1.7.1", diff --git a/docs/extension.schema.v1.json b/docs/extension.schema.v1.json index f6f3b2188f..7e4203587d 100644 --- a/docs/extension.schema.v1.json +++ b/docs/extension.schema.v1.json @@ -693,6 +693,14 @@ "type": "string" } }, + "ReauthenticateTime": { + "type": "object", + "patternProperties": { + ".*": { + "type": "integer" + } + } + }, "callback": { "type": [ "array", diff --git a/docs/extension.schema.v2.json b/docs/extension.schema.v2.json index 8ade991d4d..c5c3b5d6bb 100644 --- a/docs/extension.schema.v2.json +++ b/docs/extension.schema.v2.json @@ -715,6 +715,14 @@ "type": "string" } }, + "ReauthenticateTime": { + "type": "object", + "patternProperties": { + ".*": { + "type": "integer" + } + } + }, "callback": { "type": [ "array", diff --git a/docs/hooks.txt b/docs/hooks.txt index bd06d52b4a..28065fc6d2 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -786,7 +786,11 @@ $response: The MediaWiki\Auth\AuthenticationResponse in either a PASS or FAIL $user: The User object being authenticated against, or null if authentication failed before getting that far. $username: A guess at the user name being authenticated, or null if we can't - even determine that. + even determine that. When $user is not null, it can be in the form of + @ (e.g. for bot passwords). +$extraData: An array (string => string) with extra information, intended to be + added to log contexts. Fields it might include: + - appId: the application ID, only if the login was with a bot password 'AuthPluginAutoCreate': DEPRECATED since 1.27! Use the 'LocalUserCreated' hook instead. Called when creating a local account for an user logged in from an @@ -1735,7 +1739,6 @@ $out: OutputPage object (to check what type of page the user is on) 'GetPreferences': Modify user preferences. $user: User whose preferences are being modified. &$preferences: Preferences description array, to be fed to an HTMLForm object -$context: IContextSource object (added in 1.33) 'GetRelativeTimestamp': Pre-emptively override the relative timestamp generated by MWTimestamp::getRelativeTimestamp(). Return false in this hook to use the @@ -3418,6 +3421,9 @@ $title: Title object that is being checked $old: old title $nt: new title $user: user who does the move +$reason: string of the reason provided by the user +&$status: Status object. To abort the move, add a fatal error to this object + (i.e. call $status->fatal()). 'TitleMoveStarting': Before moving an article (title), but just after the atomic DB section starts. diff --git a/includes/Block.php b/includes/Block.php index 39f2b2990b..ec8cae80a5 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -766,11 +766,11 @@ class Block { return; } - $target = $block->getTarget(); - // FIXME: Push this into getTargetActor() or whatever to reuse - if ( is_string( $target ) ) { - $target = User::newFromName( $target, false ); + // Autoblocks only apply to TYPE_USER + if ( $block->getType() !== self::TYPE_USER ) { + return; } + $target = $block->getTarget(); // TYPE_USER => always a User object $dbr = wfGetDB( DB_REPLICA ); $rcQuery = ActorMigration::newMigration()->getWhere( $dbr, 'rc_user', $target, false ); @@ -1168,7 +1168,12 @@ class Block { $res = $this->isSitewide(); break; case 'editownusertalk': + // NOTE: this check is not reliable on partial blocks + // since partially blocked users are always allowed to edit + // their own talk page unless a restriction exists on the + // page or User_talk: namespace $res = wfSetVar( $this->mDisableUsertalk, $x ); + // edit own user talk can be disabled by config if ( !$blockAllowsUTEdit ) { $res = true; @@ -1595,7 +1600,6 @@ class Block { * @param User|string $user Local User object or username string */ public function setBlocker( $user ) { - // FIXME: Push this into getTargetActor() or whatever to reuse if ( is_string( $user ) ) { $user = User::newFromName( $user, false ); } @@ -1796,39 +1800,28 @@ class Block { } /** - * Checks if a block prevents an edit on a given article + * Checks if a block applies to a particular title * - * @param \Title $title + * This check does not consider whether `$this->prevents( 'editownusertalk' )` + * returns false, as the identity of the user making the hypothetical edit + * isn't known here (particularly in the case of IP hardblocks, range + * blocks, and auto-blocks). + * + * @param Title $title * @return bool */ - public function preventsEdit( \Title $title ) { - $blocked = $this->isSitewide(); - - // user talk page has its own rules - // This check happens before partial blocks because the flag - // to allow user to edit their user talk page could be - // overwritten by a partial block restriction (E.g. user talk namespace) - $user = $this->getTarget(); - - // Not all blocked `$user`s are self::TYPE_USER - // FIXME: Push this into getTargetActor() or whatever to reuse - if ( is_string( $user ) ) { - $user = User::newFromName( $user, false ); - } - - if ( $title->equals( $user->getTalkPage() ) ) { - $blocked = $this->prevents( 'editownusertalk' ); + public function appliesToTitle( Title $title ) { + if ( $this->isSitewide() ) { + return true; } - if ( !$this->isSitewide() ) { - $restrictions = $this->getRestrictions(); - foreach ( $restrictions as $restriction ) { - if ( $restriction->matches( $title ) ) { - $blocked = true; - } + $restrictions = $this->getRestrictions(); + foreach ( $restrictions as $restriction ) { + if ( $restriction->matches( $title ) ) { + return true; } } - return $blocked; + return false; } } diff --git a/includes/CommentStore.php b/includes/CommentStore.php index 1be79510cb..cba7a150d3 100644 --- a/includes/CommentStore.php +++ b/includes/CommentStore.php @@ -70,11 +70,7 @@ class CommentStore { 'deprecatedIn' => null, ], 'img_description' => [ - 'table' => 'image_comment_temp', - 'pk' => 'imgcomment_name', - 'field' => 'imgcomment_description_id', - 'joinPK' => 'img_name', - 'stage' => MIGRATION_WRITE_NEW, + 'stage' => MIGRATION_NEW, 'deprecatedIn' => '1.32', ], ]; @@ -226,8 +222,14 @@ class CommentStore { if ( $tempTableStage === MIGRATION_OLD ) { $joinField = "{$alias}.{$t['field']}"; } else { + // Nothing hits this code path for now, but will in the future when we set + // $this->tempTables['rev_comment']['stage'] to MIGRATION_WRITE_NEW while + // merging revision_comment_temp into revision. + // @codeCoverageIgnoreStart $joins[$alias][0] = 'LEFT JOIN'; $joinField = "(CASE WHEN {$key}_id != 0 THEN {$key}_id ELSE {$alias}.{$t['field']} END)"; + throw new LogicException( 'Nothing should reach this code path at this time' ); + // @codeCoverageIgnoreEnd } } else { $joinField = "{$key}_id"; @@ -349,14 +351,13 @@ class CommentStore { $msg = null; if ( $data !== null ) { - $data = FormatJson::decode( $data ); - if ( !is_object( $data ) ) { + $data = FormatJson::decode( $data, true ); + if ( !is_array( $data ) ) { // @codeCoverageIgnoreStart wfLogWarning( "Invalid JSON object in comment: $data" ); $data = null; // @codeCoverageIgnoreEnd } else { - $data = (array)$data; if ( isset( $data['_message'] ) ) { $msg = self::decodeMessage( $data['_message'] ) ->setInterfaceMessageFlag( true ); diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 9c26a285e1..f7c3fce124 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -37,6 +37,7 @@ * * @file */ +use MediaWiki\MediaWikiServices; /** * @cond file_level_code @@ -1867,7 +1868,7 @@ $wgDBserver = 'localhost'; $wgDBport = 5432; /** - * Name of the database + * Name of the database; this should be alphanumeric and not contain spaces nor hyphens */ $wgDBname = 'my_wiki'; @@ -1934,7 +1935,7 @@ $wgSearchType = null; $wgSearchTypeAlternatives = null; /** - * Table name prefix + * Table name prefix; this should be alphanumeric and not contain spaces nor hyphens */ $wgDBprefix = ''; @@ -1952,7 +1953,7 @@ $wgDBTableOptions = 'ENGINE=InnoDB, DEFAULT CHARSET=binary'; $wgSQLMode = ''; /** - * Mediawiki schema + * Mediawiki schema; this should be alphanumeric and not contain spaces nor hyphens */ $wgDBmwschema = null; @@ -2164,7 +2165,15 @@ $wgDBOracleDRCP = false; /** * Other wikis on this site, can be administered from a single developer account. * - * Array numeric key => database name + * @var string[] List of wiki DB domain IDs; the format of each ID consist of 1-3 hyphen + * delimited alphanumeric components (each with no hyphens nor spaces) of any of the forms: + * - "--" + * - "-
" + * - "" + * If hyphens appear in any of the components, then the domain ID parsing may not work + * in all cases and site functionality might be affected. If the schema ($wgDBmwschema) + * is left to the default "mediawiki" for all wikis, then the schema should be omitted + * from these IDs. */ $wgLocalDatabases = []; @@ -4273,74 +4282,24 @@ $wgAllowImageTag = false; /** * Configuration for HTML postprocessing tool. Set this to a configuration * 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/ - * - * Setting this to null is deprecated. - * - * If this is null and $wgUseTidy is true, the deprecated configuration - * parameters will be used instead. - * - * If this is null and $wgUseTidy is false, a pure PHP fallback will be used. - * (Equivalent to setting `$wgTidyConfig['driver'] = 'disabled'`.) - * - * Keys are: - * - driver: May be: - * - RemexHtml: Use the RemexHtml library in PHP - * - RaggettInternalHHVM: Use the limited-functionality HHVM extension - * Deprecated since 1.32. - * - RaggettInternalPHP: Use the PECL extension - * Deprecated since 1.32. - * - RaggettExternal: Shell out to an external binary (tidyBin) - * Deprecated since 1.32. - * - disabled: Disable tidy pass and use a hacky pure PHP workaround - * (this is what setting $wgUseTidy to false used to do) - * Deprecated since 1.32. - * - * - tidyConfigFile: Path to configuration file for any of the Raggett drivers - * - debugComment: True to add a comment to the output with warning messages - * - tidyBin: For RaggettExternal, the path to the tidy binary. - * - tidyCommandLine: For RaggettExternal, additional command line options. + * library; historically, other postprocessors were used. + * + * Setting this to null will use default settings. + * + * Keys include: + * - driver: formerly used to select a postprocessor; now ignored. + * - treeMutationTrace: a boolean to turn on Remex tracing + * - serializerTrace: a boolean to turn on Remex tracing + * - mungerTrace: a boolean to turn on Remex tracing + * - pwrap: whether

wrapping should be done (default true) + * + * See includes/tidy/RemexDriver.php for detail on configuration. + * + * Overriding the default configuration is strongly discouraged in + * production. */ $wgTidyConfig = [ 'driver' => 'RemexHtml' ]; -/** - * Set this to true to use the deprecated tidy configuration parameters. - * @deprecated since 1.26, use $wgTidyConfig['driver'] = 'disabled' - */ -$wgUseTidy = false; - -/** - * The path to the tidy binary. - * @deprecated since 1.26, use $wgTidyConfig['tidyBin'] - */ -$wgTidyBin = 'tidy'; - -/** - * The path to the tidy config file - * @deprecated since 1.26, use $wgTidyConfig['tidyConfigFile'] - */ -$wgTidyConf = $IP . '/includes/tidy/tidy.conf'; - -/** - * The command line options to the tidy binary - * @deprecated since 1.26, use $wgTidyConfig['tidyCommandLine'] - */ -$wgTidyOpts = ''; - -/** - * Set this to true to use the tidy extension - * @deprecated since 1.26, use $wgTidyConfig['driver'] - */ -$wgTidyInternal = extension_loaded( 'tidy' ); - -/** - * Put tidy warnings in HTML comments - * Only works for internal tidy. - * @deprecated since 1.26, use $wgTidyConfig['debugComment'] - */ -$wgDebugTidy = false; - /** * Allow raw, unchecked HTML in "..." sections. * THIS IS VERY DANGEROUS on a publicly editable site, so USE wgGroupPermissions @@ -4492,52 +4451,72 @@ $wgCentralIdLookupProviders = [ $wgCentralIdLookupProvider = 'local'; /** - * Password policy for local wiki users. A user's effective policy - * is the superset of all policy statements from the policies for the - * groups where the user is a member. If more than one group policy - * include the same policy statement, the value is the max() of the - * values. Note true > false. The 'default' policy group is required, - * and serves as the minimum policy for all users. New statements can - * be added by appending to $wgPasswordPolicy['checks']. - * Statements: - * - MinimalPasswordLength - minimum length a user can set - * - MinimumPasswordLengthToLogin - passwords shorter than this will + * Password policy for the wiki. + * Structured as + * [ + * 'policies' => [ => [ => , ... ], ... ], + * 'checks' => [ => , ... ], + * ] + * where is a user group, is a password policy name + * (arbitrary string) defined in the 'checks' part, is the + * PHP callable implementing the policy check, is a number, + * boolean or null that gets passed to the callback. + * + * A user's effective policy is the superset of all policy statements + * from the policies for the groups where the user is a member. If more + * than one group policy include the same policy statement, the value is + * the max() of the values. Note true > false. The 'default' policy group + * is required, and serves as the minimum policy for all users. + * + * Callbacks receive three arguments: the policy value, the User object + * and the password; and must return a StatusValue. A non-good status + * means the password will not be accepted for new accounts, and existing + * accounts will be prompted for password change or barred from logging in + * (depending on whether the status is a fatal or merely error/warning). + * + * The checks supported by core are: + * - MinimalPasswordLength - Minimum length a user can set. + * - MinimumPasswordLengthToLogin - Passwords shorter than this will * not be allowed to login, regardless if it is correct. * - MaximalPasswordLength - maximum length password a user is allowed * to attempt. Prevents DoS attacks with pbkdf2. - * - PasswordCannotMatchUsername - Password cannot match username to + * - PasswordCannotMatchUsername - Password cannot match the username. * - PasswordCannotMatchBlacklist - Username/password combination cannot - * match a specific, hardcoded blacklist. + * match a blacklist of default passwords used by MediaWiki in the past. * - PasswordCannotBePopular - Blacklist passwords which are known to be * commonly chosen. Set to integer n to ban the top n passwords. * If you want to ban all common passwords on file, use the * PHP_INT_MAX constant. + * Deprecated since 1.33. Use PasswordNotInLargeBlacklist instead. + * - PasswordNotInLargeBlacklist - Password not in best practices list of + * 100,000 commonly used passwords. Due to the size of the list this + * is a probabilistic test. + * * @since 1.26 + * @see PasswordPolicyChecks + * @see User::checkPasswordValidity() */ $wgPasswordPolicy = [ 'policies' => [ 'bureaucrat' => [ - 'MinimalPasswordLength' => 8, + 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, - 'PasswordCannotMatchUsername' => true, - 'PasswordCannotBePopular' => 25, + 'PasswordNotInLargeBlacklist' => true, ], 'sysop' => [ - 'MinimalPasswordLength' => 8, + 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, - 'PasswordCannotMatchUsername' => true, - 'PasswordCannotBePopular' => 25, + 'PasswordNotInLargeBlacklist' => true, ], 'interface-admin' => [ - 'MinimalPasswordLength' => 8, + 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, - 'PasswordCannotMatchUsername' => true, - 'PasswordCannotBePopular' => 25, + 'PasswordNotInLargeBlacklist' => true, ], 'bot' => [ - 'MinimalPasswordLength' => 8, + 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, - 'PasswordCannotMatchUsername' => true, + 'PasswordNotInLargeBlacklist' => true, ], 'default' => [ 'MinimalPasswordLength' => 1, @@ -4552,7 +4531,8 @@ $wgPasswordPolicy = [ 'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername', 'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist', 'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength', - 'PasswordCannotBePopular' => 'PasswordPolicyChecks::checkPopularPasswordBlacklist' + 'PasswordCannotBePopular' => 'PasswordPolicyChecks::checkPopularPasswordBlacklist', + 'PasswordNotInLargeBlacklist' => 'PasswordPolicyChecks::checkPasswordNotInLargeBlacklist', ], ]; @@ -5008,6 +4988,10 @@ $wgAutoblockExpiry = 86400; /** * Set this to true to allow blocked users to edit their own user talk page. + * + * This only applies to sitewide blocks. Partial blocks always allow users to + * edit their own user talk page unless otherwise specified in the block + * restrictions. */ $wgBlockAllowsUTEdit = true; @@ -5849,6 +5833,7 @@ $wgGrantPermissions['editmycssjs']['editmyuserjson'] = true; $wgGrantPermissions['editmycssjs']['editmyuserjs'] = true; $wgGrantPermissions['editmyoptions']['editmyoptions'] = true; +$wgGrantPermissions['editmyoptions']['editmyuserjson'] = true; $wgGrantPermissions['editinterface'] = $wgGrantPermissions['editpage']; $wgGrantPermissions['editinterface']['editinterface'] = true; @@ -5900,6 +5885,8 @@ $wgGrantPermissions['delete']['deletelogentry'] = true; $wgGrantPermissions['delete']['deleterevision'] = true; $wgGrantPermissions['delete']['undelete'] = true; +$wgGrantPermissions['oversight']['suppressrevision'] = true; + $wgGrantPermissions['protect'] = $wgGrantPermissions['editprotected']; $wgGrantPermissions['protect']['protect'] = true; @@ -5945,6 +5932,7 @@ $wgGrantPermissionGroups = [ 'viewdeleted' => 'administration', 'viewrestrictedlogs' => 'administration', 'protect' => 'administration', + 'oversight' => 'administration', 'createaccount' => 'administration', 'highvolume' => 'high-volume', @@ -6182,7 +6170,6 @@ $wgTrxProfilerLimits = [ 'maxAffected' => 1000 ], 'POST-nonwrite' => [ - 'masterConns' => 0, 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000 @@ -7550,7 +7537,10 @@ $wgJobClasses = [ 'refreshLinksPrioritized' => RefreshLinksJob::class, 'refreshLinksDynamic' => RefreshLinksJob::class, 'activityUpdateJob' => ActivityUpdateJob::class, - 'categoryMembershipChange' => CategoryMembershipChangeJob::class, + 'categoryMembershipChange' => function ( Title $title, $params = [] ) { + $pc = MediaWikiServices::getInstance()->getParserCache(); + return new CategoryMembershipChangeJob( $pc, $title, $params ); + }, 'clearUserWatchlist' => ClearUserWatchlistJob::class, 'cdnPurge' => CdnPurgeJob::class, 'userGroupExpiry' => UserGroupExpiryJob::class, @@ -8599,6 +8589,9 @@ $wgUploadMaintenance = false; * defined for a given namespace, pages in that namespace will use the CONTENT_MODEL_WIKITEXT * (except for the special case of JS and CS pages). * + * @note To determine the default model for a new page's main slot, or any slot in general, + * use SlotRoleHandler::getDefaultModel() together with SlotRoleRegistry::getRoleHandler(). + * * @since 1.21 */ $wgNamespaceContentModels = []; @@ -8785,6 +8778,7 @@ $wgSearchRunSuggestedQuery = true; * * @see maintenance/createCommonPasswordCdb.php * @since 1.27 + * @deprecated since 1.33 * @var string path to file */ $wgPopularPasswordFile = __DIR__ . '/password/commonpasswords.cdb'; @@ -8916,6 +8910,9 @@ $wgCSPFalsePositiveUrls = [ 'https://rtb.metrigo.com' => true, 'https://d5p.de17a.com' => true, 'https://ad.lkqd.net/vpaid/vpaid.js' => true, + 'https://ad.lkqd.net/vpaid/vpaid.js?fusion=1.0' => true, + 'https://t.lkqd.net/t' => true, + 'chrome-extension' => true, ]; /** @@ -8971,7 +8968,7 @@ $wgInterwikiPrefixDisplayTypes = []; * @since 1.30 * @var int One of the MIGRATION_* constants */ -$wgCommentTableSchemaMigrationStage = MIGRATION_OLD; +$wgCommentTableSchemaMigrationStage = MIGRATION_NEW; /** * RevisionStore table schema migration stage (content, slots, content_models & slot_roles tables). @@ -9013,40 +9010,24 @@ $wgMultiContentRevisionSchemaMigrationStage = SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_ $wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_OLD; /** - * change_tag table schema migration stage. - * - * - MIGRATION_OLD: Do not use change_tag_def table or ct_tag_id. - * - MIGRATION_WRITE_BOTH: Write to the change_tag_def table and ct_tag_id, but read from - * the old schema. This is different from the formal definition of the constants - * - MIGRATION_WRITE_NEW: Behaves the same as MIGRATION_WRITE_BOTH - * - MIGRATION_NEW: Use the change_tag_def table and ct_tag_id, do not read/write ct_tag - * - * @since 1.32 - * @var int One of the MIGRATION_* constants - */ -$wgChangeTagsSchemaMigrationStage = MIGRATION_WRITE_BOTH; - -/** - * Temporarily flag to use change_tag_def table as backend of change tag statistics. - * For example in case of Special:Tags. If set to false, it will use change_tag table. - * Before setting it to true set $wgChangeTagsSchemaMigrationStage to MIGRATION_WRITE_BOTH and run - * PopulateChangeTagDef maintaince script. - * It's redundant when $wgChangeTagsSchemaMigrationStage is set to MIGRATION_NEW + * Flag to enable Partial Blocks. This allows an admin to prevent a user from editing specific pages + * or namespaces. * * @since 1.32 + * @deprecated 1.32 * @var bool */ -$wgTagStatisticsNewTable = false; +$wgEnablePartialBlocks = false; /** - * Flag to enable Partial Blocks. This allows an admin to prevent a user from editing specific pages - * or namespaces. + * Enable stats monitoring when Block Notices are displayed in different places around core + * and extensions. * - * @since 1.32 - * @deprecated 1.32 + * @since 1.34 + * @deprecated 1.34 * @var bool */ -$wgEnablePartialBlocks = false; +$wgEnableBlockNoticeStats = false; /** * For really cool vim folding this needs to be at the end: diff --git a/includes/DummyLinker.php b/includes/DummyLinker.php index 2f5455ef5a..ba1233e546 100644 --- a/includes/DummyLinker.php +++ b/includes/DummyLinker.php @@ -345,11 +345,11 @@ class DummyLinker { return Linker::tocLineEnd(); } - public function tocList( $toc, $lang = false ) { + public function tocList( $toc, $lang = null ) { return Linker::tocList( $toc, $lang ); } - public function generateTOC( $tree, $lang = false ) { + public function generateTOC( $tree, $lang = null ) { return Linker::generateTOC( $tree, $lang ); } diff --git a/includes/EditPage.php b/includes/EditPage.php index 0f7d9a7a39..45995646da 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -476,7 +476,15 @@ class EditPage { $this->mArticle = $article; $this->page = $article->getPage(); // model object $this->mTitle = $article->getTitle(); - $this->context = $article->getContext(); + + // Make sure the local context is in sync with other member variables. + // Particularly make sure everything is using the same WikiPage instance. + // This should probably be the case in Article as well, but it's + // particularly important for EditPage, to make use of the in-place caching + // facility in WikiPage::prepareContentForEdit. + $this->context = new DerivativeContext( $article->getContext() ); + $this->context->setWikiPage( $this->page ); + $this->context->setTitle( $this->mTitle ); $this->contentModel = $this->mTitle->getContentModel(); @@ -619,14 +627,23 @@ class EditPage { if ( $permErrors ) { wfDebug( __METHOD__ . ": User can't edit\n" ); - // track block with a cookie if it doesn't exists already - $this->context->getUser()->trackBlockWithCookie(); + if ( $this->context->getUser()->getBlock() ) { + // track block with a cookie if it doesn't exists already + $this->context->getUser()->trackBlockWithCookie(); - // Auto-block user's IP if the account was "hard" blocked - if ( !wfReadOnly() ) { - DeferredUpdates::addCallableUpdate( function () { - $this->context->getUser()->spreadAnyEditBlock(); - } ); + // Auto-block user's IP if the account was "hard" blocked + if ( !wfReadOnly() ) { + DeferredUpdates::addCallableUpdate( function () { + $this->context->getUser()->spreadAnyEditBlock(); + } ); + } + + $config = $this->context->getConfig(); + if ( $config->get( 'EnableBlockNoticeStats' ) ) { + $wiki = $config->get( 'DBname' ); + $statsd = MediaWikiServices::getInstance()->getStatsdDataFactory(); + $statsd->increment( 'BlockNotices.' . $wiki . '.WikitextEditor.shown' ); + } } $this->displayPermissionsError( $permErrors ); @@ -672,7 +689,7 @@ class EditPage { # that edit() already checked just in case someone tries to sneak # in the back door with a hand-edited submission URL. - if ( 'save' == $this->formtype ) { + if ( $this->formtype == 'save' ) { $resultDetails = null; $status = $this->attemptSave( $resultDetails ); if ( !$this->handleStatus( $status, $resultDetails ) ) { @@ -682,7 +699,7 @@ class EditPage { # First time through: get contents, set time for conflict # checking, etc. - if ( 'initial' == $this->formtype || $this->firsttime ) { + if ( $this->formtype == 'initial' || $this->firsttime ) { if ( $this->initialiseForm() === false ) { $out = $this->context->getOutput(); if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it @@ -1952,7 +1969,7 @@ ERROR; return $status; } - if ( $user->isBlockedFrom( $this->mTitle, false ) ) { + if ( $user->isBlockedFrom( $this->mTitle ) ) { // Auto-block user's IP if the account was "hard" blocked if ( !wfReadOnly() ) { $user->spreadAnyEditBlock(); @@ -2594,8 +2611,13 @@ ERROR; if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist $out->wrapWikiMsg( "

\n$1\n
", [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] ); - } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) { - # Show log extract if the user is currently blocked + } elseif ( + !is_null( $block ) && + $block->getType() != Block::TYPE_AUTO && + ( $block->isSitewide() || $user->isBlockedFrom( $this->mTitle ) ) + ) { + // Show log extract if the user is sitewide blocked or is partially + // blocked and not allowed to edit their user page or user talk page LogEventsList::showLogExtract( $out, 'block', @@ -2841,7 +2863,7 @@ ERROR; // Put these up at the top to ensure they aren't lost on early form submission $this->showFormBeforeText(); - if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) { + if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) { $username = $this->lastDelete->user_name; $comment = CommentStore::getStore() ->getComment( 'log_comment', $this->lastDelete )->text; @@ -3123,7 +3145,10 @@ ERROR; if ( !$revision->isCurrent() ) { $this->mArticle->setOldSubtitle( $revision->getId() ); - $out->addWikiMsg( 'editingold' ); + $out->wrapWikiMsg( + Html::warningBox( "\n$1\n" ), + 'editingold' + ); $this->isOldRev = true; } } elseif ( $this->mTitle->exists() ) { @@ -3142,16 +3167,22 @@ ERROR; ); } elseif ( $user->isAnon() ) { if ( $this->formtype != 'preview' ) { + $returntoquery = array_diff_key( + $this->context->getRequest()->getValues(), + [ 'title' => true, 'returnto' => true, 'returntoquery' => true ] + ); $out->wrapWikiMsg( "
\n$1\n
", [ 'anoneditwarning', // Log-in link SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [ - 'returnto' => $this->getTitle()->getPrefixedDBkey() + 'returnto' => $this->getTitle()->getPrefixedDBkey(), + 'returntoquery' => wfArrayToCgi( $returntoquery ), ] ), // Sign-up link SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [ - 'returnto' => $this->getTitle()->getPrefixedDBkey() + 'returnto' => $this->getTitle()->getPrefixedDBkey(), + 'returntoquery' => wfArrayToCgi( $returntoquery ), ] ) ] ); diff --git a/includes/ForeignResourceManager.php b/includes/ForeignResourceManager.php new file mode 100644 index 0000000000..d6175f6dba --- /dev/null +++ b/includes/ForeignResourceManager.php @@ -0,0 +1,327 @@ +registryFile = $registryFile; + $this->libDir = $libDir; + $this->infoPrinter = $infoPrinter ?? function () { + }; + $this->errorPrinter = $errorPrinter ?? $this->infoPrinter; + $this->verbosePrinter = $verbosePrinter ?? function () { + }; + + // Use a temporary directory under the destination directory instead + // of wfTempDir() because PHP's rename() does not work across file + // systems, as the user's /tmp and $IP may be on different filesystems. + $this->tmpParentDir = "{$this->libDir}/.tmp"; + } + + /** + * @return bool + * @throws Exception + */ + public function run( $action, $module ) { + if ( !in_array( $action, [ 'update', 'verify', 'make-sri' ] ) ) { + throw new Exception( 'Invalid action parameter.' ); + } + $this->action = $action; + + $registry = $this->parseBasicYaml( file_get_contents( $this->registryFile ) ); + if ( $module === 'all' ) { + $modules = $registry; + } elseif ( isset( $registry[ $module ] ) ) { + $modules = [ $module => $registry[ $module ] ]; + } else { + throw new Exception( 'Unknown module name.' ); + } + + foreach ( $modules as $moduleName => $info ) { + $this->verbose( "\n### {$moduleName}\n\n" ); + $destDir = "{$this->libDir}/$moduleName"; + + if ( $this->action === 'update' ) { + $this->output( "... updating '{$moduleName}'\n" ); + $this->verbose( "... emptying directory for $moduleName\n" ); + wfRecursiveRemoveDir( $destDir ); + } elseif ( $this->action === 'verify' ) { + $this->output( "... verifying '{$moduleName}'\n" ); + } else { + $this->output( "... checking '{$moduleName}'\n" ); + } + + $this->verbose( "... preparing {$this->tmpParentDir}\n" ); + wfRecursiveRemoveDir( $this->tmpParentDir ); + if ( !wfMkdirParents( $this->tmpParentDir ) ) { + throw new Exception( "Unable to create {$this->tmpParentDir}" ); + } + + if ( !isset( $info['type'] ) ) { + throw new Exception( "Module '$moduleName' must have a 'type' key." ); + } + switch ( $info['type'] ) { + case 'tar': + $this->handleTypeTar( $moduleName, $destDir, $info ); + break; + case 'file': + $this->handleTypeFile( $moduleName, $destDir, $info ); + break; + case 'multi-file': + $this->handleTypeMultiFile( $moduleName, $destDir, $info ); + break; + default: + throw new Exception( "Unknown type '{$info['type']}' for '$moduleName'" ); + } + } + + $this->cleanUp(); + $this->output( "\nDone!\n" ); + if ( $this->hasErrors ) { + // The verify mode should check all modules/files and fail after, not during. + return false; + } + + return true; + } + + private function fetch( $src, $integrity ) { + $data = Http::get( $src, [ 'followRedirects' => false ] ); + if ( $data === false ) { + throw new Exception( "Failed to download resource at {$src}" ); + } + $algo = $integrity === null ? $this->defaultAlgo : explode( '-', $integrity )[0]; + $actualIntegrity = $algo . '-' . base64_encode( hash( $algo, $data, true ) ); + if ( $integrity === $actualIntegrity ) { + $this->verbose( "... passed integrity check for {$src}\n" ); + } else { + if ( $this->action === 'make-sri' ) { + $this->output( "Integrity for {$src}\n\tintegrity: ${actualIntegrity}\n" ); + } else { + throw new Exception( "Integrity check failed for {$src}\n" . + "\tExpected: {$integrity}\n" . + "\tActual: {$actualIntegrity}" + ); + } + } + return $data; + } + + private function handleTypeFile( $moduleName, $destDir, array $info ) { + if ( !isset( $info['src'] ) ) { + throw new Exception( "Module '$moduleName' must have a 'src' key." ); + } + $data = $this->fetch( $info['src'], $info['integrity'] ?? null ); + $dest = $info['dest'] ?? basename( $info['src'] ); + $path = "$destDir/$dest"; + if ( $this->action === 'verify' && sha1_file( $path ) !== sha1( $data ) ) { + throw new Exception( "File for '$moduleName' is different." ); + } + if ( $this->action === 'update' ) { + wfMkdirParents( $destDir ); + file_put_contents( "$destDir/$dest", $data ); + } + } + + private function handleTypeMultiFile( $moduleName, $destDir, array $info ) { + if ( !isset( $info['files'] ) ) { + throw new Exception( "Module '$moduleName' must have a 'files' key." ); + } + foreach ( $info['files'] as $dest => $file ) { + if ( !isset( $file['src'] ) ) { + throw new Exception( "Module '$moduleName' file '$dest' must have a 'src' key." ); + } + $data = $this->fetch( $file['src'], $file['integrity'] ?? null ); + $path = "$destDir/$dest"; + if ( $this->action === 'verify' && sha1_file( $path ) !== sha1( $data ) ) { + throw new Exception( "File '$dest' for '$moduleName' is different." ); + } elseif ( $this->action === 'update' ) { + wfMkdirParents( $destDir ); + file_put_contents( "$destDir/$dest", $data ); + } + } + } + + private function handleTypeTar( $moduleName, $destDir, array $info ) { + $info += [ 'src' => null, 'integrity' => null, 'dest' => null ]; + if ( $info['src'] === null ) { + throw new Exception( "Module '$moduleName' must have a 'src' key." ); + } + // Download the resource to a temporary file and open it + $data = $this->fetch( $info['src'], $info['integrity' ] ); + $tmpFile = "{$this->tmpParentDir}/$moduleName.tar"; + $this->verbose( "... writing '$moduleName' src to $tmpFile\n" ); + file_put_contents( $tmpFile, $data ); + $p = new PharData( $tmpFile ); + $tmpDir = "{$this->tmpParentDir}/$moduleName"; + $p->extractTo( $tmpDir ); + unset( $data, $p ); + + if ( $info['dest'] === null ) { + // Default: Replace the entire directory + $toCopy = [ $tmpDir => $destDir ]; + } else { + // Expand and normalise the 'dest' entries + $toCopy = []; + foreach ( $info['dest'] as $fromSubPath => $toSubPath ) { + // Use glob() to expand wildcards and check existence + $fromPaths = glob( "{$tmpDir}/{$fromSubPath}", GLOB_BRACE ); + if ( !$fromPaths ) { + throw new Exception( "Path '$fromSubPath' of '$moduleName' not found." ); + } + foreach ( $fromPaths as $fromPath ) { + $toCopy[$fromPath] = $toSubPath === null + ? "$destDir/" . basename( $fromPath ) + : "$destDir/$toSubPath/" . basename( $fromPath ); + } + } + } + foreach ( $toCopy as $from => $to ) { + if ( $this->action === 'verify' ) { + $this->verbose( "... verifying $to\n" ); + if ( is_dir( $from ) ) { + $rii = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( + $from, + RecursiveDirectoryIterator::SKIP_DOTS + ) ); + foreach ( $rii as $file ) { + $remote = $file->getPathname(); + $local = strtr( $remote, [ $from => $to ] ); + if ( sha1_file( $remote ) !== sha1_file( $local ) ) { + $this->error( "File '$local' is different." ); + $this->hasErrors = true; + } + } + } elseif ( sha1_file( $from ) !== sha1_file( $to ) ) { + $this->error( "File '$to' is different." ); + $this->hasErrors = true; + } + } elseif ( $this->action === 'update' ) { + $this->verbose( "... moving $from to $to\n" ); + wfMkdirParents( dirname( $to ) ); + if ( !rename( $from, $to ) ) { + throw new Exception( "Could not move $from to $to." ); + } + } + } + } + + private function verbose( $text ) { + ( $this->verbosePrinter )( $text ); + } + + private function output( $text ) { + ( $this->infoPrinter )( $text ); + } + + private function error( $text ) { + ( $this->errorPrinter )( $text ); + } + + private function cleanUp() { + wfRecursiveRemoveDir( $this->tmpParentDir ); + } + + /** + * Basic YAML parser. + * + * Supports only string or object values, and 2 spaces indentation. + * + * @todo Just ship symfony/yaml. + * @param string $input + * @return array + */ + private function parseBasicYaml( $input ) { + $lines = explode( "\n", $input ); + $root = []; + $stack = [ &$root ]; + $prev = 0; + foreach ( $lines as $i => $text ) { + $line = $i + 1; + $trimmed = ltrim( $text, ' ' ); + if ( $trimmed === '' || $trimmed[0] === '#' ) { + continue; + } + $indent = strlen( $text ) - strlen( $trimmed ); + if ( $indent % 2 !== 0 ) { + throw new Exception( __METHOD__ . ": Odd indentation on line $line." ); + } + $depth = $indent === 0 ? 0 : ( $indent / 2 ); + if ( $depth < $prev ) { + // Close previous branches we can't re-enter + array_splice( $stack, $depth + 1 ); + } + if ( !array_key_exists( $depth, $stack ) ) { + throw new Exception( __METHOD__ . ": Too much indentation on line $line." ); + } + if ( strpos( $trimmed, ':' ) === false ) { + throw new Exception( __METHOD__ . ": Missing colon on line $line." ); + } + $dest =& $stack[ $depth ]; + if ( $dest === null ) { + // Promote from null to object + $dest = []; + } + list( $key, $val ) = explode( ':', $trimmed, 2 ); + $val = ltrim( $val, ' ' ); + if ( $val !== '' ) { + // Add string + $dest[ $key ] = $val; + } else { + // Add null (may become an object later) + $val = null; + $stack[] = &$val; + $dest[ $key ] = &$val; + } + $prev = $depth; + unset( $dest, $val ); + } + return $root; + } +} diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 6e95871885..a5f4def18f 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -266,7 +266,7 @@ function wfObjectToArray( $objOrArray, $recursive = true ) { } /** - * Get a random decimal value between 0 and 1, in a way + * Get a random decimal value in the domain of [0, 1), in a way * not likely to give duplicate values for any realistic * number of articles. * @@ -471,7 +471,7 @@ function wfAppendQuery( $url, $query ) { } // Add parameter - if ( false === strpos( $url, '?' ) ) { + if ( strpos( $url, '?' ) === false ) { $url .= '?'; } else { $url .= '&'; @@ -894,55 +894,13 @@ function wfExpandIRI( $url ) { /** * Make URL indexes, appropriate for the el_index field of externallinks. * + * @deprecated since 1.33, use LinkFilter::makeIndexes() instead * @param string $url * @return array */ function wfMakeUrlIndexes( $url ) { - $bits = wfParseUrl( $url ); - - // Reverse the labels in the hostname, convert to lower case - // For emails reverse domainpart only - if ( $bits['scheme'] == 'mailto' ) { - $mailparts = explode( '@', $bits['host'], 2 ); - if ( count( $mailparts ) === 2 ) { - $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); - } else { - // No domain specified, don't mangle it - $domainpart = ''; - } - $reversedHost = $domainpart . '@' . $mailparts[0]; - } else { - $reversedHost = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) ); - } - // Add an extra dot to the end - // Why? Is it in wrong place in mailto links? - if ( substr( $reversedHost, -1, 1 ) !== '.' ) { - $reversedHost .= '.'; - } - // Reconstruct the pseudo-URL - $prot = $bits['scheme']; - $index = $prot . $bits['delimiter'] . $reversedHost; - // Leave out user and password. Add the port, path, query and fragment - if ( isset( $bits['port'] ) ) { - $index .= ':' . $bits['port']; - } - if ( isset( $bits['path'] ) ) { - $index .= $bits['path']; - } else { - $index .= '/'; - } - if ( isset( $bits['query'] ) ) { - $index .= '?' . $bits['query']; - } - if ( isset( $bits['fragment'] ) ) { - $index .= '#' . $bits['fragment']; - } - - if ( $prot == '' ) { - return [ "http:$index", "https:$index" ]; - } else { - return [ $index ]; - } + wfDeprecated( __FUNCTION__, '1.33' ); + return LinkFilter::makeIndexes( $url ); } /** diff --git a/includes/Html.php b/includes/Html.php index aac492c921..0aea7eabc3 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -255,6 +255,12 @@ class Html { // consistency and better compression. $element = strtolower( $element ); + // Some people were abusing this by passing things like + // 'h1 id="foo" to $element, which we don't want. + if ( strpos( $element, ' ' ) !== false ) { + wfWarn( __METHOD__ . " given element name with space '$element'" ); + } + // Remove invalid input types if ( $element == 'input' ) { $validTypes = [ @@ -840,9 +846,14 @@ class Html { // Value is provided by user, the name shown is localized for the user. $options[$params['all']] = wfMessage( 'namespacesall' )->text(); } - // Add all namespaces as options (in the content language) - $options += - MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces(); + if ( $params['in-user-lang'] ?? false ) { + global $wgLang; + $lang = $wgLang; + } else { + $lang = MediaWikiServices::getInstance()->getContentLanguage(); + } + // Add all namespaces as options + $options += $lang->getFormattedNamespaces(); $optionsOut = []; // Filter out namespaces below 0 and massage labels @@ -855,8 +866,7 @@ class Html { // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)") $nsName = wfMessage( 'blanknamespace' )->text(); } elseif ( is_int( $nsId ) ) { - $nsName = MediaWikiServices::getInstance()->getContentLanguage()-> - convertNamespace( $nsId ); + $nsName = $lang->convertNamespace( $nsId ); } $optionsOut[$nsId] = $nsName; } diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php index 3b03f87976..ffb36e0ba1 100644 --- a/includes/LinkFilter.php +++ b/includes/LinkFilter.php @@ -32,6 +32,11 @@ use Wikimedia\Rdbms\LikeMatch; * Another cool thing to do would be a web interface for fast spam removal. */ class LinkFilter { + /** + * Increment this when makeIndexes output changes. It'll cause + * maintenance/refreshExternallinksIndex.php to run from update.php. + */ + const VERSION = 1; /** * Check whether $content contains a link to $filterEntry @@ -58,6 +63,7 @@ class LinkFilter { /** * Builds a regex pattern for $filterEntry. * + * @todo This doesn't match the rest of the functionality here. * @param string $filterEntry URL, if it begins with "*.", it'll be * replaced to match any subdomain * @param string $protocol 'http://' or 'https://' @@ -75,23 +81,231 @@ class LinkFilter { } /** - * Make an array to be used for calls to Database::buildLike(), which - * will match the specified string. There are several kinds of filter entry: - * *.domain.com - Produces http://com.domain.%, matches domain.com - * and www.domain.com - * domain.com - Produces http://com.domain./%, matches domain.com - * or domain.com/ but not www.domain.com - * *.domain.com/x - Produces http://com.domain.%/x%, matches - * www.domain.com/xy - * domain.com/x - Produces http://com.domain./x%, matches - * domain.com/xy but not www.domain.com/xy + * Indicate whether LinkFilter IDN support is available + * @since 1.33 + * @return bool + */ + public static function supportsIDN() { + return is_callable( 'idn_to_utf8' ) && defined( 'INTL_IDNA_VARIANT_UTS46' ); + } + + /** + * Canonicalize a hostname for el_index + * @param string $hose + * @return string + */ + private static function indexifyHost( $host ) { + // NOTE: If you change the output of this method, you'll probably have to increment self::VERSION! + + // Canonicalize. + $host = rawurldecode( $host ); + if ( $host !== '' && self::supportsIDN() ) { + // @todo Add a PHP fallback + $tmp = idn_to_utf8( $host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 ); + if ( $tmp !== false ) { + $host = $tmp; + } + } + $okChars = 'a-zA-Z0-9\\-._~!$&\'()*+,;='; + if ( StringUtils::isUtf8( $host ) ) { + // Save a little space by not percent-encoding valid UTF-8 bytes + $okChars .= '\x80-\xf4'; + } + $host = preg_replace_callback( + '<[^' . $okChars . ']>', + function ( $m ) { + return rawurlencode( $m[0] ); + }, + strtolower( $host ) + ); + + // IPv6? RFC 3986 syntax. + if ( preg_match( '/^\[([0-9a-f:*]+)\]$/', rawurldecode( $host ), $m ) ) { + $ip = $m[1]; + if ( IP::isValid( $ip ) ) { + return 'V6.' . implode( '.', explode( ':', IP::sanitizeIP( $ip ) ) ) . '.'; + } + if ( substr( $ip, -2 ) === ':*' ) { + $cutIp = substr( $ip, 0, -2 ); + if ( IP::isValid( "{$cutIp}::" ) ) { + // Wildcard IP doesn't contain "::", so multiple parts can be wild + $ct = count( explode( ':', $ip ) ) - 1; + return 'V6.' . + implode( '.', array_slice( explode( ':', IP::sanitizeIP( "{$cutIp}::" ) ), 0, $ct ) ) . + '.*.'; + } + if ( IP::isValid( "{$cutIp}:1" ) ) { + // Wildcard IP does contain "::", so only the last part is wild + return 'V6.' . + substr( implode( '.', explode( ':', IP::sanitizeIP( "{$cutIp}:1" ) ) ), 0, -1 ) . + '*.'; + } + } + } + + // Regularlize explicit specification of the DNS root. + // Browsers seem to do this for IPv4 literals too. + if ( substr( $host, -1 ) === '.' ) { + $host = substr( $host, 0, -1 ); + } + + // IPv4? + $b = '(?:0*25[0-5]|0*2[0-4][0-9]|0*1[0-9][0-9]|0*[0-9]?[0-9])'; + if ( preg_match( "/^(?:{$b}\.){3}{$b}$|^(?:{$b}\.){1,3}\*$/", $host ) ) { + return 'V4.' . implode( '.', array_map( function ( $v ) { + return $v === '*' ? $v : (int)$v; + }, explode( '.', $host ) ) ) . '.'; + } + + // Must be a host name. + return implode( '.', array_reverse( explode( '.', $host ) ) ) . '.'; + } + + /** + * Converts a URL into a format for el_index + * @since 1.33 + * @param string $url + * @return string[] Usually one entry, but might be two in case of + * protocol-relative URLs. Empty array on error. + */ + public static function makeIndexes( $url ) { + // NOTE: If you change the output of this method, you'll probably have to increment self::VERSION! + + // NOTE: refreshExternallinksIndex.php assumes that only protocol-relative URLs return more + // than one index, and that the indexes for protocol-relative URLs only vary in the "http://" + // versus "https://" prefix. If you change that, you'll likely need to update + // refreshExternallinksIndex.php accordingly. + + $bits = wfParseUrl( $url ); + if ( !$bits ) { + return []; + } + + // Reverse the labels in the hostname, convert to lower case, unless it's an IP. + // For emails turn it into "domain.reversed@localpart" + if ( $bits['scheme'] == 'mailto' ) { + $mailparts = explode( '@', $bits['host'], 2 ); + if ( count( $mailparts ) === 2 ) { + $domainpart = self::indexifyHost( $mailparts[1] ); + } else { + // No @, assume it's a local part with no domain + $domainpart = ''; + } + $bits['host'] = $domainpart . '@' . $mailparts[0]; + } else { + $bits['host'] = self::indexifyHost( $bits['host'] ); + } + + // Reconstruct the pseudo-URL + $index = $bits['scheme'] . $bits['delimiter'] . $bits['host']; + // Leave out user and password. Add the port, path, query and fragment + if ( isset( $bits['port'] ) ) { + $index .= ':' . $bits['port']; + } + if ( isset( $bits['path'] ) ) { + $index .= $bits['path']; + } else { + $index .= '/'; + } + if ( isset( $bits['query'] ) ) { + $index .= '?' . $bits['query']; + } + if ( isset( $bits['fragment'] ) ) { + $index .= '#' . $bits['fragment']; + } + + if ( $bits['scheme'] == '' ) { + return [ "http:$index", "https:$index" ]; + } else { + return [ $index ]; + } + } + + /** + * Return query conditions which will match the specified string. There are + * several kinds of filter entry: + * + * *.domain.com - Matches domain.com and www.domain.com + * domain.com - Matches domain.com or domain.com/ but not www.domain.com + * *.domain.com/x - Matches domain.com/xy or www.domain.com/xy. Also probably matches + * domain.com/foobar/xy due to limitations of LIKE syntax. + * domain.com/x - Matches domain.com/xy but not www.domain.com/xy + * 192.0.2.* - Matches any IP in 192.0.2.0/24. Can also have a path appended. + * [2001:db8::*] - Matches any IP in 2001:db8::/112. Can also have a path appended. + * [2001:db8:*] - Matches any IP in 2001:db8::/32. Can also have a path appended. + * foo@domain.com - With protocol 'mailto:', matches the email address foo@domain.com. + * *@domain.com - With protocol 'mailto:', matches any email address at domain.com, but + * not subdomains like foo@mail.domain.com * * Asterisks in any other location are considered invalid. * - * This function does the same as wfMakeUrlIndexes(), except it also takes care + * @since 1.33 + * @param string $filterEntry Filter entry, as described above + * @param array $options Options are: + * - protocol: (string) Protocol to query (default http://) + * - oneWildcard: (bool) Stop at the first wildcard (default false) + * - prefix: (string) Field prefix (default 'el'). The query will test + * fields '{$prefix}_index' and '{$prefix}_index_60' + * - db: (IDatabase|null) Database to use. + * @return array|bool Conditions to be used for the query (to be ANDed) or + * false on error. To determine if the query is constant on the + * el_index_60 field, check whether key 'el_index_60' is set. + */ + public static function getQueryConditions( $filterEntry, array $options = [] ) { + $options += [ + 'protocol' => 'http://', + 'oneWildcard' => false, + 'prefix' => 'el', + 'db' => null, + ]; + + // First, get the like array + $like = self::makeLikeArray( $filterEntry, $options['protocol'] ); + if ( $like === false ) { + return $like; + } + + // Get the constant prefix (i.e. everything up to the first wildcard) + $trimmedLike = self::keepOneWildcard( $like ); + if ( $options['oneWildcard'] ) { + $like = $trimmedLike; + } + if ( $trimmedLike[count( $trimmedLike ) - 1] instanceof LikeMatch ) { + array_pop( $trimmedLike ); + } + $index = implode( '', $trimmedLike ); + + $p = $options['prefix']; + $db = $options['db'] ?: wfGetDB( DB_REPLICA ); + + // Build the query + $l = strlen( $index ); + if ( $l >= 60 ) { + // The constant prefix is larger than el_index_60, so we can use a + // constant comparison. + return [ + "{$p}_index_60" => substr( $index, 0, 60 ), + "{$p}_index" . $db->buildLike( $like ), + ]; + } + + // The constant prefix is smaller than el_index_60, so we use a LIKE + // for a prefix search. + return [ + "{$p}_index_60" . $db->buildLike( [ $index, $db->anyString() ] ), + "{$p}_index" . $db->buildLike( $like ), + ]; + } + + /** + * Make an array to be used for calls to Database::buildLike(), which + * will match the specified string. + * + * This function does the same as LinkFilter::makeIndexes(), except it also takes care * of adding wildcards * - * @param string $filterEntry Domainparts + * @note You probably want self::getQueryConditions() instead + * @param string $filterEntry Filter entry, @see self::getQueryConditions() * @param string $protocol Protocol (default http://) * @return array|bool Array to be passed to Database::buildLike() or false on error */ @@ -100,38 +314,27 @@ class LinkFilter { $target = $protocol . $filterEntry; $bits = wfParseUrl( $target ); - - if ( $bits == false ) { - // Unknown protocol? + if ( !$bits ) { return false; } - if ( substr( $bits['host'], 0, 2 ) == '*.' ) { - $subdomains = true; - $bits['host'] = substr( $bits['host'], 2 ); - if ( $bits['host'] == '' ) { - // We don't want to make a clause that will match everything, - // that could be dangerous - return false; - } - } else { - $subdomains = false; - } - - // Reverse the labels in the hostname, convert to lower case - // For emails reverse domainpart only + $subdomains = false; if ( $bits['scheme'] === 'mailto' && strpos( $bits['host'], '@' ) ) { - // complete email address - $mailparts = explode( '@', $bits['host'] ); - $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); - $bits['host'] = $domainpart . '@' . $mailparts[0]; - } elseif ( $bits['scheme'] === 'mailto' ) { - // domainpart of email address only, do not add '.' - $bits['host'] = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) ); + // Email address with domain and non-empty local part + $mailparts = explode( '@', $bits['host'], 2 ); + $domainpart = self::indexifyHost( $mailparts[1] ); + if ( $mailparts[0] === '*' ) { + $subdomains = true; + $bits['host'] = $domainpart . '@'; + } else { + $bits['host'] = $domainpart . '@' . $mailparts[0]; + } } else { - $bits['host'] = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) ); - if ( substr( $bits['host'], -1, 1 ) !== '.' ) { - $bits['host'] .= '.'; + // Non-email, or email with only a domain part. + $bits['host'] = self::indexifyHost( $bits['host'] ); + if ( substr( $bits['host'], -3 ) === '.*.' ) { + $subdomains = true; + $bits['host'] = substr( $bits['host'], 0, -2 ); } } @@ -175,6 +378,7 @@ class LinkFilter { * Filters an array returned by makeLikeArray(), removing everything past first * pattern placeholder. * + * @note You probably want self::getQueryConditions() instead * @param array $arr Array to filter * @return array Filtered array */ diff --git a/includes/Linker.php b/includes/Linker.php index 1d1ad0696f..b605acd894 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -227,7 +227,7 @@ class Linker { */ private static function fnamePart( $url ) { $basename = strrchr( $url, '/' ); - if ( false === $basename ) { + if ( $basename === false ) { $basename = $url; } else { $basename = substr( $basename, 1 ); @@ -334,7 +334,7 @@ class Linker { $prefix = $postfix = ''; - if ( 'center' == $frameParams['align'] ) { + if ( $frameParams['align'] == 'center' ) { $prefix = '
'; $postfix = '
'; $frameParams['align'] = 'none'; @@ -916,7 +916,7 @@ class Linker { $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null ) { global $wgUser, $wgDisableAnonTalk, $wgLang; - $talkable = !( $wgDisableAnonTalk && 0 == $userId ); + $talkable = !( $wgDisableAnonTalk && $userId == 0 ); $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK ); $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId; @@ -1151,7 +1151,6 @@ class Linker { ); if ( $comment === null ) { - $link = ''; if ( $title ) { $section = $auto; # Remove links that a user may have manually put in the autosummary @@ -1160,6 +1159,10 @@ class Linker { $section = str_replace( '[[', '', $section ); $section = str_replace( ']]', '', $section ); + // We don't want any links in the auto text to be linked, but we still + // want to show any [[ ]] + $sectionText = str_replace( '[[', '[[', $auto ); + $section = substr( Parser::guessSectionNameFromStrippedText( $section ), 1 ); if ( $local ) { $sectionTitle = Title::makeTitleSafe( NS_MAIN, '', $section ); @@ -1168,9 +1171,10 @@ class Linker { $title->getDBkey(), $section ); } if ( $sectionTitle ) { - $link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' ); - } else { - $link = ''; + $auto = Linker::makeCommentLink( + $sectionTitle, $wgLang->getArrow() . $wgLang->getDirMark() . $sectionText, + $wikiId, 'noclasses' + ); } } if ( $pre ) { @@ -1181,10 +1185,11 @@ class Linker { # autocomment $postsep written summary (/* section */ summary) $auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped(); } - $auto = '' . $auto . ''; - $comment = $pre . $link . $wgLang->getDirMark() - . '' . $auto; - $append .= ''; + if ( $auto ) { + $auto = '' . $auto . ''; + $append .= ''; + } + $comment = $pre . $auto; } return $comment; }, @@ -1440,7 +1445,7 @@ class Linker { * @return string */ public static function commentBlock( - $comment, $title = null, $local = false, $wikiId = null + $comment, $title = null, $local = false, $wikiId = null, $useParentheses = true ) { // '*' used to be the comment inserted by the software way back // in antiquity in case none was provided, here for backwards @@ -1449,8 +1454,13 @@ class Linker { return ''; } else { $formatted = self::formatComment( $comment, $title, $local, $wikiId ); - $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped(); - return " $formatted"; + if ( $useParentheses ) { + $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped(); + $classNames = 'comment'; + } else { + $classNames = 'comment comment--without-parentheses'; + } + return " $formatted"; } } @@ -1462,9 +1472,12 @@ class Linker { * @param Revision $rev * @param bool $local Whether section links should refer to local page * @param bool $isPublic Show only if all users can see it + * @param bool $useParentheses (optional) Wrap comments in parentheses where needed * @return string HTML fragment */ - public static function revComment( Revision $rev, $local = false, $isPublic = false ) { + public static function revComment( Revision $rev, $local = false, $isPublic = false, + $useParentheses = true + ) { if ( $rev->getComment( Revision::RAW ) == "" ) { return ""; } @@ -1472,7 +1485,7 @@ class Linker { $block = " " . wfMessage( 'rev-deleted-comment' )->escaped() . ""; } elseif ( $rev->userCan( Revision::DELETED_COMMENT ) ) { $block = self::commentBlock( $rev->getComment( Revision::FOR_THIS_USER ), - $rev->getTitle(), $local ); + $rev->getTitle(), $local, null, $useParentheses ); } else { $block = " " . wfMessage( 'rev-deleted-comment' )->escaped() . ""; } @@ -1562,11 +1575,18 @@ class Linker { * * @since 1.16.3 * @param string $toc Html of the Table Of Contents - * @param string|Language|bool $lang Language for the toc title, defaults to user language + * @param string|Language|bool|null $lang Language for the toc title, defaults to user language. + * The types string and bool are deprecated. * @return string Full html of the TOC */ - public static function tocList( $toc, $lang = false ) { - $lang = wfGetLangObj( $lang ); + public static function tocList( $toc, $lang = null ) { + global $wgLang; + $lang = $lang ?? $wgLang; + if ( !is_object( $lang ) ) { + wfDeprecated( __METHOD__ . ' with type other than Language for $lang', '1.33' ); + $lang = wfGetLangObj( $lang ); + } + $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped(); return '
' @@ -1598,10 +1618,11 @@ class Linker { * * @since 1.16.3. $lang added in 1.17 * @param array $tree Return value of ParserOutput::getSections() - * @param string|Language|bool $lang Language for the toc title, defaults to user language + * @param string|Language|bool|null $lang Language for the toc title, defaults to user language. + * The types string and bool are deprecated. * @return string HTML fragment */ - public static function generateTOC( $tree, $lang = false ) { + public static function generateTOC( $tree, $lang = null ) { $toc = ''; $lastLevel = 0; foreach ( $tree as $section ) { diff --git a/includes/MWNamespace.php b/includes/MWNamespace.php index e03a29b80f..98e70bf9f7 100644 --- a/includes/MWNamespace.php +++ b/includes/MWNamespace.php @@ -19,6 +19,7 @@ * * @file */ +use MediaWiki\MediaWikiServices; /** * This is a utility class with only static functions @@ -462,13 +463,17 @@ class MWNamespace { * Get the default content model for a namespace * This does not mean that all pages in that namespace have the model * + * @note To determine the default model for a new page's main slot, or any slot in general, + * use SlotRoleHandler::getDefaultModel() together with SlotRoleRegistry::getRoleHandler(). + * * @since 1.21 * @param int $index Index to check * @return null|string Default model name for the given namespace, if set */ public static function getNamespaceContentModel( $index ) { - global $wgNamespaceContentModels; - return $wgNamespaceContentModels[$index] ?? null; + $config = MediaWikiServices::getInstance()->getMainConfig(); + $models = $config->get( 'NamespaceContentModels' ); + return $models[$index] ?? null; } /** diff --git a/includes/MagicWordFactory.php b/includes/MagicWordFactory.php index e62716d67e..4e9bfaf1f7 100644 --- a/includes/MagicWordFactory.php +++ b/includes/MagicWordFactory.php @@ -173,6 +173,7 @@ class MagicWordFactory { 'newsectionlink', 'nonewsectionlink', 'hiddencat', + 'expectunusedcategory', 'index', 'noindex', 'staticredirect', diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index 267b589ae8..f5a954dc8d 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -893,8 +893,7 @@ class MediaWiki { // Loosen DB query expectations since the HTTP client is unblocked $trxProfiler = Profiler::instance()->getTransactionProfiler(); - $trxProfiler->resetExpectations(); - $trxProfiler->setExpectations( + $trxProfiler->redefineExpectations( $this->context->getRequest()->hasSafeMethod() ? $this->config->get( 'TrxProfilerLimits' )['PostSend-GET'] : $this->config->get( 'TrxProfilerLimits' )['PostSend-POST'], diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index f3ca7d469e..0e36b22367 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -17,6 +17,7 @@ use MediaWiki\Http\HttpRequestFactory; use MediaWiki\Preferences\PreferencesFactory; use MediaWiki\Shell\CommandFactory; use MediaWiki\Revision\RevisionRenderer; +use MediaWiki\Revision\SlotRoleRegistry; use MediaWiki\Special\SpecialPageFactory; use MediaWiki\Storage\BlobStore; use MediaWiki\Storage\BlobStoreFactory; @@ -840,6 +841,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'SkinFactory' ); } + /** + * @since 1.33 + * @return SlotRoleRegistry + */ + public function getSlotRoleRegistry() { + return $this->getService( 'SlotRoleRegistry' ); + } + /** * @since 1.31 * @return NameTableStore diff --git a/includes/Message.php b/includes/Message.php index 3bd775537f..4049e114f3 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -289,7 +289,7 @@ class Message implements MessageSpecifier, Serializable { 'parameters' => $this->parameters, 'format' => $this->format, 'useDatabase' => $this->useDatabase, - 'title' => $this->title, + 'titlestr' => $this->title ? $this->title->getFullText() : null, ] ); } @@ -300,6 +300,10 @@ class Message implements MessageSpecifier, Serializable { */ public function unserialize( $serialized ) { $data = unserialize( $serialized ); + if ( !is_array( $data ) ) { + throw new InvalidArgumentException( __METHOD__ . ': Invalid serialized data' ); + } + $this->interface = $data['interface']; $this->key = $data['key']; $this->keysToTry = $data['keysToTry']; @@ -307,7 +311,15 @@ class Message implements MessageSpecifier, Serializable { $this->format = $data['format']; $this->useDatabase = $data['useDatabase']; $this->language = $data['language'] ? Language::factory( $data['language'] ) : false; - $this->title = $data['title']; + + if ( isset( $data['titlestr'] ) ) { + $this->title = Title::newFromText( $data['titlestr'] ); + } elseif ( isset( $data['title'] ) && $data['title'] instanceof Title ) { + // Old serializations from before December 2018 + $this->title = $data['title']; + } else { + $this->title = null; // Explicit for sanity + } } /** diff --git a/includes/MovePage.php b/includes/MovePage.php index 5213fc171d..7d27a277b4 100644 --- a/includes/MovePage.php +++ b/includes/MovePage.php @@ -20,6 +20,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\Revision\SlotRecord; /** * Handles the backend logic of moving a page from one title @@ -137,7 +138,8 @@ class MovePage { $status->fatal( 'content-not-allowed-here', ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ), - $this->newTitle->getPrefixedText() + $this->newTitle->getPrefixedText(), + SlotRecord::MAIN ); } @@ -240,25 +242,14 @@ class MovePage { public function move( User $user, $reason, $createRedirect, array $changeTags = [] ) { global $wgCategoryCollation; - Hooks::run( 'TitleMove', [ $this->oldTitle, $this->newTitle, $user ] ); - - // If it is a file, move it first. - // It is done before all other moving stuff is done because it's hard to revert. - $dbw = wfGetDB( DB_MASTER ); - if ( $this->oldTitle->getNamespace() == NS_FILE ) { - $file = wfLocalFile( $this->oldTitle ); - $file->load( File::READ_LATEST ); - if ( $file->exists() ) { - $status = $file->move( $this->newTitle ); - if ( !$status->isOK() ) { - return $status; - } - } - // Clear RepoGroup process cache - RepoGroup::singleton()->clearCache( $this->oldTitle ); - RepoGroup::singleton()->clearCache( $this->newTitle ); # clear false negative cache + $status = Status::newGood(); + Hooks::run( 'TitleMove', [ $this->oldTitle, $this->newTitle, $user, $reason, &$status ] ); + if ( !$status->isOK() ) { + // Move was aborted by the hook + return $status; } + $dbw = wfGetDB( DB_MASTER ); $dbw->startAtomic( __METHOD__ ); Hooks::run( 'TitleMoveStarting', [ $this->oldTitle, $this->newTitle, $user ] ); @@ -387,6 +378,16 @@ class MovePage { $store->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle ); } + // If it is a file then move it last. + // This is done after all database changes so that file system errors cancel the transaction. + if ( $this->oldTitle->getNamespace() == NS_FILE ) { + $status = $this->moveFile( $this->oldTitle, $this->newTitle ); + if ( !$status->isOK() ) { + $dbw->cancelAtomic( __METHOD__ ); + return $status; + } + } + Hooks::run( 'TitleMoveCompleting', [ $this->oldTitle, $this->newTitle, @@ -420,6 +421,33 @@ class MovePage { return Status::newGood(); } + /** + * Move a file associated with a page to a new location. + * Can also be used to revert after a DB failure. + * + * @access private + * @param Title Old location to move the file from. + * @param Title New location to move the file to. + * @return Status + */ + private function moveFile( $oldTitle, $newTitle ) { + $status = Status::newFatal( + 'cannotdelete', + $oldTitle->getPrefixedText() + ); + + $file = wfLocalFile( $oldTitle ); + $file->load( File::READ_LATEST ); + if ( $file->exists() ) { + $status = $file->move( $newTitle ); + } + + // Clear RepoGroup process cache + RepoGroup::singleton()->clearCache( $oldTitle ); + RepoGroup::singleton()->clearCache( $newTitle ); # clear false negative cache + return $status; + } + /** * Move page to a title which is either a redirect to the * source page or nonexistent diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 4a9b542c3a..02e13e74fe 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -2098,6 +2098,7 @@ class OutputPage extends ContextSource { * parseAsInterface() if $interface is true. */ public function parse( $text, $linestart = true, $interface = false, $language = null ) { + wfDeprecated( __METHOD__, '1.33' ); return $this->parseInternal( $text, $this->getTitle(), $linestart, /*tidy*/false, $interface, $language )->getText( [ @@ -2180,6 +2181,7 @@ class OutputPage extends ContextSource { * Parser::stripOuterParagraph($outputPage->parseAsContent(...)). */ public function parseInline( $text, $linestart = true, $interface = false ) { + wfDeprecated( __METHOD__, '1.33' ); $parsed = $this->parseInternal( $text, $this->getTitle(), $linestart, /*tidy*/false, $interface, /*language*/null )->getText( [ @@ -2845,16 +2847,18 @@ class OutputPage extends ContextSource { $query['returntoquery'] = wfArrayToCgi( $returntoquery ); } } + $title = SpecialPage::getTitleFor( 'Userlogin' ); $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + $loginUrl = $title->getLinkURL( $query, false, PROTO_RELATIVE ); $loginLink = $linkRenderer->makeKnownLink( - SpecialPage::getTitleFor( 'Userlogin' ), + $title, $this->msg( 'loginreqlink' )->text(), [], $query ); $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) ); - $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->parse() ); + $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->params( $loginUrl )->parse() ); # Don't return to a page the user can't read otherwise # we'll end up in a pointless loop @@ -2924,7 +2928,7 @@ class OutputPage extends ContextSource { * then the warning is a bit more obvious. If the lag is * lower than $wgSlaveLagWarning, then no warning is shown. * - * @param int $lag Slave lag + * @param int $lag Replica lag */ public function showLagWarning( $lag ) { $config = $this->getConfig(); diff --git a/includes/Preferences.php b/includes/Preferences.php index 6d379dcb49..70f70609ae 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -46,13 +46,6 @@ class Preferences { return $preferencesFactory; } - /** - * @return array - */ - public static function getSaveBlacklist() { - throw new Exception( __METHOD__ . '() is deprecated and does nothing' ); - } - /** * @throws MWException * @param User $user @@ -60,6 +53,7 @@ class Preferences { * @return array|null */ public static function getPreferences( $user, IContextSource $context ) { + wfDeprecated( __METHOD__, '1.31' ); $preferencesFactory = self::getDefaultPreferencesFactory(); return $preferencesFactory->getFormDescriptor( $user, $context ); } @@ -202,6 +196,7 @@ class Preferences { * @param array &$defaultPreferences */ public static function miscPreferences( $user, IContextSource $context, &$defaultPreferences ) { + wfDeprecated( __METHOD__, '1.31' ); } /** @@ -271,45 +266,8 @@ class Preferences { $formClass = PreferencesFormLegacy::class, array $remove = [] ) { + wfDeprecated( __METHOD__, '1.31' ); $preferencesFactory = self::getDefaultPreferencesFactory(); return $preferencesFactory->getForm( $user, $context, $formClass, $remove ); } - - /** - * @param IContextSource $context - * @return array - */ - public static function getTimezoneOptions( IContextSource $context ) { - throw new Exception( __METHOD__ . '() is deprecated and does nothing' ); - } - - /** - * @param string $value - * @param array $alldata - * @return int - */ - public static function filterIntval( $value, $alldata ) { - throw new Exception( __METHOD__ . '() is deprecated and does nothing' ); - } - - /** - * @param string $tz - * @param array $alldata - * @return string - */ - public static function filterTimezoneInput( $tz, $alldata ) { - throw new Exception( __METHOD__ . '() is deprecated and does nothing' ); - } - - /** - * 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 - */ - public static function getTimeZoneList( Language $language ) { - throw new Exception( __METHOD__ . '() is deprecated and does nothing' ); - } } diff --git a/includes/Revision.php b/includes/Revision.php index 6d1812a98e..b0a3ba35f2 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -29,6 +29,7 @@ use MediaWiki\Revision\RevisionStore; use MediaWiki\Revision\RevisionStoreRecord; use MediaWiki\Revision\SlotRecord; use MediaWiki\Storage\SqlBlobStore; +use Wikimedia\Assert\Assert; use Wikimedia\Rdbms\IDatabase; use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; @@ -589,6 +590,8 @@ class Revision implements IDBAccessObject { '$row must be a row object, an associative array, or a RevisionRecord' ); } + + Assert::postcondition( $this->mRecord !== null, 'Failed to construct a RevisionRecord' ); } /** @@ -1180,9 +1183,7 @@ class Revision implements IDBAccessObject { $rec = self::getRevisionStore()->insertRevisionOn( $this->mRecord, $dbw ); $this->mRecord = $rec; - - // Avoid PHP 7.1 warning of passing $this by reference - $revision = $this; + Assert::postcondition( $this->mRecord !== null, 'Failed to acquire a RevisionRecord' ); return $rec->getId(); } diff --git a/includes/Revision/FallbackSlotRoleHandler.php b/includes/Revision/FallbackSlotRoleHandler.php new file mode 100644 index 0000000000..78dfd39861 --- /dev/null +++ b/includes/Revision/FallbackSlotRoleHandler.php @@ -0,0 +1,71 @@ + 'none'] here, causing undefined slots + // to be hidden? We'd still need some place to surface the content of such + // slots, see T209923. + + return parent::getOutputLayoutHints(); // TODO: Change the autogenerated stub + } + +} diff --git a/includes/Revision/MainSlotRoleHandler.php b/includes/Revision/MainSlotRoleHandler.php new file mode 100644 index 0000000000..6c6fdd6d05 --- /dev/null +++ b/includes/Revision/MainSlotRoleHandler.php @@ -0,0 +1,132 @@ +namespaceContentModels = $namespaceContentModels; + } + + public function supportsArticleCount() { + return true; + } + + /** + * @param string $model + * @param LinkTarget $page + * + * @return bool + */ + public function isAllowedModel( $model, LinkTarget $page ) { + $title = Title::newFromLinkTarget( $page ); + $handler = ContentHandler::getForModelID( $model ); + return $handler->canBeUsedOn( $title ); + } + + /** + * @param LinkTarget $page + * + * @return string + */ + public function getDefaultModel( LinkTarget $page ) { + // NOTE: this method must not rely on $title->getContentModel() directly or indirectly, + // because it is used to initialize the mContentModel member. + + $ext = ''; + $ns = $page->getNamespace(); + $model = $this->namespaceContentModels[$ns] ?? null; + + // Hook can determine default model + $title = Title::newFromLinkTarget( $page ); + if ( !Hooks::run( 'ContentHandlerDefaultModelFor', [ $title, &$model ] ) ) { + if ( !is_null( $model ) ) { + return $model; + } + } + + // Could this page contain code based on the title? + $isCodePage = $ns === NS_MEDIAWIKI && preg_match( '!\.(css|js|json)$!u', $title->getText(), $m ); + if ( $isCodePage ) { + $ext = $m[1]; + } + + // Is this a user subpage containing code? + $isCodeSubpage = $ns === NS_USER + && !$isCodePage + && preg_match( "/\\/.*\\.(js|css|json)$/", $title->getText(), $m ); + + if ( $isCodeSubpage ) { + $ext = $m[1]; + } + + // Is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook? + $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT; + $isWikitext = $isWikitext && !$isCodePage && !$isCodeSubpage; + + if ( !$isWikitext ) { + switch ( $ext ) { + case 'js': + return CONTENT_MODEL_JAVASCRIPT; + case 'css': + return CONTENT_MODEL_CSS; + case 'json': + return CONTENT_MODEL_JSON; + default: + return is_null( $model ) ? CONTENT_MODEL_TEXT : $model; + } + } + + // We established that it must be wikitext + + return CONTENT_MODEL_WIKITEXT; + } + +} diff --git a/includes/Revision/RenderedRevision.php b/includes/Revision/RenderedRevision.php index 6eee3c4cf6..094105aae8 100644 --- a/includes/Revision/RenderedRevision.php +++ b/includes/Revision/RenderedRevision.php @@ -159,6 +159,28 @@ class RenderedRevision implements SlotRenderingProvider { return $this->options; } + /** + * Sets a ParserOutput to be returned by getRevisionParserOutput(). + * + * @note For internal use by RevisionRenderer only! This method may be modified + * or removed without notice per the deprecation policy. + * + * @internal + * + * @param ParserOutput $output + */ + public function setRevisionParserOutput( ParserOutput $output ) { + $this->revisionOutput = $output; + + // If there is only one slot, we assume that the combined output is identical + // with the main slot's output. This is intended to prevent a redundant re-parse of + // the content in case getSlotParserOutput( SlotRecord::MAIN ) is called, for instance + // from ContentHandler::getSecondaryDataUpdates. + if ( $this->revision->getSlotRoles() === [ SlotRecord::MAIN ] ) { + $this->slotsOutput[ SlotRecord::MAIN ] = $output; + } + } + /** * @param array $hints Hints given as an associative array. Known keys: * - 'generate-html' => bool: Whether the caller is interested in output HTML (as opposed @@ -208,6 +230,7 @@ class RenderedRevision implements SlotRenderingProvider { 'Access to the content has been suppressed for this audience' ); } else { + // XXX: allow SlotRoleHandler to control the ParserOutput? $output = $this->getSlotParserOutputUncached( $content, $withHtml ); if ( $withHtml && !$output->hasText() ) { diff --git a/includes/Revision/RevisionRenderer.php b/includes/Revision/RevisionRenderer.php index e2e84b60ca..f97390ad49 100644 --- a/includes/Revision/RevisionRenderer.php +++ b/includes/Revision/RevisionRenderer.php @@ -50,15 +50,24 @@ class RevisionRenderer { /** @var ILoadBalancer */ private $loadBalancer; + /** @var SlotRoleRegistry */ + private $roleRegistery; + /** @var string|bool */ private $wikiId; /** * @param ILoadBalancer $loadBalancer + * @param SlotRoleRegistry $roleRegistry * @param bool|string $wikiId */ - public function __construct( ILoadBalancer $loadBalancer, $wikiId = false ) { + public function __construct( + ILoadBalancer $loadBalancer, + SlotRoleRegistry $roleRegistry, + $wikiId = false + ) { $this->loadBalancer = $loadBalancer; + $this->roleRegistery = $roleRegistry; $this->wikiId = $wikiId; $this->saveParseLogger = new NullLogger(); @@ -82,6 +91,11 @@ class RevisionRenderer { * - 'audience' the audience to use for content access. Default is * RevisionRecord::FOR_PUBLIC if $forUser is not set, RevisionRecord::FOR_THIS_USER * if $forUser is set. Can be set to RevisionRecord::RAW to disable audience checks. + * - 'known-revision-output' a combined ParserOutput for the revision, perhaps from + * some cache. the caller is responsible for ensuring that the ParserOutput indeed + * matched the $rev and $options. This mechanism is intended as a temporary stop-gap, + * for the time until caches have been changed to store RenderedRevision states instead + * of ParserOutput objects. * * @return RenderedRevision|null The rendered revision, or null if the audience checks fails. */ @@ -133,6 +147,10 @@ class RevisionRenderer { $renderedRevision->setSaveParseLogger( $this->saveParseLogger ); + if ( isset( $hints['known-revision-output'] ) ) { + $renderedRevision->setRevisionParserOutput( $hints['known-revision-output'] ); + } + return $renderedRevision; } @@ -175,8 +193,6 @@ class RevisionRenderer { return $rrev->getSlotParserOutput( SlotRecord::MAIN ); } - // TODO: put fancy layout logic here, see T200915. - // move main slot to front if ( isset( $slots[SlotRecord::MAIN] ) ) { $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots; @@ -192,6 +208,7 @@ class RevisionRenderer { $out = $rrev->getSlotParserOutput( $role, $hints ); $slotOutput[$role] = $out; + // XXX: should the SlotRoleHandler be able to intervene here? $combinedOutput->mergeInternalMetaDataFrom( $out, $role ); $combinedOutput->mergeTrackingMetaDataFrom( $out ); } @@ -201,6 +218,16 @@ class RevisionRenderer { $first = true; /** @var ParserOutput $out */ foreach ( $slotOutput as $role => $out ) { + $roleHandler = $this->roleRegistery->getRoleHandler( $role ); + + // TODO: put more fancy layout logic here, see T200915. + $layout = $roleHandler->getOutputLayoutHints(); + $display = $layout['display'] ?? 'section'; + + if ( $display === 'none' ) { + continue; + } + if ( $first ) { // skip header for the first slot $first = false; @@ -210,6 +237,8 @@ class RevisionRenderer { $html .= Html::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText ); } + // XXX: do we want to put a wrapper div around the output? + // Do we want to let $roleHandler do that? $html .= $out->getRawText(); $combinedOutput->mergeHtmlMetaDataFrom( $out ); } diff --git a/includes/Revision/RevisionStore.php b/includes/Revision/RevisionStore.php index 6d3b72cdf9..cf19ffbc47 100644 --- a/includes/Revision/RevisionStore.php +++ b/includes/Revision/RevisionStore.php @@ -132,6 +132,9 @@ class RevisionStore /** @var int An appropriate combination of SCHEMA_COMPAT_XXX flags. */ private $mcrMigrationStage; + /** @var SlotRoleRegistry */ + private $slotRoleRegistry; + /** * @todo $blobStore should be allowed to be any BlobStore! * @@ -146,11 +149,11 @@ class RevisionStore * @param CommentStore $commentStore * @param NameTableStore $contentModelStore * @param NameTableStore $slotRoleStore + * @param SlotRoleRegistry $slotRoleRegistry * @param int $mcrMigrationStage An appropriate combination of SCHEMA_COMPAT_XXX flags * @param ActorMigration $actorMigration * @param bool|string $wikiId * - * @throws MWException if $mcrMigrationStage or $wikiId is invalid. */ public function __construct( ILoadBalancer $loadBalancer, @@ -159,6 +162,7 @@ class RevisionStore CommentStore $commentStore, NameTableStore $contentModelStore, NameTableStore $slotRoleStore, + SlotRoleRegistry $slotRoleRegistry, $mcrMigrationStage, ActorMigration $actorMigration, $wikiId = false @@ -199,6 +203,7 @@ class RevisionStore $this->commentStore = $commentStore; $this->contentModelStore = $contentModelStore; $this->slotRoleStore = $slotRoleStore; + $this->slotRoleRegistry = $slotRoleRegistry; $this->mcrMigrationStage = $mcrMigrationStage; $this->actorMigration = $actorMigration; $this->wikiId = $wikiId; @@ -923,7 +928,7 @@ class RevisionStore $format = $content->getDefaultFormat(); $model = $content->getModel(); - $this->checkContent( $content, $title ); + $this->checkContent( $content, $title, $slot->getRole() ); return $this->blobStore->storeBlob( $content->serialize( $format ), @@ -982,11 +987,12 @@ class RevisionStore * * @param Content $content * @param Title $title + * @param string $role * * @throws MWException * @throws MWUnknownContentModelException */ - private function checkContent( Content $content, Title $title ) { + private function checkContent( Content $content, Title $title, $role ) { // Note: may return null for revisions that have not yet been inserted $model = $content->getModel(); @@ -1005,7 +1011,8 @@ class RevisionStore $this->assertCrossWikiContentLoadingIsSafe(); - $defaultModel = ContentHandler::getDefaultModelFor( $title ); + $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role ); + $defaultModel = $roleHandler->getDefaultModel( $title ); $defaultHandler = ContentHandler::getForModelID( $defaultModel ); $defaultFormat = $defaultHandler->getDefaultFormat(); @@ -1350,9 +1357,8 @@ class RevisionStore $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) { $this->assertCrossWikiContentLoadingIsSafe(); - // TODO: MCR: consider slot role in getDefaultModelFor()! Use LinkTarget! - // TODO: MCR: deprecate $title->getModel(). - return ContentHandler::getDefaultModelFor( $title ); + return $this->slotRoleRegistry->getRoleHandler( $slot->getRole() ) + ->getDefaultModel( $title ); }; } @@ -1624,7 +1630,7 @@ class RevisionStore $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id ); $row->model_name = $this->contentModelStore->getName( (int)$row->content_model ); - $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags, $row ) { + $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags ) { return $this->loadSlotContent( $slot, null, null, null, $queryFlags ); }; @@ -2517,45 +2523,78 @@ class RevisionStore } /** - * Get previous revision for this title + * Get the revision before $rev in the page's history, if any. + * Will return null for the first revision but also for deleted or unsaved revisions. * * MCR migration note: this replaces Revision::getPrevious * + * @see Title::getPreviousRevisionID + * @see PageArchive::getPreviousRevision + * * @param RevisionRecord $rev * @param Title|null $title if known (optional) * * @return RevisionRecord|null */ public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) { + if ( !$rev->getId() || !$rev->getPageId() ) { + // revision is unsaved or otherwise incomplete + return null; + } + + if ( $rev instanceof RevisionArchiveRecord ) { + // revision is deleted, so it's not part of the page history + return null; + } + if ( $title === null ) { + // this would fail for deleted revisions $title = $this->getTitle( $rev->getPageId(), $rev->getId() ); } + $prev = $title->getPreviousRevisionID( $rev->getId() ); - if ( $prev ) { - return $this->getRevisionByTitle( $title, $prev ); + if ( !$prev ) { + return null; } - return null; + + return $this->getRevisionByTitle( $title, $prev ); } /** - * Get next revision for this title + * Get the revision after $rev in the page's history, if any. + * Will return null for the latest revision but also for deleted or unsaved revisions. * * MCR migration note: this replaces Revision::getNext * + * @see Title::getNextRevisionID + * * @param RevisionRecord $rev * @param Title|null $title if known (optional) * * @return RevisionRecord|null */ public function getNextRevision( RevisionRecord $rev, Title $title = null ) { + if ( !$rev->getId() || !$rev->getPageId() ) { + // revision is unsaved or otherwise incomplete + return null; + } + + if ( $rev instanceof RevisionArchiveRecord ) { + // revision is deleted, so it's not part of the page history + return null; + } + if ( $title === null ) { + // this would fail for deleted revisions $title = $this->getTitle( $rev->getPageId(), $rev->getId() ); } + $next = $title->getNextRevisionID( $rev->getId() ); - if ( $next ) { - return $this->getRevisionByTitle( $title, $next ); + if ( !$next ) { + return null; } - return null; + + return $this->getRevisionByTitle( $title, $next ); } /** diff --git a/includes/Revision/RevisionStoreFactory.php b/includes/Revision/RevisionStoreFactory.php index 30ffc997ef..6b3117fc78 100644 --- a/includes/Revision/RevisionStoreFactory.php +++ b/includes/Revision/RevisionStoreFactory.php @@ -72,10 +72,14 @@ class RevisionStoreFactory { /** @var NameTableStoreFactory */ private $nameTables; + /** @var SlotRoleRegistry */ + private $slotRoleRegistry; + /** * @param ILBFactory $dbLoadBalancerFactory * @param BlobStoreFactory $blobStoreFactory * @param NameTableStoreFactory $nameTables + * @param SlotRoleRegistry $slotRoleRegistry * @param WANObjectCache $cache * @param CommentStore $commentStore * @param ActorMigration $actorMigration @@ -88,6 +92,7 @@ class RevisionStoreFactory { ILBFactory $dbLoadBalancerFactory, BlobStoreFactory $blobStoreFactory, NameTableStoreFactory $nameTables, + SlotRoleRegistry $slotRoleRegistry, WANObjectCache $cache, CommentStore $commentStore, ActorMigration $actorMigration, @@ -98,6 +103,7 @@ class RevisionStoreFactory { Assert::parameterType( 'integer', $migrationStage, '$migrationStage' ); $this->dbLoadBalancerFactory = $dbLoadBalancerFactory; $this->blobStoreFactory = $blobStoreFactory; + $this->slotRoleRegistry = $slotRoleRegistry; $this->nameTables = $nameTables; $this->cache = $cache; $this->commentStore = $commentStore; @@ -124,6 +130,7 @@ class RevisionStoreFactory { $this->commentStore, $this->nameTables->getContentModels( $wikiId ), $this->nameTables->getSlotRoles( $wikiId ), + $this->slotRoleRegistry, $this->mcrMigrationStage, $this->actorMigration, $wikiId diff --git a/includes/Revision/SlotRoleHandler.php b/includes/Revision/SlotRoleHandler.php new file mode 100644 index 0000000000..85b4c5ab34 --- /dev/null +++ b/includes/Revision/SlotRoleHandler.php @@ -0,0 +1,159 @@ + 'section', // use 'none' to suppress + 'region' => 'center', + 'placement' => 'append' + ]; + + /** + * @var string + */ + private $contentModel; + + /** + * @param string $role The name of the slot role defined by this SlotRoleHandler. See + * SlotRoleRegistry::defineRole for more information. + * @param string $contentModel The default content model for this slot. As per the default + * implementation of isAllowedModel(), also the only content model allowed for the + * slot. Subclasses may however handle default and allowed models differently. + * @param array $layout Layout hints, for use by RevisionRenderer. See getOutputLayoutHints. + */ + public function __construct( $role, $contentModel, $layout = [] ) { + $this->role = $role; + $this->contentModel = $contentModel; + $this->layout = array_merge( $this->layout, $layout ); + } + + /** + * @return string The role this SlotRoleHandler applies to + */ + public function getRole() { + return $this->role; + } + + /** + * Layout hints for use while laying out the combined output of all slots, typically by + * RevisionRenderer. The layout hints are given as an associative array. Well-known keys + * to use: + * + * * "display": how the output of this slot should be represented. Supported values: + * - "section": show as a top level section of the region. + * - "none": do not show at all + * Further values that may be supported in the future include "box" and "banner". + * * "region": in which region of the page the output should be placed. Supported values: + * - "center": the central content area. + * Further values that may be supported in the future include "top" and "bottom", "left" + * and "right", "header" and "footer". + * * "placement": placement relative to other content of the same area. + * - "append": place at the end, after any output processed previously. + * Further values that may be supported in the future include "prepend". A "weight" key + * may be introduced for more fine grained control. + * + * @return array an associative array of hints + */ + public function getOutputLayoutHints() { + return $this->layout; + } + + /** + * The message key for the translation of the slot name. + * + * @return string + */ + public function getNameMessageKey() { + return 'slot-name-' . $this->role; + } + + /** + * Determines the content model to use per default for this slot on the given page. + * + * The default implementation always returns the content model provided to the constructor. + * Subclasses may base the choice on default model on the page title or namespace. + * The choice should not depend on external state, such as the page content. + * + * @param LinkTarget $page + * + * @return string + */ + public function getDefaultModel( LinkTarget $page ) { + return $this->contentModel; + } + + /** + * Determines whether the given model can be used on this slot on the given page. + * + * The default implementation checks whether $model is the content model provided to the + * constructor. Subclasses may allow other models and may base the decision on the page title + * or namespace. The choice should not depend on external state, such as the page content. + * + * @note This should be checked when creating new revisions. Existing revisions + * are not guaranteed to comply with the return value. + * + * @param string $model + * @param LinkTarget $page + * + * @return bool + */ + public function isAllowedModel( $model, LinkTarget $page ) { + return ( $model === $this->contentModel ); + } + + /** + * Whether this slot should be considered when determining whether a page should be counted + * as an "article" in the site statistics. + * + * For a page to be considered countable, one of the page's slots must return true from this + * method, and Content::isCountable() must return true for the content of that slot. + * + * The default implementation always returns false. + * + * @return string + */ + public function supportsArticleCount() { + return false; + } + +} diff --git a/includes/Revision/SlotRoleRegistry.php b/includes/Revision/SlotRoleRegistry.php new file mode 100644 index 0000000000..b108b98d43 --- /dev/null +++ b/includes/Revision/SlotRoleRegistry.php @@ -0,0 +1,236 @@ +roleNamesStore = $roleNamesStore; + } + + /** + * Defines a slot role. + * + * For use by extensions that wish to define roles beyond the main slot role. + * + * @see defineRoleWithModel() + * + * @param string $role The role name of the slot to define. This should follow the + * same convention as message keys: + * @param callable $instantiator called with $role as a parameter; + * Signature: function ( string $role ): SlotRoleHandler + */ + public function defineRole( $role, callable $instantiator ) { + if ( $this->isDefinedRole( $role ) ) { + throw new LogicException( "Role $role is already defined" ); + } + + $this->instantiators[$role] = $instantiator; + } + + /** + * Defines a slot role that allows only the given content model, and has no special + * behavior. + * + * For use by extensions that wish to define roles beyond the main slot role, but have + * no need to implement any special behavior for that slot. + * + * @see defineRole() + * + * @param string $role The role name of the slot to define, see defineRole() + * for more information. + * @param string $model A content model name, see ContentHandler + * @param array $layout See SlotRoleHandler getOutputLayoutHints + */ + public function defineRoleWithModel( $role, $model, $layout = [] ) { + $this->defineRole( + $role, + function ( $role ) use ( $model, $layout ) { + return new SlotRoleHandler( $role, $model, $layout ); + } + ); + } + + /** + * Gets the SlotRoleHandler that should be used when processing content of the given role. + * + * @param string $role + * + * @throws InvalidArgumentException If $role is not a known slot role. + * @return SlotRoleHandler The handler to be used for $role. This may be a + * FallbackSlotRoleHandler if the slot is "known" but not "defined". + */ + public function getRoleHandler( $role ) { + if ( !isset( $this->handlers[$role] ) ) { + if ( !$this->isDefinedRole( $role ) ) { + if ( $this->isKnownRole( $role ) ) { + // The role has no handler defined, but is represented in the database. + // This may happen e.g. when the extension that defined the role was uninstalled. + wfWarn( __METHOD__ . ": known but undefined slot role $role" ); + $this->handlers[$role] = new FallbackSlotRoleHandler( $role ); + } else { + // The role doesn't have a handler defined, and is not represented in + // the database. Something must be quite wrong. + throw new InvalidArgumentException( "Unknown role $role" ); + } + } else { + $handler = call_user_func( $this->instantiators[$role], $role ); + + Assert::postcondition( + $handler instanceof SlotRoleHandler, + "Instantiator for $role role must return a SlotRoleHandler" + ); + + $this->handlers[$role] = $handler; + } + } + + return $this->handlers[$role]; + } + + /** + * Returns the list of roles allowed when creating a new revision on the given page. + * The choice should not depend on external state, such as the page content. + * Note that existing revisions of that page are not guaranteed to comply with this list. + * + * All implementations of this method are required to return at least all "required" roles. + * + * @param LinkTarget $title + * + * @return string[] + */ + public function getAllowedRoles( LinkTarget $title ) { + // TODO: allow this to be overwritten per namespace (or page type) + // TODO: decide how to control which slots are offered for editing per default (T209927) + return $this->getDefinedRoles(); + } + + /** + * Returns the list of roles required when creating a new revision on the given page. + * The should not depend on external state, such as the page content. + * Note that existing revisions of that page are not guaranteed to comply with this list. + * + * All required roles are implicitly considered "allowed", so any roles + * returned by this method will also be returned by getAllowedRoles(). + * + * @param LinkTarget $title + * + * @return string[] + */ + public function getRequiredRoles( LinkTarget $title ) { + // TODO: allow this to be overwritten per namespace (or page type) + return [ 'main' ]; + } + + /** + * Returns the list of roles defined by calling defineRole(). + * + * This list should be used when enumerating slot roles that can be used for editing. + * + * @return string[] + */ + public function getDefinedRoles() { + return array_keys( $this->instantiators ); + } + + /** + * Returns the list of known roles, including the ones returned by getDefinedRoles(), + * and roles that exist according to the NameTableStore provided to the constructor. + * + * This list should be used when enumerating slot roles that can be used in queries or + * for display. + * + * @return string[] + */ + public function getKnownRoles() { + return array_unique( array_merge( + $this->getDefinedRoles(), + $this->roleNamesStore->getMap() + ) ); + } + + /** + * Whether the given role is defined, that is, it was defined by calling defineRole(). + * + * @param string $role + * @return bool + */ + public function isDefinedRole( $role ) { + return in_array( $role, $this->getDefinedRoles(), true ); + } + + /** + * Whether the given role is known, that is, it's either defined or exist according to + * the NameTableStore provided to the constructor. + * + * @param string $role + * @return bool + */ + public function isKnownRole( $role ) { + return in_array( $role, $this->getKnownRoles(), true ); + } + +} diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 33517a0665..9a94389f74 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -48,8 +48,10 @@ use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; use MediaWiki\Preferences\PreferencesFactory; use MediaWiki\Preferences\DefaultPreferencesFactory; +use MediaWiki\Revision\MainSlotRoleHandler; use MediaWiki\Revision\RevisionFactory; use MediaWiki\Revision\RevisionLookup; +use MediaWiki\Revision\SlotRoleRegistry; use MediaWiki\Revision\RevisionRenderer; use MediaWiki\Revision\RevisionStore; use MediaWiki\Revision\RevisionStoreFactory; @@ -420,9 +422,12 @@ return [ }, 'RevisionRenderer' => function ( MediaWikiServices $services ) : RevisionRenderer { - $renderer = new RevisionRenderer( $services->getDBLoadBalancer() ); - $renderer->setLogger( LoggerFactory::getInstance( 'SaveParse' ) ); + $renderer = new RevisionRenderer( + $services->getDBLoadBalancer(), + $services->getSlotRoleRegistry() + ); + $renderer->setLogger( LoggerFactory::getInstance( 'SaveParse' ) ); return $renderer; }, @@ -436,6 +441,7 @@ return [ $services->getDBLoadBalancerFactory(), $services->getBlobStoreFactory(), $services->getNameTableStoreFactory(), + $services->getSlotRoleRegistry(), $services->getMainWANObjectCache(), $services->getCommentStore(), $services->getActorMigration(), @@ -519,6 +525,22 @@ return [ return $factory; }, + 'SlotRoleRegistry' => function ( MediaWikiServices $services ) : SlotRoleRegistry { + $config = $services->getMainConfig(); + + $registry = new SlotRoleRegistry( + $services->getNameTableStoreFactory()->getSlotRoles() + ); + + $registry->defineRole( 'main', function () use ( $config ) { + return new MainSlotRoleHandler( + $config->get( 'NamespaceContentModels' ) + ); + } ); + + return $registry; + }, + 'SpecialPageFactory' => function ( MediaWikiServices $services ) : SpecialPageFactory { return new SpecialPageFactory( $services->getMainConfig(), diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php index af65e457a5..7af80dcfd6 100644 --- a/includes/SiteConfiguration.php +++ b/includes/SiteConfiguration.php @@ -483,13 +483,13 @@ class SiteConfiguration { /** * Work out the site and language name from a database name - * @param string $db + * @param string $wiki Wiki ID * * @return array */ - public function siteFromDB( $db ) { + public function siteFromDB( $wiki ) { // Allow override - $def = $this->getWikiParams( $db ); + $def = $this->getWikiParams( $wiki ); if ( !is_null( $def['suffix'] ) && !is_null( $def['lang'] ) ) { return [ $def['suffix'], $def['lang'] ]; } @@ -499,15 +499,16 @@ class SiteConfiguration { foreach ( $this->suffixes as $altSite => $suffix ) { if ( $suffix === '' ) { $site = ''; - $lang = $db; + $lang = $wiki; break; - } elseif ( substr( $db, -strlen( $suffix ) ) == $suffix ) { + } elseif ( substr( $wiki, -strlen( $suffix ) ) == $suffix ) { $site = is_numeric( $altSite ) ? $suffix : $altSite; - $lang = substr( $db, 0, strlen( $db ) - strlen( $suffix ) ); + $lang = substr( $wiki, 0, strlen( $wiki ) - strlen( $suffix ) ); break; } } $lang = str_replace( '_', '-', $lang ); + return [ $site, $lang ]; } diff --git a/includes/Storage/DerivedPageDataUpdater.php b/includes/Storage/DerivedPageDataUpdater.php index e908968b43..9ce12b4b13 100644 --- a/includes/Storage/DerivedPageDataUpdater.php +++ b/includes/Storage/DerivedPageDataUpdater.php @@ -44,6 +44,7 @@ use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\RevisionRenderer; use MediaWiki\Revision\RevisionSlots; use MediaWiki\Revision\RevisionStore; +use MediaWiki\Revision\SlotRoleRegistry; use MediaWiki\Revision\SlotRecord; use MediaWiki\User\UserIdentity; use MessageCache; @@ -150,6 +151,9 @@ class DerivedPageDataUpdater implements IDBAccessObject { */ private $options = [ 'changed' => true, + // newrev is true if prepareUpdate is handling the creation of a new revision, + // as opposed to a null edit or a forced update. + 'newrev' => false, 'created' => false, 'moved' => false, 'restored' => false, @@ -209,6 +213,9 @@ class DerivedPageDataUpdater implements IDBAccessObject { */ private $revisionRenderer; + /** @var SlotRoleRegistry */ + private $slotRoleRegistry; + /** * A stage identifier for managing the life cycle of this instance. * Possible stages are 'new', 'knows-current', 'has-content', 'has-revision', and 'done'. @@ -255,6 +262,7 @@ class DerivedPageDataUpdater implements IDBAccessObject { * @param WikiPage $wikiPage , * @param RevisionStore $revisionStore * @param RevisionRenderer $revisionRenderer + * @param SlotRoleRegistry $slotRoleRegistry * @param ParserCache $parserCache * @param JobQueueGroup $jobQueueGroup * @param MessageCache $messageCache @@ -265,6 +273,7 @@ class DerivedPageDataUpdater implements IDBAccessObject { WikiPage $wikiPage, RevisionStore $revisionStore, RevisionRenderer $revisionRenderer, + SlotRoleRegistry $slotRoleRegistry, ParserCache $parserCache, JobQueueGroup $jobQueueGroup, MessageCache $messageCache, @@ -276,10 +285,11 @@ class DerivedPageDataUpdater implements IDBAccessObject { $this->parserCache = $parserCache; $this->revisionStore = $revisionStore; $this->revisionRenderer = $revisionRenderer; + $this->slotRoleRegistry = $slotRoleRegistry; $this->jobQueueGroup = $jobQueueGroup; $this->messageCache = $messageCache; $this->contLang = $contLang; - // XXX only needed for waiting for slaves to catch up; there should be a narrower + // XXX only needed for waiting for replicas to catch up; there should be a narrower // interface for that. $this->loadbalancerFactory = $loadbalancerFactory; } @@ -351,13 +361,9 @@ class DerivedPageDataUpdater implements IDBAccessObject { throw new InvalidArgumentException( '$parentId should match the parent of $revision' ); } - if ( $revision - && $user - && $revision->getUser( RevisionRecord::RAW )->getName() !== $user->getName() - ) { - throw new InvalidArgumentException( '$user should match the author of $revision' ); - } - + // NOTE: For null revisions, $user may be different from $this->revision->getUser + // and also from $revision->getUser. + // But $user should always match $this->user. if ( $user && $this->user && $user->getName() !== $this->user->getName() ) { return false; } @@ -368,10 +374,6 @@ class DerivedPageDataUpdater implements IDBAccessObject { return false; } - if ( $revision && !$user ) { - $user = $revision->getUser( RevisionRecord::RAW ); - } - if ( $this->pageState && $revision && $revision->getParentId() !== null @@ -387,22 +389,6 @@ class DerivedPageDataUpdater implements IDBAccessObject { return false; } - if ( $this->revision - && $user - && $this->revision->getUser( RevisionRecord::RAW ) - && $this->revision->getUser( RevisionRecord::RAW )->getName() !== $user->getName() - ) { - return false; - } - - if ( $revision - && $this->user - && $this->revision->getUser( RevisionRecord::RAW ) - && $revision->getUser( RevisionRecord::RAW )->getName() !== $this->user->getName() - ) { - return false; - } - // NOTE: this check is the primary reason for having the $this->slotsUpdate field! if ( $this->slotsUpdate && $slotsUpdate @@ -660,12 +646,26 @@ class DerivedPageDataUpdater implements IDBAccessObject { $hasLinks = null; if ( $this->articleCountMethod === 'link' ) { + // NOTE: it would be more appropriate to determine for each slot separately + // whether it has links, and use that information with that slot's + // isCountable() method. However, that would break parity with + // WikiPage::isCountable, which uses the pagelinks table to determine + // whether the current revision has links. $hasLinks = (bool)count( $this->getCanonicalParserOutput()->getLinks() ); } - // TODO: MCR: ask all slots if they have links [SlotHandler/PageTypeHandler] - $mainContent = $this->getRawContent( SlotRecord::MAIN ); - return $mainContent->isCountable( $hasLinks ); + foreach ( $this->getModifiedSlotRoles() as $role ) { + $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role ); + if ( $roleHandler->supportsArticleCount() ) { + $content = $this->getRawContent( $role ); + + if ( $content->isCountable( $hasLinks ) ) { + return true; + } + } + } + + return false; } /** @@ -673,6 +673,7 @@ class DerivedPageDataUpdater implements IDBAccessObject { */ public function isRedirect() { // NOTE: main slot determines redirect status + // TODO: MCR: this should be controlled by a PageTypeHandler $mainContent = $this->getRawContent( SlotRecord::MAIN ); return $mainContent->isRedirect(); @@ -762,17 +763,6 @@ class DerivedPageDataUpdater implements IDBAccessObject { $stashedEdit = ApiStashEdit::checkCache( $title, $mainContent, $legacyUser ); } - if ( $stashedEdit ) { - /** @var ParserOutput $output */ - $output = $stashedEdit->output; - - // TODO: this should happen when stashing the ParserOutput, not now! - $output->setCacheTime( $stashedEdit->timestamp ); - - // TODO: MCR: allow output for all slots to be stashed. - $this->canonicalParserOutput = $output; - } - $userPopts = ParserOptions::newFromUserAndLang( $user, $this->contLang ); Hooks::run( 'ArticlePrepareTextForEdit', [ $wikiPage, $userPopts ] ); @@ -853,6 +843,27 @@ class DerivedPageDataUpdater implements IDBAccessObject { } else { $this->parentRevision = $parentRevision; } + + $renderHints = [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ]; + + if ( $stashedEdit ) { + /** @var ParserOutput $output */ + $output = $stashedEdit->output; + + // TODO: this should happen when stashing the ParserOutput, not now! + $output->setCacheTime( $stashedEdit->timestamp ); + + $renderHints['known-revision-output'] = $output; + } + + // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions + // NOTE: the revision is either new or current, so we can bypass audience checks. + $this->renderedRevision = $this->revisionRenderer->getRenderedRevision( + $this->revision, + null, + null, + $renderHints + ); } /** @@ -879,18 +890,7 @@ class DerivedPageDataUpdater implements IDBAccessObject { * @return RenderedRevision */ public function getRenderedRevision() { - if ( !$this->renderedRevision ) { - $this->assertPrepared( __METHOD__ ); - - // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions - // NOTE: the revision is either new or current, so we can bypass audience checks. - $this->renderedRevision = $this->revisionRenderer->getRenderedRevision( - $this->revision, - null, - null, - [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ] - ); - } + $this->assertPrepared( __METHOD__ ); return $this->renderedRevision; } @@ -1113,12 +1113,14 @@ class DerivedPageDataUpdater implements IDBAccessObject { // Override fields defined in $this->options with values from $options. $this->options = array_intersect_key( $options, $this->options ) + $this->options; - if ( isset( $this->pageState['oldId'] ) ) { - $oldId = $this->pageState['oldId']; + if ( $this->revision ) { + $oldId = $this->pageState['oldId'] ?? 0; + $this->options['newrev'] = ( $revision->getId() !== $oldId ); } elseif ( isset( $this->options['oldrevision'] ) ) { /** @var Revision|RevisionRecord $oldRev */ $oldRev = $this->options['oldrevision']; $oldId = $oldRev->getId(); + $this->options['newrev'] = ( $revision->getId() !== $oldId ); } else { $oldId = $revision->getParentId(); } @@ -1210,6 +1212,19 @@ class DerivedPageDataUpdater implements IDBAccessObject { // Prune any output that depends on the revision ID. if ( $this->renderedRevision ) { $this->renderedRevision->updateRevision( $revision ); + } else { + + // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions + // NOTE: the revision is either new or current, so we can bypass audience checks. + $this->renderedRevision = $this->revisionRenderer->getRenderedRevision( + $this->revision, + null, + null, + [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ] + ); + + // XXX: Since we presumably are dealing with the current revision, + // we could try to get the ParserOutput from the parser cache. } // TODO: optionally get ParserOutput from the ParserCache here. @@ -1407,12 +1422,9 @@ class DerivedPageDataUpdater implements IDBAccessObject { // the recent change entry (also done via deferred updates) and carry over any // bot/deletion/IP flags, ect. $this->jobQueueGroup->lazyPush( - new CategoryMembershipChangeJob( + CategoryMembershipChangeJob::newSpec( $this->getTitle(), - [ - 'pageId' => $this->getPageId(), - 'revTimestamp' => $this->revision->getTimestamp(), - ] + $this->revision->getTimestamp() ) ); } @@ -1604,8 +1616,8 @@ class DerivedPageDataUpdater implements IDBAccessObject { // Save it to the parser cache. Use the revision timestamp in the case of a // freshly saved edit, as that matches page_touched and a mismatch would trigger an // unnecessary reparse. - $timestamp = $this->options['changed'] ? $this->revision->getTimestamp() - : $output->getTimestamp(); + $timestamp = $this->options['newrev'] ? $this->revision->getTimestamp() + : $output->getCacheTime(); $this->parserCache->save( $output, $wikiPage, $this->getCanonicalParserOptions(), $timestamp, $this->revision->getId() diff --git a/includes/Storage/NameTableStore.php b/includes/Storage/NameTableStore.php index c1dd09dfb7..27194ab219 100644 --- a/includes/Storage/NameTableStore.php +++ b/includes/Storage/NameTableStore.php @@ -205,7 +205,7 @@ class NameTableStore { * * @param int $connFlags ILoadBalancer::CONN_XXX flags. Optional. * - * @return \string[] The freshly reloaded name map + * @return string[] The freshly reloaded name map */ public function reloadMap( $connFlags = 0 ) { $this->tableCache = $this->loadTable( diff --git a/includes/Storage/PageUpdater.php b/includes/Storage/PageUpdater.php index 043e00ebf6..6cbdcc6e91 100644 --- a/includes/Storage/PageUpdater.php +++ b/includes/Storage/PageUpdater.php @@ -31,7 +31,6 @@ use Content; use ContentHandler; use DeferredUpdates; use Hooks; -use InvalidArgumentException; use LogicException; use ManualLogEntry; use MediaWiki\Linker\LinkTarget; @@ -39,6 +38,7 @@ use MediaWiki\Revision\MutableRevisionRecord; use MediaWiki\Revision\RevisionAccessException; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\RevisionStore; +use MediaWiki\Revision\SlotRoleRegistry; use MediaWiki\Revision\SlotRecord; use MWException; use RecentChange; @@ -96,6 +96,11 @@ class PageUpdater { */ private $revisionStore; + /** + * @var SlotRoleRegistry + */ + private $slotRoleRegistry; + /** * @var boolean see $wgUseAutomaticEditSummaries * @see $wgUseAutomaticEditSummaries @@ -148,13 +153,15 @@ class PageUpdater { * @param DerivedPageDataUpdater $derivedDataUpdater * @param LoadBalancer $loadBalancer * @param RevisionStore $revisionStore + * @param SlotRoleRegistry $slotRoleRegistry */ public function __construct( User $user, WikiPage $wikiPage, DerivedPageDataUpdater $derivedDataUpdater, LoadBalancer $loadBalancer, - RevisionStore $revisionStore + RevisionStore $revisionStore, + SlotRoleRegistry $slotRoleRegistry ) { $this->user = $user; $this->wikiPage = $wikiPage; @@ -162,6 +169,7 @@ class PageUpdater { $this->loadBalancer = $loadBalancer; $this->revisionStore = $revisionStore; + $this->slotRoleRegistry = $slotRoleRegistry; $this->slotsUpdate = new RevisionSlotsUpdate(); } @@ -317,14 +325,6 @@ class PageUpdater { return $this->derivedDataUpdater->grabCurrentRevision(); } - /** - * @return string - */ - private function getTimestampNow() { - // TODO: allow an override to be injected for testing - return wfTimestampNow(); - } - /** * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed. * @@ -346,8 +346,7 @@ class PageUpdater { * @param Content $content */ public function setContent( $role, Content $content ) { - // TODO: MCR: check the role and the content's model against the list of supported - // roles, see T194046. + $this->ensureRoleAllowed( $role ); $this->slotsUpdate->modifyContent( $role, $content ); } @@ -358,6 +357,8 @@ class PageUpdater { * @param SlotRecord $slot */ public function setSlot( SlotRecord $slot ) { + $this->ensureRoleAllowed( $slot->getRole() ); + $this->slotsUpdate->modifySlot( $slot ); } @@ -376,6 +377,7 @@ class PageUpdater { * by the new revision. */ public function inheritSlot( SlotRecord $originalSlot ) { + // NOTE: slots can be inherited even if the role is not "allowed" on the title. // NOTE: this slot is inherited from some other revision, but it's // a "modified" slot for the RevisionSlotsUpdate and DerivedPageDataUpdater, // since it's not implicitly inherited from the parent revision. @@ -393,9 +395,7 @@ class PageUpdater { * @param string $role A slot role name (but not "main") */ public function removeSlot( $role ) { - if ( $role === SlotRecord::MAIN ) { - throw new InvalidArgumentException( 'Cannot remove the main slot!' ); - } + $this->ensureRoleNotRequired( $role ); $this->slotsUpdate->removeSlot( $role ); } @@ -635,20 +635,38 @@ class PageUpdater { throw new RuntimeException( 'Something is trying to edit an article with an empty title' ); } - // TODO: MCR: check the role and the content's model against the list of supported - // and required roles, see T194046. + // NOTE: slots can be inherited even if the role is not "allowed" on the title. + $status = Status::newGood(); + $this->checkAllRolesAllowed( + $this->slotsUpdate->getModifiedRoles(), + $status + ); + $this->checkNoRolesRequired( + $this->slotsUpdate->getRemovedRoles(), + $status + ); - // Make sure the given content type is allowed for this page - // TODO: decide: Extend check to other slots? Consider the role in check? [PageType] - $mainContentHandler = $this->getContentHandler( SlotRecord::MAIN ); - if ( !$mainContentHandler->canBeUsedOn( $this->getTitle() ) ) { - $this->status = Status::newFatal( 'content-not-allowed-here', - ContentHandler::getLocalizedName( $mainContentHandler->getModelID() ), - $this->getTitle()->getPrefixedText() - ); + if ( !$status->isOK() ) { return null; } + // Make sure the given content is allowed in the respective slots of this page + foreach ( $this->slotsUpdate->getModifiedRoles() as $role ) { + $slot = $this->slotsUpdate->getModifiedSlot( $role ); + $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role ); + + if ( !$roleHandler->isAllowedModel( $slot->getModel(), $this->getTitle() ) ) { + $contentHandler = ContentHandler::getForModelID( $slot->getModel() ); + $this->status = Status::newFatal( 'content-not-allowed-here', + ContentHandler::getLocalizedName( $contentHandler->getModelID() ), + $this->getTitle()->getPrefixedText(), + wfMessage( $roleHandler->getNameMessageKey() ) + // TODO: defer message lookup to caller + ); + return null; + } + } + // Load the data from the master database if needed. Needed to check flags. // NOTE: This grabs the parent revision as the CAS token, if grabParentRevision // wasn't called yet. If the page is modified by another process before we are done with @@ -882,13 +900,19 @@ class PageUpdater { $content = $slot->getContent(); // XXX: We may push this up to the "edit controller" level, see T192777. - // TODO: change the signature of PrepareSave to not take a WikiPage! + // XXX: prepareSave() and isValid() could live in SlotRoleHandler + // XXX: PrepareSave should not take a WikiPage! $prepStatus = $content->prepareSave( $wikiPage, $flags, $oldid, $user ); // TODO: MCR: record which problem arose in which slot. $status->merge( $prepStatus ); } + $this->checkAllRequiredRoles( + $rev->getSlotRoles(), + $status + ); + return $rev; } @@ -1216,4 +1240,71 @@ class PageUpdater { ); } + /** + * @return string[] Slots required for this page update, as a list of role names. + */ + private function getRequiredSlotRoles() { + return $this->slotRoleRegistry->getRequiredRoles( $this->getTitle() ); + } + + /** + * @return string[] Slots allowed for this page update, as a list of role names. + */ + private function getAllowedSlotRoles() { + return $this->slotRoleRegistry->getAllowedRoles( $this->getTitle() ); + } + + private function ensureRoleAllowed( $role ) { + $allowedRoles = $this->getAllowedSlotRoles(); + if ( !in_array( $role, $allowedRoles ) ) { + throw new PageUpdateException( "Slot role `$role` is not allowed." ); + } + } + + private function ensureRoleNotRequired( $role ) { + $requiredRoles = $this->getRequiredSlotRoles(); + if ( in_array( $role, $requiredRoles ) ) { + throw new PageUpdateException( "Slot role `$role` is required." ); + } + } + + private function checkAllRolesAllowed( array $roles, Status $status ) { + $allowedRoles = $this->getAllowedSlotRoles(); + + $forbidden = array_diff( $roles, $allowedRoles ); + if ( !empty( $forbidden ) ) { + $status->error( + 'edit-slots-cannot-add', + count( $forbidden ), + implode( ', ', $forbidden ) + ); + } + } + + private function checkNoRolesRequired( array $roles, Status $status ) { + $requiredRoles = $this->getRequiredSlotRoles(); + + $needed = array_diff( $roles, $requiredRoles ); + if ( !empty( $needed ) ) { + $status->error( + 'edit-slots-cannot-remove', + count( $needed ), + implode( ', ', $needed ) + ); + } + } + + private function checkAllRequiredRoles( array $roles, Status $status ) { + $requiredRoles = $this->getRequiredSlotRoles(); + + $missing = array_diff( $requiredRoles, $roles ); + if ( !empty( $missing ) ) { + $status->error( + 'edit-slots-missing', + count( $missing ), + implode( ', ', $missing ) + ); + } + } + } diff --git a/includes/Title.php b/includes/Title.php index 038e8b1b20..909f528637 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -954,6 +954,7 @@ class Title implements LinkTarget { /** * Get the DB key with the initial letter case as specified by the user + * @deprecated since 1.33; please use Title::getDBKey() instead * * @return string DB key */ @@ -978,6 +979,8 @@ class Title implements LinkTarget { /** * Get the page's content model id, see the CONTENT_MODEL_XXX constants. * + * @todo Deprecate this in favor of SlotRecord::getModel() + * * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update * @return string Content model id */ @@ -1659,7 +1662,7 @@ class Title implements LinkTarget { $p = $this->mInterwiki . ':'; } - if ( 0 != $this->mNamespace ) { + if ( $this->mNamespace != 0 ) { $nsText = $this->getNsText(); if ( $nsText === false ) { @@ -2687,9 +2690,9 @@ class Title implements LinkTarget { $errors[] = [ 'confirmedittext' ]; } - $useSlave = ( $rigor !== 'secure' ); + $useReplica = ( $rigor !== 'secure' ); if ( ( $action == 'edit' || $action == 'create' ) - && !$user->isBlockedFrom( $this, $useSlave ) + && !$user->isBlockedFrom( $this, $useReplica ) ) { // Don't block the user from editing their own talk page unless they've been // explicitly blocked from that too. @@ -2858,10 +2861,12 @@ class Title implements LinkTarget { } $errors = []; - while ( count( $checks ) > 0 && - !( $short && count( $errors ) > 0 ) ) { - $method = array_shift( $checks ); + foreach ( $checks as $method ) { $errors = $this->$method( $action, $user, $errors, $rigor, $short ); + + if ( $short && $errors !== [] ) { + break; + } } return $errors; @@ -3273,9 +3278,13 @@ class Title implements LinkTarget { * 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" + * @param bool $readLatest When true, skip replicas and read from the master DB. */ - public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { - $dbr = wfGetDB( DB_REPLICA ); + public function loadRestrictionsFromRows( + $rows, $oldFashionedRestrictions = null, $readLatest = false + ) { + $whichDb = $readLatest ? DB_MASTER : DB_REPLICA; + $dbr = wfGetDB( $whichDb ); $restrictionTypes = $this->getRestrictionTypes(); @@ -3345,9 +3354,10 @@ class Title implements LinkTarget { * 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" + * @param bool $readLatest When true, skip replicas and read from the master DB. */ - public function loadRestrictions( $oldFashionedRestrictions = null ) { - if ( $this->mRestrictionsLoaded ) { + public function loadRestrictions( $oldFashionedRestrictions = null, $readLatest = false ) { + if ( $this->mRestrictionsLoaded && !$readLatest ) { return; } @@ -3357,10 +3367,11 @@ class Title implements LinkTarget { $fname = __METHOD__; $rows = $cache->getWithSetCallback( // Page protections always leave a new null revision - $cache->makeKey( 'page-restrictions', $id, $this->getLatestRevID() ), + $cache->makeKey( 'page-restrictions', $id, $this->getLatestRevID(), $readLatest ), $cache::TTL_DAY, - function ( $curValue, &$ttl, array &$setOpts ) use ( $fname ) { - $dbr = wfGetDB( DB_REPLICA ); + function ( $curValue, &$ttl, array &$setOpts ) use ( $fname, $readLatest ) { + $whichDb = $readLatest ? DB_MASTER : DB_REPLICA; + $dbr = wfGetDB( $whichDb ); $setOpts += Database::getCacheSetOptions( $dbr ); @@ -3375,7 +3386,7 @@ class Title implements LinkTarget { } ); - $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions ); + $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions, $readLatest ); } else { $title_protection = $this->getTitleProtectionInternal(); @@ -3571,7 +3582,7 @@ class Title implements LinkTarget { $this->mArticleID = $linkCache->addLinkObj( $this ); $linkCache->forUpdate( $oldUpdate ); } else { - if ( -1 == $this->mArticleID ) { + if ( $this->mArticleID == -1 ) { $this->mArticleID = $linkCache->addLinkObj( $this ); } } @@ -5218,10 +5229,9 @@ class Title implements LinkTarget { if ( MWNamespace::hasSubpages( $this->mNamespace ) ) { // Optional notice for page itself and any parent page - $parts = explode( '/', $this->mDbkeyform ); $editnotice_base = $editnotice_ns; - while ( count( $parts ) > 0 ) { - $editnotice_base .= '-' . array_shift( $parts ); + foreach ( explode( '/', $this->mDbkeyform ) as $part ) { + $editnotice_base .= '-' . $part; $msg = wfMessage( $editnotice_base ); if ( $msg->exists() ) { $html = $msg->parseAsBlock(); diff --git a/includes/WikiMap.php b/includes/WikiMap.php index b731d7bd95..3305f9f7d3 100644 --- a/includes/WikiMap.php +++ b/includes/WikiMap.php @@ -246,6 +246,7 @@ class WikiMap { * Get the wiki ID of a database domain * * This is like DatabaseDomain::getId() without encoding (for legacy reasons) + * and without the schema if it merely set to the generic value "mediawiki" * * @param string|DatabaseDomain $domain * @return string @@ -253,9 +254,19 @@ class WikiMap { public static function getWikiIdFromDomain( $domain ) { $domain = DatabaseDomain::newFromId( $domain ); + if ( !in_array( $domain->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 --, meaning that + // the schema portion must be accounted for to disambiguate wikis. + return "{$domain->getDatabase()}-{$domain->getSchema()}-{$domain->getTablePrefix()}"; + } + + // Note that if this wiki ID is passed a a domain ID to LoadBalancer, then it can + // handle the schema by assuming the generic "mediawiki" schema if needed. return strlen( $domain->getTablePrefix() ) ? "{$domain->getDatabase()}-{$domain->getTablePrefix()}" - : $domain->getDatabase(); + : (string)$domain->getDatabase(); } /** @@ -277,9 +288,16 @@ class WikiMap { $domain = DatabaseDomain::newFromId( $domain ); $curDomain = self::getCurrentWikiDomain(); + 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 --, meaning that + // the schema portion must be accounted for to disambiguate wikis. + return $curDomain->equals( $domain ); + } + return ( $curDomain->getDatabase() === $domain->getDatabase() && - // @TODO: check schema instead of assuming it's ""/"mediawiki" and never collides $curDomain->getTablePrefix() === $domain->getTablePrefix() ); } diff --git a/includes/XmlSelect.php b/includes/XmlSelect.php index 5d7406c64d..45002e82f6 100644 --- a/includes/XmlSelect.php +++ b/includes/XmlSelect.php @@ -75,7 +75,7 @@ class XmlSelect { /** * @param string $label - * @param string $value If not given, assumed equal to $label + * @param string|false $value If not given, assumed equal to $label */ public function addOption( $label, $value = false ) { $value = $value !== false ? $value : $label; @@ -99,7 +99,7 @@ class XmlSelect { * label => ( label => value, label => value ) * * @param array $options - * @param string|array $default + * @param string|array|false $default * @return string */ static function formatOptions( $options, $default = false ) { diff --git a/includes/actions/Action.php b/includes/actions/Action.php index e5233f0607..f288a5cb1f 100644 --- a/includes/actions/Action.php +++ b/includes/actions/Action.php @@ -314,9 +314,14 @@ abstract class Action implements MessageLocalizer { } } - if ( $this->requiresUnblock() && $user->isBlocked() ) { + // If the action requires an unblock, explicitly check the user's block. + if ( $this->requiresUnblock() && $user->isBlockedFrom( $this->getTitle() ) ) { $block = $user->getBlock(); - throw new UserBlockedError( $block ); + if ( $block ) { + throw new UserBlockedError( $block ); + } + + throw new PermissionsError( $this->getName(), [ 'badaccess-group0' ] ); } // This should be checked at the end so that the user won't think the diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index 7d6b548b0f..140ca2e2e8 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -492,7 +492,7 @@ class HistoryPager extends ReverseChronologicalPager { return $s; } - function doBatchLookups() { + protected function doBatchLookups() { if ( !Hooks::run( 'PageHistoryPager::doBatchLookups', [ $this, $this->mResult ] ) ) { return; } @@ -523,7 +523,7 @@ class HistoryPager extends ReverseChronologicalPager { * * @return string HTML output */ - function getStartBody() { + protected function getStartBody() { $this->lastRow = false; $this->counter = 1; $this->oldIdChecked = 0; @@ -585,7 +585,7 @@ class HistoryPager extends ReverseChronologicalPager { return $element; } - function getEndBody() { + protected function getEndBody() { if ( $this->lastRow ) { $latest = $this->counter == 1 && $this->mIsFirst; $firstInList = $this->counter == 1; diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php index d0145034c8..49a6bb5a5f 100644 --- a/includes/actions/InfoAction.php +++ b/includes/actions/InfoAction.php @@ -174,7 +174,7 @@ class InfoAction extends FormlessAction { * @param string $table The table that will be added to the content * @param string $name The name of the row * @param string $value The value of the row - * @param string $id The ID to use for the 'tr' element + * @param string|null $id The ID to use for the 'tr' element * @return string The table with the row added */ protected function addRow( $table, $name, $value, $id ) { @@ -264,6 +264,12 @@ class InfoAction extends FormlessAction { $this->msg( 'pageinfo-length' ), $lang->formatNum( $title->getLength() ) ]; + // Page namespace + $pageNamespace = $title->getNsText(); + if ( $pageNamespace ) { + $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-namespace' ), $pageNamespace ]; + } + // Page ID (number not localised, as it's a database ID) $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-article-id' ), $id ]; diff --git a/includes/actions/RollbackAction.php b/includes/actions/RollbackAction.php index c962e20d22..03a5bc840a 100644 --- a/includes/actions/RollbackAction.php +++ b/includes/actions/RollbackAction.php @@ -70,9 +70,13 @@ class RollbackAction extends FormlessAction { } // @TODO: remove this hack once rollback uses POST (T88044) + $fname = __METHOD__; $trxLimits = $this->context->getConfig()->get( 'TrxProfilerLimits' ); $trxProfiler = Profiler::instance()->getTransactionProfiler(); - $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ ); + $trxProfiler->redefineExpectations( $trxLimits['POST'], $fname ); + DeferredUpdates::addCallableUpdate( function () use ( $trxProfiler, $trxLimits, $fname ) { + $trxProfiler->redefineExpectations( $trxLimits['PostSend-POST'], $fname ); + } ); $data = null; $errors = $this->page->doRollback( diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index c66e5d52bb..1efd747f80 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -21,6 +21,7 @@ */ use Wikimedia\Rdbms\IDatabase; +use MediaWiki\MediaWikiServices; /** * This abstract class implements many basic API functions, and is the base of @@ -267,11 +268,14 @@ abstract class ApiBase extends ContextSource { /** @var array Maps extension paths to info arrays */ private static $extensionInfo = null; + /** @var int[][][] Cache for self::filterIDs() */ + private static $filterIDsCache = []; + /** @var ApiMain */ private $mMainModule; /** @var string */ private $mModuleName, $mModulePrefix; - private $mSlaveDB = null; + private $mReplicaDB = null; private $mParamCache = []; /** @var array|null|bool */ private $mModuleSource = false; @@ -647,11 +651,11 @@ abstract class ApiBase extends ContextSource { * @return IDatabase */ protected function getDB() { - if ( !isset( $this->mSlaveDB ) ) { - $this->mSlaveDB = wfGetDB( DB_REPLICA, 'api' ); + if ( !isset( $this->mReplicaDB ) ) { + $this->mReplicaDB = wfGetDB( DB_REPLICA, 'api' ); } - return $this->mSlaveDB; + return $this->mReplicaDB; } /** @@ -1831,6 +1835,41 @@ abstract class ApiBase extends ContextSource { } } + /** + * Filter out-of-range values from a list of positive integer IDs + * @since 1.33 + * @param array $fields Array of pairs of table and field to check + * @param (string|int)[] $ids IDs to filter. Strings in the array are + * expected to be stringified ints. + * @return (string|int)[] Filtered IDs. + */ + protected function filterIDs( $fields, array $ids ) { + $min = INF; + $max = 0; + foreach ( $fields as list( $table, $field ) ) { + if ( isset( self::$filterIDsCache[$table][$field] ) ) { + $row = self::$filterIDsCache[$table][$field]; + } else { + $row = $this->getDB()->selectRow( + $table, + [ + 'min_id' => "MIN($field)", + 'max_id' => "MAX($field)", + ], + '', + __METHOD__ + ); + self::$filterIDsCache[$table][$field] = $row; + } + $min = min( $min, $row->min_id ); + $max = max( $max, $row->max_id ); + } + return array_filter( $ids, function ( $id ) use ( $min, $max ) { + return ( is_int( $id ) && $id >= 0 || ctype_digit( $id ) ) + && $id >= $min && $id <= $max; + } ); + } + /**@}*/ /************************************************************************//** @@ -1910,9 +1949,14 @@ abstract class ApiBase extends ContextSource { * @since 1.29 * @param StatusValue $status * @param string[] $types 'warning' and/or 'error' + * @param string[] $filter Message keys to filter out (since 1.33) */ - public function addMessagesFromStatus( StatusValue $status, $types = [ 'warning', 'error' ] ) { - $this->getErrorFormatter()->addMessagesFromStatus( $this->getModulePath(), $status, $types ); + public function addMessagesFromStatus( + StatusValue $status, $types = [ 'warning', 'error' ], array $filter = [] + ) { + $this->getErrorFormatter()->addMessagesFromStatus( + $this->getModulePath(), $status, $types, $filter + ); } /** @@ -2069,11 +2113,41 @@ abstract class ApiBase extends ContextSource { foreach ( (array)$actions as $action ) { $errors = array_merge( $errors, $title->getUserPermissionsErrors( $action, $user ) ); } + if ( $errors ) { + // track block notices + if ( $this->getConfig()->get( 'EnableBlockNoticeStats' ) ) { + $this->trackBlockNotices( $errors ); + } + $this->dieStatus( $this->errorArrayToStatus( $errors, $user ) ); } } + /** + * Keep track of errors messages resulting from a block + * + * @param array $errors + */ + private function trackBlockNotices( array $errors ) { + $errorMessageKeys = [ + 'blockedtext', + 'blockedtext-partial', + 'autoblockedtext', + 'systemblockedtext', + ]; + + $statsd = MediaWikiServices::getInstance()->getStatsdDataFactory(); + + foreach ( $errors as $error ) { + if ( in_array( $error[0], $errorMessageKeys ) ) { + $wiki = $this->getConfig()->get( 'DBname' ); + $statsd->increment( 'BlockNotices.' . $wiki . '.MediaWikiApi.returned' ); + break; + } + } + } + /** * Will only set a warning instead of failing if the global $wgDebugAPI * is set to true. Otherwise behaves exactly as self::dieWithError(). diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index 3581ac8514..ed3d01ce8e 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -57,25 +57,11 @@ class ApiBlock extends ApiBase { $editingRestriction = 'sitewide'; $pageRestrictions = ''; if ( $this->getConfig()->get( 'EnablePartialBlocks' ) ) { - if ( $params['pagerestrictions'] ) { - $count = count( $params['pagerestrictions'] ); - if ( $count > 10 ) { - $this->dieWithError( - $this->msg( - 'apierror-integeroutofrange-abovebotmax', - 'pagerestrictions', - 10, - $count - ) - ); - } - } - if ( $params['partial'] ) { $editingRestriction = 'partial'; } - $pageRestrictions = implode( "\n", $params['pagerestrictions'] ); + $pageRestrictions = implode( "\n", (array)$params['pagerestrictions'] ); } if ( $params['userid'] !== null ) { @@ -207,6 +193,8 @@ class ApiBlock extends ApiBase { $params['partial'] = false; $params['pagerestrictions'] = [ ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_ISMULTI_LIMIT1 => 10, + ApiBase::PARAM_ISMULTI_LIMIT2 => 10, ]; } diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index 76b7bce67b..4ba30abfd4 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -22,6 +22,7 @@ use MediaWiki\MediaWikiServices; use MediaWiki\Revision\MutableRevisionRecord; use MediaWiki\Revision\RevisionRecord; +use MediaWiki\Revision\RevisionArchiveRecord; use MediaWiki\Revision\RevisionStore; use MediaWiki\Revision\SlotRecord; @@ -30,11 +31,15 @@ class ApiComparePages extends ApiBase { /** @var RevisionStore */ private $revisionStore; + /** @var \MediaWiki\Revision\SlotRoleRegistry */ + private $slotRoleRegistry; + private $guessedTitle = false, $props; public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) { parent::__construct( $mainModule, $moduleName, $modulePrefix ); $this->revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); + $this->slotRoleRegistry = MediaWikiServices::getInstance()->getSlotRoleRegistry(); } public function execute() { @@ -61,19 +66,56 @@ class ApiComparePages extends ApiBase { if ( !$fromRelRev ) { $this->dieWithError( 'apierror-compare-relative-to-nothing' ); } + if ( $params['torelative'] !== 'cur' && $fromRelRev instanceof RevisionArchiveRecord ) { + // RevisionStore's getPreviousRevision/getNextRevision blow up + // when passed an RevisionArchiveRecord for a deleted page + $this->dieWithError( [ 'apierror-compare-relative-to-deleted', $params['torelative'] ] ); + } switch ( $params['torelative'] ) { case 'prev': // Swap 'from' and 'to' - list( $toRev, $toRelRev2, $toValsRev ) = [ $fromRev, $fromRelRev, $fromValsRev ]; - $fromRev = $this->revisionStore->getPreviousRevision( $fromRelRev ); + list( $toRev, $toRelRev, $toValsRev ) = [ $fromRev, $fromRelRev, $fromValsRev ]; + $fromRev = $this->revisionStore->getPreviousRevision( $toRelRev ); $fromRelRev = $fromRev; $fromValsRev = $fromRev; + if ( !$fromRev ) { + $title = Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ); + $this->addWarning( [ + 'apiwarn-compare-no-prev', + wfEscapeWikiText( $title->getPrefixedText() ), + $toRelRev->getId() + ] ); + + // (T203433) Create an empty dummy revision as the "previous". + // The main slot has to exist, the rest will be handled by DifferenceEngine. + $fromRev = $this->revisionStore->newMutableRevisionFromArray( [ + 'title' => $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . __METHOD__ ) + ] ); + $fromRev->setContent( + SlotRecord::MAIN, + $toRelRev->getContent( SlotRecord::MAIN, RevisionRecord::RAW ) + ->getContentHandler() + ->makeEmptyContent() + ); + } break; case 'next': $toRev = $this->revisionStore->getNextRevision( $fromRelRev ); $toRelRev = $toRev; $toValsRev = $toRev; + if ( !$toRev ) { + $title = Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ); + $this->addWarning( [ + 'apiwarn-compare-no-next', + wfEscapeWikiText( $title->getPrefixedText() ), + $fromRelRev->getId() + ] ); + + // (T203433) The web UI treats "next" as "cur" in this case. + // Avoid repeating metadata by making a MutableRevisionRecord with no changes. + $toRev = MutableRevisionRecord::newFromParentRevision( $fromRelRev ); + } break; case 'cur': @@ -93,10 +135,12 @@ class ApiComparePages extends ApiBase { list( $toRev, $toRelRev, $toValsRev ) = $this->getDiffRevision( 'to', $params ); } - // Handle missing from or to revisions + // Handle missing from or to revisions (should never happen) + // @codeCoverageIgnoreStart if ( !$fromRev || !$toRev ) { $this->dieWithError( 'apierror-baddiff' ); } + // @codeCoverageIgnoreEnd // Handle revdel if ( !$fromRev->audienceCan( @@ -272,9 +316,8 @@ class ApiComparePages extends ApiBase { } $guessedTitle = $this->guessTitle(); - if ( $guessedTitle && $role === SlotRecord::MAIN ) { - // @todo: Use SlotRoleRegistry and do this for all slots - return $guessedTitle->getContentModel(); + if ( $guessedTitle ) { + return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle ); } if ( isset( $params["fromcontentmodel-$role"] ) ) { @@ -582,10 +625,7 @@ class ApiComparePages extends ApiBase { } public function getAllowedParams() { - $slotRoles = MediaWikiServices::getInstance()->getSlotRoleStore()->getMap(); - if ( !in_array( SlotRecord::MAIN, $slotRoles, true ) ) { - $slotRoles[] = SlotRecord::MAIN; - } + $slotRoles = $this->slotRoleRegistry->getKnownRoles(); sort( $slotRoles, SORT_STRING ); // Parameters for the 'from' and 'to' content diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index ec857b7da9..7e8041d667 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -75,9 +75,10 @@ class ApiDelete extends ApiBase { $status = self::delete( $pageObj, $user, $reason, $params['tags'] ); } - if ( !$status->isGood() ) { + if ( !$status->isOk() ) { $this->dieStatus( $status ); } + $this->addMessagesFromStatus( $status, [ 'warning' ], [ 'delete-scheduled' ] ); // Deprecated parameters if ( $params['watch'] ) { @@ -92,8 +93,14 @@ class ApiDelete extends ApiBase { $r = [ 'title' => $titleObj->getPrefixedText(), 'reason' => $reason, - 'logid' => $status->value ]; + if ( $status->hasMessage( 'delete-scheduled' ) ) { + $r['scheduled'] = true; + } + if ( $status->value !== null ) { + // Scheduled deletions don't currently have a log entry available at this point + $r['logid'] = $status->value; + } $this->getResult()->addValue( null, $this->getModuleName(), $r ); } diff --git a/includes/api/ApiErrorFormatter.php b/includes/api/ApiErrorFormatter.php index 847afd8634..9669464733 100644 --- a/includes/api/ApiErrorFormatter.php +++ b/includes/api/ApiErrorFormatter.php @@ -58,6 +58,26 @@ class ApiErrorFormatter { $this->format = $format; } + /** + * Test whether a code is a valid API error code + * + * A valid code contains only ASCII letters, numbers, underscore, and + * hyphen and is not the empty string. + * + * For backwards compatibility, any code beginning 'internal_api_error_' is + * also allowed. + * + * @param string $code + * @return bool + */ + public static function isValidApiCode( $code ) { + return is_string( $code ) && ( + preg_match( '/^[a-zA-Z0-9_-]+$/', $code ) || + // TODO: Deprecate this + preg_match( '/^internal_api_error_[^\0\r\n]+$/', $code ) + ); + } + /** * Return a formatter like this one but with a different format * @@ -133,9 +153,10 @@ class ApiErrorFormatter { * @param string|null $modulePath * @param StatusValue $status * @param string[]|string $types 'warning' and/or 'error' + * @param string[] $filter Messages to filter out (since 1.33) */ public function addMessagesFromStatus( - $modulePath, StatusValue $status, $types = [ 'warning', 'error' ] + $modulePath, StatusValue $status, $types = [ 'warning', 'error' ], array $filter = [] ) { if ( $status->isGood() || !$status->getErrors() ) { return; @@ -158,7 +179,9 @@ class ApiErrorFormatter { ->inLanguage( $this->lang ) ->title( $this->getDummyTitle() ) ->useDatabase( $this->useDB ); - $this->addWarningOrError( $tag, $modulePath, $msg ); + if ( !in_array( $msg->getKey(), $filter, true ) ) { + $this->addWarningOrError( $tag, $modulePath, $msg ); + } } } @@ -191,6 +214,7 @@ class ApiErrorFormatter { if ( !isset( $options['code'] ) ) { $class = preg_replace( '#^Wikimedia\\\Rdbms\\\#', '', get_class( $exception ) ); $options['code'] = 'internal_api_error_' . $class; + $options['data']['errorclass'] = get_class( $exception ); } } $params = [ wfEscapeWikiText( $exception->getMessage() ) ]; diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index 5bf8da9ff8..9edf929f8e 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -172,33 +172,29 @@ class ApiFeedContributions extends ApiBase { * @return string */ protected function feedItemDesc( RevisionRecord $revision ) { - if ( $revision ) { - $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text(); - try { - $content = $revision->getContent( SlotRecord::MAIN ); - } catch ( RevisionAccessException $e ) { - $content = null; - } - - if ( $content instanceof TextContent ) { - // only textual content has a "source view". - $html = nl2br( htmlspecialchars( $content->getNativeData() ) ); - } else { - // XXX: we could get an HTML representation of the content via getParserOutput, but that may - // contain JS magic and generally may not be suitable for inclusion in a feed. - // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method. - // Compare also FeedUtils::formatDiffRow. - $html = ''; - } - - $comment = $revision->getComment(); + $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text(); + try { + $content = $revision->getContent( SlotRecord::MAIN ); + } catch ( RevisionAccessException $e ) { + $content = null; + } - return '

' . htmlspecialchars( $this->feedItemAuthor( $revision ) ) . $msg . - htmlspecialchars( FeedItem::stripComment( $comment ? $comment->text : '' ) ) . - "

\n
\n
" . $html . '
'; + if ( $content instanceof TextContent ) { + // only textual content has a "source view". + $html = nl2br( htmlspecialchars( $content->getNativeData() ) ); + } else { + // XXX: we could get an HTML representation of the content via getParserOutput, but that may + // contain JS magic and generally may not be suitable for inclusion in a feed. + // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method. + // Compare also FeedUtils::formatDiffRow. + $html = ''; } - return ''; + $comment = $revision->getComment(); + + return '

' . htmlspecialchars( $this->feedItemAuthor( $revision ) ) . $msg . + htmlspecialchars( FeedItem::stripComment( $comment ? $comment->text : '' ) ) . + "

\n
\n
" . $html . '
'; } public function getAllowedParams() { diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index 37ec3cfd32..8c0b42df21 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -69,7 +69,7 @@ class ApiFeedWatchlist extends ApiBase { 'meta' => 'siteinfo', 'siprop' => 'general', 'list' => 'watchlist', - 'wlprop' => 'title|user|comment|timestamp|ids', + 'wlprop' => 'title|user|comment|timestamp|ids|loginfo', 'wldir' => 'older', // reverse order - from newest to oldest 'wlend' => $endTime, // stop at this time 'wllimit' => min( 50, $this->getConfig()->get( 'FeedLimit' ) ) @@ -193,7 +193,12 @@ class ApiFeedWatchlist extends ApiBase { } } if ( isset( $info['revid'] ) ) { - $titleUrl = $title->getFullURL( [ 'diff' => $info['revid'] ] ); + if ( $info['revid'] === 0 && isset( $info['logid'] ) ) { + $logTitle = Title::makeTitle( NS_SPECIAL, 'Log' ); + $titleUrl = $logTitle->getFullURL( [ 'logid' => $info['logid'] ] ); + } else { + $titleUrl = $title->getFullURL( [ 'diff' => $info['revid'] ] ); + } } else { $titleUrl = $title->getFullURL( $curidParam ); } diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index 9dcde8f32b..8cb22dd1ba 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -88,6 +88,13 @@ class ApiFormatJson extends ApiFormatBase { } $data = $this->getResult()->getResultData( null, $transform ); $json = FormatJson::encode( $data, $this->getIsHtml(), $opt ); + if ( $json === false ) { + // This should never happen, but it's a bug which could crop up + // if you use ApiResult::NO_VALIDATE for instance. + // @codeCoverageIgnoreStart + $this->dieDebug( __METHOD__, 'Unable to encode API result as JSON' ); + // @codeCoverageIgnoreEnd + } // T68776: OutputHandler::mangleFlashPolicy() avoids a nasty bug in // Flash, but what it does isn't friendly for the API, so we need to diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index 84fcbeff7b..886dbcc059 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -102,7 +102,6 @@ class ApiHelp extends ApiBase { 'mediawiki.apihelp', ] ); if ( !empty( $options['toc'] ) ) { - $out->addModules( 'mediawiki.toc' ); $out->addModuleStyles( 'mediawiki.toc.styles' ); } $out->setPageTitle( $context->msg( 'api-help-title' ) ); diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index d2a7db2bea..3cc34070e2 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -829,12 +829,17 @@ class ApiMain extends ApiBase { 'dnt', 'origin', /* MediaWiki whitelist */ + 'user-agent', 'api-user-agent', ] ); foreach ( $requestedHeaders as $rHeader ) { $rHeader = strtolower( trim( $rHeader ) ); if ( !isset( $allowedAuthorHeaders[$rHeader] ) ) { - wfDebugLog( 'api', 'CORS preflight failed on requested header: ' . $rHeader ); + LoggerFactory::getInstance( 'api-warning' )->warning( + 'CORS preflight failed on requested header: {header}', [ + 'header' => $rHeader + ] + ); return false; } } @@ -1025,8 +1030,10 @@ class ApiMain extends ApiBase { } else { // Something is seriously wrong $config = $this->getConfig(); + // TODO: Avoid embedding arbitrary class names in the error code. $class = preg_replace( '#^Wikimedia\\\Rdbms\\\#', '', get_class( $e ) ); $code = 'internal_api_error_' . $class; + $data = [ 'errorclass' => get_class( $e ) ]; if ( $config->get( 'ShowExceptionDetails' ) ) { if ( $e instanceof ILocalizedException ) { $msg = $e->getMessageObject(); @@ -1040,7 +1047,7 @@ class ApiMain extends ApiBase { $params = [ 'apierror-exceptioncaughttype', WebRequest::getRequestId(), get_class( $e ) ]; } - $messages[] = ApiMessage::create( $params, $code ); + $messages[] = ApiMessage::create( $params, $code, $data ); } return $messages; } @@ -1077,7 +1084,15 @@ class ApiMain extends ApiBase { // Add errors from the exception $modulePath = $e instanceof ApiUsageException ? $e->getModulePath() : null; foreach ( $this->errorMessagesFromException( $e, 'error' ) as $msg ) { - $errorCodes[$msg->getApiCode()] = true; + if ( ApiErrorFormatter::isValidApiCode( $msg->getApiCode() ) ) { + $errorCodes[$msg->getApiCode()] = true; + } else { + LoggerFactory::getInstance( 'api-warning' )->error( 'Invalid API error code "{code}"', [ + 'code' => $msg->getApiCode(), + 'exception' => $e, + ] ); + $errorCodes[''] = true; + } $formatter->addError( $modulePath, $msg ); } foreach ( $this->errorMessagesFromException( $e, 'warning' ) as $msg ) { @@ -1464,9 +1479,14 @@ class ApiMain extends ApiBase { if ( $numLagged >= ceil( $replicaCount / 2 ) ) { $laggedServers = implode( ', ', $laggedServers ); wfDebugLog( - 'api-readonly', + 'api-readonly', // Deprecate this channel in favor of api-warning? "Api request failed as read only because the following DBs are lagged: $laggedServers" ); + LoggerFactory::getInstance( 'api-warning' )->warning( + "Api request failed as read only because the following DBs are lagged: {laggeddbs}", [ + 'laggeddbs' => $laggedServers, + ] + ); $this->dieWithError( 'readonly_lag', @@ -1512,7 +1532,13 @@ class ApiMain extends ApiBase { * @param array $params An array with the request parameters */ protected function setupExternalResponse( $module, $params ) { + $validMethods = [ 'GET', 'HEAD', 'POST', 'OPTIONS' ]; $request = $this->getRequest(); + + if ( !in_array( $request->getMethod(), $validMethods ) ) { + $this->dieWithError( 'apierror-invalidmethod', null, null, 405 ); + } + if ( !$request->wasPosted() && $module->mustBePosted() ) { // Module requires POST. GET request might still be allowed // if $wgDebugApi is true, otherwise fail. diff --git a/includes/api/ApiMessageTrait.php b/includes/api/ApiMessageTrait.php index 18b6bc42c1..6894d2809f 100644 --- a/includes/api/ApiMessageTrait.php +++ b/includes/api/ApiMessageTrait.php @@ -105,12 +105,15 @@ trait ApiMessageTrait { } else { $this->apiCode = $key; } + + // Ensure the code is actually valid + $this->apiCode = preg_replace( '/[^a-zA-Z0-9_-]/', '_', $this->apiCode ); } return $this->apiCode; } public function setApiCode( $code, array $data = null ) { - if ( $code !== null && !( is_string( $code ) && $code !== '' ) ) { + if ( $code !== null && !ApiErrorFormatter::isValidApiCode( $code ) ) { throw new InvalidArgumentException( "Invalid code \"$code\"" ); } diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index 194a511061..4ffe873ccf 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -819,8 +819,9 @@ class ApiPageSet extends ApiBase { /** * Does the same as initFromTitles(), but is based on page IDs instead * @param array $pageids Array of page IDs + * @param bool $filterIds Whether the IDs need filtering */ - private function initFromPageIds( $pageids ) { + private function initFromPageIds( $pageids, $filterIds = true ) { if ( !$pageids ) { return; } @@ -828,7 +829,9 @@ class ApiPageSet extends ApiBase { $pageids = array_map( 'intval', $pageids ); // paranoia $remaining = array_flip( $pageids ); - $pageids = self::getPositiveIntegers( $pageids ); + if ( $filterIds ) { + $pageids = $this->filterIDs( [ [ 'page', 'page_id' ] ], $pageids ); + } $res = null; if ( !empty( $pageids ) ) { @@ -939,9 +942,10 @@ class ApiPageSet extends ApiBase { $pageids = []; $remaining = array_flip( $revids ); - $revids = self::getPositiveIntegers( $revids ); + $revids = $this->filterIDs( [ [ 'revision', 'rev_id' ], [ 'archive', 'ar_rev_id' ] ], $revids ); + $goodRemaining = array_flip( $revids ); - if ( !empty( $revids ) ) { + if ( $revids ) { $tables = [ 'revision', 'page' ]; $fields = [ 'rev_id', 'rev_page' ]; $where = [ 'rev_id' => $revids, 'rev_page = page_id' ]; @@ -955,22 +959,20 @@ class ApiPageSet extends ApiBase { $this->mLiveRevIDs[$revid] = $pageid; $pageids[$pageid] = ''; unset( $remaining[$revid] ); + unset( $goodRemaining[$revid] ); } } - $this->mMissingRevIDs = array_keys( $remaining ); - // Populate all the page information - $this->initFromPageIds( array_keys( $pageids ) ); + $this->initFromPageIds( array_keys( $pageids ), false ); // If the user can see deleted revisions, pull out the corresponding // titles from the archive table and include them too. We ignore // ar_page_id because deleted revisions are tied by title, not page_id. - if ( !empty( $this->mMissingRevIDs ) && $this->getUser()->isAllowed( 'deletedhistory' ) ) { - $remaining = array_flip( $this->mMissingRevIDs ); + if ( $goodRemaining && $this->getUser()->isAllowed( 'deletedhistory' ) ) { $tables = [ 'archive' ]; $fields = [ 'ar_rev_id', 'ar_namespace', 'ar_title' ]; - $where = [ 'ar_rev_id' => $this->mMissingRevIDs ]; + $where = [ 'ar_rev_id' => array_keys( $goodRemaining ) ]; $res = $db->select( $tables, $fields, $where, __METHOD__ ); $titles = []; @@ -1002,9 +1004,9 @@ class ApiPageSet extends ApiBase { $remaining[$revid] = true; } } - - $this->mMissingRevIDs = array_keys( $remaining ); } + + $this->mMissingRevIDs = array_keys( $remaining ); } /** @@ -1416,25 +1418,6 @@ class ApiPageSet extends ApiBase { return $this->mDbSource->getDB(); } - /** - * Returns the input array of integers with all values < 0 removed - * - * @param array $array - * @return array - */ - private static function getPositiveIntegers( $array ) { - // T27734 API: possible issue with revids validation - // It seems with a load of revision rows, MySQL gets upset - // Remove any < 0 integers, as they can't be valid - foreach ( $array as $i => $int ) { - if ( $int < 0 ) { - unset( $array[$i] ); - } - } - - return $array; - } - public function getAllowedParams( $flags = 0 ) { $result = [ 'titles' => [ diff --git a/includes/api/ApiQueryAllDeletedRevisions.php b/includes/api/ApiQueryAllDeletedRevisions.php index 7b4f15e517..7a1c461000 100644 --- a/includes/api/ApiQueryAllDeletedRevisions.php +++ b/includes/api/ApiQueryAllDeletedRevisions.php @@ -43,8 +43,6 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { * @return void */ protected function run( ApiPageSet $resultPageSet = null ) { - global $wgChangeTagsSchemaMigrationStage; - // Before doing anything at all, let's check permissions $this->checkUserRightsAny( 'deletedhistory' ); @@ -127,11 +125,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { } if ( $this->fld_tags ) { - $this->addTables( 'tag_summary' ); - $this->addJoinConds( - [ 'tag_summary' => [ 'LEFT JOIN', [ 'ar_rev_id=ts_rev_id' ] ] ] - ); - $this->addFields( 'ts_tags' ); + $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] ); } if ( !is_null( $params['tag'] ) ) { @@ -139,16 +133,12 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { $this->addJoinConds( [ 'change_tag' => [ 'INNER JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ] ); - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); - try { - $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); - } catch ( NameTableAccessException $exception ) { - // Return nothing. - $this->addWhere( '1=0' ); - } - } else { - $this->addWhereFld( 'ct_tag', $params['tag'] ); + $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); + try { + $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); + } catch ( NameTableAccessException $exception ) { + // Return nothing. + $this->addWhere( '1=0' ); } } diff --git a/includes/api/ApiQueryAllRevisions.php b/includes/api/ApiQueryAllRevisions.php index 922d2c3e25..5343c33747 100644 --- a/includes/api/ApiQueryAllRevisions.php +++ b/includes/api/ApiQueryAllRevisions.php @@ -140,11 +140,7 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { $this->addTimestampWhereRange( $tsField, $dir, $params['start'], $params['end'] ); if ( $this->fld_tags ) { - $this->addTables( 'tag_summary' ); - $this->addJoinConds( - [ 'tag_summary' => [ 'LEFT JOIN', [ 'rev_id=ts_rev_id' ] ] ] - ); - $this->addFields( 'ts_tags' ); + $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'revision' ) ] ); } if ( $params['user'] !== null ) { diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index 35cb83ace3..f4e7463e22 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -418,7 +418,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if ( is_null( $resultPageSet ) ) { // Try to add the result data in one go and pray that it fits $code = $this->bl_code; - $data = array_map( function ( $arr ) use ( $result, $code ) { + $data = array_map( function ( $arr ) use ( $code ) { if ( isset( $arr['redirlinks'] ) ) { $arr['redirlinks'] = array_values( $arr['redirlinks'] ); ApiResult::setIndexedTagName( $arr['redirlinks'], $code ); diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 8630561b2e..d9fe50b8d6 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -263,6 +263,30 @@ abstract class ApiQueryBase extends ApiBase { } } + /** + * Like addWhereFld for an integer list of IDs + * @since 1.33 + * @param string $table Table name + * @param string $field Field name + * @param int[] $ids IDs + * @return int Count of IDs actually included + */ + protected function addWhereIDsFld( $table, $field, $ids ) { + // Use count() to its full documented capabilities to simultaneously + // test for null, empty array or empty countable object + if ( count( $ids ) ) { + $ids = $this->filterIDs( [ [ $table, $field ] ], $ids ); + + if ( !count( $ids ) ) { + // Return nothing, no IDs are valid + $this->where[] = '0 = 1'; + } else { + $this->where[$field] = $ids; + } + } + return count( $ids ); + } + /** * Add a WHERE clause corresponding to a range, and an ORDER BY * clause to sort in the right direction @@ -402,13 +426,15 @@ abstract class ApiQueryBase extends ApiBase { } /** + * @deprecated since 1.33, use LinkFilter::getQueryConditions() instead * @param string|null $query * @param string|null $protocol * @return null|string */ public function prepareUrlQuerySearchString( $query = null, $protocol = null ) { + wfDeprecated( __METHOD__, '1.33' ); $db = $this->getDB(); - if ( !is_null( $query ) || $query != '' ) { + if ( $query !== null && $query !== '' ) { if ( is_null( $protocol ) ) { $protocol = 'http://'; } @@ -438,32 +464,31 @@ abstract class ApiQueryBase extends ApiBase { public function showHiddenUsersAddBlockInfo( $showBlockInfo ) { $db = $this->getDB(); - $this->addTables( 'ipblocks' ); - $this->addJoinConds( [ - 'ipblocks' => [ 'LEFT JOIN', [ + $tables = [ 'ipblocks' ]; + $fields = [ 'ipb_deleted' ]; + $joinConds = [ + 'blk' => [ 'LEFT JOIN', [ 'ipb_user=user_id', 'ipb_expiry > ' . $db->addQuotes( $db->timestamp() ), ] ], - ] ); - - $this->addFields( 'ipb_deleted' ); + ]; if ( $showBlockInfo ) { - $this->addFields( [ + $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' ); + $commentQuery = CommentStore::getStore()->getJoin( 'ipb_reason' ); + $tables += $actorQuery['tables'] + $commentQuery['tables']; + $joinConds += $actorQuery['joins'] + $commentQuery['joins']; + $fields = array_merge( $fields, [ 'ipb_id', 'ipb_expiry', 'ipb_timestamp' - ] ); - $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'] ); + ], $actorQuery['fields'], $commentQuery['fields'] ); } + $this->addTables( [ 'blk' => $tables ] ); + $this->addFields( $fields ); + $this->addJoinConds( $joinConds ); + // Don't show hidden names if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { $this->addWhere( 'ipb_deleted = 0 OR ipb_deleted IS NULL' ); diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index 3cd2aceceb..95f8cda818 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -103,7 +103,7 @@ class ApiQueryBlocks extends ApiQueryBase { } if ( isset( $params['ids'] ) ) { - $this->addWhereFld( 'ipb_id', $params['ids'] ); + $this->addWhereIDsFld( 'ipblocks', 'ipb_id', $params['ids'] ); } if ( isset( $params['users'] ) ) { $usernames = []; @@ -187,7 +187,7 @@ class ApiQueryBlocks extends ApiQueryBase { $restrictions = []; if ( $fld_restrictions ) { - $restrictions = $this->getRestrictionData( $res, $params['limit'] ); + $restrictions = self::getRestrictionData( $res, $params['limit'] ); } $count = 0; diff --git a/includes/api/ApiQueryDeletedRevisions.php b/includes/api/ApiQueryDeletedRevisions.php index 8f71c1c4d7..9275a7c727 100644 --- a/includes/api/ApiQueryDeletedRevisions.php +++ b/includes/api/ApiQueryDeletedRevisions.php @@ -39,8 +39,6 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { } protected function run( ApiPageSet $resultPageSet = null ) { - global $wgChangeTagsSchemaMigrationStage; - $user = $this->getUser(); // Before doing anything at all, let's check permissions $this->checkUserRightsAny( 'deletedhistory' ); @@ -79,11 +77,7 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { } if ( $this->fld_tags ) { - $this->addTables( 'tag_summary' ); - $this->addJoinConds( - [ 'tag_summary' => [ 'LEFT JOIN', [ 'ar_rev_id=ts_rev_id' ] ] ] - ); - $this->addFields( 'ts_tags' ); + $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] ); } if ( !is_null( $params['tag'] ) ) { @@ -91,16 +85,12 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { $this->addJoinConds( [ 'change_tag' => [ 'INNER JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ] ); - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); - try { - $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); - } catch ( NameTableAccessException $exception ) { - // Return nothing. - $this->addWhere( '1=0' ); - } - } else { - $this->addWhereFld( 'ct_tag', $params['tag'] ); + $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); + try { + $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); + } catch ( NameTableAccessException $exception ) { + // Return nothing. + $this->addWhere( '1=0' ); } } diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index e84b9b2247..8540190f1e 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -36,8 +36,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase { } public function execute() { - global $wgChangeTagsSchemaMigrationStage; - // Before doing anything at all, let's check permissions $this->checkUserRightsAny( 'deletedhistory' ); @@ -133,11 +131,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { } if ( $fld_tags ) { - $this->addTables( 'tag_summary' ); - $this->addJoinConds( - [ 'tag_summary' => [ 'LEFT JOIN', [ 'ar_rev_id=ts_rev_id' ] ] ] - ); - $this->addFields( 'ts_tags' ); + $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] ); } if ( !is_null( $params['tag'] ) ) { @@ -145,16 +139,12 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $this->addJoinConds( [ 'change_tag' => [ 'INNER JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ] ); - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); - try { - $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); - } catch ( NameTableAccessException $exception ) { - // Return nothing. - $this->addWhere( '1=0' ); - } - } else { - $this->addWhereFld( 'ct_tag', $params['tag'] ); + $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); + try { + $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); + } catch ( NameTableAccessException $exception ) { + // Return nothing. + $this->addWhere( '1=0' ); } } diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index fc5d8a0425..d508c55fec 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -47,12 +47,12 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { */ private function run( $resultPageSet = null ) { $params = $this->extractRequestParams(); + $db = $this->getDB(); $query = $params['query']; $protocol = self::getProtocolPrefix( $params['protocol'] ); - $this->addTables( [ 'page', 'externallinks' ] ); // must be in this order for 'USE INDEX' - $this->addOption( 'USE INDEX', 'el_index' ); + $this->addTables( [ 'page', 'externallinks' ] ); $this->addWhere( 'page_id=el_from' ); $miser_ns = []; @@ -62,15 +62,46 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { $this->addWhereFld( 'page_namespace', $params['namespace'] ); } - // Normalize query to match the normalization applied for the externallinks table - $query = Parser::normalizeLinkUrl( $query ); + $orderBy = []; - $whereQuery = $this->prepareUrlQuerySearchString( $query, $protocol ); + if ( $query !== null && $query !== '' ) { + if ( $protocol === null ) { + $protocol = 'http://'; + } + + // Normalize query to match the normalization applied for the externallinks table + $query = Parser::normalizeLinkUrl( $protocol . $query ); + + $conds = LinkFilter::getQueryConditions( $query, [ + 'protocol' => '', + 'oneWildcard' => true, + 'db' => $db + ] ); + if ( !$conds ) { + $this->dieWithError( 'apierror-badquery' ); + } + $this->addWhere( $conds ); + if ( !isset( $conds['el_index_60'] ) ) { + $orderBy[] = 'el_index_60'; + } + } else { + $orderBy[] = 'el_index_60'; - if ( $whereQuery !== null ) { - $this->addWhere( $whereQuery ); + if ( $protocol !== null ) { + $this->addWhere( 'el_index_60' . $db->buildLike( "$protocol", $db->anyString() ) ); + } else { + // We're querying all protocols, filter out duplicate protocol-relative links + $this->addWhere( $db->makeList( [ + 'el_to NOT' . $db->buildLike( '//', $db->anyString() ), + 'el_index_60 ' . $db->buildLike( 'http://', $db->anyString() ), + ], LIST_OR ) ); + } } + $orderBy[] = 'el_id'; + $this->addOption( 'ORDER BY', $orderBy ); + $this->addFields( $orderBy ); // Make sure + $prop = array_flip( $params['prop'] ); $fld_ids = isset( $prop['ids'] ); $fld_title = isset( $prop['title'] ); @@ -88,10 +119,19 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { } $limit = $params['limit']; - $offset = $params['offset']; $this->addOption( 'LIMIT', $limit + 1 ); - if ( isset( $offset ) ) { - $this->addOption( 'OFFSET', $offset ); + + if ( $params['continue'] !== null ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) !== count( $orderBy ) ); + $i = count( $cont ) - 1; + $cond = $orderBy[$i] . ' >= ' . $db->addQuotes( rawurldecode( $cont[$i] ) ); + while ( $i-- > 0 ) { + $field = $orderBy[$i]; + $v = $db->addQuotes( rawurldecode( $cont[$i] ) ); + $cond = "($field > $v OR ($field = $v AND $cond))"; + } + $this->addWhere( $cond ); } $res = $this->select( __METHOD__ ); @@ -102,7 +142,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { if ( ++$count > $limit ) { // We've reached the one extra which shows that there are // additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'offset', $offset + $limit ); + $this->setContinue( $orderBy, $row ); break; } @@ -131,7 +171,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { } $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'offset', $offset + $count - 1 ); + $this->setContinue( $orderBy, $row ); break; } } else { @@ -145,6 +185,14 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { } } + private function setContinue( $orderBy, $row ) { + $fields = []; + foreach ( $orderBy as $field ) { + $fields[] = strtr( $row->$field, [ '%' => '%25', '|' => '%7C' ] ); + } + $this->setContinueEnumParameter( 'continue', implode( '|', $fields ) ); + } + public function getAllowedParams() { $ret = [ 'prop' => [ @@ -157,8 +205,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { ], ApiBase::PARAM_HELP_MSG_PER_VALUE => [], ], - 'offset' => [ - ApiBase::PARAM_TYPE => 'integer', + 'continue' => [ ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', ], 'protocol' => [ diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php index 6c219d4a91..b5731a33f6 100644 --- a/includes/api/ApiQueryExternalLinks.php +++ b/includes/api/ApiQueryExternalLinks.php @@ -37,6 +37,7 @@ class ApiQueryExternalLinks extends ApiQueryBase { } $params = $this->extractRequestParams(); + $db = $this->getDB(); $query = $params['query']; $protocol = ApiQueryExtLinksUsage::getProtocolPrefix( $params['protocol'] ); @@ -49,26 +50,64 @@ class ApiQueryExternalLinks extends ApiQueryBase { $this->addTables( 'externallinks' ); $this->addWhereFld( 'el_from', array_keys( $this->getPageSet()->getGoodTitles() ) ); - $whereQuery = $this->prepareUrlQuerySearchString( $query, $protocol ); - - if ( $whereQuery !== null ) { - $this->addWhere( $whereQuery ); - } + $orderBy = []; // Don't order by el_from if it's constant in the WHERE clause if ( count( $this->getPageSet()->getGoodTitles() ) != 1 ) { - $this->addOption( 'ORDER BY', 'el_from' ); + $orderBy[] = 'el_from'; } - // If we're querying all protocols, use DISTINCT to avoid repeating protocol-relative links twice - if ( $protocol === null ) { - $this->addOption( 'DISTINCT' ); + if ( $query !== null && $query !== '' ) { + if ( $protocol === null ) { + $protocol = 'http://'; + } + + // Normalize query to match the normalization applied for the externallinks table + $query = Parser::normalizeLinkUrl( $protocol . $query ); + + $conds = LinkFilter::getQueryConditions( $query, [ + 'protocol' => '', + 'oneWildcard' => true, + 'db' => $db + ] ); + if ( !$conds ) { + $this->dieWithError( 'apierror-badquery' ); + } + $this->addWhere( $conds ); + if ( !isset( $conds['el_index_60'] ) ) { + $orderBy[] = 'el_index_60'; + } + } else { + $orderBy[] = 'el_index_60'; + + if ( $protocol !== null ) { + $this->addWhere( 'el_index_60' . $db->buildLike( "$protocol", $db->anyString() ) ); + } else { + // We're querying all protocols, filter out duplicate protocol-relative links + $this->addWhere( $db->makeList( [ + 'el_to NOT' . $db->buildLike( '//', $db->anyString() ), + 'el_index_60 ' . $db->buildLike( 'http://', $db->anyString() ), + ], LIST_OR ) ); + } } + $orderBy[] = 'el_id'; + $this->addOption( 'ORDER BY', $orderBy ); + $this->addFields( $orderBy ); // Make sure + $this->addOption( 'LIMIT', $params['limit'] + 1 ); - $offset = $params['offset'] ?? 0; - if ( $offset ) { - $this->addOption( 'OFFSET', $params['offset'] ); + + if ( $params['continue'] !== null ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) !== count( $orderBy ) ); + $i = count( $cont ) - 1; + $cond = $orderBy[$i] . ' >= ' . $db->addQuotes( rawurldecode( $cont[$i] ) ); + while ( $i-- > 0 ) { + $field = $orderBy[$i]; + $v = $db->addQuotes( rawurldecode( $cont[$i] ) ); + $cond = "($field > $v OR ($field = $v AND $cond))"; + } + $this->addWhere( $cond ); } $res = $this->select( __METHOD__ ); @@ -78,7 +117,7 @@ class ApiQueryExternalLinks 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( 'offset', $offset + $params['limit'] ); + $this->setContinue( $orderBy, $row ); break; } $entry = []; @@ -90,12 +129,20 @@ class ApiQueryExternalLinks extends ApiQueryBase { ApiResult::setContentValue( $entry, 'url', $to ); $fit = $this->addPageSubItem( $row->el_from, $entry ); if ( !$fit ) { - $this->setContinueEnumParameter( 'offset', $offset + $count - 1 ); + $this->setContinue( $orderBy, $row ); break; } } } + private function setContinue( $orderBy, $row ) { + $fields = []; + foreach ( $orderBy as $field ) { + $fields[] = strtr( $row->$field, [ '%' => '%25', '|' => '%7C' ] ); + } + $this->setContinueEnumParameter( 'continue', implode( '|', $fields ) ); + } + public function getCacheMode( $params ) { return 'public'; } @@ -109,8 +156,7 @@ class ApiQueryExternalLinks extends ApiQueryBase { ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 ], - 'offset' => [ - ApiBase::PARAM_TYPE => 'integer', + 'continue' => [ ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', ], 'protocol' => [ diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 3cb55e4169..edf7002280 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -42,8 +42,6 @@ class ApiQueryLogEvents extends ApiQueryBase { $fld_details = false, $fld_tags = false; public function execute() { - global $wgChangeTagsSchemaMigrationStage; - $params = $this->extractRequestParams(); $db = $this->getDB(); $this->commentStore = CommentStore::getStore(); @@ -109,25 +107,19 @@ class ApiQueryLogEvents extends ApiQueryBase { } if ( $this->fld_tags ) { - $this->addTables( 'tag_summary' ); - $this->addJoinConds( [ 'tag_summary' => [ 'LEFT JOIN', 'log_id=ts_log_id' ] ] ); - $this->addFields( 'ts_tags' ); + $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'logging' ) ] ); } if ( !is_null( $params['tag'] ) ) { $this->addTables( 'change_tag' ); $this->addJoinConds( [ 'change_tag' => [ 'INNER JOIN', [ 'log_id=ct_log_id' ] ] ] ); - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); - try { - $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); - } catch ( NameTableAccessException $exception ) { - // Return nothing. - $this->addWhere( '1=0' ); - } - } else { - $this->addWhereFld( 'ct_tag', $params['tag'] ); + $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); + try { + $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); + } catch ( NameTableAccessException $exception ) { + // Return nothing. + $this->addWhere( '1=0' ); } } diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php index 8758d9c232..ea2066490e 100644 --- a/includes/api/ApiQueryQueryPage.php +++ b/includes/api/ApiQueryQueryPage.php @@ -20,24 +20,33 @@ * @file */ +use MediaWiki\MediaWikiServices; +use MediaWiki\Special\SpecialPageFactory; + /** * Query module to get the results of a QueryPage-based special page * * @ingroup API */ class ApiQueryQueryPage extends ApiQueryGeneratorBase { - private $qpMap; + + /** + * @var string[] list of special page names + */ + private $queryPages; + + /** + * @var SpecialPageFactory + */ + private $specialPageFactory; public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'qp' ); - // Build mapping from special page names to QueryPage classes - $uselessQueryPages = $this->getConfig()->get( 'APIUselessQueryPages' ); - $this->qpMap = []; - foreach ( QueryPage::getPages() as $page ) { - if ( !in_array( $page[1], $uselessQueryPages ) ) { - $this->qpMap[$page[1]] = $page[0]; - } - } + $this->queryPages = array_values( array_diff( + array_column( QueryPage::getPages(), 1 ), // [ class, name ] + $this->getConfig()->get( 'APIUselessQueryPages' ) + ) ); + $this->specialPageFactory = MediaWikiServices::getInstance()->getSpecialPageFactory(); } public function execute() { @@ -48,6 +57,27 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { $this->run( $resultPageSet ); } + /** + * @param string $name + * @return QueryPage + */ + private function getSpecialPage( $name ) { + $qp = $this->specialPageFactory->getPage( $name ); + if ( !$qp ) { + self::dieDebug( + __METHOD__, + 'SpecialPageFactory failed to create special page ' . $name + ); + } + if ( !( $qp instanceof QueryPage ) ) { + self::dieDebug( + __METHOD__, + 'Special page ' . $name . ' is not a QueryPage' + ); + } + return $qp; + } + /** * @param ApiPageSet|null $resultPageSet */ @@ -55,8 +85,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); $result = $this->getResult(); - /** @var QueryPage $qp */ - $qp = new $this->qpMap[$params['page']](); + $qp = $this->getSpecialPage( $params['page'] ); if ( !$qp->userCanExecute( $this->getUser() ) ) { $this->dieWithError( 'apierror-specialpage-cantexecute' ); } @@ -125,8 +154,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { } public function getCacheMode( $params ) { - /** @var QueryPage $qp */ - $qp = new $this->qpMap[$params['page']](); + $qp = $this->getSpecialPage( $params['page'] ); if ( $qp->getRestriction() != '' ) { return 'private'; } @@ -137,7 +165,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { public function getAllowedParams() { return [ 'page' => [ - ApiBase::PARAM_TYPE => array_keys( $this->qpMap ), + ApiBase::PARAM_TYPE => $this->queryPages, ApiBase::PARAM_REQUIRED => true ], 'offset' => [ diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index b1dcf0dba1..7c6b4634e5 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -143,8 +143,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { * @param ApiPageSet|null $resultPageSet */ public function run( $resultPageSet = null ) { - global $wgChangeTagsSchemaMigrationStage; - $user = $this->getUser(); /* Get the parameters of the request. */ $params = $this->extractRequestParams(); @@ -339,9 +337,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $resultPageSet && $params['generaterevisions'] ); if ( $this->fld_tags ) { - $this->addTables( 'tag_summary' ); - $this->addJoinConds( [ 'tag_summary' => [ 'LEFT JOIN', [ 'rc_id=ts_rc_id' ] ] ] ); - $this->addFields( 'ts_tags' ); + $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'recentchanges' ) ] ); } if ( $this->fld_sha1 ) { @@ -365,16 +361,12 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { if ( !is_null( $params['tag'] ) ) { $this->addTables( 'change_tag' ); $this->addJoinConds( [ 'change_tag' => [ 'INNER JOIN', [ 'rc_id=ct_rc_id' ] ] ] ); - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); - try { - $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); - } catch ( NameTableAccessException $exception ) { - // Return nothing. - $this->addWhere( '1=0' ); - } - } else { - $this->addWhereFld( 'ct_tag', $params['tag'] ); + $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); + try { + $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); + } catch ( NameTableAccessException $exception ) { + // Return nothing. + $this->addWhere( '1=0' ); } } diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index ac7ee0ae9f..cb2f6168ae 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -84,7 +84,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { } protected function run( ApiPageSet $resultPageSet = null ) { - global $wgActorTableSchemaMigrationStage, $wgChangeTagsSchemaMigrationStage; + global $wgActorTableSchemaMigrationStage; $params = $this->extractRequestParams( false ); $revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); @@ -185,11 +185,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { } if ( $this->fld_tags ) { - $this->addTables( 'tag_summary' ); - $this->addJoinConds( - [ 'tag_summary' => [ 'LEFT JOIN', [ 'rev_id=ts_rev_id' ] ] ] - ); - $this->addFields( 'ts_tags' ); + $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'revision' ) ] ); } if ( $params['tag'] !== null ) { @@ -197,16 +193,12 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { $this->addJoinConds( [ 'change_tag' => [ 'INNER JOIN', [ 'rev_id=ct_rev_id' ] ] ] ); - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); - try { - $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); - } catch ( NameTableAccessException $exception ) { - // Return nothing. - $this->addWhere( '1=0' ); - } - } else { - $this->addWhereFld( 'ct_tag', $params['tag'] ); + $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); + try { + $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) ); + } catch ( NameTableAccessException $exception ) { + // Return nothing. + $this->addWhere( '1=0' ); } } diff --git a/includes/api/ApiQueryRevisionsBase.php b/includes/api/ApiQueryRevisionsBase.php index c00010a131..3d0a0fba62 100644 --- a/includes/api/ApiQueryRevisionsBase.php +++ b/includes/api/ApiQueryRevisionsBase.php @@ -616,10 +616,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { } public function getAllowedParams() { - $slotRoles = MediaWikiServices::getInstance()->getSlotRoleStore()->getMap(); - if ( !in_array( SlotRecord::MAIN, $slotRoles, true ) ) { - $slotRoles[] = SlotRecord::MAIN; - } + $slotRoles = MediaWikiServices::getInstance()->getSlotRoleRegistry()->getKnownRoles(); sort( $slotRoles, SORT_STRING ); return [ diff --git a/includes/api/ApiQueryUserContribs.php b/includes/api/ApiQueryUserContribs.php index f16f958eb1..ed831306eb 100644 --- a/includes/api/ApiQueryUserContribs.php +++ b/includes/api/ApiQueryUserContribs.php @@ -321,7 +321,7 @@ class ApiQueryUserContribs extends ApiQueryBase { * @param int $limit */ private function prepareQuery( array $users, $limit ) { - global $wgActorTableSchemaMigrationStage, $wgChangeTagsSchemaMigrationStage; + global $wgActorTableSchemaMigrationStage; $this->resetQueryParams(); $db = $this->getDB(); @@ -478,11 +478,7 @@ class ApiQueryUserContribs extends ApiQueryBase { $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled ); if ( $this->fld_tags ) { - $this->addTables( 'tag_summary' ); - $this->addJoinConds( - [ 'tag_summary' => [ 'LEFT JOIN', [ $idField . ' = ts_rev_id' ] ] ] - ); - $this->addFields( 'ts_tags' ); + $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'revision' ) ] ); } if ( isset( $this->params['tag'] ) ) { @@ -490,16 +486,12 @@ class ApiQueryUserContribs extends ApiQueryBase { $this->addJoinConds( [ 'change_tag' => [ 'INNER JOIN', [ $idField . ' = ct_rev_id' ] ] ] ); - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); - try { - $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $this->params['tag'] ) ); - } catch ( NameTableAccessException $exception ) { - // Return nothing. - $this->addWhere( '1=0' ); - } - } else { - $this->addWhereFld( 'ct_tag', $this->params['tag'] ); + $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); + try { + $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $this->params['tag'] ) ); + } catch ( NameTableAccessException $exception ) { + // Return nothing. + $this->addWhere( '1=0' ); } } } diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index db57f7e034..18aa6daa66 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -56,9 +56,13 @@ class ApiRollback extends ApiBase { } // @TODO: remove this hack once rollback uses POST (T88044) + $fname = __METHOD__; $trxLimits = $this->getConfig()->get( 'TrxProfilerLimits' ); $trxProfiler = Profiler::instance()->getTransactionProfiler(); - $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ ); + $trxProfiler->redefineExpectations( $trxLimits['POST'], $fname ); + DeferredUpdates::addCallableUpdate( function () use ( $trxProfiler, $trxLimits, $fname ) { + $trxProfiler->redefineExpectations( $trxLimits['PostSend-POST'], $fname ); + } ); $retval = $pageObj->doRollback( $this->getRbUser( $params ), diff --git a/includes/api/i18n/ar.json b/includes/api/i18n/ar.json index c75238c40a..518a4fa685 100644 --- a/includes/api/i18n/ar.json +++ b/includes/api/i18n/ar.json @@ -1461,11 +1461,11 @@ "apihelp-json-param-callback": "إذا تم تحديده، فسيقوم بإخراج الإخراج في استدعاء دالة معينة، للسلامة; سيتم تقييد جميع البيانات الخاصة بالمستخدم.", "apihelp-json-param-utf8": "إذا تم تحديده، يقوم بترميز معظم (وليس كل) الأحرف غير ASCII كـUTF-8 بدلا من استبدالها بتسلسلات الهروب السداسية العشرية، افتراضي عندما لا يكون formatversion 1.", "apihelp-json-param-ascii": "إذا تم تحديده، يشفر كل غير ASCII باستخدام تسلسلات الهروب السداسية العشرية، افتراضي عندما يكون formatversion 1.", - "apihelp-json-param-formatversion": "تنسيق الإخراج: \n;1:تنسيق متوافق مع الإصدارات السابقة (مصفوفات منطقية بتنسيق XML، ومفاتيح * لعقد المحتوى، وما إلى ذلك).\n;2:التنسيق الحديث التجريبي، التفاصيل قد تتغير!\n;الأحدث: استخدم أحدث تنسيق (حاليا 2)، قد يتغير دون سابق إنذار.", + "apihelp-json-param-formatversion": "تنسيق الإخراج: \n;1:تنسيق متوافق مع الإصدارات السابقة (مصفوفات منطقية بتنسيق XML، ومفاتيح * لعقد المحتوى، وما إلى ذلك).\n;2:التنسيق الحديث.\n;الأحدث: استخدم أحدث تنسيق (حاليا 2)، قد يتغير دون سابق إنذار.", "apihelp-jsonfm-summary": "بيانات الإخراج بتنسيق JSON (الطباعة بـHTML).", "apihelp-none-summary": "عدم إخراج أي شيء.", "apihelp-php-summary": "بيانات الإخراج بتنسيق PHP المتسلسل.", - "apihelp-php-param-formatversion": "تنسيق الإخراج: \n;1:تنسيق متوافق مع الإصدارات السابقة (مصفوفات منطقية بتنسيق XML، ومفاتيح * لعقد المحتوى، وما إلى ذلك).\n;2:التنسيق الحديث التجريبي، التفاصيل قد تتغير!\n;الأحدث: استخدم أحدث تنسيق (حاليا 2)، قد يتغير دون سابق إنذار.", + "apihelp-php-param-formatversion": "تنسيق الإخراج: \n;1:تنسيق متوافق مع الإصدارات السابقة (مصفوفات منطقية بتنسيق XML، ومفاتيح * لعقد المحتوى، وما إلى ذلك).\n;2:التنسيق الحديث.\n;الأحدث: استخدم أحدث تنسيق (حاليا 2)، قد يتغير دون سابق إنذار.", "apihelp-phpfm-summary": "بيانات الإخراج بتنسيق JSON (الطباعة بـHTML).", "apihelp-rawfm-summary": "بيانات الإخراج، بما في ذلك عناصر تصحيح الأخطاء، بتنسيق JSON (الطباعة بـHTML).", "apihelp-xml-summary": "بيانات الإخراج بتنسيق XML.", @@ -1611,6 +1611,7 @@ "apierror-compare-nofromrevision": "ليس 'من' مراجعة، حدد fromrev أو fromtitle أو fromid.", "apierror-compare-notext": "لا يمكن استخدام الوسيط $1 بدون $2.", "apierror-compare-notorevision": "ليس 'إلى' مراجعة، حدد torev أو totitle أو toid.", + "apierror-compare-relative-to-deleted": "لا يمكن استخدام torelative=$1 بالنسبة لمراجعة محذوفة.", "apierror-compare-relative-to-nothing": "لا توجد مراجعة 'من' لـtorelative لتكون نسبة.", "apierror-contentserializationexception": "فشل تسلسل المحتوى: $1", "apierror-contenttoobig": "يتجاوز المحتوى الذي أدخلته حد حجم المقالة البالغ $1 {{PLURAL:$1|كيلوبايت}}.", @@ -1639,6 +1640,7 @@ "apierror-invalidexpiry": "وقت انتهاء الصلاحية غير صالح \"$1\".", "apierror-invalid-file-key": "ليس مفتاح ملف صالح.", "apierror-invalidlang": "رمز لغة غير صالح للوسيط $1.", + "apierror-invalidmethod": "طريقة HTTP غير صالحة; فكر في استخدام GET أو POST.", "apierror-invalidoldimage": "يحتوي الوسيط oldimage على تنسيق غير صالح.", "apierror-invalidparammix-cannotusewith": "لا يمكن استخدام الوسيط $1 مع $2.", "apierror-invalidparammix-mustusewith": "يمكن استخدام الوسيط $1 مع $2 فقط.", @@ -1767,6 +1769,8 @@ "apiwarn-badurlparam": "تعذر تحليل $1urlparam لـ$2، باستخدام العرض والطول فقط.", "apiwarn-badutf8": "تحتوي القيمة التي تم تمريرها لـ$1 على بيانات غير صالحة أو غير طبيعية، يجب أن تكون البيانات النصية صالحة، NFC-normalized Unicode بدون أحرف تحكم C0 غير HT (\\t) وLF (\\n) وCR (\\r).", "apiwarn-checktoken-percentencoding": "تحقق من أن الرموز مثل \"+\" في الرمز المميز يتم ترميزها بشكل صحيح في المسار.", + "apiwarn-compare-no-next": "المراجعة $2 هي أحدث مراجعة من $1، ولا توجد مراجعة لـtorelative=next للمقارنة بها.", + "apiwarn-compare-no-prev": "المراجعة $2 هي أقدم مراجعة من $1، ولا توجد مراجعة لـtorelative=next للمقارنة بها.", "apiwarn-compare-nocontentmodel": "لا يمكن تحديد نموذج محتوى، على افتراض $1.", "apiwarn-deprecation-deletedrevs": "تم إيقاف list=deletedrevs; الرجاء استخدام prop=deletedrevisions or list=alldeletedrevisions بدلا من ذلك.", "apiwarn-deprecation-httpsexpected": "HTTP المستخدمة عند توقع HTTPS.", diff --git a/includes/api/i18n/de.json b/includes/api/i18n/de.json index e6f6bafcd3..f626d2b1e3 100644 --- a/includes/api/i18n/de.json +++ b/includes/api/i18n/de.json @@ -19,7 +19,8 @@ "Luke081515", "Eddie", "Zenith", - "Tacsipacsi" + "Tacsipacsi", + "FF11" ] }, "apihelp-main-extended-description": "
\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
\nStatus: 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\nFehlerhafte Anfragen: 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

Testen: Zum einfachen Testen von API-Anfragen, siehe [[Special:ApiSandbox]].

", @@ -696,6 +697,7 @@ "apihelp-query+filearchive-example-simple": "Eine Liste aller gelöschten Dateien auflisten", "apihelp-query+filerepoinfo-summary": "Gebe Metainformationen über Bild-Repositorien zurück, die im Wiki eingerichtet sind.", "apihelp-query+filerepoinfo-paramvalue-prop-displayname": "Der menschenlesbare Name des Repositoriumwikis.", + "apihelp-query+filerepoinfo-paramvalue-prop-initialCapital": "Ob Dateinamen implizit mit einem Großbuchstaben beginnen.", "apihelp-query+filerepoinfo-paramvalue-prop-local": "Ob dieses Repositorium das lokale ist oder nicht.", "apihelp-query+filerepoinfo-paramvalue-prop-rootUrl": "Wurzel-URL-Pfad für Bildpfade.", "apihelp-query+filerepoinfo-paramvalue-prop-scriptDirUrl": "Wurzel-URL-Pfad für die MediaWiki-Installation des Repositoriumwikis.", @@ -766,6 +768,7 @@ "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-paramvalue-testactionsdetail-boolean": "Gibt einen booleschen Wert für jede Aktion zurück.", + "apihelp-query+info-paramvalue-testactionsdetail-full": "Gibt Nachrichten zurück, die erklären, warum diese Aktion nicht erlaubt ist oder ein leeres Array, wenn sie erlaubt ist.", "apihelp-query+info-paramvalue-testactionsdetail-quick": "Wie full, aber mit Überspringen von Aufwandsüberprüfungen.", "apihelp-query+info-example-simple": "Ruft Informationen über die Seite Hauptseite ab.", "apihelp-query+iwbacklinks-summary": "Findet alle Seiten, die auf einen angegebenen Interwikilink verlinken.", @@ -797,6 +800,7 @@ "apihelp-query+langlinks-paramvalue-prop-url": "Ergänzt die vollständige URL.", "apihelp-query+langlinks-paramvalue-prop-autonym": "Ergänzt den Namen der Muttersprache.", "apihelp-query+langlinks-param-dir": "Die Auflistungsrichtung.", + "apihelp-query+langlinks-param-inlanguagecode": "Sprachcode für lokalisierte Sprachnamen.", "apihelp-query+links-summary": "Gibt alle Links von den angegebenen Seiten zurück.", "apihelp-query+links-param-namespace": "Zeigt nur Links in diesen Namensräumen.", "apihelp-query+links-param-limit": "Wie viele Links zurückgegeben werden sollen.", @@ -824,12 +828,15 @@ "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-title": "Filtert Einträge auf solche, die einer Seite ähnlich sind.", "apihelp-query+logevents-param-prefix": "Filtert Einträge, die mit diesem Präfix beginnen.", + "apihelp-query+logevents-param-tag": "Listet nur Ereigniseinträge auf, die mit dieser Markierung markiert sind.", "apihelp-query+logevents-param-limit": "Wie viele Ereigniseinträge insgesamt zurückgegeben werden sollen.", "apihelp-query+logevents-example-simple": "Listet die letzten Logbuch-Ereignisse auf.", "apihelp-query+pagepropnames-param-limit": "Die maximale Anzahl zurückzugebender Namen.", "apihelp-query+pagepropnames-example-simple": "Ruft die ersten 10 Eigenschaftsnamen auf.", "apihelp-query+pageswithprop-paramvalue-prop-ids": "Fügt die Seitenkennung hinzu.", + "apihelp-query+pageswithprop-paramvalue-prop-title": "Ergänzt den Titel und die Namensraumkennung der Seite.", "apihelp-query+pageswithprop-paramvalue-prop-value": "Ergänzt den Wert der Seiteneigenschaft.", "apihelp-query+pageswithprop-param-limit": "Die maximale Anzahl zurückzugebender Seiten.", "apihelp-query+pageswithprop-param-dir": "In welche Richtung sortiert werden soll.", @@ -840,15 +847,19 @@ "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-namespace": "Listet nur Titel in diesen Namensräumen auf.", + "apihelp-query+protectedtitles-param-level": "Listet nur Titel mit diesen Schutzstufen auf.", "apihelp-query+protectedtitles-param-limit": "Wie viele Seiten insgesamt zurückgegeben werden sollen.", "apihelp-query+protectedtitles-param-start": "Startet die Auflistung bei diesem Schutz-Zeitstempel.", "apihelp-query+protectedtitles-param-end": "Stoppt die Auflistung bei diesem Schutz-Zeitstempel.", "apihelp-query+protectedtitles-param-prop": "Zurückzugebende Eigenschaften:", "apihelp-query+protectedtitles-paramvalue-prop-timestamp": "Ergänzt den Zeitstempel, wann der Schutz hinzugefügt wurde.", + "apihelp-query+protectedtitles-paramvalue-prop-comment": "Ergänzt den Kommentar für den Schutz.", + "apihelp-query+protectedtitles-paramvalue-prop-parsedcomment": "Ergänzt den geparsten Kommentar für den Schutz.", "apihelp-query+protectedtitles-paramvalue-prop-level": "Ergänzt den Schutzstatus.", "apihelp-query+protectedtitles-example-simple": "Listet geschützte Titel auf.", "apihelp-query+querypage-param-limit": "Anzahl der zurückzugebenden Ergebnisse.", "apihelp-query+random-summary": "Ruft einen Satz an zufälligen Seiten ab.", + "apihelp-query+random-param-namespace": "Gibt nur Seiten in diesen Namensräumen zurück.", "apihelp-query+recentchanges-summary": "Listet die letzten Änderungen auf.", "apihelp-query+recentchanges-param-user": "Listet nur Änderungen von diesem Benutzer auf.", "apihelp-query+recentchanges-param-excludeuser": "Listet keine Änderungen von diesem Benutzer auf.", @@ -1118,10 +1129,12 @@ "apierror-compare-nofromrevision": "Keine Version „from“. fromrev, fromtitle oder fromid angeben.", "apierror-compare-notext": "Der Parameter $1 kann nicht ohne $2 verwendet werden.", "apierror-compare-notorevision": "Keine Version „to“. torev, totitle oder toid angeben.", + "apierror-compare-relative-to-deleted": "torelative=$1 kann nicht relativ zu einer gelöschten Version verwendet werden.", "apierror-emptypage": "Das Erstellen neuer leerer Seiten ist nicht erlaubt.", "apierror-filedoesnotexist": "Die Datei ist nicht vorhanden.", "apierror-import-unknownerror": "Unbekannter Fehler beim Importieren: $1.", "apierror-invalid-file-key": "Kein gültiger Dateischlüssel.", + "apierror-invalidmethod": "Ungültige HTTP-Methode. Ziehe in Erwägung, GET oder POST zu verwenden.", "apierror-invalidsection": "Der Parameter section muss eine gültige Abschnittskennung oder new sein.", "apierror-invaliduserid": "Die Benutzerkennung $1 ist nicht gültig.", "apierror-maxbytes": "Der Parameter $1 kann nicht länger sein als {{PLURAL:$2|ein Byte|$2 Bytes}}", @@ -1145,6 +1158,8 @@ "apierror-unknownerror-nocode": "Unbekannter Fehler.", "apierror-unknownerror": "Unbekannter Fehler: „$1“.", "apierror-unknownformat": "Nicht erkanntes Format „$1“.", + "apiwarn-compare-no-next": "Die Version $2 ist die aktuelle Version von $1. Es gibt keine zu vergleichende Version für torelative=next.", + "apiwarn-compare-no-prev": "Die Version $2 ist die aktuelle Version von $1. Es gibt keine zu vergleichende Version für torelative=prev.", "apiwarn-deprecation-missingparam": "Da $1 nicht angegeben wurde, wurde ein veraltetes Format für die Ausgabe verwendet. Dieses Format ist veraltet und in Zukunft wird immer das neue Format benutzt.", "apiwarn-ignoring-invalid-templated-value": "Ignorieren des Wertes $2 in $1 bei der Verarbeitung von Vorlagenparametern.", "apiwarn-invalidcategory": "„$1“ ist keine Kategorie.", diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 83bb6e65d5..8f5ba8d492 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -1570,11 +1570,11 @@ "apihelp-json-param-callback": "If specified, wraps the output into a given function call. For safety, all user-specific data will be restricted.", "apihelp-json-param-utf8": "If specified, encodes most (but not all) non-ASCII characters as UTF-8 instead of replacing them with hexadecimal escape sequences. Default when formatversion is not 1.", "apihelp-json-param-ascii": "If specified, encodes all non-ASCII using hexadecimal escape sequences. Default when formatversion is 1.", - "apihelp-json-param-formatversion": "Output formatting:\n;1:Backwards-compatible format (XML-style booleans, * keys for content nodes, etc.).\n;2:Experimental modern format. Details may change!\n;latest:Use the latest format (currently 2), may change without warning.", + "apihelp-json-param-formatversion": "Output formatting:\n;1:Backwards-compatible format (XML-style booleans, * keys for content nodes, etc.).\n;2:Modern format.\n;latest:Use the latest format (currently 2), may change without warning.", "apihelp-jsonfm-summary": "Output data in JSON format (pretty-print in HTML).", "apihelp-none-summary": "Output nothing.", "apihelp-php-summary": "Output data in serialized PHP format.", - "apihelp-php-param-formatversion": "Output formatting:\n;1:Backwards-compatible format (XML-style booleans, * keys for content nodes, etc.).\n;2:Experimental modern format. Details may change!\n;latest:Use the latest format (currently 2), may change without warning.", + "apihelp-php-param-formatversion": "Output formatting:\n;1:Backwards-compatible format (XML-style booleans, * keys for content nodes, etc.).\n;2:Modern format.\n;latest:Use the latest format (currently 2), may change without warning.", "apihelp-phpfm-summary": "Output data in serialized PHP format (pretty-print in HTML).", "apihelp-rawfm-summary": "Output data, including debugging elements, in JSON format (pretty-print in HTML).", "apihelp-xml-summary": "Output data in XML format.", @@ -1730,6 +1730,7 @@ "apierror-compare-nofromrevision": "No 'from' revision. Specify fromrev, fromtitle, or fromid.", "apierror-compare-notext": "Parameter $1 cannot be used without $2.", "apierror-compare-notorevision": "No 'to' revision. Specify torev, totitle, or toid.", + "apierror-compare-relative-to-deleted": "Cannot use torelative=$1 relative to a deleted revision.", "apierror-compare-relative-to-nothing": "No 'from' revision for torelative 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}}.", @@ -1758,6 +1759,7 @@ "apierror-invalidexpiry": "Invalid expiry time \"$1\".", "apierror-invalid-file-key": "Not a valid file key.", "apierror-invalidlang": "Invalid language code for parameter $1.", + "apierror-invalidmethod": "Invalid HTTP method. Consider using GET or POST.", "apierror-invalidoldimage": "The oldimage parameter has an invalid format.", "apierror-invalidparammix-cannotusewith": "The $1 parameter cannot be used with $2.", "apierror-invalidparammix-mustusewith": "The $1 parameter may only be used with $2.", @@ -1888,6 +1890,8 @@ "apiwarn-badurlparam": "Could not parse $1urlparam for $2. Using only width and height.", "apiwarn-badutf8": "The value passed for $1 contains invalid or non-normalized data. Textual data should be valid, NFC-normalized Unicode without C0 control characters other than HT (\\t), LF (\\n), and CR (\\r).", "apiwarn-checktoken-percentencoding": "Check that symbols such as \"+\" in the token are properly percent-encoded in the URL.", + "apiwarn-compare-no-next": "Revision $2 is the latest revision of $1, there is no revision for torelative=next to compare to.", + "apiwarn-compare-no-prev": "Revision $2 is the earliest revision of $1, there is no revision for torelative=prev to compare to.", "apiwarn-compare-nocontentmodel": "No content model could be determined, assuming $1.", "apiwarn-deprecation-deletedrevs": "list=deletedrevs has been deprecated. Please use prop=deletedrevisions or list=alldeletedrevisions instead.", "apiwarn-deprecation-httpsexpected": "HTTP used when HTTPS was expected.", diff --git a/includes/api/i18n/es.json b/includes/api/i18n/es.json index f807411433..afddc10a88 100644 --- a/includes/api/i18n/es.json +++ b/includes/api/i18n/es.json @@ -31,13 +31,15 @@ "Luzcaru", "Javiersanp", "KATRINE1992", - "Adjen" + "Adjen", + "Tiberius1701", + "Jelou" ] }, - "apihelp-main-extended-description": "
\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
\nStatus: 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\nErroneous requests: 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\nTesting: Para facilitar la comprobación de las solicitudes de API, consulte [[Special:ApiSandbox]].", + "apihelp-main-extended-description": "
\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 correo]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anuncios API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Errores y peticiones]\n
\nEstado: La API de MediaWiki es una interfaz madura y estable que se mejora y prueba activamente. Aunque tratamos de evitarlo, es posible que ocasionalmente debamos hacer cambios importantes; Suscribase a la [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de correo the mediawiki-api-announce] para recibir avisos de actualizaciones.\n\nSolicitudes erróneas: 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 obtener más información, consulte [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errores y advertencias]].\n\n

Pruebas: 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 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 maxlag con un mensaje como Esperando a $host: $lag segundos de retardo.
Consulta [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: parámetro Maxlag]] para más información.", + "apihelp-main-param-maxlag": "Se puede usar el retardo máximo cuando se instala MediaWiki en un clúster replicado de base de datos. Para evitar acciones que causen más retardo en la replicación del sitio, este parámetro puede hacer que el cliente espere hasta que el retardo en la replicación sea menor que el valor especificado. En caso de retardo excesivo, se devuelve el código de error maxlag con un mensaje como Esperando a $host: $lag segundos de retardo.
Consulta [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manual: parámetro Maxlag]] para más información.", "apihelp-main-param-smaxage": "Establece la cabecera HTTP s-maxage 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 max-age 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 user o si tiene el permiso de bot si es bot.", @@ -89,7 +91,7 @@ "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-param-prop": "Qué 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.", @@ -119,8 +121,8 @@ "apihelp-delete-param-watchlist": "Añadir o quitar incondicionalmente la página de la lista de seguimiento del usuario actual, usar preferencias o no cambiar el estado de seguimiento.", "apihelp-delete-param-unwatch": "Quitar la página de la lista de seguimiento del usuario actual.", "apihelp-delete-param-oldimage": "El nombre de la imagen antigua es proporcionado conforme a lo dispuesto por [[Special:ApiHelp/query+imageinfo|action=query&prop=imageinfo&iiprop=archivename]].", - "apihelp-delete-example-simple": "Borrar Main Page.", - "apihelp-delete-example-reason": "Eliminar Main Page con el motivo Preparing for move.", + "apihelp-delete-example-simple": "Borrar Pagina principal.", + "apihelp-delete-example-reason": "Eliminar Main Page con el motivo Preparándose para traslado.", "apihelp-disabled-summary": "Se desactivó este módulo.", "apihelp-edit-summary": "Crear y editar páginas.", "apihelp-edit-param-title": "Título de la página a editar. No se puede utilizar junto a $1pageid.", @@ -132,7 +134,7 @@ "apihelp-edit-param-tags": "Cambia las etiquetas para aplicarlas a la revisión.", "apihelp-edit-param-minor": "Edición menor.", "apihelp-edit-param-notminor": "Edición no menor.", - "apihelp-edit-param-bot": "Marcar esta como una edición de robot.", + "apihelp-edit-param-bot": "Marcar esta como una edición de bot.", "apihelp-edit-param-basetimestamp": "Marca de tiempo de la revisión base, usada para detectar conflictos de edición. Se puede obtener mediante [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]]", "apihelp-edit-param-starttimestamp": "Marca de tiempo de cuando empezó el proceso de edición, usada para detectar conflictos de edición. Se puede obtener un valor apropiado usando [[Special:ApiHelp/main|curtimestamp]] cuando comiences el proceso de edición (por ejemplo, al cargar el contenido de la página por editar).", "apihelp-edit-param-recreate": "Reemplazar los errores acerca de la página de haber sido eliminados en el ínterin.", @@ -155,13 +157,13 @@ "apihelp-edit-example-undo": "Deshacer intervalo de revisiones 13579-13585 con resumen automático", "apihelp-emailuser-summary": "Enviar un mensaje de correo electrónico a un usuario.", "apihelp-emailuser-param-target": "Cuenta de usuario destinatario.", - "apihelp-emailuser-param-subject": "Cabecera de asunto.", + "apihelp-emailuser-param-subject": "Cabecera del asunto.", "apihelp-emailuser-param-text": "Cuerpo del mensaje.", "apihelp-emailuser-param-ccme": "Enviarme una copia de este mensaje.", "apihelp-emailuser-example-email": "Enviar un correo al usuario WikiSysop con el texto Content.", "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-text": "Wikitexto que se convertirá.", "apihelp-expandtemplates-param-revid": "Id. de revisión, para {{REVISIONID}} 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.", @@ -326,7 +328,7 @@ "apihelp-paraminfo-example-1": "Mostrar información para [[Special:ApiHelp/parse|action=parse]], [[Special:ApiHelp/jsonfm|format=jsonfm]], [[Special:ApiHelp/query+allpages|action=query&list=allpages]] y [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]].", "apihelp-paraminfo-example-2": "Mostrar información para todos los submódulos de [[Special:ApiHelp/query|action=query]].", "apihelp-parse-summary": "Analiza el contenido y devuelve la salida del analizador sintáctico.", - "apihelp-parse-extended-description": "Véanse los distintos módulos prop de [[Special:ApiHelp/query|action=query]] para obtener información de la versión actual de una página.\n\nHay varias maneras de especificar el texto que analizar:\n# Especificar una página o revisión, mediante $1page, $1pageid o $1oldid.\n# Especificar explícitamente el contenido, mediante $1text, $1title y $1contentmodel.\n# Especificar solamente un resumen que analizar. Se debería asignar a $1prop un valor vacío.", + "apihelp-parse-extended-description": "Véanse los distintos módulos prop de [[Special:ApiHelp/query|action=query]] para obtener información de la versión actual de una página.\n\nHay varias maneras de especificar el texto que analizar:\n# Especificar una página o revisión, mediante $1page, $1pageid o $1oldid.\n# Especificar explícitamente el contenido, mediante $1text, $1title, $1revid, y $1contentmodel.\n# Especificar solamente un resumen que analizar. Se debería asignar a $1prop un valor vacío.", "apihelp-parse-param-title": "Título de la página a la que pertenece el texto. Si se omite se debe especificar $1contentmodel y se debe utilizar el [[API]] como título.", "apihelp-parse-param-text": "Texto a analizar. Utiliza $1title or $1contentmodel para controlar el modelo del contenido.", "apihelp-parse-param-summary": "Resumen a analizar.", @@ -761,7 +763,7 @@ "apihelp-query+filearchive-paramvalue-prop-archivename": "Añade el nombre de archivo de la versión archivada para las versiones que no son las últimas.", "apihelp-query+filearchive-example-simple": "Mostrar una lista de todos los archivos eliminados.", "apihelp-query+filerepoinfo-summary": "Devuelve metainformación sobre los repositorios de imágenes configurados en el wiki.", - "apihelp-query+filerepoinfo-param-prop": "Propiedades del repositorio a obtener (puede haber más disponibles en algunos wikis):\n;apiurl:URL del repositorio API - útil para obtener información de imagen del servidor.\n;name:La clave del repositorio - usado in e.g. [[mw:Special:MyLanguage/Manual:$wgForeignFileRepos|$wgForeignFileRepos]] y [[Special:ApiHelp/query+imageinfo|imageinfo]] devuelve valores.\n;displayname:El nombre legible del repositorio wiki.\n;rooturl:Raíz URL para rutas de imágenes.\n;local:Si ese repositorio es local o no.", + "apihelp-query+filerepoinfo-param-prop": "Qué propiedades del repositorio obtener (las propiedades disponibles pueden variar en otras wikis).", "apihelp-query+filerepoinfo-example-simple": "Obtener información acerca de los repositorios de archivos.", "apihelp-query+fileusage-summary": "Encontrar todas las páginas que utilizan los archivos dados.", "apihelp-query+fileusage-param-prop": "Qué propiedades se obtendrán:", @@ -835,7 +837,7 @@ "apihelp-query+info-paramvalue-prop-notificationtimestamp": "La hora de notificación de la lista de seguimiento de cada página.", "apihelp-query+info-paramvalue-prop-subjectid": "La ID de página de la página principal de cada página de discusión.", "apihelp-query+info-paramvalue-prop-url": "Muestra una URL completa, una URL de edición y la URL canónica de cada página.", - "apihelp-query+info-paramvalue-prop-readable": "Si el usuario puede leer esta página.", + "apihelp-query+info-paramvalue-prop-readable": "Si el usuario puede leer esta página. Usa intestactions=read en su lugar.", "apihelp-query+info-paramvalue-prop-preload": "Muestra el texto devuelto por EditFormPreloadText.", "apihelp-query+info-paramvalue-prop-displaytitle": "Proporciona la manera en que se muestra realmente el título de la página", "apihelp-query+info-param-testactions": "Comprobar su el usuario actual puede realizar determinadas acciones en la página.", @@ -1022,8 +1024,8 @@ "apihelp-query+revisions-summary": "Obtener información de la revisión.", "apihelp-query+revisions-extended-description": "Puede ser utilizado de varias maneras:\n# Obtener datos sobre un conjunto de páginas (última revisión), estableciendo títulos o ID de paginas.\n# Obtener revisiones para una página determinada, usando títulos o ID de páginas con inicio, fin o límite.\n# Obtener datos sobre un conjunto de revisiones estableciendo sus ID con revids.", "apihelp-query+revisions-paraminfo-singlepageonly": "Solo se puede usar con una sola página (modo n.º 2).", - "apihelp-query+revisions-param-startid": "Identificador de revisión a partir del cual empezar la enumeración.", - "apihelp-query+revisions-param-endid": "Identificador de revisión en el que detener la enumeración.", + "apihelp-query+revisions-param-startid": "Iniciar la enumeración desde la marca de tiempo de esta revisión. La revisión debe existir, pero no es necesario que pertenezca a esta página.", + "apihelp-query+revisions-param-endid": "Detener la enumeración en la marca de tiempo de esta revisión. La revisión debe existir, pero no es necesario que pertenezca a esta página.", "apihelp-query+revisions-param-start": "Marca de tiempo a partir de la cual empezar la enumeración.", "apihelp-query+revisions-param-end": "Enumerar hasta esta marca de tiempo.", "apihelp-query+revisions-param-user": "Incluir solo las revisiones realizadas por el usuario.", @@ -1043,16 +1045,16 @@ "apihelp-query+revisions+base-paramvalue-prop-userid": "Identificador de usuario del creador de la revisión.", "apihelp-query+revisions+base-paramvalue-prop-size": "Longitud (en bytes) de la revisión.", "apihelp-query+revisions+base-paramvalue-prop-sha1": "SHA-1 (base 16) de la revisión.", - "apihelp-query+revisions+base-paramvalue-prop-contentmodel": "Identificador del modelo de contenido de la revisión.", + "apihelp-query+revisions+base-paramvalue-prop-contentmodel": "Id. del modelo de contenido en cada espacio de revisión.", "apihelp-query+revisions+base-paramvalue-prop-comment": "Comentario del usuario para la revisión.", "apihelp-query+revisions+base-paramvalue-prop-parsedcomment": "Comentario analizado del usuario para la revisión.", - "apihelp-query+revisions+base-paramvalue-prop-content": "Texto de la revisión.", + "apihelp-query+revisions+base-paramvalue-prop-content": "Contenido de cada espacio de revisión.", "apihelp-query+revisions+base-paramvalue-prop-tags": "Etiquetas para la revisión.", - "apihelp-query+revisions+base-paramvalue-prop-parsetree": "El árbol de análisis sintáctico XML del contenido de la revisión (requiere el modelo de contenido $1).", + "apihelp-query+revisions+base-paramvalue-prop-parsetree": "Usa [[Special:ApiHelp/expandtemplates|action=expandtemplates]] o [[Special:ApiHelp/parse|action=parse]] en su lugar.\nEl árbol de análisis sintáctico XML del contenido de la revisión (necesita el modelo de contenido $1).", "apihelp-query+revisions+base-param-limit": "Limitar la cantidad de revisiones que se devolverán.", - "apihelp-query+revisions+base-param-expandtemplates": "Expandir las plantillas en el contenido de la revisión (requiere $1prop=content).", - "apihelp-query+revisions+base-param-generatexml": "Generar el árbol de análisis sintáctico XML para el contenido de la revisión (requiere $1prop=content; reemplazado por $1prop=parsetree).", - "apihelp-query+revisions+base-param-parse": "Analizar el contenido de la revisión (requiere $1prop=content). Por motivos de rendimiento, si se utiliza esta opción, el valor de $1limit es forzado a 1.", + "apihelp-query+revisions+base-param-expandtemplates": "Usa [[Special:ApiHelp/expandtemplates|action=expandtemplates]] en su lugar.\nExpandir las plantillas en el contenido de la revisión (necesita $1prop=content).", + "apihelp-query+revisions+base-param-generatexml": "Usa [[Special:ApiHelp/expandtemplates|action=expandtemplates]] o [[Special:ApiHelp/parse|action=parse]] en su lugar.\nGenerar el árbol de análisis sintáctico XML para el contenido de la revisión (necesita $1prop=content).", + "apihelp-query+revisions+base-param-parse": "Usa [[Special:ApiHelp/parse|action=parse]] en su lugar.\nAnalizar el contenido de la revisión (requiere $1prop=content). Por razones de rendimiento, si se usa esta opción, el valor de $1limit es forzado a 1.", "apihelp-query+revisions+base-param-section": "Recuperar solamente el contenido de este número de sección.", "apihelp-query+revisions+base-param-contentformat": "Formato de serialización utilizado para $1difftotext y esperado para la salida de contenido.", "apihelp-query+search-summary": "Realizar una búsqueda de texto completa.", @@ -1221,7 +1223,7 @@ "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", + "apihelp-query+watchlist-paramvalue-type-edit": "Ediciones comunes en páginas", "apihelp-query+watchlist-paramvalue-type-external": "Cambios externos.", "apihelp-query+watchlist-paramvalue-type-new": "Creaciones de páginas.", "apihelp-query+watchlist-paramvalue-type-log": "Entradas del registro.", @@ -1268,7 +1270,7 @@ "apihelp-rollback-param-tags": "Etiquetas que aplicar a la reversión.", "apihelp-rollback-param-user": "Nombre del usuario cuyas ediciones se van a revertir.", "apihelp-rollback-param-summary": "Resumen de edición personalizado. Si se deja vacío se utilizará el predeterminado.", - "apihelp-rollback-param-markbot": "Marcar las acciones revertidas y la reversión como ediciones por bots.", + "apihelp-rollback-param-markbot": "Marca las ediciones como revertidas y las revierte como ediciones de un bot.", "apihelp-rollback-param-watchlist": "Añadir o borrar incondicionalmente la página de la lista de seguimiento del usuario actual, usar preferencias o no cambiar seguimiento.", "apihelp-rollback-example-simple": "Revertir las últimas ediciones de la página Main Page por el usuario Example.", "apihelp-rollback-example-summary": "Revertir las últimas ediciones de la página Main Page por el usuario de IP 192.0.2.5 con resumen Reverting vandalism, y marcar esas ediciones y la reversión como ediciones realizadas por bots.", @@ -1373,11 +1375,11 @@ "apihelp-json-param-callback": "Si se especifica, envuelve la salida dentro de una llamada a una función dada. Por motivos de seguridad, cualquier dato específico del usuario estará restringido.", "apihelp-json-param-utf8": "Si se especifica, codifica la mayoría (pero no todos) de los caracteres no pertenecientes a ASCII como UTF-8 en lugar de reemplazarlos por secuencias de escape hexadecimal. Toma el comportamiento por defecto si formatversion no es 1.", "apihelp-json-param-ascii": "Si se especifica, codifica todos los caracteres no pertenecientes a ASCII mediante secuencias de escape hexadecimal. Toma el comportamiento por defecto si formatversion no es 1.", - "apihelp-json-param-formatversion": "Formato de salida:\n;1: Formato retrocompatible (booleanos con estilo XML, claves * para nodos de contenido, etc.).\n;2: Formato moderno experimental. ¡Atención, las especificaciones pueden cambiar!\n;latest: Utiliza el último formato (actualmente 2). Puede cambiar sin aviso.", + "apihelp-json-param-formatversion": "Formato de salida:\n;1: Formato retrocompatible (booleanos con estilo XML, claves * para nodos de contenido, etc.).\n;2: Formato moderno.\n;último: Utiliza el último formato (actualmente 2), puede cambiar sin aviso.", "apihelp-jsonfm-summary": "Producir los datos de salida en formato JSON (con resaltado sintáctico en HTML).", "apihelp-none-summary": "No extraer nada.", "apihelp-php-summary": "Extraer los datos de salida en formato serializado PHP.", - "apihelp-php-param-formatversion": "Formato de salida:\n;1: Formato retrocompatible (booleanos con estilo XML, claves * para nodos de contenido, etc.).\n;2: Formato moderno experimental. ¡Atención, las especificaciones pueden cambiar!\n;latest: Utilizar el último formato (actualmente 2). Puede cambiar sin aviso.", + "apihelp-php-param-formatversion": "Formato de salida:\n;1: Formato retrocompatible (booleanos con estilo XML, claves * para nodos de contenido, etc.).\n;2: Formato moderno.\n;último: Utiliza el último formato (actualmente 2), puede cambiar sin aviso.", "apihelp-phpfm-summary": "Producir los datos de salida en formato PHP serializado (con resaltado sintáctico en HTML).", "apihelp-rawfm-summary": "Extraer los datos de salida, incluidos los elementos de depuración, en formato JSON (embellecido en HTML).", "apihelp-xml-summary": "Producir los datos de salida en formato XML.", @@ -1567,7 +1569,7 @@ "apierror-nosuchuserid": "No hay ningún usuario con ID $1.", "apierror-notarget": "No has especificado un destino válido para esta acción.", "apierror-notpatrollable": "La revisión r$1 no se puede patrullar por ser demasiado antigua.", - "apierror-offline": "No se puede continuar debido a problemas de conectividad de la red. Asegúrate de que tienes una conexión activa a internet e inténtalo de nuevo.", + "apierror-offline": "No se pudo continuar debido a problemas de conectividad de red. Asegúrate de tener una conexión a Internet que funcione y vuelve a intentarlo.", "apierror-opensearch-json-warnings": "No se pueden representar los avisos en formato JSON de OpenSearch.", "apierror-pagecannotexist": "En este espacio de nombres no se permiten páginas reales.", "apierror-pagedeleted": "La página ha sido borrada en algún momento desde que obtuviste su marca de tiempo.", diff --git a/includes/api/i18n/fr.json b/includes/api/i18n/fr.json index 1af75a0690..6801faa255 100644 --- a/includes/api/i18n/fr.json +++ b/includes/api/i18n/fr.json @@ -1481,11 +1481,11 @@ "apihelp-json-param-callback": "Si spécifié, inclut la sortie dans l’appel d’une fonction fournie. Pour plus de sûreté, toutes les données spécifiques à l’utilisateur seront restreintes.", "apihelp-json-param-utf8": "Si spécifié, encode la plupart (mais pas tous) des caractères non ASCII en URF-8 au lieu de les remplacer par leur séquence d’échappement hexadécimale. Valeur par défaut quand formatversion ne vaut pas 1.", "apihelp-json-param-ascii": "Si spécifié, encode toutes ses séquences d’échappement non ASCII utilisant l’hexadécimal. Valeur par défaut quand formatversion vaut 1.", - "apihelp-json-param-formatversion": "Mise en forme de sortie :\n;1:Format rétro-compatible (booléens de style XML, clés * pour les nœuds de contenu, etc.).\n;2:Format moderne expérimental. Des détails peuvent changer !\n;latest:Utilise le dernier format (actuellement 2), peut changer sans avertissement.", + "apihelp-json-param-formatversion": "Mise en forme de sortie :\n;1:Format rétro-compatible (booléens de style XML, clés * pour les nœuds de contenu, etc.).\n;2:Format moderne.\n;latest:Utilise le dernier format (actuellement 2), peut changer sans avertissement.", "apihelp-jsonfm-summary": "Extraire les données au format JSON (affiché proprement en HTML).", "apihelp-none-summary": "Ne rien extraire.", "apihelp-php-summary": "Extraire les données au format sérialisé de PHP.", - "apihelp-php-param-formatversion": "Mise en forme de la sortie :\n;1:Format rétro-compatible (bool&ens de style XML, clés * pour les nœuds de contenu, etc.).\n;2:Format moderne expérimental. Des détails peuvent changer !\n;latest:Utilise le dernier format (actuellement 2), peut changer sans avertissement.", + "apihelp-php-param-formatversion": "Mise en forme de la sortie :\n;1:format rétro-compatible (booléens de style XML, clés * pour les nœuds de contenu, etc.).\n;2:format moderne.\n;latest:utilise le dernier format (actuellement 2), peut changer sans avertissement.", "apihelp-phpfm-summary": "Extraire les données au format sérialisé de PHP (affiché proprement en HTML).", "apihelp-rawfm-summary": "Extraire les données, y compris les éléments de débogage, au format JSON (affiché proprement en HTML).", "apihelp-xml-summary": "Extraire les données au format XML.", @@ -1631,6 +1631,7 @@ "apierror-compare-nofromrevision": "Aucune révision 'from'. Spécifiez fromrev, fromtitle, ou fromid.", "apierror-compare-notext": "Le paramètre $1 ne peut pas être utilisé sans $2.", "apierror-compare-notorevision": "Aucune révision 'to'. Spécifiez torev, totitle, ou toid.", + "apierror-compare-relative-to-deleted": "Impossible d’utiliser torelative=$1 par rapport à une révision supprimée.", "apierror-compare-relative-to-nothing": "Pas de révision 'depuis' pour torelative à 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}}.", @@ -1659,6 +1660,7 @@ "apierror-invalidexpiry": "Heure d'expiration invalide \"$1\".", "apierror-invalid-file-key": "Ne correspond pas à une clé valide de fichier.", "apierror-invalidlang": "Code de langue non valide pour le paramètre $1.", + "apierror-invalidmethod": "Méthode HTTP non valide. Seul GET ou POST est autorisé.", "apierror-invalidoldimage": "Le paramètre oldimage a un format non valide.", "apierror-invalidparammix-cannotusewith": "Le paramètre $1 ne peut pas être utilisé avec $2.", "apierror-invalidparammix-mustusewith": "Le paramètre $1 ne peut être utilisé qu’avec $2.", @@ -1787,6 +1789,8 @@ "apiwarn-badurlparam": "Impossible d'analyser $1urlparam pour $2. En utilisant seulement la largeur et la hauteur.", "apiwarn-badutf8": "La valeur passée pour $1 contient des données non valides ou non normalisées. Les données textuelles doivent être de l’Unicode valide normalisé en NFC sans caractères de contrôle c0 autres que HT (\\t), LF (\\n) et CR (\\r).", "apiwarn-checktoken-percentencoding": "Vérifier que les symboles tels que \"+\" dans le jeton sont correctement codés avec des pourcents dans l'URL.", + "apiwarn-compare-no-next": "La version $2 est la dernière version de $1, il n'existe pas de version torelative=next à comparer.", + "apiwarn-compare-no-prev": "La version $2 est la plus ancienne de $1, il n'existe pas de version torelative=prev à comparer.", "apiwarn-compare-nocontentmodel": "Aucun modèle de contenu n’a pu être déterminé, $1 est supposé.", "apiwarn-deprecation-deletedrevs": "list=deletedrevs est devenu désuet. Veuillez utiliser prop=deletedrevisions ou list=alldeletedrevisions à la place.", "apiwarn-deprecation-httpsexpected": "HTTP est utilisé alors que HTTPS est attendu.", diff --git a/includes/api/i18n/he.json b/includes/api/i18n/he.json index 3370d73848..94ee481345 100644 --- a/includes/api/i18n/he.json +++ b/includes/api/i18n/he.json @@ -49,6 +49,8 @@ "apihelp-block-param-reblock": "אם המשתמש כבר חסום, לדרוס את החסימה הנוכחית.", "apihelp-block-param-watchuser": "לעקוב אחרי דף המשתמש ודף השיחה של המשתמש או של כתובת ה־IP.", "apihelp-block-param-tags": "תגי שינוי שיחולו על העיול ביומן החסימה.", + "apihelp-block-param-partial": "חסימת משתמש מעריכת דפים או מרחבי שם מסוימים ולא מכל האתר.", + "apihelp-block-param-pagerestrictions": "רשימת כותרות שהמשתמש ייחסם מלערוך. חל רק כאשר \"partial\" מוגדל ל־true.", "apihelp-block-example-ip-simple": "חסימת כתובת ה־IP‏ 192.0.2.5 לשלושה ימים עם הסיבה First strike.", "apihelp-block-example-user-complex": "חסימת המשתמש Vandal ללא הגבלת זמן עם הסיבה Vandalism, ומניעת יצירת חשבונות חדשים ושליחת דוא\"ל.", "apihelp-changeauthenticationdata-summary": "שינוי נתוני אימות עבור המשתמש הנוכחי.", @@ -68,19 +70,29 @@ "apihelp-compare-param-fromtitle": "כותרת ראשונה להשוואה.", "apihelp-compare-param-fromid": "מס׳ זיהוי של הדף הראשון להשוואה.", "apihelp-compare-param-fromrev": "גרסה ראשונה להשוואה.", - "apihelp-compare-param-frompst": "לעשות התמרה לפני שמירה ב־fromtext.", - "apihelp-compare-param-fromtext": "להשתמש בטקסט הזה במקום תוכן הגרסה שהוגדרה על־ידי fromtitle, fromid או fromrev.", - "apihelp-compare-param-fromcontentmodel": "מודל התוכן של fromtext. אם זה לא סופק, ייעשה ניחוש על סמך פרמטרים אחרים.", - "apihelp-compare-param-fromcontentformat": "תסדיר הסדרת תוכן של fromtext.", + "apihelp-compare-param-frompst": "לעשות התמרה לפני שמירה ב־romtext-{slot}.", + "apihelp-compare-param-fromslots": "דריסת תוכן הגרסה שצוינה ב־fromtitle, ב־fromid, או ב־fromrev.\n\nהפרמטר הזה מציין את המשבצות שישונו. יש להשתמש ב־fromtext-{slot}, ב־fromcontentmodel-{slot}, וב־fromcontentformat-{slot} לציון תוכן עבור כל משבצת.", + "apihelp-compare-param-fromtext-{slot}": "הטקסט של המשבצת שמצוינת. אם זה מושמט, המשבצת מוּסרת מהגרסה.", + "apihelp-compare-param-fromsection-{slot}": "כאשר fromtext-{slot} הוא התוכן של פסקה אחת, זהו מספר הפסקה. הוא ימוזג לתוך הגרסה שמצוינת ב־fromtitle, ב־fromid, או ב־fromrev כמו בעריכת פסקה.", + "apihelp-compare-param-fromcontentmodel-{slot}": "מודל התוכן של fromtext-{slot}. אם זה לא סופק, זה ינוחש לפי הפרמטרים האחרים.", + "apihelp-compare-param-fromcontentformat-{slot}": "תסדיר להסדרת תוכן של fromtext-{slot}.", + "apihelp-compare-param-fromtext": "יש לציין fromslots=main ולהשתמש ב־fromtext-main במקום זה.", + "apihelp-compare-param-fromcontentmodel": "יש לציין fromslots=main ולהשתמש ב־fromcontentmodel-main במקום זה.", + "apihelp-compare-param-fromcontentformat": "יש לציין fromslots=main ולהשתמש ב־fromcontentformat-main במקום זה.", "apihelp-compare-param-fromsection": "יש להשתמש רק בפסקה שצוינה בתוכן של הפרמטר 'from'.", "apihelp-compare-param-totitle": "כותרת שנייה להשוואה.", "apihelp-compare-param-toid": "מס׳ מזהה של הדף השני להשוואה.", "apihelp-compare-param-torev": "גרסה שנייה להשוואה.", "apihelp-compare-param-torelative": "להשתמש בגרסה יחסית לגרסה שהוסקה מfromtitle, fromid או fromrev. לכל אפשריות ה־\"to\" האחרות לא תהיה השפעה.", "apihelp-compare-param-topst": "לעשות התמרה לפני שמירה ב־totext.", - "apihelp-compare-param-totext": "להשתמש בטקסט הזה במקום התוכן של הגרסה שהוגדר ב־totitle, toid or torev.", - "apihelp-compare-param-tocontentmodel": "מודל התוכן של totext. אם זה לא סופק, ייעשה ניחוש על סמך פרמטרים אחרים.", - "apihelp-compare-param-tocontentformat": "תסדיר הסדרת תוכן של fromtext.", + "apihelp-compare-param-toslots": "דריסת תוכן הגרסה שצוינה ב־totitle, ב־toid, או ב־torev.\n\nהפרמטר הזה מציין את המשבצות שישונו. יש להשתמש ב־totext-{slot}, ב־tocontentmodel-{slot}, וב־tocontentformat-{slot} לציון תוכן עבור כל משבצת.", + "apihelp-compare-param-totext-{slot}": "הטקסט של המשבצת שמצוינת. אם זה מושמט, המשבצת מוּסרת מהגרסה.", + "apihelp-compare-param-tosection-{slot}": "כאשר totext-{slot} הוא התוכן של פסקה אחת, זהו מספר הפסקה. הוא ימוזג לתוך הגרסה שמצוינת ב־totitle, ב־toid, או ב־torev כמו בעריכת פסקה.", + "apihelp-compare-param-tocontentmodel-{slot}": "מודל התוכן של totext-{slot}. אם זה לא סופק, זה ינוחש לפי הפרמטרים האחרים.", + "apihelp-compare-param-tocontentformat-{slot}": "תסדיר להסדרת תוכן של totext-{slot}.", + "apihelp-compare-param-totext": "יש לציין toslots=main ולהשתמש ב־totext-main במקום זה.", + "apihelp-compare-param-tocontentmodel": "יש לציין toslots=main ולהשתמש ב־tocontentmodel-main במקום זה.", + "apihelp-compare-param-tocontentformat": "יש לציין toslots=main ולהשתמש ב־tocontentformat-main במקום זה.", "apihelp-compare-param-tosection": "יש להשתמש רק בפסקה שצוינה בתוכן של הפרמטר 'to'.", "apihelp-compare-param-prop": "אילו פריטי מידע לקבל.", "apihelp-compare-paramvalue-prop-diff": "ה־HTML של ההשוואה.", @@ -92,6 +104,7 @@ "apihelp-compare-paramvalue-prop-comment": "התקציר על גרסאות ה־\"from\" וה־\"to\".", "apihelp-compare-paramvalue-prop-parsedcomment": "התקציר המפוענח על גרסאות ה־\"from\" וה־\"to\".", "apihelp-compare-paramvalue-prop-size": "הגודל של גרסאות ה־\"from\" וה־\"to\".", + "apihelp-compare-param-slots": "החזרת השוואות פרטניו למשבצות האלה, ולא השוואה אחת משולבת לכל המשבצות.", "apihelp-compare-example-1": "יצירת תיעוד שינוי בין גרסה 1 ל־2.", "apihelp-createaccount-summary": "יצירת חשבון משתמש חדש.", "apihelp-createaccount-param-preservestate": "אם [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] החזיר true עבור hasprimarypreservedstate, בקשות שמסומנות בתור primary-required אמורות להיות מושמטות. אם מוחזר ערך לא ריק ל־preservedusername, שם המשתמש הזה ישמש לפרמטר username.", @@ -632,6 +645,7 @@ "apihelp-query+blocks-paramvalue-prop-reason": "הוספת הסיבה שניתנה לחסימה.", "apihelp-query+blocks-paramvalue-prop-range": "הוספת טווח כתובות ה־IP שהחסימה משפיעה עליהן.", "apihelp-query+blocks-paramvalue-prop-flags": "מתייג את ההחרמה (autoblock‏, anononly, וכו'.).", + "apihelp-query+blocks-paramvalue-prop-restrictions": "הוספת הגבלות החסימה החלקית אם החסימה אינה לכל האתר.", "apihelp-query+blocks-param-show": "להציג רק פריטים שמתאימים לאמות המידה האלו.\nלמשל, כדי לראות רק חסימות ללא לצמיתות על כתובות IP יש להגדיר $1show=ip|!temp.", "apihelp-query+blocks-example-simple": "רשימת חסימות.", "apihelp-query+blocks-example-users": "רשימת חסימות של המשתמשים Alice ו־Bob.", @@ -852,11 +866,15 @@ "apihelp-query+info-paramvalue-prop-notificationtimestamp": "חותם־זמן של הודעת רשימת מעקב של כל דף.", "apihelp-query+info-paramvalue-prop-subjectid": "מזהה הדף של הדף העיקרי של כל דף שיחה.", "apihelp-query+info-paramvalue-prop-url": "נותן URL מלא, URL לעריכה ו־URL קנוני לכל דף.", - "apihelp-query+info-paramvalue-prop-readable": "האם המשתמש יכול להציג דף זה.", + "apihelp-query+info-paramvalue-prop-readable": "האם המשתמש יכול לקרוא את הדף הזה. יש להשתמש ב־intestactions=read במקום זה.", "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-testactionsdetail": "רמת פירוט של $1testactions. יש להשתמש בפרמטרים errorformat ו־errorlang של [[Special:ApiHelp/main|המודול הראשי]] כדי לשלוט בתסדיר את ההודעות המוחזרות.", + "apihelp-query+info-paramvalue-testactionsdetail-boolean": "החזרת ערך בוליאני עבור כל פעולה.", + "apihelp-query+info-paramvalue-testactionsdetail-full": "החזרת הודעות שמתארות למה הפעולה אינה מותרת, או מערך ריק אם היא מותרת.", + "apihelp-query+info-paramvalue-testactionsdetail-quick": "כמו full, אבל בלי בדיקות יקרות.", "apihelp-query+info-param-token": "להשתמש ב־[[Special:ApiHelp/query+tokens|action=query&meta=tokens]] במקום.", "apihelp-query+info-example-simple": "קבלת מידע על הדף Main Page", "apihelp-query+info-example-protection": "קבלת מידע כללי ומידע על הגנה של הדף Main Page.", @@ -1447,11 +1465,11 @@ "apihelp-json-param-callback": "אם זה צוין, עוטף את הפלט לתוך קריאת פונקציה נתונה. למען הבטיחות, כל הנתונים הייחודיים למשתמש יוגבלו.", "apihelp-json-param-utf8": "אם זה צוין, רוב התווים שאינם ASCII (אבל לא כולם) יקודדו בתור UTF-8 במקום להתחלף בסדרות חילוף הקסדצימליות. זאת בררת המחדל אם הערך של formatversion הוא לא 1.", "apihelp-json-param-ascii": "אם זה צוין, לקודד את כל מה שאינו ASCII בסדרות חילוף הקסדצימליות. זאת בררת המחדל כש־formatversion היא 1.", - "apihelp-json-param-formatversion": "תסדיר הפלט:\n;1:תסדיר עם תאימות אחורה (ערכים בוליאניים בסגנון XML, מפתחות * לצומתי תוכן, וכו').\n;2:תסדיר מודרני ניסיוני. הפרטים יכולים להשתנות!\n;latest:להשתמש בתסדיר החדש ביותר (כרגע 2), יכול להשתנות ללא התראה.", + "apihelp-json-param-formatversion": "תסדיר הפלט:\n;1:תסדיר עם תאימות אחורה (ערכים בוליאניים בסגנון XML, מפתחות * לצומתי תוכן, וכו').\n;2:תסדיר מודרני.\n;latest:להשתמש בתסדיר החדש ביותר (כרגע 2), יכול להשתנות ללא התראה.", "apihelp-jsonfm-summary": "לפלוט נתונים בתסדיר JSON (עם הדפסה יפה ב־HTML).", "apihelp-none-summary": "לא לפלוט שום דבר.", "apihelp-php-summary": "לפלוט נתונים בתסדיר PHP מוסדר.", - "apihelp-php-param-formatversion": "תסדיר הפלט:\n;1:תסדיר עם תאימות אחורה (ערכים בוליאניים בסגנון XML, מפתחות * לצומתי תוכן, וכו').\n;2:תסדיר מודרני ניסיוני. הפרטים יכולים להשתנות!\n;latest:להשתמש בתסדיר החדש ביותר (כרגע 2), יכול להשתנות ללא התראה.", + "apihelp-php-param-formatversion": "תסדיר הפלט:\n;1:תסדיר עם תאימות אחורה (ערכים בוליאניים בסגנון XML, מפתחות * לצומתי תוכן, וכו').\n;2:תסדיר מודרני.\n;latest:להשתמש בתסדיר החדש ביותר (כרגע 2), יכול להשתנות ללא התראה.", "apihelp-phpfm-summary": "לפלוט נתונים בתסדיר PHP מוסדר (עם הדפסה יפה ב־HTML).", "apihelp-rawfm-summary": "לפלוט את הנתונים, כולל אלמנטים לניפוי שגיאות, בתסדיר JSON (עם הדפסה יפה ב־HTML).", "apihelp-xml-summary": "לפלוט נתונים בתסדיר XML.", @@ -1549,6 +1567,7 @@ "apierror-assertnameduserfailed": "הבדיקה שהמשתמש הוא \"$1\" נכשלה.", "apierror-assertuserfailed": "הבדיקה שהמשתמש נכנס לחשבון נכשלה.", "apierror-autoblocked": "כתובת ה־IP שלך נחסמה אוטומטית, כי היא שימשה משתמש חסום.", + "apierror-bad-badfilecontexttitle": "כותרת בלתי־תקינה בפרמטר $1badfilecontexttitle.", "apierror-badconfig-resulttoosmall": "הערך של $wgAPIMaxResultSize בוויקי הזה קטן מלהחזיק מידע בסיסי על תוצאה.", "apierror-badcontinue": "פרמטר continue בלתי־תקין. יש להעביר את הערך המקורי שהחזירה השאילתה הקודמת.", "apierror-baddiff": "לא ניתן לאחזר את ההשוואה. גרסה אחת לא קיימת או ששתיהן לא קיימות, או שאין לך הרשאה להציג אותן.", @@ -1572,6 +1591,7 @@ "apierror-bad-watchlist-token": "סופק אסימון רשימת מעקב בלתי־תקין. נא להשתמש באסימון תקין ב־[[Special:Preferences]].", "apierror-blockedfrommail": "נחסמת משליחת דוא״ל.", "apierror-blocked": "נחסמת מעריכה.", + "apierror-blocked-partial": "נחסמת מעריכת הדף הזה.", "apierror-botsnotsupported": "הממשק הזה לא נתמך עבור בוטים.", "apierror-cannot-async-upload-file": "הפרמטרים async ו־file אינם יכולים להיות משולבים. אם ברצונך לבצע עיבוד אסינכרוני של הקובץ המועלה שלך, יש להעלות אותו תחילה לסליק (באמצעות הפרמטר stash) ואז לפרסם את הקובץ המוסלק באופן אסינכרוני (באמצעות filekey ו־async).", "apierror-cannotreauthenticate": "הפעולה הזאת אינה זמינה, כי הזהות שלך לא יכולה להיות מאומתת.", @@ -1588,9 +1608,14 @@ "apierror-changeauth-norequest": "יצירת בקשת השינוי נכשלה.", "apierror-chunk-too-small": "גודל הפלח המזערי הוא {{PLURAL:$1|בית אחד|$1 בתים}} בשביל פלחים לא סופיים.", "apierror-cidrtoobroad": "טווחי CIDR של $1 שרחבים יותר מ־/$2 אינם קבילים.", + "apierror-compare-maintextrequired": "הפרמטר $1text-main נדרש כאשר $1slots מכיל main (לא ניתן למחוק את המשבצת הראשית).", "apierror-compare-no-title": "לא ניתן לעשות התמרה לפני שמירה ללא כותרת. נא לנסות לציין fromtitle או totitle.", "apierror-compare-nosuchfromsection": "הפסקה $1 אינה קיימת בתוכן של 'from'.", "apierror-compare-nosuchtosection": "הפסקה $1 אינה קיימת בתוכן של 'to'.", + "apierror-compare-nofromrevision": "אין גרסת \"from\". יש לציין fromrev‏, fromtitle, או fromid.", + "apierror-compare-notext": "הפרמטר $1 אינו יכול לשמש ללא $2.", + "apierror-compare-notorevision": "אין גרסת \"to\". יש לציין torev‏, totitle, או toid.", + "apierror-compare-relative-to-deleted": "לא ניתן להשתמש ב־torelative=$1 יחסית לגרסה מחוקה.", "apierror-compare-relative-to-nothing": "אין גרסת \"from\" עבור torelative שתהיה יחסית.", "apierror-contentserializationexception": "הסדרת התוכן נכשלה: $1", "apierror-contenttoobig": "התוכן שסיפקת חורג מגודל הערך המרבי של {{PLURAL:$1|קילובייט אחד|$1 קילובייטים}}.", @@ -1619,6 +1644,7 @@ "apierror-invalidexpiry": "זמן תפוגה בלתי־תקין \"$1\".", "apierror-invalid-file-key": "לא מפתח קובץ תקין.", "apierror-invalidlang": "קוד שפה בלתי־תקין לפרמטר $1.", + "apierror-invalidmethod": "שיטת HTTP בלתי־תקינה. נא לשקול להשתמש ב־GET או ב־POST.", "apierror-invalidoldimage": "הפרמטר oldimage נשלח בתסדיר בלתי־תקין.", "apierror-invalidparammix-cannotusewith": "הפרמטר $1 אינו יכול לשמש עם $2.", "apierror-invalidparammix-mustusewith": "הפרמטר $1 יכול לשמש רק עם $2.", @@ -1638,6 +1664,7 @@ "apierror-mimesearchdisabled": "חיפוש MIME כבוי במצב קמצן.", "apierror-missingcontent-pageid": "תוכן חסר עבור מזהה הדף $1.", "apierror-missingcontent-revid": "תוכן חסר עבור מזהה הגרסה $1.", + "apierror-missingcontent-revid-role": "תוכן חסר כבור מזהה גרסה $1 לתפקיד $2.", "apierror-missingparam-at-least-one-of": "דרוש {{PLURAL:$2|הפרמטר|לפחות אחד מהפרמטרים}} $1.", "apierror-missingparam-one-of": "דרוש {{PLURAL:$2|הפרמטר|אחד מהפרמטרים}} $1.", "apierror-missingparam": "הפרמטר $1 צריך להיות מוגדר.", @@ -1746,6 +1773,8 @@ "apiwarn-badurlparam": "לא היה אפשר לפענח את $1urlparam עבור $2. משתמשים רק ב־width ו־height.", "apiwarn-badutf8": "הערך שהועבר ל־$1 מכיל נתונים בלתי־תקינים או בלתי־מנורמלים. נתונים טקסט אמורים להיות תקינים, מנורמלי NFC וללא תווי בקרה C0 למעט HT (\\t)‏, LF (\\n)‏ ו־CR (\\r).", "apiwarn-checktoken-percentencoding": "נא לבדוק שסימנים כמו \"+\" באסימון מקודדים עם אחוזים בצורה נכונה ב־URL.", + "apiwarn-compare-no-next": "הגרסה $2 היא הגרסה האחרונה של $1, אין גרסה עבור torelative=next להשוואה.", + "apiwarn-compare-no-prev": "הגרסה $2 היא הגרסה המוקדמת ביותר של $1, אין גרסה עבור torelative=prev להשוואה.", "apiwarn-compare-nocontentmodel": "לא היה אפשר לקבוע את מודל התוכן, נניח שזה $1.", "apiwarn-deprecation-deletedrevs": "list=deletedrevs הוצהר בתור מיושן. נא להשתמש ב־ prop=deletedrevisions או ב־list=alldeletedrevisions במקום זה.", "apiwarn-deprecation-httpsexpected": "משמש HTTP כשהיה צפוי HTTPS.", diff --git a/includes/api/i18n/hu.json b/includes/api/i18n/hu.json index 1211693b63..4258674e11 100644 --- a/includes/api/i18n/hu.json +++ b/includes/api/i18n/hu.json @@ -38,6 +38,9 @@ "apihelp-block-param-allowusertalk": "A felhasználó szerkeszthesse a saját vitalapját (a [[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]] beállítástól függ).", "apihelp-block-param-reblock": "Jelenlegi blokk felülírása, ha a felhasználó már blokkolva van.", "apihelp-block-param-watchuser": "A szerkesztő vagy IP-cím szerkesztői- és vitalapjának figyelése.", + "apihelp-block-param-tags": "A blokknapló naplóbejegyzésére érvényesítendő változtatáscímkék.", + "apihelp-block-param-partial": "Teljes blokk helyett a felhasználó eltiltása bizonyos lapok vagy névterek szerkesztésétől.", + "apihelp-block-param-pagerestrictions": "A felhasználó számára blokkolandó címek listája. Csak akkor van hatása, ha a partial igaz.", "apihelp-block-example-ip-simple": "A 192.0.2.5 IP-cím blokkolása három napra First strike indoklással.", "apihelp-block-example-user-complex": "Vandal blokkolása határozatlan időre Vandalism indoklással, új fiók létrehozásának és e-mail küldésének megakadályozása.", "apihelp-checktoken-summary": "Egy [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] kéréssel szerzett token érvényességének vizsgálata.", diff --git a/includes/api/i18n/ja.json b/includes/api/i18n/ja.json index 5bccabfdab..c13a10ede0 100644 --- a/includes/api/i18n/ja.json +++ b/includes/api/i18n/ja.json @@ -44,6 +44,7 @@ "apihelp-block-param-reblock": "その利用者がすでにブロックされている場合、ブロックを上書きします。", "apihelp-block-param-watchuser": "その利用者またはIPアドレスの利用者ページとトークページをウォッチします。", "apihelp-block-param-tags": "ブロック記録の項目に適用する変更タグ。", + "apihelp-block-param-partial": "サイト全体ではなく特定のページまたは名前空間での編集をブロックします。", "apihelp-block-example-ip-simple": "IPアドレス 192.0.2.5 を First strike という理由で3日ブロックする", "apihelp-block-example-user-complex": "利用者 Vandal を Vandalism という理由で無期限ブロックし、新たなアカウント作成とメールの送信を禁止する。", "apihelp-changeauthenticationdata-summary": "現在の利用者の認証データを変更します。", @@ -65,13 +66,16 @@ "apihelp-compare-param-fromrev": "比較する1つ目の版。", "apihelp-compare-param-frompst": "fromtext-{slot}に保存前変換を行います。", "apihelp-compare-param-fromtext": "fromslots=mainを指定し、代わりにfromtext-main を使用してください。", - "apihelp-compare-param-fromcontentmodel": "fromtextのコンテンツモデル。指定されていない場合は、他のパラメータに基づいて推測されます。", + "apihelp-compare-param-fromcontentmodel": "fromslots=mainを指定し、代わりにfromcontentmodel-main を使用してください。", + "apihelp-compare-param-fromcontentformat": "fromslots=mainを指定し、代わりにfromcontentformat-main を使用してください。", "apihelp-compare-param-fromsection": "'from' の内容のうち指定された節のみを使用します。", "apihelp-compare-param-totitle": "比較する2つ目のページ名。", "apihelp-compare-param-toid": "比較する2つ目のページID。", "apihelp-compare-param-torev": "比較する2つ目の版。", "apihelp-compare-param-topst": "totextに保存前変換を行います。", - "apihelp-compare-param-tocontentmodel": "totext のコンテンツモデル。指定されていない場合は、他のパラメータに基づいて推測されます。", + "apihelp-compare-param-totext": "toslots=mainを指定し、代わりにtotext-main を使用してください。", + "apihelp-compare-param-tocontentmodel": "toslots=mainを指定し、代わりにtocontentmodel-main を使用してください。", + "apihelp-compare-param-tocontentformat": "toslots=mainを指定し、代わりにtocontentformat-main を使用してください。", "apihelp-compare-param-tosection": "'to' の内容のうち指定された節のみを使用します。", "apihelp-compare-param-prop": "どの情報を取得するか:", "apihelp-compare-paramvalue-prop-diff": "差分HTML。", @@ -1054,6 +1058,7 @@ "apierror-permissiondenied": "$1に必要な権限がありません。", "apierror-permissiondenied-generic": "アクセスが拒否されました。", "apierror-readonly": "ウィキは現在読み取り専用モードです。", + "apierror-revwrongpage": "版 $1 は $2 の版ではありません。", "apierror-timeout": "サーバーが決められた時間内に応答しませんでした。", "apierror-unknownerror-editpage": "不明な編集ページのエラー:$1", "apierror-unknownerror-nocode": "不明なエラーです。", diff --git a/includes/api/i18n/ko.json b/includes/api/i18n/ko.json index b9f8868f2d..3ff6f2fdba 100644 --- a/includes/api/i18n/ko.json +++ b/includes/api/i18n/ko.json @@ -863,6 +863,7 @@ "apierror-invalidcategory": "입력한 분류 이름이 올바르지 않습니다.", "apierror-invalidexpiry": "잘못된 만료 기한 \"$1\".", "apierror-invalid-file-key": "유효한 파일 키가 아닙니다.", + "apierror-invalidmethod": "유효하지 않은 HTTP 메서드입니다. GET 또는 POST의 사용을 고려하십시오.", "apierror-invalidoldimage": "oldimage 변수에 유효하지 않은 형식이 있습니다.", "apierror-invalidparammix-cannotusewith": "$1 변수는 $2와(ê³¼) 함께 사용할 수 없습니다.", "apierror-invalidsection": "section 변수는 유효한 섹션 ID 또는 new이어야 합니다.", diff --git a/includes/api/i18n/mk.json b/includes/api/i18n/mk.json index 562f2165b5..56826b4c25 100644 --- a/includes/api/i18n/mk.json +++ b/includes/api/i18n/mk.json @@ -6,7 +6,7 @@ "Vlad5250" ] }, - "apihelp-main-extended-description": "
\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 Соопштенија за Извршникот]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Грешки и барања]\n
\nСтатус: Сите ставки на страницава би требало да работат, но Извршникот сепак е во активна разработка, што значи дека може да се смени во секое време. Објавите за измени можете да ги дознавате ако се пријавите на [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ поштенскиот список „the mediawiki-api-announce“].\n\nПогрешни барања: Кога Извршникот ќе добие погрешни барања, ќе се испрати HTTP-заглавие со клучот „MediaWiki-API-Error“ и потоа на вредностите на заглавието и шифрата на грешката што ќе се појават ќе им биде зададена истата вредност. ПОвеќе информации ќе најдете на [[mw:API:Errors_and_warnings|Извршник: Грешки и предупредувања]].", + "apihelp-main-extended-description": "
\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 Соопштенија за Извршникот]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Грешки и барања]\n
\nСтатус: Сите ставки на страницава би требало да работат, но Извршникот сепак е во активна разработка, што значи дека може да се смени во секое време. Објавите за измени можете да ги дознавате ако се пријавите на [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ поштенскиот список „the mediawiki-api-announce“].\n\nПогрешни барања: Кога Извршникот ќе добие погрешни барања, ќе се испрати HTTP-заглавие со клучот „MediaWiki-API-Error“ и потоа на вредностите на заглавието и шифрата на грешката што ќе се појават ќе им биде зададена истата вредност. Повеќе информации ќе најдете на [[mw:API:Errors_and_warnings|Извршник: Грешки и предупредувања]].", "apihelp-main-param-action": "Кое дејство да се изврши.", "apihelp-main-param-format": "Формат на изводот.", "apihelp-main-param-maxlag": "Најголемиот допуштен заостаток може да се користи кога МедијаВики е воспоставен на грозд умножен од базата. За да спречите дополнителни заостатоци од дејства, овој параметар му наложува на клиентот да почека додека заостатокот не се намали под укажаната вредност. Во случај на преголем заостаток, системт ја дава грешката со код maxlag со порака од обликот Го чекам $host: има заостаток од $lag секунди.
Погл. [[mw:Manual:Maxlag_parameter|Прирачник: Параметар Maxlag]]", @@ -81,7 +81,7 @@ "apihelp-edit-param-tags": "Ознаки за измена што се однесуваат на преработката.", "apihelp-edit-param-minor": "Ситно уредување.", "apihelp-edit-param-notminor": "Неситно уредување.", - "apihelp-edit-param-bot": "Означи го уредувањево како ботовско.", + "apihelp-edit-param-bot": "Означи го уредувањето како ботовско.", "apihelp-edit-param-basetimestamp": "Датум и време на преработката на базата, кои се користат за утврдување на спротиставености во уредувањето. Може да се добие преку [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].", "apihelp-edit-param-starttimestamp": "Датум и време кога сте почнало уредувањето, кои се користат за утврдување на спротиставености во уредувањата. Соодветната вредност се добива користејќи [[Special:ApiHelp/main|curtimestamp]] кога ќе почнете со уредување (на пр. кога ќе се вчита содржината што ќе ја уредувате).", "apihelp-edit-param-recreate": "Занемари ги грешките што се појавуваат во врска со страницата што е избришана во меѓувреме.", @@ -359,11 +359,11 @@ "apihelp-json-param-callback": "Ако е укажано, го обвива изводот во даден повик на функција. За безбедност, ќе се ограничат сите податоци што се однесуваат на корисниците.", "apihelp-json-param-utf8": "Ако е укажано, ги шифрира највеќето (но не сите) не-ASCII знаци како UTF-8 наместо да ги заменува со хексадецимални изводни низи. Ова е стандардно кога formatversion не е 1.", "apihelp-json-param-ascii": "Ако е укажано, ги шифрира сите не-ASCII знаци како хексадецимални изводни низи. Ова е стандардно кога formatversion is 1.", - "apihelp-json-param-formatversion": "Форматирање на изводот:\n;1:Назадно-складен формат (булови во XML-стил, клучеви * за содржински јазли и тн.).\n;2:Пробен современ формат. Поединостите може да се изменат!\n;најнов:Користење на најновиот формат (тековно 2), може да се смени без предупредување.", + "apihelp-json-param-formatversion": "Форматирање на изводот:\n;1:Назадно-складен формат (булови во XML-стил, клучеви * за содржински јазли и тн.).\n;2:Пробен современ формат.\n;latest:Користење на најновиот формат (тековно 2), може да се смени без предупредување.", "apihelp-jsonfm-summary": "Давај го изводот во JSON-формат (подобрен испис во HTML).", "apihelp-none-summary": "Де давај извод.", "apihelp-php-summary": "Давај го изводот во серијализиран PHP-формат.", - "apihelp-php-param-formatversion": "Форматирање на изводот:\n;1:Назадно-складен формат (булови во XML-стил, клучеви * за содржински јазли и тн.).\n;2:Пробен современ формат. Поединостите може да се изменат!\n;најнов:Користење на најновиот формат (тековно 2), може да се смени без предупредување.", + "apihelp-php-param-formatversion": "Форматирање на изводот:\n;1:Назадно-складен формат (булови во XML-стил, клучеви * за содржински јазли и тн.).\n;2:Пробен современ формат.\n;latest:Користење на најновиот формат (тековно 2), може да се смени без предупредување.", "apihelp-phpfm-summary": "Давај го изводот во серијализиран PHP-формат (подобрен испис во HTML).", "apihelp-rawfm-summary": "Давај го изводот со елементи за отстранување грешки во JSON-формат (подобрен испис во HTML).", "apihelp-xml-summary": "Давај го изводот во XML-формат.", diff --git a/includes/api/i18n/nb.json b/includes/api/i18n/nb.json index 5e30c1a094..3ba6228cf9 100644 --- a/includes/api/i18n/nb.json +++ b/includes/api/i18n/nb.json @@ -24,7 +24,7 @@ "apihelp-main-param-origin": "Når man aksesserer API-en som bruker en domene-kryssende AJAX-forespørsel (CORS), sett denne til det opprinnelige domenet. Denne må tas med i alle pre-flight-forespørsler, og derfor være en del av spørre-URI-en (ikke POST-kroppen).\n\nFor autentiserte forespørsler må denne stemme helt med en av de opprinnelige i Origin-headeren, slik at den må settes til noe a la https://en.wikipedia.org eller https://meta.wikimedia.org. Hvis denne parameteren ikke stemmer med Origin-headeren, returneres et 403-svar. Hvis denne parameteren stemmer med Origin-headeren og originalen er hvitlistet, vil Access-Control-Allow-Origin og Access-Control-Allow-Credentials-headere bli satt.\n\nFor ikke-autentiserte forepørsler, spesifiser *. Denne vil gjøre at Access-Control-Allow-Origin-headeren blir satt, men Access-Control-Allow-Credentials blir false og alle bruerspesifikke data blir begrenset.", "apihelp-main-param-uselang": "Språk å bruke for meldingsoversettelser. [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] med siprop=languages returnerer en liste over språkkoder, eller spesifiser user for å bruke den nåværende brukerens språkpreferanser, eller spesifiser content for å bruke denne wikiens innholdsspråk.", "apihelp-main-param-errorformat": "Formater som kan brukes for advarsels- og feiltekster.\n; plaintext: Wikitext der HTML-tagger er fjernet og elementer byttet ut.\n; wikitext: Ubehandlet wikitext.\n; html: HTML.\n; raw: Meldingsnøkler og -parametre.\n; none: Ingen tekst, bare feilkoder.\n; bc: Format brukt før MediaWiki 1.29. errorlang og errorsuselocal ses bort fra.", - "apihelp-main-param-errorlang": "Språk som skal brukes for advarsler og feil. [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] med siprop=languages/ returnerer ei liste over språkkoder, eller angi content for å bruke wikiens innholdsspråk, eller angi uselang for å bruke samme verdi som uselang-parameteren.", + "apihelp-main-param-errorlang": "Språk som skal brukes for advarsler og feil. [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] med siprop=languages returnerer ei liste over språkkoder, eller angi content for å bruke wikiens innholdsspråk, eller angi uselang for å bruke samme verdi som uselang-parameteren.", "apihelp-main-param-errorsuselocal": "Hvis gitt, vil feiltekster bruke lokalt tilpassede meldinger fra {{ns:MediaWiki}}-navnerommet.", "apihelp-block-summary": "Blokker en bruker.", "apihelp-block-param-user": "Brukernavn, IP-adresse eller IP-intervall som skal blokkeres. Kan ikke brukes sammen med $1userid", diff --git a/includes/api/i18n/nl.json b/includes/api/i18n/nl.json index d7735f64e9..c77d82de9a 100644 --- a/includes/api/i18n/nl.json +++ b/includes/api/i18n/nl.json @@ -16,7 +16,8 @@ "Lemondoge", "Hex", "Mainframe98", - "Southparkfan" + "Southparkfan", + "Elroy" ] }, "apihelp-main-extended-description": "
\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
\nStatus: 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\nFoutieve verzoeken: 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

Testen: u kunt [[Special:ApiSandbox|eenvoudig API-verzoeken testen]].

", @@ -460,6 +461,7 @@ "apierror-import-unknownerror": "Onbekende fout trad op tijdens het importeren: $1.", "apierror-integeroutofrange-belowminimum": "$1 mag niet minder zijn dan $2 (ingesteld op $3).", "apierror-invalidcategory": "De opgegeven categorienaam is niet geldig.", + "apierror-invalidmethod": "Ongeldige http-methode. Overweeg de methodes GET of POST.", "apierror-invalidtitle": "Ongeldige titel \"$1\".", "apierror-invaliduser": "Ongeldige gebruikersnaam \"$1\".", "apierror-invaliduserid": "Gebruikers-ID $1 is ongeldig.", diff --git a/includes/api/i18n/pl.json b/includes/api/i18n/pl.json index 7b29b1f04e..3740243775 100644 --- a/includes/api/i18n/pl.json +++ b/includes/api/i18n/pl.json @@ -14,7 +14,8 @@ "Matma Rex", "Sethakill", "Woytecr", - "InternerowyGołąb" + "InternerowyGołąb", + "CiaPan" ] }, "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentacja]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista dyskusyjna]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Ogłoszenia dotyczące API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Błędy i propozycje]\n
\nStan: Wszystkie funkcje opisane na tej stronie powinny działać, ale API nadal jest aktywnie rozwijane i mogą się zmienić w dowolnym czasie. Subskrybuj [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ listę dyskusyjną mediawiki-api-announce], aby móc na bieżąco dowiadywać się o aktualizacjach.\n\nBłędne żądania: Gdy zostanie wysłane błędne żądanie do API, zostanie wysłany w odpowiedzi nagłówek HTTP z kluczem \"MediaWiki-API-Error\" i zarówno jego wartość jak i wartość kodu błędu wysłanego w odpowiedzi będą miały taką samą wartość. Aby uzyskać więcej informacji, zobacz [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Błędy i ostrzeżenia]].\n\nTestowanie: Aby łatwo testować żądania API, zobacz [[Special:ApiSandbox]].", @@ -42,6 +43,8 @@ "apihelp-block-param-reblock": "Jeżeli ten użytkownik jest już zablokowany, nadpisz blokadę.", "apihelp-block-param-watchuser": "Obserwuj stronę użytkownika lub IP oraz ich strony dyskusji.", "apihelp-block-param-tags": "Zmieniaj tagi by potwierdzić wejście do bloku logów.", + "apihelp-block-param-partial": "Zablokuj użytkownikowi dostęp do wybranych stron lub przestrzeni nazw zamiast do całej witryny.", + "apihelp-block-param-pagerestrictions": "Lista tytułów stron do zablokowania użytkownikowi możliwości edycji. Ma zastosowanie tylko gdy 'partial' (częściowa) jest ustawione na true (prawda).", "apihelp-block-example-ip-simple": "Zablokuj IP 192.0.2.5 na 3 dni z powodem First strike.", "apihelp-block-example-user-complex": "Zablokuj użytkownika Vandal na zawsze z powodem Vandalism i uniemożliw utworzenie nowego konta oraz wysyłanie emaili.", "apihelp-changeauthenticationdata-summary": "Zmień dane logowania bieżącego użytkownika.", @@ -223,7 +226,7 @@ "apihelp-opensearch-summary": "Przeszukaj wiki przy użyciu protokołu OpenSearch.", "apihelp-opensearch-param-search": "Wyszukaj tekst.", "apihelp-opensearch-param-limit": "Maksymalna liczba zwracanych wyników.", - "apihelp-opensearch-param-namespace": "Przestrzenie nazw do przeszukania.", + "apihelp-opensearch-param-namespace": "Przestrzenie nazw do przeszukania. Pomijane jeśli $1search zaczyna się od poprawnego przedrostka przestrzeni nazw.", "apihelp-opensearch-param-suggest": "Nic nie robi, jeżeli [[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]] ustawiono na false.", "apihelp-opensearch-param-redirects": "Jak obsługiwać przekierowania:\n;return:Zwróć samo przekierowanie.\n;resolve:Zwróć stronę docelową. Może zwrócić mniej niż wyników określonych w $1limit.\nZ powodów historycznych, domyślnie jest to \"return\" dla $1format=json, a \"resolve\" dla innych formatów.", "apihelp-opensearch-param-format": "Format danych wyjściowych.", @@ -381,6 +384,7 @@ "apihelp-query+blocks-paramvalue-prop-expiry": "Dodaje znacznik czasu wygaśnięcia blokady.", "apihelp-query+blocks-paramvalue-prop-reason": "Dodaje powód zablokowania.", "apihelp-query+blocks-paramvalue-prop-range": "Dodaje zakres adresów IP, na który zastosowano blokadę.", + "apihelp-query+blocks-paramvalue-prop-restrictions": "Dodaje częściowe ograniczenia jeśli blokada nie jest całościowa.", "apihelp-query+blocks-example-simple": "Listuj blokady.", "apihelp-query+categories-summary": "Lista kategorii, do których należą strony", "apihelp-query+categories-paramvalue-prop-timestamp": "Dodaje znacznik czasu dodania kategorii.", @@ -440,12 +444,12 @@ "apihelp-query+imageusage-example-simple": "Pokaż strony, które korzystają z [[:File:Albert Einstein Head.jpg]].", "apihelp-query+info-summary": "Pokaż podstawowe informacje o stronie.", "apihelp-query+info-paramvalue-prop-watchers": "Liczba obserwujących, jeśli jest to dozwolone.", - "apihelp-query+info-paramvalue-prop-readable": "Czy użytkownik może przeczytać tę stronę.", + "apihelp-query+info-paramvalue-prop-readable": "Czy użytkownik może przeczytać tę stronę. Zamiast tego użyj intestactions=read.", "apihelp-query+iwbacklinks-param-prefix": "Prefix interwiki.", "apihelp-query+iwbacklinks-param-limit": "Łączna liczba stron do zwrócenia.", "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "Dodaje prefiks interwiki.", "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "Dodaje tytuł interwiki.", - "apihelp-query+iwlinks-summary": "Wyświetla wszystkie liki interwiki z danych stron.", + "apihelp-query+iwlinks-summary": "Wyświetla wszystkie linki interwiki z danych stron.", "apihelp-query+iwlinks-paramvalue-prop-url": "Dodaje pełny adres URL.", "apihelp-query+iwlinks-param-limit": "Łączna liczba linków interwiki do zwrócenia.", "apihelp-query+langbacklinks-param-limit": "Łączna liczba stron do zwrócenia.", @@ -470,7 +474,7 @@ "apihelp-query+pageswithprop-example-simple": "Lista pierwszych 10 stron za pomocą {{DISPLAYTITLE:}}.", "apihelp-query+pageswithprop-example-generator": "Pobierz dodatkowe informacje o pierwszych 10 stronach wykorzystując __NOTOC__.", "apihelp-query+prefixsearch-param-search": "Wyszukaj tekst.", - "apihelp-query+prefixsearch-param-namespace": "Przestrzenie nazw do przeszukania.", + "apihelp-query+prefixsearch-param-namespace": "Przestrzenie nazw do przeszukania. Pomijane jeśli $1search zaczyna się od poprawnego przedrostka przestrzeni nazw.", "apihelp-query+prefixsearch-param-limit": "Maksymalna liczba zwracanych wyników.", "apihelp-query+prefixsearch-param-offset": "Liczba wyników do pominięcia.", "apihelp-query+protectedtitles-summary": "Lista wszystkich tytułów zabezpieczonych przed tworzeniem.", diff --git a/includes/api/i18n/pt-br.json b/includes/api/i18n/pt-br.json index df7ab405ba..1af3ce9cae 100644 --- a/includes/api/i18n/pt-br.json +++ b/includes/api/i18n/pt-br.json @@ -1464,11 +1464,11 @@ "apihelp-json-param-callback": "Se especificado, envolve a saída para uma determinada chamada de função. Por segurança, todos os dados específicos do usuário serão restritos.", "apihelp-json-param-utf8": "Se especificado, codifica a maioria (mas não todos) caracteres não-ASCII como UTF-8 em vez de substituí-los por sequências de escape hexadecimais. Padrão quando formatversion não é 1.", "apihelp-json-param-ascii": "Se especificado, codifica todos os não-ASCII usando sequências de escape hexadecimais. Padrão quando formatversion é 1.", - "apihelp-json-param-formatversion": "Formatação de saída:\n;1:formato compatível com versões anteriores (XML-style booleans, * chaves para nós de conteúdo, etc.).\n;2: formato moderno experimental. Detalhes podem ser alterados!\n;mais recente: use o formato mais recente (atualmente 2), pode mudar sem aviso prévio.", + "apihelp-json-param-formatversion": "Formatação do resultado de saída:\n;1:Formato compatível com versões anteriores (boolianos ao estilo XML, * chaves para nodos de conteúdo, etc.).\n;2:Formato moderno.\n;latest:Usar o formato mais recente (atualmente 2), mas pode ser alterado sem aviso prévio.", "apihelp-jsonfm-summary": "Dados de saída no formato JSON (pretty-print em HTML).", "apihelp-none-summary": "Nenhuma saída.", "apihelp-php-summary": "Dados de saída no formato PHP serializado.", - "apihelp-php-param-formatversion": "Formatação de saída:\n;1:formato compatível com versões anteriores (XML-style booleans, * chaves para nós de conteúdo, etc.).\n;2: formato moderno experimental. Detalhes podem ser alterados!\n;mais recente: use o formato mais recente (atualmente 2), pode mudar sem aviso prévio.", + "apihelp-php-param-formatversion": "Formatação do resultado de saída:\n;1:Formato compatível com versões anteriores (boolianos ao estilo XML, * chaves para nodos de conteúdo, etc.).\n;2:Formato moderno.\n;latest:Usar o formato mais recente (atualmente 2), mas pode ser alterado sem aviso prévio.", "apihelp-phpfm-summary": "Dados de saída em formato serializado em PHP (pretty-print em HTML).", "apihelp-rawfm-summary": "Dados de saída, incluindo elementos de depuração, no formato JSON (pretty-print em HTML).", "apihelp-xml-summary": "Dados de saída em formato XML.", @@ -1614,6 +1614,7 @@ "apierror-compare-nofromrevision": "Não foi especificada uma revisão 'from'. Especificar fromrev, fromtitle ou fromid.", "apierror-compare-notext": "O parâmetro $1 não pode ser usado sem $2.", "apierror-compare-notorevision": "Não foi especificada uma revisão 'to'. Especificar torev, totitle ou toid.", + "apierror-compare-relative-to-deleted": "Não pode usar torelative=$1 com relação a uma revisão eliminada.", "apierror-compare-relative-to-nothing": "Nenhuma revisão 'from' para torelative 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}}.", @@ -1642,6 +1643,7 @@ "apierror-invalidexpiry": "Tempo de expiração \"$1\" não válido.", "apierror-invalid-file-key": "Não é uma chave de arquivo válida.", "apierror-invalidlang": "Código de idioma inválido para o parâmetro $1.", + "apierror-invalidmethod": "Método HTTP inválido. Considere o uso de GET ou POST.", "apierror-invalidoldimage": "O parâmetro oldimage possui um formato inválido.", "apierror-invalidparammix-cannotusewith": "O parâmetro $1 não pode ser usado com $2.", "apierror-invalidparammix-mustusewith": "O parâmetro $1 só pode ser usado com $2.", @@ -1770,6 +1772,8 @@ "apiwarn-badurlparam": "Não foi possível analisar $1urlparam por $2. Usando apenas largura e altura.", "apiwarn-badutf8": "O valor passado para $1 contém dados inválidos ou não normalizados. Os dados textuais devem ser válidos, NFC-normalizado Unicode sem caracteres de controle C0 diferentes de HT (\\t), LF (\\n) e CR (\\r).", "apiwarn-checktoken-percentencoding": "Verificar se os símbolos, como \"+\" no token, estão codificados corretamente na URL.", + "apiwarn-compare-no-next": "A revisão $2 é a revisão mais recente de $1, não há nenhuma revisão seguinte para comparar com torelative=next.", + "apiwarn-compare-no-prev": "A revisão $2 é a revisão mais antiga de $1, não há nenhuma revisão anterior para comparar com torelative=prev.", "apiwarn-compare-nocontentmodel": "Nenhum modelo de conteúdo pode ser determinado, assumindo $1.", "apiwarn-deprecation-deletedrevs": "list=deletedrevs foi depreciado. Por favor, use prop=deletedrevisions ou list=alldeletedrevisions em vez.", "apiwarn-deprecation-httpsexpected": "HTTP usado quando o HTTPS era esperado.", diff --git a/includes/api/i18n/pt.json b/includes/api/i18n/pt.json index c1e10ca526..02ae87c21d 100644 --- a/includes/api/i18n/pt.json +++ b/includes/api/i18n/pt.json @@ -1459,11 +1459,11 @@ "apihelp-json-param-callback": "Se especificado, envolve o resultado de saída na forma de uma chamada para uma função. Por segurança, todos os dados específicos do utilizador estarão restringidos.", "apihelp-json-param-utf8": "Se especificado, codifica a maioria dos caracteres não ASCII (mas não todos) em UTF-8, em vez de substitui-los por sequências de escape hexadecimais. É o comportamento padrão quando formatversion não tem o valor 1.", "apihelp-json-param-ascii": "Se especificado, codifica todos caracteres não ASCII usando sequências de escape hexadecimais. É o comportamento padrão quando formatversion tem o valor 1.", - "apihelp-json-param-formatversion": "Formatação do resultado de saída:\n;1:Formato compatível com versões anteriores (boolianos ao estilo XML, * chaves para nodos de conteúdo, etc.).\n;2:Formato moderno experimental. As especificações podem mudar!\n;latest:Usar o formato mais recente (atualmente 2), mas pode ser alterado sem aviso prévio.", + "apihelp-json-param-formatversion": "Formatação do resultado de saída:\n;1:Formato compatível com versões anteriores (boolianos ao estilo XML, * chaves para nodos de conteúdo, etc.).\n;2:Formato moderno.\n;latest:Usar o formato mais recente (atualmente 2), mas pode ser alterado sem aviso prévio.", "apihelp-jsonfm-summary": "Produzir os dados de saída em formato JSON (realce sintático em HTML).", "apihelp-none-summary": "Não produzir nada.", "apihelp-php-summary": "Produzir os dados de saída em formato PHP seriado.", - "apihelp-php-param-formatversion": "Formatação do resultado de saída:\n;1:Formato compatível com versões anteriores (boolianos ao estilo XML, * chaves para nodos de conteúdo, etc.).\n;2:Formato moderno experimental. As especificações podem mudar!\n;latest:Usar o formato mais recente (atualmente 2), mas pode ser alterado sem aviso prévio.", + "apihelp-php-param-formatversion": "Formatação do resultado de saída:\n;1:Formato compatível com versões anteriores (boolianos ao estilo XML, * chaves para nodos de conteúdo, etc.).\n;2:Formato moderno.\n;latest:Usar o formato mais recente (atualmente 2), mas pode ser alterado sem aviso prévio.", "apihelp-phpfm-summary": "Produzir os dados de saída em formato PHP seriado (realce sintático em HTML).", "apihelp-rawfm-summary": "Produzir os dados de saída, incluindo elementos para despiste de erros, em formato JSON (realce sintático em HTML).", "apihelp-xml-summary": "Produzir os dados de saída em formato XML.", @@ -1544,7 +1544,7 @@ "api-help-permissions-granted-to": "{{PLURAL:$1|Concedida a|Concedidas a}}: $2", "api-help-right-apihighlimits": "Usar limites mais altos em consultas da API (consultas lentas: $1; consultas rápidas: $2). Os limites para consultas lentas também se aplicam a parâmetros com vários valores.", "api-help-open-in-apisandbox": "[abrir na página de testes]", - "api-help-authmanager-general-usage": "O procedimento geral para usar este módulo é:\n# Obtenha os campos disponíveis usando [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] com amirequestsfor=$4 e uma chave $5 obtida de [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].\n# Apresente os campos ao utilizador e obtenha os dados fornecidos por este.\n# Publique-os para este módulo, fornecendo $1returnurl e quaisquer campos relevantes.\n# Verifique o valor de status na resposta.\n#* Se recebeu PASS ou FAIL, terminou. A operação terá tido sucesso ou falhado.\n#* Se recebeu UI, apresente os novos campos ao utilizador e obtenha os dados fornecidos por este. Depois publique-os para este módulo com $1continue e os campos relevantes preenchidos, e repita o passo 4.\n#* Se recebeu REDIRECT, encaminhe o utilizador para redirecttarget e aguarde o retorno para o URL $1returnurl. Depois publique para este módulo com $1continue quaisquer campos que tenham sido passados ao URL de retorno, e repita o passo 4.\n#* Se recebeu RESTART, isto significa que a autenticação funcionou mas não temos uma conta de utilizador associada. Pode dar-lhe o tratamento de UI ou FAIL.", + "api-help-authmanager-general-usage": "O procedimento geral para usar este módulo é:\n# Obtenha os campos disponíveis usando [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] com amirequestsfor=$4 e uma chave $5 obtida de [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].\n# Apresente os campos ao utilizador e obtenha os dados fornecidos por este.\n# Publique-os para este módulo, fornecendo $1returnurl e quaisquer campos relevantes.\n# Verifique o valor de status na resposta.\n#* Se recebeu PASS ou FAIL, terminou. A operação terá tido êxito ou falhado.\n#* Se recebeu UI, apresente os novos campos ao utilizador e obtenha os dados fornecidos por este. Depois publique-os para este módulo com $1continue e os campos relevantes preenchidos, e repita o passo 4.\n#* Se recebeu REDIRECT, encaminhe o utilizador para redirecttarget e aguarde o retorno para o URL $1returnurl. Depois publique para este módulo com $1continue quaisquer campos que tenham sido passados ao URL de retorno, e repita o passo 4.\n#* Se recebeu RESTART, isto significa que a autenticação funcionou mas não temos uma conta de utilizador associada. Pode dar-lhe o tratamento de UI ou FAIL.", "api-help-authmanagerhelper-requests": "Usar só estes pedidos de autenticação, com o id devolvido por [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] com amirequestsfor=$1 ou por uma resposta anterior deste módulo.", "api-help-authmanagerhelper-request": "Usar este pedido de autenticação, com o id devolvido por [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] com amirequestsfor=$1.", "api-help-authmanagerhelper-messageformat": "Formato a usar nas mensagens de saída.", @@ -1609,6 +1609,7 @@ "apierror-compare-nofromrevision": "Não foi especificada uma revisão 'from'. Especificar fromrev, fromtitle ou fromid.", "apierror-compare-notext": "O parâmetro $1 não pode ser usado sem $2.", "apierror-compare-notorevision": "Não foi especificada uma revisão 'to'. Especificar torev, totitle ou toid.", + "apierror-compare-relative-to-deleted": "Não pode usar torelative=$1 com relação a uma revisão eliminada.", "apierror-compare-relative-to-nothing": "Não existe uma revisão 'from' em relação à qual torelative 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}}.", @@ -1637,6 +1638,7 @@ "apierror-invalidexpiry": "A hora de expiração \"$1\" é inválida.", "apierror-invalid-file-key": "Não é uma chave de ficheiro válida.", "apierror-invalidlang": "Código de língua inválido para o parâmetro $1.", + "apierror-invalidmethod": "Método HTTP inválido. Use GET ou POST.", "apierror-invalidoldimage": "O parâmetro oldimage tem um formato inválido.", "apierror-invalidparammix-cannotusewith": "O parâmetro $1 não pode ser usado com $2.", "apierror-invalidparammix-mustusewith": "O parâmetro $1 só pode ser usado com $2.", @@ -1765,6 +1767,8 @@ "apiwarn-badurlparam": "Não foi possível analisar $1urlparam para $2. Serão utilizadas somente a largura e a altura.", "apiwarn-badutf8": "O valor passado para $1 contém dados inválidos ou não normalizados. Os dados textuais devem estar em formato Unicode válido, normalizado em NFC, sem caracteres de controlo C0 exceto HT (\\t), LF (\\n) e CR (\\r).", "apiwarn-checktoken-percentencoding": "Verifique que símbolos como \"+\" na chave estão devidamente codificados com percentagem no URL.", + "apiwarn-compare-no-next": "A revisão $2 é a revisão mais recente de $1, não há nenhuma revisão seguinte para comparar com torelative=next.", + "apiwarn-compare-no-prev": "A revisão $2 é a revisão mais antiga de $1, não há nenhuma revisão anterior para comparar com torelative=prev.", "apiwarn-compare-nocontentmodel": "Não foi possível determinar nenhum modelo de conteúdo; será assumido $1.", "apiwarn-deprecation-deletedrevs": "list=deletedrevs foi descontinuado. Em substituição, use prop=deletedrevisions ou list=alldeletedrevisions, por favor.", "apiwarn-deprecation-httpsexpected": "Foi usado HTTP quando era esperado HTTPS.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index 83427bafd8..6437adfcd0 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -1618,6 +1618,7 @@ "apierror-compare-nofromrevision": "{{doc-apierror}}", "apierror-compare-notext": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter that is not allowed without totext-{role}/fromtext-{role}.\n* $2 - The specific totext-{role}/fromtext-{role} parameter that must be present.", "apierror-compare-notorevision": "{{doc-apierror}}", + "apierror-compare-relative-to-deleted": "{{doc-apierror}}", "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.", @@ -1646,6 +1647,7 @@ "apierror-invalidexpiry": "{{doc-apierror}}\n\nParameters:\n* $1 - Value provided.", "apierror-invalid-file-key": "{{doc-apierror}}", "apierror-invalidlang": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", + "apierror-invalidmethod": "{{doc-apierror}}\n\nShown when a user tries to access the API using an HTTP method that is not supported", "apierror-invalidoldimage": "{{doc-apierror}}", "apierror-invalidparammix-cannotusewith": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name or \"parameter=value\" text.\n* $2 - Parameter name or \"parameter=value\" text.", "apierror-invalidparammix-mustusewith": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name or \"parameter=value\" text.\n* $2 - Parameter name or \"parameter=value\" text.", @@ -1775,6 +1777,8 @@ "apiwarn-badurlparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Image title.", "apiwarn-badutf8": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n{{doc-important|Do not translate \"\\t\", \"\\n\", and \"\\r\"}}", "apiwarn-checktoken-percentencoding": "{{doc-apierror}}", + "apiwarn-compare-no-next": "{{doc-apierror}}\n\nParameters:\n* $1 - Title of the page.\n* $2 - Revision ID.", + "apiwarn-compare-no-prev": "{{doc-apierror}}\n\nParameters:\n* $1 - Title of the page.\n* $2 - Revision ID.", "apiwarn-compare-nocontentmodel": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model being assumed.", "apiwarn-deprecation-deletedrevs": "{{doc-apierror}}", "apiwarn-deprecation-httpsexpected": "{{doc-apierror}}", diff --git a/includes/api/i18n/ru.json b/includes/api/i18n/ru.json index 6a239f6e17..0206463104 100644 --- a/includes/api/i18n/ru.json +++ b/includes/api/i18n/ru.json @@ -862,7 +862,7 @@ "apihelp-query+info-paramvalue-prop-notificationtimestamp": "Временная метка уведомления для списка наблюдения для каждой страницы.", "apihelp-query+info-paramvalue-prop-subjectid": "Идентификатор родительской страницы для каждой страницы обсуждения.", "apihelp-query+info-paramvalue-prop-url": "Возвращает полную ссылку, ссылку на редактирование и каноничную ссылку для каждой страницы.", - "apihelp-query+info-paramvalue-prop-readable": "Может ли участник просматривать эту страницу.", + "apihelp-query+info-paramvalue-prop-readable": "Может ли участник просматривать эту страницу. Используйте вместо этого intestactions=read.", "apihelp-query+info-paramvalue-prop-preload": "Текст, возвращённый EditFormPreloadText.", "apihelp-query+info-paramvalue-prop-displaytitle": "Возвращает стиль отображения заголовка страницы.", "apihelp-query+info-paramvalue-prop-varianttitles": "Выдаёт отображаемый заголовок во всех вариантах языка контента сайта.", @@ -1453,11 +1453,11 @@ "apihelp-json-param-callback": "Если задано, оборачивает вывод в вызов данной функции. Из соображении безопасности, вся пользовательская информация будет удалена.", "apihelp-json-param-utf8": "Если задано, кодирует большинство (но не все) не-ASCII символов в UTF-8 вместо замены их на шестнадцатеричные коды. Применяется по умолчанию, когда formatversion не равно 1.", "apihelp-json-param-ascii": "Если задано, заменяет все не-ASCII-символы на шестнадцатеричные коды. Применяется по умолчанию, когда formatversion равно 1.", - "apihelp-json-param-formatversion": "Формат вывода:\n;1: Обратно совместимый формат (логические значения в стиле XML, ключи * для узлов данных, и так далее).\n;2: Экспериментальный современный формат. Детали могут меняться!\n;latest: Использовать последний формат (сейчас 2), может меняться без предупреждения.", + "apihelp-json-param-formatversion": "Формат вывода:\n;1: Обратно совместимый формат (логические значения в стиле XML, ключи * для узлов данных, и так далее).\n;2: Современный формат. \n;latest: Использовать последний формат (сейчас 2), может меняться без предупреждения.", "apihelp-jsonfm-summary": "Выводить данные в формате JSON (отформатированном в HTML).", "apihelp-none-summary": "Ничего не выводить.", "apihelp-php-summary": "Выводить данные в сериализованном формате PHP.", - "apihelp-php-param-formatversion": "Формат вывода:\n;1: Обратно совместимый формат (логические значения в стиле XML, ключи * для узлов данных, и так далее).\n;2: Экспериментальный современный формат. Детали могут меняться!\n;latest: Использовать последний формат (сейчас 2), может меняться без предупреждения.", + "apihelp-php-param-formatversion": "Формат вывода:\n;1: Обратно совместимый формат (логические значения в стиле XML, ключи * для узлов данных, и так далее).\n;2: Современный формат.\n;latest: Использовать последний формат (сейчас 2), может меняться без предупреждения.", "apihelp-phpfm-summary": "Выводить данные в сериализованном формате PHP (отформатированном в HTML).", "apihelp-rawfm-summary": "Выводить данные, включая элементы отладки, в формате JSON (отформатированном в HTML).", "apihelp-xml-summary": "Выводить данные в формате XML.", diff --git a/includes/api/i18n/sv.json b/includes/api/i18n/sv.json index 811ccc7a74..bf5d89e2a0 100644 --- a/includes/api/i18n/sv.json +++ b/includes/api/i18n/sv.json @@ -557,5 +557,7 @@ "apierror-systemblocked": "Du har blockerats automatiskt av MediaWiki.", "apierror-timeout": "Servern svarade inte inom förväntad tid.", "apierror-unknownformat": "Okänt format \"$1\".", + "apiwarn-compare-no-next": "Sidversion $2 är den senaste sidversionen av $1, det finns ingen sidversion för torelative=next att jämföra med.", + "apiwarn-compare-no-prev": "Sidversionen $2 är den tidigaste sidversion för $1, det finns ingen sidversion för torelative=prev att jämföra med.", "api-feed-error-title": "Fel ($1)" } diff --git a/includes/api/i18n/uk.json b/includes/api/i18n/uk.json index 21601c1bd9..2a6680199e 100644 --- a/includes/api/i18n/uk.json +++ b/includes/api/i18n/uk.json @@ -14,7 +14,8 @@ "AS", "Umherirrender", "Choomaq", - "Zoranzoki21" + "Zoranzoki21", + "Vlad5250" ] }, "apihelp-main-extended-description": "
\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
\nСтатус: Усі функції, вказані на цій сторінці, мають працювати, але API далі перебуває в активній розробці і може змінитися у будь-який момент. Підпишіться на [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ список розсилки mediawiki-api-announce], щоб помічати оновлення.\n\nХибні запити: Коли до API надсилаються хибні запити, буде відіслано HTTP-шапку з ключем «MediaWiki-API-Error», а тоді і значення шапки, і код помилки, надіслані назад, будуть встановлені з тим же значенням. Більше інформації див. на сторінці [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Помилки й попередження]].\n\n

Тестування: Для зручності тестування запитів API, див. [[Special:ApiSandbox]].

", @@ -1457,11 +1458,11 @@ "apihelp-json-param-callback": "Якщо вказано, огортає вивід викликом даної функції. З міркувань безпеки, усі специфічні до користувача дані буде утримано.", "apihelp-json-param-utf8": "Якщо вказано, кодує більшість (але не всі) не-ASCII символів як UTF-8, замість заміни їх шістнадцятковими екрануючими послідовностями. За замовчуванням коли formatversion не є 1.", "apihelp-json-param-ascii": "Якщо вказано, кодує всі не-ASCII використовуючи шістнадцяткові екрануючі послідовності. За замовчуванням коли formatversion є 1.", - "apihelp-json-param-formatversion": "Форматування виводу:\n;1:Формат зворотної сумісності (булеви XML-стилю, * ключі для вузлів вмісту тощо).\n;2:Експериментальний сучасний формат. Деталі можуть змінюватись.\n;latest:Використовувати найостанніший формат (наразі 2). Може змінюватись без попередження.", + "apihelp-json-param-formatversion": "Форматування виводу:\n;1:Формат зворотної сумісності (булеви XML-стилю, * ключі для вузлів вмісту тощо).\n;2:Сучасний формат.\n;latest:Використовувати найостанніший формат (наразі 2). Може змінюватись без попередження.", "apihelp-jsonfm-summary": "Вивести дані у форматі JSON (вивід відформатованого коду за допомогою HTML).", "apihelp-none-summary": "Нічого не виводити.", "apihelp-php-summary": "Виводити дані у форматі серіалізованого PHP.", - "apihelp-php-param-formatversion": "Форматування виводу:\n;1:Формат зворотної сумісності (булеви XML-стилю, * ключі для вузлів вмісту тощо).\n;2:Експериментальний сучасний формат. Деталі можуть змінюватись.\n;latest:Використовувати найостанніший формат (наразі 2). Може змінюватись без попередження.", + "apihelp-php-param-formatversion": "Форматування виводу:\n;1:Формат зворотної сумісності (булеви XML-стилю, * ключі для вузлів вмісту тощо).\n;2:Сучасний формат.\n;latest:Використовувати найостанніший формат (наразі 2). Може змінюватись без попередження.", "apihelp-phpfm-summary": "Виводити дані у форматі серіалізованого PHP (вивід відформатованого коду за допомогою HTML).", "apihelp-rawfm-summary": "Виводити дані, включно з елементами налагодження, у форматі JSON (вивід відформатованого коду за допомогою HTML).", "apihelp-xml-summary": "Виводити дані у форматі XML.", diff --git a/includes/api/i18n/zh-hant.json b/includes/api/i18n/zh-hant.json index 0c9a0bd8a5..ab32bcb4ef 100644 --- a/includes/api/i18n/zh-hant.json +++ b/includes/api/i18n/zh-hant.json @@ -46,22 +46,28 @@ "apihelp-block-param-watchuser": "監視使用者或 IP 位址的使用者頁面與對話頁面。", "apihelp-block-param-tags": "在封鎖日誌裡更改套用到項目的標籤。", "apihelp-block-param-partial": "封鎖使用者訪問特殊頁面或命名空間,而不是整個網站。", + "apihelp-block-param-pagerestrictions": "封鎖使用者做出編輯的標題清單。僅在「partial」被設定為 true 時套用。", "apihelp-block-example-ip-simple": "封鎖 IP 位址 192.0.2.5 三天,原因為 First strike。", "apihelp-block-example-user-complex": "永久封鎖 IP 位址 Vandal,原因為 Vandalism。", "apihelp-changeauthenticationdata-summary": "為目前使用者變更身分核對資料。", "apihelp-changeauthenticationdata-example-password": "嘗試更改目前使用者的密碼至 ExamplePassword。", - "apihelp-checktoken-summary": "檢查來自 [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] 的密鑰有效性。", + "apihelp-checktoken-summary": "檢查來自 [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] 的權杖有效性。", "apihelp-checktoken-param-type": "要測試的密鑰類型。", - "apihelp-checktoken-param-token": "要測試的密鑰。", - "apihelp-checktoken-param-maxtokenage": "密鑰的有效期間,以秒為單位。", - "apihelp-checktoken-example-simple": "測試 csrf 密鑰的有效性。", + "apihelp-checktoken-param-token": "要測試的權杖。", + "apihelp-checktoken-param-maxtokenage": "權杖的有效期間,以秒為單位。", + "apihelp-checktoken-example-simple": "測試 csrf 權杖的有效性。", "apihelp-clearhasmsg-summary": "清除目前使用者的 hasmsg 標記。", "apihelp-clearhasmsg-example-1": "清除目前使用者的 hasmsg 標記。", + "apihelp-clientlogin-summary": "使用互動流程來登入 wiki。", + "apihelp-clientlogin-example-login": "開始以使用者 Example 與密碼 ExamplePassword 來登入至 wiki 的過程。", + "apihelp-clientlogin-example-login2": "在 UI 回應雙重認證後繼續登入,提供 987654 的 OATHToken。", "apihelp-compare-summary": "比較 2 個頁面間的差異。", "apihelp-compare-extended-description": "\"from\" 以及 \"to\" 的修訂編號,頁面標題或頁面 ID 為必填。", "apihelp-compare-param-fromtitle": "要比對的第一個標題。", "apihelp-compare-param-fromid": "要比對的第一個頁面 ID。", "apihelp-compare-param-fromrev": "要比對的第一個修訂。", + "apihelp-compare-param-frompst": "在 fromtext-{slot} 進行預先儲存轉換。", + "apihelp-compare-param-fromcontentmodel-{slot}": "fromtext-{slot} 內容模組。若不提供,則會根據其它參數猜測。", "apihelp-compare-param-fromcontentformat-{slot}": "fromtext-{slot} 的內容序列化格式。", "apihelp-compare-param-fromtext": "指定 fromslots=main 並改用 fromtext-main。", "apihelp-compare-param-fromcontentmodel": "指定 fromslots=main 並改使用 fromcontentmodel-main。", @@ -70,6 +76,7 @@ "apihelp-compare-param-toid": "要比對的第二個頁面 ID。", "apihelp-compare-param-torev": "要比對的第二個修訂。", "apihelp-compare-param-topst": "在 totext 執行預先保存轉換。", + "apihelp-compare-param-tocontentmodel-{slot}": "totext-{slot} 內容模組。若不提供,則會基於其它參數來猜測。", "apihelp-compare-param-tocontentformat-{slot}": "totext-{slot} 的內容序列化格式。", "apihelp-compare-param-totext": "指定 toslots=main 並改用 totext-main。", "apihelp-compare-param-tocontentmodel": "指定 toslots=main 並改使用 tocontentmodel-main。", @@ -85,10 +92,11 @@ "apihelp-compare-paramvalue-prop-size": "「from」與「to」修訂的大小。", "apihelp-compare-example-1": "建立修訂 1 與 1 的差異檔", "apihelp-createaccount-summary": "建立新使用者帳號。", + "apihelp-createaccount-example-create": "開始建立使用者 Example 與密碼 ExamplePassword 的過程。", "apihelp-createaccount-param-name": "使用者名稱。", "apihelp-createaccount-param-password": "密碼 (若有設定 $1mailpassword 則可略過)。", "apihelp-createaccount-param-domain": "外部身分核對使用的網域 (可有可無)。", - "apihelp-createaccount-param-token": "在第一次請求時已取得的帳號建立金鑰。", + "apihelp-createaccount-param-token": "在第一次請求時已取得的帳號建立權杖。", "apihelp-createaccount-param-email": "使用者的電子郵件地址 (可有可無) 。", "apihelp-createaccount-param-realname": "使用者的真實姓名 (可有可無)。", "apihelp-createaccount-param-mailpassword": "若設為其他值,將會以電子郵件寄送隨機密碼給使用者。", @@ -96,13 +104,14 @@ "apihelp-createaccount-param-language": "要設定的使用者預設語言代碼 (選填,預設依據內容語言)。", "apihelp-createaccount-example-pass": "建立使用者 testuser 使用密碼 test123", "apihelp-createaccount-example-mail": "建立使用者 testmailuser 並且電子郵件通知隨機產生的密碼。", + "apihelp-cspreport-param-source": "生成觸發此報告之 CSP 標頭的事物", "apihelp-delete-summary": "刪除頁面。", "apihelp-delete-param-title": "您欲刪除的頁面標題。 無法與 $1pageid 同時使用。", "apihelp-delete-param-pageid": "您欲刪除頁面的頁面 ID。 無法與 $1title 同時使用。", "apihelp-delete-param-reason": "刪除的原因。 若未設定,將會使用自動產生的原因。", "apihelp-delete-param-tags": "在刪除日誌裡更改套用到項目的標籤。", "apihelp-delete-param-watch": "加入目前頁面至您的監視清單。", - "apihelp-delete-param-watchlist": "無條件使用設置將頁面加入或移除目前使用者的監視清單或者是不更改監視清單。", + "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": "刪除 Main Page。", @@ -111,7 +120,7 @@ "apihelp-edit-summary": "建立與編輯頁面。", "apihelp-edit-param-title": "您欲編輯的頁面標題。 無法與 $1pageid 同時使用。", "apihelp-edit-param-pageid": "您欲編輯頁面的頁面 ID。 無法與 $1title 同時使用。", - "apihelp-edit-param-section": "章節編號。 0 代表最上層章節,new 代表新章節。", + "apihelp-edit-param-section": "章節編號。0 代表最上層章節,new 代表新章節。", "apihelp-edit-param-sectiontitle": "新章節的標題。", "apihelp-edit-param-text": "頁面內容。", "apihelp-edit-param-summary": "編輯摘要。 當未設定 $1section=new 與 $1sectiontitle 時也會當做章節標題。", @@ -120,17 +129,20 @@ "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-recreate": "覆蓋有關頁面在此期間已被刪除的任何錯誤。", "apihelp-edit-param-createonly": "若頁面已存在,則不編輯頁面。", "apihelp-edit-param-nocreate": "若頁面不存在,則產生錯誤。", "apihelp-edit-param-watch": "加入目前頁面至您的監視清單。", "apihelp-edit-param-unwatch": "從您的監視清單中移除目前頁面。", - "apihelp-edit-param-watchlist": "無條件使用設置將頁面加入或移除目前使用者的監視清單或者是不更改監視清單。", + "apihelp-edit-param-watchlist": "使用偏好設定無條件地將頁面加入至或移除自目前使用者的監視清單,或不更改監視。", "apihelp-edit-param-prependtext": "添加此文字至頁面開頭。覆蓋$1text。", + "apihelp-edit-param-appendtext": "添加至頁面結尾的文字。覆蓋$1text。\n\n使用 $1section=new 來添加新的段落,而非此參數。", "apihelp-edit-param-undo": "復原此修訂。覆寫 $1text、$1prependtext 與 $1appendtext。", "apihelp-edit-param-undoafter": "撤銷從 $1undo 至此為止的所有修訂。若不設定則僅會撤銷一次修訂。", "apihelp-edit-param-redirect": "自動化解決重新導向。", "apihelp-edit-param-contentformat": "用於輸入文字的內容序列化格式。", "apihelp-edit-param-contentmodel": "新內容的內容模組。", + "apihelp-edit-param-token": "權杖應用為發送的最後參數,或至少在 $1text 參數之後。", "apihelp-edit-example-edit": "編輯頁面", "apihelp-edit-example-prepend": "前置頁面的 __NOTOC__。", "apihelp-edit-example-undo": "撤銷從 13579 至 13585 之間的修訂,並帶自動生成的摘要。", @@ -149,10 +161,12 @@ "apihelp-expandtemplates-paramvalue-prop-volatile": "輸出內容是否易變,且是否不應在頁面其它位置裡重複使用。", "apihelp-expandtemplates-paramvalue-prop-jsconfigvars": "指定頁面的 JavaScript 設置變量。", "apihelp-expandtemplates-paramvalue-prop-encodedjsconfigvars": "指定頁面的 JavaScript 設置變量為 JSON 字串。", + "apihelp-expandtemplates-paramvalue-prop-parsetree": "輸出的 XML 解析樹狀。", "apihelp-expandtemplates-param-includecomments": "輸出裡是否包含 HTML 註解。", + "apihelp-expandtemplates-param-generatexml": "產生 XML 解析樹狀(以 $1prop=parsetree 取代)。", "apihelp-expandtemplates-example-simple": "展開 wiki 文字{{Project:Sandbox}}。", - "apihelp-feedcontributions-summary": "回傳使用者貢獻 Feed。", - "apihelp-feedcontributions-param-feedformat": "Feed 的格式。", + "apihelp-feedcontributions-summary": "回傳使用者貢獻摘要。", + "apihelp-feedcontributions-param-feedformat": "摘要的格式。", "apihelp-feedcontributions-param-user": "要取得哪些使用者的貢獻。", "apihelp-feedcontributions-param-year": "起始年份(更早之前)。", "apihelp-feedcontributions-param-month": "起始月份(更早之前)。", @@ -163,7 +177,7 @@ "apihelp-feedcontributions-param-hideminor": "隱藏小編輯。", "apihelp-feedcontributions-param-showsizediff": "顯示修訂版本之間的差異大小。", "apihelp-feedcontributions-example-simple": "返回使用者Example的貢獻。", - "apihelp-feedrecentchanges-summary": "返回最近變更摘要。", + "apihelp-feedrecentchanges-summary": "返回近期變更摘要。", "apihelp-feedrecentchanges-param-feedformat": "摘要格式。", "apihelp-feedrecentchanges-param-namespace": "用於限制結果的命名空間。", "apihelp-feedrecentchanges-param-invert": "除所選定者外的所有命名空間。", @@ -182,16 +196,20 @@ "apihelp-feedrecentchanges-param-target": "僅顯示從該頁面所連結頁面上的變更。", "apihelp-feedrecentchanges-example-simple": "顯示近期變更。", "apihelp-feedrecentchanges-example-30days": "顯示近期30天內的變動", - "apihelp-feedwatchlist-summary": "返回監視清單 feed。", - "apihelp-feedwatchlist-param-feedformat": "Feed 的格式。", + "apihelp-feedwatchlist-summary": "返回監視清單摘要。", + "apihelp-feedwatchlist-param-feedformat": "摘要的格式。", "apihelp-feedwatchlist-param-hours": "列出在幾小時內的頁面變動。", "apihelp-feedwatchlist-param-linktosections": "若可以的話,直接連結至更改過的段落。", + "apihelp-feedwatchlist-example-default": "顯示監視清單摘要。", "apihelp-feedwatchlist-example-all6hrs": "顯示過去 6 小時在監視頁面的所有更改。", "apihelp-filerevert-summary": "回退檔案至舊的版本。", "apihelp-filerevert-param-filename": "目標檔案名稱,不需包含「File:」這樣的前綴字元。", "apihelp-filerevert-param-comment": "上載意見。", + "apihelp-filerevert-param-archivename": "要復原的修訂存檔名稱。", "apihelp-filerevert-example-revert": "回退 Wiki.png 至 2011-03-05T15:27:40Z 的版本。", "apihelp-help-summary": "顯示指定模組的說明。", + "apihelp-help-param-helpformat": "說明輸出的格式。", + "apihelp-help-param-wrap": "在標準 API 回應結構裡包裹輸出。", "apihelp-help-param-toc": "在 HTML 輸出裡包含目錄。", "apihelp-help-example-main": "主模組使用說明", "apihelp-help-example-submodules": "用於 action=query 與其所有子模組的幫助。", @@ -204,6 +222,7 @@ "apihelp-imagerotate-example-simple": "90 度旋轉 File:Example.png。", "apihelp-imagerotate-example-generator": "180 度旋轉所有在 Category:Flip 裡的圖片。", "apihelp-import-summary": "從其它 wiki 或 XML 檔案來匯入頁面。", + "apihelp-import-extended-description": "請注意當發送用於 xml 參數的檔案時,必須以 HTTP POST 作為檔案上傳(註:使用 multipart/form-data)。", "apihelp-import-param-summary": "匯入摘要。", "apihelp-import-param-xml": "上載的 XML 檔。", "apihelp-import-param-assignknownusers": "分配編輯至所命名使用者已存在本地的本地使用者。", @@ -217,9 +236,12 @@ "apihelp-linkaccount-summary": "從第三方供應者來連結帳號至目前的使用者。", "apihelp-linkaccount-example-link": "開始進行從 Example 連結至帳號的程序。", "apihelp-login-summary": "登入並取得身分核對 cookies", + "apihelp-login-extended-description-nobotpasswords": "此操作已被棄用,且可能在不帶警告的情況下失敗。要安全登入請使用 [[Special:ApiHelp/clientlogin|action=clientlogin]]。", "apihelp-login-param-name": "使用者名稱。", "apihelp-login-param-password": "密碼。", "apihelp-login-param-domain": "網域名稱(可有可無)。", + "apihelp-login-param-token": "在首次請求獲得的登入權杖。", + "apihelp-login-example-gettoken": "檢索登入權杖。", "apihelp-login-example-login": "登入", "apihelp-logout-summary": "登出並清除 session 資料。", "apihelp-logout-example-logout": "登出當前使用者", @@ -248,7 +270,7 @@ "apihelp-move-param-noredirect": "不要建立重新導向。", "apihelp-move-param-watch": "將頁面和重定向加入目前使用者的監視清單。", "apihelp-move-param-unwatch": "從目前使用者的監視清單中移除頁面和重定向。", - "apihelp-move-param-watchlist": "在目前使用者的監視清單中無條件地加入或移除頁面,或使用設定,或不變更監視清單。", + "apihelp-move-param-watchlist": "使用偏好設定無條件地將頁面加入至或移除自目前使用者的監視清單,或不更改監視。", "apihelp-move-param-ignorewarnings": "忽略所有警告。", "apihelp-move-example-move": "將Badtitle移動至Goodtitle,不留下重定向。", "apihelp-opensearch-summary": "使用 OpenSearch 協定搜尋本 wiki。", @@ -258,6 +280,7 @@ "apihelp-opensearch-param-suggest": "若[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]設定為false,則不做任何事。", "apihelp-opensearch-param-redirects": "如何處理重定向:\n;return:傳回重定向本身。\n;resolve:傳回目標頁面,傳回的結果數目可能少於$1limit。\n由於歷史原因,$1format=json的預設值為「return」,其他格式則為「resolve」。", "apihelp-opensearch-param-format": "輸出的格式。", + "apihelp-opensearch-param-warningsaserror": "若警告以 format=json 提升時,回傳 API 錯誤而非忽略掉。", "apihelp-opensearch-example-te": "找出以 Te 為開頭的頁面。", "apihelp-options-summary": "更改目前使用者的偏好設定。", "apihelp-options-param-reset": "重設偏好設定為網站預設值。", @@ -269,6 +292,9 @@ "apihelp-options-example-complex": "重置所有偏好設定,然後再設定 skin 與 nickname。", "apihelp-paraminfo-summary": "獲得有關 API 模組的資訊。", "apihelp-paraminfo-param-helpformat": "說明字串的格式。", + "apihelp-paraminfo-param-querymodules": "查詢模組名稱清單(prop、meta、或 list 參數的值)。使用 $1modules=query+foo,而非 $1querymodules=foo。", + "apihelp-paraminfo-param-mainmodule": "如同取得有關主要(最高級別)模組的資訊。可改用 $1modules=main。", + "apihelp-paraminfo-param-pagesetmodule": "如同取得有關頁面設定模組(提供 titles= 與友人)的資訊。", "apihelp-paraminfo-param-formatmodules": "格式模組名稱清單(format 參數的值)。請改用 $1modules 。", "apihelp-paraminfo-example-1": "顯示 [[Special:ApiHelp/parse|action=parse]]、[[Special:ApiHelp/jsonfm|format=jsonfm]]、[[Special:ApiHelp/query+allpages|action=query&list=allpages]]、和 [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] 的資訊。", "apihelp-paraminfo-example-2": "顯示 [[Special:ApiHelp/query|action=query]] 所有子模組的資訊。", @@ -294,18 +320,28 @@ "apihelp-parse-paramvalue-prop-displaytitle": "添加已解析 wiki 文字的標題。", "apihelp-parse-paramvalue-prop-headitems": "提供放置頁面裡的 <head> 之項目。", "apihelp-parse-paramvalue-prop-headhtml": "取得頁面已解析的 <head>。", + "apihelp-parse-paramvalue-prop-jsconfigvars": "針對頁面提供指定的 JavaScript 設置變數。若要套用,請使用 mw.config.set()。", "apihelp-parse-paramvalue-prop-encodedjsconfigvars": "指定頁面的 JavaScript 設置變量為 JSON 字串。", + "apihelp-parse-paramvalue-prop-indicators": "提供使用在頁面的頁面狀態指示 HTML。", "apihelp-parse-paramvalue-prop-iwlinks": "在已解析的 wiki 文字提供跨 wiki 連結。", "apihelp-parse-paramvalue-prop-wikitext": "指定被解析的原始 wiki 文字。", "apihelp-parse-paramvalue-prop-properties": "指定多項定義在已解析原始 wiki 文字的屬性。", "apihelp-parse-paramvalue-prop-limitreportdata": "取得結構化限制報告。當有設定 $1disablelimitreport 時,則不會給予資料。", "apihelp-parse-paramvalue-prop-limitreporthtml": "取得限制報告的 HTML 版本。當有設定 $1disablelimitreport 時,則不會給予資料。", + "apihelp-parse-paramvalue-prop-parsetree": "修訂內容的 XML 解析樹狀(需要內容模組 $1)", "apihelp-parse-paramvalue-prop-parsewarnings": "提供發生在解析內容時的警告。", + "apihelp-parse-param-wrapoutputclass": "要包在解析器輸出內容的 CSS 類別。", + "apihelp-parse-param-effectivelanglinks": "包含由擴充提供的語言連結(與 $1prop=langlinks 一起使用)。", + "apihelp-parse-param-disablelimitreport": "從解析輸出內容裡省略限制報告(\"NewPP limit report\")。", "apihelp-parse-param-disablepp": "請改用$1disablelimitreport。", "apihelp-parse-param-disableeditsection": "從解析輸出內容省略編輯段落連結。", "apihelp-parse-param-disabletidy": "不要在解析輸出裡執行 HTML 內容清理(例如使用 tidy 軟體工具)", + "apihelp-parse-param-disablestylededuplication": "不要在解析結果去除重複的行內樣式表。", + "apihelp-parse-param-generatexml": "產生 XML 解析樹狀(需要被 $2prop=parsetree 給取代的 $1 內容模組)。", "apihelp-parse-param-preview": "在預覽模式下解析。", + "apihelp-parse-param-sectionpreview": "在段落預覽模式下解析(要同時啟用預覽模式)。", "apihelp-parse-param-disabletoc": "在輸出裡忽略目錄。", + "apihelp-parse-param-useskin": "套用所選的外觀至解析輸出。可能會影響以下參數:langlinks、headitems、modules、jsconfigvars、indicators。", "apihelp-parse-example-page": "解析頁面。", "apihelp-parse-example-text": "解析 wikitext。", "apihelp-parse-example-texttitle": "解析 wikitext,指定頁面標題。", @@ -314,7 +350,7 @@ "apihelp-patrol-param-rcid": "要巡查的最近變更 ID。", "apihelp-patrol-param-revid": "要巡查的修訂 ID。", "apihelp-patrol-param-tags": "在巡查日誌裡更改套用到項目的標籤。", - "apihelp-patrol-example-rcid": "巡查一次最近變更。", + "apihelp-patrol-example-rcid": "巡查一次近期變更。", "apihelp-patrol-example-revid": "巡查一個修訂。", "apihelp-protect-summary": "變更頁面的保護層級。", "apihelp-protect-param-title": "要(解除)保護頁面的標題。 不能與 $1pageid 一起使用。", @@ -325,7 +361,7 @@ "apihelp-protect-param-tags": "修改標籤以套用於保護日誌裡的項目。", "apihelp-protect-param-cascade": "啟用連鎖保護(也就是保護包含於此頁面的頁面)。如果所有提供的保護等級不支援連鎖,就將其忽略。", "apihelp-protect-param-watch": "如果被設定,就將被(解除)保護的頁面加至目前使用者的監視列表。", - "apihelp-protect-param-watchlist": "無條件地將該頁面加入至或移除自目前使用者的監視列表、使用偏好設定或不更改監視。", + "apihelp-protect-param-watchlist": "使用偏好設定無條件地將頁面加入至或移除自目前使用者的監視清單,或不更改監視。", "apihelp-protect-example-protect": "保護一個頁面。", "apihelp-protect-example-unprotect": "透過設定為 all(註:代表任何人都可以執行操作),來解除對頁面的保護。", "apihelp-protect-example-unprotect2": "透過設定為沒有限制,來解除對頁面的保護。", @@ -335,6 +371,7 @@ "apihelp-purge-example-simple": "清除 Main Page 與 API 頁面。", "apihelp-purge-example-generator": "重新整理主要命名空間的前10個頁面。", "apihelp-query-summary": "擷取來自及有關MediaWiki的數據。", + "apihelp-query-extended-description": "所有資料變動將會先使用查詢來取得權杖,以避免來自惡意網站的濫用行為。", "apihelp-query-param-prop": "替已查詢頁面所要取得的屬性。", "apihelp-query-param-list": "要取得的清單。", "apihelp-query-param-meta": "要取得的詮釋資料。", @@ -354,6 +391,8 @@ "apihelp-query+allcategories-param-prop": "要取得的屬性。", "apihelp-query+allcategories-paramvalue-prop-size": "在分類裡添加頁面數。", "apihelp-query+allcategories-paramvalue-prop-hidden": "標記由 __HIDDENCAT__ 隱藏的分類。", + "apihelp-query+allcategories-example-size": "列出分類以及各包含多少頁面的資訊。", + "apihelp-query+allcategories-example-generator": "替以 List 開頭的分類索取該分類頁面本身的資訊。", "apihelp-query+alldeletedrevisions-summary": "依使用者或所在命名空間來列出所有已刪除的修訂。", "apihelp-query+alldeletedrevisions-paraminfo-useronly": "僅與 $3user 一同使用。", "apihelp-query+alldeletedrevisions-paraminfo-nonuseronly": "不能與 $3user 一同使用。", @@ -366,6 +405,7 @@ "apihelp-query+alldeletedrevisions-param-user": "此列出由該使用者作出的修訂。", "apihelp-query+alldeletedrevisions-param-excludeuser": "不要列出由該使用者作出的修訂。", "apihelp-query+alldeletedrevisions-param-namespace": "僅列出此命名空間的頁面。", + "apihelp-query+alldeletedrevisions-param-miser-user-namespace": "注意:出於 [[mw:Special:MyLanguage/Manual:$wgMiserMode|miser 模式]]緣故,同時使用 $1user 與 $1namespace 可能會導致在繼續之前,傳回少於 $1limit 筆的結果,並可能不會傳回任何结果。", "apihelp-query+alldeletedrevisions-param-generatetitles": "當作為產生器時使用,產生標題而非修訂 ID。", "apihelp-query+alldeletedrevisions-example-user": "列出由使用者 Example 做出的最近 50 個貢獻。", "apihelp-query+alldeletedrevisions-example-ns-main": "列出在主命名空間的前 50 個已刪除修訂。", @@ -441,6 +481,7 @@ "apihelp-query+allpages-param-prlevel": "篩選基於保護級別的保護(必須與 $1prtype= 參數一起使用)。", "apihelp-query+allpages-param-limit": "要回傳的頁面總數。", "apihelp-query+allpages-param-dir": "列出時所採用的方向。", + "apihelp-query+allpages-param-filterlanglinks": "篩選基於頁面是否有語言連結。請注意這可能不會考慮由擴充所添加的語言連結。", "apihelp-query+allpages-example-B": "顯示以字母 B 為開頭的所有頁面清單。", "apihelp-query+allpages-example-generator": "顯示 4 個以 T 為開頭的頁面之資訊。", "apihelp-query+allpages-example-generator-revisions": "顯示前 2 個以 Re 為開頭的非重新導向頁面內容。", @@ -474,6 +515,7 @@ "apihelp-query+mystashedfiles-paramvalue-prop-size": "索取檔案大小與圖片尺寸。", "apihelp-query+mystashedfiles-paramvalue-prop-type": "索取檔案的 MIME 類型以及媒體類型。", "apihelp-query+mystashedfiles-param-limit": "要取得的檔案數量。", + "apihelp-query+alltransclusions-summary": "列出所有嵌入(頁面使用 {{x}} 來內嵌),包含不存在的。", "apihelp-query+alltransclusions-param-from": "要起始列舉的嵌入標題。", "apihelp-query+alltransclusions-param-to": "要終止列舉的嵌入標題。", "apihelp-query+alltransclusions-param-prefix": "搜尋以此值為開頭的所有嵌入標題。", @@ -496,8 +538,12 @@ "apihelp-query+allusers-param-excludegroup": "排除指定群組中的使用者", "apihelp-query+allusers-param-prop": "要包含的資訊部份:", "apihelp-query+allusers-paramvalue-prop-blockinfo": "添加有關使用者目前封鎖的資訊。", + "apihelp-query+allusers-paramvalue-prop-groups": "列出使用者所在的群組。這會使用到較多伺服器資源,並且可能會回傳少於限制條件的結果。", + "apihelp-query+allusers-paramvalue-prop-implicitgroups": "列出使用者自動列入的所有群組。", "apihelp-query+allusers-paramvalue-prop-rights": "列出使用者所擁有的權限。", "apihelp-query+allusers-paramvalue-prop-editcount": "添加使用者的編輯次數。", + "apihelp-query+allusers-paramvalue-prop-registration": "若可能的話,添加當使用者註冊時的時間戳記(可能為空白)。", + "apihelp-query+allusers-paramvalue-prop-centralids": "替使用者添加中心 ID 與附加狀態。", "apihelp-query+allusers-param-limit": "要回傳的使用者名稱總數。", "apihelp-query+allusers-param-witheditsonly": "僅列出有做過編輯的使用者。", "apihelp-query+allusers-param-activeusers": "僅列出在最近 $1 {{PLURAL:$1|天|天}}裡活躍的使用者。", @@ -510,6 +556,8 @@ "apihelp-query+backlinks-param-pageid": "要搜尋的頁面 ID。不能與 $1title 一起使用。", "apihelp-query+backlinks-param-namespace": "要列舉的命名空間。", "apihelp-query+backlinks-param-dir": "列出時所採用的方向。", + "apihelp-query+backlinks-param-filterredir": "如何篩選重新導向。當 $1redirect 啟用時若設定成 nonredirects,這僅會套用到第二級別。", + "apihelp-query+backlinks-param-redirect": "若連結頁面為重新導向,則找尋連結至該重新導向的所有頁面。最大限制為一半。", "apihelp-query+backlinks-example-simple": "顯示至 Main page 的連結。", "apihelp-query+backlinks-example-generator": "取得連結至 Main page 的相關頁面資訊。", "apihelp-query+blocks-summary": "列出所有被封鎖使用者與 IP 位址。", @@ -529,6 +577,7 @@ "apihelp-query+blocks-paramvalue-prop-expiry": "添加當封鎖到期的時間戳記。", "apihelp-query+blocks-paramvalue-prop-reason": "添加封鎖的原因。", "apihelp-query+blocks-paramvalue-prop-range": "添加受封鎖影響的 IP 地址範圍。", + "apihelp-query+blocks-param-show": "僅顯示符合這些標準的項目。\n例如僅想查看在 IP 地址的無限期封鎖,請設定 $1show=ip|!temp。", "apihelp-query+blocks-example-simple": "列出封鎖。", "apihelp-query+blocks-example-users": "列出使用者 Alice 與 Bob 的封鎖。", "apihelp-query+categories-summary": "列出頁面隸屬的所有分類。", @@ -550,7 +599,9 @@ "apihelp-query+categorymembers-paramvalue-prop-ids": "添加頁面 ID。", "apihelp-query+categorymembers-paramvalue-prop-title": "添加標題與頁面的命名空間 ID。", "apihelp-query+categorymembers-paramvalue-prop-sortkey": "添加使用來在分類裡排序的排序鍵(十六進位字串)。", + "apihelp-query+categorymembers-paramvalue-prop-type": "添加頁面已被分類的類型(page、subcat 或 file)。", "apihelp-query+categorymembers-paramvalue-prop-timestamp": "添加在頁面有被包含時的時間戳記。", + "apihelp-query+categorymembers-param-namespace": "僅包含在這些命名空間的頁面。請注意可能會使用 $1type=subcat 或 $1type=file,而非 $1namespace=14 或 6。", "apihelp-query+categorymembers-param-type": "包含的分類成員類型。當有設定 $1sort=timestamp 時忽略。", "apihelp-query+categorymembers-param-limit": "回傳的頁面數量上限。", "apihelp-query+categorymembers-param-sort": "作為排序順序的屬性。", @@ -561,6 +612,7 @@ "apihelp-query+categorymembers-param-endsortkey": "請改用 $1endhexsortkey。", "apihelp-query+categorymembers-example-simple": "取得在 Category:Physics 裡前 10 項的頁面。", "apihelp-query+categorymembers-example-generator": "取得在 Category:Physics 裡前 10 個頁面的頁面資訊。", + "apihelp-query+contributors-summary": "取得頁面上登入貢獻者以及匿名貢獻者數量的清單。", "apihelp-query+contributors-param-limit": "要回傳的貢獻人員數量。", "apihelp-query+contributors-example-simple": "顯示頁面 Main Page 的貢獻者。", "apihelp-query+deletedrevisions-summary": "取得已刪除修訂的資訊。", @@ -582,6 +634,7 @@ "apihelp-query+deletedrevs-param-excludeuser": "不要列出由該使用者作出的修訂。", "apihelp-query+deletedrevs-param-namespace": "僅列出此命名空間的頁面。", "apihelp-query+deletedrevs-param-limit": "修訂能列出的最大數量。", + "apihelp-query+deletedrevs-example-mode1": "以帶有內容(模式 1)列出頁面 Main Page 與 Talk:Main Page 的最新刪除修訂。", "apihelp-query+deletedrevs-example-mode2": "列出最近前 50 個已刪除掉由 Bob 所做出的貢獻(模式 2)。", "apihelp-query+deletedrevs-example-mode3-main": "列出在主命名空間的前 50 個已刪除修訂(模式 3)。", "apihelp-query+deletedrevs-example-mode3-talk": "列出在{{ns:talk}}命名空間的前 50 個已刪除頁面(模式 3)。", @@ -604,15 +657,19 @@ "apihelp-query+extlinks-summary": "回傳所有指定頁面的外部 URL (非 interwiki)。", "apihelp-query+extlinks-param-limit": "要回傳的連結數量。", "apihelp-query+extlinks-param-protocol": "URL 協定。若為空且有設定 $1query,會是 http 協定。將此與 $1query 一同留空會列出所有外部連結。", + "apihelp-query+extlinks-param-query": "不以協議來搜尋字串,對於檢查某頁面是否包含某個外部 URL 時很有用。", + "apihelp-query+extlinks-param-expandurl": "以規範協議的擴充協議關聯 URL。", "apihelp-query+extlinks-example-simple": "取得 Main Page 的外部連結清單。", "apihelp-query+exturlusage-summary": "列舉包含指定 URL 的頁面。", "apihelp-query+exturlusage-param-prop": "要包含的資訊部份:", "apihelp-query+exturlusage-paramvalue-prop-ids": "添加頁面 ID。", "apihelp-query+exturlusage-paramvalue-prop-title": "添加標題與頁面的命名空間 ID。", "apihelp-query+exturlusage-paramvalue-prop-url": "添加用於頁面的 URL。", + "apihelp-query+exturlusage-param-protocol": "URL 協定。若為空且有設定 $1query,會是 http 協定。將此與 $1query 一同留空會列出所有外部連結。", "apihelp-query+exturlusage-param-query": "不帶協定的搜尋字串。請查看 [[Special:LinkSearch]]。請留空以列出所有外部連結。", "apihelp-query+exturlusage-param-namespace": "要列舉的頁面命名空間。", "apihelp-query+exturlusage-param-limit": "要回傳的頁面數量。", + "apihelp-query+exturlusage-param-expandurl": "以規範協議的擴充協議關聯 URL。", "apihelp-query+exturlusage-example-simple": "顯示連結至 https://www.mediawiki.org 的頁面。", "apihelp-query+filearchive-summary": "依序列舉所有已刪除檔案。", "apihelp-query+filearchive-param-from": "起始列舉的圖片標題。", @@ -627,6 +684,7 @@ "apihelp-query+filearchive-paramvalue-prop-timestamp": "添加上傳版本的時間戳記。", "apihelp-query+filearchive-paramvalue-prop-user": "添加上傳該圖片版本的使用者。", "apihelp-query+filearchive-paramvalue-prop-size": "添加圖片大小(位元組)、高度、寬度、頁面計數(若可套用的話)。", + "apihelp-query+filearchive-paramvalue-prop-dimensions": "大小的別名。", "apihelp-query+filearchive-paramvalue-prop-description": "添加圖片版本的描述。", "apihelp-query+filearchive-paramvalue-prop-parseddescription": "解析版本的描述。", "apihelp-query+filearchive-paramvalue-prop-mime": "添加圖片的 MIME。", @@ -636,9 +694,12 @@ "apihelp-query+filearchive-paramvalue-prop-archivename": "添加非最新版本的存檔版本檔案名稱。", "apihelp-query+filearchive-example-simple": "顯示所有已刪除檔案的清單。", "apihelp-query+filerepoinfo-summary": "回傳有關在 wiki 上圖片儲存庫的詮釋資料。", + "apihelp-query+filerepoinfo-param-prop": "要取得的儲存庫屬性(可用屬性在其它 wiki 上可能會有差別)。", "apihelp-query+filerepoinfo-paramvalue-prop-apiurl": "儲存庫 API 的 URL - 對於從主機取得圖片資訊很有用。", "apihelp-query+filerepoinfo-paramvalue-prop-articlepath": "儲存庫 wiki 的 [[mw:Special:MyLanguage/Manual:$wgArticlePath|$wgArticlePath]] 或同等內容。", + "apihelp-query+filerepoinfo-paramvalue-prop-canUpload": "檔案是否可上傳至此儲存庫,例如透過 CORS 與共享驗證。", "apihelp-query+filerepoinfo-paramvalue-prop-displayname": "人類可讀的儲存庫 wiki 名稱。", + "apihelp-query+filerepoinfo-paramvalue-prop-favicon": "儲存庫 wiki 的網頁圖標 URL,來自於 [[mw:Special:MyLanguage/Manual:$wgFavicon|$wgFavicon]]。", "apihelp-query+filerepoinfo-paramvalue-prop-initialCapital": "檔案是否隱式地以大寫字母開頭。", "apihelp-query+filerepoinfo-paramvalue-prop-local": "儲存庫是否為本地端。", "apihelp-query+filerepoinfo-paramvalue-prop-rootUrl": "圖片路徑的根 URL 路徑。", @@ -667,6 +728,7 @@ "apihelp-query+imageinfo-paramvalue-prop-canonicaltitle": "添加檔案的規範標題。", "apihelp-query+imageinfo-paramvalue-prop-url": "提供檔案與描述頁面的 URL。", "apihelp-query+imageinfo-paramvalue-prop-size": "添加以位元組為單位的檔案大小、高度、寬度、頁面計數(若可套用的話)。", + "apihelp-query+imageinfo-paramvalue-prop-dimensions": "大小的別名。", "apihelp-query+imageinfo-paramvalue-prop-sha1": "替檔案添加 SHA-1 雜湊值。", "apihelp-query+imageinfo-paramvalue-prop-mime": "替檔案添加 MIME 類型。", "apihelp-query+imageinfo-paramvalue-prop-thumbmime": "添加圖片縮圖的 MIME 類型(需要 url 與參數 $1urlwidth)。", @@ -681,6 +743,7 @@ "apihelp-query+imageinfo-param-start": "列出的起始時間戳記。", "apihelp-query+imageinfo-param-end": "列出的終止時間戳記。", "apihelp-query+imageinfo-param-urlheight": "與 $1urlwidth 相似。", + "apihelp-query+imageinfo-param-metadataversion": "要使用的詮釋資料版本。若有指定 latest,會使用最新版本。預設為 1,以便向下兼容。", "apihelp-query+imageinfo-param-extmetadatamultilang": "若用於 extmetadata 屬性的翻譯可用,則全部索取。", "apihelp-query+imageinfo-param-extmetadatafilter": "若有指定且非空,僅會為 $1prop=extmetadata 回傳這些鍵。", "apihelp-query+imageinfo-param-badfilecontexttitle": "若有設定 $2prop=badfile,此頁面使用在當評估 [[MediaWiki:Bad image list]] 的時候", @@ -698,6 +761,7 @@ "apihelp-query+imageusage-param-pageid": "要搜尋的頁面 ID。不能與 $1title 一起使用。", "apihelp-query+imageusage-param-namespace": "要列舉的命名空間。", "apihelp-query+imageusage-param-dir": "列出時所採用的方向。", + "apihelp-query+imageusage-param-filterredir": "如何篩選重新導向。當 $1redirect 啟用時若設定成非重新導向,這僅會套用到第二級別。", "apihelp-query+imageusage-param-redirect": "若連結頁面為重新導向,則找尋連結至該重新導向的所有頁面。最大限制為一半。", "apihelp-query+imageusage-example-simple": "顯示有使用 [[:File:Albert Einstein Head.jpg]] 的頁面。", "apihelp-query+imageusage-example-generator": "取得關於有使用到 [[:File:Albert Einstein Head.jpg]] 的頁面資訊.", @@ -713,6 +777,8 @@ "apihelp-query+info-paramvalue-prop-url": "替各頁面給予一個完整 URL、一個編輯 URL,以及一個規範 URL。", "apihelp-query+info-paramvalue-prop-readable": "使用者是否可閱讀此頁面。請改用 intestactions=read。", "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-paramvalue-testactionsdetail-boolean": "回傳各操作的布林值。", "apihelp-query+info-paramvalue-testactionsdetail-full": "回傳描述出為何操作被禁止的訊息,或為允許則回傳空陣列。", @@ -829,10 +895,12 @@ "apihelp-query+protectedtitles-param-start": "在此保護時間戳記開始列出。", "apihelp-query+protectedtitles-param-end": "在此保護時間戳記停止列出。", "apihelp-query+protectedtitles-param-prop": "要取得的屬性。", + "apihelp-query+protectedtitles-paramvalue-prop-timestamp": "添加當保護被添加時的時間戳記。", "apihelp-query+protectedtitles-paramvalue-prop-user": "添加做出添加保護操作的使用者。", "apihelp-query+protectedtitles-paramvalue-prop-userid": "添加做出添加保護操作的使用者 ID。", "apihelp-query+protectedtitles-paramvalue-prop-comment": "添加保護的註釋。", "apihelp-query+protectedtitles-paramvalue-prop-parsedcomment": "添加保護的解析註釋。", + "apihelp-query+protectedtitles-paramvalue-prop-expiry": "添加當保護被提升時的時間戳記。", "apihelp-query+protectedtitles-paramvalue-prop-level": "添加保護層級。", "apihelp-query+protectedtitles-example-simple": "列出已保護的標題。", "apihelp-query+protectedtitles-example-generator": "找出在主命名空間裡連至已保護標題的連結。", @@ -847,7 +915,7 @@ "apihelp-query+random-param-filterredir": "如何過濾重新導向。", "apihelp-query+random-example-simple": "從主命名空間回傳兩個隨機頁面。", "apihelp-query+random-example-generator": "從主命名空間回傳兩個隨機頁面的相關頁面資訊。", - "apihelp-query+recentchanges-summary": "列舉出最近變更。", + "apihelp-query+recentchanges-summary": "列舉出近期變更。", "apihelp-query+recentchanges-param-start": "起始列舉的時間戳記。", "apihelp-query+recentchanges-param-end": "結束列舉的時間戳記。", "apihelp-query+recentchanges-param-namespace": "篩選僅為這些命名空間的更改。", @@ -862,7 +930,7 @@ "apihelp-query+recentchanges-paramvalue-prop-flags": "添加編輯的標籤。", "apihelp-query+recentchanges-paramvalue-prop-timestamp": "添加編輯的時間戳記。", "apihelp-query+recentchanges-paramvalue-prop-title": "添加編輯的頁面標題。", - "apihelp-query+recentchanges-paramvalue-prop-ids": "添加頁面 ID、最近更改 ID 以及新舊修訂 ID。", + "apihelp-query+recentchanges-paramvalue-prop-ids": "添加頁面 ID、近期變更 ID 以及新舊修訂 ID。", "apihelp-query+recentchanges-paramvalue-prop-sizes": "添加新舊頁面長度(位元組)。", "apihelp-query+recentchanges-paramvalue-prop-redirect": "若頁面為重新導向則標記編輯。", "apihelp-query+recentchanges-paramvalue-prop-patrolled": "標記可巡查編輯為已巡查或未巡查。", @@ -874,7 +942,7 @@ "apihelp-query+recentchanges-param-type": "要顯示的更改類型。", "apihelp-query+recentchanges-param-toponly": "僅列出最新修訂的更改。", "apihelp-query+recentchanges-param-title": "篩選與這些頁面關聯的項目。", - "apihelp-query+recentchanges-example-simple": "最近變更清單", + "apihelp-query+recentchanges-example-simple": "近期變更清單", "apihelp-query+recentchanges-example-generator": "取得有關近期尚未巡查更改的頁面資訊。", "apihelp-query+redirects-summary": "回傳連結至指定頁面的所有重新導向。", "apihelp-query+redirects-param-prop": "要取得的屬性。", @@ -883,6 +951,7 @@ "apihelp-query+redirects-paramvalue-prop-fragment": "各重新導向的片段,若有的話。", "apihelp-query+redirects-param-namespace": "僅包含這些命名空間的頁面。", "apihelp-query+redirects-param-limit": "要回傳的重新導向數量。", + "apihelp-query+redirects-param-show": "僅顯示符合這些標準的項目:\n;fragment:僅顯示帶部分內容的重新導向。\n;!fragment:僅顯示不帶部分內容的重新導向。", "apihelp-query+redirects-example-simple": "取得 [[Main Page]] 的重新導向清單", "apihelp-query+redirects-example-generator": "取得所有重新導向至 [[Main Page]] 的資訊。", "apihelp-query+revisions-summary": "取得修訂的資訊。", @@ -891,6 +960,7 @@ "apihelp-query+revisions-param-user": "僅包含由使用者做出的修訂。", "apihelp-query+revisions-param-excludeuser": "不包含由使用者做出的修訂。", "apihelp-query+revisions-param-tag": "僅列出以此標籤所標記的修訂。", + "apihelp-query+revisions-param-token": "各修訂所要獲得的權杖。", "apihelp-query+revisions-example-content": "取得用於標題 API 與 Main Page 最新修訂內容的資料。", "apihelp-query+revisions-example-last5": "取得 Main Page 的最近 5 筆修訂。", "apihelp-query+revisions-example-first5": "取得 Main Page 的最早前 5 筆修訂。", @@ -904,7 +974,13 @@ "apihelp-query+revisions+base-paramvalue-prop-user": "做出修訂的使用者。", "apihelp-query+revisions+base-paramvalue-prop-userid": "修訂創建者的使用者 ID", "apihelp-query+revisions+base-paramvalue-prop-size": "修訂的長度(位元組)。", + "apihelp-query+revisions+base-paramvalue-prop-slotsize": "各修訂間隔的長度(位元組)。", "apihelp-query+revisions+base-paramvalue-prop-sha1": "修訂的 SHA-1(base 16)。", + "apihelp-query+revisions+base-paramvalue-prop-slotsha1": "各修訂間隔的 SHA-1(base 16)。", + "apihelp-query+revisions+base-paramvalue-prop-contentmodel": "各修訂間隔的內容模組 ID。", + "apihelp-query+revisions+base-paramvalue-prop-comment": "由使用者對於修訂所做出的註釋。", + "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": "請改用 [[Special:ApiHelp/expandtemplates|action=expandtemplates]] 或 [[Special:ApiHelp/parse|action=parse]]。修訂內容的 XML 解析樹狀(需要內容模組 $1)。", "apihelp-query+revisions+base-param-limit": "限制所回傳的修訂數量。", @@ -943,14 +1019,22 @@ "apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "已註冊命名空間別名清單。", "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "特殊頁面別名清單。", "apihelp-query+siteinfo-paramvalue-prop-magicwords": "魔術字及其別名清單。", + "apihelp-query+siteinfo-paramvalue-prop-statistics": "回傳網站統計。", + "apihelp-query+siteinfo-paramvalue-prop-interwikimap": "回傳內部 wiki 對應(篩選可選用,也可透過 $1inlanguagecode 來選用本地化)。", + "apihelp-query+siteinfo-paramvalue-prop-dbrepllag": "回傳有最高複製延遲的資料庫伺服器。", "apihelp-query+siteinfo-paramvalue-prop-usergroups": "回傳使用者群組以及所分配權限。", "apihelp-query+siteinfo-paramvalue-prop-libraries": "回傳安裝在 wiki 上的函式庫。", "apihelp-query+siteinfo-paramvalue-prop-extensions": "回傳安裝在 wiki 上的擴充功能。", "apihelp-query+siteinfo-paramvalue-prop-fileextensions": "回傳允許上傳的副檔名(檔案類型)清單。", + "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "若可用時,回傳 wiki 版權(授權條款)資訊。", "apihelp-query+siteinfo-paramvalue-prop-restrictions": "回傳在可用限制(保護)類型的資訊。", + "apihelp-query+siteinfo-paramvalue-prop-languages": "回傳 MediaWiki 支援的語言清單(可透過 $1inlanguagecode 來選用本地化)。", + "apihelp-query+siteinfo-paramvalue-prop-skins": "回傳所有已啟用的外觀清單(可透過 $1inlanguagecode 來選用本地化,不然會是內容語言)。", "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "回傳解析擴充標籤清單。", "apihelp-query+siteinfo-paramvalue-prop-functionhooks": "回傳解析器函式掛勾清單。", + "apihelp-query+siteinfo-paramvalue-prop-showhooks": "回傳所有訂閱掛勾清單([[mw:Special:MyLanguage/Manual:$wgHooks|$wgHooks]] 的內容)。", "apihelp-query+siteinfo-paramvalue-prop-variables": "回傳變數 ID 清單。", + "apihelp-query+siteinfo-paramvalue-prop-protocols": "回傳在外部連結裡所允許的協議清單。", "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "回傳用於使用者偏好設定的預設值。", "apihelp-query+siteinfo-paramvalue-prop-uploaddialog": "回傳上傳對話框的設置。", "apihelp-query+siteinfo-param-filteriw": "僅回傳跨 wiki 地圖的本地端或非本地端項目。", @@ -969,18 +1053,22 @@ "apihelp-query+tags-paramvalue-prop-name": "添加標籤名稱。", "apihelp-query+tags-paramvalue-prop-displayname": "添加標籤的系統訊息。", "apihelp-query+tags-paramvalue-prop-description": "添加標籤的描述。", + "apihelp-query+tags-paramvalue-prop-hitcount": "添加含有此標籤之修訂與日誌項目的數量。", "apihelp-query+tags-paramvalue-prop-defined": "指示標籤是否已定義。", "apihelp-query+tags-paramvalue-prop-active": "標籤是否仍被套用。", "apihelp-query+tags-example-simple": "列出可用標籤。", "apihelp-query+templates-summary": "回傳指定頁面中所有引用的頁面。", "apihelp-query+templates-param-namespace": "僅顯示在這些命名空間的模板。", "apihelp-query+templates-param-limit": "要回傳的模板數量。", + "apihelp-query+templates-param-templates": "僅列出這些模板。在檢查某一頁面是否擁有某一模板時很有用。", "apihelp-query+templates-param-dir": "列出時所採用的方向。", "apihelp-query+templates-example-simple": "取得在頁面 Main Page 使用到的模坂。", "apihelp-query+templates-example-generator": "取得使用在 Main Page 的模版頁面相關資訊。", + "apihelp-query+templates-example-namespaces": "取得嵌入在頁面 Main Page 裡的 {{ns:user}} 與 {{ns:template}} 命名空間之頁面。", + "apihelp-query+tokens-summary": "取得資料修改操作的權杖。", "apihelp-query+tokens-param-type": "要求的權杖類型。", - "apihelp-query+tokens-example-simple": "接收 csrf 密鑰 (預設)。", - "apihelp-query+tokens-example-types": "接收監視密鑰以及巡邏密鑰。", + "apihelp-query+tokens-example-simple": "接收 csrf 權杖(預設)。", + "apihelp-query+tokens-example-types": "接收監視權杖以及巡邏權杖。", "apihelp-query+transcludedin-summary": "找出嵌入至指定頁面的所有頁面。", "apihelp-query+transcludedin-param-prop": "要取得的屬性。", "apihelp-query+transcludedin-paramvalue-prop-pageid": "各頁面的頁面 ID。", @@ -993,6 +1081,8 @@ "apihelp-query+transcludedin-example-generator": "取得有關嵌入 Main Page 的頁面之資訊。", "apihelp-query+usercontribs-summary": "按使用者來取得所有編輯。", "apihelp-query+usercontribs-param-limit": "回傳的貢獻數量上限。", + "apihelp-query+usercontribs-param-user": "要檢索貢獻的使用者。不能與 $1userids 或 $1userprefix 一起使用。", + "apihelp-query+usercontribs-param-userids": "要檢索貢獻的使用者 ID。不能與 $1user 或 $1userprefix 一起使用。", "apihelp-query+usercontribs-param-namespace": "僅列出這些命名空間的貢獻。", "apihelp-query+usercontribs-param-prop": "包含的額外資訊部份:", "apihelp-query+usercontribs-paramvalue-prop-ids": "添加頁面 ID 與修訂 ID。", @@ -1011,31 +1101,39 @@ "apihelp-query+usercontribs-example-ipprefix": "顯示所有來自於前綴為 192.0.2. 的 IP 地址貢獻。", "apihelp-query+userinfo-summary": "取得目前使用者的資訊。", "apihelp-query+userinfo-param-prop": "要包含的資訊部份:", + "apihelp-query+userinfo-paramvalue-prop-blockinfo": "若目前使用者被封鎖則標記出由誰做出,以及出於何種原因。", "apihelp-query+userinfo-paramvalue-prop-hasmsg": "若目前使用者有等待訊息,添加 messages 標籤。", "apihelp-query+userinfo-paramvalue-prop-groups": "列出目前使用者所隸屬的所有群組。", + "apihelp-query+userinfo-paramvalue-prop-implicitgroups": "列出目前使用者自動列入為成員的所有群組。", "apihelp-query+userinfo-paramvalue-prop-rights": "列出目前使用者所擁有的權限。", + "apihelp-query+userinfo-paramvalue-prop-changeablegroups": "列出目前使用者可以做出添加以及移除的群組。", "apihelp-query+userinfo-paramvalue-prop-options": "列出目前使用者已設定過的所有偏好設定。", + "apihelp-query+userinfo-paramvalue-prop-preferencestoken": "取得權杖來變更目前使用者的偏好設定。", "apihelp-query+userinfo-paramvalue-prop-editcount": "添加目前使用者的編輯數。", "apihelp-query+userinfo-paramvalue-prop-ratelimits": "列出所有套用到目前使用者的速率限制。", "apihelp-query+userinfo-paramvalue-prop-realname": "添加使用者的真實姓名。", "apihelp-query+userinfo-paramvalue-prop-email": "添加使用者的電子郵件地址與電子郵件驗證日期。", "apihelp-query+userinfo-paramvalue-prop-registrationdate": "添加使用者的註冊日期。", + "apihelp-query+userinfo-paramvalue-prop-centralids": "替使用者添加中心 ID 與附加狀態。", "apihelp-query+userinfo-example-simple": "取得目前使用者的資訊。", "apihelp-query+userinfo-example-data": "取得目前使用者的額外資訊。", "apihelp-query+users-summary": "取得有關使用者清單的資訊。", "apihelp-query+users-param-prop": "要包含的資訊部份:", "apihelp-query+users-paramvalue-prop-blockinfo": "若使用者被封鎖則標記出由誰做出,以及出於何種原因。", "apihelp-query+users-paramvalue-prop-groups": "列出各使用者所隸屬的所有群組。", + "apihelp-query+users-paramvalue-prop-implicitgroups": "列出使用者自動列入為成員的所有群組。", "apihelp-query+users-paramvalue-prop-rights": "列出各使用者所擁有的權限。", "apihelp-query+users-paramvalue-prop-editcount": "添加使用者的編輯數。", "apihelp-query+users-paramvalue-prop-registration": "添加使用者的註冊時間戳記。", "apihelp-query+users-paramvalue-prop-emailable": "若使用者符合條件並想要透過 [[Special:Emailuser]] 來接收電子郵件時標記。", "apihelp-query+users-paramvalue-prop-gender": "標記使用者性別。回傳「male」、「female」、或「unknown」。", + "apihelp-query+users-paramvalue-prop-centralids": "替使用者添加中心 ID 與附加狀態。", "apihelp-query+users-param-users": "要獲取的使用者清單。", "apihelp-query+users-param-userids": "要獲取的使用者 ID 清單。", "apihelp-query+users-param-token": "請改用 [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]。", "apihelp-query+users-example-simple": "返回使用者 Example 的資訊。", "apihelp-query+watchlist-summary": "取得在目前使用者的監視清單裡,頁面的近期變更。", + "apihelp-query+watchlist-param-allrev": "以指定時間範圍來包含同一頁面的多個修訂。", "apihelp-query+watchlist-param-start": "起始列舉的時間戳記。", "apihelp-query+watchlist-param-end": "結束列舉的時間戳記。", "apihelp-query+watchlist-param-namespace": "篩選僅為指定命名空間的更改。", @@ -1054,20 +1152,31 @@ "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": "一般頁面編輯。", "apihelp-query+watchlist-paramvalue-type-external": "外部更改。", "apihelp-query+watchlist-paramvalue-type-new": "頁面建立。", "apihelp-query+watchlist-paramvalue-type-log": "日誌項目。", "apihelp-query+watchlist-paramvalue-type-categorize": "分類成員更改。", + "apihelp-query+watchlist-param-owner": "與 $1token 一起使用以存取不同使用者的監視清單。", + "apihelp-query+watchlist-param-token": "允許存取其他使用者監視清單的安全權杖(可在使用者的[[Special:Preferences#mw-prefsection-watchlist|偏好設定]]找到)。", + "apihelp-query+watchlist-example-simple": "列出在目前使用者監視清單裡近期變更頁面的最新修訂。", + "apihelp-query+watchlist-example-props": "索取在目前使用者監視清單裡近期變更頁面的最新修訂額外資訊。", + "apihelp-query+watchlist-example-allrev": "索取在目前使用者監視清單裡所有近期變更頁面的資訊。", "apihelp-query+watchlist-example-generator": "索取在目前使用者監視清單裡近期變更頁面的頁面資訊。", + "apihelp-query+watchlist-example-generator-rev": "索取在目前使用者監視清單裡近期變更頁面的修訂資訊。", "apihelp-query+watchlistraw-summary": "列出在目前使用者的監視清單裡頭所有頁面。", "apihelp-query+watchlistraw-param-namespace": "僅列出在指定命名空間的頁面。", "apihelp-query+watchlistraw-param-limit": "每個請求要回傳的結果總數。", "apihelp-query+watchlistraw-param-prop": "要取得的額外屬性:", + "apihelp-query+watchlistraw-paramvalue-prop-changed": "添加使用者上一次被通知到有關編輯的時間戳記。", "apihelp-query+watchlistraw-param-show": "僅列出符合這些準則的項目。", + "apihelp-query+watchlistraw-param-owner": "與 $1token 一起使用以存取不同使用者的監視清單。", + "apihelp-query+watchlistraw-param-token": "允許存取其他使用者監視清單的安全權杖(可在使用者的[[Special:Preferences#mw-prefsection-watchlist|偏好設定]]找到)。", "apihelp-query+watchlistraw-param-dir": "列出時所採用的方向。", "apihelp-query+watchlistraw-param-fromtitle": "要開始列舉的標題(帶有命名空間前綴)。", "apihelp-query+watchlistraw-param-totitle": "要停止列舉的標題(帶有命名空間前綴)。", @@ -1083,6 +1192,8 @@ "apihelp-resetpassword-example-email": "對所有電子郵件地址為 user@example.com 的使用者發送重新設定密碼電郵。", "apihelp-revisiondelete-summary": "刪除和取消刪除修訂。", "apihelp-revisiondelete-param-type": "正執行的修訂刪除類型。", + "apihelp-revisiondelete-param-target": "要修訂刪除的頁面標題,若類型有所需要。", + "apihelp-revisiondelete-param-ids": "要刪除的修訂識別碼。", "apihelp-revisiondelete-param-hide": "各修訂所要隱藏的內容。", "apihelp-revisiondelete-param-show": "各修訂所要取消隱藏的內容。", "apihelp-revisiondelete-param-suppress": "是否對管理者及其他使用者禁止資料。", @@ -1091,31 +1202,38 @@ "apihelp-revisiondelete-example-revision": "隱藏在頁面 Main Page 的修訂 12345 內容。", "apihelp-revisiondelete-example-log": "以原因:BLP violation,來隱藏在日誌項目 67890 上的所有資料。", "apihelp-rollback-summary": "復原頁面的最後一次編輯。", + "apihelp-rollback-extended-description": "若編輯頁面的上一個使用者連續建立多個編輯,這些會全部被回退。", "apihelp-rollback-param-title": "要回退的頁面標題。不可與 $1pageid 同時使用。", "apihelp-rollback-param-pageid": "要回退的頁面 ID。不可與 $1title 同時使用。", "apihelp-rollback-param-tags": "套用到回退的標籤。", "apihelp-rollback-param-user": "編輯被回退的使用者名稱。", "apihelp-rollback-param-summary": "自定義編輯摘要。若為空,則使用預設摘要。", "apihelp-rollback-param-markbot": "將回退的編輯以及回退操作標記為機器人所做編輯。", - "apihelp-rollback-param-watchlist": "無條件使用設置將頁面加入或移除目前使用者的監視清單或者是不更改監視清單。", + "apihelp-rollback-param-watchlist": "使用偏好設定無條件地將頁面加入至或移除自目前使用者的監視清單,或不更改監視。", "apihelp-rollback-example-simple": "回退由使用者 Example 對頁面 Main Page 所做的最新編輯。", + "apihelp-rollback-example-summary": "帶編輯摘要 Reverting vandalism 來回退由 IP 使用者 192.0.2.5 對頁面 Main Page 所做的最新編輯,並標記這些編輯與回退為機器人做出的編輯。", "apihelp-rsd-summary": "匯出一個簡易探索(Really Simple Discovery、RSD)架構。", "apihelp-rsd-example-simple": "匯出 RSD 架構。", "apihelp-setnotificationtimestamp-summary": "更新監視頁面的通知時間戳記。", "apihelp-setnotificationtimestamp-param-entirewatchlist": "在所有已監視頁面運作。", + "apihelp-setnotificationtimestamp-param-timestamp": "要設定通知時間戳記的時間戳記。", + "apihelp-setnotificationtimestamp-param-torevid": "要設定通知時間戳記的修訂(僅限一個頁面)。", "apihelp-setnotificationtimestamp-example-all": "重新設定整個監視清單的通知狀態。", "apihelp-setnotificationtimestamp-example-page": "重新設定用於 Main page 的通知狀態。", + "apihelp-setnotificationtimestamp-example-pagetimestamp": "設定 Main page 的通知時間戳記,讓所有自 2012 年 1 月起的編輯為未查看。", "apihelp-setnotificationtimestamp-example-allpages": "重新設定在 {{ns:user}} 命名空間裡頁面的通知狀態。", "apihelp-setpagelanguage-summary": "更改頁面的語言。", "apihelp-setpagelanguage-extended-description-disabled": "您不被允許在此 wiki 上變更頁面的語言。\n\n請啟用 [[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]] 來進行此操作。", "apihelp-setpagelanguage-param-title": "您所想要更改語言的頁面之標題。不能與 $1pageid 一起使用。", "apihelp-setpagelanguage-param-pageid": "您所想要更改語言的頁面之頁面 ID。不能與 $1title 一起使用。", + "apihelp-setpagelanguage-param-lang": "要更改頁面的語言之代碼。使用 default 來重新設定頁面成 wiki 的預設內容語言。", "apihelp-setpagelanguage-param-reason": "變更的原因。", "apihelp-setpagelanguage-param-tags": "更改對應自此項操作所導致出日誌項目的標籤。", "apihelp-setpagelanguage-example-language": "更改 Main Page 的語言成巴斯克語。", "apihelp-setpagelanguage-example-default": "將 ID 是 123 頁面的語言更改為 wiki 的預設內容語言。", "apihelp-stashedit-summary": "在分享快取裡預備編輯。", "apihelp-stashedit-param-title": "正在編輯此頁面的標題。", + "apihelp-stashedit-param-section": "章節編號。0 代表最上層章節,new 代表新章節。", "apihelp-stashedit-param-sectiontitle": "新章節的標題。", "apihelp-stashedit-param-text": "頁面內容。", "apihelp-stashedit-param-stashedtexthash": "要替代使用的來自先前儲藏裡頁面內容雜湊。", @@ -1123,43 +1241,64 @@ "apihelp-stashedit-param-contentformat": "用於輸入文字的內容序列化格式。", "apihelp-stashedit-param-baserevid": "基本修訂的修訂 ID。", "apihelp-stashedit-param-summary": "更改摘要。", + "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-reason": "變更的原因。", - "apihelp-tokens-summary": "取得資料修改動作的密鑰。", + "apihelp-tag-param-tags": "套用到日誌項目的標籤會被建立為此操作的結果。", + "apihelp-tag-example-rev": "不指明原因將 ID 為 123 的修訂添加 vandalism 標籤", + "apihelp-tag-example-log": "將 ID 為 123 的日誌項目移除 spam 標籤,原因:Wrongly applied", + "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-unblock-summary": "解除封鎖一位使用者。", "apihelp-unblock-param-user": "要封鎖的使用者名稱、IP 位址或 IP 範圍。不能與 $1id 或 $1userid 一起使用", "apihelp-unblock-param-userid": "要封鎖的使用者 ID。不可與 $1id 或 $1user 一同使用。", "apihelp-unblock-param-reason": "解除封鎖的原因。", "apihelp-unblock-param-tags": "在封鎖日誌裡更改套用到項目的標籤。", "apihelp-unblock-example-id": "解除封鎖 ID #105。", + "apihelp-unblock-example-user": "封鎖使用者 Bob,原因:Sorry Bob。", "apihelp-undelete-summary": "恢復已刪除頁面的修訂。", "apihelp-undelete-param-title": "要恢復的頁面標題。", "apihelp-undelete-param-reason": "還原的原因。", "apihelp-undelete-param-tags": "在刪除日誌裡更改套用到項目的標籤。", + "apihelp-undelete-param-timestamps": "要復原的修訂時間戳記。若 $1timestamps 與 $1fileids 皆為空,則所有都會被復原。", + "apihelp-undelete-param-fileids": "要復原的檔案修訂 ID。若 $1timestamps 與 $1fileids 皆為空,則所有都會被復原。", + "apihelp-undelete-param-watchlist": "使用偏好設定無條件地將頁面加入至或移除自目前使用者的監視清單,或不更改監視。", "apihelp-undelete-example-page": "取消刪除頁面 Main Page。", "apihelp-undelete-example-revisions": "取消刪除 Main Page 的兩筆修訂。", "apihelp-unlinkaccount-summary": "移除目前使用者所連結到的第三方帳號。", "apihelp-upload-summary": "上傳檔案,或取得等待上傳的狀態。", "apihelp-upload-param-filename": "目標檔案名稱。", "apihelp-upload-param-comment": "上傳註釋。如果 $1text 未指定的話,也會作為新檔案用的初始頁面文字。", + "apihelp-upload-param-tags": "更改標籤來套用到上傳日誌項目以及檔案頁面修訂。", "apihelp-upload-param-text": "用於新檔案的初始頁面文字。", "apihelp-upload-param-watch": "監視頁面。", + "apihelp-upload-param-watchlist": "使用偏好設定無條件地將頁面加入至或移除自目前使用者的監視清單,或不更改監視。", "apihelp-upload-param-ignorewarnings": "忽略所有警告。", "apihelp-upload-param-file": "檔案內容。", "apihelp-upload-param-url": "索取檔案的來源 URL。", + "apihelp-upload-param-sessionkey": "如同 $1filekey,維持向下相容性。", "apihelp-upload-param-stash": "若設定的話,伺服器將會把檔案臨時暫存;而不是添加至儲存庫裡。", "apihelp-upload-param-filesize": "整體上傳的檔案大小。", "apihelp-upload-param-chunk": "大量內容。", "apihelp-upload-param-async": "在可能的情況下讓潛在的大型檔案非同步處理。", + "apihelp-upload-param-checkstatus": "僅檢索指定檔案鍵的上傳狀態。", "apihelp-upload-example-url": "從 URL 上傳。", + "apihelp-upload-example-filekey": "完成出於警告而失敗的上傳。", "apihelp-userrights-summary": "變更一位使用者的群組成員。", "apihelp-userrights-param-user": "使用者名稱。", "apihelp-userrights-param-userid": "使用者ID。", - "apihelp-userrights-param-add": "加入使用者至這些群組;若已是成員,則更新失效時間。", + "apihelp-userrights-param-add": "加入使用者至這些群組;若已是成員,則更新期限時間。", "apihelp-userrights-param-remove": "從這些群組移除使用者。", "apihelp-userrights-param-reason": "變更的原因。", + "apihelp-userrights-param-tags": "在使用者權限日誌裡更改套用到項目的標籤。", + "apihelp-userrights-example-user": "添加使用者 FooBot 至群組 bot,並從群組 sysop 與 bureaucrat 裡移除。", + "apihelp-userrights-example-userid": "添加 ID 為 123 的使用者至群組 bot,並從群組 sysop 與 bureaucrat 裡移除。", "apihelp-userrights-example-expiry": "添加使用者 SometimeSysop 至群組 sysop 為期一個月時間。", "apihelp-validatepassword-summary": "驗證密碼是否符合 wiki 的密碼方針。", "apihelp-validatepassword-param-password": "要驗證的密碼。", @@ -1169,6 +1308,8 @@ "apihelp-validatepassword-example-1": "驗證目前使用者的密碼 foobar。", "apihelp-validatepassword-example-2": "為建立的使用者 Example 驗證密碼 qwerty。", "apihelp-watch-summary": "從目前使用者的監視清單添加或移除頁面。", + "apihelp-watch-param-title": "要(取消)監視的頁面。請改用 $1titles。", + "apihelp-watch-param-unwatch": "若設定頁面,則會取消監視而非被監視。", "apihelp-watch-example-watch": "監視頁面 Main Page。", "apihelp-watch-example-unwatch": "取消監視頁面 Main Page。", "apihelp-watch-example-generator": "監視在主命名空間最前的幾個頁面。", @@ -1180,6 +1321,7 @@ "apihelp-phpfm-summary": "使用序列化 PHP 格式輸出資料 (使用 HTML 格式顯示)。", "apihelp-rawfm-summary": "使用 JSON 格式的除錯元素輸出資料 (使用 HTML 格式顯示)。", "apihelp-xml-summary": "使用 XML 格式輸出資料。", + "apihelp-xml-param-xslt": "若有指定,添加命名頁面成 XSL 樣式表。值必須是在 .xsl 結尾處 {{ns:MediaWiki}} 命名空間裡的標題。", "apihelp-xml-param-includexmlnamespace": "若有指定,添加一個 XML 命名空間。", "apihelp-xmlfm-summary": "使用 XML 格式輸出資料 (使用 HTML 格式顯示)。", "api-format-title": "MediaWiki API 結果", @@ -1187,11 +1329,14 @@ "api-format-prettyprint-header-only-html": "這是用來除錯的HTML呈現,不適合實際應用。\n\n參見[[mw:Special:MyLanguage/API|完整文件]]或[[Special:ApiHelp/main|API幫助]]以取得更多資訊。", "api-format-prettyprint-header-hyperlinked": "這是$1格式的HTML實現。HTML對除錯很有用,但不適合應用程式使用。\n\n指定format參數以更改輸出格式。要查看$1格式的非HTML實現,設置[$3 format=$2]。\n\n參見[[mw:API|完整文件]],或[[Special:ApiHelp/main|API幫助]]以獲取更多信息。", "api-format-prettyprint-status": "此回應將會傳回HTTP狀態$1 $2。", + "api-login-fail-aborted-nobotpw": "驗證需要使用者互動,該不被 action=login 所支援。要登入請查看 [[Special:ApiHelp/clientlogin|action=clientlogin]]。", "api-login-fail-badsessionprovider": "當使用$1無法登入。", "api-login-fail-sameorigin": "當未套用相同原有方針時無法登入。", "api-pageset-param-titles": "要使用的標題清單。", "api-pageset-param-pageids": "要使用的頁面 ID 清單。", "api-pageset-param-revids": "要使用的修訂 ID 清單。", + "api-pageset-param-redirects-generator": "自動解決在 $1titles、$1pageids、$1revids,以及由 $1generator 所回傳頁面裡的重新導向。", + "api-pageset-param-redirects-nogenerator": "自動解決在 $1titles、$1pageids,與 $1revids 的重新導向。", "api-help-title": "MediaWiki API 說明", "api-help-lead": "此頁為自動產生的 MediaWiki API 說明文件頁面。\n\n說明文件與範例:https://www.mediawiki.org/wiki/API", "api-help-main-header": "主要模組", @@ -1204,12 +1349,15 @@ "api-help-flag-generator": "此模組可作為產生器使用。", "api-help-source": "來源:$1", "api-help-source-unknown": "來源:未知", - "api-help-license": "協定:[[$1|$2]]", + "api-help-license": "授權條款:[[$1|$2]]", "api-help-license-noname": "協定:[[$1|查看連結]]", - "api-help-license-unknown": "協定:未知", + "api-help-license-unknown": "授權條款:未知", "api-help-parameters": "{{PLURAL:$1|參數}}:", "api-help-param-deprecated": "已停用。", "api-help-param-required": "此參數為必填。", + "api-help-param-templated": "此為[[Special:ApiHelp/main#main/templatedparams|模板參數]]。當做出請求時,$2。", + "api-help-param-templated-var-first": "在參數名稱裡的 {$1} 應替換成 $2 的值", + "api-help-param-templated-var": "{$1} 與 $2 的值", "api-help-datatypes-header": "資料類型", "api-help-datatypes": "至MediaWiki的輸入值應為NFC標準化的UTF-8。MediaWiki可以嘗試轉換其他輸入值,但這可能導致一些操作失敗(例如附帶MD5檢查的[[Special:ApiHelp/edit|編輯]])。\n\n一些在API請求中的參數類型需要更進一步解釋:\n;boolean\n:布林參數產生作用就像HTML複選框一樣:如果參數被指定,無論何值都被視為真(true)。如果要假值(false),則必須省略參數。\n;timestamp\n:時間戳記可被指定為多種格式。推荐使用ISO 8601日期和時間標準。所有時間為UTC時間,包含的任何時區都會被忽略。\n:* ISO 8601日期和時間,2001-01-15T14:56:00Z(標點和Z為選用)\n:* 帶小數秒(會被忽略)的ISO 8601日期和時間,2001-01-15T14:56:00.00001Z(破折號、冒號和Z為選用)\n:* MediaWiki格式,20010115145600\n:* 一般數字格式,2001-01-15 14:56:00(GMT、+##或-##的選用時區會被忽略)\n:* EXIF格式,2001:01:15 14:56:00\n:* RFC 2822格式(時區可省略),Mon, 15 Jan 2001 14:56:00\n:* RFC 850格式(時區可省略),Monday, 15-Jan-2001 14:56:00\n:* C ctime格式,Mon Jan 15 14:56:00 2001\n:* 從1970-01-01T00:00:00Z開始的秒數,作為1到13位數的整數(除了0)\n:* 字串now\n;替代多值分隔符號\n:使用多個值的參數通常會與垂直線符號(|)分隔的值一起提交,例如param=value1|value2或param=value1%7Cvalue2。如果值必須包含垂直線符號,使用U+001F(單位分隔符號)作為分隔符號,''並且''在值前加前綴U+001F,例如param=%1Fvalue1%1Fvalue2。", "api-help-templatedparams-header": "模板參數", @@ -1232,10 +1380,10 @@ "api-help-param-multi-all": "要指定所有值,請使用$1。", "api-help-param-default": "預設值:$1", "api-help-param-default-empty": "預設值:(空)", - "api-help-param-token": "自 [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] 接收的 \"$1\" 密鑰。", - "api-help-param-token-webui": "為顧及相容性,web UI中使用的代碼(Token)也是可接受的。", + "api-help-param-token": "自 [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] 接收的 \"$1\" 權杖。", + "api-help-param-token-webui": "為顧及相容性,web UI 中使用的權杖(Token)也是可接受的。", "api-help-param-disabled-in-miser-mode": "因[[mw:Special:MyLanguage/Manual:$wgMiserMode|miser模式]]而被停用。", - "api-help-param-limited-in-miser-mode": "注意:因[[mw:Special:MyLanguage/Manual:$wgMiserMode|miser模式]],使用這個可能導致繼續以前傳回少於$1limit筆結果;極端情況下可能不會傳回任何结果。", + "api-help-param-limited-in-miser-mode": "注意:出於 [[mw:Special:MyLanguage/Manual:$wgMiserMode|miser 模式]]緣故,使用這個可能會導致在繼續之前,傳回少於 $1limit 筆的結果,極端情況下則可能不會傳回任何结果。", "api-help-param-direction": "列舉的方向:\n;newer:最舊的優先。注意:$1start應在$1end之前。\n;older:最新的優先(預設)。注意:$1start應在$1end之後。", "api-help-param-continue": "當有更多結果可用時,使用這個繼續。", "api-help-param-no-description": "(無描述)", @@ -1244,8 +1392,9 @@ "api-help-examples": "{{PLURAL:$1|範例}}:", "api-help-permissions": "{{PLURAL:$1|權限}}:", "api-help-permissions-granted-to": "{{PLURAL:$1|已授權給}}: $2", + "api-help-right-apihighlimits": "在 API 查詢使用較高的限制(慢查詢:$1;快查詢:$2)。慢查詢的限制也適用於多值參數。", "api-help-open-in-apisandbox": "[在沙盒中開啟]", - "api-help-authmanager-general-usage": "使用此模組的一般程式是:\n# 通過amirequestsfor=$4取得來自[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]的可用欄位,和來自[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]的$5令牌。\n# 向用戶顯示欄位,並獲得其提交的內容。\n# 提交(POST)至此模組,提供$1returnurl及任何相關欄位。\n# 在回应中檢查status。\n#* 如果您收到了PASS(成功)或FAIL(失敗),則認為操作結束。成功與否如上句所示。\n#* 如果您收到了UI,向用戶顯示新欄位,並再次獲取其提交的內容。然後再次使用$1continue,向本模組提交相關欄位,並重復第四步。\n#* 如果您收到了REDIRECT,將使用者指向redirecttarget中的目標,等待其返回$1returnurl。然後再次使用$1continue,向本模組提交返回URL中提供的一切欄位,並重復第四步。\n#* 如果您收到了RESTART,這意味著身份驗證正常運作,但我們沒有連結的使用者賬戶。您可以將此看做UI或FAIL。", + "api-help-authmanager-general-usage": "使用此模組的一般程式是:\n# 通過 amirequestsfor=$4 取得來自 [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] 的可用欄位,和來自 [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] 的$5權杖。\n# 向用戶顯示欄位,並獲得其提交的內容。\n# 提交(POST)至此模組,提供 $1returnurl 及任何相關欄位。\n# 在回應中檢查 status。\n#* 如果您收到了 PASS(成功)或FAIL(失敗),則認為操作結束。成功與否如上句所示。\n#* 如果您收到了 UI,向用戶顯示新欄位,並再次獲取其提交的內容。然後再次使用 $1continue,向本模組提交相關欄位,並重復第四步。\n#* 如果您收到了 REDIRECT,將使用者指向redirecttarget 中的目標,等待其返回$1returnurl。然後再次使用 $1continue,向本模組提交返回 URL 中提供的一切欄位,並重復第四步。\n#* 如果您收到了 RESTART,這意味著身份驗證正常運作,但我們沒有連結的使用者帳戶。您可以將此視為 UI或FAIL。", "api-help-authmanagerhelper-requests": "只使用這些身份驗證請求,透過自[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]回傳的id與amirequestsfor=$1,或來自此模組之前的回應。", "api-help-authmanagerhelper-request": "使用此身份驗證請求,透過自[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]回傳的id與amirequestsfor=$1。", "api-help-authmanagerhelper-messageformat": "用於回傳訊息的格式。", @@ -1254,6 +1403,8 @@ "api-help-authmanagerhelper-returnurl": "為第三方身份驗證流程傳回URL,必須為絕對值。需要此值或$1continue兩者之一。\n\n在接收REDIRECT回應時,一般狀況下您將打開瀏覽器或網站瀏覽功能到特定的redirecttarget URL以進行第三方身份驗證流程。當它完成時,第三方會將瀏覽器或網站瀏覽功能送至此URL。您應當提取任何來自URL的查詢或POST參數,並將之作為$1continue請求傳遞至此API模組。", "api-help-authmanagerhelper-continue": "此請求是在先前的UI或REDIRECT回應之後的後續動作。必須為此值或$1returnurl。", "api-help-authmanagerhelper-additional-params": "此模組允許額外參數,取決於可用的身份驗證請求。使用[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]与amirequestsfor=$1(或之前來自此模組的回應,如果合適)以決定可用請求及其使用的欄位。", + "apierror-allimages-redirect": "當使用 allimages 作為產生器時,請改用 gaifilterredir=nonredirects 而不是 redirects。", + "apierror-allpages-generator-redirects": "當使用 allpages 作為產生器時,請改用 gapfilterredir=nonredirects 而不是 redirects。", "apierror-appendnotsupported": "無法附加到使用內容模組 $1 的頁面。", "apierror-articleexists": "您所嘗試建立的條目剛剛已被創建。", "apierror-assertbotfailed": "斷言使用者擁有的 bot 權限失效。", @@ -1261,8 +1412,12 @@ "apierror-assertuserfailed": "斷言使用者已登入失敗。", "apierror-autoblocked": "您的 IP 位址已經被自動封鎖,因為它曾經被一名已封鎖的使用者使用過。", "apierror-bad-badfilecontexttitle": "在 $1badfilecontexttitle 參數的無效標題。", + "apierror-badconfig-resulttoosmall": "在此 wiki 上 $wgAPIMaxResultSize 的值太小,而無法含有基本結果資訊。", + "apierror-badcontinue": "無效的繼續參數。您應該傳遞由前一個查詢所回傳的原有值。", + "apierror-baddiff": "無法取得差異。不存在一個或兩個修訂,或是您沒有權限來檢視它們。", "apierror-baddiffto": "$1diffto 必須設定成非負值的數字、prev、next、或 cur。", "apierror-badformat-generic": "內容模組 $2 不支援使用請求格式 $1。", + "apierror-badformat": "由 $3 所使用的內容模組 $2 不支援使用請求格式 $1。", "apierror-badgenerator-notgenerator": "模組 $1 不能作為產生器。", "apierror-badgenerator-unknown": "未知的 generator=$1。", "apierror-badip": "IP 參數無效。", @@ -1271,10 +1426,18 @@ "apierror-badmodule-nosubmodules": "模組 $1 沒有子模組。", "apierror-badparameter": "參數 $1 的值無效。", "apierror-badquery": "無效的查詢。", + "apierror-badtimestamp": "用於時間戳記參數 $1 的值「$2」無效。", + "apierror-badtoken": "無效 CSRF 權杖。", + "apierror-badupload": "檔案上傳參數 $1 不是一個檔案上傳,請確定在您的 POST 有使用 multipart/form-data,並且在 Content-Disposition 標頭有包含檔案名稱。", + "apierror-badurl": "用於 URL 參數 $1 的值「$2」無效。", + "apierror-baduser": "用於使用者參數 $1 的值「$2」無效。", + "apierror-badvalue-notmultivalue": "U+001F 多值區分僅可用於多值參數。", + "apierror-bad-watchlist-token": "提供無效的監視清單權杖。請在 [[Special:Preferences]] 設定正確權杖。", "apierror-blockedfrommail": "您已被封鎖,無法發送電子郵件。", "apierror-blocked": "您已被封鎖,無法編輯。", "apierror-blocked-partial": "您已被禁止編輯此頁面。", "apierror-botsnotsupported": "此介面不支援機器人。", + "apierror-cannotreauthenticate": "出於您的身份無法驗證,此操作不可用。", "apierror-cannotviewtitle": "您不被允許檢視$1。", "apierror-cantblock-email": "您沒有權限來封鎖使用者透過 wiki 來發送電子郵件。", "apierror-cantblock": "您沒有權限來解封使用者。", @@ -1282,27 +1445,52 @@ "apierror-canthide": "您沒有權限來從封鎖日誌隱藏使用者名稱。", "apierror-cantimport-upload": "您沒有權限來匯入已上傳頁面。", "apierror-cantimport": "您沒有權限來匯入頁面。", + "apierror-cantoverwrite-sharedfile": "目標檔案存在於分享儲存庫上,因此您沒有權限來覆蓋掉。", + "apierror-cantsend": "您尚未登入,您沒有已確認的電子郵件地址,或是您未被允許發送電子郵件給其他人,因此您不能發送電子郵件。", + "apierror-cantundelete": "無法取消刪除:請求的修訂可能不存在,或是可能已被取消刪除。", "apierror-changeauth-norequest": "建立更改請求失敗。", + "apierror-cidrtoobroad": "不能接受超出 /$2 的 $1 CIDR 範圍。", + "apierror-compare-maintextrequired": "當 $1slots 包含 main 時,需要參數 $1text-main(不能刪除主要部份)。", + "apierror-compare-no-title": "無法在不帶標題之下預先儲存轉換。請嘗試指定 fromtitle 或 totitle。", + "apierror-compare-nosuchfromsection": "在 'from' 內容裡沒有段落$1。", + "apierror-compare-nosuchtosection": "在 'to' 內容裡沒有段落$1。", + "apierror-compare-nofromrevision": "沒有「from」修訂。請指定 fromrev、fromtitle、或 fromid。", "apierror-compare-notext": "參數 $1 不能在缺少 $2 的情況下使用。", + "apierror-compare-notorevision": "沒有「to」修訂。請指定 torev、totitle、或 toid。", + "apierror-compare-relative-to-deleted": "相關已刪除修訂時不能使用 torelative=$1。", "apierror-contentserializationexception": "內容序列化失敗:$1", + "apierror-contenttoobig": "您所提供的內容超出條目的 $1 {{PLURAL:$1|位元組|位元組}}限制。", "apierror-copyuploadbaddomain": "不允許從此網域來透過 URL 上傳。", "apierror-copyuploadbadurl": "不允許從此 URL 來上傳。", + "apierror-create-titleexists": "現有標題不能以 create 來保護。", "apierror-csp-report": "處理 CSP 報告時錯誤:$1。", + "apierror-deletedrevs-param-not-1-2": "$1 參數不可用於模式 1 或 2。", + "apierror-deletedrevs-param-not-3": "$1 參數不可用於模式 3。", "apierror-emptynewsection": "新段落不可建立空白內容。", "apierror-emptypage": "不允許建立空白的新頁面。", "apierror-exceptioncaught": "[$1]捕獲異常:$2", "apierror-exceptioncaughttype": "[$1]捕獲異常類型:$2", "apierror-filedoesnotexist": "檔案不存在。", + "apierror-fileexists-sharedrepo-perm": "目標檔案存在於分享儲存庫上。請使用參數 ignorewarnings 來覆蓋掉。", "apierror-filenopath": "無法取得本地端檔案路徑。", "apierror-filetypecannotberotated": "無法旋轉的檔案類型。", + "apierror-formatphp": "此回應不能使用 format=php 來表示。請參閱 https://phabricator.wikimedia.org/T68776。", "apierror-imageusage-badtitle": "$1的標題必須是檔案。", "apierror-import-unknownerror": "未知的匯入錯誤:$1", + "apierror-integeroutofrange-abovebotmax": "對於機器人或系統管理員而言,$1 不能超過 $2(設定為 $3)。", + "apierror-integeroutofrange-abovemax": "對於使用者而言,$1 不能超過 $2(設定為 $3)。", + "apierror-integeroutofrange-belowminimum": "$1 不能小於 $2(設定為 $3)。", "apierror-invalidcategory": "您所輸入的分類名稱無效。", + "apierror-invalidexpiry": "無效的期限時間「$1」。", + "apierror-invalid-file-key": "不是有效的檔案鍵。", "apierror-invalidlang": "用於參數 $1 的語言代碼無效。", + "apierror-invalidmethod": "無效的 HTTP 方式。請考慮採用 GET 或 POST。", "apierror-invalidoldimage": "oldimage 參數含有無效格式。", "apierror-invalidparammix-cannotusewith": "參數 $1 不能與 $2 一起使用。", "apierror-invalidparammix-mustusewith": "$1 參數僅能與 $2 一起使用。", + "apierror-invalidparammix-parse-new-section": "section=new 不能連同 oldid、pageid、或 page 參數。請使用 title 與 text。", "apierror-invalidparammix": "{{PLURAL:$2|參數}} $1 不能一起使用。", + "apierror-invalidsection": "section 參數必須是有效的段落 ID 或 new。", "apierror-invalidsha1base36hash": "所提供的 SHA1Base36 雜湊無效。", "apierror-invalidsha1hash": "所提供的 SHA1 雜湊無效。", "apierror-invalidtitle": "錯誤標題「$1」。", @@ -1316,13 +1504,16 @@ "apierror-mimesearchdisabled": "MIME 搜尋在 Miser 模式裡被停用。", "apierror-missingcontent-pageid": "遺失頁面 ID 為 $1 的內容。", "apierror-missingcontent-revid": "遺失修訂 ID 為 $1 的內容。", - "apierror-missingparam-one-of": "{{PLURAL:$2|參數|參數其一}} $1 為必要。", + "apierror-missingparam-at-least-one-of": "參數$1{{PLURAL:$2||其一}}為必要。", + "apierror-missingparam-one-of": "參數$1{{PLURAL:$2||其一}}為必要。", "apierror-missingparam": "$1參數必須被設定。", "apierror-missingrev-pageid": "沒有頁面 ID 為 $1 的目前修訂。", "apierror-missingrev-title": "沒有標題為$1的目前修訂。", + "apierror-missingtitle-createonly": "遺失標題不能以 create 來保護。", "apierror-missingtitle": "您所指定的頁面不存在。", "apierror-missingtitle-byname": "頁面$1不存在。", "apierror-moduledisabled": "模組 $1 已停用。", + "apierror-multival-only-one-of": "參數 $1 僅允許$2{{PLURAL:$3||其一}}。", "apierror-multpages": "$1 僅能以單一頁面使用。", "apierror-mustbeloggedin-changeauth": "必須登入,才能變更身分核對資取。", "apierror-mustbeloggedin-generic": "您必須登入。", @@ -1331,8 +1522,11 @@ "apierror-mustbeloggedin-uploadstash": "上傳儲藏僅對已登入使用者可用。", "apierror-mustbeloggedin": "您必須登入才能$1。", "apierror-mustbeposted": "$1 模組需要 POST 請求。", + "apierror-mustpostparams": "在查詢字串裡找出以下{{PLURAL:$2|參數|參數}},而這應必須在 POST 正文裡:$1。", + "apierror-noapiwrite": "透過 API 來編輯此 wiki 已被停用。請確認 $wgEnableWriteAPI=true; 聲明有包含在 wiki 的 LocalSettings.php 檔案裡。", "apierror-nochanges": "沒有請求的更改。", "apierror-nodeleteablefile": "沒有這樣檔案的舊版本。", + "apierror-no-direct-editing": "由$2所使用的內容模組$1不支援透過 API 來直接編輯。", "apierror-noedit-anon": "匿名使用者不可編輯頁面。", "apierror-noedit": "您沒有權限來編輯頁面。", "apierror-noimageredirect-anon": "匿名使用者不能建立圖片重新導向。", @@ -1344,14 +1538,18 @@ "apierror-nosuchsection": "沒有 ID 為 $1 的段落。", "apierror-nosuchsection-what": "在$2裡沒有段落$1。", "apierror-nosuchuserid": "沒有 ID 為 $1 的使用者。", + "apierror-notarget": "您沒有替此操作指定有效目標。", "apierror-notpatrollable": "因內容過舊,修訂 r$1 無法巡查。", "apierror-nouploadmodule": "未設定上傳模組。", + "apierror-offline": "出於網路連接問題而無法進行。請確認您的網際網路連結有正常運作,並再試一次。", "apierror-opensearch-json-warnings": "警告不能以 OpenSearch JSON 格式表示。", "apierror-pagecannotexist": "命名空間不允許實際頁面。", + "apierror-pagedeleted": "自從您取得時間戳記後,該頁面已被刪除。", "apierror-pagelang-disabled": "此 wiki 不允許更改頁面的語言。", "apierror-paramempty": "參數 $1 不能為空。", "apierror-parsetree-notwikitext": "prop=parsetree 僅支援用於 wiki 文字內容。", "apierror-parsetree-notwikitext-title": "prop=parsetree 僅支援用於 wiki 文字內容。$1使用內容模組 $2。", + "apierror-pastexpiry": "期限時間「$1」已過。", "apierror-permissiondenied": "您沒有權限$1。", "apierror-permissiondenied-generic": "權限不足。", "apierror-permissiondenied-patrolflag": "您需要 patrol 或 patrolmarks 權限來請求巡查標記。", @@ -1363,28 +1561,35 @@ "apierror-readapidenied": "您需要有閱讀權限來使用此模組。", "apierror-readonly": "Wiki 目前為唯讀模式。", "apierror-reauthenticate": "於本工作階段還未核對身分,請重新核對。", + "apierror-redirect-appendonly": "您嘗試使用重新導向跟隨模式來編輯,這必須與 section=new、prependtext,或 appendtext 來結合使用。", "apierror-revdel-mutuallyexclusive": "同一欄位不可同時用在 hide 與 show。", "apierror-revdel-needtarget": "此 RevDel 類型需要目標標題。", "apierror-revdel-paramneeded": "至少需要 hide 與/或 show 其中的值。", "apierror-revisions-badid": "查無參數 $1 的修訂。", + "apierror-revisions-norevids": "revids 參數不能與清單選項($1limit、$1startid、$1endid、$1dir=newer、$1user、$1excludeuser、$1start、和 $1end)一起使用。", "apierror-revwrongpage": "r$1 不是$2的修訂。", "apierror-searchdisabled": "$1搜尋已停用。", "apierror-sectionreplacefailed": "無法合併更新的章節。", "apierror-sectionsnotsupported": "內容模組 $1 不支援段落。", "apierror-sectionsnotsupported-what": "$1 不支援段落。", "apierror-show": "不正確的參數 - 不可提供互斥值。", + "apierror-siteinfo-includealldenied": "除非 $wgShowHostnames 設為真,否則無法檢視所有伺服器資訊。", "apierror-sizediffdisabled": "大小差異功能在 Miser 模式裡已停用。", "apierror-spamdetected": "您的編輯被拒絕,因為有包含部份垃圾訊息內容:$1。", "apierror-specialpage-cantexecute": "您沒有權限來查看此特殊頁面的結果。", "apierror-stashedfilenotfound": "在儲藏裡找不到檔案:$1。", "apierror-stashedit-missingtext": "給予的雜湊裡查無儲藏文字。", + "apierror-stashfailed-complete": "大量上傳已完成,請檢查狀態來獲取詳情。", "apierror-stashfilestorage": "在儲藏裡不能儲存上傳:$1。", "apierror-stashinvalidfile": "無效的儲藏檔案。", "apierror-stashnosuchfilekey": "沒有這樣的檔案鍵:$1。", + "apierror-stashpathinvalid": "不正確格式或是其它無效的檔案鍵:$1。", "apierror-stashwrongowner": "錯誤擁有者:$1", "apierror-stashzerolength": "檔案長度為零,且無法儲存於儲藏:$1。", "apierror-systemblocked": "您已被 MediaWiki 給自動封鎖。", + "apierror-templateexpansion-notwikitext": "模板擴展僅支援用於 wiki 文字內容。$1使用內容模組 $2。", "apierror-timeout": "伺服器未有在預計的時間內回應。", + "apierror-toofewexpiries": "提供了 $1 個逾期{{PLURAL:$1|時間戳記|時間戳記}},所需要的{{PLURAL:$2|是|是}} $2 個。", "apierror-toomanyvalues": "替參數 $1 提供太多的值。限制為 $2。", "apierror-unknownaction": "指定的操作 $1 無法識別出。", "apierror-unknownerror-editpage": "不明編輯頁面錯誤:$1。", @@ -1402,9 +1607,16 @@ "apierror-writeapidenied": "您不被允許透過 API 來編輯此 wiki。", "apiwarn-alldeletedrevisions-performance": "為了在產生標題時能有更好效能,請設定 $1dir=newer。", "apiwarn-badurlparam": "無法解析$2的 $1urlparam。這僅能用在寬度與高度。", + "apiwarn-checktoken-percentencoding": "在 URL 裡進行適當百分比編碼的權杖中,檢查像是「+」的符號。", "apiwarn-compare-nocontentmodel": "沒有可確定的內容模組,假定為 $1。", + "apiwarn-deprecation-deletedrevs": "list=deletedrevs 已棄用。請改用 prop=deletedrevisions 或 list=alldeletedrevisions。", "apiwarn-deprecation-httpsexpected": "當應為 HTTPS 時,HTTP 要被使用。", + "apiwarn-deprecation-login-botpw": "透過 action=login 的主帳號登入已棄用,並可能會在無警告的情況下停止運作。要繼續以 action=clientlogin 登入,請參閱 [[Special:BotPasswords]];若要繼續安全使用主帳號登入,則請參閱 action=clientlogin。", + "apiwarn-deprecation-login-nobotpw": "透過 action=login 的主帳號登入已棄用,並可能會在無警告的情況下停止運作。若要安全登入,請參閱 action=clientlogin。", + "apiwarn-deprecation-login-token": "透過 action=login 來取得權杖已棄用。請改用 action=query&meta=tokens&type=login。", + "apiwarn-deprecation-missingparam": "因為未指定 $1,輸出內容使用到過去舊有的格式。該格式已棄用,並且往後都只會使用新格式。", "apiwarn-deprecation-parameter": "參數 $1 已棄用。", + "apiwarn-deprecation-parse-headitems": "prop=headitems 自 MediaWiki 的 1.28 版本後已被棄用。當建立新 HTML 文件時請使用 prop=headhtml,或是當更新文件客戶端時請使用 prop=modules|jsconfigvars。", "apiwarn-deprecation-purge-get": "透過 GET 方式使用的 action=purge 已棄用,請以 POST 替代。", "apiwarn-deprecation-withreplacement": "$1 已棄用,請改用 $2。", "apiwarn-difftohidden": "無法對 r$1 比較差異:內容被隱蔵。", @@ -1415,17 +1627,27 @@ "apiwarn-invalidxmlstylesheetext": "樣式表應要有 .xsl 副檔名。", "apiwarn-invalidxmlstylesheet": "指定了無效或不存在的樣式表。", "apiwarn-invalidxmlstylesheetns": "樣式表應在 {{ns:MediaWiki}} 命名空間。", + "apiwarn-moduleswithoutvars": "屬性 modules 已被設定,但不是 jsconfigvars 或 encodedjsconfigvars。需要設置變數來讓模組使用合宜。", "apiwarn-notfile": "「$1」不是一個檔案。", "apiwarn-nothumb-noimagehandler": "無法建立縮圖,因為$1沒有相關的圖片處理器。", "apiwarn-parse-nocontentmodel": "未提供 title 或 contentmodel,應是 $1。", + "apiwarn-parse-revidwithouttext": "revid 在不帶有 text 的情況下使用,且請求了已解析頁面屬性。請問您是指要使用 oldid 而不是 revid 嗎?", + "apiwarn-parse-titlewithouttext": "title 在不帶有 text 的情況下使用,且請求了已解析頁面屬性。請問您是指要使用 page 而不是 title 嗎?", + "apiwarn-redirectsandrevids": "重新導向處理不能與參數 revids 一同使用。任何 revids 所指向的重新導向都尚未被解決。", "apiwarn-tokennotallowed": "「$1」操作不允許目前的使用者。", + "apiwarn-tokens-origin": "當未套用相同來源方針,權杖可能無法取得。", + "apiwarn-truncatedresult": "結果會被截短,否則將會大於 $1 位元組限制。", "apiwarn-unrecognizedvalues": "參數 $1 有無法識別的{{PLURAL:$3|值|值}}:$2。", "apiwarn-unsupportedarray": "參數 $1 使用了不被支援的 PHP 陣列語法。", + "apiwarn-validationfailed-badchars": "在鍵裡的字元無效(僅允許 a-z、A-Z、0-9、_、和 - are allowed)。", "apiwarn-validationfailed-badpref": "不是有效的偏好設定。", "apiwarn-validationfailed-cannotset": "不能透過此模組設定。", + "apiwarn-validationfailed-keytoolong": "鍵太長(不允許超過 $1 位元組)。", "apiwarn-validationfailed": "$1驗證錯誤:$2", "apiwarn-wgDebugAPI": "安全警告:$wgDebugAPI 已啟用。", "api-feed-error-title": "錯誤($1)", + "api-usage-docref": "查看 $1 來了解 API 的使用。", + "api-exception-trace": "$1位在$2的第$3行\n$4", "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/includes/auth/AuthManager.php b/includes/auth/AuthManager.php index 32592ac770..0c9f615e5f 100644 --- a/includes/auth/AuthManager.php +++ b/includes/auth/AuthManager.php @@ -336,7 +336,7 @@ class AuthManager implements LoggerAwareInterface { $this->setSessionDataForUser( $user ); $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] ); $session->remove( 'AuthManager::authnState' ); - \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] ); + \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] ); return $ret; } @@ -352,7 +352,7 @@ class AuthManager implements LoggerAwareInterface { $this->callMethodOnProviders( 7, 'postAuthentication', [ User::newFromName( $guessUserName ) ?: null, $ret ] ); - \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] ); + \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName, [] ] ); return $ret; } } @@ -468,7 +468,7 @@ class AuthManager implements LoggerAwareInterface { [ User::newFromName( $guessUserName ) ?: null, $res ] ); $session->remove( 'AuthManager::authnState' ); - \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] ); + \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName, [] ] ); return $res; case AuthenticationResponse::ABSTAIN; // Continue loop @@ -534,7 +534,7 @@ class AuthManager implements LoggerAwareInterface { [ User::newFromName( $guessUserName ) ?: null, $res ] ); $session->remove( 'AuthManager::authnState' ); - \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] ); + \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName, [] ] ); return $res; case AuthenticationResponse::REDIRECT; case AuthenticationResponse::UI; @@ -625,7 +625,7 @@ class AuthManager implements LoggerAwareInterface { ); $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] ); $session->remove( 'AuthManager::authnState' ); - \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] ); + \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] ); return $ret; } } @@ -658,7 +658,7 @@ class AuthManager implements LoggerAwareInterface { $this->logger->debug( "Login failed in secondary authentication by $id" ); $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] ); $session->remove( 'AuthManager::authnState' ); - \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] ); + \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName(), [] ] ); return $res; case AuthenticationResponse::REDIRECT; case AuthenticationResponse::UI; @@ -694,7 +694,7 @@ class AuthManager implements LoggerAwareInterface { $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] ); $session->remove( 'AuthManager::authnState' ); $this->removeAuthenticationSessionData( null ); - \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] ); + \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] ); return $ret; } catch ( \Exception $ex ) { $session->remove( 'AuthManager::authnState' ); diff --git a/includes/block/BlockRestriction.php b/includes/block/BlockRestriction.php index 43d70e6b1c..5bf286d013 100644 --- a/includes/block/BlockRestriction.php +++ b/includes/block/BlockRestriction.php @@ -34,7 +34,6 @@ class BlockRestriction { * * @param int|array $blockId * @param IDatabase|null $db - * @param array $options Options to pass to the select query. * @return Restriction[] */ public static function loadByBlockId( $blockId, IDatabase $db = null ) { diff --git a/includes/block/Restriction/Restriction.php b/includes/block/Restriction/Restriction.php index f1cc1b0a22..5fefecc39f 100644 --- a/includes/block/Restriction/Restriction.php +++ b/includes/block/Restriction/Restriction.php @@ -63,6 +63,7 @@ interface Restriction { /** * Creates a new Restriction from a database row. * + * @param \stdClass $row * @return self */ public static function newFromRow( \stdClass $row ); diff --git a/includes/cache/CacheHelper.php b/includes/cache/CacheHelper.php index 6c6b184735..93685e35af 100644 --- a/includes/cache/CacheHelper.php +++ b/includes/cache/CacheHelper.php @@ -270,8 +270,8 @@ class CacheHelper implements ICacheHelper { $value = null; if ( is_null( $key ) ) { - $itemKey = array_keys( array_slice( $this->cachedChunks, 0, 1 ) ); - $itemKey = array_shift( $itemKey ); + reset( $this->cachedChunks ); + $itemKey = key( $this->cachedChunks ); if ( !is_int( $itemKey ) ) { wfWarn( "Attempted to get item with non-numeric key while " . diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php index 9e182c796c..b9944a82f0 100644 --- a/includes/cache/LinkCache.php +++ b/includes/cache/LinkCache.php @@ -197,6 +197,7 @@ class LinkCache { * @return int Page ID or zero */ public function addLink( $title ) { + wfDeprecated( __METHOD__, '1.27' ); $nt = Title::newFromDBkey( $title ); if ( !$nt ) { return 0; diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index 5ada42fb77..b669fcd492 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -270,6 +270,17 @@ class MessageCache { $this->overridable = array_flip( Language::getMessageKeysFor( $code ) ); + // T208897 array_flip can fail and return null + if ( is_null( $this->overridable ) ) { + LoggerFactory::getInstance( 'MessageCache' )->error( + __METHOD__ . ': $this->overridable is null', + [ + 'message_keys' => Language::getMessageKeysFor( $code ), + 'code' => $code + ] + ); + } + # 8 lines of code just to say (once) that message cache is disabled if ( $this->mDisable ) { static $shownDisabled = false; diff --git a/includes/cache/localisation/LCStoreStaticArray.php b/includes/cache/localisation/LCStoreStaticArray.php index c5a2512f50..75c8465abf 100644 --- a/includes/cache/localisation/LCStoreStaticArray.php +++ b/includes/cache/localisation/LCStoreStaticArray.php @@ -68,21 +68,21 @@ class LCStoreStaticArray implements LCStore { * Encodes a value into an array format * * @param mixed $value - * @return array + * @return array|mixed * @throws RuntimeException */ public static function encode( $value ) { - if ( is_scalar( $value ) || $value === null ) { - // [V]alue - return [ 'v', $value ]; + if ( is_array( $value ) ) { + // [a]rray + return [ 'a', array_map( 'LCStoreStaticArray::encode', $value ) ]; } if ( is_object( $value ) ) { - // [S]erialized + // [s]erialized return [ 's', serialize( $value ) ]; } - if ( is_array( $value ) ) { - // [A]rray - return [ 'a', array_map( 'LCStoreStaticArray::encode', $value ) ]; + if ( is_scalar( $value ) || $value === null ) { + // Scalar value, written directly without array + return $value; } throw new RuntimeException( 'Cannot encode ' . var_export( $value, true ) ); @@ -91,21 +91,28 @@ class LCStoreStaticArray implements LCStore { /** * Decode something that was encoded with encode * - * @param array $encoded + * @param mixed $encoded * @return array|mixed * @throws RuntimeException */ - public static function decode( array $encoded ) { + public static function decode( $encoded ) { + if ( !is_array( $encoded ) ) { + // Scalar values are written directly without array + return $encoded; + } + $type = $encoded[0]; $data = $encoded[1]; switch ( $type ) { - case 'v': - return $data; - case 's': - return unserialize( $data ); case 'a': return array_map( 'LCStoreStaticArray::decode', $data ); + case 's': + return unserialize( $data ); + case 'v': + // Support: MediaWiki 1.32 and earlier + // Backward compatibility with older file format + return $data; default: throw new RuntimeException( 'Unable to decode ' . var_export( $encoded, true ) ); diff --git a/includes/changes/ChangesList.php b/includes/changes/ChangesList.php index fea31b46d6..a39568ba91 100644 --- a/includes/changes/ChangesList.php +++ b/includes/changes/ChangesList.php @@ -351,12 +351,13 @@ class ChangesList extends ContextSource { } else { $formattedSizeClass = 'mw-plusminus-neg'; } + $formattedSizeClass .= ' mw-diff-bytes'; $formattedTotalSize = $context->msg( 'rc-change-size-new' )->numParams( $new )->text(); return Html::element( $tag, [ 'dir' => 'ltr', 'class' => $formattedSizeClass, 'title' => $formattedTotalSize ], - $context->msg( 'parentheses', $formattedSize )->plain() ) . $lang->getDirMark(); + $formattedSize ) . $lang->getDirMark(); } /** @@ -453,11 +454,9 @@ class ChangesList extends ContextSource { ); } if ( $rc->mAttribs['rc_type'] == RC_CATEGORIZE ) { - $diffhist = $diffLink . $this->message['pipe-separator'] . $this->message['hist']; + $histLink = $this->message['hist']; } else { - $diffhist = $diffLink . $this->message['pipe-separator']; - # History link - $diffhist .= $this->linkRenderer->makeKnownLink( + $histLink = $this->linkRenderer->makeKnownLink( $rc->getTitle(), new HtmlArmor( $this->message['hist'] ), [ 'class' => 'mw-changeslist-history' ], @@ -468,9 +467,11 @@ class ChangesList extends ContextSource { ); } - // @todo FIXME: Hard coded ". .". Is there a message for this? Should there be? - $s .= $this->msg( 'parentheses' )->rawParams( $diffhist )->escaped() . - ' . . '; + $s .= Html::rawElement( 'div', [ 'class' => 'mw-changeslist-links' ], + Html::rawElement( 'span', [], $diffLink ) . + Html::rawElement( 'span', [], $histLink ) + ) . + ' '; } /** @@ -534,7 +535,7 @@ class ChangesList extends ContextSource { htmlspecialchars( $this->getLanguage()->userTime( $rc->mAttribs['rc_timestamp'], $this->getUser() - ) ) . ' . . '; + ) ) . ' '; } /** diff --git a/includes/changes/OldChangesList.php b/includes/changes/OldChangesList.php index a26f5b64af..c15701ba66 100644 --- a/includes/changes/OldChangesList.php +++ b/includes/changes/OldChangesList.php @@ -121,7 +121,7 @@ class OldChangesList extends ChangesList { if ( $this->getConfig()->get( 'RCShowChangedSize' ) ) { $cd = $this->formatCharacterDifference( $rc ); if ( $cd !== '' ) { - $html .= $cd . ' . . '; + $html .= $cd . ' '; } } diff --git a/includes/changetags/ChangeTags.php b/includes/changetags/ChangeTags.php index b28983f9ef..32cfd13f58 100644 --- a/includes/changetags/ChangeTags.php +++ b/includes/changetags/ChangeTags.php @@ -262,8 +262,6 @@ class ChangeTags { &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null, User $user = null ) { - global $wgChangeTagsSchemaMigrationStage; - $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags... $tagsToRemove = array_filter( (array)$tagsToRemove ); @@ -334,12 +332,19 @@ class ChangeTags { ); } - // update the tag_summary row - $prevTags = []; - if ( !self::updateTagSummaryRow( $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, - $log_id, $prevTags ) - ) { - // nothing to do + $prevTags = self::getPrevTags( $rc_id, $log_id, $rev_id ); + + // add tags + $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) ); + $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) ); + + // remove tags + $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) ); + $newTags = array_values( array_diff( $newTags, $tagsToRemove ) ); + + sort( $prevTags ); + sort( $newTags ); + if ( $prevTags == $newTags ) { return [ [], [], $prevTags ]; } @@ -347,35 +352,28 @@ class ChangeTags { $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); if ( count( $tagsToAdd ) ) { $changeTagMapping = []; - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) { - foreach ( $tagsToAdd as $tag ) { - $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag ); - } - // T207881: update the counts at the end of the transaction - $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tagsToAdd ) { - $dbw->update( - 'change_tag_def', - [ 'ctd_count = ctd_count + 1' ], - [ 'ctd_name' => $tagsToAdd ], - __METHOD__ - ); - } ); + foreach ( $tagsToAdd as $tag ) { + $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag ); } + $fname = __METHOD__; + // T207881: update the counts at the end of the transaction + $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tagsToAdd, $fname ) { + $dbw->update( + 'change_tag_def', + [ 'ctd_count = ctd_count + 1' ], + [ 'ctd_name' => $tagsToAdd ], + $fname + ); + } ); $tagsRows = []; foreach ( $tagsToAdd as $tag ) { - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $tagName = null; - } else { - $tagName = $tag; - } // Filter so we don't insert NULLs as zero accidentally. // Keep in mind that $rc_id === null means "I don't care/know about the // rc_id, just delete $tag on this revision/log entry". It doesn't // mean "only delete tags on this revision/log WHERE rc_id IS NULL". $tagsRows[] = array_filter( [ - 'ct_tag' => $tagName, 'ct_rc_id' => $rc_id, 'ct_log_id' => $log_id, 'ct_rev_id' => $rev_id, @@ -391,38 +389,31 @@ class ChangeTags { // delete from change_tag if ( count( $tagsToRemove ) ) { + $fname = __METHOD__; foreach ( $tagsToRemove as $tag ) { - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $tagName = null; - $tagId = $changeTagDefStore->getId( $tag ); - } else { - $tagName = $tag; - $tagId = null; - } $conds = array_filter( [ - 'ct_tag' => $tagName, 'ct_rc_id' => $rc_id, 'ct_log_id' => $log_id, 'ct_rev_id' => $rev_id, - 'ct_tag_id' => $tagId, + 'ct_tag_id' => $changeTagDefStore->getId( $tag ), ] ); $dbw->delete( 'change_tag', $conds, __METHOD__ ); - if ( $dbw->affectedRows() && $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) { + if ( $dbw->affectedRows() ) { // T207881: update the counts at the end of the transaction - $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tag ) { + $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tag, $fname ) { $dbw->update( 'change_tag_def', [ 'ctd_count = ctd_count - 1' ], [ 'ctd_name' => $tag ], - __METHOD__ + $fname ); $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ], - __METHOD__ + $fname ); } ); } @@ -437,75 +428,24 @@ class ChangeTags { return [ $tagsToAdd, $tagsToRemove, $prevTags ]; } - /** - * Adds or removes a given set of tags to/from the relevant row of the - * tag_summary table. Modifies the tagsToAdd and tagsToRemove arrays to - * reflect the tags that were actually added and/or removed. - * - * @param array &$tagsToAdd - * @param array &$tagsToRemove If a tag is present in both $tagsToAdd and - * $tagsToRemove, it will be removed - * @param int|null $rc_id Null if not known or not applicable - * @param int|null $rev_id Null if not known or not applicable - * @param int|null $log_id Null if not known or not applicable - * @param array &$prevTags Optionally outputs a list of the tags that were - * in the tag_summary row to begin with - * @return bool True if any modifications were made, otherwise false - * @since 1.25 - */ - protected static function updateTagSummaryRow( &$tagsToAdd, &$tagsToRemove, - $rc_id, $rev_id, $log_id, &$prevTags = [] - ) { - $dbw = wfGetDB( DB_MASTER ); - - $tsConds = array_filter( [ - 'ts_rc_id' => $rc_id, - 'ts_rev_id' => $rev_id, - 'ts_log_id' => $log_id - ] ); - - // Can't both add and remove a tag at the same time... - $tagsToAdd = array_diff( $tagsToAdd, $tagsToRemove ); - - // Update the summary row. - // $prevTags can be out of date on replica DBs, especially when addTags is called consecutively, - // causing loss of tags added recently in tag_summary table. - $prevTags = $dbw->selectField( 'tag_summary', 'ts_tags', $tsConds, __METHOD__ ); - $prevTags = $prevTags ?: ''; - $prevTags = array_filter( explode( ',', $prevTags ) ); - - // add tags - $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) ); - $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) ); - - // remove tags - $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) ); - $newTags = array_values( array_diff( $newTags, $tagsToRemove ) ); - - sort( $prevTags ); - sort( $newTags ); - if ( $prevTags == $newTags ) { - return false; - } + private static function getPrevTags( $rc_id = null, $rev_id = null, $log_id = null ) { + $conds = array_filter( + [ + 'ct_rc_id' => $rc_id, + 'ct_log_id' => $log_id, + 'ct_rev_id' => $rev_id, + ] + ); - if ( !$newTags ) { - // No tags left, so delete the row altogether - $dbw->delete( 'tag_summary', $tsConds, __METHOD__ ); - } else { - // 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 = wfGetDB( DB_MASTER ); + $tagIds = $dbw->selectFieldValues( 'change_tag', 'ct_tag_id', $conds, __METHOD__ ); - $dbw->replace( 'tag_summary', $uniqueKeys, $row, __METHOD__ ); + $tags = []; + foreach ( $tagIds as $tagId ) { + $tags[] = MediaWikiServices::getInstance()->getChangeTagDefStore()->getName( (int)$tagId ); } - return true; + return $tags; } /** @@ -788,7 +728,7 @@ class ChangeTags { public static function modifyDisplayQuery( &$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag = '' ) { - global $wgUseTagFilter, $wgChangeTagsSchemaMigrationStage; + global $wgUseTagFilter; // Normalize to arrays $tables = (array)$tables; @@ -796,6 +736,8 @@ class ChangeTags { $conds = (array)$conds; $options = (array)$options; + $fields['ts_tags'] = self::makeTagSummarySubquery( $tables ); + // Figure out which ID field to use if ( in_array( 'recentchanges', $tables ) ) { $join_cond = 'ct_rc_id=rc_id'; @@ -809,44 +751,26 @@ class ChangeTags { throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' ); } - $tagTables[] = 'change_tag'; - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $tagTables[] = 'change_tag_def'; - $join_cond_ts_tags = [ 'change_tag_def' => [ 'INNER JOIN', 'ct_tag_id=ctd_id' ] ]; - $field = 'ctd_name'; - } else { - $field = 'ct_tag'; - $join_cond_ts_tags = []; - } - - $fields['ts_tags'] = wfGetDB( DB_REPLICA )->buildGroupConcatField( - ',', $tagTables, $field, $join_cond, $join_cond_ts_tags - ); - if ( $wgUseTagFilter && $filter_tag ) { // Somebody wants to filter on a tag. // Add an INNER JOIN on change_tag $tables[] = 'change_tag'; $join_conds['change_tag'] = [ 'INNER JOIN', $join_cond ]; - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $filterTagIds = []; - $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); - foreach ( (array)$filter_tag as $filterTagName ) { - try { - $filterTagIds[] = $changeTagDefStore->getId( $filterTagName ); - } catch ( NameTableAccessException $exception ) { - // Return nothing. - $conds[] = '0'; - break; - }; - } + $filterTagIds = []; + $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); + foreach ( (array)$filter_tag as $filterTagName ) { + try { + $filterTagIds[] = $changeTagDefStore->getId( $filterTagName ); + } catch ( NameTableAccessException $exception ) { + // Return nothing. + $conds[] = '0'; + break; + }; + } - if ( $filterTagIds !== [] ) { - $conds['ct_tag_id'] = $filterTagIds; - } - } else { - $conds['ct_tag'] = $filter_tag; + if ( $filterTagIds !== [] ) { + $conds['ct_tag_id'] = $filterTagIds; } if ( @@ -858,6 +782,40 @@ class ChangeTags { } } + /** + * Make the tag summary subquery based on the given tables and return it. + * + * @param string|array $tables Table names, see Database::select + * + * @return string tag summary subqeury + * @throws MWException When unable to determine appropriate JOIN condition for tagging + */ + public static function makeTagSummarySubquery( $tables ) { + // Normalize to arrays + $tables = (array)$tables; + + // Figure out which ID field to use + if ( in_array( 'recentchanges', $tables ) ) { + $join_cond = 'ct_rc_id=rc_id'; + } elseif ( in_array( 'logging', $tables ) ) { + $join_cond = 'ct_log_id=log_id'; + } elseif ( in_array( 'revision', $tables ) ) { + $join_cond = 'ct_rev_id=rev_id'; + } elseif ( in_array( 'archive', $tables ) ) { + $join_cond = 'ct_rev_id=ar_rev_id'; + } else { + throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' ); + } + + $tagTables = [ 'change_tag', 'change_tag_def' ]; + $join_cond_ts_tags = [ 'change_tag_def' => [ 'INNER JOIN', 'ct_tag_id=ctd_id' ] ]; + $field = 'ctd_name'; + + return wfGetDB( DB_REPLICA )->buildGroupConcatField( + ',', $tagTables, $field, $join_cond, $join_cond_ts_tags + ); + } + /** * Build a text box to select a change tag * @@ -909,8 +867,7 @@ class ChangeTags { } /** - * Defines a tag in the valid_tag table and/or update ctd_user_defined field in change_tag_def, - * without checking that the tag name is valid. + * Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid. * Extensions should NOT use this function; they can use the ListDefinedTags * hook instead. * @@ -918,38 +875,26 @@ class ChangeTags { * @since 1.25 */ public static function defineTag( $tag ) { - global $wgChangeTagsSchemaMigrationStage; - $dbw = wfGetDB( DB_MASTER ); - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) { - $tagDef = [ - 'ctd_name' => $tag, - 'ctd_user_defined' => 1, - 'ctd_count' => 0 - ]; - $dbw->upsert( - 'change_tag_def', - $tagDef, - [ 'ctd_name' ], - [ 'ctd_user_defined' => 1 ], - __METHOD__ - ); - } + $tagDef = [ + 'ctd_name' => $tag, + 'ctd_user_defined' => 1, + 'ctd_count' => 0 + ]; + $dbw->upsert( + 'change_tag_def', + $tagDef, + [ 'ctd_name' ], + [ 'ctd_user_defined' => 1 ], + __METHOD__ + ); - if ( $wgChangeTagsSchemaMigrationStage < MIGRATION_NEW ) { - $dbw->replace( - 'valid_tag', - [ 'vt_tag' ], - [ 'vt_tag' => $tag ], - __METHOD__ - ); - } // clear the memcache of defined tags self::purgeTagCacheAll(); } /** - * Removes a tag from the valid_tag table and/or update ctd_user_defined field in change_tag_def. + * Update ctd_user_defined = 0 field in change_tag_def. * The tag may remain in use by extensions, and may still show up as 'defined' * if an extension is setting it from the ListDefinedTags hook. * @@ -957,28 +902,20 @@ class ChangeTags { * @since 1.25 */ public static function undefineTag( $tag ) { - global $wgChangeTagsSchemaMigrationStage; - $dbw = wfGetDB( DB_MASTER ); - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) { - $dbw->update( - 'change_tag_def', - [ 'ctd_user_defined' => 0 ], - [ 'ctd_name' => $tag ], - __METHOD__ - ); - - $dbw->delete( - 'change_tag_def', - [ 'ctd_name' => $tag, 'ctd_count' => 0 ], - __METHOD__ - ); - } + $dbw->update( + 'change_tag_def', + [ 'ctd_user_defined' => 0 ], + [ 'ctd_name' => $tag ], + __METHOD__ + ); - if ( $wgChangeTagsSchemaMigrationStage < MIGRATION_NEW ) { - $dbw->delete( 'valid_tag', [ 'vt_tag' => $tag ], __METHOD__ ); - } + $dbw->delete( + 'change_tag_def', + [ 'ctd_name' => $tag, 'ctd_count' => 0 ], + __METHOD__ + ); // clear the memcache of defined tags self::purgeTagCacheAll(); @@ -1171,7 +1108,7 @@ class ChangeTags { return Status::newFatal( 'tags-create-no-name' ); } - // tags cannot contain commas (used as a delimiter in tag_summary table), + // tags cannot contain commas (used to be used as a delimiter in tag_summary table), // pipe (used as a delimiter between multiple tags in // SpecialRecentchanges and friends), or slashes (would break tag description messages in // MediaWiki namespace) @@ -1228,8 +1165,7 @@ class ChangeTags { } /** - * Creates a tag by adding a row to the `valid_tag` table. - * and/or add it to `change_tag_def` table. + * Creates a tag by adding it to `change_tag_def` table. * * Extensions should NOT use this function; they can use the ListDefinedTags * hook instead. @@ -1280,45 +1216,16 @@ class ChangeTags { * @since 1.25 */ public static function deleteTagEverywhere( $tag ) { - global $wgChangeTagsSchemaMigrationStage; $dbw = wfGetDB( DB_MASTER ); $dbw->startAtomic( __METHOD__ ); - // delete from valid_tag and/or set ctd_user_defined = 0 + // set ctd_user_defined = 0 self::undefineTag( $tag ); - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag ); - $conditions = [ 'ct_tag_id' => $tagId ]; - } else { - $conditions = [ 'ct_tag' => $tag ]; - } - - // find out which revisions use this tag, so we can delete from tag_summary - $result = $dbw->select( 'change_tag', - [ 'ct_rc_id', 'ct_log_id', 'ct_rev_id' ], - $conditions, - __METHOD__ ); - foreach ( $result as $row ) { - // remove the tag from the relevant row of tag_summary - $tagsToAdd = []; - $tagsToRemove = [ $tag ]; - self::updateTagSummaryRow( $tagsToAdd, $tagsToRemove, $row->ct_rc_id, - $row->ct_rev_id, $row->ct_log_id ); - } - // delete from change_tag - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag ); - $dbw->delete( 'change_tag', [ 'ct_tag_id' => $tagId ], __METHOD__ ); - } else { - $dbw->delete( 'change_tag', [ 'ct_tag' => $tag ], __METHOD__ ); - } - - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) { - $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ ); - } - + $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag ); + $dbw->delete( 'change_tag', [ 'ct_tag_id' => $tagId ], __METHOD__ ); + $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ ); $dbw->endAtomic( __METHOD__ ); // give extensions a chance @@ -1467,9 +1374,7 @@ class ChangeTags { } /** - * Lists tags explicitly defined in the `valid_tag` table of the database. - * Tags in table 'change_tag' which are not in table 'valid_tag' are not - * included. In case of new backend loads the data from `change_tag_def` table. + * Lists tags explicitly defined in the `change_tag_def` table of the database. * * Tries memcached first. * @@ -1484,16 +1389,16 @@ class ChangeTags { $cache->makeKey( 'valid-tags-db' ), WANObjectCache::TTL_MINUTE * 5, function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) { - global $wgChangeTagsSchemaMigrationStage; $dbr = wfGetDB( DB_REPLICA ); $setOpts += Database::getCacheSetOptions( $dbr ); - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { - $tags = self::listExplicitlyDefinedTagsNewBackend(); - } else { - $tags = $dbr->selectFieldValues( 'valid_tag', 'vt_tag', [], $fname ); - } + $tags = $dbr->selectFieldValues( + 'change_tag_def', + 'ctd_name', + [ 'ctd_user_defined' => 1 ], + $fname + ); return array_filter( array_unique( $tags ) ); }, @@ -1505,22 +1410,6 @@ class ChangeTags { ); } - /** - * Lists tags explicitly user defined tags. When ctd_user_defined is true. - * - * @return string[] Array of strings: tags - * @since 1.25 - */ - private static function listExplicitlyDefinedTagsNewBackend() { - $dbr = wfGetDB( DB_REPLICA ); - return $dbr->selectFieldValues( - 'change_tag_def', - 'ctd_name', - [ 'ctd_user_defined' => 1 ], - __METHOD__ - ); - } - /** * Lists tags defined by core or extensions using the ListDefinedTags hook. * Extensions need only define those tags they deem to be in active use. @@ -1566,6 +1455,8 @@ class ChangeTags { $cache->touchCheckKey( $cache->makeKey( 'valid-tags-db' ) ); $cache->touchCheckKey( $cache->makeKey( 'valid-tags-hook' ) ); + MediaWikiServices::getInstance()->getChangeTagDefStore()->reloadMap(); + self::purgeTagUsageCache(); } @@ -1583,59 +1474,9 @@ class ChangeTags { * Returns a map of any tags used on the wiki to number of edits * tagged with them, ordered descending by the hitcount. * This does not include tags defined somewhere that have never been applied. - * - * Keeps a short-term cache in memory, so calling this multiple times in the - * same request should be fine. - * * @return array Array of string => int */ public static function tagUsageStatistics() { - global $wgChangeTagsSchemaMigrationStage, $wgTagStatisticsNewTable; - if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_WRITE_BOTH || - ( $wgTagStatisticsNewTable && $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) - ) { - return self::newTagUsageStatistics(); - } - - $fname = __METHOD__; - $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); - return $cache->getWithSetCallback( - $cache->makeKey( 'change-tag-statistics' ), - WANObjectCache::TTL_MINUTE * 5, - function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) { - $dbr = wfGetDB( DB_REPLICA, 'vslow' ); - - $setOpts += Database::getCacheSetOptions( $dbr ); - - $res = $dbr->select( - 'change_tag', - [ 'ct_tag', 'hitcount' => 'count(*)' ], - [], - $fname, - [ 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' ] - ); - - $out = []; - foreach ( $res as $row ) { - $out[$row->ct_tag] = $row->hitcount; - } - - return $out; - }, - [ - 'checkKeys' => [ $cache->makeKey( 'change-tag-statistics' ) ], - 'lockTSE' => WANObjectCache::TTL_MINUTE * 5, - 'pcTTL' => WANObjectCache::TTL_PROC_LONG - ] - ); - } - - /** - * Same self::tagUsageStatistics() but uses change_tag_def. - * - * @return array Array of string => int - */ - private static function newTagUsageStatistics() { $dbr = wfGetDB( DB_REPLICA ); $res = $dbr->select( 'change_tag_def', diff --git a/includes/content/Content.php b/includes/content/Content.php index bb3fb107d7..1bb43f83b6 100644 --- a/includes/content/Content.php +++ b/includes/content/Content.php @@ -241,6 +241,8 @@ interface Content { * that it's also in a countable location (e.g. a current revision in the * main namespace). * + * @see SlotRoleHandler::supportsArticleCount + * * @since 1.21 * * @param bool|null $hasLinks If it is known whether this content contains @@ -352,6 +354,8 @@ interface Content { * Returns whether this Content represents a redirect. * Shorthand for getRedirectTarget() !== null. * + * @see SlotRoleHandler::supportsRedirects + * * @since 1.21 * * @return bool diff --git a/includes/content/ContentHandler.php b/includes/content/ContentHandler.php index fab043a2ba..5c18a330cb 100644 --- a/includes/content/ContentHandler.php +++ b/includes/content/ContentHandler.php @@ -174,62 +174,17 @@ abstract class ContentHandler { * Note: this is used by, and may thus not use, Title::getContentModel() * * @since 1.21 + * @deprecated since 1.33, use SlotRoleHandler::getDefaultModel() together with + * SlotRoleRegistry::getRoleHandler(). * * @param Title $title * * @return string Default model name for the page given by $title */ public static function getDefaultModelFor( Title $title ) { - // NOTE: this method must not rely on $title->getContentModel() directly or indirectly, - // because it is used to initialize the mContentModel member. - - $ns = $title->getNamespace(); - - $ext = false; - $m = null; - $model = MWNamespace::getNamespaceContentModel( $ns ); - - // Hook can determine default model - if ( !Hooks::run( 'ContentHandlerDefaultModelFor', [ $title, &$model ] ) ) { - if ( !is_null( $model ) ) { - return $model; - } - } - - // Could this page contain code based on the title? - $isCodePage = NS_MEDIAWIKI == $ns && preg_match( '!\.(css|js|json)$!u', $title->getText(), $m ); - if ( $isCodePage ) { - $ext = $m[1]; - } - - // Is this a user subpage containing code? - $isCodeSubpage = NS_USER == $ns - && !$isCodePage - && preg_match( "/\\/.*\\.(js|css|json)$/", $title->getText(), $m ); - if ( $isCodeSubpage ) { - $ext = $m[1]; - } - - // Is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook? - $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT; - $isWikitext = $isWikitext && !$isCodePage && !$isCodeSubpage; - - if ( !$isWikitext ) { - switch ( $ext ) { - case 'js': - return CONTENT_MODEL_JAVASCRIPT; - case 'css': - return CONTENT_MODEL_CSS; - case 'json': - return CONTENT_MODEL_JSON; - default: - return is_null( $model ) ? CONTENT_MODEL_TEXT : $model; - } - } - - // We established that it must be wikitext - - return CONTENT_MODEL_WIKITEXT; + $slotRoleregistry = MediaWikiServices::getInstance()->getSlotRoleRegistry(); + $mainSlotHandler = $slotRoleregistry->getRoleHandler( 'main' ); + return $mainSlotHandler->getDefaultModel( $title ); } /** @@ -777,7 +732,7 @@ abstract class ContentHandler { /** * Determines whether the content type handled by this ContentHandler - * can be used on the given page. + * can be used for the main slot of the given page. * * This default implementation always returns true. * Subclasses may override this to restrict the use of this content model to specific locations, @@ -787,6 +742,8 @@ abstract class ContentHandler { * @note this calls the ContentHandlerCanBeUsedOn hook which may be used to override which * content model can be used where. * + * @see SlotRoleHandler::isAllowedModel + * * @param Title $title The page's title. * * @return bool True if content of this kind can be used on the given page, false otherwise. diff --git a/includes/content/WikitextContent.php b/includes/content/WikitextContent.php index a7021b1e7b..517d807867 100644 --- a/includes/content/WikitextContent.php +++ b/includes/content/WikitextContent.php @@ -25,6 +25,7 @@ * @author Daniel Kinzler */ +use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; /** @@ -41,6 +42,11 @@ class WikitextContent extends TextContent { */ private $hadSignature = false; + /** + * @var array|null Stack trace of the previous parse + */ + private $previousParseStackTrace = null; + public function __construct( $text ) { parent::__construct( $text, CONTENT_MODEL_WIKITEXT ); } @@ -337,6 +343,28 @@ class WikitextContent extends TextContent { ) { global $wgParser; + $stackTrace = ( new RuntimeException() )->getTraceAsString(); + if ( $this->previousParseStackTrace ) { + // NOTE: there may be legitimate changes to re-parse the same WikiText content, + // e.g. if predicted revision ID for the REVISIONID magic word mismatched. + // But that should be rare. + $logger = LoggerFactory::getInstance( 'DuplicateParse' ); + $logger->debug( + __METHOD__ . ': Possibly redundant parse!', + [ + 'title' => $title->getPrefixedDBkey(), + 'rev' => $revId, + 'options-hash' => $options->optionsHash( + ParserOptions::allCacheVaryingOptions(), + $title + ), + 'trace' => $stackTrace, + 'previous-trace' => $this->previousParseStackTrace, + ] + ); + } + $this->previousParseStackTrace = $stackTrace; + list( $redir, $text ) = $this->getRedirectTargetAndText(); $output = $wgParser->parse( $text, $title, $options, true, true, $revId ); diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index 5f09555e7a..d4277245c1 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -137,7 +137,7 @@ class CloneDatabase { global $wgDBprefix; $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); - $lbFactory->setDomainPrefix( $prefix ); + $lbFactory->setLocalDomainPrefix( $prefix ); $wgDBprefix = $prefix; } } diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 696e81f50c..628b47bd53 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -972,7 +972,7 @@ class DatabaseOracle extends Database { if ( $sl < 0 ) { continue; } - if ( '-' == $line[0] && '-' == $line[1] ) { + if ( $line[0] == '-' && $line[1] == '-' ) { continue; } @@ -986,7 +986,7 @@ class DatabaseOracle extends Database { $dollarquote = true; } } elseif ( !$dollarquote ) { - if ( ';' == $line[$sl] && ( $sl < 2 || ';' != $line[$sl - 1] ) ) { + if ( $line[$sl] == ';' && ( $sl < 2 || $line[$sl - 1] != ';' ) ) { $done = true; $line = substr( $line, 0, $sl ); } @@ -1017,7 +1017,7 @@ class DatabaseOracle extends Database { call_user_func( $resultCallback, $res, $this ); } - if ( false === $res ) { + if ( $res === false ) { $err = $this->lastError(); return "Query \"{$cmd}\" failed with error code \"$err\".\n"; diff --git a/includes/debug/logger/LegacyLogger.php b/includes/debug/logger/LegacyLogger.php index 6288a504cc..9f63eded09 100644 --- a/includes/debug/logger/LegacyLogger.php +++ b/includes/debug/logger/LegacyLogger.php @@ -375,7 +375,7 @@ class LegacyLogger extends AbstractLogger { * @return string */ protected static function flatten( $item ) { - if ( null === $item ) { + if ( $item === null ) { return '[Null]'; } diff --git a/includes/debug/logger/monolog/CeeFormatter.php b/includes/debug/logger/monolog/CeeFormatter.php new file mode 100644 index 0000000000..4b0c6cb608 --- /dev/null +++ b/includes/debug/logger/monolog/CeeFormatter.php @@ -0,0 +1,23 @@ +mExternals, $existing ); foreach ( $diffs as $url => $dummy ) { - foreach ( wfMakeUrlIndexes( $url ) as $index ) { + foreach ( LinkFilter::makeIndexes( $url ) as $index ) { $arr[] = [ 'el_from' => $this->mId, 'el_to' => $url, diff --git a/includes/deferred/UserEditCountUpdate.php b/includes/deferred/UserEditCountUpdate.php index 5194e4f1cc..4ce0b18ec6 100644 --- a/includes/deferred/UserEditCountUpdate.php +++ b/includes/deferred/UserEditCountUpdate.php @@ -68,14 +68,15 @@ class UserEditCountUpdate implements DeferrableUpdate, MergeableUpdate { public function doUpdate() { $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); $dbw = $lb->getConnection( DB_MASTER ); + $fname = __METHOD__; - ( new AutoCommitUpdate( $dbw, __METHOD__, function () use ( $lb, $dbw ) { + ( new AutoCommitUpdate( $dbw, __METHOD__, function () use ( $lb, $dbw, $fname ) { foreach ( $this->infoByUser as $userId => $info ) { $dbw->update( 'user', [ 'user_editcount=user_editcount+' . (int)$info['increment'] ], [ 'user_id' => $userId, 'user_editcount IS NOT NULL' ], - __METHOD__ + $fname ); /** @var User[] $affectedInstances */ $affectedInstances = $info['instances']; @@ -87,7 +88,7 @@ class UserEditCountUpdate implements DeferrableUpdate, MergeableUpdate { if ( $dbr !== $dbw ) { // This method runs after the new revisions were committed. // Wait for the replica to catch up so they will all be counted. - $dbr->flushSnapshot( __METHOD__ ); + $dbr->flushSnapshot( $fname ); $lb->safeWaitForMasterPos( $dbr ); } $affectedInstances[0]->initEditCountInternal(); @@ -96,7 +97,7 @@ class UserEditCountUpdate implements DeferrableUpdate, MergeableUpdate { 'user', [ 'user_editcount' ], [ 'user_id' => $userId ], - __METHOD__ + $fname ); // Update the edit count in the instance caches. This is mostly useful diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index 8d0971e659..826eecbb56 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -21,8 +21,10 @@ * @ingroup DifferenceEngine */ +use MediaWiki\MediaWikiServices; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\SlotRecord; +use MediaWiki\Storage\NameTableAccessException; /** * DifferenceEngine is responsible for rendering the difference between two revisions as HTML. @@ -1055,7 +1057,7 @@ class DifferenceEngine extends ContextSource { $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'], $slotContents[$role]['new'] ); if ( $slotDiff && $role !== SlotRecord::MAIN ) { - // TODO use human-readable role name at least + // FIXME: ask SlotRoleHandler::getSlotNameMessage $slotTitle = $role; $difftext .= $this->getSlotHeader( $slotTitle ); } @@ -1797,22 +1799,42 @@ class DifferenceEngine extends ContextSource { // Load tags information for both revisions $dbr = wfGetDB( DB_REPLICA ); + $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); if ( $this->mOldid !== false ) { - $this->mOldTags = $dbr->selectField( - 'tag_summary', - 'ts_tags', - [ 'ts_rev_id' => $this->mOldid ], + $tagIds = $dbr->selectFieldValues( + 'change_tag', + 'ct_tag_id', + [ 'ct_rev_id' => $this->mOldid ], __METHOD__ ); + $tags = []; + foreach ( $tagIds as $tagId ) { + try { + $tags[] = $changeTagDefStore->getName( (int)$tagId ); + } catch ( NameTableAccessException $exception ) { + continue; + } + } + $this->mOldTags = implode( ',', $tags ); } else { $this->mOldTags = false; } - $this->mNewTags = $dbr->selectField( - 'tag_summary', - 'ts_tags', - [ 'ts_rev_id' => $this->mNewid ], + + $tagIds = $dbr->selectFieldValues( + 'change_tag', + 'ct_tag_id', + [ 'ct_rev_id' => $this->mNewid ], __METHOD__ ); + $tags = []; + foreach ( $tagIds as $tagId ) { + try { + $tags[] = $changeTagDefStore->getName( (int)$tagId ); + } catch ( NameTableAccessException $exception ) { + continue; + } + } + $this->mNewTags = implode( ',', $tags ); return true; } diff --git a/includes/exception/MWException.php b/includes/exception/MWException.php index af835e4776..7f70c4fbaf 100644 --- a/includes/exception/MWException.php +++ b/includes/exception/MWException.php @@ -121,7 +121,7 @@ class MWException extends Exception { "Fatal exception of type $1", $type, $logId, - MWExceptionHandler::getURL( $this ) + MWExceptionHandler::getURL() ) ) ) . " # Somethingcool> @@ -2026,7 +2028,19 @@ class Parser { * @return string */ public static function normalizeLinkUrl( $url ) { - # First, make sure unsafe characters are encoded + # Test for RFC 3986 IPv6 syntax + $scheme = '[a-z][a-z0-9+.-]*:'; + $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*'; + $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]'; + if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) && + IP::isValid( rawurldecode( $m[1] ) ) + ) { + $isIPv6 = rawurldecode( $m[1] ); + } else { + $isIPv6 = false; + } + + # Make sure unsafe characters are encoded $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/', function ( $m ) { return rawurlencode( $m[0] ); @@ -2058,6 +2072,16 @@ class Parser { $ret = self::normalizeUrlComponent( substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret; + # Fix IPv6 syntax + if ( $isIPv6 !== false ) { + $ipv6Host = "%5B({$isIPv6})%5D"; + $ret = preg_replace( + "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i", + "$1[$2]", + $ret + ); + } + return $ret; } @@ -3122,7 +3146,7 @@ class Parser { # $args is a list of argument nodes, starting from index 0, not including $part1 # @todo FIXME: If piece['parts'] is null then the call to getLength() # below won't work b/c this $args isn't an object - $args = ( null == $piece['parts'] ) ? [] : $piece['parts']; + $args = ( $piece['parts'] == null ) ? [] : $piece['parts']; $profileSection = null; // profile templates @@ -5042,9 +5066,10 @@ class Parser { $ig->setShowFilename( false ); } if ( isset( $params['caption'] ) ) { - $caption = $params['caption']; - $caption = htmlspecialchars( $caption ); - $caption = $this->replaceInternalLinks( $caption ); + // NOTE: We aren't passing a frame here or below. Frame info + // is currently opaque to Parsoid, which acts on OT_PREPROCESS. + // See T107332#4030581 + $caption = $this->recursiveTagParse( $params['caption'] ); $ig->setCaptionHtml( $caption ); } if ( isset( $params['perrow'] ) ) { @@ -5445,6 +5470,7 @@ class Parser { * Adds an entry to appropriate link tables. * * @since 1.32 + * @param string $value * @return array of `[ type, target ]`, where: * - `type` is one of: * - `null`: Given value is not a valid link target, use default @@ -5500,6 +5526,40 @@ class Parser { # that are later expanded to html- so expand them now and # remove the tags $tooltip = $this->mStripState->unstripBoth( $tooltip ); + # Compatibility hack! In HTML certain entity references not terminated + # by a semicolon are decoded (but not if we're in an attribute; that's + # how link URLs get away without properly escaping & in queries). + # But wikitext has always required semicolon-termination of entities, + # so encode & where needed to avoid decode of semicolon-less entities. + # See T209236 and + # https://www.w3.org/TR/html5/syntax.html#named-character-references + # T210437 discusses moving this workaround to Sanitizer::stripAllTags. + $tooltip = preg_replace( "/ + & # 1. entity prefix + (?= # 2. followed by: + (?: # a. one of the legacy semicolon-less named entities + A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)| + C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)| + GT|I(?:acute|circ|grave|uml)|LT|Ntilde| + O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN| + U(?:acute|circ|grave|uml)|Yacute| + a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar| + c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg| + divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)| + frac(?:1(?:2|4)|34)| + gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)| + i(?:acute|circ|excl|grave|quest|uml)|laquo| + lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)| + m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)| + not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)| + o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)| + p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)| + s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)| + u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml) + ) + (?:[^;]|$)) # b. and not followed by a semicolon + # S = study, for efficiency + /Sx", '&', $tooltip ); $tooltip = Sanitizer::stripAllTags( $tooltip ); return $tooltip; diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php index de67b84740..8407992ba1 100644 --- a/includes/parser/ParserOptions.php +++ b/includes/parser/ParserOptions.php @@ -57,7 +57,7 @@ class ParserOptions { /** * Lazy-loaded options - * @var callback[] + * @var callable[] */ private static $lazyOptions = [ 'dateformat' => [ __CLASS__, 'initDateFormat' ], @@ -650,8 +650,10 @@ class ParserOptions { /** * Lazy initializer for dateFormat + * @param ParserOptions $popt + * @return string */ - private static function initDateFormat( $popt ) { + private static function initDateFormat( ParserOptions $popt ) { return $popt->mUser->getDatePreference(); } diff --git a/includes/parser/RemexStripTagHandler.php b/includes/parser/RemexStripTagHandler.php index 41c6bf41df..a41e7b60ed 100644 --- a/includes/parser/RemexStripTagHandler.php +++ b/includes/parser/RemexStripTagHandler.php @@ -66,7 +66,6 @@ class RemexStripTagHandler implements TokenHandler { 'fieldset' => true, 'figcaption' => true, 'figure' => true, - 'figcaption' => true, 'footer' => true, 'form' => true, 'h1' => true, diff --git a/includes/parser/Sanitizer.php b/includes/parser/Sanitizer.php index 85c71eeb44..f8c3bc2c72 100644 --- a/includes/parser/Sanitizer.php +++ b/includes/parser/Sanitizer.php @@ -349,18 +349,18 @@ class Sanitizer { /** * Regular expression to match HTML/XML attribute pairs within a tag. - * Allows some... latitude. Based on, - * https://www.w3.org/TR/html5/syntax.html#before-attribute-value-state - * Used in Sanitizer::fixTagAttributes and Sanitizer::decodeTagAttributes + * Based on https://www.w3.org/TR/html5/syntax.html#before-attribute-name-state + * Used in Sanitizer::decodeTagAttributes * @return string */ static function getAttribsRegex() { if ( self::$attribsRegex === null ) { - $attribFirst = "[:_\p{L}\p{N}]"; - $attrib = "[:_\.\-\p{L}\p{N}]"; - $space = '[\x09\x0a\x0c\x0d\x20]'; + $spaceChars = '\x09\x0a\x0c\x0d\x20'; + $space = "[{$spaceChars}]"; + $attrib = "[^{$spaceChars}\/>=]"; + $attribFirst = "(?:{$attrib}|=)"; self::$attribsRegex = - "/(?:^|$space)({$attribFirst}{$attrib}*) + "/({$attribFirst}{$attrib}*) ($space*=$space* (?: # The attribute value: quoted or alone @@ -368,11 +368,29 @@ class Sanitizer { | '([^']*)(?:'|\$) | (((?!$space|>).)*) ) - )?(?=$space|\$)/sxu"; + )?/sxu"; } return self::$attribsRegex; } + /** + * Lazy-initialised attribute name regex, see getAttribNameRegex() + */ + private static $attribNameRegex; + + /** + * Used in Sanitizer::decodeTagAttributes to filter attributes. + * @return string + */ + static function getAttribNameRegex() { + if ( self::$attribNameRegex === null ) { + $attribFirst = "[:_\p{L}\p{N}]"; + $attrib = "[:_\.\-\p{L}\p{N}]"; + self::$attribNameRegex = "/^({$attribFirst}{$attrib}*)$/sxu"; + } + return self::$attribNameRegex; + } + /** * Return the various lists of recognized tags * @param array $extratags For any extra tags to include @@ -495,6 +513,7 @@ class Sanitizer { $bits = explode( '<', $text ); $text = str_replace( '>', '>', array_shift( $bits ) ); if ( !MWTidy::isEnabled() ) { + wfDeprecated( 'disabling tidy', '1.33' ); $tagstack = $tablestack = []; foreach ( $bits as $x ) { $regs = []; @@ -1433,18 +1452,24 @@ class Sanitizer { return []; } - $attribs = []; $pairs = []; if ( !preg_match_all( self::getAttribsRegex(), $text, $pairs, PREG_SET_ORDER ) ) { - return $attribs; + return []; } + $attribs = []; foreach ( $pairs as $set ) { $attribute = strtolower( $set[1] ); + + // Filter attribute names with unacceptable characters + if ( !preg_match( self::getAttribNameRegex(), $attribute ) ) { + continue; + } + $value = self::getTagAttributeCallback( $set ); // Normalize whitespace diff --git a/includes/password/PasswordPolicyChecks.php b/includes/password/PasswordPolicyChecks.php index 837e959780..c3af88f07d 100644 --- a/includes/password/PasswordPolicyChecks.php +++ b/includes/password/PasswordPolicyChecks.php @@ -22,15 +22,23 @@ use Cdb\Reader as CdbReader; use MediaWiki\MediaWikiServices; +use Wikimedia\PasswordBlacklist; /** - * Functions to check passwords against a policy requirement + * Functions to check passwords against a policy requirement. + * + * $policyVal is the value configured in $wgPasswordPolicy. If the return status is fatal, + * the user won't be allowed to login. If the status is not good but not fatal, the user + * will not be allowed to set the given password (on registration or password change), + * but can still log in after bypassing a warning. + * * @since 1.26 + * @see $wgPasswordPolicy */ class PasswordPolicyChecks { /** - * Check password is longer than minimum, not fatal + * Check password is longer than minimum, not fatal. * @param int $policyVal minimal length * @param User $user * @param string $password @@ -45,7 +53,7 @@ class PasswordPolicyChecks { } /** - * Check password is longer than minimum, fatal + * Check password is longer than minimum, fatal. * @param int $policyVal minimal length * @param User $user * @param string $password @@ -60,7 +68,8 @@ class PasswordPolicyChecks { } /** - * Check password is shorter than maximum, fatal + * Check password is shorter than maximum, fatal. + * Intended for preventing DoS attacks when using a more expensive password hash like PBKDF2. * @param int $policyVal maximum length * @param User $user * @param string $password @@ -75,7 +84,7 @@ class PasswordPolicyChecks { } /** - * Check if username and password match + * Check if username and password are a (case-insensitive) match. * @param bool $policyVal true to force compliance. * @param User $user * @param string $password @@ -86,7 +95,7 @@ class PasswordPolicyChecks { $username = $user->getName(); $contLang = MediaWikiServices::getInstance()->getContentLanguage(); if ( - $policyVal && $contLang->lc( $password ) === $contLang->lc( $username ) + $policyVal && hash_equals( $contLang->lc( $username ), $contLang->lc( $password ) ) ) { $status->error( 'password-name-match' ); } @@ -94,7 +103,7 @@ class PasswordPolicyChecks { } /** - * Check if username and password are on a blacklist + * Check if username and password are on a blacklist of past MediaWiki default passwords. * @param bool $policyVal true to force compliance. * @param User $user * @param string $password @@ -109,12 +118,15 @@ class PasswordPolicyChecks { $status = Status::newGood(); $username = $user->getName(); if ( $policyVal ) { - if ( isset( $blockedLogins[$username] ) && $password == $blockedLogins[$username] ) { + if ( + isset( $blockedLogins[$username] ) && + hash_equals( $blockedLogins[$username], $password ) + ) { $status->error( 'password-login-forbidden' ); } // Example from ApiChangeAuthenticationRequest - if ( $password === 'ExamplePassword' ) { + if ( hash_equals( 'ExamplePassword', $password ) ) { $status->error( 'password-login-forbidden' ); } } @@ -122,7 +134,8 @@ class PasswordPolicyChecks { } /** - * Ensure that password isn't in top X most popular passwords + * Ensure that password isn't in top X most popular passwords, as defined by + * $wgPopularPasswordFile. * * @param int $policyVal Cut off to use. Will automatically shrink to the max * supported for error messages if set to more than max number of passwords on file, @@ -130,12 +143,16 @@ class PasswordPolicyChecks { * @param User $user * @param string $password * @since 1.27 + * @deprecated since 1.33 * @return Status + * @see $wgPopularPasswordFile */ public static function checkPopularPasswordBlacklist( $policyVal, User $user, $password ) { global $wgPopularPasswordFile, $wgSitename; $status = Status::newGood(); if ( $policyVal > 0 ) { + wfDeprecated( __METHOD__, '1.33' ); + $langEn = Language::factory( 'en' ); $passwordKey = $langEn->lc( trim( $password ) ); @@ -167,4 +184,27 @@ class PasswordPolicyChecks { return $status; } + /** + * Ensure the password isn't in the list of passwords blacklisted by the + * wikimedia/password-blacklist library, which contains (as of 0.1.4) the + * 100.000 top passwords from SecLists (as a Bloom filter, with an + * 0.000001 false positive ratio). + * + * @param bool $policyVal Whether to apply this policy + * @param User $user + * @param string $password + * + * @since 1.33 + * + * @return Status + */ + public static function checkPasswordNotInLargeBlacklist( $policyVal, User $user, $password ) { + $status = Status::newGood(); + if ( $policyVal && PasswordBlacklist\PasswordBlacklist::isBlacklisted( $password ) ) { + $status->error( 'passwordinlargeblacklist' ); + } + + return $status; + } + } diff --git a/includes/password/Pbkdf2Password.php b/includes/password/Pbkdf2Password.php index 60650452fc..ce684ded40 100644 --- a/includes/password/Pbkdf2Password.php +++ b/includes/password/Pbkdf2Password.php @@ -41,54 +41,21 @@ class Pbkdf2Password extends ParameterizedPassword { return ':'; } - protected function shouldUseHashExtension() { - return $this->config['use-hash-extension'] ?? function_exists( 'hash_pbkdf2' ); - } - public function crypt( $password ) { if ( count( $this->args ) == 0 ) { $this->args[] = base64_encode( random_bytes( 16 ) ); } - if ( $this->shouldUseHashExtension() ) { - $hash = hash_pbkdf2( - $this->params['algo'], - $password, - base64_decode( $this->args[0] ), - (int)$this->params['rounds'], - (int)$this->params['length'], - true - ); - if ( !is_string( $hash ) ) { - throw new PasswordError( 'Error when hashing password.' ); - } - } else { - $hashLenHash = hash( $this->params['algo'], '', true ); - if ( !is_string( $hashLenHash ) ) { - throw new PasswordError( 'Error when hashing password.' ); - } - $hashLen = strlen( $hashLenHash ); - $blockCount = ceil( $this->params['length'] / $hashLen ); - - $hash = ''; - $salt = base64_decode( $this->args[0] ); - for ( $i = 1; $i <= $blockCount; ++$i ) { - $roundTotal = $lastRound = hash_hmac( - $this->params['algo'], - $salt . pack( 'N', $i ), - $password, - true - ); - - for ( $j = 1; $j < $this->params['rounds']; ++$j ) { - $lastRound = hash_hmac( $this->params['algo'], $lastRound, $password, true ); - $roundTotal ^= $lastRound; - } - - $hash .= $roundTotal; - } - - $hash = substr( $hash, 0, $this->params['length'] ); + $hash = hash_pbkdf2( + $this->params['algo'], + $password, + base64_decode( $this->args[0] ), + (int)$this->params['rounds'], + (int)$this->params['length'], + true + ); + if ( !is_string( $hash ) ) { + throw new PasswordError( 'Error when hashing password.' ); } $this->hash = base64_encode( $hash ); diff --git a/includes/preferences/DefaultPreferencesFactory.php b/includes/preferences/DefaultPreferencesFactory.php index c552506fe3..512a6b3829 100644 --- a/includes/preferences/DefaultPreferencesFactory.php +++ b/includes/preferences/DefaultPreferencesFactory.php @@ -127,7 +127,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { $this->watchlistPreferences( $user, $context, $preferences ); $this->searchPreferences( $preferences ); - Hooks::run( 'GetPreferences', [ $user, &$preferences, $context ] ); + Hooks::run( 'GetPreferences', [ $user, &$preferences ] ); $this->loadPreferenceValues( $user, $context, $preferences ); $this->logger->debug( "Created form descriptor for user '{$user->getName()}'" ); @@ -796,6 +796,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { 'section' => 'rendering/timeoffset', 'id' => 'wpTimeCorrection', 'filter' => TimezoneFilter::class, + 'placeholder-message' => 'timezone-useoffset-placeholder', ]; } @@ -961,7 +962,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { $defaultPreferences['hideminor'] = [ 'type' => 'toggle', 'label-message' => 'tog-hideminor', - 'section' => 'rc/advancedrc', + 'section' => 'rc/changesrc', ]; $defaultPreferences['rcfilters-rc-collapsed'] = [ 'type' => 'api', @@ -990,14 +991,14 @@ class DefaultPreferencesFactory implements PreferencesFactory { $defaultPreferences['hidecategorization'] = [ 'type' => 'toggle', 'label-message' => 'tog-hidecategorization', - 'section' => 'rc/advancedrc', + 'section' => 'rc/changesrc', ]; } if ( $user->useRCPatrol() ) { $defaultPreferences['hidepatrolled'] = [ 'type' => 'toggle', - 'section' => 'rc/advancedrc', + 'section' => 'rc/changesrc', 'label-message' => 'tog-hidepatrolled', ]; } @@ -1005,7 +1006,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { if ( $user->useNPPatrol() ) { $defaultPreferences['newpageshidepatrolled'] = [ 'type' => 'toggle', - 'section' => 'rc/advancedrc', + 'section' => 'rc/changesrc', 'label-message' => 'tog-newpageshidepatrolled', ]; } @@ -1020,7 +1021,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { $defaultPreferences['rcenhancedfilters-disable'] = [ 'type' => 'toggle', - 'section' => 'rc/optoutrc', + 'section' => 'rc/advancedrc', 'label-message' => 'rcfilters-preference-label', 'help-message' => 'rcfilters-preference-help', ]; @@ -1091,27 +1092,27 @@ class DefaultPreferencesFactory implements PreferencesFactory { ]; $defaultPreferences['watchlisthideminor'] = [ 'type' => 'toggle', - 'section' => 'watchlist/advancedwatchlist', + 'section' => 'watchlist/changeswatchlist', 'label-message' => 'tog-watchlisthideminor', ]; $defaultPreferences['watchlisthidebots'] = [ 'type' => 'toggle', - 'section' => 'watchlist/advancedwatchlist', + 'section' => 'watchlist/changeswatchlist', 'label-message' => 'tog-watchlisthidebots', ]; $defaultPreferences['watchlisthideown'] = [ 'type' => 'toggle', - 'section' => 'watchlist/advancedwatchlist', + 'section' => 'watchlist/changeswatchlist', 'label-message' => 'tog-watchlisthideown', ]; $defaultPreferences['watchlisthideanons'] = [ 'type' => 'toggle', - 'section' => 'watchlist/advancedwatchlist', + 'section' => 'watchlist/changeswatchlist', 'label-message' => 'tog-watchlisthideanons', ]; $defaultPreferences['watchlisthideliu'] = [ 'type' => 'toggle', - 'section' => 'watchlist/advancedwatchlist', + 'section' => 'watchlist/changeswatchlist', 'label-message' => 'tog-watchlisthideliu', ]; @@ -1135,7 +1136,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { if ( $this->config->get( 'RCWatchCategoryMembership' ) ) { $defaultPreferences['watchlisthidecategorization'] = [ 'type' => 'toggle', - 'section' => 'watchlist/advancedwatchlist', + 'section' => 'watchlist/changeswatchlist', 'label-message' => 'tog-watchlisthidecategorization', ]; } @@ -1143,7 +1144,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { if ( $user->useRCPatrol() ) { $defaultPreferences['watchlisthidepatrolled'] = [ 'type' => 'toggle', - 'section' => 'watchlist/advancedwatchlist', + 'section' => 'watchlist/changeswatchlist', 'label-message' => 'tog-watchlisthidepatrolled', ]; } @@ -1174,7 +1175,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { // tog-watchrollback $defaultPreferences[$pref] = [ 'type' => 'toggle', - 'section' => 'watchlist/advancedwatchlist', + 'section' => 'watchlist/pageswatchlist', 'label-message' => "tog-$pref", ]; } @@ -1201,7 +1202,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { $defaultPreferences['wlenhancedfilters-disable'] = [ 'type' => 'toggle', - 'section' => 'watchlist/optoutwatchlist', + 'section' => 'watchlist/advancedwatchlist', 'label-message' => 'rcfilters-watchlist-preference-label', 'help-message' => 'rcfilters-watchlist-preference-help', ]; diff --git a/includes/profiler/ProfilerExcimer.php b/includes/profiler/ProfilerExcimer.php new file mode 100644 index 0000000000..20f9a78508 --- /dev/null +++ b/includes/profiler/ProfilerExcimer.php @@ -0,0 +1,147 @@ +period = $params['period'] ?? 0.01; + $maxDepth = $params['maxDepth'] ?? 100; + + if ( isset( $params['cpuProfiler'] ) ) { + $this->cpuProf = $params['cpuProfiler']; + } else { + $this->cpuProf = new ExcimerProfiler; + $this->cpuProf->setEventType( EXCIMER_CPU ); + $this->cpuProf->setPeriod( $this->period ); + $this->cpuProf->setMaxDepth( $maxDepth ); + $this->cpuProf->start(); + } + + if ( isset( $params['realProfiler'] ) ) { + $this->realProf = $params['realProfiler']; + } else { + $this->realProf = new ExcimerProfiler; + $this->realProf->setEventType( EXCIMER_REAL ); + $this->realProf->setPeriod( $this->period ); + $this->realProf->setMaxDepth( $maxDepth ); + $this->realProf->start(); + } + } + + public function scopedProfileIn( $section ) { + } + + public function close() { + $this->cpuProf->stop(); + $this->realProf->stop(); + } + + public function getFunctionStats() { + $this->close(); + $cpuStats = $this->cpuProf->getLog()->aggregateByFunction(); + $realStats = $this->realProf->getLog()->aggregateByFunction(); + $allNames = array_keys( $realStats + $cpuStats ); + $cpuSamples = $this->cpuProf->getLog()->getEventCount(); + $realSamples = $this->realProf->getLog()->getEventCount(); + + $resultStats = [ [ + 'name' => '-total', + 'calls' => 1, + 'memory' => 0, + '%memory' => 0, + 'min_real' => 0, + 'max_real' => 0, + 'cpu' => $cpuSamples * $this->period * 1000, + '%cpu' => 100, + 'real' => $realSamples * $this->period * 1000, + '%real' => 100, + ] ]; + + foreach ( $allNames as $funcName ) { + $cpuEntry = $cpuStats[$funcName] ?? false; + $realEntry = $realStats[$funcName] ?? false; + $resultEntry = [ + 'name' => $funcName, + 'calls' => 0, + 'memory' => 0, + '%memory' => 0, + 'min_real' => 0, + 'max_real' => 0, + ]; + + if ( $cpuEntry ) { + $resultEntry['cpu'] = $cpuEntry['inclusive'] * $this->period * 1000; + $resultEntry['%cpu'] = $cpuEntry['inclusive'] / $cpuSamples * 100; + } else { + $resultEntry['cpu'] = 0; + $resultEntry['%cpu'] = 0; + } + if ( $realEntry ) { + $resultEntry['real'] = $realEntry['inclusive'] * $this->period * 1000; + $resultEntry['%real'] = $realEntry['inclusive'] / $realSamples * 100; + } else { + $resultEntry['real'] = 0; + $resultEntry['%real'] = 0; + } + + $resultStats[] = $resultEntry; + } + return $resultStats; + } + + public function getOutput() { + $this->close(); + $cpuLog = $this->cpuProf->getLog(); + $realLog = $this->realProf->getLog(); + $cpuStats = $cpuLog->aggregateByFunction(); + $realStats = $realLog->aggregateByFunction(); + $allNames = array_keys( $cpuStats + $realStats ); + $cpuSamples = $cpuLog->getEventCount(); + $realSamples = $realLog->getEventCount(); + + $result = ''; + + $titleFormat = "%-70s %10s %11s %10s %11s %10s %11s %10s %11s\n"; + $statsFormat = "%-70s %10d %10.1f%% %10d %10.1f%% %10d %10.1f%% %10d %10.1f%%\n"; + $result .= sprintf( $titleFormat, + 'Name', + 'CPU incl', 'CPU incl%', 'CPU self', 'CPU self%', + 'Real incl', 'Real incl%', 'Real self', 'Real self%' + ); + + foreach ( $allNames as $funcName ) { + $realEntry = $realStats[$funcName] ?? false; + $cpuEntry = $cpuStats[$funcName] ?? false; + $realIncl = $realEntry ? $realEntry['inclusive'] : 0; + $realSelf = $realEntry ? $realEntry['self'] : 0; + $cpuIncl = $cpuEntry ? $cpuEntry['inclusive'] : 0; + $cpuSelf = $cpuEntry ? $cpuEntry['self'] : 0; + $result .= sprintf( $statsFormat, + $funcName, + $cpuIncl * $this->period * 1000, + $cpuIncl == 0 ? 0 : $cpuIncl / $cpuSamples * 100, + $cpuSelf * $this->period * 1000, + $cpuSelf == 0 ? 0 : $cpuSelf / $cpuSamples * 100, + $realIncl * $this->period * 1000, + $realIncl == 0 ? 0 : $realIncl / $realSamples * 100, + $realSelf * $this->period * 1000, + $realSelf == 0 ? 0 : $realSelf / $realSamples * 100 + ); + } + + return $result; + } +} diff --git a/includes/profiler/output/ProfilerOutputDb.php b/includes/profiler/output/ProfilerOutputDb.php index 6e0085d8fc..ea5f7ad14b 100644 --- a/includes/profiler/output/ProfilerOutputDb.php +++ b/includes/profiler/output/ProfilerOutputDb.php @@ -21,7 +21,6 @@ * @ingroup Profiler */ -use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\DBError; /** @@ -56,7 +55,7 @@ class ProfilerOutputDb extends ProfilerOutput { } $fname = __METHOD__; - $dbw->onTransactionCommitOrIdle( function ( Database $dbw ) use ( $stats, $fname ) { + $dbw->onTransactionCommitOrIdle( function () use ( $stats, $fname, $dbw ) { $pfhost = $this->perHost ? wfHostname() : ''; // Sqlite: avoid excess b-tree rebuilds (mostly for non-WAL mode) // non-Sqlite: lower contention with small transactions diff --git a/includes/registration/ExtensionProcessor.php b/includes/registration/ExtensionProcessor.php index e43b3b88fa..07fab78f96 100644 --- a/includes/registration/ExtensionProcessor.php +++ b/includes/registration/ExtensionProcessor.php @@ -46,6 +46,7 @@ class ExtensionProcessor implements Processor { 'PasswordPolicy', 'RateLimits', 'RawHtmlMessages', + 'ReauthenticateTime', 'RecentChangesFlags', 'RemoveCredentialsBlacklist', 'RemoveGroups', diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 2fc81e37b6..9570e038a8 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -1529,10 +1529,12 @@ MESSAGE; self::inDebugMode() ); if ( $js === false ) { - throw new Exception( + $e = new Exception( 'JSON serialization of config data failed. ' . 'This usually means the config data is not valid UTF-8.' ); + MWExceptionHandler::logException( $e ); + $js = Xml::encodeJsCall( 'mw.log.error', [ $e->__toString() ] ); } return $js; } diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php index 3ceb915d27..57392b97f8 100644 --- a/includes/resourceloader/ResourceLoaderContext.php +++ b/includes/resourceloader/ResourceLoaderContext.php @@ -74,7 +74,7 @@ class ResourceLoaderContext implements MessageLocalizer { $this->user = $request->getRawVal( 'user' ); $this->debug = $request->getFuzzyBool( 'debug', - $resourceLoader->getConfig()->get( 'ResourceLoaderDebug' ) + $this->getConfig()->get( 'ResourceLoaderDebug' ) ); $this->only = $request->getRawVal( 'only', null ); $this->version = $request->getRawVal( 'version', null ); @@ -89,7 +89,7 @@ class ResourceLoaderContext implements MessageLocalizer { $skinnames = Skin::getSkinNames(); // If no skin is specified, or we don't recognize the skin, use the default skin if ( !$this->skin || !isset( $skinnames[$this->skin] ) ) { - $this->skin = $resourceLoader->getConfig()->get( 'DefaultSkin' ); + $this->skin = $this->getConfig()->get( 'DefaultSkin' ); } } @@ -149,6 +149,13 @@ class ResourceLoaderContext implements MessageLocalizer { return $this->resourceLoader; } + /** + * @return Config + */ + public function getConfig() { + return $this->getResourceLoader()->getConfig(); + } + /** * @return WebRequest */ @@ -181,7 +188,7 @@ class ResourceLoaderContext implements MessageLocalizer { $lang = $this->getRequest()->getRawVal( 'lang', '' ); // Stricter version of RequestContext::sanitizeLangCode() if ( !Language::isValidBuiltInCode( $lang ) ) { - $lang = $this->getResourceLoader()->getConfig()->get( 'LanguageCode' ); + $lang = $this->getConfig()->get( 'LanguageCode' ); } $this->language = $lang; } diff --git a/includes/resourceloader/ResourceLoaderImage.php b/includes/resourceloader/ResourceLoaderImage.php index d9c369dd02..ef116283fc 100644 --- a/includes/resourceloader/ResourceLoaderImage.php +++ b/includes/resourceloader/ResourceLoaderImage.php @@ -292,7 +292,7 @@ class ResourceLoaderImage { * @return string New SVG file data */ protected function variantize( $variantConf, ResourceLoaderContext $context ) { - $dom = new DomDocument; + $dom = new DOMDocument; $dom->loadXML( file_get_contents( $this->getPath( $context ) ) ); $root = $dom->documentElement; $wrapper = $dom->createElement( 'g' ); @@ -315,7 +315,7 @@ class ResourceLoaderImage { * @return string Massaged SVG image data */ protected function massageSvgPathdata( $svg ) { - $dom = new DomDocument; + $dom = new DOMDocument; $dom->loadXML( $svg ); foreach ( $dom->getElementsByTagName( 'path' ) as $node ) { $pathData = $node->getAttribute( 'd' ); diff --git a/includes/revisiondelete/RevisionDeleteUser.php b/includes/revisiondelete/RevisionDeleteUser.php index a8bf81498f..19fe0c3255 100644 --- a/includes/revisiondelete/RevisionDeleteUser.php +++ b/includes/revisiondelete/RevisionDeleteUser.php @@ -120,14 +120,17 @@ class RevisionDeleteUser { $actorId = $dbw->selectField( 'actor', 'actor_id', [ 'actor_name' => $name ], __METHOD__ ); if ( $actorId ) { # Hide name from live edits - $subquery = $dbw->selectSQLText( + $ids = $dbw->selectFieldValues( 'revision_actor_temp', 'revactor_rev', [ 'revactor_actor' => $actorId ], __METHOD__ ); - $dbw->update( - 'revision', - [ self::buildSetBitDeletedField( 'rev_deleted', $op, $delUser, $dbw ) ], - [ "rev_id IN ($subquery)" ], - __METHOD__ ); + if ( $ids ) { + $dbw->update( + 'revision', + [ self::buildSetBitDeletedField( 'rev_deleted', $op, $delUser, $dbw ) ], + [ 'rev_id' => $ids ], + __METHOD__ + ); + } # Hide name from deleted edits $dbw->update( diff --git a/includes/search/SearchDatabase.php b/includes/search/SearchDatabase.php index 93f8d2301f..54bfd28f68 100644 --- a/includes/search/SearchDatabase.php +++ b/includes/search/SearchDatabase.php @@ -30,7 +30,7 @@ use Wikimedia\Rdbms\IDatabase; */ abstract class SearchDatabase extends SearchEngine { /** - * @var IDatabase Slave database for reading from for results + * @var IDatabase Replica database from which to read results */ protected $db; diff --git a/includes/search/SearchEngine.php b/includes/search/SearchEngine.php index 2941f0a96a..a0100cab4a 100644 --- a/includes/search/SearchEngine.php +++ b/includes/search/SearchEngine.php @@ -287,6 +287,7 @@ abstract class SearchEngine { * @return SearchResultSet */ public static function getNearMatchResultSet( $searchterm ) { + wfDeprecated( __METHOD__, '1.27' ); return static::defaultNearMatcher()->getNearMatchResultSet( $searchterm ); } diff --git a/includes/search/SearchHighlighter.php b/includes/search/SearchHighlighter.php index 5dfc4dfe40..469502fd43 100644 --- a/includes/search/SearchHighlighter.php +++ b/includes/search/SearchHighlighter.php @@ -519,7 +519,7 @@ class SearchHighlighter { $extract = ""; $contLang = MediaWikiServices::getInstance()->getContentLanguage(); foreach ( $lines as $line ) { - if ( 0 == $contextlines ) { + if ( $contextlines == 0 ) { break; } ++$lineno; diff --git a/includes/search/SearchSuggestionSet.php b/includes/search/SearchSuggestionSet.php index cb1f831711..f1da246134 100644 --- a/includes/search/SearchSuggestionSet.php +++ b/includes/search/SearchSuggestionSet.php @@ -80,7 +80,7 @@ class SearchSuggestionSet { /** * Call array_map on the suggestions array - * @param callback $callback + * @param callable $callback * @return array */ public function map( $callback ) { @@ -89,7 +89,7 @@ class SearchSuggestionSet { /** * Filter the suggestions array - * @param callback $callback Callable accepting single SearchSuggestion + * @param callable $callback Callable accepting single SearchSuggestion * instance returning bool false to remove the item. * @return int The number of suggestions removed */ diff --git a/includes/shell/Command.php b/includes/shell/Command.php index 1154e05886..2afc548d08 100644 --- a/includes/shell/Command.php +++ b/includes/shell/Command.php @@ -433,8 +433,9 @@ class Command { // 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( '' ); + \MediaWiki\suppressWarnings(); + trigger_error( '' ); + \MediaWiki\restoreWarnings(); restore_error_handler(); $readPipes = array_filter( $pipes, function ( $fd ) use ( $desc ) { diff --git a/includes/site/SiteSQLStore.php b/includes/site/SiteSQLStore.php index 91d9ef7c5d..b106d11794 100644 --- a/includes/site/SiteSQLStore.php +++ b/includes/site/SiteSQLStore.php @@ -44,6 +44,7 @@ class SiteSQLStore { * @return SiteStore */ public static function newInstance( $sitesTable = null, BagOStuff $cache = null ) { + wfDeprecated( __METHOD__, '1.27' ); if ( $sitesTable !== null ) { throw new InvalidArgumentException( __METHOD__ . ': $sitesTable parameter is unused and must be null' diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php index 1b91c89974..809d411095 100644 --- a/includes/skins/Skin.php +++ b/includes/skins/Skin.php @@ -196,14 +196,15 @@ abstract class Skin extends ContextSource { 'core' => [ 'site', 'mediawiki.page.startup', - 'mediawiki.user', ], // modules that enhance the content in some way 'content' => [ 'mediawiki.page.ready', ], // modules relating to search functionality - 'search' => [], + 'search' => [ + 'mediawiki.searchSuggest', + ], // modules relating to functionality relating to watching an article 'watch' => [], // modules which relate to the current users preferences @@ -242,8 +243,6 @@ abstract class Skin extends ContextSource { $modules['watch'][] = 'mediawiki.page.watch.ajax'; } - $modules['search'][] = 'mediawiki.searchSuggest'; - if ( $user->getBoolOption( 'editsectiononrightclick' ) ) { $modules['user'][] = 'mediawiki.action.view.rightClickEdit'; } @@ -438,6 +437,7 @@ abstract class Skin extends ContextSource { */ function getPageClasses( $title ) { $numeric = 'ns-' . $title->getNamespace(); + $user = $this->getUser(); if ( $title->isSpecialPage() ) { $type = 'ns-special'; @@ -449,10 +449,16 @@ abstract class Skin extends ContextSource { } else { $type .= ' mw-invalidspecialpage'; } - } elseif ( $title->isTalkPage() ) { - $type = 'ns-talk'; } else { - $type = 'ns-subject'; + if ( $title->isTalkPage() ) { + $type = 'ns-talk'; + } else { + $type = 'ns-subject'; + } + // T208315: add HTML class when the user can edit the page + if ( $title->quickUserCan( 'edit', $user ) ) { + $type .= ' mw-editable'; + } } $name = Sanitizer::escapeClass( 'page-' . $title->getPrefixedText() ); diff --git a/includes/sparql/SparqlClient.php b/includes/sparql/SparqlClient.php index 778a3b3248..20fc9b3255 100644 --- a/includes/sparql/SparqlClient.php +++ b/includes/sparql/SparqlClient.php @@ -170,7 +170,7 @@ class SparqlClient { $status = $request->execute(); if ( !$status->isOK() ) { - throw new SparqlException( "HTTP error: {$status->getWikiText()}" ); + throw new SparqlException( 'HTTP error: ' . $status->getWikiText( false, false, 'en' ) ); } $result = $request->getContent(); \Wikimedia\suppressWarnings(); diff --git a/includes/specialpage/AuthManagerSpecialPage.php b/includes/specialpage/AuthManagerSpecialPage.php index 1476e85eb8..c56cc65db4 100644 --- a/includes/specialpage/AuthManagerSpecialPage.php +++ b/includes/specialpage/AuthManagerSpecialPage.php @@ -254,7 +254,7 @@ abstract class AuthManagerSpecialPage extends SpecialPage { $allReqs = AuthManager::singleton()->getAuthenticationRequests( $this->authAction, $this->getUser() ); - $this->authRequests = array_filter( $allReqs, function ( $req ) use ( $subPage ) { + $this->authRequests = array_filter( $allReqs, function ( $req ) { return !in_array( get_class( $req ), $this->getRequestBlacklist(), true ); } ); } @@ -456,7 +456,7 @@ abstract class AuthManagerSpecialPage extends SpecialPage { // Let's just submit the data to AuthManager directly instead. LoggerFactory::getInstance( 'authentication' ) ->warning( 'Validation error on return', [ 'data' => $form->mFieldData, - 'status' => $status->getWikiText() ] ); + 'status' => $status->getWikiText( false, false, 'en' ) ] ); $status = $this->handleFormSubmit( $form->mFieldData ); } } diff --git a/includes/specials/SpecialBlock.php b/includes/specials/SpecialBlock.php index 6b9b9d4b83..c9ce2b0837 100644 --- a/includes/specials/SpecialBlock.php +++ b/includes/specials/SpecialBlock.php @@ -66,7 +66,6 @@ class SpecialBlock extends FormSpecialPage { */ protected function checkExecutePermissions( User $user ) { parent::checkExecutePermissions( $user ); - # T17810: blocked admins should have limited access here $status = self::checkUnblockSelf( $this->target, $user ); if ( $status !== true ) { @@ -74,6 +73,15 @@ class SpecialBlock extends FormSpecialPage { } } + /** + * We allow certain special cases where user is blocked + * + * @return bool + */ + public function requiresUnblock() { + return false; + } + /** * Handle some magic here * @@ -171,6 +179,10 @@ class SpecialBlock extends FormSpecialPage { 'exists' => true, 'max' => 10, 'cssclass' => 'mw-block-page-restrictions', + 'showMissing' => false, + 'input' => [ + 'autocomplete' => false + ], ]; } @@ -382,6 +394,7 @@ class SpecialBlock extends FormSpecialPage { * @return string */ protected function preText() { + $this->getOutput()->addModuleStyles( 'mediawiki.widgets.TagMultiselectWidget.styles' ); $this->getOutput()->addModules( [ 'mediawiki.special.block' ] ); $blockCIDRLimit = $this->getConfig()->get( 'BlockCIDRLimit' ); @@ -869,6 +882,7 @@ class SpecialBlock extends FormSpecialPage { } $status = $currentBlock->update(); + // TODO handle failure $logaction = 'reblock'; @@ -881,6 +895,8 @@ class SpecialBlock extends FormSpecialPage { if ( (bool)$currentBlock->mHideName ) { $data['HideUser'] = true; } + + $block = $currentBlock; } } else { $logaction = 'block'; @@ -925,9 +941,8 @@ class SpecialBlock extends FormSpecialPage { $logEntry->setComment( $data['Reason'][0] ); $logEntry->setPerformer( $performer ); $logEntry->setParameters( $logParams ); - # Relate log ID to block IDs (T27763) - $blockIds = array_merge( [ $status['id'] ], $status['autoIds'] ); - $logEntry->setRelations( [ 'ipb_id' => $blockIds ] ); + # Relate log ID to block ID (T27763) + $logEntry->setRelations( [ 'ipb_id' => $block->getId() ] ); $logId = $logEntry->insert(); if ( !empty( $data['Tags'] ) ) { @@ -1017,19 +1032,24 @@ class SpecialBlock extends FormSpecialPage { * T17810: blocked admins should not be able to block/unblock * others, and probably shouldn't be able to unblock themselves * either. - * @param User|int|string $user + * + * Exception: Users can block the user who blocked them, to reduce + * advantage of a malicious account blocking all admins (T150826) + * + * @param User|int|string|null $target Target to block or unblock; could be a User object, + * or a user ID or username, or null when the target is not known yet (e.g. when + * displaying Special:Block) * @param User $performer User doing the request * @return bool|string True or error message key */ - public static function checkUnblockSelf( $user, User $performer ) { - if ( is_int( $user ) ) { - $user = User::newFromId( $user ); - } elseif ( is_string( $user ) ) { - $user = User::newFromName( $user ); + public static function checkUnblockSelf( $target, User $performer ) { + if ( is_int( $target ) ) { + $target = User::newFromId( $target ); + } elseif ( is_string( $target ) ) { + $target = User::newFromName( $target ); } - if ( $performer->isBlocked() ) { - if ( $user instanceof User && $user->getId() == $performer->getId() ) { + if ( $target instanceof User && $target->getId() == $performer->getId() ) { # User is trying to unblock themselves if ( $performer->isAllowed( 'unblockself' ) ) { return true; @@ -1039,6 +1059,19 @@ class SpecialBlock extends FormSpecialPage { } else { return 'ipbnounblockself'; } + } elseif ( + $target instanceof User && + $performer->getBlock() instanceof Block && + $performer->getBlock()->getBy() && + $performer->getBlock()->getBy() === $target->getId() + ) { + // Allow users to block the user that blocked them. + // This is to prevent a situation where a malicious user + // blocks all other users. This way, the non-malicious + // user can block the malicious user back, resulting + // in a stalemate. + return true; + } else { # User is trying to block/unblock someone else return 'ipbblocked'; diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index 43c7d2921d..5b939efbee 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -607,7 +607,7 @@ class SpecialContributions extends IncludableSpecialPage { '' ) . "\u{00A0}" . Html::namespaceSelector( - [ 'selected' => $this->opts['namespace'], 'all' => '' ], + [ 'selected' => $this->opts['namespace'], 'all' => '', 'in-user-lang' => true ], [ 'name' => 'namespace', 'id' => 'namespace', diff --git a/includes/specials/SpecialEditWatchlist.php b/includes/specials/SpecialEditWatchlist.php index 16cebe0fae..70b4207638 100644 --- a/includes/specials/SpecialEditWatchlist.php +++ b/includes/specials/SpecialEditWatchlist.php @@ -159,7 +159,6 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) ); } elseif ( $this->toc !== false ) { $out->prependHTML( $this->toc ); - $out->addModules( 'mediawiki.toc' ); $out->addModuleStyles( 'mediawiki.toc.styles' ); } } diff --git a/includes/specials/SpecialExpandTemplates.php b/includes/specials/SpecialExpandTemplates.php index 4587d40609..619665bdb4 100644 --- a/includes/specials/SpecialExpandTemplates.php +++ b/includes/specials/SpecialExpandTemplates.php @@ -117,6 +117,8 @@ class SpecialExpandTemplates extends SpecialPage { $config = $this->getConfig(); if ( MWTidy::isEnabled() && $options->getTidy() ) { $tmp = MWTidy::tidy( $tmp ); + } else { + wfDeprecated( 'disabling tidy', '1.33' ); } $out->addHTML( $tmp ); diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php index 513e7a96b8..ef61ac5b33 100644 --- a/includes/specials/SpecialExport.php +++ b/includes/specials/SpecialExport.php @@ -378,7 +378,6 @@ class SpecialExport extends SpecialPage { } /* Ok, let's get to it... */ - $lb = false; $db = wfGetDB( DB_REPLICA ); $exporter = new WikiExporter( $db, $history ); @@ -406,10 +405,6 @@ class SpecialExport extends SpecialPage { } $exporter->closeStream(); - - if ( $lb ) { - $lb->closeAll(); - } } /** diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php index 839a9bc487..a985bcc638 100644 --- a/includes/specials/SpecialImport.php +++ b/includes/specials/SpecialImport.php @@ -280,6 +280,7 @@ class SpecialImport extends SpecialPage { 'selected' => ( $isSameSourceAsBefore ? $this->namespace : ( $defaultNamespace || '' ) ), + 'in-user-lang' => true, ], [ 'name' => "namespace", // mw-import-namespace-interwiki, mw-import-namespace-upload diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php index ef9525438c..d08fe5cf4e 100644 --- a/includes/specials/SpecialLinkSearch.php +++ b/includes/specials/SpecialLinkSearch.php @@ -69,7 +69,7 @@ class LinkSearchPage extends QueryPage { } } - $target2 = $target; + $target2 = Parser::normalizeLinkUrl( $target ); // Get protocol, default is http:// $protocol = 'http://'; $bits = wfParseUrl( $target ); @@ -128,7 +128,7 @@ class LinkSearchPage extends QueryPage { if ( $target != '' ) { $this->setParams( [ - 'query' => Parser::normalizeLinkUrl( $target2 ), + 'query' => $target2, 'namespace' => $namespace, 'protocol' => $protocol ] ); parent::execute( $par ); @@ -146,37 +146,6 @@ class LinkSearchPage extends QueryPage { return false; } - /** - * Return an appropriately formatted LIKE query and the clause - * - * @param string $query Search pattern to search for - * @param string $prot Protocol, e.g. 'http://' - * - * @return array - */ - static function mungeQuery( $query, $prot ) { - $field = 'el_index'; - $dbr = wfGetDB( DB_REPLICA ); - - if ( $query === '*' && $prot !== '' ) { - // Allow queries like 'ftp://*' to find all ftp links - $rv = [ $prot, $dbr->anyString() ]; - } else { - $rv = LinkFilter::makeLikeArray( $query, $prot ); - } - - if ( $rv === false ) { - // LinkFilter doesn't handle wildcard in IP, so we'll have to munge here. - $pattern = '/^(:?[0-9]{1,3}\.)+\*\s*$|^(:?[0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]*\*\s*$/'; - if ( preg_match( $pattern, $query ) ) { - $rv = [ $prot . rtrim( $query, " \t*" ), $dbr->anyString() ]; - $field = 'el_to'; - } - } - - return [ $rv, $field ]; - } - function linkParameters() { $params = []; $params['target'] = $this->mProt . $this->mQuery; @@ -189,16 +158,29 @@ class LinkSearchPage extends QueryPage { public function getQueryInfo() { $dbr = wfGetDB( DB_REPLICA ); - // strip everything past first wildcard, so that - // index-based-only lookup would be done - list( $this->mungedQuery, $clause ) = self::mungeQuery( $this->mQuery, $this->mProt ); + + if ( $this->mQuery === '*' && $this->mProt !== '' ) { + $this->mungedQuery = [ + 'el_index_60' . $dbr->buildLike( $this->mProt, $dbr->anyString() ), + ]; + } else { + $this->mungedQuery = LinkFilter::getQueryConditions( $this->mQuery, [ + 'protocol' => $this->mProt, + 'oneWildcard' => true, + 'db' => $dbr + ] ); + } if ( $this->mungedQuery === false ) { // Invalid query; return no results return [ 'tables' => 'page', 'fields' => 'page_id', 'conds' => '0=1' ]; } - $stripped = LinkFilter::keepOneWildcard( $this->mungedQuery ); - $like = $dbr->buildLike( $stripped ); + $orderBy = []; + if ( !isset( $this->mungedQuery['el_index_60'] ) ) { + $orderBy[] = 'el_index_60'; + } + $orderBy[] = 'el_id'; + $retval = [ 'tables' => [ 'page', 'externallinks' ], 'fields' => [ @@ -207,11 +189,13 @@ class LinkSearchPage extends QueryPage { 'value' => 'el_index', 'url' => 'el_to' ], - 'conds' => [ - 'page_id = el_from', - "$clause $like" - ], - 'options' => [ 'USE INDEX' => $clause ] + 'conds' => array_merge( + [ + 'page_id = el_from', + ], + $this->mungedQuery + ), + 'options' => [ 'ORDER BY' => $orderBy ] ]; if ( $this->mNs !== null && !$this->getConfig()->get( 'MiserMode' ) ) { @@ -248,9 +232,7 @@ class LinkSearchPage extends QueryPage { /** * Override to squash the ORDER BY. - * We do a truncated index search, so the optimizer won't trust - * it as good enough for optimizing sort. The implicit ordering - * from the scan will usually do well enough for our needs. + * Not much point in descending order here. * @return array */ function getOrderFields() { diff --git a/includes/specials/SpecialLockdb.php b/includes/specials/SpecialLockdb.php index a2c25305ca..c7c45b5510 100644 --- a/includes/specials/SpecialLockdb.php +++ b/includes/specials/SpecialLockdb.php @@ -84,7 +84,7 @@ class SpecialLockdb extends FormSpecialPage { $fp = fopen( $this->getConfig()->get( 'ReadOnlyFile' ), 'w' ); Wikimedia\restoreWarnings(); - if ( false === $fp ) { + if ( $fp === false ) { # This used to show a file not found error, but the likeliest reason for fopen() # to fail at this point is insufficient permission to write to the file...good old # is_writable() is plain wrong in some cases, it seems... diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php index 5cbad8a33b..599ab31135 100644 --- a/includes/specials/SpecialMovepage.php +++ b/includes/specials/SpecialMovepage.php @@ -122,7 +122,7 @@ class MovePageForm extends UnlistedSpecialPage { $this->moveOverShared = $request->getBool( 'wpMoveOverSharedFile' ); $this->watch = $request->getCheck( 'wpWatch' ) && $user->isLoggedIn(); - if ( 'submit' == $request->getVal( 'action' ) && $request->wasPosted() + if ( $request->getVal( 'action' ) == 'submit' && $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) { $this->doSubmit(); @@ -137,8 +137,9 @@ class MovePageForm extends UnlistedSpecialPage { * @param array $err Error messages. Each item is an error message. * It may either be a string message name or array message name and * parameters, like the second argument to OutputPage::wrapWikiMsg(). + * @param bool $isPermError Whether the error message is about user permissions. */ - function showForm( $err ) { + function showForm( $err, $isPermError = false ) { $this->getSkin()->setRelevantTitle( $this->oldTitle ); $out = $this->getOutput(); @@ -235,9 +236,13 @@ class MovePageForm extends UnlistedSpecialPage { } if ( count( $err ) ) { - $action_desc = $this->msg( 'action-move' )->plain(); - $errMsgHtml = $this->msg( 'permissionserrorstext-withaction', - count( $err ), $action_desc )->parseAsBlock(); + if ( $isPermError ) { + $action_desc = $this->msg( 'action-move' )->plain(); + $errMsgHtml = $this->msg( 'permissionserrorstext-withaction', + count( $err ), $action_desc )->parseAsBlock(); + } else { + $errMsgHtml = $this->msg( 'cannotmove', count( $err ) )->parseAsBlock(); + } if ( count( $err ) == 1 ) { $errMsg = $err[0]; @@ -542,7 +547,7 @@ class MovePageForm extends UnlistedSpecialPage { $permErrors = $nt->getUserPermissionsErrors( 'delete', $user ); if ( count( $permErrors ) ) { # Only show the first error - $this->showForm( $permErrors ); + $this->showForm( $permErrors, true ); return; } @@ -596,7 +601,7 @@ class MovePageForm extends UnlistedSpecialPage { $permStatus = $mp->checkPermissions( $user, $this->reason ); if ( !$permStatus->isOK() ) { - $this->showForm( $permStatus->getErrorsArray() ); + $this->showForm( $permStatus->getErrorsArray(), true ); return; } diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index 5ba7c88e01..8051b0b4e4 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -76,19 +76,19 @@ class SpecialNewpages extends IncludableSpecialPage { protected function parseParams( $par ) { $bits = preg_split( '/\s*,\s*/', trim( $par ) ); foreach ( $bits as $bit ) { - if ( 'shownav' == $bit ) { + if ( $bit === 'shownav' ) { $this->showNavigation = true; } - if ( 'hideliu' === $bit ) { + if ( $bit === 'hideliu' ) { $this->opts->setValue( 'hideliu', true ); } - if ( 'hidepatrolled' == $bit ) { + if ( $bit === 'hidepatrolled' ) { $this->opts->setValue( 'hidepatrolled', true ); } - if ( 'hidebots' == $bit ) { + if ( $bit === 'hidebots' ) { $this->opts->setValue( 'hidebots', true ); } - if ( 'showredirs' == $bit ) { + if ( $bit === 'showredirs' ) { $this->opts->setValue( 'hideredirs', false ); } if ( is_numeric( $bit ) ) { diff --git a/includes/specials/SpecialPasswordReset.php b/includes/specials/SpecialPasswordReset.php index 7ea9ba019c..3524d79e65 100644 --- a/includes/specials/SpecialPasswordReset.php +++ b/includes/specials/SpecialPasswordReset.php @@ -79,7 +79,8 @@ class SpecialPasswordReset extends FormSpecialPage { $a = []; if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) { $a['Username'] = [ - 'type' => 'user', + 'type' => 'text', + 'default' => $this->getRequest()->getSession()->suggestLoginUsername(), 'label-message' => 'passwordreset-username', ]; diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php index 04be22b92d..cc7ed5598b 100644 --- a/includes/specials/SpecialPreferences.php +++ b/includes/specials/SpecialPreferences.php @@ -53,7 +53,10 @@ class SpecialPreferences extends SpecialPage { } $out->addModules( 'mediawiki.special.preferences.ooui' ); - $out->addModuleStyles( 'mediawiki.special.preferences.styles.ooui' ); + $out->addModuleStyles( [ + 'mediawiki.special.preferences.styles.ooui', + 'mediawiki.widgets.TagMultiselectWidget.styles', + ] ); $out->addModuleStyles( 'oojs-ui-widgets.styles' ); $session = $this->getRequest()->getSession(); diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index 01ad657f3a..60e797e0c3 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -678,7 +678,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { */ protected function namespaceFilterForm( FormOptions $opts ) { $nsSelect = Html::namespaceSelector( - [ 'selected' => $opts['namespace'], 'all' => '' ], + [ 'selected' => $opts['namespace'], 'all' => '', 'in-user-lang' => true ], [ 'name' => 'namespace', 'id' => 'namespace' ] ); $nsLabel = Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ); diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php index 7661f28694..50f3710143 100644 --- a/includes/specials/SpecialRevisiondelete.php +++ b/includes/specials/SpecialRevisiondelete.php @@ -205,7 +205,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { # Either submit or create our form if ( $this->mIsAllowed && $this->submitClicked ) { - $this->submit( $request ); + $this->submit(); } else { $this->showForm(); } diff --git a/includes/specials/SpecialUnblock.php b/includes/specials/SpecialUnblock.php index b2d5a1633b..632415cc1d 100644 --- a/includes/specials/SpecialUnblock.php +++ b/includes/specials/SpecialUnblock.php @@ -248,6 +248,7 @@ class SpecialUnblock extends SpecialPage { if ( isset( $data['Tags'] ) ) { $logEntry->setTags( $data['Tags'] ); } + $logEntry->setRelations( [ 'ipb_id' => $block->getId() ] ); $logId = $logEntry->insert(); $logEntry->publish( $logId ); diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index be0d3faad6..cd754ca03d 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -23,6 +23,7 @@ use MediaWiki\MediaWikiServices; use MediaWiki\Revision\RevisionRecord; +use MediaWiki\Storage\NameTableAccessException; use Wikimedia\Rdbms\IResultWrapper; /** @@ -555,15 +556,9 @@ class SpecialUndelete extends SpecialPage { $diffContext->setWikiPage( WikiPage::factory( $currentRev->getTitle() ) ); $diffEngine = $currentRev->getContentHandler()->createDifferenceEngine( $diffContext ); + $diffEngine->setRevisions( $previousRev->getRevisionRecord(), $currentRev->getRevisionRecord() ); $diffEngine->showDiffStyle(); - - $formattedDiff = $diffEngine->generateContentDiffBody( - $previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ), - $currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ) - ); - - $formattedDiff = $diffEngine->addHeader( - $formattedDiff, + $formattedDiff = $diffEngine->getDiff( $this->diffHeader( $previousRev, 'o' ), $this->diffHeader( $currentRev, 'n' ) ); @@ -602,12 +597,22 @@ class SpecialUndelete extends SpecialPage { $minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : ''; - $tags = wfGetDB( DB_REPLICA )->selectField( - 'tag_summary', - 'ts_tags', - [ 'ts_rev_id' => $rev->getId() ], + $tagIds = wfGetDB( DB_REPLICA )->selectFieldValues( + 'change_tag', + 'ct_tag_id', + [ 'ct_rev_id' => $rev->getId() ], __METHOD__ ); + $tags = []; + $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore(); + foreach ( $tagIds as $tagId ) { + try { + $tags[] = $changeTagDefStore->getName( (int)$tagId ); + } catch ( NameTableAccessException $exception ) { + continue; + } + } + $tags = implode( ',', $tags ); $tagSummary = ChangeTags::formatSummaryRow( $tags, 'deleteddiff', $this->getContext() ); // FIXME This is reimplementing DifferenceEngine#getRevisionHeader diff --git a/includes/specials/SpecialUnusedcategories.php b/includes/specials/SpecialUnusedcategories.php index 1469742a4b..2577a100cf 100644 --- a/includes/specials/SpecialUnusedcategories.php +++ b/includes/specials/SpecialUnusedcategories.php @@ -39,7 +39,7 @@ class UnusedCategoriesPage extends QueryPage { public function getQueryInfo() { return [ - 'tables' => [ 'page', 'categorylinks' ], + 'tables' => [ 'page', 'categorylinks', 'page_props' ], 'fields' => [ 'namespace' => 'page_namespace', 'title' => 'page_title', @@ -48,9 +48,16 @@ class UnusedCategoriesPage extends QueryPage { 'conds' => [ 'cl_from IS NULL', 'page_namespace' => NS_CATEGORY, - 'page_is_redirect' => 0 + 'page_is_redirect' => 0, + 'pp_page IS NULL' ], - 'join_conds' => [ 'categorylinks' => [ 'LEFT JOIN', 'cl_to = page_title' ] ] + 'join_conds' => [ + 'categorylinks' => [ 'LEFT JOIN', 'cl_to = page_title' ], + 'page_props' => [ 'LEFT JOIN', [ + 'page_id = pp_page', + 'pp_propname' => 'expectunusedcategory' + ] ] + ] ]; } diff --git a/includes/specials/SpecialUnusedimages.php b/includes/specials/SpecialUnusedimages.php index 9fcbf15f78..bcb3b85e36 100644 --- a/includes/specials/SpecialUnusedimages.php +++ b/includes/specials/SpecialUnusedimages.php @@ -76,6 +76,11 @@ class UnusedimagesPage extends ImageQueryPage { } function getPageHeader() { + if ( $this->getConfig()->get( 'CountCategorizedImagesAsUsed' ) ) { + return $this->msg( + 'unusedimagestext-categorizedimgisused' + )->parseAsBlock(); + } return $this->msg( 'unusedimagestext' )->parseAsBlock(); } diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index feb449c847..0fc6e13c41 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -155,7 +155,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { 'activeValue' => false, 'default' => $this->getUser()->getBoolOption( 'extendwatchlist' ), 'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, - &$fields, &$conds, &$query_options, &$join_conds ) { + &$fields, &$conds, &$query_options, &$join_conds ) { $nonRevisionTypes = [ RC_LOG ]; Hooks::run( 'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] ); if ( $nonRevisionTypes ) { @@ -211,7 +211,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { ], 'default' => ChangesListStringOptionsFilterGroup::NONE, 'queryCallable' => function ( $specialPageClassName, $context, $dbr, - &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues ) { + &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues ) { if ( $selectedValues === [ 'seen' ] ) { $conds[] = $dbr->makeList( [ 'wl_notificationtimestamp IS NULL', @@ -652,7 +652,8 @@ class SpecialWatchlist extends ChangesListSpecialPage { [ 'selected' => $opts['namespace'], 'all' => '', - 'label' => $this->msg( 'namespace' )->text() + 'label' => $this->msg( 'namespace' )->text(), + 'in-user-lang' => true, ], [ 'name' => 'namespace', 'id' => 'namespace', diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php index 1d3ffd795a..766e190250 100644 --- a/includes/specials/SpecialWhatlinkshere.php +++ b/includes/specials/SpecialWhatlinkshere.php @@ -200,7 +200,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { && ( $hidetrans || !$tlRes->numRows() ) && ( $hideimages || !$ilRes->numRows() ) ) { - if ( 0 == $level ) { + if ( $level == 0 ) { if ( !$this->including() ) { $out->addHTML( $this->whatlinkshereForm() ); @@ -461,11 +461,11 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { $changed = $this->opts->getChangedValues(); unset( $changed['target'] ); // Already in the request title - if ( 0 != $prevId ) { + if ( $prevId != 0 ) { $overrides = [ 'from' => $this->opts->getValue( 'back' ) ]; $prev = $this->makeSelfLink( $prev, array_merge( $changed, $overrides ) ); } - if ( 0 != $nextId ) { + if ( $nextId != 0 ) { $overrides = [ 'from' => $nextId, 'back' => $prevId ]; $next = $this->makeSelfLink( $next, array_merge( $changed, $overrides ) ); } @@ -515,7 +515,8 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { [ 'selected' => $namespace, 'all' => '', - 'label' => $this->msg( 'namespace' )->text() + 'label' => $this->msg( 'namespace' )->text(), + 'in-user-lang' => true, ], [ 'name' => 'namespace', 'id' => 'namespace', diff --git a/includes/specials/forms/PreferencesFormOOUI.php b/includes/specials/forms/PreferencesFormOOUI.php index bf4d9af80e..81abf1c8e1 100644 --- a/includes/specials/forms/PreferencesFormOOUI.php +++ b/includes/specials/forms/PreferencesFormOOUI.php @@ -227,8 +227,7 @@ class PreferencesFormOOUI extends OOUIHTMLForm { * @return string */ function getLegend( $key ) { - $aliasKey = ( $key === 'optoutwatchlist' || $key === 'optoutrc' ) ? 'opt-out' : $key; - $legend = parent::getLegend( $aliasKey ); + $legend = parent::getLegend( $key ); Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] ); return $legend; } diff --git a/includes/specials/pagers/ActiveUsersPager.php b/includes/specials/pagers/ActiveUsersPager.php index 552e92fb00..50f1d6b8b0 100644 --- a/includes/specials/pagers/ActiveUsersPager.php +++ b/includes/specials/pagers/ActiveUsersPager.php @@ -136,7 +136,7 @@ class ActiveUsersPager extends UsersPager { ]; } - function doBatchLookups() { + protected function doBatchLookups() { parent::doBatchLookups(); $uids = []; @@ -149,14 +149,17 @@ class ActiveUsersPager extends UsersPager { // is done in two queries to avoid huge quicksorts and to make COUNT(*) correct. $dbr = $this->getDatabase(); $res = $dbr->select( 'ipblocks', - [ 'ipb_user', 'MAX(ipb_deleted) AS block_status' ], + [ 'ipb_user', 'MAX(ipb_deleted) AS deleted, MAX(ipb_sitewide) AS sitewide' ], [ 'ipb_user' => $uids ], __METHOD__, [ 'GROUP BY' => [ 'ipb_user' ] ] ); $this->blockStatusByUid = []; foreach ( $res as $row ) { - $this->blockStatusByUid[$row->ipb_user] = $row->block_status; // 0 or 1 + $this->blockStatusByUid[$row->ipb_user] = [ + 'deleted' => $row->deleted, + 'sitewide' => $row->sitewide, + ]; } $this->mResult->seek( 0 ); } @@ -181,13 +184,20 @@ class ActiveUsersPager extends UsersPager { $item = $lang->specialList( $ulinks, $groups ); + // If there is a block, 'deleted' and 'sitewide' are both set on + // $this->blockStatusByUid[$row->user_id]. + $blocked = ''; $isBlocked = isset( $this->blockStatusByUid[$row->user_id] ); - if ( $isBlocked && $this->blockStatusByUid[$row->user_id] == 1 ) { - $item = "$item"; + if ( $isBlocked ) { + if ( $this->blockStatusByUid[$row->user_id]['deleted'] == 1 ) { + $item = "$item"; + } + if ( $this->blockStatusByUid[$row->user_id]['sitewide'] == 1 ) { + $blocked = ' ' . $this->msg( 'listusers-blocked', $userName )->escaped(); + } } $count = $this->msg( 'activeusers-count' )->numParams( $row->recentedits ) ->params( $userName )->numParams( $this->RCMaxAge )->escaped(); - $blocked = $isBlocked ? ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() : ''; return Html::rawElement( 'li', [], "{$item} [{$count}]{$blocked}" ); } diff --git a/includes/specials/pagers/AllMessagesTablePager.php b/includes/specials/pagers/AllMessagesTablePager.php index 6d5f64b7a2..bde7559f46 100644 --- a/includes/specials/pagers/AllMessagesTablePager.php +++ b/includes/specials/pagers/AllMessagesTablePager.php @@ -276,7 +276,7 @@ class AllMessagesTablePager extends TablePager { return $result; } - function getStartBody() { + protected function getStartBody() { $tableClass = $this->getTableClass(); return Xml::openElement( 'table', [ 'class' => "mw-datatable $tableClass", @@ -295,7 +295,11 @@ class AllMessagesTablePager extends TablePager {
- \n"; + \n"; + } + + function getEndBody() { + return Html::closeElement( 'table' ); } function formatValue( $field, $value ) { @@ -345,54 +349,49 @@ class AllMessagesTablePager extends TablePager { return ''; } + /** @return string HTML */ function formatRow( $row ) { // Do all the normal stuff $s = parent::formatRow( $row ); // But if there's a customised message, add that too. if ( $row->am_customised ) { - $s .= Xml::openElement( 'tr', $this->getRowAttrs( $row, true ) ); + $s .= Html::openElement( 'tr', $this->getRowAttrs( $row, true ) ); $formatted = strval( $this->formatValue( 'am_actual', $row->am_actual ) ); if ( $formatted === '' ) { $formatted = "\u{00A0}"; } - $s .= Xml::tags( 'td', $this->getCellAttrs( 'am_actual', $row->am_actual ), $formatted ) - . "\n"; + $s .= Html::element( 'td', $this->getCellAttrs( 'am_actual', $row->am_actual ), $formatted ) + . Html::closeElement( 'tr' ); } - return $s; + return Html::rawElement( 'tbody', [], $s ); } - function getRowAttrs( $row, $isSecond = false ) { - $arr = []; - - if ( $row->am_customised ) { - $arr['class'] = 'allmessages-customised'; - } - - if ( !$isSecond ) { - $arr['id'] = Sanitizer::escapeIdForAttribute( - 'msg_' . $this->getLanguage()->lcfirst( $row->am_title ) - ); - } - - return $arr; + function getRowAttrs( $row ) { + return []; } + /** @return array HTML attributes */ function getCellAttrs( $field, $value ) { - if ( $this->mCurrentRow->am_customised && $field === 'am_title' ) { - return [ 'rowspan' => '2', 'class' => $field ]; - } elseif ( $field === 'am_title' ) { - return [ 'class' => $field ]; + $attr = []; + if ( $field === 'am_title' ) { + if ( $this->mCurrentRow->am_customised ) { + $attr += [ 'rowspan' => '2' ]; + } } else { - return [ + $attr += [ 'lang' => $this->lang->getHtmlCode(), 'dir' => $this->lang->getDir(), - 'class' => $field ]; + if ( $this->mCurrentRow->am_customised ) { + // CSS class: am_default, am_actual + $attr += [ 'class' => $field ]; + } } + return $attr; } // This is not actually used, as getStartBody is overridden above diff --git a/includes/specials/pagers/BlockListPager.php b/includes/specials/pagers/BlockListPager.php index 74ec6b55d3..e8a7d2d69d 100644 --- a/includes/specials/pagers/BlockListPager.php +++ b/includes/specials/pagers/BlockListPager.php @@ -224,7 +224,7 @@ class BlockListPager extends TablePager { 'ul', [], implode( '', array_map( function ( $prop ) { - return HTML::rawElement( + return Html::rawElement( 'li', [], $prop @@ -264,7 +264,7 @@ class BlockListPager extends TablePager { continue; } - $items[] = HTML::rawElement( + $items[] = Html::rawElement( 'li', [], Linker::link( $restriction->getTitle() ) diff --git a/includes/specials/pagers/ContribsPager.php b/includes/specials/pagers/ContribsPager.php index 18da235fde..ca13f3de85 100644 --- a/includes/specials/pagers/ContribsPager.php +++ b/includes/specials/pagers/ContribsPager.php @@ -30,27 +30,79 @@ use Wikimedia\Rdbms\IDatabase; class ContribsPager extends RangeChronologicalPager { - public $mDefaultDirection = IndexPager::DIR_DESCENDING; - public $messages; - public $target; - public $namespace = ''; - public $mDb; - public $preventClickjacking = false; + /** + * @var string[] Local cache for escaped messages + */ + private $messages; + + /** + * @var string User name, or a string describing an IP address range + */ + private $target; + + /** + * @var string Set to "newbie" to list contributions from the most recent 1% registered users. + * $this->target is ignored then. Defaults to "users". + */ + private $contribs; + + /** + * @var string|int A single namespace number, or an empty string for all namespaces + */ + private $namespace = ''; + + /** + * @var string|false Name of tag to filter, or false to ignore tags + */ + private $tagFilter; + + /** + * @var bool Set to true to invert the namespace selection + */ + private $nsInvert; + + /** + * @var bool Set to true to show both the subject and talk namespace, no matter which got + * selected + */ + private $associated; + + /** + * @var bool Set to true to show only deleted revisions + */ + private $deletedOnly; + + /** + * @var bool Set to true to show only latest (a.k.a. current) revisions + */ + private $topOnly; + + /** + * @var bool Set to true to show only new pages + */ + private $newOnly; + + /** + * @var bool Set to true to hide edits marked as minor by the user + */ + private $hideMinor; + + private $preventClickjacking = false; /** @var IDatabase */ - public $mDbSecondary; + private $mDbSecondary; /** * @var array */ - protected $mParentLens; + private $mParentLens; /** * @var TemplateParser */ - protected $templateParser; + private $templateParser; - function __construct( IContextSource $context, array $options ) { + public function __construct( IContextSource $context, array $options ) { parent::__construct( $context ); $msgs = [ @@ -87,10 +139,6 @@ class ContribsPager extends RangeChronologicalPager { } $this->getDateRangeCond( $startTimestamp, $endTimestamp ); - // This property on IndexPager is set by $this->getIndexField() in parent::__construct(). - // We need to reassign it here so that it is used when the actual query is ran. - $this->mIndexField = $this->getIndexField(); - // Most of this code will use the 'contributions' group DB, which can map to replica DBs // with extra user based indexes or partioning by user. The additional metadata // queries should use a regular replica DB since the lookup pattern is not all by user. @@ -212,6 +260,22 @@ class ContribsPager extends RangeChronologicalPager { $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) : null; if ( $ipRangeConds ) { $queryInfo['tables'][] = 'ip_changes'; + /** + * These aliases make `ORDER BY rev_timestamp, rev_id` from {@see getIndexField} and + * {@see getExtraSortFields} use the replicated `ipc_rev_timestamp` and `ipc_rev_id` + * columns from the `ip_changes` table, for more efficient queries. + * @see https://phabricator.wikimedia.org/T200259#4832318 + */ + $queryInfo['fields'] = array_merge( + [ + 'rev_timestamp' => 'ipc_rev_timestamp', + 'rev_id' => 'ipc_rev_id', + ], + array_diff( $queryInfo['fields'], [ + 'rev_timestamp', + 'rev_id', + ] ) + ); $queryInfo['join_conds']['ip_changes'] = [ 'LEFT JOIN', [ 'ipc_rev_id = rev_id' ] ]; @@ -350,20 +414,22 @@ class ContribsPager extends RangeChronologicalPager { } /** - * Override of getIndexField() in IndexPager. - * For IP ranges, it's faster to use the replicated ipc_rev_timestamp - * on the `ip_changes` table than the rev_timestamp on the `revision` table. - * @return string Name of field + * @return string */ public function getIndexField() { - if ( $this->isQueryableRange( $this->target ) ) { - return 'ipc_rev_timestamp'; - } else { - return 'rev_timestamp'; - } + // Note this is run via parent::__construct() *before* $this->target is set! + return 'rev_timestamp'; } - function doBatchLookups() { + /** + * @return string[] + */ + protected function getExtraSortFields() { + // Note this is run via parent::__construct() *before* $this->target is set! + return [ 'rev_id' ]; + } + + protected function doBatchLookups() { # Do a link batch query $this->mResult->seek( 0 ); $parentRevIds = []; @@ -399,14 +465,14 @@ class ContribsPager extends RangeChronologicalPager { /** * @return string */ - function getStartBody() { + protected function getStartBody() { return "
    \n"; } /** * @return string */ - function getEndBody() { + protected function getEndBody() { return "
\n"; } @@ -415,9 +481,10 @@ class ContribsPager extends RangeChronologicalPager { * id then null is returned. * * @param object $row + * @param Title|null $title * @return Revision|null */ - public function tryToCreateValidRevision( $row ) { + public function tryToCreateValidRevision( $row, $title = null ) { /* * There may be more than just revision rows. To make sure that we'll only be processing * revisions here, let's _try_ to build a revision out of our row (without displaying @@ -427,7 +494,7 @@ class ContribsPager extends RangeChronologicalPager { */ Wikimedia\suppressWarnings(); try { - $rev = new Revision( $row ); + $rev = new Revision( $row, 0, $title ); $validRevision = (bool)$rev->getId(); } catch ( Exception $e ) { $validRevision = false; @@ -455,11 +522,16 @@ class ContribsPager extends RangeChronologicalPager { $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); - $rev = $this->tryToCreateValidRevision( $row ); + $page = null; + // Create a title for the revision if possible + // Rows from the hook may not include title information + if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) { + $page = Title::newFromRow( $row ); + } + $rev = $this->tryToCreateValidRevision( $row, $page ); if ( $rev ) { $attribs['data-mw-revid'] = $rev->getId(); - $page = Title::newFromRow( $row ); $link = $linkRenderer->makeLink( $page, $page->getPrefixedText(), @@ -525,7 +597,7 @@ class ContribsPager extends RangeChronologicalPager { } $lang = $this->getLanguage(); - $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true ); + $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true, false ); $date = $lang->userTimeAndDate( $row->rev_timestamp, $user ); if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { $d = $linkRenderer->makeKnownLink( @@ -568,10 +640,17 @@ class ContribsPager extends RangeChronologicalPager { $del .= ' '; } - $diffHistLinks = Html::rawElement( 'ul', - [ 'class' => 'mw-changeslist-link-list' ], - Html::rawElement( 'li', [], $difftext ) . - Html::rawElement( 'li', [], $histlink ) + // While it might be tempting to use a list here + // this would result in clutter and slows down navigating the content + // in assistive technology. + // See https://phabricator.wikimedia.org/T205581#4734812 + $diffHistLinks = Html::rawElement( 'span', + [ 'class' => 'mw-changeslist-links' ], + // The spans are needed to ensure the dividing '|' elements are not + // themselves styled as links. + Html::rawElement( 'span', [], $difftext ) . + ' ' . // Space needed for separating two words. + Html::rawElement( 'span', [], $histlink ) ); # Tags, if any. diff --git a/includes/specials/pagers/DeletedContribsPager.php b/includes/specials/pagers/DeletedContribsPager.php index ee7eb3e4d8..c69a18321b 100644 --- a/includes/specials/pagers/DeletedContribsPager.php +++ b/includes/specials/pagers/DeletedContribsPager.php @@ -139,11 +139,11 @@ class DeletedContribsPager extends IndexPager { return 'ar_timestamp'; } - function getStartBody() { + protected function getStartBody() { return "
    \n"; } - function getEndBody() { + protected function getEndBody() { return "
\n"; } diff --git a/includes/specials/pagers/ImageListPager.php b/includes/specials/pagers/ImageListPager.php index 889ec1af7c..a10c32f98b 100644 --- a/includes/specials/pagers/ImageListPager.php +++ b/includes/specials/pagers/ImageListPager.php @@ -134,7 +134,16 @@ 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 ), + // oldimage doesn't have an index on oi_user, while image does. Set $useId accordingly. + $prefix === 'img' + ); + $conds[] = $actorWhere['conds']; } if ( $this->mSearch !== '' ) { @@ -413,7 +422,7 @@ class ImageListPager extends TablePager { } } - function doBatchLookups() { + protected function doBatchLookups() { $userIds = []; $this->mResult->seek( 0 ); foreach ( $this->mResult as $row ) { diff --git a/includes/specials/pagers/MergeHistoryPager.php b/includes/specials/pagers/MergeHistoryPager.php index 6a8f7da74e..3dbec6a2be 100644 --- a/includes/specials/pagers/MergeHistoryPager.php +++ b/includes/specials/pagers/MergeHistoryPager.php @@ -48,7 +48,7 @@ class MergeHistoryPager extends ReverseChronologicalPager { parent::__construct( $form->getContext() ); } - function getStartBody() { + protected function getStartBody() { # Do a link batch query $this->mResult->seek( 0 ); $batch = new LinkBatch(); diff --git a/includes/specials/pagers/NewFilesPager.php b/includes/specials/pagers/NewFilesPager.php index 6b7e4b8b31..6e16e7962e 100644 --- a/includes/specials/pagers/NewFilesPager.php +++ b/includes/specials/pagers/NewFilesPager.php @@ -168,7 +168,7 @@ class NewFilesPager extends RangeChronologicalPager { return 'img_timestamp'; } - function getStartBody() { + protected function getStartBody() { if ( !$this->gallery ) { // Note that null for mode is taken to mean use default. $mode = $this->getRequest()->getVal( 'gallerymode', null ); @@ -183,7 +183,7 @@ class NewFilesPager extends RangeChronologicalPager { return ''; } - function getEndBody() { + protected function getEndBody() { return $this->gallery->toHTML(); } diff --git a/includes/specials/pagers/NewPagesPager.php b/includes/specials/pagers/NewPagesPager.php index f16a5cb615..0c95b7e434 100644 --- a/includes/specials/pagers/NewPagesPager.php +++ b/includes/specials/pagers/NewPagesPager.php @@ -59,8 +59,6 @@ class NewPagesPager extends ReverseChronologicalPager { } } - $rcIndexes = []; - if ( $namespace !== false ) { if ( $this->opts->getValue( 'invert' ) ) { $conds[] = 'rc_namespace != ' . $this->mDb->addQuotes( $namespace ); @@ -105,17 +103,11 @@ class NewPagesPager extends ReverseChronologicalPager { Hooks::run( 'SpecialNewpagesConditions', [ &$pager, $this->opts, &$conds, &$tables, &$fields, &$join_conds ] ); - $options = []; - - if ( $rcIndexes ) { - $options = [ 'USE INDEX' => [ 'recentchanges' => $rcIndexes ] ]; - } - $info = [ 'tables' => $tables, 'fields' => $fields, 'conds' => $conds, - 'options' => $options, + 'options' => [], 'join_conds' => $join_conds ]; @@ -140,7 +132,7 @@ class NewPagesPager extends ReverseChronologicalPager { return $this->mForm->formatRow( $row ); } - function getStartBody() { + protected function getStartBody() { # Do a batch existence check on pages $linkBatch = new LinkBatch(); foreach ( $this->mResult as $row ) { @@ -153,7 +145,7 @@ class NewPagesPager extends ReverseChronologicalPager { return '
    '; } - function getEndBody() { + protected function getEndBody() { return '
'; } } diff --git a/includes/specials/pagers/ProtectedTitlesPager.php b/includes/specials/pagers/ProtectedTitlesPager.php index ed437be5ab..d17e3c617b 100644 --- a/includes/specials/pagers/ProtectedTitlesPager.php +++ b/includes/specials/pagers/ProtectedTitlesPager.php @@ -37,7 +37,7 @@ class ProtectedTitlesPager extends AlphabeticPager { parent::__construct( $form->getContext() ); } - function getStartBody() { + protected function getStartBody() { # Do a link batch query $this->mResult->seek( 0 ); $lb = new LinkBatch; diff --git a/includes/specials/pagers/UsersPager.php b/includes/specials/pagers/UsersPager.php index bc24d26a16..391c3132e2 100644 --- a/includes/specials/pagers/UsersPager.php +++ b/includes/specials/pagers/UsersPager.php @@ -142,7 +142,8 @@ class UsersPager extends AlphabeticPager { 'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)', 'edits' => 'MAX(user_editcount)', 'creation' => 'MIN(user_registration)', - 'ipb_deleted' => 'MAX(ipb_deleted)' // block/hide status + 'ipb_deleted' => 'MAX(ipb_deleted)', // block/hide status + 'ipb_sitewide' => 'MAX(ipb_sitewide)' ], 'options' => $options, 'join_conds' => [ @@ -214,7 +215,8 @@ class UsersPager extends AlphabeticPager { $created = $this->msg( 'usercreated', $d, $t, $row->user_name )->escaped(); $created = ' ' . $this->msg( 'parentheses' )->rawParams( $created )->escaped(); } - $blocked = !is_null( $row->ipb_deleted ) ? + + $blocked = !is_null( $row->ipb_deleted ) && $row->ipb_sitewide === '1' ? ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() : ''; @@ -223,7 +225,7 @@ class UsersPager extends AlphabeticPager { return Html::rawElement( 'li', [], "{$item}{$edits}{$created}{$blocked}" ); } - function doBatchLookups() { + protected function doBatchLookups() { $batch = new LinkBatch(); $userIds = []; # Give some pointers to make user links diff --git a/includes/tidy/RaggettBase.php b/includes/tidy/RaggettBase.php deleted file mode 100644 index 878099ff7d..0000000000 --- a/includes/tidy/RaggettBase.php +++ /dev/null @@ -1,60 +0,0 @@ -getWrapped( $text ); - - $retVal = null; - $correctedtext = $this->cleanWrapped( $wrappedtext, false, $retVal ); - - if ( $retVal < 0 ) { - wfDebug( "Possible tidy configuration error!\n" ); - return $text . "\n\n"; - } elseif ( is_null( $correctedtext ) ) { - wfDebug( "Tidy error detected!\n" ); - return $text . "\n\n"; - } - - $correctedtext = $wrapper->postprocess( $correctedtext ); // restore any hidden tokens - - return $correctedtext; - } - - public function validate( $text, &$errorStr ) { - $retval = 0; - $errorStr = $this->cleanWrapped( $text, true, $retval ); - return ( $retval < 0 && $errorStr == '' ) || $retval == 0; - } - - /** - * Perform a clean/repair operation - * @param string $text HTML to check - * @param bool $stderr Whether to read result from STDERR rather than STDOUT - * @param int|null &$retval Exit code (-1 on internal error) - * @return null|string - * @throws MWException - */ - abstract protected function cleanWrapped( $text, $stderr = false, &$retval = null ); -} diff --git a/includes/tidy/RaggettExternal.php b/includes/tidy/RaggettExternal.php deleted file mode 100644 index 0b485c7cc6..0000000000 --- a/includes/tidy/RaggettExternal.php +++ /dev/null @@ -1,76 +0,0 @@ - [ 'pipe', 'r' ], - 1 => [ 'file', wfGetNull(), 'a' ], - 2 => [ 'pipe', 'w' ] - ]; - } else { - $descriptorspec = [ - 0 => [ 'pipe', 'r' ], - 1 => [ 'pipe', 'w' ], - 2 => [ 'file', wfGetNull(), 'a' ] - ]; - } - - $readpipe = $stderr ? 2 : 1; - $pipes = []; - - $process = proc_open( - "{$this->config['tidyBin']} -config {$this->config['tidyConfigFile']} " . - $this->config['tidyCommandLine'] . $opts, $descriptorspec, $pipes ); - - // NOTE: At least on linux, the process will be created even if tidy is not installed. - // This means that missing tidy will be treated as a validation failure. - - if ( is_resource( $process ) ) { - // Theoretically, this style of communication could cause a deadlock - // here. If the stdout buffer fills up, then writes to stdin could - // block. This doesn't appear to happen with tidy, because tidy only - // writes to stdout after it's finished reading from stdin. Search - // for tidyParseStdin and tidySaveStdout in console/tidy.c - fwrite( $pipes[0], $text ); - fclose( $pipes[0] ); - while ( !feof( $pipes[$readpipe] ) ) { - $cleansource .= fgets( $pipes[$readpipe], 1024 ); - } - fclose( $pipes[$readpipe] ); - $retval = proc_close( $process ); - } else { - wfWarn( "Unable to start external tidy process" ); - $retval = -1; - } - - if ( !$stderr && $cleansource == '' && $text != '' ) { - // Some kind of error happened, so we couldn't get the corrected text. - // Just give up; we'll use the source text and append a warning. - $cleansource = null; - } - - return $cleansource; - } - - public function supportsValidate() { - return true; - } -} diff --git a/includes/tidy/RaggettInternalHHVM.php b/includes/tidy/RaggettInternalHHVM.php deleted file mode 100644 index 1681dc45e4..0000000000 --- a/includes/tidy/RaggettInternalHHVM.php +++ /dev/null @@ -1,32 +0,0 @@ -config['tidyConfigFile'], 'utf8' ); - if ( $cleansource === false ) { - $cleansource = null; - $retval = -1; - } else { - $retval = 0; - } - - return $cleansource; - } -} diff --git a/includes/tidy/RaggettInternalPHP.php b/includes/tidy/RaggettInternalPHP.php deleted file mode 100644 index c1050cc222..0000000000 --- a/includes/tidy/RaggettInternalPHP.php +++ /dev/null @@ -1,55 +0,0 @@ -parseString( $text, $this->config['tidyConfigFile'], 'utf8' ); - - if ( $stderr ) { - $retval = $tidy->getStatus(); - return $tidy->errorBuffer; - } - - $tidy->cleanRepair(); - $retval = $tidy->getStatus(); - if ( $retval == 2 ) { - // 2 is magic number for fatal error - // https://secure.php.net/manual/en/tidy.getstatus.php - $cleansource = null; - } else { - $cleansource = tidy_get_output( $tidy ); - if ( !empty( $this->config['debugComment'] ) && $retval > 0 ) { - $cleansource .= "', '-->', $tidy->errorBuffer ) . - "\n-->"; - } - } - - return $cleansource; - } - - public function supportsValidate() { - return true; - } -} diff --git a/includes/tidy/RaggettWrapper.php b/includes/tidy/RaggettWrapper.php deleted file mode 100644 index 855282d342..0000000000 --- a/includes/tidy/RaggettWrapper.php +++ /dev/null @@ -1,101 +0,0 @@ -mTokens = []; - $this->mMarkerIndex = 0; - - // Replace elements with placeholders - $wrappedtext = preg_replace_callback( ParserOutput::EDITSECTION_REGEX, - [ $this, 'replaceCallback' ], $text ); - // ...and markers - $wrappedtext = preg_replace_callback( '/\<\\/?mw:toc\>/', - [ $this, 'replaceCallback' ], $wrappedtext ); - // ... and tags - $wrappedtext = preg_replace_callback( '/\/s', - [ $this, 'replaceCallback' ], $wrappedtext ); - // Modify inline Microdata and elements so they say and so - // we can trick Tidy into not stripping them out by including them in tidy's new-empty-tags config - $wrappedtext = preg_replace( '!<(link|meta)([^>]*?)(/{0,1}>)!', ' tags, but those aren't empty. - $wrappedtext = preg_replace_callback( '!]*)>(.*?)!s', function ( $m ) { - return '' - . $this->replaceCallback( [ $m[2] ] ) - . ''; - }, $wrappedtext ); - - // Preserve empty li elements (T49673) by abusing Tidy's datafld hack - // The whitespace class is as in TY_(InitMap) - $wrappedtext = preg_replace( "!
  • ([ \r\n\t\f]*)
  • !", - '
  • \1
  • ', $wrappedtext ); - - // Wrap the whole thing in a doctype and body for Tidy. - $wrappedtext = '' . - 'test' . $wrappedtext . ''; - - return $wrappedtext; - } - - /** - * @param array $m - * @return string - */ - private function replaceCallback( array $m ) { - $marker = Parser::MARKER_PREFIX . "-item-{$this->mMarkerIndex}" . Parser::MARKER_SUFFIX; - $this->mMarkerIndex++; - $this->mTokens[$marker] = $m[0]; - return $marker; - } - - /** - * @param string $text - * @return string - */ - public function postprocess( $text ) { - // Revert back to <{link,meta,style}> - $text = preg_replace( '!]*?)(/{0,1}>)!', '<$1$2$3', $text ); - $text = preg_replace( '!<(/?)html-(style)([^>]*)>!', '<$1$2$3>', $text ); - - // Remove datafld - $text = str_replace( '
  • mTokens ); - - return $text; - } - -} diff --git a/includes/tidy/tidy.conf b/includes/tidy/tidy.conf deleted file mode 100644 index d4a3199367..0000000000 --- a/includes/tidy/tidy.conf +++ /dev/null @@ -1,24 +0,0 @@ -# html tidy (http://tidy.sf.net) configuration -# tidy - validate, correct, and pretty-print HTML files -# see: man 1 tidy, http://tidy.sourceforge.net/docs/quickref.html - -show-body-only: yes -force-output: yes -tidy-mark: no -wrap: 0 -wrap-attributes: no -literal-attributes: yes -output-xhtml: yes -numeric-entities: yes -enclose-text: yes -enclose-block-text: yes -quiet: yes -quote-nbsp: yes -fix-backslash: no -fix-uri: no -# Don't strip html5 elements we support -# html-{meta,link} is a hack we use to prevent Tidy from stripping and used in the body for Microdata -new-empty-tags: html-meta, html-link, wbr, source, track -new-inline-tags: video, audio, bdi, data, time, mark -# html-style is a hack we use to prevent pre-HTML5 versions of Tidy from stripping
  • " . $this->msg( 'allmessagescurrent' )->escaped() . "